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
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectResourcesRouter = projectResourcesRouter;
4
+ exports.rememberDisconnectedBuiltin = rememberDisconnectedBuiltin;
5
+ const express_1 = require("express");
6
+ const manager_1 = require("../../models/manager");
7
+ const auth_1 = require("../../utils/auth");
8
+ const dependents_1 = require("../../services/dependents");
9
+ // A project's resources as a whole: what depends on one, asked before removing it.
10
+ function projectResourcesRouter(_config) {
11
+ const router = (0, express_1.Router)();
12
+ const Project = (0, manager_1.getModel)('projects');
13
+ async function projectFor(req, res, edit = false) {
14
+ const auth = req.auth;
15
+ const project = await Project.findOne({ _id: req.params.id });
16
+ if (!project) {
17
+ res.status(404).json({ message: 'Project not found' });
18
+ return null;
19
+ }
20
+ if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
21
+ res.status(403).json({ message: 'Unauthorized project access' });
22
+ return null;
23
+ }
24
+ return { project, auth };
25
+ }
26
+ router.get('/projects/:id/dependents/:kind/:resourceId', async (req, res) => {
27
+ try {
28
+ const found = await projectFor(req, res);
29
+ if (!found)
30
+ return;
31
+ const kind = req.params.kind;
32
+ if (!dependents_1.RESOURCE_KINDS.includes(kind))
33
+ return res.status(400).json({ message: `kind is one of: ${dependents_1.RESOURCE_KINDS.join(', ')}` });
34
+ if (!(0, dependents_1.findResource)(found.project, kind, req.params.resourceId))
35
+ return res.status(404).json({ message: 'Not found in this project' });
36
+ res.json({ dependents: await (0, dependents_1.dependentsOf)(found.project, kind, req.params.resourceId) });
37
+ }
38
+ catch (err) {
39
+ res.status(500).json({ message: err.message });
40
+ }
41
+ });
42
+ return router;
43
+ }
44
+ /** A built-in disconnected: its id kept, to connect it again as it was (services/projectIntegration). */
45
+ async function rememberDisconnectedBuiltin(projectId, kind, conn) {
46
+ if (!conn.integrationId)
47
+ return;
48
+ const Project = (0, manager_1.getModel)('projects');
49
+ await Project.update({ _id: projectId }, { $pull: { disconnectedBuiltins: { integrationId: conn.integrationId } } });
50
+ await Project.update({ _id: projectId }, { $push: { disconnectedBuiltins: { kind, integrationId: conn.integrationId, connectionId: conn.id } } });
51
+ }
@@ -3,74 +3,94 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.rightsRouter = rightsRouter;
4
4
  const express_1 = require("express");
5
5
  const auth_1 = require("../../utils/auth");
6
- const connectionManager_1 = require("../../adapters/database/connectionManager");
7
6
  const manager_1 = require("../../models/manager");
8
- function getDb() {
9
- const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
10
- if (!ids.length)
11
- return null;
12
- return (0, connectionManager_1.getConnection)(ids[0]).client.db();
13
- }
7
+ const nosqlDb_1 = require("../../utils/nosqlDb");
8
+ const rights_1 = require("../../services/rights");
9
+ const documents_1 = require("../../services/documents");
10
+ const creditsSheets_1 = require("../../services/creditsSheets");
14
11
  function rightsRouter(_config) {
15
12
  const router = (0, express_1.Router)();
16
13
  const Project = (0, manager_1.getModel)('projects');
17
- // GET /projects/:projectId/assets/:assetId/rights
14
+ // The project, if the caller may see it (or, with `edit`, change it); otherwise the answer.
15
+ async function projectFor(req, res, edit = false) {
16
+ const auth = req.auth;
17
+ const project = await Project.findOne({ _id: req.params.projectId });
18
+ if (!project) {
19
+ res.status(404).json({ message: 'Project not found' });
20
+ return null;
21
+ }
22
+ if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
23
+ res.status(403).json({ message: 'Unauthorized' });
24
+ return null;
25
+ }
26
+ return { project, auth, projectId: String(req.params.projectId) };
27
+ }
18
28
  router.get('/projects/:projectId/assets/:assetId/rights', async (req, res) => {
19
29
  try {
20
- const auth = req.auth;
21
- const { projectId, assetId } = req.params;
22
- const project = await Project.findOne({ _id: projectId });
23
- if (!project)
24
- return res.status(404).json({ message: 'Project not found' });
25
- if (!(0, auth_1.isProjectMember)(project, auth.userId))
26
- return res.status(403).json({ message: 'Unauthorized' });
27
- const db = getDb();
28
- if (!db)
30
+ const found = await projectFor(req, res);
31
+ if (!found)
32
+ return;
33
+ if (!(0, nosqlDb_1.nosqlDb)())
29
34
  return res.status(500).json({ message: 'Database not available' });
30
- const rights = await db.collection('asset_rights').findOne({ assetId, projectId });
31
- res.json({ rights: rights ?? null });
35
+ res.json({ rights: await (0, rights_1.getRights)(found.projectId, req.params.assetId) });
32
36
  }
33
37
  catch (err) {
34
38
  res.status(500).json({ message: err.message });
35
39
  }
36
40
  });
37
- // PUT /projects/:projectId/assets/:assetId/rights (upsert)
41
+ // Sets the fields given of the asset's license metadata (who may edit the asset may).
38
42
  router.put('/projects/:projectId/assets/:assetId/rights', async (req, res) => {
39
43
  try {
40
- const auth = req.auth;
41
- const { projectId, assetId } = req.params;
42
- const project = await Project.findOne({ _id: projectId });
43
- if (!project)
44
- return res.status(404).json({ message: 'Project not found' });
45
- if (!(0, auth_1.canEditProject)(project, auth.userId))
46
- return res.status(403).json({ message: 'Unauthorized' });
47
- const db = getDb();
44
+ const found = await projectFor(req, res, true);
45
+ if (!found)
46
+ return;
47
+ const db = (0, nosqlDb_1.nosqlDb)();
48
48
  if (!db)
49
49
  return res.status(500).json({ message: 'Database not available' });
50
- const { rightsHolder, licenseType, licenseUrl, territory, expiresAt, usageRestrictions, notes } = req.body;
51
- const now = new Date();
52
- const payload = {
53
- assetId,
54
- projectId,
55
- licenseType: licenseType ?? 'unknown',
56
- updatedBy: auth.userId,
57
- updatedAt: now,
58
- };
59
- if (rightsHolder !== undefined)
60
- payload.rightsHolder = rightsHolder;
61
- if (licenseUrl !== undefined)
62
- payload.licenseUrl = licenseUrl;
63
- if (territory !== undefined)
64
- payload.territory = territory;
65
- if (expiresAt !== undefined)
66
- payload.expiresAt = expiresAt ? new Date(expiresAt) : null;
67
- if (usageRestrictions !== undefined)
68
- payload.usageRestrictions = usageRestrictions;
69
- if (notes !== undefined)
70
- payload.notes = notes;
71
- await db.collection('asset_rights').updateOne({ assetId, projectId }, { $set: payload }, { upsert: true });
72
- const rights = await db.collection('asset_rights').findOne({ assetId, projectId });
73
- res.json({ rights });
50
+ const { assetId } = req.params;
51
+ if (!(await db.collection('storage_files').findOne({ projectId: found.projectId, fileId: assetId }))) {
52
+ return res.status(404).json({ message: 'Asset not found' });
53
+ }
54
+ const checked = (0, rights_1.checkRights)(req.body);
55
+ if ('error' in checked)
56
+ return res.status(400).json({ message: checked.error });
57
+ res.json({ rights: await (0, rights_1.saveRights)(found.project, assetId, checked.values, found.auth.userId) });
58
+ }
59
+ catch (err) {
60
+ res.status(500).json({ message: err.message });
61
+ }
62
+ });
63
+ // The project's credits sheets, each with its status (as its node shows it).
64
+ router.get('/projects/:projectId/credits-sheets', async (req, res) => {
65
+ try {
66
+ const found = await projectFor(req, res);
67
+ if (!found)
68
+ return;
69
+ const store = (0, documents_1.documentStore)();
70
+ const sheets = (await store.list(found.projectId)).filter(d => d.kind === 'credits');
71
+ const read = await Promise.all(sheets.map(async (summary) => {
72
+ const document = await store.get(found.projectId, summary.id);
73
+ if (!document)
74
+ return null;
75
+ const { rows, status } = await (0, creditsSheets_1.readSheet)(found.project, document);
76
+ return { id: document.id, title: document.title, status, rows: rows.length, incomplete: rows.filter(r => r.missing.length).length };
77
+ }));
78
+ res.json({ sheets: read.filter(Boolean) });
79
+ }
80
+ catch (err) {
81
+ res.status(500).json({ message: err.message });
82
+ }
83
+ });
84
+ // A credits sheet: its rows, made now from its sources and its assets' metadata, and its status.
85
+ router.get('/projects/:projectId/credits-sheets/:docId', async (req, res) => {
86
+ try {
87
+ const found = await projectFor(req, res);
88
+ if (!found)
89
+ return;
90
+ const document = await (0, documents_1.documentStore)().get(found.projectId, req.params.docId);
91
+ if (!document || document.kind !== 'credits')
92
+ return res.status(404).json({ message: 'Credits sheet not found' });
93
+ res.json(await (0, creditsSheets_1.readSheet)(found.project, document));
74
94
  }
75
95
  catch (err) {
76
96
  res.status(500).json({ message: err.message });
@@ -18,6 +18,8 @@ const crypto_1 = __importDefault(require("crypto"));
18
18
  const signedUrls_1 = require("../../services/signedUrls");
19
19
  const assetVersions_1 = require("../../services/assetVersions");
20
20
  const deliverables_1 = require("../../services/deliverables");
21
+ const rights_1 = require("../../services/rights");
22
+ const auth_2 = require("../../services/auth");
21
23
  const assetCategories_1 = require("../../utils/assetCategories");
22
24
  function getRawFilesCollection() {
23
25
  const model = (0, manager_1.getModel)('storage_files', 'nosql');
@@ -116,6 +118,9 @@ function storageRouter(config) {
116
118
  return res.status(404).json({ message: 'Project not found' });
117
119
  if (!(0, auth_1.canEditProject)(project, auth.userId))
118
120
  return res.status(403).json({ message: 'Unauthorized access' });
121
+ const rights = uploadRights(req, res);
122
+ if (rights === false)
123
+ return;
119
124
  if (!adapter.getPresignedUploadUrl) {
120
125
  return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
121
126
  }
@@ -137,6 +142,8 @@ function storageRouter(config) {
137
142
  };
138
143
  await collection.insertOne(fileDoc);
139
144
  await (0, assetVersions_1.recordFirstVersion)(fileDoc);
145
+ if (rights)
146
+ await (0, rights_1.saveRights)(project, fileId, rights, auth.userId);
140
147
  }
141
148
  const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
142
149
  const url = await adapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
@@ -233,6 +240,7 @@ function storageRouter(config) {
233
240
  return res.status(404).json({ message: 'File not found' });
234
241
  await deleteAsset(fileDoc, adapter);
235
242
  await collection.deleteOne({ fileId, projectId });
243
+ (0, rights_1.broadcastRights)(project, { action: 'deleted', assetIds: [fileId] });
236
244
  res.json({ message: 'File deleted successfully' });
237
245
  }
238
246
  catch (err) {
@@ -350,6 +358,9 @@ function storageRouter(config) {
350
358
  return res.status(404).json({ message: 'Project not found' });
351
359
  if (!(0, auth_1.canEditProject)(project, auth.userId))
352
360
  return res.status(403).json({ message: 'Unauthorized access' });
361
+ const rights = uploadRights(req, res);
362
+ if (rights === false)
363
+ return;
353
364
  const connAdapter = await resolveConnAdapter(connId, project, auth);
354
365
  if ('error' in connAdapter)
355
366
  return res.status(connAdapter.status).json({ message: connAdapter.error });
@@ -374,6 +385,8 @@ function storageRouter(config) {
374
385
  };
375
386
  await collection.insertOne(fileDoc);
376
387
  await (0, assetVersions_1.recordFirstVersion)(fileDoc);
388
+ if (rights)
389
+ await (0, rights_1.saveRights)(project, fileId, rights, auth.userId);
377
390
  }
378
391
  const expiresIn = (0, signedUrls_1.signedUrlTtl)(auth, config, 'upload');
379
392
  const url = await connAdapter.getPresignedUploadUrl(storageKey, contentType, expiresIn);
@@ -525,6 +538,7 @@ function storageRouter(config) {
525
538
  return res.status(404).json({ message: 'File not found' });
526
539
  await deleteAsset(fileDoc, connAdapter);
527
540
  await collection.deleteOne(connFilter(projectId, connId, project, { fileId }));
541
+ (0, rights_1.broadcastRights)(project, { action: 'deleted', assetIds: [fileId] });
528
542
  res.json({ message: 'File deleted successfully' });
529
543
  }
530
544
  catch (err) {
@@ -818,8 +832,26 @@ async function resolveFileUrl(file, adapter, expiresIn) {
818
832
  }
819
833
  // When a signed URL stops working, for a client to know (only when it's short-lived).
820
834
  const expiry = (expiresIn) => expiresIn ? { expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() } : {};
821
- // An asset's files (every version), its versions and its deliverables. Its record is the
822
- // caller's to delete.
835
+ // The license metadata given with an upload (licenseType, sourceUrl, creatorName, creatorUrl,
836
+ // comments as query params), checked: null if none, false if it was refused (and answered).
837
+ // An API token sets it only with rights:write.
838
+ function uploadRights(req, res) {
839
+ const checked = (0, rights_1.rightsFromQuery)(req.query);
840
+ if (!checked)
841
+ return null;
842
+ const auth = req.auth;
843
+ if (auth.apiToken && !(0, auth_2.tokenAllows)(auth.apiToken, 'rights:write')) {
844
+ res.status(403).json({ error: "This API token doesn't have the 'rights:write' permission" });
845
+ return false;
846
+ }
847
+ if ('error' in checked) {
848
+ res.status(400).json({ message: checked.error });
849
+ return false;
850
+ }
851
+ return checked.values;
852
+ }
853
+ // An asset's files (every version), its versions, its deliverables and its license metadata.
854
+ // Its record is the caller's to delete.
823
855
  async function deleteAsset(fileDoc, adapter) {
824
856
  const keys = await (0, assetVersions_1.versionStorageKeys)(fileDoc.projectId, fileDoc.fileId, fileDoc.storageKey);
825
857
  if (adapter.deleteFile)
@@ -827,4 +859,5 @@ async function deleteAsset(fileDoc, adapter) {
827
859
  await adapter.deleteFile(key);
828
860
  await (0, assetVersions_1.deleteVersions)(fileDoc.projectId, fileDoc.fileId);
829
861
  await (0, deliverables_1.deleteDeliverablesOf)(fileDoc.projectId, { assetId: fileDoc.fileId });
862
+ await (0, rights_1.deleteRightsOf)(fileDoc.projectId, fileDoc.fileId);
830
863
  }
@@ -2,16 +2,29 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.storageConnectionRouter = storageConnectionRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
5
6
  const mongodb_1 = require("mongodb");
6
7
  const manager_1 = require("../../models/manager");
7
8
  const auth_1 = require("../../utils/auth");
8
9
  const builtinAccess_1 = require("../../utils/builtinAccess");
9
10
  const sanitize_1 = require("../../utils/sanitize");
10
11
  const encryption_1 = require("../../utils/encryption");
12
+ const projectResources_1 = require("./projectResources");
13
+ const projectIntegration_1 = require("../../services/projectIntegration");
11
14
  const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
12
15
  function storageConnectionRouter(config) {
13
16
  const router = (0, express_1.Router)();
14
17
  const Project = (0, manager_1.getModel)('projects');
18
+ // The project's storage connections as they're answered, by the list and after each change.
19
+ // Availability is recomputed live for builtins — never trust a stored flag, per "validate on
20
+ // every use" (TCORE-69). BYOC (source: 'integration') is always available.
21
+ const listOf = (project, auth) => Promise.all((project.storageConnections || []).map(async (conn) => {
22
+ if (conn.source !== 'builtin')
23
+ return (0, sanitize_1.sanitizeConnection)(conn);
24
+ const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
25
+ return (0, sanitize_1.sanitizeConnection)({ ...conn, isBuiltin: true, available });
26
+ }));
27
+ const listNow = async (projectId, auth) => listOf(await Project.findOne({ _id: projectId }), auth);
15
28
  // ─── List storage connections ─────────────────────────────────────────────
16
29
  router.get('/projects/:id/connections/storage', async (req, res) => {
17
30
  try {
@@ -21,16 +34,7 @@ function storageConnectionRouter(config) {
21
34
  return res.status(404).json({ message: 'Project not found' });
22
35
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
23
36
  return res.status(403).json({ message: 'Unauthorized project access' });
24
- const connections = project.storageConnections || [];
25
- // Recompute availability live for builtins — never trust a stored flag, per
26
- // "validate on every use" (TCORE-69). BYOC (source: 'integration') is always available.
27
- const data = await Promise.all(connections.map(async (conn) => {
28
- if (conn.source !== 'builtin')
29
- return (0, sanitize_1.sanitizeConnection)(conn);
30
- const available = await (0, builtinAccess_1.assertBuiltinAccess)(auth, conn.integrationId ?? conn.id, config, 'storage');
31
- return (0, sanitize_1.sanitizeConnection)({ ...conn, isBuiltin: true, available });
32
- }));
33
- return res.status(200).json({ data });
37
+ return res.status(200).json({ data: await listOf(project, auth) });
34
38
  }
35
39
  catch (err) {
36
40
  res.status(500).json({ message: 'Failed to fetch storage connections', details: err.message });
@@ -41,6 +45,22 @@ function storageConnectionRouter(config) {
41
45
  try {
42
46
  const auth = req.auth;
43
47
  const { name, type, credential, integrationId, description } = req.body;
48
+ if (credential && integrationId) {
49
+ return res.status(400).json({ message: 'Provide only one of credential or integrationId' });
50
+ }
51
+ // An integration (the user's, or a built-in) is a named, typed storage already.
52
+ if (integrationId) {
53
+ const project = await Project.findOne({ _id: req.params.id });
54
+ if (!project)
55
+ return res.status(404).json({ message: 'Project not found' });
56
+ if (!(0, auth_1.canEditProject)(project, auth.userId))
57
+ return res.status(403).json({ message: 'Unauthorized project access' });
58
+ const result = await (0, projectIntegration_1.connectIntegration)(project, integrationId, auth, config, { kind: 'storage', description });
59
+ if ('error' in result)
60
+ return res.status(result.status).json({ message: result.error });
61
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'storage', result.connection);
62
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(result.connection), storageConnections: await listNow(req.params.id, auth) });
63
+ }
44
64
  if (!name || !type) {
45
65
  return res.status(400).json({ message: 'name and type are required' });
46
66
  }
@@ -84,7 +104,8 @@ function storageConnectionRouter(config) {
84
104
  ...(description !== undefined && { description }),
85
105
  };
86
106
  await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
87
- return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection) });
107
+ await (0, workflowResources_1.addToWorkflow)(req.params.id, 'storage', connection);
108
+ return res.status(201).json({ data: (0, sanitize_1.sanitizeConnection)(connection), storageConnections: await listNow(req.params.id, auth) });
88
109
  }
89
110
  catch (err) {
90
111
  res.status(500).json({ message: 'Failed to add storage connection', details: err.message });
@@ -103,11 +124,13 @@ function storageConnectionRouter(config) {
103
124
  const conn = project.storageConnections?.find((c) => c.id === connId);
104
125
  if (!conn)
105
126
  return res.status(404).json({ message: 'Storage connection not found' });
106
- if (conn.source === 'builtin') {
107
- return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
108
- }
127
+ // A built-in is disconnected, not deleted: its files stay in it, and it can be connected
128
+ // again with them (routes/projectResources).
109
129
  await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
110
- return res.status(200).json({ message: 'Storage connection deleted successfully' });
130
+ if (conn.source === 'builtin')
131
+ await (0, projectResources_1.rememberDisconnectedBuiltin)(req.params.id, 'storage', conn);
132
+ await (0, workflowResources_1.removeFromWorkflow)(req.params.id, 'storage', connId);
133
+ return res.status(200).json({ message: 'Storage connection deleted successfully', storageConnections: await listNow(req.params.id, auth) });
111
134
  }
112
135
  catch (err) {
113
136
  res.status(500).json({ message: 'Failed to delete storage connection', details: err.message });
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.userIntegrationRouter = userIntegrationRouter;
4
4
  const express_1 = require("express");
5
+ const workflowResources_1 = require("../../services/workflowResources");
5
6
  const builtinAccess_1 = require("../../utils/builtinAccess");
6
7
  const userIntegration_1 = require("../../services/userIntegration");
7
8
  const sanitize_1 = require("../../utils/sanitize");
9
+ const manager_1 = require("../../models/manager");
8
10
  const VALID_KINDS = ['database', 'storage', 'service'];
9
11
  const VALID_DB_PROVIDERS = ['postgresql', 'mongodb'];
10
12
  const VALID_STORAGE_PROVIDERS = ['s3', 'gcs', 'r2', 'azure'];
@@ -91,6 +93,29 @@ function userIntegrationRouter(config) {
91
93
  res.status(500).json({ message: 'Failed to update integration', details: err.message });
92
94
  }
93
95
  });
96
+ // The projects' connections that use one of the user's integrations: a connection can't
97
+ // work without it, so deleting it disconnects them.
98
+ const LISTS = [['dbConnections', 'db'], ['storageConnections', 'storage'], ['integrationConnections', 'service']];
99
+ async function connectionsUsing(integrationId) {
100
+ // Only the projects using it, asked of the database (not every project, read and filtered).
101
+ const projects = await (0, manager_1.getModel)('projects').find({ connectedIntegration: integrationId });
102
+ return projects.flatMap(project => LISTS.flatMap(([list, kind]) => (project[list] ?? [])
103
+ .filter(c => c.integrationId === integrationId && c.source !== 'builtin')
104
+ .map(c => ({ projectId: String(project._id ?? project.id), projectName: project.name, kind, list, connectionId: c.id, name: c.name }))));
105
+ }
106
+ router.get('/user/integrations/:integrationId/dependents', async (req, res) => {
107
+ try {
108
+ const auth = req.auth;
109
+ const integration = await (0, manager_1.getModel)('user_integrations').findOne({ id: req.params.integrationId, userId: auth.userId });
110
+ if (!integration)
111
+ return res.status(404).json({ message: 'Integration not found' });
112
+ const found = await connectionsUsing(req.params.integrationId);
113
+ res.json({ dependents: found.map(({ list: _list, ...d }) => d) });
114
+ }
115
+ catch (err) {
116
+ res.status(500).json({ message: err.message });
117
+ }
118
+ });
94
119
  // ─── Delete ────────────────────────────────────────────────────────────────
95
120
  router.delete('/user/integrations/:integrationId', async (req, res) => {
96
121
  try {
@@ -98,7 +123,23 @@ function userIntegrationRouter(config) {
98
123
  const deleted = await (0, userIntegration_1.deleteUserIntegration)(auth.userId, req.params.integrationId);
99
124
  if (!deleted)
100
125
  return res.status(404).json({ message: 'Integration not found' });
101
- res.json({ message: 'Integration deleted' });
126
+ const Project = (0, manager_1.getModel)('projects');
127
+ const disconnected = await connectionsUsing(req.params.integrationId);
128
+ for (const c of disconnected) {
129
+ await Project.update({ _id: c.projectId }, { $pull: { [c.list]: { id: c.connectionId } } });
130
+ await (0, workflowResources_1.removeFromWorkflow)(c.projectId, c.kind, c.connectionId);
131
+ }
132
+ // Each project it was disconnected from, with its connections as they are now.
133
+ const affected = await Promise.all([...new Set(disconnected.map(c => c.projectId))].map(async (projectId) => {
134
+ const project = await Project.findOne({ _id: projectId });
135
+ return {
136
+ projectId,
137
+ dbConnections: (0, sanitize_1.sanitizeConnections)(project?.dbConnections),
138
+ storageConnections: (0, sanitize_1.sanitizeConnections)(project?.storageConnections),
139
+ integrationConnections: (0, sanitize_1.sanitizeConnections)(project?.integrationConnections),
140
+ };
141
+ }));
142
+ res.json({ message: 'Integration deleted', disconnected: disconnected.length, affected });
102
143
  }
103
144
  catch (err) {
104
145
  res.status(500).json({ message: 'Failed to delete integration', details: err.message });