streamby-core 0.29.8 → 0.29.11

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.
@@ -73,13 +73,15 @@ const ensureTablesExist = async (pool) => {
73
73
  console.log('✅ "projects" table ensured to exist.');
74
74
  // Add JSONB columns to existing tables that predate this schema version
75
75
  await pool.query(`
76
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
77
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
78
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT '[]';
79
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections" JSONB DEFAULT '[]';
80
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
81
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
82
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
76
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
77
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
78
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT '[]';
79
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections" JSONB DEFAULT '[]';
80
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
81
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
82
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
83
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
84
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
83
85
  `);
84
86
  // Ensure dbType exists with a non-null default
85
87
  await pool.query(`
@@ -124,6 +126,24 @@ const ensureTablesExist = async (pool) => {
124
126
  );
125
127
  `);
126
128
  console.log('✅ "exports" table ensured to exist.');
129
+ // user_integrations is registered against the main db only (see createRouter.ts), but
130
+ // this function runs per-SQL-connection like projects/exports above — harmless on
131
+ // non-main connections since the model never queries them there.
132
+ await pool.query(`
133
+ CREATE TABLE IF NOT EXISTS streamby.user_integrations (
134
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
135
+ "userId" VARCHAR(255) NOT NULL,
136
+ kind VARCHAR(20) NOT NULL,
137
+ provider VARCHAR(20) NOT NULL,
138
+ name VARCHAR(255) NOT NULL,
139
+ description TEXT,
140
+ "encryptedCredential" TEXT NOT NULL,
141
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
142
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
143
+ );
144
+ CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
145
+ `);
146
+ console.log('✅ "user_integrations" table ensured to exist.');
127
147
  }
128
148
  catch (error) {
129
149
  console.error('❌ Error ensuring tables exist:', error);
@@ -8,6 +8,7 @@ const express_1 = __importDefault(require("express"));
8
8
  const manager_1 = require("../models/manager");
9
9
  const storage_1 = require("../providers/storage");
10
10
  const connectionManager_1 = require("../adapters/database/connectionManager");
11
+ const backfillBuiltinConnections_1 = require("../services/backfillBuiltinConnections");
11
12
  const wsHub_1 = require("../services/wsHub");
12
13
  const auth_1 = require("../services/auth");
13
14
  const encryption_1 = require("../utils/encryption");
@@ -22,6 +23,7 @@ const credential_1 = require("./routes/credential");
22
23
  const connection_1 = require("./routes/connection");
23
24
  const dbConnection_1 = require("./routes/dbConnection");
24
25
  const storageConnection_1 = require("./routes/storageConnection");
26
+ const userIntegration_1 = require("./routes/userIntegration");
25
27
  const notification_1 = require("./routes/notification");
26
28
  const subscription_1 = require("./routes/subscription");
27
29
  const media_1 = require("./routes/media");
@@ -46,6 +48,10 @@ function createStreamByRouter(config) {
46
48
  const readyPromise = (0, connectionManager_1.initConnections)(config.databases || [])
47
49
  .catch((err) => {
48
50
  console.error('❌ StreamBy: failed to initialize database connections:', err);
51
+ })
52
+ .then(() => (0, backfillBuiltinConnections_1.backfillBuiltinConnections)(config))
53
+ .catch((err) => {
54
+ console.error('❌ StreamBy: failed to backfill built-in connections:', err);
49
55
  })
50
56
  .finally(() => {
51
57
  ready = true;
@@ -77,6 +83,7 @@ function createStreamByRouter(config) {
77
83
  if (mainDb) {
78
84
  (0, manager_1.registerModel)('users', [mainDb.id], 'users', 'accounts');
79
85
  (0, manager_1.registerModel)('user_subscriptions', [mainDb.id], 'user_subscriptions', 'streamby');
86
+ (0, manager_1.registerModel)('user_integrations', [mainDb.id], 'user_integrations', streambySchema);
80
87
  }
81
88
  }
82
89
  if (config.websocket?.server) {
@@ -95,6 +102,7 @@ function createStreamByRouter(config) {
95
102
  router.use((0, connection_1.connectionRouter)(config));
96
103
  router.use((0, dbConnection_1.dbConnectionRouter)(config));
97
104
  router.use((0, storageConnection_1.storageConnectionRouter)(config));
105
+ router.use((0, userIntegration_1.userIntegrationRouter)(config));
98
106
  router.use((0, notification_1.notificationRouter)(config));
99
107
  router.use((0, subscription_1.subscriptionRouter)(config));
100
108
  router.use((0, media_1.mediaRouter)(config));
@@ -39,27 +39,42 @@ const mongodb_1 = require("mongodb");
39
39
  const manager_1 = require("../../models/manager");
40
40
  const auth_1 = require("../../utils/auth");
41
41
  const connectionManager_1 = require("../../adapters/database/connectionManager");
42
+ const builtinAccess_1 = require("../../utils/builtinAccess");
43
+ const userIntegration_1 = require("../../services/userIntegration");
42
44
  const dbConnection_1 = require("../../services/dbConnection");
43
45
  const VALID_DB_TYPES = ['postgresql', 'mongodb'];
44
- function isBuiltinDb(connId, config) {
45
- return (config.databases ?? []).some(db => db.id === connId);
46
- }
47
- function resolveBuiltinConnection(connId, config) {
48
- const db = (config.databases ?? []).find(d => d.id === connId);
46
+ // `builtinId` is config.databases[].id (e.g. 'mongo'), NOT the DbConnection row's own id —
47
+ // those are different values since TCORE-69's backfill gives each row its own UUID.
48
+ function resolveBuiltinConnection(builtinId, config) {
49
+ const db = (config.databases ?? []).find(d => d.id === builtinId);
49
50
  if (!db)
50
- return { error: `Database '${connId}' not found in config`, status: 404 };
51
+ return { error: `Database '${builtinId}' not found in config`, status: 404 };
51
52
  try {
52
- const { client, type } = (0, connectionManager_1.getConnection)(connId);
53
+ const { client, type } = (0, connectionManager_1.getConnection)(builtinId);
53
54
  return { client: client, dbType: type };
54
55
  }
55
56
  catch (e) {
56
57
  return { error: e.message, status: 503 };
57
58
  }
58
59
  }
60
+ // Finds the connection row + classifies it. A row is builtin iff conn.source === 'builtin'
61
+ // (persisted at creation/backfill time) — NOT by comparing the row's own id against
62
+ // config.databases, which no longer holds since builtin rows get their own generated id.
63
+ function findDbConnection(project, connId) {
64
+ return project.dbConnections?.find((c) => c.id === connId);
65
+ }
59
66
  async function getDecryptedConnectionString(project, connId) {
60
67
  const conn = project.dbConnections?.find((c) => c.id === connId);
61
68
  if (!conn)
62
69
  return { error: 'DB connection not found', status: 404 };
70
+ if (conn.source === 'integration') {
71
+ if (!conn.integrationId)
72
+ return { error: 'Connection is missing its integrationId', status: 500 };
73
+ const credential = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
74
+ if (!credential)
75
+ return { error: 'Integration not found', status: 400 };
76
+ return { connectionString: credential, conn };
77
+ }
63
78
  const { decrypt, isEncryptionKeySet } = await Promise.resolve().then(() => __importStar(require('../../utils/encryption')));
64
79
  if (!isEncryptionKeySet())
65
80
  return { error: 'Encryption key is not set', status: 500 };
@@ -93,9 +108,15 @@ function dbConnectionRouter(config) {
93
108
  if (auth.role !== 'admin' && auth.role !== 'editor') {
94
109
  return res.status(403).json({ message: 'Permission denied' });
95
110
  }
96
- const { name, dbType, credentialId, description } = req.body;
97
- if (!name || !dbType || !credentialId) {
98
- return res.status(400).json({ message: 'name, dbType, and credentialId are required' });
111
+ const { name, dbType, credentialId, integrationId, description } = req.body;
112
+ if (!name || !dbType) {
113
+ return res.status(400).json({ message: 'name and dbType are required' });
114
+ }
115
+ if (!credentialId && !integrationId) {
116
+ return res.status(400).json({ message: 'Either credentialId or integrationId is required' });
117
+ }
118
+ if (credentialId && integrationId) {
119
+ return res.status(400).json({ message: 'Provide only one of credentialId or integrationId' });
99
120
  }
100
121
  if (!VALID_DB_TYPES.includes(dbType)) {
101
122
  return res.status(400).json({ message: `dbType must be one of: ${VALID_DB_TYPES.join(', ')}` });
@@ -105,16 +126,28 @@ function dbConnectionRouter(config) {
105
126
  return res.status(404).json({ message: 'Project not found' });
106
127
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
107
128
  return res.status(403).json({ message: 'Unauthorized project access' });
108
- const credExists = project.credentials?.some((c) => c.id === credentialId);
109
- if (!credExists)
110
- return res.status(400).json({ message: 'Credential not found in project' });
129
+ if (integrationId) {
130
+ const UserIntegrationModel = (0, manager_1.getModel)('user_integrations');
131
+ const integration = await UserIntegrationModel.findOne({ id: integrationId, userId: auth.userId });
132
+ if (!integration)
133
+ return res.status(404).json({ message: 'Integration not found' });
134
+ if (integration.kind !== 'database')
135
+ return res.status(400).json({ message: 'Integration is not a database integration' });
136
+ }
137
+ else {
138
+ const credExists = project.credentials?.some((c) => c.id === credentialId);
139
+ if (!credExists)
140
+ return res.status(400).json({ message: 'Credential not found in project' });
141
+ }
111
142
  const connection = {
112
143
  id: new mongodb_1.ObjectId().toHexString(),
113
144
  name,
114
145
  dbType,
115
- credentialId,
146
+ credentialId: credentialId ?? '',
116
147
  projectId: req.params.id,
117
148
  createdAt: new Date(),
149
+ source: integrationId ? 'integration' : 'manual',
150
+ ...(integrationId && { integrationId }),
118
151
  ...(description !== undefined && { description }),
119
152
  };
120
153
  await Project.update({ _id: req.params.id }, { $push: { dbConnections: connection } });
@@ -127,9 +160,6 @@ function dbConnectionRouter(config) {
127
160
  // ─── Delete DB connection ─────────────────────────────────────────────────
128
161
  router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
129
162
  try {
130
- if (isBuiltinDb(req.params.connId, config)) {
131
- return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
132
- }
133
163
  const auth = req.auth;
134
164
  if (auth.role !== 'admin' && auth.role !== 'editor') {
135
165
  return res.status(403).json({ message: 'Permission denied' });
@@ -139,9 +169,12 @@ function dbConnectionRouter(config) {
139
169
  return res.status(404).json({ message: 'Project not found' });
140
170
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
141
171
  return res.status(403).json({ message: 'Unauthorized project access' });
142
- const exists = project.dbConnections?.some((c) => c.id === req.params.connId);
143
- if (!exists)
172
+ const conn = findDbConnection(project, req.params.connId);
173
+ if (!conn)
144
174
  return res.status(404).json({ message: 'DB connection not found' });
175
+ if (conn.source === 'builtin') {
176
+ return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
177
+ }
145
178
  await Project.update({ _id: req.params.id }, { $pull: { dbConnections: { id: req.params.connId } } });
146
179
  return res.status(200).json({ message: 'DB connection deleted successfully' });
147
180
  }
@@ -158,8 +191,15 @@ function dbConnectionRouter(config) {
158
191
  return res.status(404).json({ message: 'Project not found' });
159
192
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
160
193
  return res.status(403).json({ message: 'Unauthorized project access' });
161
- if (isBuiltinDb(req.params.connId, config)) {
162
- const internal = resolveBuiltinConnection(req.params.connId, config);
194
+ const conn = findDbConnection(project, req.params.connId);
195
+ if (!conn)
196
+ return res.status(404).json({ message: 'DB connection not found' });
197
+ if (conn.source === 'builtin') {
198
+ const builtinId = conn.integrationId;
199
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
200
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
201
+ }
202
+ const internal = resolveBuiltinConnection(builtinId, config);
163
203
  if ('error' in internal)
164
204
  return res.status(internal.status).json({ message: internal.error });
165
205
  const tables = await (0, dbConnection_1.listTablesInternal)(internal.client, internal.dbType, req.params.id);
@@ -190,8 +230,15 @@ function dbConnectionRouter(config) {
190
230
  const schema = req.body;
191
231
  if (!schema.tableName)
192
232
  return res.status(400).json({ message: 'tableName is required' });
193
- if (isBuiltinDb(req.params.connId, config)) {
194
- const internal = resolveBuiltinConnection(req.params.connId, config);
233
+ const conn = findDbConnection(project, req.params.connId);
234
+ if (!conn)
235
+ return res.status(404).json({ message: 'DB connection not found' });
236
+ if (conn.source === 'builtin') {
237
+ const builtinId = conn.integrationId;
238
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
239
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
240
+ }
241
+ const internal = resolveBuiltinConnection(builtinId, config);
195
242
  if ('error' in internal)
196
243
  return res.status(internal.status).json({ message: internal.error });
197
244
  if (internal.dbType === 'sql' && (!Array.isArray(schema.columns) || schema.columns.length === 0)) {
@@ -225,8 +272,15 @@ function dbConnectionRouter(config) {
225
272
  return res.status(404).json({ message: 'Project not found' });
226
273
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
227
274
  return res.status(403).json({ message: 'Unauthorized project access' });
228
- if (isBuiltinDb(req.params.connId, config)) {
229
- const internal = resolveBuiltinConnection(req.params.connId, config);
275
+ const conn = findDbConnection(project, req.params.connId);
276
+ if (!conn)
277
+ return res.status(404).json({ message: 'DB connection not found' });
278
+ if (conn.source === 'builtin') {
279
+ const builtinId = conn.integrationId;
280
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
281
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
282
+ }
283
+ const internal = resolveBuiltinConnection(builtinId, config);
230
284
  if ('error' in internal)
231
285
  return res.status(internal.status).json({ message: internal.error });
232
286
  await (0, dbConnection_1.deleteTableOrCollectionInternal)(internal.client, internal.dbType, req.params.tableName, req.params.id);
@@ -253,8 +307,15 @@ function dbConnectionRouter(config) {
253
307
  return res.status(403).json({ message: 'Unauthorized project access' });
254
308
  const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
255
309
  const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
256
- if (isBuiltinDb(req.params.connId, config)) {
257
- const internal = resolveBuiltinConnection(req.params.connId, config);
310
+ const conn = findDbConnection(project, req.params.connId);
311
+ if (!conn)
312
+ return res.status(404).json({ message: 'DB connection not found' });
313
+ if (conn.source === 'builtin') {
314
+ const builtinId = conn.integrationId;
315
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
316
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
317
+ }
318
+ const internal = resolveBuiltinConnection(builtinId, config);
258
319
  if ('error' in internal)
259
320
  return res.status(internal.status).json({ message: internal.error });
260
321
  const records = await (0, dbConnection_1.queryRecordsInternal)(internal.client, internal.dbType, req.params.tableName, limit, offset, req.params.id);
@@ -286,8 +347,15 @@ function dbConnectionRouter(config) {
286
347
  if (!record || typeof record !== 'object' || Array.isArray(record)) {
287
348
  return res.status(400).json({ message: 'Request body must be a JSON object' });
288
349
  }
289
- if (isBuiltinDb(req.params.connId, config)) {
290
- const internal = resolveBuiltinConnection(req.params.connId, config);
350
+ const conn = findDbConnection(project, req.params.connId);
351
+ if (!conn)
352
+ return res.status(404).json({ message: 'DB connection not found' });
353
+ if (conn.source === 'builtin') {
354
+ const builtinId = conn.integrationId;
355
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
356
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
357
+ }
358
+ const internal = resolveBuiltinConnection(builtinId, config);
291
359
  if ('error' in internal)
292
360
  return res.status(internal.status).json({ message: internal.error });
293
361
  const inserted = await (0, dbConnection_1.insertRecordInternal)(internal.client, internal.dbType, req.params.tableName, record, req.params.id);
@@ -318,8 +386,15 @@ function dbConnectionRouter(config) {
318
386
  if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
319
387
  return res.status(400).json({ message: 'Request body must be a JSON object' });
320
388
  }
321
- if (isBuiltinDb(req.params.connId, config)) {
322
- const internal = resolveBuiltinConnection(req.params.connId, config);
389
+ const conn = findDbConnection(project, req.params.connId);
390
+ if (!conn)
391
+ return res.status(404).json({ message: 'DB connection not found' });
392
+ if (conn.source === 'builtin') {
393
+ const builtinId = conn.integrationId;
394
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
395
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
396
+ }
397
+ const internal = resolveBuiltinConnection(builtinId, config);
323
398
  if ('error' in internal)
324
399
  return res.status(internal.status).json({ message: internal.error });
325
400
  const updated = await (0, dbConnection_1.updateRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, updates, req.params.id);
@@ -346,8 +421,15 @@ function dbConnectionRouter(config) {
346
421
  return res.status(404).json({ message: 'Project not found' });
347
422
  if (!(0, auth_1.isProjectMember)(project, auth.userId))
348
423
  return res.status(403).json({ message: 'Unauthorized project access' });
349
- if (isBuiltinDb(req.params.connId, config)) {
350
- const internal = resolveBuiltinConnection(req.params.connId, config);
424
+ const conn = findDbConnection(project, req.params.connId);
425
+ if (!conn)
426
+ return res.status(404).json({ message: 'DB connection not found' });
427
+ if (conn.source === 'builtin') {
428
+ const builtinId = conn.integrationId;
429
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
430
+ return res.status(403).json({ message: 'Access to this built-in database is not permitted' });
431
+ }
432
+ const internal = resolveBuiltinConnection(builtinId, config);
351
433
  if ('error' in internal)
352
434
  return res.status(internal.status).json({ message: internal.error });
353
435
  await (0, dbConnection_1.deleteRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, req.params.id);
@@ -7,6 +7,7 @@ const auth_1 = require("../../utils/auth");
7
7
  const connectionManager_1 = require("../../adapters/database/connectionManager");
8
8
  const sql_1 = require("../../adapters/database/sql");
9
9
  const sanitize_1 = require("../../utils/sanitize");
10
+ const projectIntegration_1 = require("../../services/projectIntegration");
10
11
  function projectRouter(config) {
11
12
  const router = (0, express_1.Router)();
12
13
  const Project = (0, manager_1.getModel)('projects');
@@ -74,7 +75,7 @@ function projectRouter(config) {
74
75
  if (auth.role !== 'admin' && auth.role !== 'editor') {
75
76
  return res.status(403).json({ message: 'Permission denied' });
76
77
  }
77
- const { name, description, image, allowedOrigin, public: isPublic = true, category } = req.body;
78
+ const { name, description, image, allowedOrigin, public: isPublic = true, category, integrationIds } = req.body;
78
79
  if (isPublic === false && req.subscription === 'freemium') {
79
80
  return res.status(403).json({ error: 'Private projects require a subscriber plan' });
80
81
  }
@@ -88,7 +89,22 @@ function projectRouter(config) {
88
89
  if (!user) {
89
90
  return res.status(404).json({ message: 'User not found' });
90
91
  }
91
- const newProject = await Project.create({
92
+ // Classify + revalidate every integrationId server-side — never trust a client-supplied
93
+ // builtin id. Any invalid id rejects the whole request (no project is created).
94
+ const dbConnections = [];
95
+ const storageConnections = [];
96
+ if (Array.isArray(integrationIds)) {
97
+ for (const id of integrationIds) {
98
+ const result = await (0, projectIntegration_1.classifyIntegrationId)(id, auth, config, '');
99
+ if ('error' in result)
100
+ return res.status(result.status).json({ message: result.error });
101
+ if (result.kind === 'database')
102
+ dbConnections.push(result.connection);
103
+ else
104
+ storageConnections.push(result.connection);
105
+ }
106
+ }
107
+ let newProject = await Project.create({
92
108
  dbType,
93
109
  name,
94
110
  description: description || '',
@@ -97,7 +113,18 @@ function projectRouter(config) {
97
113
  public: isPublic,
98
114
  category: category || null,
99
115
  members: [{ userId: auth.userId, username: user.username, role: "admin", status: "active", archived: false }],
116
+ dbConnections: [],
117
+ storageConnections: [],
100
118
  });
119
+ const newProjectId = newProject._id || newProject.id;
120
+ if (dbConnections.length || storageConnections.length) {
121
+ const stamped = await Project.update({ _id: newProjectId }, {
122
+ dbConnections: dbConnections.map(c => ({ ...c, projectId: newProjectId })),
123
+ storageConnections: storageConnections.map(c => ({ ...c, projectId: newProjectId })),
124
+ });
125
+ if (stamped)
126
+ newProject = stamped;
127
+ }
101
128
  const allProjects = await Project.find({});
102
129
  const projects = allProjects
103
130
  .filter(project => {
@@ -366,5 +393,68 @@ function projectRouter(config) {
366
393
  res.status(500).json({ message: 'Failed to update project origins', details: err.message });
367
394
  }
368
395
  });
396
+ // ─── Add an integration to an existing project ───────────────────────────
397
+ router.post('/projects/:id/integrations', async (req, res) => {
398
+ try {
399
+ const auth = req.auth;
400
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
401
+ return res.status(403).json({ message: 'Permission denied' });
402
+ }
403
+ const projectId = req.params.id;
404
+ const { integrationId } = req.body;
405
+ if (!integrationId)
406
+ return res.status(400).json({ message: 'integrationId is required' });
407
+ const project = await Project.findOne({ _id: projectId });
408
+ if (!project)
409
+ return res.status(404).json({ message: 'Project not found' });
410
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
411
+ return res.status(403).json({ message: 'Unauthorized project access' });
412
+ const alreadyConnected = [...(project.dbConnections ?? []), ...(project.storageConnections ?? [])]
413
+ .some((c) => c.integrationId === integrationId);
414
+ if (alreadyConnected)
415
+ return res.status(400).json({ message: 'This integration is already connected to the project' });
416
+ const result = await (0, projectIntegration_1.classifyIntegrationId)(integrationId, auth, config, projectId);
417
+ if ('error' in result)
418
+ return res.status(result.status).json({ message: result.error });
419
+ const updated = result.kind === 'database'
420
+ ? await Project.update({ _id: projectId }, { $push: { dbConnections: result.connection } })
421
+ : await Project.update({ _id: projectId }, { $push: { storageConnections: result.connection } });
422
+ if (!updated)
423
+ return res.status(404).json({ message: 'Project not found or not updated' });
424
+ res.status(201).json({ data: result.connection, project: (0, sanitize_1.sanitizeProject)(updated) });
425
+ }
426
+ catch (err) {
427
+ res.status(500).json({ message: 'Failed to add integration', details: err.message });
428
+ }
429
+ });
430
+ // ─── Remove an integration from a project ────────────────────────────────
431
+ router.delete('/projects/:id/integrations/:integrationId', async (req, res) => {
432
+ try {
433
+ const auth = req.auth;
434
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
435
+ return res.status(403).json({ message: 'Permission denied' });
436
+ }
437
+ const projectId = req.params.id;
438
+ const { integrationId } = req.params;
439
+ const project = await Project.findOne({ _id: projectId });
440
+ if (!project)
441
+ return res.status(404).json({ message: 'Project not found' });
442
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
443
+ return res.status(403).json({ message: 'Unauthorized project access' });
444
+ const dbConn = (project.dbConnections ?? []).find((c) => c.integrationId === integrationId);
445
+ const storageConn = (project.storageConnections ?? []).find((c) => c.integrationId === integrationId);
446
+ if (!dbConn && !storageConn)
447
+ return res.status(404).json({ message: 'Integration not connected to this project' });
448
+ const updated = dbConn
449
+ ? await Project.update({ _id: projectId }, { $pull: { dbConnections: { id: dbConn.id } } })
450
+ : await Project.update({ _id: projectId }, { $pull: { storageConnections: { id: storageConn.id } } });
451
+ if (!updated)
452
+ return res.status(404).json({ message: 'Project not found or not updated' });
453
+ res.status(200).json({ message: 'Integration removed', project: (0, sanitize_1.sanitizeProject)(updated) });
454
+ }
455
+ catch (err) {
456
+ res.status(500).json({ message: 'Failed to remove integration', details: err.message });
457
+ }
458
+ });
369
459
  return router;
370
460
  }