streamby-core 0.26.10 → 0.27.1
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/README.md +283 -283
- package/dist/adapters/database/connectionManager.js +71 -44
- package/dist/adapters/database/sql.js +22 -22
- package/dist/middleware/createRouter.js +4 -0
- package/dist/middleware/routes/dbConnection.d.ts +3 -0
- package/dist/middleware/routes/dbConnection.js +367 -0
- package/dist/middleware/routes/export.js +35 -11
- package/dist/middleware/routes/project.js +6 -6
- package/dist/middleware/routes/storage.js +179 -0
- package/dist/middleware/routes/storageConnection.d.ts +3 -0
- package/dist/middleware/routes/storageConnection.js +108 -0
- package/dist/models/Model.d.ts +1 -0
- package/dist/models/Model.js +35 -9
- package/dist/services/dbConnection.d.ts +18 -0
- package/dist/services/dbConnection.js +304 -0
- package/dist/services/export.d.ts +2 -2
- package/dist/services/export.js +71 -23
- package/dist/services/pipeline.js +80 -10
- package/dist/types/index.d.ts +37 -3
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.js +32 -0
- package/package.json +63 -63
|
@@ -168,8 +168,11 @@ function exportRouter(config) {
|
|
|
168
168
|
let responseData;
|
|
169
169
|
responseData = {
|
|
170
170
|
...data,
|
|
171
|
+
id: exportId,
|
|
171
172
|
allowedOrigin: exportMetadata.allowedOrigin,
|
|
172
173
|
private: exportMetadata.private,
|
|
174
|
+
devMode: exportMetadata.devMode,
|
|
175
|
+
devPorts: exportMetadata.devPorts,
|
|
173
176
|
};
|
|
174
177
|
res.json({
|
|
175
178
|
data: responseData,
|
|
@@ -189,7 +192,7 @@ function exportRouter(config) {
|
|
|
189
192
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
190
193
|
}
|
|
191
194
|
const projectId = req.params.id;
|
|
192
|
-
const { name, description, isPrivate, allowedOrigin, useConnections, useCredentials, nodeSchema } = req.body;
|
|
195
|
+
const { name, description, isPrivate, allowedOrigin, useConnections, useCredentials, nodeSchema, storageDbId } = req.body;
|
|
193
196
|
if (!name) {
|
|
194
197
|
return res.status(400).json({ message: 'Missing export name' });
|
|
195
198
|
}
|
|
@@ -199,7 +202,9 @@ function exportRouter(config) {
|
|
|
199
202
|
if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
200
203
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
201
204
|
}
|
|
202
|
-
const
|
|
205
|
+
const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
|
|
206
|
+
const dbType = mainDb?.type ?? 'nosql';
|
|
207
|
+
const result = await (0, export_1.createExport)(config, projectId, description, name, dbType, exportType, isPrivate, allowedOrigin, resolvedNodeSchema, useConnections, useCredentials, storageDbId);
|
|
203
208
|
res.status(201).json({ data: result, message: result.message });
|
|
204
209
|
}
|
|
205
210
|
catch (err) {
|
|
@@ -213,7 +218,7 @@ function exportRouter(config) {
|
|
|
213
218
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
214
219
|
}
|
|
215
220
|
const { id: projectId, export_id: exportId } = req.params;
|
|
216
|
-
const { name, description, isPrivate, allowedOrigin, useConnections, useCredentials, nodeSchema } = req.body;
|
|
221
|
+
const { name, description, isPrivate, allowedOrigin, devMode, devPorts, useConnections, useCredentials, nodeSchema } = req.body;
|
|
217
222
|
const project = await Project.findOne({ _id: projectId });
|
|
218
223
|
if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
|
|
219
224
|
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
@@ -226,7 +231,7 @@ function exportRouter(config) {
|
|
|
226
231
|
const resolvedNodeSchema = nodeSchema ?? exportMetadata.nodeSchema;
|
|
227
232
|
const resolvedUseConnections = useConnections ?? exportMetadata.useConnections;
|
|
228
233
|
const exportType = resolvedUseConnections ? 'externalApi' : 'json';
|
|
229
|
-
const result = await (0, export_1.updateExport)(config, projectId, exportId, description ?? exportMetadata.description, resolvedName, project.dbType, exportType, isPrivate ?? exportMetadata.private, allowedOrigin ?? exportMetadata.allowedOrigin, resolvedNodeSchema, resolvedUseConnections, useCredentials ?? exportMetadata.useCredentials);
|
|
234
|
+
const result = await (0, export_1.updateExport)(config, projectId, exportId, description ?? exportMetadata.description, resolvedName, project.dbType, exportType, isPrivate ?? exportMetadata.private, allowedOrigin ?? exportMetadata.allowedOrigin, devMode ?? exportMetadata.devMode, devPorts ?? exportMetadata.devPorts, resolvedNodeSchema, resolvedUseConnections, useCredentials ?? exportMetadata.useCredentials);
|
|
230
235
|
res.status(200).json({ data: result, message: result.message });
|
|
231
236
|
}
|
|
232
237
|
catch (err) {
|
|
@@ -282,14 +287,33 @@ function exportRouter(config) {
|
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
// Dev mode: auto-allow localhost requests on configured ports.
|
|
291
|
+
let devModeAllowed = false;
|
|
292
|
+
if (exportMetadata.devMode && origin) {
|
|
293
|
+
const match = origin.match(/^https?:\/\/localhost:(\d+)$/);
|
|
294
|
+
if (match) {
|
|
295
|
+
const port = parseInt(match[1]);
|
|
296
|
+
const allowedPorts = exportMetadata.devPorts?.length
|
|
297
|
+
? exportMetadata.devPorts
|
|
298
|
+
: [3000, 5173, 8080, 4200];
|
|
299
|
+
if (allowedPorts.includes(port)) {
|
|
300
|
+
devModeAllowed = true;
|
|
301
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
302
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
303
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
288
306
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
if (!devModeAllowed) {
|
|
308
|
+
// If there's no origin header, deny access unless the effective scope is public.
|
|
309
|
+
if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
|
|
310
|
+
return res.status(403).json({ message: 'Origin header required' });
|
|
311
|
+
}
|
|
312
|
+
// Check for public access ('*') or if the request's origin is in the effective list.
|
|
313
|
+
const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
|
|
314
|
+
if (!isAllowed) {
|
|
315
|
+
return res.status(403).json({ message: 'Unauthorized' });
|
|
316
|
+
}
|
|
293
317
|
}
|
|
294
318
|
let data;
|
|
295
319
|
if (exportMetadata.nodeSchema) {
|
|
@@ -50,19 +50,19 @@ function projectRouter(config) {
|
|
|
50
50
|
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
51
51
|
return res.status(403).json({ message: 'Permission denied' });
|
|
52
52
|
}
|
|
53
|
-
const { name, description,
|
|
54
|
-
const mainDb = config.databases?.find(db => db.main);
|
|
53
|
+
const { name, description, image, allowedOrigin } = req.body;
|
|
54
|
+
const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
|
|
55
55
|
if (!mainDb) {
|
|
56
|
-
return res.status(500).json({ message: '
|
|
56
|
+
return res.status(500).json({ message: 'No database configured' });
|
|
57
57
|
}
|
|
58
|
-
const
|
|
59
|
-
const User = (0, manager_1.getModel)('users',
|
|
58
|
+
const dbType = mainDb.type;
|
|
59
|
+
const User = (0, manager_1.getModel)('users', dbType);
|
|
60
60
|
const user = await User.findOne({ _id: auth.userId });
|
|
61
61
|
if (!user) {
|
|
62
62
|
return res.status(404).json({ message: 'User not found' });
|
|
63
63
|
}
|
|
64
64
|
const newProject = await Project.create({
|
|
65
|
-
dbType
|
|
65
|
+
dbType,
|
|
66
66
|
name,
|
|
67
67
|
description: description || '',
|
|
68
68
|
image: image || '',
|
|
@@ -8,9 +8,11 @@ const express_1 = require("express");
|
|
|
8
8
|
const file_1 = require("../../services/file");
|
|
9
9
|
const presign_1 = require("../../services/presign");
|
|
10
10
|
const storage_1 = require("../../providers/storage");
|
|
11
|
+
const s3_1 = require("../../adapters/storage/s3");
|
|
11
12
|
const manager_1 = require("../../models/manager");
|
|
12
13
|
const connectionManager_1 = require("../../adapters/database/connectionManager");
|
|
13
14
|
const auth_1 = require("../../utils/auth");
|
|
15
|
+
const encryption_1 = require("../../utils/encryption");
|
|
14
16
|
const mongodb_1 = require("mongodb");
|
|
15
17
|
const crypto_1 = __importDefault(require("crypto"));
|
|
16
18
|
const VALID_CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
@@ -36,6 +38,32 @@ function getRawFilesCollection() {
|
|
|
36
38
|
return null;
|
|
37
39
|
return (0, connectionManager_1.getConnection)(activeId).client.db().collection('storage_files');
|
|
38
40
|
}
|
|
41
|
+
function connFilter(projectId, connId, extra) {
|
|
42
|
+
const isBuiltin = connId === 'builtin' || connId.startsWith('builtin-');
|
|
43
|
+
const connMatch = isBuiltin
|
|
44
|
+
? { $or: [{ storageConnectionId: connId }, { storageConnectionId: { $exists: false } }] }
|
|
45
|
+
: { storageConnectionId: connId };
|
|
46
|
+
return { projectId, ...connMatch, ...(extra ?? {}) };
|
|
47
|
+
}
|
|
48
|
+
async function resolveConnAdapter(connId, project, globalAdapter) {
|
|
49
|
+
if (connId === 'builtin' || connId.startsWith('builtin-'))
|
|
50
|
+
return globalAdapter;
|
|
51
|
+
const conn = project.storageConnections?.find((c) => c.id === connId);
|
|
52
|
+
if (!conn)
|
|
53
|
+
return { error: 'Storage connection not found', status: 404 };
|
|
54
|
+
if (!(0, encryption_1.isEncryptionKeySet)())
|
|
55
|
+
return { error: 'Encryption key not set', status: 500 };
|
|
56
|
+
const credential = project.credentials?.find((c) => c.id === conn.credentialId);
|
|
57
|
+
if (!credential)
|
|
58
|
+
return { error: 'Credential not found in project', status: 400 };
|
|
59
|
+
try {
|
|
60
|
+
const s3Config = JSON.parse((0, encryption_1.decrypt)(credential.encryptedValue));
|
|
61
|
+
return new s3_1.S3Adapter(s3Config);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
39
67
|
function storageRouter(config) {
|
|
40
68
|
const router = (0, express_1.Router)();
|
|
41
69
|
const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
|
|
@@ -212,6 +240,157 @@ function storageRouter(config) {
|
|
|
212
240
|
res.status(500).json({ message: 'Failed to list files', details: err.message });
|
|
213
241
|
}
|
|
214
242
|
});
|
|
243
|
+
// ─── Per-connection file routes ───────────────────────────────────────────
|
|
244
|
+
router.get('/projects/:projectId/connections/storage/:connId/upload-url', async (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const auth = req.auth;
|
|
247
|
+
const { projectId, connId } = req.params;
|
|
248
|
+
const { fileName, contentType, category } = req.query;
|
|
249
|
+
if (!category || !VALID_CATEGORIES.includes(category)) {
|
|
250
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
251
|
+
}
|
|
252
|
+
if (!fileName || !contentType) {
|
|
253
|
+
return res.status(400).json({ message: 'Query params fileName and contentType are required' });
|
|
254
|
+
}
|
|
255
|
+
if (!validateContentType(contentType, fileName, category)) {
|
|
256
|
+
return res.status(400).json({ message: `File does not match category ${category}` });
|
|
257
|
+
}
|
|
258
|
+
const project = await Project.findOne({ _id: projectId });
|
|
259
|
+
if (!project)
|
|
260
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
261
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
262
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
263
|
+
const connAdapter = await resolveConnAdapter(connId, project, adapter);
|
|
264
|
+
if ('error' in connAdapter)
|
|
265
|
+
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
266
|
+
if (!connAdapter.getPresignedUploadUrl)
|
|
267
|
+
return res.status(501).json({ message: 'Storage adapter does not support presigned uploads' });
|
|
268
|
+
const ext = fileName.includes('.') ? `.${fileName.split('.').pop().toLowerCase()}` : '';
|
|
269
|
+
const fileId = crypto_1.default.randomUUID();
|
|
270
|
+
const storageKey = `${projectId}/${category}/${fileId}${ext}`;
|
|
271
|
+
const collection = getRawFilesCollection();
|
|
272
|
+
if (collection) {
|
|
273
|
+
await collection.insertOne({
|
|
274
|
+
_id: new mongodb_1.ObjectId(),
|
|
275
|
+
fileId,
|
|
276
|
+
projectId,
|
|
277
|
+
storageConnectionId: connId,
|
|
278
|
+
category,
|
|
279
|
+
storageKey,
|
|
280
|
+
displayName: fileName,
|
|
281
|
+
contentType,
|
|
282
|
+
uploadedBy: auth.userId,
|
|
283
|
+
createdAt: new Date(),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const url = await connAdapter.getPresignedUploadUrl(storageKey, contentType);
|
|
287
|
+
res.json({ url, fileId, storageKey, displayName: fileName });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
res.status(500).json({ message: 'Failed to generate upload URL', details: err.message });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
router.get('/projects/:projectId/connections/storage/:connId/files/lasts', async (req, res) => {
|
|
294
|
+
try {
|
|
295
|
+
const auth = req.auth;
|
|
296
|
+
const { projectId, connId } = req.params;
|
|
297
|
+
const project = await Project.findOne({ _id: projectId });
|
|
298
|
+
if (!project)
|
|
299
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
300
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
301
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
302
|
+
const connAdapter = await resolveConnAdapter(connId, project, adapter);
|
|
303
|
+
if ('error' in connAdapter)
|
|
304
|
+
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
305
|
+
const collection = getRawFilesCollection();
|
|
306
|
+
if (!collection)
|
|
307
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
308
|
+
const files = await collection.find(connFilter(projectId, connId)).sort({ createdAt: -1 }).limit(12).toArray();
|
|
309
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
310
|
+
res.json({ data });
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
res.status(500).json({ message: 'Failed to fetch latest files', details: err.message });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
router.get('/projects/:projectId/connections/storage/:connId/files/:category', async (req, res) => {
|
|
317
|
+
try {
|
|
318
|
+
const auth = req.auth;
|
|
319
|
+
const { projectId, connId, category } = req.params;
|
|
320
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
321
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
322
|
+
}
|
|
323
|
+
const project = await Project.findOne({ _id: projectId });
|
|
324
|
+
if (!project)
|
|
325
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
326
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
327
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
328
|
+
const connAdapter = await resolveConnAdapter(connId, project, adapter);
|
|
329
|
+
if ('error' in connAdapter)
|
|
330
|
+
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
331
|
+
const collection = getRawFilesCollection();
|
|
332
|
+
if (!collection)
|
|
333
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
334
|
+
const files = await collection.find(connFilter(projectId, connId, { category })).sort({ createdAt: -1 }).toArray();
|
|
335
|
+
const data = await Promise.all(files.map(f => resolveFileUrl(f, connAdapter)));
|
|
336
|
+
res.json({ data });
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
res.status(500).json({ message: 'Failed to list files', details: err.message });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
router.patch('/projects/:projectId/connections/storage/:connId/files/:fileId', async (req, res) => {
|
|
343
|
+
try {
|
|
344
|
+
const auth = req.auth;
|
|
345
|
+
const { projectId, connId, fileId } = req.params;
|
|
346
|
+
const { displayName } = req.body;
|
|
347
|
+
if (!displayName || typeof displayName !== 'string') {
|
|
348
|
+
return res.status(400).json({ message: 'displayName is required' });
|
|
349
|
+
}
|
|
350
|
+
const project = await Project.findOne({ _id: projectId });
|
|
351
|
+
if (!project)
|
|
352
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
353
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
354
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
355
|
+
const collection = getRawFilesCollection();
|
|
356
|
+
if (!collection)
|
|
357
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
358
|
+
const result = await collection.findOneAndUpdate(connFilter(projectId, connId, { fileId }), { $set: { displayName } }, { returnDocument: 'after' });
|
|
359
|
+
if (!result)
|
|
360
|
+
return res.status(404).json({ message: 'File not found' });
|
|
361
|
+
res.json({ message: 'File renamed successfully', file: result });
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
res.status(500).json({ message: 'Failed to rename file', details: err.message });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
router.delete('/projects/:projectId/connections/storage/:connId/files/:fileId', async (req, res) => {
|
|
368
|
+
try {
|
|
369
|
+
const auth = req.auth;
|
|
370
|
+
const { projectId, connId, fileId } = req.params;
|
|
371
|
+
const project = await Project.findOne({ _id: projectId });
|
|
372
|
+
if (!project)
|
|
373
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
374
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
375
|
+
return res.status(403).json({ message: 'Unauthorized access' });
|
|
376
|
+
const connAdapter = await resolveConnAdapter(connId, project, adapter);
|
|
377
|
+
if ('error' in connAdapter)
|
|
378
|
+
return res.status(connAdapter.status).json({ message: connAdapter.error });
|
|
379
|
+
const collection = getRawFilesCollection();
|
|
380
|
+
if (!collection)
|
|
381
|
+
return res.status(500).json({ message: 'Database not available' });
|
|
382
|
+
const fileDoc = await collection.findOne(connFilter(projectId, connId, { fileId }));
|
|
383
|
+
if (!fileDoc)
|
|
384
|
+
return res.status(404).json({ message: 'File not found' });
|
|
385
|
+
if (connAdapter.deleteFile)
|
|
386
|
+
await connAdapter.deleteFile(fileDoc.storageKey);
|
|
387
|
+
await collection.deleteOne(connFilter(projectId, connId, { fileId }));
|
|
388
|
+
res.json({ message: 'File deleted successfully' });
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
res.status(500).json({ message: 'Failed to delete file', details: err.message });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
215
394
|
router.get('/storages', async (_req, res) => {
|
|
216
395
|
try {
|
|
217
396
|
const STORAGE_DISPLAY = {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.storageConnectionRouter = storageConnectionRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const mongodb_1 = require("mongodb");
|
|
6
|
+
const manager_1 = require("../../models/manager");
|
|
7
|
+
const auth_1 = require("../../utils/auth");
|
|
8
|
+
const VALID_STORAGE_TYPES = ['s3', 'gcs', 'r2', 'azure'];
|
|
9
|
+
const STORAGE_DISPLAY = {
|
|
10
|
+
s3: 'AWS S3',
|
|
11
|
+
gcs: 'Google Cloud Storage',
|
|
12
|
+
r2: 'Cloudflare R2',
|
|
13
|
+
azure: 'Azure Blob Storage',
|
|
14
|
+
};
|
|
15
|
+
function storageConnectionRouter(config) {
|
|
16
|
+
const router = (0, express_1.Router)();
|
|
17
|
+
const Project = (0, manager_1.getModel)('projects');
|
|
18
|
+
// ─── List storage connections ─────────────────────────────────────────────
|
|
19
|
+
router.get('/projects/:id/connections/storage', async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const auth = req.auth;
|
|
22
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
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 project access' });
|
|
27
|
+
const builtinConnections = (config.storageProviders || []).map((provider, i) => ({
|
|
28
|
+
id: i === 0 ? 'builtin' : `builtin-${i}`,
|
|
29
|
+
name: STORAGE_DISPLAY[provider.type] || provider.type,
|
|
30
|
+
type: provider.type,
|
|
31
|
+
credentialId: '',
|
|
32
|
+
projectId: req.params.id,
|
|
33
|
+
createdAt: new Date(0),
|
|
34
|
+
isBuiltin: true,
|
|
35
|
+
}));
|
|
36
|
+
const projectConnections = project.storageConnections || [];
|
|
37
|
+
return res.status(200).json({ data: [...builtinConnections, ...projectConnections] });
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
res.status(500).json({ message: 'Failed to fetch storage connections', details: err.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// ─── Add storage connection ───────────────────────────────────────────────
|
|
44
|
+
router.post('/projects/:id/connections/storage', async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const auth = req.auth;
|
|
47
|
+
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
48
|
+
return res.status(403).json({ message: 'Permission denied' });
|
|
49
|
+
}
|
|
50
|
+
const { name, type, credentialId, description } = req.body;
|
|
51
|
+
if (!name || !type || !credentialId) {
|
|
52
|
+
return res.status(400).json({ message: 'name, type, and credentialId are required' });
|
|
53
|
+
}
|
|
54
|
+
if (!VALID_STORAGE_TYPES.includes(type)) {
|
|
55
|
+
return res.status(400).json({ message: `type must be one of: ${VALID_STORAGE_TYPES.join(', ')}` });
|
|
56
|
+
}
|
|
57
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
58
|
+
if (!project)
|
|
59
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
60
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
61
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
62
|
+
const credExists = project.credentials?.some((c) => c.id === credentialId);
|
|
63
|
+
if (!credExists)
|
|
64
|
+
return res.status(400).json({ message: 'Credential not found in project' });
|
|
65
|
+
const connection = {
|
|
66
|
+
id: new mongodb_1.ObjectId().toHexString(),
|
|
67
|
+
name,
|
|
68
|
+
type,
|
|
69
|
+
credentialId,
|
|
70
|
+
projectId: req.params.id,
|
|
71
|
+
createdAt: new Date(),
|
|
72
|
+
...(description !== undefined && { description }),
|
|
73
|
+
};
|
|
74
|
+
await Project.update({ _id: req.params.id }, { $push: { storageConnections: connection } });
|
|
75
|
+
return res.status(201).json({ data: connection });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
res.status(500).json({ message: 'Failed to add storage connection', details: err.message });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// ─── Delete storage connection ────────────────────────────────────────────
|
|
82
|
+
router.delete('/projects/:id/connections/storage/:connId', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const { connId } = req.params;
|
|
85
|
+
if (connId === 'builtin' || connId.startsWith('builtin-')) {
|
|
86
|
+
return res.status(403).json({ message: 'Cannot delete a built-in storage connection' });
|
|
87
|
+
}
|
|
88
|
+
const auth = req.auth;
|
|
89
|
+
if (auth.role !== 'admin' && auth.role !== 'editor') {
|
|
90
|
+
return res.status(403).json({ message: 'Permission denied' });
|
|
91
|
+
}
|
|
92
|
+
const project = await Project.findOne({ _id: req.params.id });
|
|
93
|
+
if (!project)
|
|
94
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
95
|
+
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
96
|
+
return res.status(403).json({ message: 'Unauthorized project access' });
|
|
97
|
+
const exists = project.storageConnections?.some((c) => c.id === connId);
|
|
98
|
+
if (!exists)
|
|
99
|
+
return res.status(404).json({ message: 'Storage connection not found' });
|
|
100
|
+
await Project.update({ _id: req.params.id }, { $pull: { storageConnections: { id: connId } } });
|
|
101
|
+
return res.status(200).json({ message: 'Storage connection deleted successfully' });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
res.status(500).json({ message: 'Failed to delete storage connection', details: err.message });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return router;
|
|
108
|
+
}
|
package/dist/models/Model.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class Model<T extends Document> {
|
|
|
11
11
|
private transformResult;
|
|
12
12
|
findOne(filter: any): Promise<T | null>;
|
|
13
13
|
create(data: T): Promise<T>;
|
|
14
|
+
private updateSqlJsonbArray;
|
|
14
15
|
update(filter: any, data: Partial<T>): Promise<T | null>;
|
|
15
16
|
delete(filter: any): Promise<number>;
|
|
16
17
|
}
|
package/dist/models/Model.js
CHANGED
|
@@ -45,19 +45,20 @@ 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.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 => ({
|
|
56
56
|
...row,
|
|
57
57
|
members: [{
|
|
58
58
|
userId: row.memberUserId,
|
|
59
|
+
role: row.memberRole,
|
|
60
|
+
status: row.memberStatus,
|
|
59
61
|
archived: row.memberArchived,
|
|
60
|
-
// Add other member fields if necessary
|
|
61
62
|
}]
|
|
62
63
|
}));
|
|
63
64
|
}
|
|
@@ -72,7 +73,8 @@ class Model {
|
|
|
72
73
|
const projectMembers = await sql_1.sqlAdapter.find(connection, 'project_members', { projectId: transformedItem.id }, this.schema);
|
|
73
74
|
transformedItem.members = projectMembers.map((member) => ({
|
|
74
75
|
userId: member.userId,
|
|
75
|
-
role: member.role,
|
|
76
|
+
role: member.role,
|
|
77
|
+
status: member.status,
|
|
76
78
|
archived: member.archived,
|
|
77
79
|
}));
|
|
78
80
|
}
|
|
@@ -122,7 +124,8 @@ class Model {
|
|
|
122
124
|
const projectMembers = await sql_1.sqlAdapter.find(connection, 'project_members', { projectId: transformedResult.id }, this.schema);
|
|
123
125
|
transformedResult.members = projectMembers.map((member) => ({
|
|
124
126
|
userId: member.userId,
|
|
125
|
-
role: member.role,
|
|
127
|
+
role: member.role,
|
|
128
|
+
status: member.status,
|
|
126
129
|
archived: member.archived,
|
|
127
130
|
}));
|
|
128
131
|
}
|
|
@@ -195,6 +198,22 @@ class Model {
|
|
|
195
198
|
}
|
|
196
199
|
return data;
|
|
197
200
|
}
|
|
201
|
+
async updateSqlJsonbArray(connection, id, op, field, value) {
|
|
202
|
+
const schema = this.schema || 'streamby';
|
|
203
|
+
const tbl = `"${schema}"."${this.tableName}"`;
|
|
204
|
+
if (op === '$push') {
|
|
205
|
+
await connection.query(`UPDATE ${tbl} SET "${field}" = "${field}" || $1::jsonb WHERE id = $2`, [JSON.stringify([value]), id]);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
|
|
209
|
+
const matchVal = String(value[matchKey]);
|
|
210
|
+
await connection.query(`UPDATE ${tbl} SET "${field}" = (
|
|
211
|
+
SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
|
|
212
|
+
FROM jsonb_array_elements("${field}") elem
|
|
213
|
+
WHERE elem->>'${matchKey}' != $1
|
|
214
|
+
) WHERE id = $2`, [matchVal, id]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
198
217
|
async update(filter, data) {
|
|
199
218
|
const existingProject = await this.findOne(filter); // Find the project first
|
|
200
219
|
if (!existingProject) {
|
|
@@ -270,8 +289,15 @@ class Model {
|
|
|
270
289
|
}
|
|
271
290
|
else {
|
|
272
291
|
// General update logic
|
|
273
|
-
// Use the correctly constructed updateFilter
|
|
274
292
|
if (dbType === 'sql') {
|
|
293
|
+
// Detect MongoDB-style array operators ($push / $pull) and map to JSONB operations
|
|
294
|
+
const op = Object.keys(data).find(k => k === '$push' || k === '$pull');
|
|
295
|
+
if (op) {
|
|
296
|
+
const field = Object.keys(data[op])[0];
|
|
297
|
+
const value = data[op][field];
|
|
298
|
+
await this.updateSqlJsonbArray(connection, existingProject.id, op, field, value);
|
|
299
|
+
return await this.findOne({ _id: existingProject.id });
|
|
300
|
+
}
|
|
275
301
|
const result = await sql_1.sqlAdapter.update(connection, this.tableName, updateFilter, data, this.schema);
|
|
276
302
|
if (result)
|
|
277
303
|
return result;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { MongoClient } from 'mongodb';
|
|
3
|
+
import { ExternalDbType, CreateTableSchema } from '../types';
|
|
4
|
+
export declare function listTablesOrCollections(connectionString: string, dbType: ExternalDbType): Promise<string[]>;
|
|
5
|
+
export declare function createTableOrCollection(connectionString: string, dbType: ExternalDbType, schema: CreateTableSchema): Promise<void>;
|
|
6
|
+
export declare function queryRecords(connectionString: string, dbType: ExternalDbType, tableName: string, limit?: number, offset?: number): Promise<any[]>;
|
|
7
|
+
export declare function insertRecord(connectionString: string, dbType: ExternalDbType, tableName: string, record: Record<string, unknown>): Promise<any>;
|
|
8
|
+
export declare function updateRecord(connectionString: string, dbType: ExternalDbType, tableName: string, recordId: string, updates: Record<string, unknown>): Promise<any | null>;
|
|
9
|
+
export declare function deleteRecord(connectionString: string, dbType: ExternalDbType, tableName: string, recordId: string): Promise<boolean>;
|
|
10
|
+
export declare function listTablesInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', projectId?: string): Promise<string[]>;
|
|
11
|
+
export declare function queryRecordsInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, limit?: number, offset?: number, projectId?: string): Promise<any[]>;
|
|
12
|
+
export declare function queryRecordByIdInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, recordId: string, projectId?: string): Promise<any | null>;
|
|
13
|
+
export declare function createTableOrCollectionInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', schema: CreateTableSchema, projectId?: string): Promise<void>;
|
|
14
|
+
export declare function insertRecordInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, record: Record<string, unknown>, projectId?: string): Promise<any>;
|
|
15
|
+
export declare function updateRecordInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, recordId: string, updates: Record<string, unknown>, projectId?: string): Promise<any | null>;
|
|
16
|
+
export declare function deleteRecordInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, recordId: string, projectId?: string): Promise<boolean>;
|
|
17
|
+
export declare function deleteTableOrCollection(connectionString: string, dbType: ExternalDbType, tableName: string): Promise<void>;
|
|
18
|
+
export declare function deleteTableOrCollectionInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, projectId?: string): Promise<void>;
|