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.
- package/dist/adapters/database/connectionManager.js +27 -7
- package/dist/middleware/createRouter.js +8 -0
- package/dist/middleware/routes/dbConnection.js +115 -33
- package/dist/middleware/routes/project.js +92 -2
- package/dist/middleware/routes/storage.js +84 -49
- package/dist/middleware/routes/storageConnection.js +40 -28
- package/dist/middleware/routes/userIntegration.d.ts +3 -0
- package/dist/middleware/routes/userIntegration.js +93 -0
- package/dist/services/backfillBuiltinConnections.d.ts +2 -0
- package/dist/services/backfillBuiltinConnections.js +57 -0
- package/dist/services/projectIntegration.d.ts +13 -0
- package/dist/services/projectIntegration.js +63 -0
- package/dist/services/userIntegration.d.ts +17 -0
- package/dist/services/userIntegration.js +86 -0
- package/dist/types/index.d.ts +22 -4
- package/dist/utils/builtinAccess.d.ts +6 -0
- package/dist/utils/builtinAccess.js +32 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +7 -0
- package/package.json +1 -1
|
@@ -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"
|
|
77
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports
|
|
78
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS credentials
|
|
79
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "apiConnections"
|
|
80
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections"
|
|
81
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS
|
|
82
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 '${
|
|
51
|
+
return { error: `Database '${builtinId}' not found in config`, status: 404 };
|
|
51
52
|
try {
|
|
52
|
-
const { client, type } = (0, connectionManager_1.getConnection)(
|
|
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
|
|
98
|
-
return res.status(400).json({ message: 'name
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
143
|
-
if (!
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
}
|