streamby-core 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +92 -4
- package/dist/adapters/database/connectionManager.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/mcp/tools.js +25 -11
- package/dist/mcp/workflowNodes.js +9 -3
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/connection.js +5 -2
- package/dist/middleware/routes/dbConnection.js +29 -5
- package/dist/middleware/routes/document.js +11 -6
- package/dist/middleware/routes/export.d.ts +28 -0
- package/dist/middleware/routes/export.js +8 -4
- package/dist/middleware/routes/pipeline.js +5 -2
- package/dist/middleware/routes/project.js +14 -12
- package/dist/middleware/routes/projectResources.d.ts +8 -0
- package/dist/middleware/routes/projectResources.js +51 -0
- package/dist/middleware/routes/rights.js +72 -52
- package/dist/middleware/routes/storage.js +35 -2
- package/dist/middleware/routes/storageConnection.js +38 -15
- package/dist/middleware/routes/userIntegration.js +42 -1
- package/dist/middleware/routes/workflow.js +120 -37
- package/dist/models/Model.js +12 -1
- package/dist/services/apiTokenPermissions.js +2 -0
- package/dist/services/creditsSheets.d.ts +39 -0
- package/dist/services/creditsSheets.js +129 -0
- package/dist/services/dependents.d.ts +11 -0
- package/dist/services/dependents.js +81 -0
- package/dist/services/production.d.ts +1 -1
- package/dist/services/production.js +2 -2
- package/dist/services/projectIntegration.d.ts +14 -0
- package/dist/services/projectIntegration.js +32 -0
- package/dist/services/rights.d.ts +44 -0
- package/dist/services/rights.js +123 -0
- package/dist/services/workflowResources.d.ts +90 -0
- package/dist/services/workflowResources.js +288 -0
- package/dist/types/index.d.ts +6 -1
- package/package.json +1 -1
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.workflowRouter = workflowRouter;
|
|
4
4
|
const express_1 = require("express");
|
|
5
|
-
const crypto_1 = require("crypto");
|
|
6
5
|
const manager_1 = require("../../models/manager");
|
|
7
6
|
const auth_1 = require("../../utils/auth");
|
|
8
|
-
const
|
|
7
|
+
const projectIntegration_1 = require("../../services/projectIntegration");
|
|
8
|
+
const connection_1 = require("../../services/connection");
|
|
9
|
+
const export_1 = require("../../services/export");
|
|
10
|
+
const export_2 = require("./export");
|
|
11
|
+
const workflowResources_1 = require("../../services/workflowResources");
|
|
12
|
+
const isSchema = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.nodes) && Array.isArray(value.edges);
|
|
13
|
+
// The integration kind a node's pending connection is of, by the node's type, and its field.
|
|
14
|
+
const INTEGRATION_KIND = {
|
|
15
|
+
dataSourceNode: { kind: 'database', field: 'connectionId' },
|
|
16
|
+
ingestNode: { kind: 'storage', field: 'storageConnectionId' },
|
|
17
|
+
integrationConnectionNode: { kind: 'service', field: 'integrationConnectionId' },
|
|
18
|
+
};
|
|
9
19
|
function workflowRouter(config) {
|
|
10
20
|
const router = (0, express_1.Router)();
|
|
11
21
|
const Project = (0, manager_1.getModel)('projects');
|
|
12
22
|
// A project has exactly one Workflow (the central canvas), stored as the single
|
|
13
|
-
// `project.workflow` object.
|
|
14
|
-
const resolveWorkflow = (project) => project.workflow ?? null;
|
|
23
|
+
// `project.workflow` object. It defines the project: each resource is one of its nodes.
|
|
15
24
|
router.get('/projects/:id/workflow', async (req, res) => {
|
|
16
25
|
try {
|
|
17
26
|
const auth = await config.authProvider(req);
|
|
@@ -22,55 +31,129 @@ function workflowRouter(config) {
|
|
|
22
31
|
return res.status(404).json({ message: 'Project not found' });
|
|
23
32
|
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
24
33
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
25
|
-
|
|
26
|
-
if (!workflow)
|
|
27
|
-
return res.status(404).json({ message: 'No workflow found for this project' });
|
|
28
|
-
res.json({ data: workflow, message: 'Workflow fetched successfully' });
|
|
34
|
+
res.json({ data: await (0, workflowResources_1.readWorkflow)(project), message: 'Workflow fetched successfully' });
|
|
29
35
|
}
|
|
30
36
|
catch (err) {
|
|
31
37
|
res.status(500).json({ message: 'Failed to fetch workflow', details: err.message });
|
|
32
38
|
}
|
|
33
39
|
});
|
|
40
|
+
// Saves the workflow, and makes the project what it defines: a node set to an integration the
|
|
41
|
+
// project doesn't have connects it, a new API node's connection is made, and a resource whose
|
|
42
|
+
// nodes are all gone is removed. Answers the workflow and the project's lists as they are then,
|
|
43
|
+
// and what couldn't be done (its node kept as it was sent).
|
|
34
44
|
router.patch('/projects/:id/workflow', async (req, res) => {
|
|
35
45
|
try {
|
|
36
46
|
const auth = await config.authProvider(req);
|
|
37
47
|
if (!auth?.userId)
|
|
38
48
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
39
49
|
const projectId = req.params.id;
|
|
40
|
-
|
|
50
|
+
let project = await Project.findOne({ _id: projectId });
|
|
41
51
|
if (!project || !(0, auth_1.canEditProject)(project, auth.userId)) {
|
|
42
52
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
43
53
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
id: base.id,
|
|
59
|
-
projectId,
|
|
60
|
-
name: name ?? base.name,
|
|
61
|
-
description: description ?? base.description,
|
|
62
|
-
status: status ?? base.status,
|
|
63
|
-
nodeSchema: nodeSchema !== undefined ? nodeSchema : base.nodeSchema,
|
|
64
|
-
updatedAt: now,
|
|
65
|
-
};
|
|
66
|
-
if (project.dbType === 'sql') {
|
|
67
|
-
const SqlProject = (0, manager_1.getModel)('projects', 'sql');
|
|
68
|
-
await SqlProject.update({ _id: projectId }, { workflow: updated });
|
|
54
|
+
const { name, description, status, nodeSchema, baseUpdatedAt } = req.body;
|
|
55
|
+
if (nodeSchema !== undefined) {
|
|
56
|
+
// A schema saved removes what it lacks: one that isn't a schema is never read as empty.
|
|
57
|
+
if (!isSchema(nodeSchema))
|
|
58
|
+
return res.status(400).json({ message: 'nodeSchema is { nodes: [...], edges: [...] }' });
|
|
59
|
+
// Made on the version it was read at, always: a change saved since (a resource added from
|
|
60
|
+
// its section, its node with it) would be undone, its resource removed.
|
|
61
|
+
if (typeof baseUpdatedAt !== 'string' || Number.isNaN(new Date(baseUpdatedAt).getTime())) {
|
|
62
|
+
return res.status(400).json({ message: "baseUpdatedAt is required: the workflow's updatedAt as it was read" });
|
|
63
|
+
}
|
|
64
|
+
const current = project.workflow?.updatedAt ? new Date(project.workflow.updatedAt).toISOString() : null;
|
|
65
|
+
if (current !== new Date(baseUpdatedAt).toISOString()) {
|
|
66
|
+
return res.status(409).json({ message: 'The workflow changed since it was opened: open it again to see the change.', data: await (0, workflowResources_1.readWorkflow)(project) });
|
|
67
|
+
}
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const changes = {
|
|
70
|
+
...(name !== undefined && { name }), ...(description !== undefined && { description }), ...(status !== undefined && { status }),
|
|
71
|
+
};
|
|
72
|
+
const errors = [];
|
|
73
|
+
if (nodeSchema !== undefined) {
|
|
74
|
+
const before = project.workflow?.resourceNodesAt
|
|
75
|
+
? { nodes: project.workflow?.nodeSchema?.nodes ?? [], edges: project.workflow?.nodeSchema?.edges ?? [] }
|
|
76
|
+
: (0, workflowResources_1.migrateSchema)(project, { nodes: project.workflow?.nodeSchema?.nodes ?? [], edges: project.workflow?.nodeSchema?.edges ?? [] });
|
|
77
|
+
const given = { nodes: nodeSchema.nodes, edges: nodeSchema.edges };
|
|
78
|
+
// One not brought yet to define the project (see migrateSchema) is, before it's compared:
|
|
79
|
+
// its resources without a node aren't removed for it.
|
|
80
|
+
const sent = project.workflow?.resourceNodesAt ? given : (0, workflowResources_1.migrateSchema)(project, given);
|
|
81
|
+
// What it would remove: a resource bound to a node before, to none now. An app connected
|
|
82
|
+
// with OAuth deletes nothing (as its DELETEs are refused), nor through the workflow.
|
|
83
|
+
const was = (0, workflowResources_1.referencedIn)(before);
|
|
84
|
+
const removing = Object.keys(was).flatMap(kind => [...was[kind]].filter(id => !(0, workflowResources_1.referencedIn)(sent)[kind].has(id)).map(id => ({ kind, id })));
|
|
85
|
+
if (req.auth?.oauth && removing.length) {
|
|
86
|
+
const named = removing.map(({ kind, id }) => before.nodes.find((n) => n.data && Object.values(n.data).includes(id))?.data?.label ?? `${kind} ${id}`);
|
|
87
|
+
return res.status(403).json({ message: `An app connected with OAuth removes nothing: this would remove ${named.join(', ')}`, removing });
|
|
88
|
+
}
|
|
89
|
+
// What its nodes make: the integrations they were set to, the APIs, pipelines and exports
|
|
90
|
+
// drafted on them.
|
|
91
|
+
const connected = new Map();
|
|
92
|
+
const nodes = [];
|
|
93
|
+
for (const node of sent.nodes) {
|
|
94
|
+
let data = node?.data ?? {};
|
|
95
|
+
const pending = INTEGRATION_KIND[node?.type];
|
|
96
|
+
const integrationId = pending && (0, workflowResources_1.pendingOf)(data[pending.field]);
|
|
97
|
+
if (pending && integrationId) {
|
|
98
|
+
if (!connected.has(integrationId)) {
|
|
99
|
+
const result = await (0, projectIntegration_1.connectIntegration)(await Project.findOne({ _id: projectId }), integrationId, auth, config, { kind: pending.kind });
|
|
100
|
+
if ('error' in result)
|
|
101
|
+
errors.push(`${data.label ?? integrationId}: ${result.error}`);
|
|
102
|
+
else
|
|
103
|
+
connected.set(integrationId, result.connection.id);
|
|
104
|
+
}
|
|
105
|
+
if (connected.has(integrationId))
|
|
106
|
+
data = { ...data, [pending.field]: connected.get(integrationId) };
|
|
107
|
+
}
|
|
108
|
+
if (node.type === 'pipelineRefNode' && data.pipelineDraft && !data.pipelineId) {
|
|
109
|
+
try {
|
|
110
|
+
const pipeline = await (0, workflowResources_1.createPipelineOf)(projectId, String(data.pipelineDraft.name ?? data.label), auth.userId);
|
|
111
|
+
const { pipelineDraft: _draft, ...rest } = data;
|
|
112
|
+
data = { ...rest, pipelineId: pipeline.id, label: pipeline.name };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
errors.push(`${data.pipelineDraft?.name ?? 'Pipeline'}: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (node.type === 'exportNode' && data.exportDraft && !data.exportId) {
|
|
119
|
+
try {
|
|
120
|
+
const exportName = String(data.exportDraft.name ?? data.label);
|
|
121
|
+
const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
|
|
122
|
+
const created = await (0, export_1.createExport)(config, projectId, '', exportName, mainDb?.type ?? 'nosql', 'json', false, ['*'], export_2.DEFAULT_NODE_SCHEMA, false, false, undefined);
|
|
123
|
+
const { exportDraft: _draft, ...rest } = data;
|
|
124
|
+
data = { ...rest, exportId: created.exportId, label: exportName };
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
errors.push(`${data.exportDraft?.name ?? 'Export'}: ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (node.type === 'connectionNode' && data.connectionDraft && !data.connectionId) {
|
|
131
|
+
try {
|
|
132
|
+
const { name: apiName, apiUrl, method, token, prefix, description: apiDescription } = data.connectionDraft;
|
|
133
|
+
const created = await (0, connection_1.addConnection)(config, projectId, { name: apiName, apiUrl, method: method ?? 'GET', token, prefix, description: apiDescription });
|
|
134
|
+
const { connectionDraft: _draft, ...rest } = data;
|
|
135
|
+
data = { ...rest, connectionId: created.id, label: created.name, subtitle: created.apiUrl };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
errors.push(`${data.connectionDraft?.name ?? 'API'}: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
nodes.push(data === node?.data ? node : { ...node, data });
|
|
142
|
+
}
|
|
143
|
+
project = await Project.findOne({ _id: projectId });
|
|
144
|
+
for (const { kind, id } of removing) {
|
|
145
|
+
try {
|
|
146
|
+
await (0, workflowResources_1.removeResourceOf)(project, kind, id, config);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
errors.push(`${id}: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
changes.nodeSchema = (0, workflowResources_1.oneNodePerResource)({ nodes, edges: sent.edges });
|
|
153
|
+
changes.resourceNodesAt = project.workflow?.resourceNodesAt ?? new Date();
|
|
72
154
|
}
|
|
73
|
-
|
|
155
|
+
const updated = await (0, workflowResources_1.writeWorkflow)(await Project.findOne({ _id: projectId }), changes);
|
|
156
|
+
res.json({ data: updated, project: await (0, workflowResources_1.listsOf)(projectId), errors, message: 'Workflow updated successfully' });
|
|
74
157
|
}
|
|
75
158
|
catch (err) {
|
|
76
159
|
res.status(500).json({ message: 'Failed to update workflow', details: err.message });
|
package/dist/models/Model.js
CHANGED
|
@@ -41,7 +41,14 @@ class Model {
|
|
|
41
41
|
delete processedFilter._id;
|
|
42
42
|
}
|
|
43
43
|
let sqlResults = [];
|
|
44
|
-
if (this.tableName === 'projects' && processedFilter.
|
|
44
|
+
if (this.tableName === 'projects' && typeof processedFilter.connectedIntegration === 'string') {
|
|
45
|
+
// The projects one of whose connections uses an integration (its id in any of their lists).
|
|
46
|
+
const schema = this.schema || 'streamby';
|
|
47
|
+
const has = JSON.stringify([{ integrationId: processedFilter.connectedIntegration }]);
|
|
48
|
+
const result = await connection.query(`SELECT * FROM "${schema}"."projects" WHERE "dbConnections" @> $1::jsonb OR "storageConnections" @> $1::jsonb OR "integrationConnections" @> $1::jsonb`, [has]);
|
|
49
|
+
sqlResults = result.rows;
|
|
50
|
+
}
|
|
51
|
+
else if (this.tableName === 'projects' && processedFilter.members && processedFilter.members.$elemMatch) {
|
|
45
52
|
// Handle members filter for SQL projects
|
|
46
53
|
const userId = processedFilter.members.$elemMatch.userId;
|
|
47
54
|
const schema = this.schema || 'streamby'; // Default to 'streamby' if schema not provided
|
|
@@ -82,6 +89,10 @@ class Model {
|
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
else if (dbType === 'nosql') {
|
|
92
|
+
if (this.tableName === 'projects' && typeof processedFilter.connectedIntegration === 'string') {
|
|
93
|
+
const integrationId = processedFilter.connectedIntegration;
|
|
94
|
+
processedFilter = { $or: ['dbConnections', 'storageConnections', 'integrationConnections'].map(list => ({ [`${list}.integrationId`]: integrationId })) };
|
|
95
|
+
}
|
|
85
96
|
const nosqlResults = await nosql_1.nosqlAdapter.find(connection, this.tableName, processedFilter);
|
|
86
97
|
for (const item of nosqlResults) {
|
|
87
98
|
allResults.push(this.transformResult(item));
|
|
@@ -75,7 +75,9 @@ const RULES = [
|
|
|
75
75
|
// Rights.
|
|
76
76
|
rule('read', `${P}/assets/[^/]+/rights`, 'rights:read'),
|
|
77
77
|
rule(['PUT'], `${P}/assets/[^/]+/rights`, 'rights:write'),
|
|
78
|
+
rule('read', `${P}/credits-sheets(/[^/]+)?`, 'rights:read'),
|
|
78
79
|
// Connections (API, database, storage, distribution, workers): the project's settings.
|
|
80
|
+
rule('read', `${P}/dependents/[^/]+/[^/]+`, 'project:read'),
|
|
79
81
|
rule('read', `${P}/(connections|get-connection|distribution-connections|workers)(/.+)?`, 'project:read'),
|
|
80
82
|
rule('write', `${P}/(connections|distribution-connections|workers)(/.+)?`, 'project:update'),
|
|
81
83
|
rule('delete', `${P}/(connections|distribution-connections|workers)/.+`, 'project:update'),
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ProjectDocument } from '../types';
|
|
2
|
+
import { LicenseType } from './rights';
|
|
3
|
+
export declare const CREDITS_SHEET_NODE = "creditsSheetNode";
|
|
4
|
+
export interface SheetSources {
|
|
5
|
+
assetIds: string[];
|
|
6
|
+
folderIds: string[];
|
|
7
|
+
}
|
|
8
|
+
declare const REQUIRED: readonly ["creatorName", "sourceUrl", "licenseType"];
|
|
9
|
+
type Required = typeof REQUIRED[number];
|
|
10
|
+
export type RowSource = 'asset' | 'folder' | 'ingest';
|
|
11
|
+
export interface SheetRow {
|
|
12
|
+
assetId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
category: string | null;
|
|
15
|
+
sourceUrl: string | null;
|
|
16
|
+
creatorName: string | null;
|
|
17
|
+
creatorUrl: string | null;
|
|
18
|
+
licenseType: LicenseType | null;
|
|
19
|
+
comments: string | null;
|
|
20
|
+
missing: Required[];
|
|
21
|
+
from: RowSource[];
|
|
22
|
+
}
|
|
23
|
+
export type SheetStatus = 'complete' | 'incomplete';
|
|
24
|
+
export interface CreditsSheet {
|
|
25
|
+
document: ProjectDocument;
|
|
26
|
+
sources: SheetSources;
|
|
27
|
+
rows: SheetRow[];
|
|
28
|
+
status: SheetStatus;
|
|
29
|
+
}
|
|
30
|
+
/** A sheet's content as it's saved: its sources, as JSON. */
|
|
31
|
+
export declare function checkSheetContent(value: unknown): string | {
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function sheetSources(document: Pick<ProjectDocument, 'content'>): SheetSources;
|
|
35
|
+
/** The assets imported by the Ingest nodes connected (either way) to the sheet's node. */
|
|
36
|
+
export declare function ingestAssetsOf(workflow: any, documentId: string): string[];
|
|
37
|
+
/** A sheet as it reads now: its rows, from its sources and its assets' metadata, and its status. */
|
|
38
|
+
export declare function readSheet(project: any, document: ProjectDocument): Promise<CreditsSheet>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CREDITS_SHEET_NODE = void 0;
|
|
4
|
+
exports.checkSheetContent = checkSheetContent;
|
|
5
|
+
exports.sheetSources = sheetSources;
|
|
6
|
+
exports.ingestAssetsOf = ingestAssetsOf;
|
|
7
|
+
exports.readSheet = readSheet;
|
|
8
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
9
|
+
const rights_1 = require("./rights");
|
|
10
|
+
// A credits sheet: a project document (kind 'credits') that a Credits sheet node of the
|
|
11
|
+
// workflow made, listing the assets fed to it with their license metadata. It keeps only what
|
|
12
|
+
// feeds it, never a copy of the metadata: assets and Storage folders picked on it (its content),
|
|
13
|
+
// and the Ingest nodes connected to its node (the asset each imports). Its rows are made each
|
|
14
|
+
// time it's read, from each asset's metadata, so every sheet shows the same, current, values.
|
|
15
|
+
exports.CREDITS_SHEET_NODE = 'creditsSheetNode';
|
|
16
|
+
// What a sheet needs of each asset to credit it.
|
|
17
|
+
const REQUIRED = ['creatorName', 'sourceUrl', 'licenseType'];
|
|
18
|
+
const ids = (value) => Array.isArray(value) ? [...new Set(value.filter((v) => typeof v === 'string' && !!v.trim()))] : null;
|
|
19
|
+
/** A sheet's content as it's saved: its sources, as JSON. */
|
|
20
|
+
function checkSheetContent(value) {
|
|
21
|
+
if (value === undefined || value === '')
|
|
22
|
+
return JSON.stringify({ assetIds: [], folderIds: [] });
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { error: 'A credits sheet\'s content is its sources, as JSON: { "assetIds": [...], "folderIds": [...] }' };
|
|
29
|
+
}
|
|
30
|
+
const assetIds = ids(parsed?.assetIds ?? []);
|
|
31
|
+
const folderIds = ids(parsed?.folderIds ?? []);
|
|
32
|
+
if (!assetIds || !folderIds)
|
|
33
|
+
return { error: 'assetIds and folderIds are lists of ids' };
|
|
34
|
+
return JSON.stringify({ assetIds, folderIds });
|
|
35
|
+
}
|
|
36
|
+
function sheetSources(document) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(document.content || '{}');
|
|
39
|
+
return { assetIds: ids(parsed.assetIds) ?? [], folderIds: ids(parsed.folderIds) ?? [] };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { assetIds: [], folderIds: [] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The assets imported by the Ingest nodes connected (either way) to the sheet's node. */
|
|
46
|
+
function ingestAssetsOf(workflow, documentId) {
|
|
47
|
+
const nodes = Array.isArray(workflow?.nodeSchema?.nodes) ? workflow.nodeSchema.nodes : [];
|
|
48
|
+
const edges = Array.isArray(workflow?.nodeSchema?.edges) ? workflow.nodeSchema.edges : [];
|
|
49
|
+
const sheetNodes = new Set(nodes.filter(n => n?.type === exports.CREDITS_SHEET_NODE && n.data?.documentId === documentId).map(n => n.id));
|
|
50
|
+
const byId = new Map(nodes.map(n => [n?.id, n]));
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const edge of edges) {
|
|
53
|
+
const other = sheetNodes.has(edge?.target) ? edge.source : sheetNodes.has(edge?.source) ? edge.target : null;
|
|
54
|
+
const node = other != null ? byId.get(other) : null;
|
|
55
|
+
if (node?.type === 'ingestNode' && typeof node.data?.fileId === 'string' && node.data.fileId)
|
|
56
|
+
found.push(node.data.fileId);
|
|
57
|
+
}
|
|
58
|
+
return [...new Set(found)];
|
|
59
|
+
}
|
|
60
|
+
// The folders given and every folder inside them.
|
|
61
|
+
async function withSubfolders(db, projectId, folderIds) {
|
|
62
|
+
if (!folderIds.length)
|
|
63
|
+
return [];
|
|
64
|
+
const all = await db.collection('storage_folders').find({ projectId }).toArray();
|
|
65
|
+
const children = new Map();
|
|
66
|
+
for (const f of all) {
|
|
67
|
+
if (f.parentId)
|
|
68
|
+
children.set(f.parentId, [...(children.get(f.parentId) ?? []), f.folderId]);
|
|
69
|
+
}
|
|
70
|
+
const found = new Set();
|
|
71
|
+
const queue = folderIds.filter(id => all.some(f => f.folderId === id));
|
|
72
|
+
while (queue.length) {
|
|
73
|
+
const id = queue.shift();
|
|
74
|
+
if (found.has(id))
|
|
75
|
+
continue;
|
|
76
|
+
found.add(id);
|
|
77
|
+
queue.push(...(children.get(id) ?? []));
|
|
78
|
+
}
|
|
79
|
+
return [...found];
|
|
80
|
+
}
|
|
81
|
+
const missingOf = (rights) => REQUIRED.filter(field => !rights?.[field]);
|
|
82
|
+
/** A sheet as it reads now: its rows, from its sources and its assets' metadata, and its status. */
|
|
83
|
+
async function readSheet(project, document) {
|
|
84
|
+
const projectId = String(project._id ?? project.id);
|
|
85
|
+
const sources = sheetSources(document);
|
|
86
|
+
const db = (0, nosqlDb_1.nosqlDb)();
|
|
87
|
+
const order = sources.assetIds.map(assetId => ({ assetId, from: 'asset' }));
|
|
88
|
+
let files = [];
|
|
89
|
+
if (db) {
|
|
90
|
+
const folders = await withSubfolders(db, projectId, sources.folderIds);
|
|
91
|
+
if (folders.length) {
|
|
92
|
+
const inFolders = await db.collection('storage_files').find({ projectId, folderId: { $in: folders } }).toArray();
|
|
93
|
+
order.push(...inFolders.map(f => ({ assetId: f.fileId, from: 'folder' })));
|
|
94
|
+
}
|
|
95
|
+
order.push(...ingestAssetsOf(project.workflow, document.id).map(assetId => ({ assetId, from: 'ingest' })));
|
|
96
|
+
const wanted = [...new Set(order.map(o => o.assetId))];
|
|
97
|
+
files = wanted.length ? await db.collection('storage_files').find({ projectId, fileId: { $in: wanted } }).toArray() : [];
|
|
98
|
+
}
|
|
99
|
+
// An asset deleted is no longer in Storage: it leaves the sheet.
|
|
100
|
+
const fileOf = new Map(files.map(f => [f.fileId, f]));
|
|
101
|
+
const rights = await (0, rights_1.getRightsOf)(projectId, [...fileOf.keys()]);
|
|
102
|
+
const rows = new Map();
|
|
103
|
+
for (const { assetId, from } of order) {
|
|
104
|
+
const file = fileOf.get(assetId);
|
|
105
|
+
if (!file)
|
|
106
|
+
continue;
|
|
107
|
+
const row = rows.get(assetId);
|
|
108
|
+
if (row) {
|
|
109
|
+
if (!row.from.includes(from))
|
|
110
|
+
row.from.push(from);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const r = rights.get(assetId);
|
|
114
|
+
rows.set(assetId, {
|
|
115
|
+
assetId,
|
|
116
|
+
name: file.displayName ?? assetId,
|
|
117
|
+
category: file.category ?? null,
|
|
118
|
+
sourceUrl: r?.sourceUrl ?? null,
|
|
119
|
+
creatorName: r?.creatorName ?? null,
|
|
120
|
+
creatorUrl: r?.creatorUrl ?? null,
|
|
121
|
+
licenseType: r?.licenseType ?? null,
|
|
122
|
+
comments: r?.comments ?? null,
|
|
123
|
+
missing: missingOf(r),
|
|
124
|
+
from: [from],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const list = [...rows.values()];
|
|
128
|
+
return { document, sources, rows: list, status: list.length && list.every(r => !r.missing.length) ? 'complete' : 'incomplete' };
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ResourceKind = 'db' | 'api' | 'storage' | 'service' | 'export' | 'pipeline';
|
|
2
|
+
export declare const RESOURCE_KINDS: ResourceKind[];
|
|
3
|
+
export interface Dependent {
|
|
4
|
+
type: 'workflow' | 'export' | 'pipeline' | 'files' | 'folders' | 'deliverables' | 'shots';
|
|
5
|
+
name: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
/** The resource of that kind in the project, if it has it. */
|
|
10
|
+
export declare function findResource(project: any, kind: ResourceKind, id: string): any | null;
|
|
11
|
+
export declare function dependentsOf(project: any, kind: ResourceKind, id: string): Promise<Dependent[]>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESOURCE_KINDS = void 0;
|
|
4
|
+
exports.findResource = findResource;
|
|
5
|
+
exports.dependentsOf = dependentsOf;
|
|
6
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
7
|
+
const workflowResources_1 = require("./workflowResources");
|
|
8
|
+
exports.RESOURCE_KINDS = ['db', 'api', 'storage', 'service', 'export', 'pipeline'];
|
|
9
|
+
// The node data fields that point at each kind of resource.
|
|
10
|
+
const FIELDS = {
|
|
11
|
+
db: ['connectionId'],
|
|
12
|
+
api: ['connectionId'],
|
|
13
|
+
storage: ['storageConnectionId'],
|
|
14
|
+
service: ['integrationConnectionId'],
|
|
15
|
+
export: ['exportId'],
|
|
16
|
+
pipeline: ['pipelineId'],
|
|
17
|
+
};
|
|
18
|
+
// A resource's own node in the workflow isn't a dependent of it: its other nodes using it are
|
|
19
|
+
// (a data source reading one of a database's tables, an Ingest importing one file).
|
|
20
|
+
const ownNode = (node, kind, id) => (0, workflowResources_1.isOwnNode)(node, kind, id);
|
|
21
|
+
const nodesUsing = (schema, kind, id, skipOwn = false) => (Array.isArray(schema?.nodes) ? schema.nodes : []).filter((node) => !(skipOwn && ownNode(node, kind, id))
|
|
22
|
+
&& FIELDS[kind].some(field => node?.data?.[field] === id));
|
|
23
|
+
const labels = (nodes) => [...new Set(nodes.map(n => n?.data?.label || n?.type).filter(Boolean))].join(', ');
|
|
24
|
+
/** The resource of that kind in the project, if it has it. */
|
|
25
|
+
function findResource(project, kind, id) {
|
|
26
|
+
const list = {
|
|
27
|
+
db: project.dbConnections, api: project.connections, storage: project.storageConnections,
|
|
28
|
+
service: project.integrationConnections, export: project.exports, pipeline: project.pipelines,
|
|
29
|
+
}[kind] ?? [];
|
|
30
|
+
return list.find((r) => String(r.id) === id) ?? null;
|
|
31
|
+
}
|
|
32
|
+
// A storage connection's files: a built-in one's older files were stamped with the provider's
|
|
33
|
+
// id, or with nothing (see routes/storage's connFilter).
|
|
34
|
+
const filesOf = (projectId, conn) => conn?.source === 'builtin'
|
|
35
|
+
? { projectId, $or: [{ storageConnectionId: conn.id }, { storageConnectionId: conn.integrationId }, { storageConnectionId: { $exists: false } }] }
|
|
36
|
+
: { projectId, storageConnectionId: conn.id };
|
|
37
|
+
async function dependentsOf(project, kind, id) {
|
|
38
|
+
const projectId = String(project._id ?? project.id);
|
|
39
|
+
const found = [];
|
|
40
|
+
const inWorkflow = nodesUsing(project.workflow?.nodeSchema, kind, id, true);
|
|
41
|
+
if (inWorkflow.length)
|
|
42
|
+
found.push({ type: 'workflow', name: labels(inWorkflow), count: inWorkflow.length });
|
|
43
|
+
for (const exp of project.exports ?? []) {
|
|
44
|
+
if (kind === 'export' && String(exp.id) === id)
|
|
45
|
+
continue;
|
|
46
|
+
const nodes = nodesUsing(exp.nodeSchema, kind, id);
|
|
47
|
+
if (nodes.length)
|
|
48
|
+
found.push({ type: 'export', id: String(exp.id), name: exp.name ?? String(exp.id), count: nodes.length });
|
|
49
|
+
}
|
|
50
|
+
const db = (0, nosqlDb_1.nosqlDb)();
|
|
51
|
+
if (!db)
|
|
52
|
+
return found;
|
|
53
|
+
const pipelines = await db.collection('pipelines').find({ projectId }).toArray();
|
|
54
|
+
for (const pipeline of pipelines) {
|
|
55
|
+
if (kind === 'pipeline' && pipeline.id === id)
|
|
56
|
+
continue;
|
|
57
|
+
const nodes = nodesUsing(pipeline.nodeSchema, kind, id);
|
|
58
|
+
if (nodes.length)
|
|
59
|
+
found.push({ type: 'pipeline', id: pipeline.id, name: pipeline.name ?? pipeline.id, count: nodes.length });
|
|
60
|
+
}
|
|
61
|
+
if (kind === 'storage') {
|
|
62
|
+
const conn = findResource(project, 'storage', id);
|
|
63
|
+
if (conn) {
|
|
64
|
+
const files = await db.collection('storage_files').countDocuments(filesOf(projectId, conn));
|
|
65
|
+
if (files)
|
|
66
|
+
found.push({ type: 'files', name: 'Files', count: files });
|
|
67
|
+
const folders = await db.collection('storage_folders').countDocuments(filesOf(projectId, conn));
|
|
68
|
+
if (folders)
|
|
69
|
+
found.push({ type: 'folders', name: 'Folders', count: folders });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (kind === 'export') {
|
|
73
|
+
const deliverables = await db.collection('deliverables').countDocuments({ projectId, exportId: id });
|
|
74
|
+
if (deliverables)
|
|
75
|
+
found.push({ type: 'deliverables', name: 'Deliverables', count: deliverables });
|
|
76
|
+
const shots = await db.collection('production_shots').countDocuments({ projectId, exportId: id });
|
|
77
|
+
if (shots)
|
|
78
|
+
found.push({ type: 'shots', name: 'Shots', count: shots });
|
|
79
|
+
}
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** A deleted project's sequences, shots, tasks and
|
|
1
|
+
/** A deleted project's sequences, shots, tasks, deliverables and its assets' license metadata: only that project's. */
|
|
2
2
|
export declare function deleteProductionData(projectId: string): Promise<void>;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deleteProductionData = deleteProductionData;
|
|
4
4
|
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
5
|
-
/** A deleted project's sequences, shots, tasks and
|
|
5
|
+
/** A deleted project's sequences, shots, tasks, deliverables and its assets' license metadata: only that project's. */
|
|
6
6
|
async function deleteProductionData(projectId) {
|
|
7
7
|
const db = (0, nosqlDb_1.nosqlDb)();
|
|
8
8
|
if (!db || !projectId)
|
|
9
9
|
return;
|
|
10
|
-
for (const name of ['production_tasks', 'production_shots', 'production_sequences', 'deliverables']) {
|
|
10
|
+
for (const name of ['production_tasks', 'production_shots', 'production_sequences', 'deliverables', 'asset_rights']) {
|
|
11
11
|
await db.collection(name).deleteMany({ projectId });
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -13,4 +13,18 @@ type ClassifyResult = {
|
|
|
13
13
|
status: number;
|
|
14
14
|
};
|
|
15
15
|
export declare function classifyIntegrationId(id: string, auth: Auth, config: StreamByConfig, projectId: string): Promise<ClassifyResult>;
|
|
16
|
+
declare const LIST_OF: {
|
|
17
|
+
readonly database: "dbConnections";
|
|
18
|
+
readonly storage: "storageConnections";
|
|
19
|
+
readonly service: "integrationConnections";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Connects an integration to a project: a built-in (the host's database or storage) or one of
|
|
23
|
+
* the user's own. A built-in it was disconnected from comes back with the id it had, so what
|
|
24
|
+
* was kept for it (its files, its records) is its own again. With `kind`, only one of that kind.
|
|
25
|
+
*/
|
|
26
|
+
export declare function connectIntegration(project: any, integrationId: string, auth: Auth, config: StreamByConfig, options?: {
|
|
27
|
+
kind?: keyof typeof LIST_OF;
|
|
28
|
+
description?: string;
|
|
29
|
+
}): Promise<ClassifyResult>;
|
|
16
30
|
export {};
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.classifyIntegrationId = classifyIntegrationId;
|
|
7
|
+
exports.connectIntegration = connectIntegration;
|
|
7
8
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
9
|
const manager_1 = require("../models/manager");
|
|
9
10
|
const builtinAccess_1 = require("../utils/builtinAccess");
|
|
@@ -71,3 +72,34 @@ async function classifyIntegrationId(id, auth, config, projectId) {
|
|
|
71
72
|
},
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
const LIST_OF = { database: 'dbConnections', storage: 'storageConnections', service: 'integrationConnections' };
|
|
76
|
+
const NOUN_OF = { database: 'a database', storage: 'a storage', service: 'a service' };
|
|
77
|
+
/**
|
|
78
|
+
* Connects an integration to a project: a built-in (the host's database or storage) or one of
|
|
79
|
+
* the user's own. A built-in it was disconnected from comes back with the id it had, so what
|
|
80
|
+
* was kept for it (its files, its records) is its own again. With `kind`, only one of that kind.
|
|
81
|
+
*/
|
|
82
|
+
async function connectIntegration(project, integrationId, auth, config, options = {}) {
|
|
83
|
+
const projectId = String(project._id ?? project.id);
|
|
84
|
+
const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? []), ...(project.integrationConnections ?? [])]
|
|
85
|
+
.some((c) => c.integrationId === integrationId);
|
|
86
|
+
if (alreadyConnected)
|
|
87
|
+
return { error: 'This integration is already connected to the project', status: 400 };
|
|
88
|
+
const result = await classifyIntegrationId(integrationId, auth, config, projectId);
|
|
89
|
+
if ('error' in result)
|
|
90
|
+
return result;
|
|
91
|
+
if (options.kind && result.kind !== options.kind)
|
|
92
|
+
return { error: `This integration isn't ${NOUN_OF[options.kind]}`, status: 400 };
|
|
93
|
+
const before = (project.disconnectedBuiltins ?? []).find((d) => d.integrationId === integrationId);
|
|
94
|
+
const connection = {
|
|
95
|
+
...result.connection,
|
|
96
|
+
...(before && result.connection.source === 'builtin' ? { id: before.connectionId } : {}),
|
|
97
|
+
...(options.description ? { description: options.description } : {}),
|
|
98
|
+
};
|
|
99
|
+
const Project = (0, manager_1.getModel)('projects');
|
|
100
|
+
await Project.update({ _id: projectId }, { $push: { [LIST_OF[result.kind]]: connection } });
|
|
101
|
+
// By integrationId alone: a SQL project's $pull matches one key.
|
|
102
|
+
if (before)
|
|
103
|
+
await Project.update({ _id: projectId }, { $pull: { disconnectedBuiltins: { integrationId } } });
|
|
104
|
+
return { kind: result.kind, connection };
|
|
105
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const LICENSE_TYPES: readonly ["CC BY", "CC BY-SA", "CC BY-ND", "CC BY-NC", "CC BY-NC-SA", "CC BY-NC-ND", "CC0", "Free Standard", "Standard", "Editorial"];
|
|
2
|
+
export type LicenseType = typeof LICENSE_TYPES[number];
|
|
3
|
+
export declare const RIGHTS_FIELDS: readonly ["sourceUrl", "creatorName", "creatorUrl", "licenseType", "comments"];
|
|
4
|
+
type RightsField = typeof RIGHTS_FIELDS[number];
|
|
5
|
+
export interface AssetRights {
|
|
6
|
+
assetId: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
sourceUrl: string | null;
|
|
9
|
+
creatorName: string | null;
|
|
10
|
+
creatorUrl: string | null;
|
|
11
|
+
licenseType: LicenseType | null;
|
|
12
|
+
comments: string | null;
|
|
13
|
+
updatedBy?: string;
|
|
14
|
+
updatedAt?: Date;
|
|
15
|
+
}
|
|
16
|
+
export type RightsValues = Partial<Pick<AssetRights, RightsField>>;
|
|
17
|
+
/**
|
|
18
|
+
* The fields given of an asset's license metadata, checked: text (a link, an http(s) URL) and a
|
|
19
|
+
* license from the list. A field given empty or null is cleared.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkRights(body: any): {
|
|
22
|
+
values: RightsValues;
|
|
23
|
+
} | {
|
|
24
|
+
error: string;
|
|
25
|
+
};
|
|
26
|
+
/** The license metadata given as query params (an upload's), checked; null if none given. */
|
|
27
|
+
export declare function rightsFromQuery(query: Record<string, unknown>): {
|
|
28
|
+
values: RightsValues;
|
|
29
|
+
} | {
|
|
30
|
+
error: string;
|
|
31
|
+
} | null;
|
|
32
|
+
export declare function getRights(projectId: string, assetId: string): Promise<AssetRights | null>;
|
|
33
|
+
/** Several assets' license metadata, by asset id (those with none are missing). */
|
|
34
|
+
export declare function getRightsOf(projectId: string, assetIds: string[]): Promise<Map<string, AssetRights>>;
|
|
35
|
+
export declare function broadcastRights(project: any, data: {
|
|
36
|
+
action: 'saved' | 'deleted';
|
|
37
|
+
assetIds: string[];
|
|
38
|
+
rights?: AssetRights;
|
|
39
|
+
}): void;
|
|
40
|
+
/** Sets the fields given of an asset's license metadata (checked already), and tells the project. */
|
|
41
|
+
export declare function saveRights(project: any, assetId: string, values: RightsValues, userId: string): Promise<AssetRights>;
|
|
42
|
+
/** An asset's license metadata, gone with it (or, without an asset, a whole project's). */
|
|
43
|
+
export declare function deleteRightsOf(projectId: string, assetId?: string): Promise<void>;
|
|
44
|
+
export {};
|