streamby-core 0.37.0 → 0.39.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 +75 -0
- package/LICENSE +661 -661
- package/README.md +573 -512
- package/dist/adapters/database/connectionManager.js +124 -124
- package/dist/adapters/database/sql.js +1 -1
- package/dist/adapters/storage/s3.d.ts +3 -2
- package/dist/adapters/storage/s3.js +18 -5
- package/dist/mcp/server.js +12 -0
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +107 -14
- package/dist/mcp/workflowNodes.d.ts +24 -0
- package/dist/mcp/workflowNodes.js +48 -0
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/apiConnection.d.ts +3 -0
- package/dist/middleware/routes/apiConnection.js +100 -0
- package/dist/middleware/routes/credential.d.ts +3 -0
- package/dist/middleware/routes/credential.js +80 -0
- package/dist/middleware/routes/deliverable.d.ts +3 -0
- package/dist/middleware/routes/deliverable.js +131 -0
- package/dist/middleware/routes/export.js +18 -0
- package/dist/middleware/routes/production.js +60 -67
- package/dist/middleware/routes/project.js +39 -11
- package/dist/middleware/routes/storage.js +115 -24
- package/dist/models/Model.js +9 -9
- package/dist/services/apiConnection.d.ts +18 -0
- package/dist/services/apiConnection.js +65 -0
- package/dist/services/apiTokenPermissions.js +7 -3
- package/dist/services/assetVersions.d.ts +48 -0
- package/dist/services/assetVersions.js +84 -0
- package/dist/services/credential.d.ts +8 -0
- package/dist/services/credential.js +70 -0
- package/dist/services/deliverables.d.ts +26 -0
- package/dist/services/deliverables.js +55 -0
- package/dist/services/mediaProcessor.js +3 -1
- package/dist/services/pipeline.d.ts +2 -0
- package/dist/services/pipeline.js +292 -0
- package/dist/services/production.d.ts +2 -0
- package/dist/services/production.js +13 -0
- package/dist/services/productionFields.d.ts +16 -0
- package/dist/services/productionFields.js +80 -0
- package/dist/services/signedUrls.d.ts +6 -0
- package/dist/services/signedUrls.js +9 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.js +32 -0
- package/dist/utils/nosqlDb.d.ts +2 -0
- package/dist/utils/nosqlDb.js +11 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +14 -0
- package/package.json +1 -1
|
@@ -9,12 +9,16 @@ const auth_1 = require("../../utils/auth");
|
|
|
9
9
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
10
10
|
const manager_1 = require("../../models/manager");
|
|
11
11
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const deliverables_1 = require("../../services/deliverables");
|
|
13
|
+
const productionFields_1 = require("../../services/productionFields");
|
|
12
14
|
function getDb() {
|
|
13
15
|
const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
|
|
14
16
|
if (!ids.length)
|
|
15
17
|
return null;
|
|
16
18
|
return (0, connectionManager_1.getConnection)(ids[0]).client.db();
|
|
17
19
|
}
|
|
20
|
+
const SHOT_FIELDS = ['name', 'description', 'status', 'assignees', 'assetId', 'exportId', 'dueDate', 'order'];
|
|
21
|
+
const TASK_FIELDS = ['name', 'status', 'priority', 'assignee', 'dueDate', 'notes'];
|
|
18
22
|
function productionRouter(_config) {
|
|
19
23
|
const router = (0, express_1.Router)();
|
|
20
24
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -45,14 +49,15 @@ function productionRouter(_config) {
|
|
|
45
49
|
try {
|
|
46
50
|
const auth = req.auth;
|
|
47
51
|
const { projectId } = req.params;
|
|
48
|
-
const { name, description, order } = req.body;
|
|
49
|
-
if (!name)
|
|
50
|
-
return res.status(400).json({ message: 'name is required' });
|
|
51
52
|
const project = await Project.findOne({ _id: projectId });
|
|
52
53
|
if (!project)
|
|
53
54
|
return res.status(404).json({ message: 'Project not found' });
|
|
54
55
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
55
56
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
57
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, ['name', 'description', 'order'], project, { creating: true });
|
|
58
|
+
if ('error' in checked)
|
|
59
|
+
return res.status(400).json({ message: checked.error });
|
|
60
|
+
const { values } = checked;
|
|
56
61
|
const db = getDb();
|
|
57
62
|
if (!db)
|
|
58
63
|
return res.status(500).json({ message: 'Database not available' });
|
|
@@ -60,9 +65,9 @@ function productionRouter(_config) {
|
|
|
60
65
|
const sequence = {
|
|
61
66
|
id: crypto_1.default.randomUUID(),
|
|
62
67
|
projectId,
|
|
63
|
-
name,
|
|
64
|
-
description: description ?? null,
|
|
65
|
-
order: order ?? 0,
|
|
68
|
+
name: values.name,
|
|
69
|
+
description: values.description ?? null,
|
|
70
|
+
order: values.order ?? 0,
|
|
66
71
|
createdBy: auth.userId,
|
|
67
72
|
createdAt: now,
|
|
68
73
|
updatedAt: now,
|
|
@@ -86,14 +91,10 @@ function productionRouter(_config) {
|
|
|
86
91
|
const db = getDb();
|
|
87
92
|
if (!db)
|
|
88
93
|
return res.status(500).json({ message: 'Database not available' });
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (description !== undefined)
|
|
94
|
-
$set.description = description;
|
|
95
|
-
if (order !== undefined)
|
|
96
|
-
$set.order = order;
|
|
94
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, ['name', 'description', 'order'], project);
|
|
95
|
+
if ('error' in checked)
|
|
96
|
+
return res.status(400).json({ message: checked.error });
|
|
97
|
+
const $set = { ...checked.values, updatedAt: new Date() };
|
|
97
98
|
const result = await db.collection('production_sequences').findOneAndUpdate({ id: seqId, projectId }, { $set }, { returnDocument: 'after' });
|
|
98
99
|
if (!result)
|
|
99
100
|
return res.status(404).json({ message: 'Sequence not found' });
|
|
@@ -115,8 +116,15 @@ function productionRouter(_config) {
|
|
|
115
116
|
const db = getDb();
|
|
116
117
|
if (!db)
|
|
117
118
|
return res.status(500).json({ message: 'Database not available' });
|
|
118
|
-
|
|
119
|
+
// With its shots, and their tasks and deliverables: nothing is left behind.
|
|
120
|
+
const shotIds = (await db.collection('production_shots').find({ sequenceId: seqId, projectId }).toArray()).map(s => s.id);
|
|
121
|
+
const { deletedCount } = await db.collection('production_sequences').deleteOne({ id: seqId, projectId });
|
|
122
|
+
if (!deletedCount)
|
|
123
|
+
return res.status(404).json({ message: 'Sequence not found' });
|
|
119
124
|
await db.collection('production_shots').deleteMany({ sequenceId: seqId, projectId });
|
|
125
|
+
if (shotIds.length)
|
|
126
|
+
await db.collection('production_tasks').deleteMany({ projectId, shotId: { $in: shotIds } });
|
|
127
|
+
await (0, deliverables_1.deleteDeliverablesOf)(projectId, { shotIds });
|
|
120
128
|
res.json({ message: 'Sequence deleted' });
|
|
121
129
|
}
|
|
122
130
|
catch (err) {
|
|
@@ -150,30 +158,34 @@ function productionRouter(_config) {
|
|
|
150
158
|
try {
|
|
151
159
|
const auth = req.auth;
|
|
152
160
|
const { projectId, seqId } = req.params;
|
|
153
|
-
const { name, description, status, assignedTo, assetId, exportId, dueDate, order } = req.body;
|
|
154
|
-
if (!name)
|
|
155
|
-
return res.status(400).json({ message: 'name is required' });
|
|
156
161
|
const project = await Project.findOne({ _id: projectId });
|
|
157
162
|
if (!project)
|
|
158
163
|
return res.status(404).json({ message: 'Project not found' });
|
|
159
164
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
160
165
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
166
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, SHOT_FIELDS, project, { creating: true });
|
|
167
|
+
if ('error' in checked)
|
|
168
|
+
return res.status(400).json({ message: checked.error });
|
|
169
|
+
const { values } = checked;
|
|
161
170
|
const db = getDb();
|
|
162
171
|
if (!db)
|
|
163
172
|
return res.status(500).json({ message: 'Database not available' });
|
|
173
|
+
if (!(await db.collection('production_sequences').findOne({ id: seqId, projectId }))) {
|
|
174
|
+
return res.status(404).json({ message: 'Sequence not found' });
|
|
175
|
+
}
|
|
164
176
|
const now = new Date();
|
|
165
177
|
const shot = {
|
|
166
178
|
id: crypto_1.default.randomUUID(),
|
|
167
179
|
sequenceId: seqId,
|
|
168
180
|
projectId,
|
|
169
|
-
name,
|
|
170
|
-
description: description ?? null,
|
|
171
|
-
order: order ?? 0,
|
|
172
|
-
status: status ?? 'todo',
|
|
173
|
-
assignedTo: assignedTo ?? [],
|
|
174
|
-
assetId: assetId ?? null,
|
|
175
|
-
exportId: exportId ?? null,
|
|
176
|
-
dueDate: dueDate
|
|
181
|
+
name: values.name,
|
|
182
|
+
description: values.description ?? null,
|
|
183
|
+
order: values.order ?? 0,
|
|
184
|
+
status: values.status ?? 'todo',
|
|
185
|
+
assignedTo: values.assignedTo ?? [],
|
|
186
|
+
assetId: values.assetId ?? null,
|
|
187
|
+
exportId: values.exportId ?? null,
|
|
188
|
+
dueDate: values.dueDate ?? null,
|
|
177
189
|
createdBy: auth.userId,
|
|
178
190
|
createdAt: now,
|
|
179
191
|
updatedAt: now,
|
|
@@ -197,24 +209,10 @@ function productionRouter(_config) {
|
|
|
197
209
|
const db = getDb();
|
|
198
210
|
if (!db)
|
|
199
211
|
return res.status(500).json({ message: 'Database not available' });
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (description !== undefined)
|
|
205
|
-
$set.description = description;
|
|
206
|
-
if (status !== undefined)
|
|
207
|
-
$set.status = status;
|
|
208
|
-
if (assignedTo !== undefined)
|
|
209
|
-
$set.assignedTo = assignedTo;
|
|
210
|
-
if (assetId !== undefined)
|
|
211
|
-
$set.assetId = assetId;
|
|
212
|
-
if (exportId !== undefined)
|
|
213
|
-
$set.exportId = exportId;
|
|
214
|
-
if (dueDate !== undefined)
|
|
215
|
-
$set.dueDate = dueDate ? new Date(dueDate) : null;
|
|
216
|
-
if (order !== undefined)
|
|
217
|
-
$set.order = order;
|
|
212
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, SHOT_FIELDS, project);
|
|
213
|
+
if ('error' in checked)
|
|
214
|
+
return res.status(400).json({ message: checked.error });
|
|
215
|
+
const $set = { ...checked.values, updatedAt: new Date() };
|
|
218
216
|
const result = await db.collection('production_shots').findOneAndUpdate({ id: shotId, projectId }, { $set }, { returnDocument: 'after' });
|
|
219
217
|
if (!result)
|
|
220
218
|
return res.status(404).json({ message: 'Shot not found' });
|
|
@@ -238,6 +236,7 @@ function productionRouter(_config) {
|
|
|
238
236
|
return res.status(500).json({ message: 'Database not available' });
|
|
239
237
|
await db.collection('production_shots').deleteOne({ id: shotId, projectId });
|
|
240
238
|
await db.collection('production_tasks').deleteMany({ shotId, projectId });
|
|
239
|
+
await (0, deliverables_1.deleteDeliverablesOf)(projectId, { shotIds: [shotId] });
|
|
241
240
|
res.json({ message: 'Shot deleted' });
|
|
242
241
|
}
|
|
243
242
|
catch (err) {
|
|
@@ -271,28 +270,32 @@ function productionRouter(_config) {
|
|
|
271
270
|
try {
|
|
272
271
|
const auth = req.auth;
|
|
273
272
|
const { projectId, shotId } = req.params;
|
|
274
|
-
const { name, status, priority, assignedTo, dueDate, notes } = req.body;
|
|
275
|
-
if (!name)
|
|
276
|
-
return res.status(400).json({ message: 'name is required' });
|
|
277
273
|
const project = await Project.findOne({ _id: projectId });
|
|
278
274
|
if (!project)
|
|
279
275
|
return res.status(404).json({ message: 'Project not found' });
|
|
280
276
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
281
277
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
278
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, TASK_FIELDS, project, { creating: true });
|
|
279
|
+
if ('error' in checked)
|
|
280
|
+
return res.status(400).json({ message: checked.error });
|
|
281
|
+
const { values } = checked;
|
|
282
282
|
const db = getDb();
|
|
283
283
|
if (!db)
|
|
284
284
|
return res.status(500).json({ message: 'Database not available' });
|
|
285
|
+
if (!(await db.collection('production_shots').findOne({ id: shotId, projectId }))) {
|
|
286
|
+
return res.status(404).json({ message: 'Shot not found' });
|
|
287
|
+
}
|
|
285
288
|
const now = new Date();
|
|
286
289
|
const task = {
|
|
287
290
|
id: crypto_1.default.randomUUID(),
|
|
288
291
|
shotId,
|
|
289
292
|
projectId,
|
|
290
|
-
name,
|
|
291
|
-
status: status ?? 'todo',
|
|
292
|
-
priority: priority ?? 'medium',
|
|
293
|
-
assignedTo: assignedTo ?? null,
|
|
294
|
-
dueDate: dueDate
|
|
295
|
-
notes: notes ?? null,
|
|
293
|
+
name: values.name,
|
|
294
|
+
status: values.status ?? 'todo',
|
|
295
|
+
priority: values.priority ?? 'medium',
|
|
296
|
+
assignedTo: values.assignedTo ?? null,
|
|
297
|
+
dueDate: values.dueDate ?? null,
|
|
298
|
+
notes: values.notes ?? null,
|
|
296
299
|
createdBy: auth.userId,
|
|
297
300
|
createdAt: now,
|
|
298
301
|
updatedAt: now,
|
|
@@ -316,20 +319,10 @@ function productionRouter(_config) {
|
|
|
316
319
|
const db = getDb();
|
|
317
320
|
if (!db)
|
|
318
321
|
return res.status(500).json({ message: 'Database not available' });
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (status !== undefined)
|
|
324
|
-
$set.status = status;
|
|
325
|
-
if (priority !== undefined)
|
|
326
|
-
$set.priority = priority;
|
|
327
|
-
if (assignedTo !== undefined)
|
|
328
|
-
$set.assignedTo = assignedTo;
|
|
329
|
-
if (dueDate !== undefined)
|
|
330
|
-
$set.dueDate = dueDate ? new Date(dueDate) : null;
|
|
331
|
-
if (notes !== undefined)
|
|
332
|
-
$set.notes = notes;
|
|
322
|
+
const checked = (0, productionFields_1.checkProductionFields)(req.body, TASK_FIELDS, project);
|
|
323
|
+
if ('error' in checked)
|
|
324
|
+
return res.status(400).json({ message: checked.error });
|
|
325
|
+
const $set = { ...checked.values, updatedAt: new Date() };
|
|
333
326
|
const result = await db.collection('production_tasks').findOneAndUpdate({ id: taskId, shotId, projectId }, { $set }, { returnDocument: 'after' });
|
|
334
327
|
if (!result)
|
|
335
328
|
return res.status(404).json({ message: 'Task not found' });
|
|
@@ -10,6 +10,8 @@ const sanitize_1 = require("../../utils/sanitize");
|
|
|
10
10
|
const projectIntegration_1 = require("../../services/projectIntegration");
|
|
11
11
|
const auth_2 = require("../../services/auth");
|
|
12
12
|
const apiTokens_1 = require("../../services/apiTokens");
|
|
13
|
+
const nosqlDb_1 = require("../../utils/nosqlDb");
|
|
14
|
+
const production_1 = require("../../services/production");
|
|
13
15
|
function projectRouter(config) {
|
|
14
16
|
const router = (0, express_1.Router)();
|
|
15
17
|
const Project = (0, manager_1.getModel)('projects');
|
|
@@ -194,17 +196,12 @@ function projectRouter(config) {
|
|
|
194
196
|
if (!(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
195
197
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
196
198
|
}
|
|
197
|
-
const exports = (project.exports
|
|
198
|
-
id: e.id,
|
|
199
|
-
name: e.name,
|
|
200
|
-
description: e.description,
|
|
201
|
-
method: e.method,
|
|
202
|
-
private: e.private,
|
|
203
|
-
useConnections: e.useConnections,
|
|
204
|
-
useCredentials: e.useCredentials,
|
|
205
|
-
}));
|
|
199
|
+
const exports = (0, sanitize_1.exportSummaries)(project.exports);
|
|
206
200
|
const sanitized = (0, sanitize_1.sanitizeProject)({ ...project, id: project._id || project.id, _id: undefined, connections: project.connections || [], exports });
|
|
207
|
-
|
|
201
|
+
// Whether it has production data, for its Production section to show even without a
|
|
202
|
+
// production node in its workflow.
|
|
203
|
+
const hasProductionData = !!(await (0, nosqlDb_1.nosqlDb)()?.collection('production_sequences').findOne({ projectId }));
|
|
204
|
+
res.json({ project: { ...sanitized, hasProductionData }, message: 'Project fetched successfully' });
|
|
208
205
|
}
|
|
209
206
|
catch (err) {
|
|
210
207
|
res.status(500).json({ message: 'Failed to fetch project', details: err.message });
|
|
@@ -257,8 +254,9 @@ function projectRouter(config) {
|
|
|
257
254
|
return res.status(403).json({ message: 'Only a project admin can delete it' });
|
|
258
255
|
}
|
|
259
256
|
await Project.useDbType(project.dbType).delete({ _id: projectId });
|
|
260
|
-
// Its tokens have nothing left to reach.
|
|
257
|
+
// Its tokens have nothing left to reach, and its production data nothing to belong to.
|
|
261
258
|
await (0, apiTokens_1.deleteProjectApiTokens)(String(projectId));
|
|
259
|
+
await (0, production_1.deleteProductionData)(String(projectId));
|
|
262
260
|
const allProjects = await Project.find({});
|
|
263
261
|
const projects = allProjects
|
|
264
262
|
.filter(project => {
|
|
@@ -375,6 +373,21 @@ function projectRouter(config) {
|
|
|
375
373
|
res.status(500).json({ message: 'Failed to update project image', details: err.message });
|
|
376
374
|
}
|
|
377
375
|
});
|
|
376
|
+
// ─── A project's allowed origins, on their own ────────────────────────────
|
|
377
|
+
router.get('/projects/:id/origins', async (req, res) => {
|
|
378
|
+
try {
|
|
379
|
+
const auth = req.auth;
|
|
380
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
381
|
+
if (!project)
|
|
382
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
383
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
384
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
385
|
+
return res.status(200).json({ data: project.allowedOrigin || [] });
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
res.status(500).json({ message: 'Failed to fetch project origins', details: err.message });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
378
391
|
router.patch('/projects/:id/origins', async (req, res) => {
|
|
379
392
|
try {
|
|
380
393
|
const auth = req.auth;
|
|
@@ -397,6 +410,21 @@ function projectRouter(config) {
|
|
|
397
410
|
res.status(500).json({ message: 'Failed to update project origins', details: err.message });
|
|
398
411
|
}
|
|
399
412
|
});
|
|
413
|
+
// ─── A project's integrations, on their own ───────────────────────────────
|
|
414
|
+
router.get('/projects/:id/integrations', async (req, res) => {
|
|
415
|
+
try {
|
|
416
|
+
const auth = req.auth;
|
|
417
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
418
|
+
if (!project)
|
|
419
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
420
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
421
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
422
|
+
return res.status(200).json({ data: (0, sanitize_1.sanitizeIntegrationConnections)(project.integrationConnections) });
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
res.status(500).json({ message: 'Failed to fetch project integrations', details: err.message });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
400
428
|
// ─── Add an integration to an existing project ───────────────────────────
|
|
401
429
|
router.post('/projects/:id/integrations', async (req, res) => {
|
|
402
430
|
try {
|
|
@@ -15,6 +15,9 @@ const builtinAccess_1 = require("../../utils/builtinAccess");
|
|
|
15
15
|
const connectionResolver_1 = require("../../services/connectionResolver");
|
|
16
16
|
const mongodb_1 = require("mongodb");
|
|
17
17
|
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
+
const signedUrls_1 = require("../../services/signedUrls");
|
|
19
|
+
const assetVersions_1 = require("../../services/assetVersions");
|
|
20
|
+
const deliverables_1 = require("../../services/deliverables");
|
|
18
21
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
19
22
|
const VALID_3D_EXTENSIONS = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.ply']);
|
|
20
23
|
function validateContentType(contentType, filename, category) {
|
|
@@ -135,7 +138,7 @@ function storageRouter(config) {
|
|
|
135
138
|
const storageKey = `${projectId}/${category}/${fileId}${ext}`;
|
|
136
139
|
const collection = getRawFilesCollection();
|
|
137
140
|
if (collection) {
|
|
138
|
-
|
|
141
|
+
const fileDoc = {
|
|
139
142
|
_id: new mongodb_1.ObjectId(),
|
|
140
143
|
fileId,
|
|
141
144
|
projectId,
|
|
@@ -145,10 +148,13 @@ function storageRouter(config) {
|
|
|
145
148
|
contentType,
|
|
146
149
|
uploadedBy: auth.userId,
|
|
147
150
|
createdAt: new Date(),
|
|
148
|
-
}
|
|
151
|
+
};
|
|
152
|
+
await collection.insertOne(fileDoc);
|
|
153
|
+
await (0, assetVersions_1.recordFirstVersion)(fileDoc);
|
|
149
154
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
155
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
156
|
+
const url = await adapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
|
|
157
|
+
res.json({ url, fileId, storageKey, displayName: fileName, ...expiry(expiresIn) });
|
|
152
158
|
}
|
|
153
159
|
catch (err) {
|
|
154
160
|
res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
|
|
@@ -165,7 +171,7 @@ function storageRouter(config) {
|
|
|
165
171
|
const project = await Project.findOne({ _id: projectId });
|
|
166
172
|
if (!project)
|
|
167
173
|
return res.status(404).json({ message: 'Project not found' });
|
|
168
|
-
if (!(0, auth_1.
|
|
174
|
+
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
169
175
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
170
176
|
const collection = getRawFilesCollection();
|
|
171
177
|
if (!collection)
|
|
@@ -179,9 +185,21 @@ function storageRouter(config) {
|
|
|
179
185
|
if (!adapter.getPresignedUploadUrl) {
|
|
180
186
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
181
187
|
}
|
|
182
|
-
|
|
188
|
+
// The new version goes to the asset's own key, so its URL doesn't change; the current
|
|
189
|
+
// one is archived first, and without a way to archive it nothing is replaced.
|
|
190
|
+
if (!adapter.copyFile)
|
|
191
|
+
return res.status(501).json({ message: 'Storage adapter cannot keep versions' });
|
|
192
|
+
let version;
|
|
193
|
+
try {
|
|
194
|
+
version = await (0, assetVersions_1.archiveCurrentVersion)(fileDoc, { fileName, contentType, userId: auth.userId }, adapter.copyFile.bind(adapter));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return res.status(502).json({ message: 'Failed to archive the current version; nothing was replaced', details: err.message });
|
|
198
|
+
}
|
|
199
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
200
|
+
const url = await adapter.getPresignedUploadUrl(fileDoc.storageKey, contentType, expiresIn);
|
|
183
201
|
await collection.updateOne({ fileId, projectId }, { $set: { contentType, updatedAt: new Date() } });
|
|
184
|
-
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
202
|
+
res.json({ url, storageKey: fileDoc.storageKey, fileId, versionId: version.versionId, version: version.version, ...expiry(expiresIn) });
|
|
185
203
|
}
|
|
186
204
|
catch (err) {
|
|
187
205
|
res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
|
|
@@ -227,8 +245,7 @@ function storageRouter(config) {
|
|
|
227
245
|
const fileDoc = await collection.findOne({ fileId, projectId });
|
|
228
246
|
if (!fileDoc)
|
|
229
247
|
return res.status(404).json({ message: 'File not found' });
|
|
230
|
-
|
|
231
|
-
await adapter.deleteFile(fileDoc.storageKey);
|
|
248
|
+
await deleteAsset(fileDoc, adapter);
|
|
232
249
|
await collection.deleteOne({ fileId, projectId });
|
|
233
250
|
res.json({ message: 'File deleted successfully' });
|
|
234
251
|
}
|
|
@@ -249,7 +266,7 @@ function storageRouter(config) {
|
|
|
249
266
|
if (!collection)
|
|
250
267
|
return res.status(500).json({ message: 'Database not available' });
|
|
251
268
|
const files = await collection.find({ projectId }).sort({ createdAt: -1 }).limit(12).toArray();
|
|
252
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter)));
|
|
269
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
253
270
|
res.json({ data });
|
|
254
271
|
}
|
|
255
272
|
catch (err) {
|
|
@@ -272,13 +289,61 @@ function storageRouter(config) {
|
|
|
272
289
|
if (!collection)
|
|
273
290
|
return res.status(500).json({ message: 'Database not available' });
|
|
274
291
|
const files = await collection.find({ projectId, category }).sort({ createdAt: -1 }).toArray();
|
|
275
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter)));
|
|
292
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, adapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
276
293
|
res.json({ data });
|
|
277
294
|
}
|
|
278
295
|
catch (err) {
|
|
279
296
|
res.status(500).json({ message: 'Failed to list files', details: err.message });
|
|
280
297
|
}
|
|
281
298
|
});
|
|
299
|
+
// A short-lived URL to download an asset, or one of its versions, wherever it's stored
|
|
300
|
+
// (the built-in storage or one of the project's storage connections). It's for that file
|
|
301
|
+
// only, and downloads it with its name.
|
|
302
|
+
router.get('/projects/:projectId/assets/:fileId/download-url', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const auth = req.auth;
|
|
305
|
+
const { projectId, fileId } = req.params;
|
|
306
|
+
const { versionId } = req.query;
|
|
307
|
+
const project = await Project.findOne({ _id: projectId });
|
|
308
|
+
if (!project)
|
|
309
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
310
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
311
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
312
|
+
const collection = getRawFilesCollection();
|
|
313
|
+
if (!collection)
|
|
314
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
315
|
+
const fileDoc = await collection.findOne({ fileId, projectId });
|
|
316
|
+
if (!fileDoc)
|
|
317
|
+
return res.status(404).json({ message: 'File not found' });
|
|
318
|
+
let storageKey = fileDoc.storageKey;
|
|
319
|
+
let fileName = fileDoc.displayName;
|
|
320
|
+
let contentType = fileDoc.contentType;
|
|
321
|
+
let version;
|
|
322
|
+
if (versionId) {
|
|
323
|
+
const found = await (0, assetVersions_1.findVersion)(projectId, fileId, versionId);
|
|
324
|
+
if (!found)
|
|
325
|
+
return res.status(404).json({ message: 'Version not found' });
|
|
326
|
+
storageKey = found.storageKey;
|
|
327
|
+
fileName = found.fileName ?? fileName;
|
|
328
|
+
contentType = found.contentType ?? contentType;
|
|
329
|
+
version = found.version;
|
|
330
|
+
}
|
|
331
|
+
// Its storage connection's adapter; files stored before connections had their own row
|
|
332
|
+
// (no connection, or the built-in provider's id) are in the default storage.
|
|
333
|
+
const connId = fileDoc.storageConnectionId;
|
|
334
|
+
const fileAdapter = connId && findStorageConn(project, connId) ? await resolveConnAdapter(connId, project, auth) : adapter;
|
|
335
|
+
if ('error' in fileAdapter)
|
|
336
|
+
return res.status(fileAdapter.status).json({ message: fileAdapter.error });
|
|
337
|
+
if (!fileAdapter.getPresignedGetUrl)
|
|
338
|
+
return res.status(501).json({ message: 'Storage adapter does not support download URLs' });
|
|
339
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'download') ?? 3600;
|
|
340
|
+
const url = await fileAdapter.getPresignedGetUrl(storageKey, expiresIn, fileName);
|
|
341
|
+
res.json({ url, fileId, fileName, contentType: contentType ?? null, ...(version ? { versionId, version } : {}), ...expiry(expiresIn) });
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
res.status(500).json({ message: 'Failed to generate download URL', details: err.message });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
282
347
|
// ─── Per-connection file routes ───────────────────────────────────────────
|
|
283
348
|
router.get('/projects/:projectId/connections/storage/:connId/upload-url', async (req, res) => {
|
|
284
349
|
try {
|
|
@@ -309,7 +374,7 @@ function storageRouter(config) {
|
|
|
309
374
|
const storageKey = `${projectId}/${category}/${fileId}${ext}`;
|
|
310
375
|
const collection = getRawFilesCollection();
|
|
311
376
|
if (collection) {
|
|
312
|
-
|
|
377
|
+
const fileDoc = {
|
|
313
378
|
_id: new mongodb_1.ObjectId(),
|
|
314
379
|
fileId,
|
|
315
380
|
projectId,
|
|
@@ -320,10 +385,13 @@ function storageRouter(config) {
|
|
|
320
385
|
contentType,
|
|
321
386
|
uploadedBy: auth.userId,
|
|
322
387
|
createdAt: new Date(),
|
|
323
|
-
}
|
|
388
|
+
};
|
|
389
|
+
await collection.insertOne(fileDoc);
|
|
390
|
+
await (0, assetVersions_1.recordFirstVersion)(fileDoc);
|
|
324
391
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
392
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
393
|
+
const url = await connAdapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
|
|
394
|
+
res.json({ url, fileId, storageKey, displayName: fileName, ...expiry(expiresIn) });
|
|
327
395
|
}
|
|
328
396
|
catch (err) {
|
|
329
397
|
res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
|
|
@@ -345,7 +413,7 @@ function storageRouter(config) {
|
|
|
345
413
|
if (!collection)
|
|
346
414
|
return res.status(500).json({ message: 'Database not available' });
|
|
347
415
|
const files = await collection.find(connFilter(projectId, connId, project)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
348
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
416
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
349
417
|
res.json({ data });
|
|
350
418
|
}
|
|
351
419
|
catch (err) {
|
|
@@ -371,7 +439,7 @@ function storageRouter(config) {
|
|
|
371
439
|
if (!collection)
|
|
372
440
|
return res.status(500).json({ message: 'Database not available' });
|
|
373
441
|
const files = await collection.find(connFilter(projectId, connId, project, { category })).sort({ createdAt: -1 }).toArray();
|
|
374
|
-
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
442
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter, (0, signedUrls_1.signedUrlTtl)(auth, config, 'download'))));
|
|
375
443
|
res.json({ data });
|
|
376
444
|
}
|
|
377
445
|
catch (err) {
|
|
@@ -389,7 +457,7 @@ function storageRouter(config) {
|
|
|
389
457
|
const project = await Project.findOne({ _id: projectId });
|
|
390
458
|
if (!project)
|
|
391
459
|
return res.status(404).json({ message: 'Project not found' });
|
|
392
|
-
if (!(0, auth_1.
|
|
460
|
+
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
393
461
|
return res.status(403).json({ message: 'Unauthorized access' });
|
|
394
462
|
const collection = getRawFilesCollection();
|
|
395
463
|
if (!collection)
|
|
@@ -406,9 +474,21 @@ function storageRouter(config) {
|
|
|
406
474
|
if (!connAdapter.getPresignedUploadUrl) {
|
|
407
475
|
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
408
476
|
}
|
|
409
|
-
|
|
477
|
+
// The new version goes to the asset's own key, so its URL doesn't change; the current
|
|
478
|
+
// one is archived first, and without a way to archive it nothing is replaced.
|
|
479
|
+
if (!connAdapter.copyFile)
|
|
480
|
+
return res.status(501).json({ message: 'Storage adapter cannot keep versions' });
|
|
481
|
+
let version;
|
|
482
|
+
try {
|
|
483
|
+
version = await (0, assetVersions_1.archiveCurrentVersion)(fileDoc, { fileName, contentType, userId: auth.userId }, connAdapter.copyFile.bind(connAdapter));
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
return res.status(502).json({ message: 'Failed to archive the current version; nothing was replaced', details: err.message });
|
|
487
|
+
}
|
|
488
|
+
const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
|
|
489
|
+
const url = await connAdapter.getPresignedUploadUrl(fileDoc.storageKey, contentType, expiresIn);
|
|
410
490
|
await collection.updateOne(connFilter(projectId, connId, project, { fileId }), { $set: { contentType, updatedAt: new Date() } });
|
|
411
|
-
res.json({ url, storageKey: fileDoc.storageKey, fileId });
|
|
491
|
+
res.json({ url, storageKey: fileDoc.storageKey, fileId, versionId: version.versionId, version: version.version, ...expiry(expiresIn) });
|
|
412
492
|
}
|
|
413
493
|
catch (err) {
|
|
414
494
|
res.status(500).json({ message: 'Failed to generate replace URL', details: err.message });
|
|
@@ -457,8 +537,7 @@ function storageRouter(config) {
|
|
|
457
537
|
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
458
538
|
if (!fileDoc)
|
|
459
539
|
return res.status(404).json({ message: 'File not found' });
|
|
460
|
-
|
|
461
|
-
await connAdapter.deleteFile(fileDoc.storageKey);
|
|
540
|
+
await deleteAsset(fileDoc, connAdapter);
|
|
462
541
|
await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
|
|
463
542
|
res.json({ message: 'File deleted successfully' });
|
|
464
543
|
}
|
|
@@ -731,11 +810,11 @@ function storageRouter(config) {
|
|
|
731
810
|
});
|
|
732
811
|
return router;
|
|
733
812
|
}
|
|
734
|
-
async function resolveFileUrl(file, adapter) {
|
|
813
|
+
async function resolveFileUrl(file, adapter, expiresIn) {
|
|
735
814
|
let url = null;
|
|
736
815
|
if (adapter.getPresignedGetUrl) {
|
|
737
816
|
try {
|
|
738
|
-
url = await adapter.getPresignedGetUrl(file.storageKey);
|
|
817
|
+
url = await adapter.getPresignedGetUrl(file.storageKey, expiresIn);
|
|
739
818
|
}
|
|
740
819
|
catch { /* leave null */ }
|
|
741
820
|
}
|
|
@@ -751,3 +830,15 @@ async function resolveFileUrl(file, adapter) {
|
|
|
751
830
|
url,
|
|
752
831
|
};
|
|
753
832
|
}
|
|
833
|
+
// When a signed URL stops working, for a client to know (only when it's short-lived).
|
|
834
|
+
const expiry = (expiresIn) => expiresIn ? { expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() } : {};
|
|
835
|
+
// An asset's files (every version), its versions and its deliverables. Its record is the
|
|
836
|
+
// caller's to delete.
|
|
837
|
+
async function deleteAsset(fileDoc, adapter) {
|
|
838
|
+
const keys = await (0, assetVersions_1.versionStorageKeys)(fileDoc.projectId, fileDoc.fileId, fileDoc.storageKey);
|
|
839
|
+
if (adapter.deleteFile)
|
|
840
|
+
for (const key of keys)
|
|
841
|
+
await adapter.deleteFile(key);
|
|
842
|
+
await (0, assetVersions_1.deleteVersions)(fileDoc.projectId, fileDoc.fileId);
|
|
843
|
+
await (0, deliverables_1.deleteDeliverablesOf)(fileDoc.projectId, { assetId: fileDoc.fileId });
|
|
844
|
+
}
|
package/dist/models/Model.js
CHANGED
|
@@ -45,11 +45,11 @@ class Model {
|
|
|
45
45
|
// Handle members filter for SQL projects
|
|
46
46
|
const userId = processedFilter.members.$elemMatch.userId;
|
|
47
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
|
|
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
53
|
`;
|
|
54
54
|
const result = await connection.query(query, [userId]);
|
|
55
55
|
sqlResults = result.rows.map(row => ({
|
|
@@ -219,10 +219,10 @@ class Model {
|
|
|
219
219
|
else {
|
|
220
220
|
const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
|
|
221
221
|
const matchVal = String(value[matchKey]);
|
|
222
|
-
await connection.query(`UPDATE ${tbl} SET "${field}" = (
|
|
223
|
-
SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
|
|
224
|
-
FROM jsonb_array_elements("${field}") elem
|
|
225
|
-
WHERE elem->>'${matchKey}' != $1
|
|
222
|
+
await connection.query(`UPDATE ${tbl} SET "${field}" = (
|
|
223
|
+
SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
|
|
224
|
+
FROM jsonb_array_elements("${field}") elem
|
|
225
|
+
WHERE elem->>'${matchKey}' != $1
|
|
226
226
|
) WHERE id = $2`, [matchVal, id]);
|
|
227
227
|
}
|
|
228
228
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StreamByConfig, ApiConnection, ApiConnectionMethod } from '../types';
|
|
2
|
+
export declare function addApiConnection(config: StreamByConfig, projectId: string, data: {
|
|
3
|
+
name: string;
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
method: ApiConnectionMethod;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
credentialId?: string;
|
|
9
|
+
}): Promise<ApiConnection>;
|
|
10
|
+
export declare function updateApiConnection(config: StreamByConfig, projectId: string, connectionId: string, data: {
|
|
11
|
+
name?: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
method?: ApiConnectionMethod;
|
|
14
|
+
prefix?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
credentialId?: string;
|
|
17
|
+
}): Promise<ApiConnection>;
|
|
18
|
+
export declare function deleteApiConnection(config: StreamByConfig, projectId: string, connectionId: string): Promise<void>;
|