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.
- package/CHANGELOG.md +89 -0
- package/README.md +143 -8
- package/dist/adapters/database/connectionManager.js +33 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/mcp/tools.js +93 -11
- package/dist/mcp/workflowNodes.js +9 -3
- package/dist/middleware/createRouter.js +6 -0
- package/dist/middleware/routes/connection.js +5 -2
- package/dist/middleware/routes/dbConnection.js +29 -5
- package/dist/middleware/routes/document.d.ts +3 -0
- package/dist/middleware/routes/document.js +348 -0
- 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 +17 -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.d.ts +1 -1
- package/dist/services/apiTokenPermissions.js +10 -0
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +17 -1
- 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/documents.d.ts +66 -0
- package/dist/services/documents.js +347 -0
- package/dist/services/executor.js +8 -0
- package/dist/services/oauthTokens.js +2 -2
- 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 +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setDocumentStoreDatabase = void 0;
|
|
4
|
+
exports.documentStore = documentStore;
|
|
5
|
+
exports.authorOf = authorOf;
|
|
6
|
+
exports.withDescendants = withDescendants;
|
|
7
|
+
exports.replaceSection = replaceSection;
|
|
8
|
+
exports.publishedDocuments = publishedDocuments;
|
|
9
|
+
const connectionManager_1 = require("../adapters/database/connectionManager");
|
|
10
|
+
const withoutContent = ({ content: _content, ...rest }) => rest;
|
|
11
|
+
const byPlace = (a, b) => a.position - b.position || new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
12
|
+
// ── PostgreSQL ──
|
|
13
|
+
const sqlStore = (pool) => {
|
|
14
|
+
const DOCS = 'streamby.documents';
|
|
15
|
+
const VERSIONS = 'streamby.document_versions';
|
|
16
|
+
const SUMMARY = `id, "projectId", kind, title, "parentId", position, version, exportable, "publishedVersion", "createdAt", "createdBy", "updatedAt", "updatedBy"`;
|
|
17
|
+
const insertVersion = (client, v) => client.query(`INSERT INTO ${VERSIONS} ("documentId", "projectId", version, title, content, author, "createdAt") VALUES ($1, $2, $3, $4, $5, $6, $7)`, [v.documentId, v.projectId, v.version, v.title, v.content, JSON.stringify(v.author), v.createdAt]);
|
|
18
|
+
// A save changes two tables: both, or neither.
|
|
19
|
+
const transaction = async (work) => {
|
|
20
|
+
const client = await pool.connect();
|
|
21
|
+
try {
|
|
22
|
+
await client.query('BEGIN');
|
|
23
|
+
const result = await work(client);
|
|
24
|
+
await client.query(result === null ? 'ROLLBACK' : 'COMMIT');
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
client.release();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
async list(projectId) {
|
|
37
|
+
const { rows } = await pool.query(`SELECT ${SUMMARY} FROM ${DOCS} WHERE "projectId" = $1 ORDER BY position, "createdAt"`, [projectId]);
|
|
38
|
+
return rows;
|
|
39
|
+
},
|
|
40
|
+
async get(projectId, id) {
|
|
41
|
+
const { rows } = await pool.query(`SELECT * FROM ${DOCS} WHERE "projectId" = $1 AND id = $2`, [projectId, id]);
|
|
42
|
+
return rows[0] ?? null;
|
|
43
|
+
},
|
|
44
|
+
async create(d) {
|
|
45
|
+
return (await transaction(async (client) => {
|
|
46
|
+
const { rows } = await client.query(`INSERT INTO ${DOCS} (id, "projectId", kind, title, "parentId", position, content, version, exportable, "publishedVersion", "createdAt", "createdBy", "updatedAt", "updatedBy")
|
|
47
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`, [d.id, d.projectId, d.kind, d.title, d.parentId, d.position, d.content, d.version, d.exportable, d.publishedVersion,
|
|
48
|
+
d.createdAt, JSON.stringify(d.createdBy), d.updatedAt, JSON.stringify(d.updatedBy)]);
|
|
49
|
+
await insertVersion(client, { documentId: d.id, projectId: d.projectId, version: d.version, title: d.title, content: d.content, author: d.createdBy, createdAt: d.createdAt });
|
|
50
|
+
return rows[0];
|
|
51
|
+
}));
|
|
52
|
+
},
|
|
53
|
+
async save(projectId, id, baseVersion, change, author, at) {
|
|
54
|
+
return transaction(async (client) => {
|
|
55
|
+
const { rows } = await client.query(`UPDATE ${DOCS} SET title = $4, content = $5, version = version + 1, "updatedAt" = $6, "updatedBy" = $7
|
|
56
|
+
WHERE "projectId" = $1 AND id = $2 AND version = $3 RETURNING *`, [projectId, id, baseVersion, change.title, change.content, at, JSON.stringify(author)]);
|
|
57
|
+
const saved = rows[0];
|
|
58
|
+
if (!saved)
|
|
59
|
+
return null;
|
|
60
|
+
await insertVersion(client, { documentId: id, projectId, version: saved.version, title: saved.title, content: saved.content, author, createdAt: at });
|
|
61
|
+
return saved;
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
async update(projectId, id, patch) {
|
|
65
|
+
const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
|
|
66
|
+
if (!entries.length)
|
|
67
|
+
return this.get(projectId, id);
|
|
68
|
+
const sets = entries.map(([key], i) => `"${key}" = $${i + 3}`).join(', ');
|
|
69
|
+
const { rows } = await pool.query(`UPDATE ${DOCS} SET ${sets} WHERE "projectId" = $1 AND id = $2 RETURNING *`, [projectId, id, ...entries.map(([, value]) => value)]);
|
|
70
|
+
return rows[0] ?? null;
|
|
71
|
+
},
|
|
72
|
+
async versions(projectId, id) {
|
|
73
|
+
const { rows } = await pool.query(`SELECT "documentId", "projectId", version, title, author, "createdAt" FROM ${VERSIONS}
|
|
74
|
+
WHERE "projectId" = $1 AND "documentId" = $2 ORDER BY version DESC`, [projectId, id]);
|
|
75
|
+
return rows;
|
|
76
|
+
},
|
|
77
|
+
async version(projectId, id, version) {
|
|
78
|
+
const { rows } = await pool.query(`SELECT * FROM ${VERSIONS} WHERE "projectId" = $1 AND "documentId" = $2 AND version = $3`, [projectId, id, version]);
|
|
79
|
+
return rows[0] ?? null;
|
|
80
|
+
},
|
|
81
|
+
async remove(projectId, ids) {
|
|
82
|
+
if (!ids.length)
|
|
83
|
+
return;
|
|
84
|
+
await transaction(async (client) => {
|
|
85
|
+
await client.query(`DELETE FROM ${VERSIONS} WHERE "projectId" = $1 AND "documentId" = ANY($2)`, [projectId, ids]);
|
|
86
|
+
await client.query(`DELETE FROM ${DOCS} WHERE "projectId" = $1 AND id = ANY($2)`, [projectId, ids]);
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async deleteProject(projectId) {
|
|
91
|
+
await pool.query(`DELETE FROM ${VERSIONS} WHERE "projectId" = $1`, [projectId]);
|
|
92
|
+
await pool.query(`DELETE FROM ${DOCS} WHERE "projectId" = $1`, [projectId]);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
// ── MongoDB ──
|
|
97
|
+
// A save writes the document and then its version: in a transaction, both or neither, when
|
|
98
|
+
// the server supports them (a replica set or a sharded cluster, as Atlas is). A standalone
|
|
99
|
+
// server doesn't: there they're two writes, and if the version's failed after the document's,
|
|
100
|
+
// the document would be at a version its history lacks.
|
|
101
|
+
let mongoTransactions;
|
|
102
|
+
const noTransactions = (err) => {
|
|
103
|
+
const { code, message } = (err ?? {});
|
|
104
|
+
return code === 20 || /Transaction numbers are only allowed|replica set/i.test(message ?? '');
|
|
105
|
+
};
|
|
106
|
+
const mongoStore = (client) => {
|
|
107
|
+
const docs = () => client.db().collection('documents');
|
|
108
|
+
const versions = () => client.db().collection('document_versions');
|
|
109
|
+
const strip = (doc) => {
|
|
110
|
+
if (!doc)
|
|
111
|
+
return null;
|
|
112
|
+
const { _id, ...rest } = doc;
|
|
113
|
+
return rest;
|
|
114
|
+
};
|
|
115
|
+
const together = async (work) => {
|
|
116
|
+
if (mongoTransactions !== false && typeof client.startSession === 'function') {
|
|
117
|
+
const session = client.startSession();
|
|
118
|
+
try {
|
|
119
|
+
let result;
|
|
120
|
+
await session.withTransaction(async () => { result = await work(session); });
|
|
121
|
+
mongoTransactions = true;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!noTransactions(err))
|
|
126
|
+
throw err;
|
|
127
|
+
mongoTransactions = false;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await session.endSession();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return work();
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
async list(projectId) {
|
|
137
|
+
const found = await docs().find({ projectId }, { projection: { content: 0 } }).toArray();
|
|
138
|
+
return found.map(d => strip(d)).sort(byPlace);
|
|
139
|
+
},
|
|
140
|
+
async get(projectId, id) {
|
|
141
|
+
return strip(await docs().findOne({ projectId, id }));
|
|
142
|
+
},
|
|
143
|
+
async create(d) {
|
|
144
|
+
return together(async (session) => {
|
|
145
|
+
await docs().insertOne({ ...d }, { session });
|
|
146
|
+
await versions().insertOne({ documentId: d.id, projectId: d.projectId, version: d.version, title: d.title, content: d.content, author: d.createdBy, createdAt: d.createdAt }, { session });
|
|
147
|
+
return d;
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
async save(projectId, id, baseVersion, change, author, at) {
|
|
151
|
+
return together(async (session) => {
|
|
152
|
+
// Conditional on the version: of two saves made on the same one, only the first goes through.
|
|
153
|
+
const saved = strip(await docs().findOneAndUpdate({ projectId, id, version: baseVersion }, { $set: { ...change, version: baseVersion + 1, updatedAt: at, updatedBy: author } }, { returnDocument: 'after', session }));
|
|
154
|
+
if (!saved)
|
|
155
|
+
return null;
|
|
156
|
+
await versions().insertOne({ documentId: id, projectId, version: saved.version, ...change, author, createdAt: at }, { session });
|
|
157
|
+
return saved;
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
async update(projectId, id, patch) {
|
|
161
|
+
const $set = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
|
|
162
|
+
if (!Object.keys($set).length)
|
|
163
|
+
return this.get(projectId, id);
|
|
164
|
+
return strip(await docs().findOneAndUpdate({ projectId, id }, { $set }, { returnDocument: 'after' }));
|
|
165
|
+
},
|
|
166
|
+
async versions(projectId, id) {
|
|
167
|
+
const found = await versions().find({ projectId, documentId: id }, { projection: { content: 0 } }).toArray();
|
|
168
|
+
return found.map(v => strip(v)).sort((a, b) => b.version - a.version);
|
|
169
|
+
},
|
|
170
|
+
async version(projectId, id, version) {
|
|
171
|
+
return strip(await versions().findOne({ projectId, documentId: id, version }));
|
|
172
|
+
},
|
|
173
|
+
async remove(projectId, ids) {
|
|
174
|
+
if (!ids.length)
|
|
175
|
+
return;
|
|
176
|
+
await together(async (session) => {
|
|
177
|
+
await versions().deleteMany({ projectId, documentId: { $in: ids } }, { session });
|
|
178
|
+
await docs().deleteMany({ projectId, id: { $in: ids } }, { session });
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
async deleteProject(projectId) {
|
|
182
|
+
await versions().deleteMany({ projectId });
|
|
183
|
+
await docs().deleteMany({ projectId });
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
// ── In memory (no main database) ──
|
|
188
|
+
const memoryStore = () => {
|
|
189
|
+
const docs = new Map();
|
|
190
|
+
const versions = [];
|
|
191
|
+
const own = (projectId, id) => {
|
|
192
|
+
const d = docs.get(id);
|
|
193
|
+
return d && d.projectId === projectId ? d : undefined;
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
async list(projectId) {
|
|
197
|
+
return [...docs.values()].filter(d => d.projectId === projectId).map(withoutContent).sort(byPlace);
|
|
198
|
+
},
|
|
199
|
+
async get(projectId, id) {
|
|
200
|
+
const d = own(projectId, id);
|
|
201
|
+
return d ? { ...d } : null;
|
|
202
|
+
},
|
|
203
|
+
async create(d) {
|
|
204
|
+
docs.set(d.id, { ...d });
|
|
205
|
+
versions.push({ documentId: d.id, projectId: d.projectId, version: d.version, title: d.title, content: d.content, author: d.createdBy, createdAt: d.createdAt });
|
|
206
|
+
return { ...d };
|
|
207
|
+
},
|
|
208
|
+
async save(projectId, id, baseVersion, change, author, at) {
|
|
209
|
+
const d = own(projectId, id);
|
|
210
|
+
if (!d || d.version !== baseVersion)
|
|
211
|
+
return null;
|
|
212
|
+
Object.assign(d, change, { version: baseVersion + 1, updatedAt: at, updatedBy: author });
|
|
213
|
+
versions.push({ documentId: id, projectId, version: d.version, ...change, author, createdAt: at });
|
|
214
|
+
return { ...d };
|
|
215
|
+
},
|
|
216
|
+
async update(projectId, id, patch) {
|
|
217
|
+
const d = own(projectId, id);
|
|
218
|
+
if (!d)
|
|
219
|
+
return null;
|
|
220
|
+
Object.assign(d, Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)));
|
|
221
|
+
return { ...d };
|
|
222
|
+
},
|
|
223
|
+
async versions(projectId, id) {
|
|
224
|
+
return versions.filter(v => v.projectId === projectId && v.documentId === id).map(withoutContent).sort((a, b) => b.version - a.version);
|
|
225
|
+
},
|
|
226
|
+
async version(projectId, id, version) {
|
|
227
|
+
const v = versions.find(x => x.projectId === projectId && x.documentId === id && x.version === version);
|
|
228
|
+
return v ? { ...v } : null;
|
|
229
|
+
},
|
|
230
|
+
async remove(projectId, ids) {
|
|
231
|
+
for (const id of ids)
|
|
232
|
+
if (own(projectId, id))
|
|
233
|
+
docs.delete(id);
|
|
234
|
+
for (let i = versions.length - 1; i >= 0; i--)
|
|
235
|
+
if (versions[i].projectId === projectId && ids.includes(versions[i].documentId))
|
|
236
|
+
versions.splice(i, 1);
|
|
237
|
+
},
|
|
238
|
+
async deleteProject(projectId) {
|
|
239
|
+
for (const [id, d] of docs)
|
|
240
|
+
if (d.projectId === projectId)
|
|
241
|
+
docs.delete(id);
|
|
242
|
+
for (let i = versions.length - 1; i >= 0; i--)
|
|
243
|
+
if (versions[i].projectId === projectId)
|
|
244
|
+
versions.splice(i, 1);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
let mainDbId;
|
|
249
|
+
let memory;
|
|
250
|
+
/** The router's main database, where documents are kept. */
|
|
251
|
+
const setDocumentStoreDatabase = (config) => {
|
|
252
|
+
mainDbId = config.databases?.find(db => db.main)?.id;
|
|
253
|
+
};
|
|
254
|
+
exports.setDocumentStoreDatabase = setDocumentStoreDatabase;
|
|
255
|
+
function documentStore() {
|
|
256
|
+
if (mainDbId && (0, connectionManager_1.getConnectedIds)().includes(mainDbId)) {
|
|
257
|
+
const entry = (0, connectionManager_1.getConnection)(mainDbId);
|
|
258
|
+
return entry.type === 'sql' ? sqlStore(entry.client) : mongoStore(entry.client);
|
|
259
|
+
}
|
|
260
|
+
return (memory ?? (memory = memoryStore()));
|
|
261
|
+
}
|
|
262
|
+
// ── Who saved it ──
|
|
263
|
+
/** Who's saving: the user, through the UI, an API token, or an app acting for them. */
|
|
264
|
+
function authorOf(auth) {
|
|
265
|
+
if (auth.oauth)
|
|
266
|
+
return { userId: auth.userId, username: auth.username, via: 'oauth', clientId: auth.oauth.clientId, clientName: auth.oauth.clientName };
|
|
267
|
+
if (auth.apiToken)
|
|
268
|
+
return { userId: auth.userId, username: auth.username, via: 'apiToken', tokenId: auth.apiToken.tokenId };
|
|
269
|
+
return { userId: auth.userId, username: auth.username, via: 'session' };
|
|
270
|
+
}
|
|
271
|
+
// ── The tree ──
|
|
272
|
+
/** A document and everything inside it (a folder's documents, at any depth). */
|
|
273
|
+
function withDescendants(documents, id) {
|
|
274
|
+
const ids = [id];
|
|
275
|
+
for (let i = 0; i < ids.length; i++) {
|
|
276
|
+
for (const d of documents)
|
|
277
|
+
if (d.parentId === ids[i])
|
|
278
|
+
ids.push(d.id);
|
|
279
|
+
}
|
|
280
|
+
return ids;
|
|
281
|
+
}
|
|
282
|
+
// ── Sections ──
|
|
283
|
+
// A section is what's under a markdown heading ("## Architecture"), up to the next heading of
|
|
284
|
+
// the same level or above. Headings inside a fenced code block aren't headings.
|
|
285
|
+
const HEADING = /^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/;
|
|
286
|
+
const FENCE = /^[ \t]*(```|~~~)/;
|
|
287
|
+
const headingOf = (text) => {
|
|
288
|
+
const match = text.trim().match(/^(#{1,6})[ \t]+(.*?)[ \t]*#*$/);
|
|
289
|
+
return match ? { level: match[1].length, title: match[2].trim() } : { level: null, title: text.trim() };
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* The markdown with what's under `heading` replaced by `body`, the rest untouched. `heading` is
|
|
293
|
+
* its title, with or without its #s ("## Architecture", "Architecture"): with them, only a
|
|
294
|
+
* heading of that level matches.
|
|
295
|
+
*/
|
|
296
|
+
function replaceSection(markdown, heading, body) {
|
|
297
|
+
const wanted = headingOf(heading);
|
|
298
|
+
const lines = markdown.split('\n');
|
|
299
|
+
const headings = [];
|
|
300
|
+
let fenced = false;
|
|
301
|
+
lines.forEach((line, i) => {
|
|
302
|
+
if (FENCE.test(line))
|
|
303
|
+
fenced = !fenced;
|
|
304
|
+
const match = !fenced && line.match(HEADING);
|
|
305
|
+
if (match)
|
|
306
|
+
headings.push({ line: i, level: match[1].length, title: match[2].trim() });
|
|
307
|
+
});
|
|
308
|
+
const matches = headings.filter(h => h.title === wanted.title && (wanted.level === null || h.level === wanted.level));
|
|
309
|
+
if (!matches.length)
|
|
310
|
+
return { error: 'not-found' };
|
|
311
|
+
if (matches.length > 1)
|
|
312
|
+
return { error: 'ambiguous' };
|
|
313
|
+
const [target] = matches;
|
|
314
|
+
const next = headings.find(h => h.line > target.line && h.level <= target.level);
|
|
315
|
+
const end = next ? next.line : lines.length;
|
|
316
|
+
const text = body.replace(/^(\s*\n)+/, '').replace(/\s+$/, '');
|
|
317
|
+
const replaced = [
|
|
318
|
+
...lines.slice(0, target.line + 1),
|
|
319
|
+
...(text ? ['', ...text.split('\n')] : []),
|
|
320
|
+
...(next ? [''] : []),
|
|
321
|
+
...lines.slice(end),
|
|
322
|
+
];
|
|
323
|
+
// A document's last line ends it, as it did.
|
|
324
|
+
if (!next && markdown.endsWith('\n'))
|
|
325
|
+
replaced.push('');
|
|
326
|
+
return { content: replaced.join('\n') };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The project's exportable documents that have a published version (in one folder, at any
|
|
330
|
+
* depth, or only those given), as that version was saved.
|
|
331
|
+
*/
|
|
332
|
+
async function publishedDocuments(projectId, scope = {}) {
|
|
333
|
+
const store = documentStore();
|
|
334
|
+
const documents = await store.list(projectId);
|
|
335
|
+
const within = scope.folderId ? new Set(withDescendants(documents, scope.folderId)) : null;
|
|
336
|
+
const chosen = documents.filter(d => d.kind === 'document' && d.exportable && d.publishedVersion !== null
|
|
337
|
+
&& (!within || within.has(d.id))
|
|
338
|
+
&& (!scope.documentIds?.length || scope.documentIds.includes(d.id)));
|
|
339
|
+
const published = await Promise.all(chosen.map(async (d) => {
|
|
340
|
+
const version = await store.version(projectId, d.id, d.publishedVersion);
|
|
341
|
+
return version && {
|
|
342
|
+
id: d.id, title: version.title, parentId: d.parentId, position: d.position,
|
|
343
|
+
version: version.version, content: version.content, publishedAt: version.createdAt,
|
|
344
|
+
};
|
|
345
|
+
}));
|
|
346
|
+
return published.filter((d) => !!d);
|
|
347
|
+
}
|
|
@@ -8,6 +8,7 @@ const connectionResolver_1 = require("./connectionResolver");
|
|
|
8
8
|
const userIntegration_1 = require("./userIntegration");
|
|
9
9
|
const oauthClient_1 = require("./oauthClient");
|
|
10
10
|
const jobQueue_1 = require("./jobQueue");
|
|
11
|
+
const documents_1 = require("./documents");
|
|
11
12
|
const mediaProcessor_1 = require("./mediaProcessor");
|
|
12
13
|
const vfxProcessor_1 = require("./vfxProcessor");
|
|
13
14
|
const aiProcessor_1 = require("./aiProcessor");
|
|
@@ -143,6 +144,13 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
143
144
|
await (0, connectionResolver_1.releaseDbClient)(resolved);
|
|
144
145
|
}
|
|
145
146
|
}
|
|
147
|
+
else if (node.type === 'documentsNode') {
|
|
148
|
+
// The project's own documents, as published: an edit is a draft, delivered only once a
|
|
149
|
+
// version of it is published. Its own data, so public exports read it too.
|
|
150
|
+
const documentIds = Array.isArray(node.data?.documentIds) ? node.data.documentIds : undefined;
|
|
151
|
+
const folderId = node.data?.folderId || undefined;
|
|
152
|
+
dataResults.push(await (0, documents_1.publishedDocuments)(project._id?.toString() ?? project.id, { folderId, documentIds }));
|
|
153
|
+
}
|
|
146
154
|
else if (node.type === 'connectionNode') {
|
|
147
155
|
const connection = project.connections?.find((c) => c.id === node.data?.connectionId);
|
|
148
156
|
if (!connection)
|
|
@@ -16,12 +16,12 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
16
16
|
exports.OAUTH_SCOPES = ['streamby:read', 'streamby:write'];
|
|
17
17
|
const READ_PERMISSIONS = [
|
|
18
18
|
'project:read', 'members:read', 'assets:read', 'production:read', 'workflow:read',
|
|
19
|
-
'exports:read', 'rights:read', 'jobs:read',
|
|
19
|
+
'exports:read', 'rights:read', 'jobs:read', 'docs:read',
|
|
20
20
|
];
|
|
21
21
|
// Making and changing, as the consent screen says: never deleting, nor managing members.
|
|
22
22
|
const WRITE_PERMISSIONS = [
|
|
23
23
|
'project:update', 'assets:write', 'production:write', 'workflow:write', 'exports:write',
|
|
24
|
-
'rights:write', 'jobs:run', 'projects:create',
|
|
24
|
+
'rights:write', 'jobs:run', 'projects:create', 'docs:write',
|
|
25
25
|
];
|
|
26
26
|
/** The permissions each scope grants. */
|
|
27
27
|
exports.OAUTH_SCOPE_PERMISSIONS = {
|
|
@@ -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 {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RIGHTS_FIELDS = exports.LICENSE_TYPES = void 0;
|
|
4
|
+
exports.checkRights = checkRights;
|
|
5
|
+
exports.rightsFromQuery = rightsFromQuery;
|
|
6
|
+
exports.getRights = getRights;
|
|
7
|
+
exports.getRightsOf = getRightsOf;
|
|
8
|
+
exports.broadcastRights = broadcastRights;
|
|
9
|
+
exports.saveRights = saveRights;
|
|
10
|
+
exports.deleteRightsOf = deleteRightsOf;
|
|
11
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
12
|
+
const auth_1 = require("../utils/auth");
|
|
13
|
+
const wsHub_1 = require("./wsHub");
|
|
14
|
+
// An asset's license metadata: where it was taken from, who made it, and under what license.
|
|
15
|
+
// Kept in the `asset_rights` collection, one record per asset (for all its versions), the
|
|
16
|
+
// only place it lives: a credits sheet reads it from here, and writes to here.
|
|
17
|
+
exports.LICENSE_TYPES = [
|
|
18
|
+
'CC BY', 'CC BY-SA', 'CC BY-ND', 'CC BY-NC', 'CC BY-NC-SA', 'CC BY-NC-ND', 'CC0', 'Free Standard', 'Standard', 'Editorial',
|
|
19
|
+
];
|
|
20
|
+
exports.RIGHTS_FIELDS = ['sourceUrl', 'creatorName', 'creatorUrl', 'licenseType', 'comments'];
|
|
21
|
+
const URL_FIELDS = ['sourceUrl', 'creatorUrl'];
|
|
22
|
+
const MAX_TEXT = 2000;
|
|
23
|
+
const isLicense = (value) => exports.LICENSE_TYPES.includes(value);
|
|
24
|
+
const isUrl = (value) => {
|
|
25
|
+
try {
|
|
26
|
+
return ['http:', 'https:'].includes(new URL(value).protocol);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The fields given of an asset's license metadata, checked: text (a link, an http(s) URL) and a
|
|
34
|
+
* license from the list. A field given empty or null is cleared.
|
|
35
|
+
*/
|
|
36
|
+
function checkRights(body) {
|
|
37
|
+
const values = {};
|
|
38
|
+
for (const field of exports.RIGHTS_FIELDS) {
|
|
39
|
+
const raw = body?.[field];
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (raw !== null && typeof raw !== 'string')
|
|
43
|
+
return { error: `${field} must be text` };
|
|
44
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
45
|
+
if (!value) {
|
|
46
|
+
values[field] = null;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (value.length > MAX_TEXT)
|
|
50
|
+
return { error: `${field} has at most ${MAX_TEXT} characters` };
|
|
51
|
+
if (field === 'licenseType') {
|
|
52
|
+
if (!isLicense(value))
|
|
53
|
+
return { error: `licenseType is one of: ${exports.LICENSE_TYPES.join(', ')}` };
|
|
54
|
+
values.licenseType = value;
|
|
55
|
+
}
|
|
56
|
+
else if (URL_FIELDS.includes(field) && !isUrl(value)) {
|
|
57
|
+
return { error: `${field} must be an http(s) URL` };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
values[field] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { values };
|
|
64
|
+
}
|
|
65
|
+
/** The license metadata given as query params (an upload's), checked; null if none given. */
|
|
66
|
+
function rightsFromQuery(query) {
|
|
67
|
+
const given = Object.fromEntries(exports.RIGHTS_FIELDS.filter(f => typeof query[f] === 'string').map(f => [f, query[f]]));
|
|
68
|
+
return Object.keys(given).length ? checkRights(given) : null;
|
|
69
|
+
}
|
|
70
|
+
const collection = () => (0, nosqlDb_1.nosqlDb)()?.collection('asset_rights') ?? null;
|
|
71
|
+
// A record as it's answered: only the fields it has now (a license off the list reads as none).
|
|
72
|
+
const readRights = (record) => record && {
|
|
73
|
+
assetId: record.assetId,
|
|
74
|
+
projectId: record.projectId,
|
|
75
|
+
sourceUrl: record.sourceUrl ?? null,
|
|
76
|
+
creatorName: record.creatorName ?? null,
|
|
77
|
+
creatorUrl: record.creatorUrl ?? null,
|
|
78
|
+
licenseType: isLicense(record.licenseType) ? record.licenseType : null,
|
|
79
|
+
comments: record.comments ?? null,
|
|
80
|
+
...(record.updatedBy ? { updatedBy: record.updatedBy } : {}),
|
|
81
|
+
...(record.updatedAt ? { updatedAt: record.updatedAt } : {}),
|
|
82
|
+
};
|
|
83
|
+
async function getRights(projectId, assetId) {
|
|
84
|
+
const col = collection();
|
|
85
|
+
return col ? readRights(await col.findOne({ projectId, assetId })) : null;
|
|
86
|
+
}
|
|
87
|
+
/** Several assets' license metadata, by asset id (those with none are missing). */
|
|
88
|
+
async function getRightsOf(projectId, assetIds) {
|
|
89
|
+
const col = collection();
|
|
90
|
+
if (!col || !assetIds.length)
|
|
91
|
+
return new Map();
|
|
92
|
+
const records = await col.find({ projectId, assetId: { $in: assetIds } }).toArray();
|
|
93
|
+
return new Map(records.map(r => [r.assetId, readRights(r)]));
|
|
94
|
+
}
|
|
95
|
+
// What changed in a project's assets' license metadata, to whoever is in it now: the credits
|
|
96
|
+
// sheets they have open read their rows again.
|
|
97
|
+
function broadcastRights(project, data) {
|
|
98
|
+
const projectId = String(project._id ?? project.id);
|
|
99
|
+
for (const member of project.members ?? []) {
|
|
100
|
+
if ((0, auth_1.activeMemberOf)(project, member.userId))
|
|
101
|
+
(0, wsHub_1.emitToUser)(member.userId, { type: 'assetRightsEvent', data: { projectId, ...data } });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Sets the fields given of an asset's license metadata (checked already), and tells the project. */
|
|
105
|
+
async function saveRights(project, assetId, values, userId) {
|
|
106
|
+
const col = collection();
|
|
107
|
+
if (!col)
|
|
108
|
+
throw new Error('Database not available');
|
|
109
|
+
const projectId = String(project._id ?? project.id);
|
|
110
|
+
// The fields an earlier record had that this one doesn't (holder, territory...) go.
|
|
111
|
+
const $unset = { rightsHolder: '', licenseUrl: '', territory: '', expiresAt: '', usageRestrictions: '', notes: '' };
|
|
112
|
+
await col.updateOne({ projectId, assetId }, { $set: { assetId, projectId, ...values, updatedBy: userId, updatedAt: new Date() }, $unset }, { upsert: true });
|
|
113
|
+
const rights = (await getRights(projectId, assetId));
|
|
114
|
+
broadcastRights(project, { action: 'saved', assetIds: [assetId], rights });
|
|
115
|
+
return rights;
|
|
116
|
+
}
|
|
117
|
+
/** An asset's license metadata, gone with it (or, without an asset, a whole project's). */
|
|
118
|
+
async function deleteRightsOf(projectId, assetId) {
|
|
119
|
+
const col = collection();
|
|
120
|
+
if (!col || !projectId)
|
|
121
|
+
return;
|
|
122
|
+
await col.deleteMany(assetId ? { projectId, assetId } : { projectId });
|
|
123
|
+
}
|