streamby-core 0.26.10 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,58 +43,81 @@ const getConnection = (id) => {
43
43
  exports.getConnection = getConnection;
44
44
  const ensureTablesExist = async (pool) => {
45
45
  try {
46
- await pool.query(`
47
- CREATE SCHEMA IF NOT EXISTS streamby;
48
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
49
- CREATE TABLE IF NOT EXISTS streamby.projects (
50
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
51
- name VARCHAR(255) NOT NULL,
52
- description TEXT,
53
- image TEXT,
54
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
55
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
56
- );
46
+ await pool.query(`
47
+ CREATE SCHEMA IF NOT EXISTS streamby;
48
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
49
+ `);
50
+ // Migrate id column from UUID to TEXT if it was created with the old schema
51
+ await pool.query(`
52
+ DO $$ BEGIN
53
+ ALTER TABLE streamby.projects ALTER COLUMN id TYPE TEXT USING id::text;
54
+ ALTER TABLE streamby.projects ALTER COLUMN id SET DEFAULT uuid_generate_v4()::text;
55
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
56
+ `);
57
+ await pool.query(`
58
+ CREATE TABLE IF NOT EXISTS streamby.projects (
59
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
60
+ name VARCHAR(255) NOT NULL,
61
+ description TEXT,
62
+ image TEXT,
63
+ "allowedOrigin" JSONB DEFAULT '[]',
64
+ exports JSONB DEFAULT '[]',
65
+ credentials JSONB DEFAULT '[]',
66
+ "apiConnections" JSONB DEFAULT '[]',
67
+ "dbConnections" JSONB DEFAULT '[]',
68
+ "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
69
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
70
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
71
+ );
57
72
  `);
58
73
  console.log('✅ "projects" table ensured to exist.');
59
- // Ensure dbType column exists and is correctly configured
60
- await pool.query(`
61
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50);
74
+ // Add JSONB columns to existing tables that predate this schema version
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 '[]';
62
81
  `);
63
- await pool.query(`
64
- UPDATE streamby.projects SET "dbType" = 'nosql' WHERE "dbType" IS NULL;
82
+ // Ensure dbType exists with a non-null default
83
+ await pool.query(`
84
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
65
85
  `);
66
- await pool.query(`
67
- ALTER TABLE streamby.projects ALTER COLUMN "dbType" SET NOT NULL;
86
+ await pool.query(`
87
+ CREATE TABLE IF NOT EXISTS streamby.project_members (
88
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
89
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
90
+ "userId" VARCHAR(255) NOT NULL,
91
+ role VARCHAR(255) NOT NULL DEFAULT 'member',
92
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
93
+ "archivedBy" VARCHAR(255),
94
+ "archivedAt" TIMESTAMP WITH TIME ZONE,
95
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
96
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
97
+ UNIQUE ("projectId", "userId")
98
+ );
68
99
  `);
69
- await pool.query(`
70
- ALTER TABLE streamby.projects ALTER COLUMN "dbType" SET DEFAULT 'nosql';
100
+ console.log('✅ "project_members" table ensured to exist.');
101
+ // Migrate projectId FK column from UUID to TEXT if table predates this schema version
102
+ await pool.query(`
103
+ DO $$ BEGIN
104
+ ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
105
+ ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
106
+ ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
107
+ FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
108
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
71
109
  `);
72
- await pool.query(`
73
- CREATE TABLE IF NOT EXISTS streamby.exports (
74
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
75
- "projectId" UUID NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
76
- status VARCHAR(50) NOT NULL DEFAULT 'pending',
77
- "filePath" TEXT,
78
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
79
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
80
- );
110
+ await pool.query(`
111
+ CREATE TABLE IF NOT EXISTS streamby.exports (
112
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
113
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
114
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
115
+ "filePath" TEXT,
116
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
117
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
118
+ );
81
119
  `);
82
120
  console.log('✅ "exports" table ensured to exist.');
83
- await pool.query(`
84
- CREATE TABLE IF NOT EXISTS streamby.project_members (
85
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
86
- "projectId" UUID NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
87
- "userId" VARCHAR(255) NOT NULL,
88
- role VARCHAR(255) NOT NULL DEFAULT 'member',
89
- archived BOOLEAN NOT NULL DEFAULT FALSE,
90
- "archivedBy" VARCHAR(255),
91
- "archivedAt" TIMESTAMP WITH TIME ZONE,
92
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
93
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
94
- UNIQUE ("projectId", "userId")
95
- );
96
- `);
97
- console.log('✅ "project_members" table ensured to exist.');
98
121
  }
99
122
  catch (error) {
100
123
  console.error('❌ Error ensuring tables exist:', error);
@@ -6,13 +6,13 @@ const createSQLExportTable = async (connection, projectId, exportName, fields) =
6
6
  const slug = exportName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
7
7
  const tableName = `export_${projectId}_${slug}`;
8
8
  const columns = fields.map(field => `"${field.name}" ${field.type}`).join(',');
9
- const createTableQuery = `
10
- CREATE TABLE IF NOT EXISTS "${tableName}" (
11
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
12
- "projectId" UUID NOT NULL,
13
- __metadata JSONB NOT NULL,
14
- ${columns ? `, ${columns}` : ''}
15
- );
9
+ const createTableQuery = `
10
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
11
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
12
+ "projectId" UUID NOT NULL,
13
+ __metadata JSONB NOT NULL,
14
+ ${columns ? `, ${columns}` : ''}
15
+ );
16
16
  `;
17
17
  await connection.query(createTableQuery);
18
18
  console.log(`✅ Table '${tableName}' created with metadata.`);
@@ -30,15 +30,15 @@ exports.createSQLExportTable = createSQLExportTable;
30
30
  const createSQLRawExportTable = async (connection, projectId, exportName, nodeSchema) => {
31
31
  const tableName = projectId;
32
32
  const exportId = (0, crypto_1.randomUUID)();
33
- const createTableQuery = `
34
- CREATE TABLE IF NOT EXISTS "${tableName}" (
35
- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
36
- node_schema JSONB,
37
- name TEXT,
38
- method TEXT,
39
- created_at TIMESTAMPTZ DEFAULT NOW(),
40
- updated_at TIMESTAMPTZ DEFAULT NOW()
41
- );
33
+ const createTableQuery = `
34
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
35
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
36
+ node_schema JSONB,
37
+ name TEXT,
38
+ method TEXT,
39
+ created_at TIMESTAMPTZ DEFAULT NOW(),
40
+ updated_at TIMESTAMPTZ DEFAULT NOW()
41
+ );
42
42
  `;
43
43
  await connection.query(createTableQuery);
44
44
  await connection.query(`INSERT INTO "${tableName}" (id, node_schema, name, method) VALUES ($1, $2, $3, $4)`, [exportId, nodeSchema ? JSON.stringify(nodeSchema) : null, exportName, 'GET']);
@@ -53,7 +53,7 @@ exports.sqlAdapter = {
53
53
  const fullTableName = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
54
54
  let query = `SELECT * FROM ${fullTableName}`;
55
55
  if (keys.length > 0) {
56
- const where = keys.map((key, i) => `"${key}" = ${i + 1}`).join(' AND ');
56
+ const where = keys.map((key, i) => `"${key}" = $${i + 1}`).join(' AND ');
57
57
  query += ` WHERE ${where}`;
58
58
  }
59
59
  const result = await connection.query(query, values);
@@ -62,7 +62,7 @@ exports.sqlAdapter = {
62
62
  findOne: async (connection, tableName, filter, schema) => {
63
63
  const keys = Object.keys(filter);
64
64
  const values = Object.values(filter);
65
- const where = keys.map((key, i) => `"${key}" = ${i + 1}`).join(' AND ');
65
+ const where = keys.map((key, i) => `"${key}" = $${i + 1}`).join(' AND ');
66
66
  const fullTableName = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
67
67
  const result = await connection.query(`SELECT * FROM ${fullTableName} WHERE ${where} LIMIT 1`, values);
68
68
  return result.rows[0] || null;
@@ -70,7 +70,7 @@ exports.sqlAdapter = {
70
70
  create: async (connection, tableName, data, schema) => {
71
71
  const keys = Object.keys(data).map(key => `"${key}"`).join(', ');
72
72
  const values = Object.values(data);
73
- const placeholders = values.map((_, i) => `${i + 1}`).join(', ');
73
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
74
74
  const fullTableName = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
75
75
  const result = await connection.query(`INSERT INTO ${fullTableName} (${keys}) VALUES (${placeholders}) RETURNING *`, values);
76
76
  return result.rows[0];
@@ -79,8 +79,8 @@ exports.sqlAdapter = {
79
79
  const dataKeys = Object.keys(data);
80
80
  const filterKeys = Object.keys(filter);
81
81
  const values = [...Object.values(data), ...Object.values(filter)];
82
- const setClause = dataKeys.map((key, i) => `"${key}" = ${i + 1}`).join(', ');
83
- const whereClause = filterKeys.map((key, i) => `"${key}" = ${dataKeys.length + i + 1}`).join(' AND ');
82
+ const setClause = dataKeys.map((key, i) => `"${key}" = $${i + 1}`).join(', ');
83
+ const whereClause = filterKeys.map((key, i) => `"${key}" = $${dataKeys.length + i + 1}`).join(' AND ');
84
84
  const fullTableName = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
85
85
  const result = await connection.query(`UPDATE ${fullTableName} SET ${setClause} WHERE ${whereClause} RETURNING *`, values);
86
86
  return result.rows[0] || null;
@@ -20,6 +20,7 @@ const project_1 = require("./routes/project");
20
20
  const database_1 = require("./routes/database");
21
21
  const credential_1 = require("./routes/credential");
22
22
  const connection_1 = require("./routes/connection");
23
+ const dbConnection_1 = require("./routes/dbConnection");
23
24
  const notification_1 = require("./routes/notification");
24
25
  function createStreamByRouter(config) {
25
26
  const router = express_1.default.Router();
@@ -55,6 +56,7 @@ function createStreamByRouter(config) {
55
56
  router.use((0, database_1.databaseRouter)(config));
56
57
  router.use((0, credential_1.credentialRouter)(config));
57
58
  router.use((0, connection_1.connectionRouter)(config));
59
+ router.use((0, dbConnection_1.dbConnectionRouter)(config));
58
60
  router.use((0, notification_1.notificationRouter)(config));
59
61
  return router;
60
62
  }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function dbConnectionRouter(config: StreamByConfig): Router;
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.dbConnectionRouter = dbConnectionRouter;
37
+ const express_1 = require("express");
38
+ const mongodb_1 = require("mongodb");
39
+ const manager_1 = require("../../models/manager");
40
+ const auth_1 = require("../../utils/auth");
41
+ const connectionManager_1 = require("../../adapters/database/connectionManager");
42
+ const dbConnection_1 = require("../../services/dbConnection");
43
+ 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);
49
+ if (!db)
50
+ return { error: `Database '${connId}' not found in config`, status: 404 };
51
+ try {
52
+ const { client, type } = (0, connectionManager_1.getConnection)(connId);
53
+ return { client: client, dbType: type };
54
+ }
55
+ catch (e) {
56
+ return { error: e.message, status: 503 };
57
+ }
58
+ }
59
+ async function getDecryptedConnectionString(project, connId) {
60
+ const conn = project.dbConnections?.find((c) => c.id === connId);
61
+ if (!conn)
62
+ return { error: 'DB connection not found', status: 404 };
63
+ const { decrypt, isEncryptionKeySet } = await Promise.resolve().then(() => __importStar(require('../../utils/encryption')));
64
+ if (!isEncryptionKeySet())
65
+ return { error: 'Encryption key is not set', status: 500 };
66
+ const credential = project.credentials?.find((c) => c.id === conn.credentialId);
67
+ if (!credential)
68
+ return { error: 'Credential not found in project', status: 400 };
69
+ return { connectionString: decrypt(credential.encryptedValue), conn };
70
+ }
71
+ function dbConnectionRouter(config) {
72
+ const router = (0, express_1.Router)();
73
+ const Project = (0, manager_1.getModel)('projects');
74
+ // ─── List DB connections ──────────────────────────────────────────────────
75
+ router.get('/projects/:id/connections/db', async (req, res) => {
76
+ try {
77
+ const auth = req.auth;
78
+ const project = await Project.findOne({ _id: req.params.id });
79
+ if (!project)
80
+ return res.status(404).json({ message: 'Project not found' });
81
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
82
+ return res.status(403).json({ message: 'Unauthorized project access' });
83
+ return res.status(200).json({ data: project.dbConnections || [] });
84
+ }
85
+ catch (err) {
86
+ res.status(500).json({ message: 'Failed to fetch DB connections', details: err.message });
87
+ }
88
+ });
89
+ // ─── Add DB connection ────────────────────────────────────────────────────
90
+ router.post('/projects/:id/connections/db', async (req, res) => {
91
+ try {
92
+ const auth = req.auth;
93
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
94
+ return res.status(403).json({ message: 'Permission denied' });
95
+ }
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' });
99
+ }
100
+ if (!VALID_DB_TYPES.includes(dbType)) {
101
+ return res.status(400).json({ message: `dbType must be one of: ${VALID_DB_TYPES.join(', ')}` });
102
+ }
103
+ const project = await Project.findOne({ _id: req.params.id });
104
+ if (!project)
105
+ return res.status(404).json({ message: 'Project not found' });
106
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
107
+ 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' });
111
+ const connection = {
112
+ id: new mongodb_1.ObjectId().toHexString(),
113
+ name,
114
+ dbType,
115
+ credentialId,
116
+ projectId: req.params.id,
117
+ createdAt: new Date(),
118
+ ...(description !== undefined && { description }),
119
+ };
120
+ await Project.update({ _id: req.params.id }, { $push: { dbConnections: connection } });
121
+ return res.status(201).json({ data: connection });
122
+ }
123
+ catch (err) {
124
+ res.status(500).json({ message: 'Failed to add DB connection', details: err.message });
125
+ }
126
+ });
127
+ // ─── Delete DB connection ─────────────────────────────────────────────────
128
+ router.delete('/projects/:id/connections/db/:connId', async (req, res) => {
129
+ try {
130
+ if (isBuiltinDb(req.params.connId, config)) {
131
+ return res.status(403).json({ message: 'Cannot delete a built-in database connection' });
132
+ }
133
+ const auth = req.auth;
134
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
135
+ return res.status(403).json({ message: 'Permission denied' });
136
+ }
137
+ const project = await Project.findOne({ _id: req.params.id });
138
+ if (!project)
139
+ return res.status(404).json({ message: 'Project not found' });
140
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
141
+ return res.status(403).json({ message: 'Unauthorized project access' });
142
+ const exists = project.dbConnections?.some((c) => c.id === req.params.connId);
143
+ if (!exists)
144
+ return res.status(404).json({ message: 'DB connection not found' });
145
+ await Project.update({ _id: req.params.id }, { $pull: { dbConnections: { id: req.params.connId } } });
146
+ return res.status(200).json({ message: 'DB connection deleted successfully' });
147
+ }
148
+ catch (err) {
149
+ res.status(500).json({ message: 'Failed to delete DB connection', details: err.message });
150
+ }
151
+ });
152
+ // ─── List tables / collections ────────────────────────────────────────────
153
+ router.get('/projects/:id/connections/db/:connId/tables', async (req, res) => {
154
+ try {
155
+ const auth = req.auth;
156
+ const project = await Project.findOne({ _id: req.params.id });
157
+ if (!project)
158
+ return res.status(404).json({ message: 'Project not found' });
159
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
160
+ return res.status(403).json({ message: 'Unauthorized project access' });
161
+ if (isBuiltinDb(req.params.connId, config)) {
162
+ const internal = resolveBuiltinConnection(req.params.connId, config);
163
+ if ('error' in internal)
164
+ return res.status(internal.status).json({ message: internal.error });
165
+ const tables = await (0, dbConnection_1.listTablesInternal)(internal.client, internal.dbType, req.params.id);
166
+ return res.status(200).json({ data: tables });
167
+ }
168
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
169
+ if ('error' in resolved)
170
+ return res.status(resolved.status).json({ message: resolved.error });
171
+ const tables = await (0, dbConnection_1.listTablesOrCollections)(resolved.connectionString, resolved.conn.dbType);
172
+ return res.status(200).json({ data: tables });
173
+ }
174
+ catch (err) {
175
+ res.status(500).json({ message: 'Failed to list tables', details: err.message });
176
+ }
177
+ });
178
+ // ─── Create table / collection ────────────────────────────────────────────
179
+ router.post('/projects/:id/connections/db/:connId/tables', async (req, res) => {
180
+ try {
181
+ const auth = req.auth;
182
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
183
+ return res.status(403).json({ message: 'Permission denied' });
184
+ }
185
+ const project = await Project.findOne({ _id: req.params.id });
186
+ if (!project)
187
+ return res.status(404).json({ message: 'Project not found' });
188
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
189
+ return res.status(403).json({ message: 'Unauthorized project access' });
190
+ const schema = req.body;
191
+ if (!schema.tableName)
192
+ return res.status(400).json({ message: 'tableName is required' });
193
+ if (isBuiltinDb(req.params.connId, config)) {
194
+ const internal = resolveBuiltinConnection(req.params.connId, config);
195
+ if ('error' in internal)
196
+ return res.status(internal.status).json({ message: internal.error });
197
+ if (internal.dbType === 'sql' && (!Array.isArray(schema.columns) || schema.columns.length === 0)) {
198
+ return res.status(400).json({ message: 'columns are required for SQL tables' });
199
+ }
200
+ await (0, dbConnection_1.createTableOrCollectionInternal)(internal.client, internal.dbType, schema, req.params.id);
201
+ return res.status(201).json({ message: `${internal.dbType === 'sql' ? 'Table' : 'Collection'} created successfully` });
202
+ }
203
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
204
+ if ('error' in resolved)
205
+ return res.status(resolved.status).json({ message: resolved.error });
206
+ if (resolved.conn.dbType === 'postgresql' && (!Array.isArray(schema.columns) || schema.columns.length === 0)) {
207
+ return res.status(400).json({ message: 'columns are required for PostgreSQL tables' });
208
+ }
209
+ await (0, dbConnection_1.createTableOrCollection)(resolved.connectionString, resolved.conn.dbType, schema);
210
+ return res.status(201).json({ message: `${resolved.conn.dbType === 'postgresql' ? 'Table' : 'Collection'} created successfully` });
211
+ }
212
+ catch (err) {
213
+ res.status(500).json({ message: 'Failed to create table/collection', details: err.message });
214
+ }
215
+ });
216
+ // ─── Query records ────────────────────────────────────────────────────────
217
+ router.get('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
218
+ try {
219
+ const auth = req.auth;
220
+ const project = await Project.findOne({ _id: req.params.id });
221
+ if (!project)
222
+ return res.status(404).json({ message: 'Project not found' });
223
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
224
+ return res.status(403).json({ message: 'Unauthorized project access' });
225
+ const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
226
+ const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
227
+ if (isBuiltinDb(req.params.connId, config)) {
228
+ const internal = resolveBuiltinConnection(req.params.connId, config);
229
+ if ('error' in internal)
230
+ return res.status(internal.status).json({ message: internal.error });
231
+ const records = await (0, dbConnection_1.queryRecordsInternal)(internal.client, internal.dbType, req.params.tableName, limit, offset, req.params.id);
232
+ return res.status(200).json({ data: records });
233
+ }
234
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
235
+ if ('error' in resolved)
236
+ return res.status(resolved.status).json({ message: resolved.error });
237
+ const records = await (0, dbConnection_1.queryRecords)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, limit, offset);
238
+ return res.status(200).json({ data: records });
239
+ }
240
+ catch (err) {
241
+ res.status(500).json({ message: 'Failed to query records', details: err.message });
242
+ }
243
+ });
244
+ // ─── Insert record ────────────────────────────────────────────────────────
245
+ router.post('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
246
+ try {
247
+ const auth = req.auth;
248
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
249
+ return res.status(403).json({ message: 'Permission denied' });
250
+ }
251
+ const project = await Project.findOne({ _id: req.params.id });
252
+ if (!project)
253
+ return res.status(404).json({ message: 'Project not found' });
254
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
255
+ return res.status(403).json({ message: 'Unauthorized project access' });
256
+ const record = req.body;
257
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
258
+ return res.status(400).json({ message: 'Request body must be a JSON object' });
259
+ }
260
+ if (isBuiltinDb(req.params.connId, config)) {
261
+ const internal = resolveBuiltinConnection(req.params.connId, config);
262
+ if ('error' in internal)
263
+ return res.status(internal.status).json({ message: internal.error });
264
+ const inserted = await (0, dbConnection_1.insertRecordInternal)(internal.client, internal.dbType, req.params.tableName, record, req.params.id);
265
+ return res.status(201).json({ data: inserted });
266
+ }
267
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
268
+ if ('error' in resolved)
269
+ return res.status(resolved.status).json({ message: resolved.error });
270
+ const inserted = await (0, dbConnection_1.insertRecord)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, record);
271
+ return res.status(201).json({ data: inserted });
272
+ }
273
+ catch (err) {
274
+ res.status(500).json({ message: 'Failed to insert record', details: err.message });
275
+ }
276
+ });
277
+ return router;
278
+ }
@@ -189,7 +189,7 @@ function exportRouter(config) {
189
189
  return res.status(401).json({ message: 'Unauthorized' });
190
190
  }
191
191
  const projectId = req.params.id;
192
- const { name, description, isPrivate, allowedOrigin, useConnections, useCredentials, nodeSchema } = req.body;
192
+ const { name, description, isPrivate, allowedOrigin, useConnections, useCredentials, nodeSchema, storageDbId } = req.body;
193
193
  if (!name) {
194
194
  return res.status(400).json({ message: 'Missing export name' });
195
195
  }
@@ -199,7 +199,9 @@ function exportRouter(config) {
199
199
  if (!project || !(0, auth_1.isProjectMember)(project, auth.userId)) {
200
200
  return res.status(403).json({ message: 'Unauthorized project access' });
201
201
  }
202
- const result = await (0, export_1.createExport)(config, projectId, description, name, project.dbType, exportType, isPrivate, allowedOrigin, resolvedNodeSchema, useConnections, useCredentials);
202
+ const mainDb = config.databases?.find(db => db.main) ?? config.databases?.[0];
203
+ const dbType = mainDb?.type ?? 'nosql';
204
+ const result = await (0, export_1.createExport)(config, projectId, description, name, dbType, exportType, isPrivate, allowedOrigin, resolvedNodeSchema, useConnections, useCredentials, storageDbId);
203
205
  res.status(201).json({ data: result, message: result.message });
204
206
  }
205
207
  catch (err) {
@@ -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 || '',
@@ -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,11 +45,11 @@ 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.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 => ({
@@ -195,6 +195,22 @@ class Model {
195
195
  }
196
196
  return data;
197
197
  }
198
+ async updateSqlJsonbArray(connection, id, op, field, value) {
199
+ const schema = this.schema || 'streamby';
200
+ const tbl = `"${schema}"."${this.tableName}"`;
201
+ if (op === '$push') {
202
+ await connection.query(`UPDATE ${tbl} SET "${field}" = "${field}" || $1::jsonb WHERE id = $2`, [JSON.stringify([value]), id]);
203
+ }
204
+ else {
205
+ const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
206
+ const matchVal = String(value[matchKey]);
207
+ await connection.query(`UPDATE ${tbl} SET "${field}" = (
208
+ SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
209
+ FROM jsonb_array_elements("${field}") elem
210
+ WHERE elem->>'${matchKey}' != $1
211
+ ) WHERE id = $2`, [matchVal, id]);
212
+ }
213
+ }
198
214
  async update(filter, data) {
199
215
  const existingProject = await this.findOne(filter); // Find the project first
200
216
  if (!existingProject) {
@@ -270,8 +286,15 @@ class Model {
270
286
  }
271
287
  else {
272
288
  // General update logic
273
- // Use the correctly constructed updateFilter
274
289
  if (dbType === 'sql') {
290
+ // Detect MongoDB-style array operators ($push / $pull) and map to JSONB operations
291
+ const op = Object.keys(data).find(k => k === '$push' || k === '$pull');
292
+ if (op) {
293
+ const field = Object.keys(data[op])[0];
294
+ const value = data[op][field];
295
+ await this.updateSqlJsonbArray(connection, existingProject.id, op, field, value);
296
+ return await this.findOne({ _id: existingProject.id });
297
+ }
275
298
  const result = await sql_1.sqlAdapter.update(connection, this.tableName, updateFilter, data, this.schema);
276
299
  if (result)
277
300
  return result;
@@ -0,0 +1,11 @@
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 listTablesInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', projectId?: string): Promise<string[]>;
9
+ export declare function queryRecordsInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, limit?: number, offset?: number, projectId?: string): Promise<any[]>;
10
+ export declare function createTableOrCollectionInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', schema: CreateTableSchema, projectId?: string): Promise<void>;
11
+ export declare function insertRecordInternal(client: Pool | MongoClient, dbType: 'sql' | 'nosql', tableName: string, record: Record<string, unknown>, projectId?: string): Promise<any>;