streamby-core 0.41.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +143 -8
  3. package/dist/adapters/database/connectionManager.js +33 -0
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.js +5 -1
  6. package/dist/mcp/tools.js +93 -11
  7. package/dist/mcp/workflowNodes.js +9 -3
  8. package/dist/middleware/createRouter.js +6 -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.d.ts +3 -0
  12. package/dist/middleware/routes/document.js +348 -0
  13. package/dist/middleware/routes/export.d.ts +28 -0
  14. package/dist/middleware/routes/export.js +8 -4
  15. package/dist/middleware/routes/pipeline.js +5 -2
  16. package/dist/middleware/routes/project.js +17 -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 +12 -1
  25. package/dist/services/apiTokenPermissions.d.ts +1 -1
  26. package/dist/services/apiTokenPermissions.js +10 -0
  27. package/dist/services/auth.d.ts +2 -0
  28. package/dist/services/auth.js +17 -1
  29. package/dist/services/creditsSheets.d.ts +39 -0
  30. package/dist/services/creditsSheets.js +129 -0
  31. package/dist/services/dependents.d.ts +11 -0
  32. package/dist/services/dependents.js +81 -0
  33. package/dist/services/documents.d.ts +66 -0
  34. package/dist/services/documents.js +347 -0
  35. package/dist/services/executor.js +8 -0
  36. package/dist/services/oauthTokens.js +2 -2
  37. package/dist/services/production.d.ts +1 -1
  38. package/dist/services/production.js +2 -2
  39. package/dist/services/projectIntegration.d.ts +14 -0
  40. package/dist/services/projectIntegration.js +32 -0
  41. package/dist/services/rights.d.ts +44 -0
  42. package/dist/services/rights.js +123 -0
  43. package/dist/services/workflowResources.d.ts +90 -0
  44. package/dist/services/workflowResources.js +288 -0
  45. package/dist/types/index.d.ts +45 -0
  46. 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 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,7 +41,14 @@ class Model {
41
41
  delete processedFilter._id;
42
42
  }
43
43
  let sqlResults = [];
44
- if (this.tableName === 'projects' && processedFilter.members && processedFilter.members.$elemMatch) {
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));
@@ -1,5 +1,5 @@
1
1
  /** Permissions on a project, for the projects a token reaches. */
2
- export declare const PROJECT_PERMISSIONS: readonly ["project:read", "project:update", "project:delete", "members:read", "members:manage", "assets:read", "assets:write", "assets:delete", "production:read", "production:write", "production:delete", "workflow:read", "workflow:write", "exports:read", "exports:write", "rights:read", "rights:write", "jobs:read", "jobs:run"];
2
+ export declare const PROJECT_PERMISSIONS: readonly ["project:read", "project:update", "project:delete", "members:read", "members:manage", "assets:read", "assets:write", "assets:delete", "production:read", "production:write", "production:delete", "workflow:read", "workflow:write", "exports:read", "exports:write", "rights:read", "rights:write", "jobs:read", "jobs:run", "docs:read", "docs:write", "docs:publish", "docs:delete"];
3
3
  /** Permissions on the account: only a user's token has them. */
4
4
  export declare const ACCOUNT_PERMISSIONS: readonly ["projects:create"];
5
5
  export type ProjectPermission = typeof PROJECT_PERMISSIONS[number];
@@ -15,6 +15,7 @@ exports.PROJECT_PERMISSIONS = [
15
15
  'exports:read', 'exports:write',
16
16
  'rights:read', 'rights:write',
17
17
  'jobs:read', 'jobs:run',
18
+ 'docs:read', 'docs:write', 'docs:publish', 'docs:delete',
18
19
  ];
19
20
  /** Permissions on the account: only a user's token has them. */
20
21
  exports.ACCOUNT_PERMISSIONS = ['projects:create'];
@@ -74,7 +75,9 @@ const RULES = [
74
75
  // Rights.
75
76
  rule('read', `${P}/assets/[^/]+/rights`, 'rights:read'),
76
77
  rule(['PUT'], `${P}/assets/[^/]+/rights`, 'rights:write'),
78
+ rule('read', `${P}/credits-sheets(/[^/]+)?`, 'rights:read'),
77
79
  // Connections (API, database, storage, distribution, workers): the project's settings.
80
+ rule('read', `${P}/dependents/[^/]+/[^/]+`, 'project:read'),
78
81
  rule('read', `${P}/(connections|get-connection|distribution-connections|workers)(/.+)?`, 'project:read'),
79
82
  rule('write', `${P}/(connections|distribution-connections|workers)(/.+)?`, 'project:update'),
80
83
  rule('delete', `${P}/(connections|distribution-connections|workers)/.+`, 'project:update'),
@@ -94,6 +97,13 @@ const RULES = [
94
97
  // Jobs: transcoding, rendering, AI, publishing a deliverable.
95
98
  rule('read', `${P}/jobs`, 'jobs:read'),
96
99
  rule(['POST'], `${P}/(jobs/[^/]+(/retry)?|pipeline-suggest|deliverables/[^/]+/publish)`, 'jobs:run'),
100
+ // Documents: writing them is saving a version or moving them; publishing one (choosing what
101
+ // the exports deliver, or whether they deliver it at all) is apart. A PATCH that changes
102
+ // `exportable` needs docs:publish too (see routes/document).
103
+ rule('read', `${P}/documents(/.+)?`, 'docs:read'),
104
+ rule(['POST'], `${P}/documents/[^/]+/publish`, 'docs:publish'),
105
+ rule('write', `${P}/documents(/[^/]+(/sections)?)?`, 'docs:write'),
106
+ rule('delete', `${P}/documents/[^/]+`, 'docs:delete'),
97
107
  ];
98
108
  /** The rule for a request made with an API token, or undefined: not reachable with one. */
99
109
  function routeRuleFor(method, path) {
@@ -8,5 +8,7 @@ export declare const scopeReachesProject: (scope: Auth["apiToken"], projectId: s
8
8
  export declare const tokenAllows: (scope: Auth["apiToken"], permission: ApiTokenPermission) => boolean;
9
9
  /** Whether an app (an OAuth request) may make this request's method on its path. */
10
10
  export declare const oauthMayDelete: (method: string, path: string) => boolean;
11
+ /** Whether a request publishes a document (its version, or whether it's exported). */
12
+ export declare const publishesDocument: (method: string, path: string, body: unknown) => boolean;
11
13
  export declare const enforceApiTokenScope: (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
12
14
  export declare const authenticate: (config: StreamByConfig) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.authenticate = exports.enforceApiTokenScope = exports.oauthMayDelete = exports.tokenAllows = exports.scopeReachesProject = exports.isPublicRoute = void 0;
3
+ exports.authenticate = exports.enforceApiTokenScope = exports.publishesDocument = exports.oauthMayDelete = exports.tokenAllows = exports.scopeReachesProject = exports.isPublicRoute = void 0;
4
4
  const auth_1 = require("../utils/auth");
5
5
  const apiTokenPermissions_1 = require("./apiTokenPermissions");
6
6
  const oauthTokens_1 = require("./oauthTokens");
@@ -46,6 +46,19 @@ const OAUTH_DELETE_ALLOWED = new RegExp(`^/projects/[^/]+/deliverables/[^/]+/?$`
46
46
  /** Whether an app (an OAuth request) may make this request's method on its path. */
47
47
  const oauthMayDelete = (method, path) => method.toUpperCase() !== 'DELETE' || OAUTH_DELETE_ALLOWED.test(path);
48
48
  exports.oauthMayDelete = oauthMayDelete;
49
+ // Nor does an app publish: what it edits stays a draft until someone publishes it. Publishing
50
+ // is choosing a document's version for its exports (POST .../publish) or whether they deliver
51
+ // it (`exportable`). Refused whatever its scopes grant, as deleting is.
52
+ const DOCUMENT_PUBLISH = /^\/projects\/[^/]+\/documents\/[^/]+\/publish\/?$/;
53
+ const DOCUMENT = /^\/projects\/[^/]+\/documents\/[^/]+\/?$/;
54
+ /** Whether a request publishes a document (its version, or whether it's exported). */
55
+ const publishesDocument = (method, path, body) => {
56
+ const upper = method.toUpperCase();
57
+ if (upper === 'POST' && DOCUMENT_PUBLISH.test(path))
58
+ return true;
59
+ return upper === 'PATCH' && DOCUMENT.test(path) && typeof body === 'object' && body !== null && 'exportable' in body;
60
+ };
61
+ exports.publishesDocument = publishesDocument;
49
62
  const enforceApiTokenScope = (req, res, next) => {
50
63
  const auth = req.auth;
51
64
  const scope = auth?.apiToken;
@@ -57,6 +70,9 @@ const enforceApiTokenScope = (req, res, next) => {
57
70
  if (auth?.oauth && !(0, exports.oauthMayDelete)(req.method, req.path)) {
58
71
  return res.status(403).json({ error: 'An app connected with OAuth can\'t delete' });
59
72
  }
73
+ if (auth?.oauth && (0, exports.publishesDocument)(req.method, req.path, req.body)) {
74
+ return res.status(403).json({ error: 'An app connected with OAuth can\'t publish: what it edits stays a draft' });
75
+ }
60
76
  const rule = (0, apiTokenPermissions_1.routeRuleFor)(req.method, req.path);
61
77
  if (!rule || (rule.userTokenOnly && scope.kind !== 'user')) {
62
78
  return res.status(403).json({ error: 'Not available with an API token' });
@@ -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
+ }
@@ -0,0 +1,66 @@
1
+ import { Auth, DocumentAuthor, DocumentVersion, ProjectDocument, StreamByConfig } from '../types';
2
+ /** A document as the tree lists it: all but its content. */
3
+ export type DocumentSummary = Omit<ProjectDocument, 'content'>;
4
+ /** A version as the history lists it: all but its content. */
5
+ export type VersionSummary = Omit<DocumentVersion, 'content'>;
6
+ type Structure = Partial<Pick<ProjectDocument, 'parentId' | 'position' | 'exportable' | 'publishedVersion'>>;
7
+ export interface DocumentStore {
8
+ list(projectId: string): Promise<DocumentSummary[]>;
9
+ get(projectId: string, id: string): Promise<ProjectDocument | null>;
10
+ /** A new document, and its first version. */
11
+ create(document: ProjectDocument): Promise<ProjectDocument>;
12
+ /**
13
+ * A new version, only if the document is still at `baseVersion`: null if it isn't (someone
14
+ * saved in between) or there's no such document.
15
+ */
16
+ save(projectId: string, id: string, baseVersion: number, change: {
17
+ title: string;
18
+ content: string;
19
+ }, author: DocumentAuthor, at: Date): Promise<ProjectDocument | null>;
20
+ /** Where it is in the tree, and what's published of it: not a version. */
21
+ update(projectId: string, id: string, patch: Structure): Promise<ProjectDocument | null>;
22
+ /** Its versions, the latest first. */
23
+ versions(projectId: string, id: string): Promise<VersionSummary[]>;
24
+ version(projectId: string, id: string, version: number): Promise<DocumentVersion | null>;
25
+ /** Documents and their versions. */
26
+ remove(projectId: string, ids: string[]): Promise<void>;
27
+ /** A deleted project's documents. */
28
+ deleteProject(projectId: string): Promise<void>;
29
+ }
30
+ /** The router's main database, where documents are kept. */
31
+ export declare const setDocumentStoreDatabase: (config: StreamByConfig) => void;
32
+ export declare function documentStore(): DocumentStore;
33
+ /** Who's saving: the user, through the UI, an API token, or an app acting for them. */
34
+ export declare function authorOf(auth: Auth): DocumentAuthor;
35
+ /** A document and everything inside it (a folder's documents, at any depth). */
36
+ export declare function withDescendants(documents: DocumentSummary[], id: string): string[];
37
+ export type SectionResult = {
38
+ content: string;
39
+ } | {
40
+ error: 'not-found' | 'ambiguous';
41
+ };
42
+ /**
43
+ * The markdown with what's under `heading` replaced by `body`, the rest untouched. `heading` is
44
+ * its title, with or without its #s ("## Architecture", "Architecture"): with them, only a
45
+ * heading of that level matches.
46
+ */
47
+ export declare function replaceSection(markdown: string, heading: string, body: string): SectionResult;
48
+ /** A document as an export delivers it: its published version, never a draft. */
49
+ export interface PublishedDocument {
50
+ id: string;
51
+ title: string;
52
+ parentId: string | null;
53
+ position: number;
54
+ version: number;
55
+ content: string;
56
+ publishedAt: Date;
57
+ }
58
+ /**
59
+ * The project's exportable documents that have a published version (in one folder, at any
60
+ * depth, or only those given), as that version was saved.
61
+ */
62
+ export declare function publishedDocuments(projectId: string, scope?: {
63
+ folderId?: string;
64
+ documentIds?: string[];
65
+ }): Promise<PublishedDocument[]>;
66
+ export {};