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.
@@ -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 result = await (0, export_1.createExport)(config, projectId, description, name, project.dbType, exportType, isPrivate, allowedOrigin, resolvedNodeSchema, useConnections, useCredentials);
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
- // If there's no origin header, deny access unless the effective scope is public.
286
- if (!origin && !(effectiveAllowedOrigins && effectiveAllowedOrigins.includes('*'))) {
287
- return res.status(403).json({ message: 'Origin header required' });
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
- // Check for public access ('*') or if the request's origin is in the effective list.
290
- const isAllowed = effectiveAllowedOrigins && (effectiveAllowedOrigins.includes('*') || (origin && effectiveAllowedOrigins.includes(origin)));
291
- if (!isAllowed) {
292
- return res.status(403).json({ message: 'Unauthorized' });
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, dbType, image, allowedOrigin } = req.body;
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: 'Main database not configured' });
56
+ return res.status(500).json({ message: 'No database configured' });
57
57
  }
58
- const userDbType = mainDb.type;
59
- const User = (0, manager_1.getModel)('users', userDbType);
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: dbType || 'nosql',
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,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function storageConnectionRouter(config: StreamByConfig): Router;
@@ -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
+ }
@@ -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
  }
@@ -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, // Assuming role is also stored in project_members
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, // Assuming role is also stored in project_members
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>;