streamby-core 0.31.0 → 0.32.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.
@@ -0,0 +1,19 @@
1
+ import { Pool } from 'pg';
2
+ import { MongoClient } from 'mongodb';
3
+ import { StreamByConfig, Auth, DbConnection, StorageConnection, StorageAdapter } from '../types';
4
+ export type ResolvedDbClient = {
5
+ client: Pool | MongoClient;
6
+ type: 'sql' | 'nosql';
7
+ ephemeral: boolean;
8
+ };
9
+ export type ResolveError = {
10
+ error: string;
11
+ status: number;
12
+ };
13
+ export declare function findDbConnection(project: any, connId: string): DbConnection | undefined;
14
+ export declare function findStorageConnection(project: any, connId: string): StorageConnection | undefined;
15
+ export declare function resolveDbConnectionClient(project: any, connId: string, config: StreamByConfig, auth: Auth): Promise<ResolvedDbClient | ResolveError>;
16
+ export declare function releaseDbClient(resolved: ResolvedDbClient): Promise<void>;
17
+ export declare function resolveStorageAdapter(project: any, connId: string, config: StreamByConfig & {
18
+ adapter?: StorageAdapter;
19
+ }, auth: Auth): Promise<StorageAdapter | ResolveError>;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findDbConnection = findDbConnection;
4
+ exports.findStorageConnection = findStorageConnection;
5
+ exports.resolveDbConnectionClient = resolveDbConnectionClient;
6
+ exports.releaseDbClient = releaseDbClient;
7
+ exports.resolveStorageAdapter = resolveStorageAdapter;
8
+ const pg_1 = require("pg");
9
+ const mongodb_1 = require("mongodb");
10
+ const connectionManager_1 = require("../adapters/database/connectionManager");
11
+ const builtinAccess_1 = require("../utils/builtinAccess");
12
+ const userIntegration_1 = require("./userIntegration");
13
+ const encryption_1 = require("../utils/encryption");
14
+ const storage_1 = require("../providers/storage");
15
+ const s3_1 = require("../adapters/storage/s3");
16
+ // Matches by row id first; falls back to matching a builtin row by integrationId (the
17
+ // config id, e.g. 'mongo') for nodes/connections saved before TCORE-69 gave builtin rows
18
+ // their own generated id — same legacy-id class as connFilter in storage.ts.
19
+ function findDbConnection(project, connId) {
20
+ return project.dbConnections?.find((c) => c.id === connId)
21
+ ?? project.dbConnections?.find((c) => c.source === 'builtin' && c.integrationId === connId);
22
+ }
23
+ function findStorageConnection(project, connId) {
24
+ return project.storageConnections?.find((c) => c.id === connId)
25
+ ?? project.storageConnections?.find((c) => c.source === 'builtin' && c.integrationId === connId);
26
+ }
27
+ // `connId` must be the `id` of a real row in project.dbConnections — never a bare
28
+ // config id (config.databases[].id). Builtin rows reuse the shared pooled client keyed by
29
+ // conn.integrationId (config id); BYOC/manual rows open a short-lived client from their own
30
+ // decrypted credential, which the caller must close via `releaseDbClient` once done.
31
+ async function resolveDbConnectionClient(project, connId, config, auth) {
32
+ const conn = findDbConnection(project, connId);
33
+ if (!conn)
34
+ return { error: 'DB connection not found', status: 404 };
35
+ if (conn.source === 'builtin') {
36
+ const builtinId = conn.integrationId;
37
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'database'))) {
38
+ return { error: 'Access to this built-in database is not permitted', status: 403 };
39
+ }
40
+ const db = (config.databases ?? []).find(d => d.id === builtinId);
41
+ if (!db)
42
+ return { error: `Database '${builtinId}' not found in config`, status: 404 };
43
+ try {
44
+ const { client, type } = (0, connectionManager_1.getConnection)(builtinId);
45
+ return { client: client, type, ephemeral: false };
46
+ }
47
+ catch (e) {
48
+ return { error: e.message, status: 503 };
49
+ }
50
+ }
51
+ let connectionString;
52
+ if (conn.source === 'integration') {
53
+ if (!conn.integrationId)
54
+ return { error: 'Connection is missing its integrationId', status: 500 };
55
+ const credential = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
56
+ if (!credential)
57
+ return { error: 'Integration not found', status: 400 };
58
+ connectionString = credential;
59
+ }
60
+ else {
61
+ if (!(0, encryption_1.isEncryptionKeySet)())
62
+ return { error: 'Encryption key is not set', status: 500 };
63
+ if (!conn.encryptedCredential)
64
+ return { error: 'Connection has no stored credential', status: 400 };
65
+ connectionString = (0, encryption_1.decrypt)(conn.encryptedCredential);
66
+ }
67
+ const type = conn.dbType === 'postgresql' ? 'sql' : 'nosql';
68
+ try {
69
+ if (type === 'sql') {
70
+ const pool = new pg_1.Pool({ connectionString });
71
+ await pool.query('SELECT 1');
72
+ return { client: pool, type, ephemeral: true };
73
+ }
74
+ const client = new mongodb_1.MongoClient(connectionString);
75
+ await client.connect();
76
+ return { client, type, ephemeral: true };
77
+ }
78
+ catch (e) {
79
+ return { error: `Failed to connect: ${e.message}`, status: 503 };
80
+ }
81
+ }
82
+ // Only closes clients this resolver opened itself (`ephemeral: true`) — the shared pooled
83
+ // builtin clients are owned by connectionManager and must outlive a single request.
84
+ async function releaseDbClient(resolved) {
85
+ if (!resolved.ephemeral)
86
+ return;
87
+ if (resolved.type === 'sql')
88
+ await resolved.client.end();
89
+ else
90
+ await resolved.client.close();
91
+ }
92
+ async function resolveStorageAdapter(project, connId, config, auth) {
93
+ const conn = findStorageConnection(project, connId);
94
+ if (!conn)
95
+ return { error: 'Storage connection not found', status: 404 };
96
+ if (conn.source === 'builtin') {
97
+ const builtinId = conn.integrationId;
98
+ if (!(await (0, builtinAccess_1.assertBuiltinAccess)(auth, builtinId, config, 'storage'))) {
99
+ return { error: 'Access to this built-in storage is not permitted', status: 403 };
100
+ }
101
+ const provider = config.storageProviders.find(p => p.id === builtinId);
102
+ if (!provider)
103
+ return { error: 'Storage provider not found', status: 404 };
104
+ if (provider.type === 's3')
105
+ return new s3_1.S3Adapter(provider.config);
106
+ return config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
107
+ }
108
+ if (conn.source === 'integration') {
109
+ if (!conn.integrationId)
110
+ return { error: 'Connection is missing its integrationId', status: 500 };
111
+ try {
112
+ const s3Config = await (0, userIntegration_1.getDecryptedIntegrationCredentialById)(conn.integrationId);
113
+ if (!s3Config)
114
+ return { error: 'Integration not found', status: 400 };
115
+ return new s3_1.S3Adapter(s3Config);
116
+ }
117
+ catch (e) {
118
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
119
+ }
120
+ }
121
+ if (!(0, encryption_1.isEncryptionKeySet)())
122
+ return { error: 'Encryption key not set', status: 500 };
123
+ if (!conn.encryptedCredential)
124
+ return { error: 'Connection has no stored credential', status: 400 };
125
+ try {
126
+ const s3Config = JSON.parse((0, encryption_1.decrypt)(conn.encryptedCredential));
127
+ return new s3_1.S3Adapter(s3Config);
128
+ }
129
+ catch (e) {
130
+ return { error: `Failed to initialize storage adapter: ${e.message}`, status: 500 };
131
+ }
132
+ }
@@ -1,2 +1,7 @@
1
1
  import { NodeSchema, StreamByConfig } from '../types';
2
+ export declare class NodeExecutionError extends Error {
3
+ nodeId: string;
4
+ nodeType: string;
5
+ constructor(nodeId: string, nodeType: string, message: string);
6
+ }
2
7
  export declare function executeExport(nodeSchema: NodeSchema, project: any, config: StreamByConfig): Promise<any>;
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeExecutionError = void 0;
3
4
  exports.executeExport = executeExport;
4
- const connectionManager_1 = require("../adapters/database/connectionManager");
5
5
  const encryption_1 = require("../utils/encryption");
6
6
  const dbConnection_1 = require("./dbConnection");
7
+ const connectionResolver_1 = require("./connectionResolver");
7
8
  const jobQueue_1 = require("./jobQueue");
8
9
  const mediaProcessor_1 = require("./mediaProcessor");
9
10
  const vfxProcessor_1 = require("./vfxProcessor");
@@ -75,6 +76,21 @@ function getTarget(nodes, edges, sourceId, sourceHandle) {
75
76
  const edge = edges.find(e => e.source === sourceId && e.sourceHandle === sourceHandle);
76
77
  return edge ? (nodes.find(n => n.id === edge.target) ?? null) : null;
77
78
  }
79
+ class NodeExecutionError extends Error {
80
+ constructor(nodeId, nodeType, message) {
81
+ super(message);
82
+ this.nodeId = nodeId;
83
+ this.nodeType = nodeType;
84
+ this.name = 'NodeExecutionError';
85
+ }
86
+ }
87
+ exports.NodeExecutionError = NodeExecutionError;
88
+ // Export execution is public (gated by allowedOrigin, not user identity — see
89
+ // export.ts:339) — there is no real Auth to pass through. This sentinel satisfies
90
+ // resolveDbConnectionClient's signature without adding a gate that never existed;
91
+ // canUseBuiltin gating is for connection *management* by a user, not for serving an
92
+ // already-published export.
93
+ const SYSTEM_AUTH = { userId: 'system', username: 'system', role: 'admin' };
78
94
  async function executeExport(nodeSchema, project, config) {
79
95
  const nodes = nodeSchema.nodes;
80
96
  const edges = nodeSchema.edges;
@@ -94,24 +110,26 @@ async function executeExport(nodeSchema, project, config) {
94
110
  const tableName = (node.data?.tableName || node.data?.subtitle || node.data?.label);
95
111
  if (!tableName)
96
112
  continue;
113
+ // dataSourceNode always declares its own database connection explicitly (persistence:
114
+ // 'database' in types/nodePersistence.ts) — a project has no notion of a "default"
115
+ // connection, since a project can have many database/storage/API providers connected.
97
116
  const connectionId = node.data?.connectionId;
117
+ if (!connectionId) {
118
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}) has no database connection — connect one on the node.`);
119
+ }
98
120
  const recordId = node.data?.recordId;
99
121
  const projectIdentifier = project._id?.toString() ?? project.id;
100
- const fetchData = async (client, type) => recordId
101
- ? (0, dbConnection_1.queryRecordByIdInternal)(client, type, tableName, recordId, projectIdentifier)
102
- : (0, dbConnection_1.queryRecordsInternal)(client, type, tableName, 500, 0, projectIdentifier);
103
- if (connectionId) {
104
- const { client, type } = (0, connectionManager_1.getConnection)(connectionId);
105
- dataResults.push(await fetchData(client, type));
122
+ const resolved = await (0, connectionResolver_1.resolveDbConnectionClient)(project, connectionId, config, SYSTEM_AUTH);
123
+ if ('error' in resolved) {
124
+ throw new NodeExecutionError(node.id, node.type, `Node '${node.id}' (${node.type}): ${resolved.error}`);
106
125
  }
107
- else {
108
- // legacy fallback for nodes saved before connectionId was introduced
109
- const targetDb = config.databases?.find((db) => db.type === project.dbType && db.main)
110
- ?? config.databases?.find((db) => db.type === project.dbType);
111
- if (!targetDb)
112
- throw new Error(`No database connection for type ${project.dbType}`);
113
- const { client, type } = (0, connectionManager_1.getConnection)(targetDb.id);
114
- dataResults.push(await fetchData(client, type));
126
+ try {
127
+ dataResults.push(recordId
128
+ ? await (0, dbConnection_1.queryRecordByIdInternal)(resolved.client, resolved.type, tableName, recordId, projectIdentifier)
129
+ : await (0, dbConnection_1.queryRecordsInternal)(resolved.client, resolved.type, tableName, 500, 0, projectIdentifier));
130
+ }
131
+ finally {
132
+ await (0, connectionResolver_1.releaseDbClient)(resolved);
115
133
  }
116
134
  }
117
135
  else if (node.type === 'connectionNode') {
@@ -170,6 +188,17 @@ async function executeExport(nodeSchema, project, config) {
170
188
  payload = { ...payload, jobId: job.jobId, jobType: 'thumbnail' };
171
189
  }
172
190
  else if (processNode.type === 'ingestNode' && fileId) {
191
+ // ingestNode always declares its own storage connection explicitly (persistence:
192
+ // 'storage' in types/nodePersistence.ts) — a project has no notion of a "default"
193
+ // connection, since a project can have many database/storage/API providers connected.
194
+ const storageConnectionId = nodeData.storageConnectionId;
195
+ if (!storageConnectionId) {
196
+ throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}) has no storage connection — connect one on the node.`);
197
+ }
198
+ const storageResolved = await (0, connectionResolver_1.resolveStorageAdapter)(project, storageConnectionId, config, SYSTEM_AUTH);
199
+ if ('error' in storageResolved) {
200
+ throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}): ${storageResolved.error}`);
201
+ }
173
202
  const job = (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
174
203
  setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
175
204
  payload = { ...payload, jobId: job.jobId, jobType: 'ingest' };
@@ -78,6 +78,18 @@ export interface ProjectInfo {
78
78
  storageConnections?: StorageConnection[];
79
79
  pipelines?: PipelineRef[];
80
80
  builtinBackfilledAt?: Date;
81
+ workflow?: Workflow;
82
+ }
83
+ export type NodePersistence = 'database' | 'storage' | 'both' | 'none';
84
+ export interface Workflow {
85
+ id: string;
86
+ name: string;
87
+ description?: string;
88
+ status: string;
89
+ projectId: string;
90
+ nodeSchema: NodeSchema | null;
91
+ createdAt: Date;
92
+ updatedAt: Date;
81
93
  }
82
94
  export type ConnectionMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
83
95
  export interface Connection {
@@ -0,0 +1,3 @@
1
+ import { NodePersistence } from './index';
2
+ export declare const NODE_PERSISTENCE: Record<string, NodePersistence>;
3
+ export declare function getNodePersistence(nodeType: string): NodePersistence;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NODE_PERSISTENCE = void 0;
4
+ exports.getNodePersistence = getNodePersistence;
5
+ // Only node types that write to a real database/storage connection need an entry here.
6
+ // Process nodes (transcodeNode, captionNode, etc.) operate on a fileId already resolved by
7
+ // the internal job pipeline (mediaProcessor.ts), not directly on project connections, so
8
+ // they're 'none' — same for filter/deliverable/distribution/annotation/connection nodes.
9
+ exports.NODE_PERSISTENCE = {
10
+ dataSourceNode: 'database',
11
+ ingestNode: 'storage',
12
+ };
13
+ function getNodePersistence(nodeType) {
14
+ return exports.NODE_PERSISTENCE[nodeType] ?? 'none';
15
+ }
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "name": "streamby-core",
3
- "version": "0.31.0",
4
- "description": "StreamBy middleware framework for media storage management",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist"
9
- ],
10
- "scripts": {
11
- "build": "tsc",
12
- "test": "vitest",
13
- "dev": "ts-node-dev --respawn --clear dev.ts",
14
- "prepublishOnly": "npm run build"
15
- },
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/GwerhDev/StreamBy-Package.git"
19
- },
20
- "keywords": [
21
- "streamby",
22
- "middleware",
23
- "upload",
24
- "storage",
25
- "s3",
26
- "express",
27
- "typescript"
28
- ],
29
- "author": "TerminalCore Labs",
30
- "license": "MIT",
31
- "dependencies": {
32
- "@aws-sdk/client-s3": "^3.528.0",
33
- "@aws-sdk/s3-request-presigner": "^3.787.0",
34
- "@prisma/client": "^6.12.0",
35
- "bcrypt": "^5.1.1",
36
- "dotenv": "^16.5.0",
37
- "express": "^4.0.0",
38
- "mongodb": "^6.17.0",
39
- "mongoose": "^8.13.2",
40
- "node-fetch": "^3.0.0",
41
- "pg": "^8.16.3",
42
- "prisma": "^6.12.0",
43
- "ws": "^8.20.0"
44
- },
45
- "devDependencies": {
46
- "@types/bcrypt": "^5.0.2",
47
- "@types/cors": "^2.8.18",
48
- "@types/express": "^4.17.17",
49
- "@types/mongodb": "^4.0.7",
50
- "@types/node": "^22.15.21",
51
- "@types/pg": "^8.15.4",
52
- "@types/supertest": "^6.0.3",
53
- "@types/ws": "^8.18.1",
54
- "cors": "^2.8.5",
55
- "supertest": "^7.1.4",
56
- "ts-node-dev": "^2.0.0",
57
- "typescript": "^5.8.3",
58
- "vitest": "^3.1.1"
59
- },
60
- "peerDependencies": {
61
- "express": "^4.0.0"
62
- }
63
- }
1
+ {
2
+ "name": "streamby-core",
3
+ "version": "0.32.1",
4
+ "description": "StreamBy middleware framework for media storage management",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest",
13
+ "dev": "ts-node-dev --respawn --clear dev.ts",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/GwerhDev/StreamBy-Package.git"
19
+ },
20
+ "keywords": [
21
+ "streamby",
22
+ "middleware",
23
+ "upload",
24
+ "storage",
25
+ "s3",
26
+ "express",
27
+ "typescript"
28
+ ],
29
+ "author": "TerminalCore Labs",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@aws-sdk/client-s3": "^3.528.0",
33
+ "@aws-sdk/s3-request-presigner": "^3.787.0",
34
+ "@prisma/client": "^6.12.0",
35
+ "bcrypt": "^5.1.1",
36
+ "dotenv": "^16.5.0",
37
+ "express": "^4.0.0",
38
+ "mongodb": "^6.17.0",
39
+ "mongoose": "^8.13.2",
40
+ "node-fetch": "^3.0.0",
41
+ "pg": "^8.16.3",
42
+ "prisma": "^6.12.0",
43
+ "ws": "^8.20.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bcrypt": "^5.0.2",
47
+ "@types/cors": "^2.8.18",
48
+ "@types/express": "^4.17.17",
49
+ "@types/mongodb": "^4.0.7",
50
+ "@types/node": "^22.15.21",
51
+ "@types/pg": "^8.15.4",
52
+ "@types/supertest": "^6.0.3",
53
+ "@types/ws": "^8.18.1",
54
+ "cors": "^2.8.5",
55
+ "supertest": "^7.1.4",
56
+ "ts-node-dev": "^2.0.0",
57
+ "typescript": "^5.8.3",
58
+ "vitest": "^3.1.1"
59
+ },
60
+ "peerDependencies": {
61
+ "express": "^4.0.0"
62
+ }
63
+ }
@@ -1,3 +0,0 @@
1
- export declare function encrypt(text: string, secretKey: string): string;
2
- export declare function decrypt(text: string, secretKey: string): string;
3
- export declare function hash(text: string): string;
@@ -1,32 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.encrypt = encrypt;
7
- exports.decrypt = decrypt;
8
- exports.hash = hash;
9
- const crypto_1 = __importDefault(require("crypto"));
10
- const ALGORITHM = 'aes-256-cbc';
11
- const IV_LENGTH = 16;
12
- function encrypt(text, secretKey) {
13
- const iv = crypto_1.default.randomBytes(IV_LENGTH);
14
- const key = crypto_1.default.createHash('sha256').update(String(secretKey)).digest('base64').substr(0, 32);
15
- const cipher = crypto_1.default.createCipheriv(ALGORITHM, Buffer.from(key), iv);
16
- let encrypted = cipher.update(text);
17
- encrypted = Buffer.concat([encrypted, cipher.final()]);
18
- return iv.toString('hex') + ':' + encrypted.toString('hex');
19
- }
20
- function decrypt(text, secretKey) {
21
- const textParts = text.split(':');
22
- const iv = Buffer.from(textParts.shift(), 'hex');
23
- const encryptedText = Buffer.from(textParts.join(':'), 'hex');
24
- const key = crypto_1.default.createHash('sha256').update(String(secretKey)).digest('base64').substr(0, 32);
25
- const decipher = crypto_1.default.createDecipheriv(ALGORITHM, Buffer.from(key), iv);
26
- let decrypted = decipher.update(encryptedText);
27
- decrypted = Buffer.concat([decrypted, decipher.final()]);
28
- return decrypted.toString();
29
- }
30
- function hash(text) {
31
- return crypto_1.default.createHash('sha256').update(text).digest('hex');
32
- }