streamby-core 0.42.0 → 0.43.1

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +92 -4
  3. package/dist/adapters/database/connectionManager.js +4 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +5 -1
  6. package/dist/mcp/tools.js +25 -11
  7. package/dist/mcp/workflowNodes.js +9 -3
  8. package/dist/middleware/createRouter.js +2 -0
  9. package/dist/middleware/routes/connection.js +5 -2
  10. package/dist/middleware/routes/dbConnection.js +29 -5
  11. package/dist/middleware/routes/document.js +11 -6
  12. package/dist/middleware/routes/export.d.ts +28 -0
  13. package/dist/middleware/routes/export.js +8 -4
  14. package/dist/middleware/routes/member.js +24 -5
  15. package/dist/middleware/routes/pipeline.js +5 -2
  16. package/dist/middleware/routes/project.js +14 -12
  17. package/dist/middleware/routes/projectResources.d.ts +8 -0
  18. package/dist/middleware/routes/projectResources.js +51 -0
  19. package/dist/middleware/routes/rights.js +72 -52
  20. package/dist/middleware/routes/storage.js +35 -2
  21. package/dist/middleware/routes/storageConnection.js +38 -15
  22. package/dist/middleware/routes/userIntegration.js +42 -1
  23. package/dist/middleware/routes/workflow.js +120 -37
  24. package/dist/models/Model.js +13 -21
  25. package/dist/services/apiTokenPermissions.js +2 -0
  26. package/dist/services/creditsSheets.d.ts +39 -0
  27. package/dist/services/creditsSheets.js +129 -0
  28. package/dist/services/dependents.d.ts +11 -0
  29. package/dist/services/dependents.js +81 -0
  30. package/dist/services/production.d.ts +1 -1
  31. package/dist/services/production.js +2 -2
  32. package/dist/services/projectIntegration.d.ts +14 -0
  33. package/dist/services/projectIntegration.js +32 -0
  34. package/dist/services/rights.d.ts +44 -0
  35. package/dist/services/rights.js +123 -0
  36. package/dist/services/workflowResources.d.ts +90 -0
  37. package/dist/services/workflowResources.js +288 -0
  38. package/dist/types/index.d.ts +6 -1
  39. package/package.json +1 -1
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.userIntegrationRouter = userIntegrationRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
5
6
  const builtinAccess_1 = require("../../utils/builtinAccess");
6
7
  const userIntegration_1 = require("../../services/userIntegration");
7
8
  const sanitize_1 = require("../../utils/sanitize");
9
+ const manager_1 = require("../../models/manager");
8
10
  const VALID_KINDS = ['database', 'storage', 'service'];
9
11
  const VALID_DB_PROVIDERS = ['postgresql', 'mongodb'];
10
12
  const VALID_STORAGE_PROVIDERS = ['s3', 'gcs', 'r2', 'azure'];
@@ -91,6 +93,29 @@ function userIntegrationRouter(config) {
91
93
  res.status(500).json({ message: 'Failed to update integration', details: err.message });
92
94
  }
93
95
  });
96
+ // The projects' connections that use one of the user's integrations: a connection can't
97
+ // work without it, so deleting it disconnects them.
98
+ const LISTS = [['dbConnections', 'db'], ['storageConnections', 'storage'], ['integrationConnections', 'service']];
99
+ async function connectionsUsing(integrationId) {
100
+ // Only the projects using it, asked of the database (not every project, read and filtered).
101
+ const projects = await (0, manager_1.getModel)('projects').find({ connectedIntegration: integrationId });
102
+ return projects.flatMap(project => LISTS.flatMap(([list, kind]) => (project[list] ?? [])
103
+ .filter(c => c.integrationId === integrationId && c.source !== 'builtin')
104
+ .map(c => ({ projectId: String(project._id ?? project.id), projectName: project.name, kind, list, connectionId: c.id, name: c.name }))));
105
+ }
106
+ router.get('/user/integrations/:integrationId/dependents', async (req, res) => {
107
+ try {
108
+ const auth = req.auth;
109
+ const integration = await (0, manager_1.getModel)('user_integrations').findOne({ id: req.params.integrationId, userId: auth.userId });
110
+ if (!integration)
111
+ return res.status(404).json({ message: 'Integration not found' });
112
+ const found = await connectionsUsing(req.params.integrationId);
113
+ res.json({ dependents: found.map(({ list: _list, ...d }) => d) });
114
+ }
115
+ catch (err) {
116
+ res.status(500).json({ message: err.message });
117
+ }
118
+ });
94
119
  // ─── Delete ────────────────────────────────────────────────────────────────
95
120
  router.delete('/user/integrations/:integrationId', async (req, res) => {
96
121
  try {
@@ -98,7 +123,23 @@ function userIntegrationRouter(config) {
98
123
  const deleted = await (0, userIntegration_1.deleteUserIntegration)(auth.userId, req.params.integrationId);
99
124
  if (!deleted)
100
125
  return res.status(404).json({ message: 'Integration not found' });
101
- res.json({ message: 'Integration deleted' });
126
+ const Project = (0, manager_1.getModel)('projects');
127
+ const disconnected = await connectionsUsing(req.params.integrationId);
128
+ for (const c of disconnected) {
129
+ await Project.update({ _id: c.projectId }, { $pull: { [c.list]: { id: c.connectionId } } });
130
+ await (0, workflowResources_1.removeFromWorkflow)(c.projectId, c.kind, c.connectionId);
131
+ }
132
+ // Each project it was disconnected from, with its connections as they are now.
133
+ const affected = await Promise.all([...new Set(disconnected.map(c => c.projectId))].map(async (projectId) => {
134
+ const project = await Project.findOne({ _id: projectId });
135
+ return {
136
+ projectId,
137
+ dbConnections: (0, sanitize_1.sanitizeConnections)(project?.dbConnections),
138
+ storageConnections: (0, sanitize_1.sanitizeConnections)(project?.storageConnections),
139
+ integrationConnections: (0, sanitize_1.sanitizeConnections)(project?.integrationConnections),
140
+ };
141
+ }));
142
+ res.json({ message: 'Integration deleted', disconnected: disconnected.length, affected });
102
143
  }
103
144
  catch (err) {
104
145
  res.status(500).json({ message: 'Failed to delete integration', details: err.message });
@@ -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 mongodb_1 = require("mongodb");
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. PATCH upserts it.
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
- const workflow = resolveWorkflow(project);
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
- const project = await Project.findOne({ _id: projectId });
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 now = new Date();
45
- const existing = resolveWorkflow(project); // upsert: create the single workflow if absent
46
- const base = existing ?? {
47
- id: (0, crypto_1.randomUUID)(),
48
- name: 'Workflow',
49
- description: '',
50
- status: 'draft',
51
- projectId,
52
- nodeSchema: null,
53
- createdAt: now,
54
- };
55
- const { name, description, status, nodeSchema } = req.body;
56
- const updated = {
57
- ...base,
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
- else {
71
- await Project.update({ _id: new mongodb_1.ObjectId(projectId) }, { $set: { workflow: updated } });
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
- res.json({ data: updated, message: 'Workflow updated successfully' });
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 });
@@ -41,26 +41,12 @@ class Model {
41
41
  delete processedFilter._id;
42
42
  }
43
43
  let sqlResults = [];
44
- if (this.tableName === 'projects' && processedFilter.members && processedFilter.members.$elemMatch) {
45
- // Handle members filter for SQL projects
46
- const userId = processedFilter.members.$elemMatch.userId;
47
- const schema = this.schema || 'streamby'; // Default to 'streamby' if schema not provided
48
- const query = `
49
- SELECT p.*, pm."userId" as memberUserId, pm.role as memberRole, pm.status as memberStatus, pm.archived as memberArchived
50
- FROM "${schema}"."projects" p
51
- JOIN "${schema}"."project_members" pm ON p.id = pm."projectId"
52
- WHERE pm."userId" = $1
53
- `;
54
- const result = await connection.query(query, [userId]);
55
- sqlResults = result.rows.map(row => ({
56
- ...row,
57
- members: [{
58
- userId: row.memberUserId,
59
- role: row.memberRole,
60
- status: row.memberStatus,
61
- archived: row.memberArchived,
62
- }]
63
- }));
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;
64
50
  }
65
51
  else {
66
52
  // General SQL find
@@ -69,19 +55,24 @@ class Model {
69
55
  for (const item of sqlResults) {
70
56
  const transformedItem = this.transformResult(item);
71
57
  if (this.tableName === 'projects' && !transformedItem.members) {
72
- // For projects, if members not already populated by join, fetch from project_members table
58
+ // A SQL project's members are rows of project_members, read with it
73
59
  const projectMembers = await sql_1.sqlAdapter.find(connection, 'project_members', { projectId: transformedItem.id }, this.schema);
74
60
  transformedItem.members = projectMembers.map((member) => ({
75
61
  userId: member.userId,
76
62
  role: member.role,
77
63
  status: member.status,
78
64
  archived: member.archived,
65
+ ...(member.invitedBy ? { invitedBy: member.invitedBy } : {}),
79
66
  }));
80
67
  }
81
68
  allResults.push(transformedItem);
82
69
  }
83
70
  }
84
71
  else if (dbType === 'nosql') {
72
+ if (this.tableName === 'projects' && typeof processedFilter.connectedIntegration === 'string') {
73
+ const integrationId = processedFilter.connectedIntegration;
74
+ processedFilter = { $or: ['dbConnections', 'storageConnections', 'integrationConnections'].map(list => ({ [`${list}.integrationId`]: integrationId })) };
75
+ }
85
76
  const nosqlResults = await nosql_1.nosqlAdapter.find(connection, this.tableName, processedFilter);
86
77
  for (const item of nosqlResults) {
87
78
  allResults.push(this.transformResult(item));
@@ -127,6 +118,7 @@ class Model {
127
118
  role: member.role,
128
119
  status: member.status,
129
120
  archived: member.archived,
121
+ ...(member.invitedBy ? { invitedBy: member.invitedBy } : {}),
130
122
  }));
131
123
  }
132
124
  // Track which connection found this record so update() routes correctly
@@ -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 deliverables: only that project's. */
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 deliverables: only that project's. */
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
  }