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.
@@ -43,58 +43,85 @@ 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";
57
49
  `);
58
- 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);
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 $$;
62
56
  `);
63
- await pool.query(`
64
- UPDATE streamby.projects SET "dbType" = 'nosql' WHERE "dbType" IS NULL;
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
+ );
65
72
  `);
66
- await pool.query(`
67
- ALTER TABLE streamby.projects ALTER COLUMN "dbType" SET NOT NULL;
73
+ console.log('✅ "projects" table ensured to exist.');
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 '[]';
68
81
  `);
69
- await pool.query(`
70
- ALTER TABLE streamby.projects ALTER COLUMN "dbType" SET DEFAULT 'nosql';
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';
71
85
  `);
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
- );
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
+ status VARCHAR(50) NOT NULL DEFAULT 'active',
93
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
94
+ "archivedBy" VARCHAR(255),
95
+ "archivedAt" TIMESTAMP WITH TIME ZONE,
96
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
97
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
98
+ UNIQUE ("projectId", "userId")
99
+ );
81
100
  `);
82
- 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
- );
101
+ await pool.query(`
102
+ ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
96
103
  `);
97
104
  console.log('✅ "project_members" table ensured to exist.');
105
+ // Migrate projectId FK column from UUID to TEXT if table predates this schema version
106
+ await pool.query(`
107
+ DO $$ BEGIN
108
+ ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
109
+ ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
110
+ ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
111
+ FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
112
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
113
+ `);
114
+ await pool.query(`
115
+ CREATE TABLE IF NOT EXISTS streamby.exports (
116
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
117
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
118
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
119
+ "filePath" TEXT,
120
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
121
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
122
+ );
123
+ `);
124
+ console.log('✅ "exports" table ensured to exist.');
98
125
  }
99
126
  catch (error) {
100
127
  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;
@@ -88,7 +88,7 @@ exports.sqlAdapter = {
88
88
  delete: async (connection, tableName, filter, schema) => {
89
89
  const keys = Object.keys(filter);
90
90
  const values = Object.values(filter);
91
- const where = keys.map((key, i) => `"${key}" = ${i + 1}`).join(' AND ');
91
+ const where = keys.map((key, i) => `"${key}" = $${i + 1}`).join(' AND ');
92
92
  const fullTableName = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
93
93
  const result = await connection.query(`DELETE FROM ${fullTableName} WHERE ${where}`, values);
94
94
  return result.rowCount || 0;
@@ -20,6 +20,8 @@ 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");
24
+ const storageConnection_1 = require("./routes/storageConnection");
23
25
  const notification_1 = require("./routes/notification");
24
26
  function createStreamByRouter(config) {
25
27
  const router = express_1.default.Router();
@@ -55,6 +57,8 @@ function createStreamByRouter(config) {
55
57
  router.use((0, database_1.databaseRouter)(config));
56
58
  router.use((0, credential_1.credentialRouter)(config));
57
59
  router.use((0, connection_1.connectionRouter)(config));
60
+ router.use((0, dbConnection_1.dbConnectionRouter)(config));
61
+ router.use((0, storageConnection_1.storageConnectionRouter)(config));
58
62
  router.use((0, notification_1.notificationRouter)(config));
59
63
  return router;
60
64
  }
@@ -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,367 @@
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
+ // ─── Delete table / collection ────────────────────────────────────────────
217
+ router.delete('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
218
+ try {
219
+ const auth = req.auth;
220
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
221
+ return res.status(403).json({ message: 'Permission denied' });
222
+ }
223
+ const project = await Project.findOne({ _id: req.params.id });
224
+ if (!project)
225
+ return res.status(404).json({ message: 'Project not found' });
226
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
227
+ return res.status(403).json({ message: 'Unauthorized project access' });
228
+ if (isBuiltinDb(req.params.connId, config)) {
229
+ const internal = resolveBuiltinConnection(req.params.connId, config);
230
+ if ('error' in internal)
231
+ return res.status(internal.status).json({ message: internal.error });
232
+ await (0, dbConnection_1.deleteTableOrCollectionInternal)(internal.client, internal.dbType, req.params.tableName, req.params.id);
233
+ return res.status(200).json({ message: 'Table/collection deleted' });
234
+ }
235
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
236
+ if ('error' in resolved)
237
+ return res.status(resolved.status).json({ message: resolved.error });
238
+ await (0, dbConnection_1.deleteTableOrCollection)(resolved.connectionString, resolved.conn.dbType, req.params.tableName);
239
+ return res.status(200).json({ message: 'Table/collection deleted' });
240
+ }
241
+ catch (err) {
242
+ res.status(500).json({ message: 'Failed to delete table/collection', details: err.message });
243
+ }
244
+ });
245
+ // ─── Query records ────────────────────────────────────────────────────────
246
+ router.get('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
247
+ try {
248
+ const auth = req.auth;
249
+ const project = await Project.findOne({ _id: req.params.id });
250
+ if (!project)
251
+ return res.status(404).json({ message: 'Project not found' });
252
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
253
+ return res.status(403).json({ message: 'Unauthorized project access' });
254
+ const limit = Math.min(parseInt(String(req.query.limit ?? 50), 10), 500);
255
+ const offset = Math.max(parseInt(String(req.query.offset ?? 0), 10), 0);
256
+ if (isBuiltinDb(req.params.connId, config)) {
257
+ const internal = resolveBuiltinConnection(req.params.connId, config);
258
+ if ('error' in internal)
259
+ return res.status(internal.status).json({ message: internal.error });
260
+ const records = await (0, dbConnection_1.queryRecordsInternal)(internal.client, internal.dbType, req.params.tableName, limit, offset, req.params.id);
261
+ return res.status(200).json({ data: records });
262
+ }
263
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
264
+ if ('error' in resolved)
265
+ return res.status(resolved.status).json({ message: resolved.error });
266
+ const records = await (0, dbConnection_1.queryRecords)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, limit, offset);
267
+ return res.status(200).json({ data: records });
268
+ }
269
+ catch (err) {
270
+ res.status(500).json({ message: 'Failed to query records', details: err.message });
271
+ }
272
+ });
273
+ // ─── Insert record ────────────────────────────────────────────────────────
274
+ router.post('/projects/:id/connections/db/:connId/tables/:tableName', async (req, res) => {
275
+ try {
276
+ const auth = req.auth;
277
+ if (auth.role !== 'admin' && auth.role !== 'editor') {
278
+ return res.status(403).json({ message: 'Permission denied' });
279
+ }
280
+ const project = await Project.findOne({ _id: req.params.id });
281
+ if (!project)
282
+ return res.status(404).json({ message: 'Project not found' });
283
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
284
+ return res.status(403).json({ message: 'Unauthorized project access' });
285
+ const record = req.body;
286
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
287
+ return res.status(400).json({ message: 'Request body must be a JSON object' });
288
+ }
289
+ if (isBuiltinDb(req.params.connId, config)) {
290
+ const internal = resolveBuiltinConnection(req.params.connId, config);
291
+ if ('error' in internal)
292
+ return res.status(internal.status).json({ message: internal.error });
293
+ const inserted = await (0, dbConnection_1.insertRecordInternal)(internal.client, internal.dbType, req.params.tableName, record, req.params.id);
294
+ return res.status(201).json({ data: inserted });
295
+ }
296
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
297
+ if ('error' in resolved)
298
+ return res.status(resolved.status).json({ message: resolved.error });
299
+ const inserted = await (0, dbConnection_1.insertRecord)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, record);
300
+ return res.status(201).json({ data: inserted });
301
+ }
302
+ catch (err) {
303
+ res.status(500).json({ message: 'Failed to insert record', details: err.message });
304
+ }
305
+ });
306
+ // ─── Update record ────────────────────────────────────────────────────────
307
+ router.put('/projects/:id/connections/db/:connId/tables/:tableName/:recordId', async (req, res) => {
308
+ try {
309
+ const auth = req.auth;
310
+ if (auth.role !== 'admin' && auth.role !== 'editor')
311
+ return res.status(403).json({ message: 'Permission denied' });
312
+ const project = await Project.findOne({ _id: req.params.id });
313
+ if (!project)
314
+ return res.status(404).json({ message: 'Project not found' });
315
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
316
+ return res.status(403).json({ message: 'Unauthorized project access' });
317
+ const updates = req.body;
318
+ if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
319
+ return res.status(400).json({ message: 'Request body must be a JSON object' });
320
+ }
321
+ if (isBuiltinDb(req.params.connId, config)) {
322
+ const internal = resolveBuiltinConnection(req.params.connId, config);
323
+ if ('error' in internal)
324
+ return res.status(internal.status).json({ message: internal.error });
325
+ const updated = await (0, dbConnection_1.updateRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, updates, req.params.id);
326
+ return res.status(200).json({ data: updated });
327
+ }
328
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
329
+ if ('error' in resolved)
330
+ return res.status(resolved.status).json({ message: resolved.error });
331
+ const updated = await (0, dbConnection_1.updateRecord)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, req.params.recordId, updates);
332
+ return res.status(200).json({ data: updated });
333
+ }
334
+ catch (err) {
335
+ res.status(500).json({ message: 'Failed to update record', details: err.message });
336
+ }
337
+ });
338
+ // ─── Delete record ────────────────────────────────────────────────────────
339
+ router.delete('/projects/:id/connections/db/:connId/tables/:tableName/:recordId', async (req, res) => {
340
+ try {
341
+ const auth = req.auth;
342
+ if (auth.role !== 'admin' && auth.role !== 'editor')
343
+ return res.status(403).json({ message: 'Permission denied' });
344
+ const project = await Project.findOne({ _id: req.params.id });
345
+ if (!project)
346
+ return res.status(404).json({ message: 'Project not found' });
347
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
348
+ return res.status(403).json({ message: 'Unauthorized project access' });
349
+ if (isBuiltinDb(req.params.connId, config)) {
350
+ const internal = resolveBuiltinConnection(req.params.connId, config);
351
+ if ('error' in internal)
352
+ return res.status(internal.status).json({ message: internal.error });
353
+ await (0, dbConnection_1.deleteRecordInternal)(internal.client, internal.dbType, req.params.tableName, req.params.recordId, req.params.id);
354
+ return res.status(200).json({ message: 'Record deleted' });
355
+ }
356
+ const resolved = await getDecryptedConnectionString(project, req.params.connId);
357
+ if ('error' in resolved)
358
+ return res.status(resolved.status).json({ message: resolved.error });
359
+ await (0, dbConnection_1.deleteRecord)(resolved.connectionString, resolved.conn.dbType, req.params.tableName, req.params.recordId);
360
+ return res.status(200).json({ message: 'Record deleted' });
361
+ }
362
+ catch (err) {
363
+ res.status(500).json({ message: 'Failed to delete record', details: err.message });
364
+ }
365
+ });
366
+ return router;
367
+ }