streamby-core 0.29.6 → 0.29.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,283 +1,283 @@
1
- # @streamby/core
2
-
3
- Middleware framework for building storage-agnostic media APIs.
4
- Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs.
5
-
6
- ---
7
-
8
- ## 🚀 What is it?
9
-
10
- `@streamby/core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
11
-
12
- - ✅ AWS S3
13
- - ✅ Google Cloud Storage (soon)
14
- - ✅ Cloudflare R2 (soon)
15
- - ✅ Local/Personal servers (soon)
16
-
17
- It's designed to be installed inside your existing API as a library — **no need to run a separate backend**.
18
-
19
- ---
20
-
21
- ## 📦 Installation
22
-
23
- ```bash
24
- npm install @streamby/core ws
25
- npm install -D @types/ws
26
- ```
27
-
28
- ---
29
-
30
- ## 🧱 Basic usage
31
-
32
- ```ts
33
- import http from 'http';
34
- import express from 'express';
35
- import { WebSocketServer } from 'ws';
36
- import { createStreamByRouter } from '@streamby/core';
37
-
38
- const app = express();
39
- const server = http.createServer(app);
40
- const wss = new WebSocketServer({ server, path: '/streamby/ws' });
41
-
42
- app.use('/streamby', express.json(), createStreamByRouter({
43
- authProvider: async (req) => {
44
- // validate token/session and return user info
45
- return { userId: 'user-id', username: 'username', role: 'admin' };
46
- },
47
- databases: [
48
- { id: 'mongo', type: 'nosql', connectionString: process.env.MONGO_URI! },
49
- { id: 'postgres', type: 'sql', connectionString: process.env.POSTGRES_URI!, main: true },
50
- ],
51
- storageProviders: [
52
- {
53
- type: 's3',
54
- config: {
55
- bucket: process.env.AWS_BUCKET!,
56
- region: process.env.AWS_REGION!,
57
- accessKeyId: process.env.AWS_ACCESS_KEY!,
58
- secretAccessKey: process.env.AWS_SECRET_KEY!,
59
- },
60
- },
61
- ],
62
- encrypt: process.env.STREAMBY_ENCRYPTION_KEY,
63
- websocket: { server: wss },
64
- }));
65
-
66
- server.listen(3000);
67
- ```
68
-
69
- > **Important:** use `http.createServer(app)` and pass `server.listen()` instead of `app.listen()`.
70
- > This is required so the `WebSocketServer` shares the same port as the HTTP server.
71
-
72
- ---
73
-
74
- ## 🗄️ Database Schema Setup
75
-
76
- `@streamby/core` requires a pre-configured database schema for its internal operations. It does not manage database connections directly; instead, it expects the host application to provide initialized connection instances. This ensures full control and flexibility for the host environment.
77
-
78
- The library will only interact with the specified schema and will not touch other schemas in your database. The `reset` option is opt-in and will only drop and recreate the specified `streamby` schema if explicitly set to `true`.
79
-
80
- ### PostgreSQL Example
81
-
82
- ```ts
83
- import { Pool } from "pg";
84
- import { setupStreambyPg } from "@streamby/core/pg/setup"; // Adjust path as per your package structure
85
-
86
- const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
-
88
- // Call setupStreambyPg to ensure the schema and tables are created
89
- // reset: false (default) ensures idempotency and no data loss
90
- await setupStreambyPg({ pool, schema: "streamby", reset: false });
91
- ```
92
-
93
- ### MongoDB Example
94
-
95
- ```ts
96
- import { MongoClient } from "mongodb";
97
- import { setupStreambyMongo } from "@streamby/core/mongo/setup"; // Adjust path as per your package structure
98
-
99
- const client = new MongoClient(process.env.MONGO_URL!);
100
-
101
- // Connect the MongoDB client
102
- await client.connect();
103
-
104
- // Call setupStreambyMongo to ensure collections and indexes are created
105
- await setupStreambyMongo({ client, dbName: "streamby" });
106
- ```
107
-
108
- ```ts
109
- import express from 'express';
110
- import { createStreamByRouter } from '@streamby/core';
111
-
112
- const app = express();
113
-
114
- app.use('/streamby', createStreamByRouter({
115
- storageProvider: {
116
- type: 's3',
117
- config: {
118
- bucket: 'your-bucket-name',
119
- region: 'your-region',
120
- accessKeyId: 'YOUR_AWS_KEY',
121
- secretAccessKey: 'YOUR_AWS_SECRET'
122
- }
123
- },
124
- authProvider: async (req) => {
125
- // extract token/cookie and validate
126
- return {
127
- userId: 'demo',
128
- projects: ['demo-project'],
129
- role: 'admin',
130
- };
131
- },
132
- projectProvider: async (projectId) => {
133
- // fetch project from your DB
134
- return {
135
- id: projectId,
136
- name: 'Demo Project',
137
- description: 'Demo for StreamBy integration',
138
- rootFolders: [],
139
- settings: { allowUpload: true }
140
- };
141
- }
142
- }));
143
-
144
- app.listen(3000);
145
- ```
146
-
147
- ---
148
-
149
- ## 🧪 Testing
150
-
151
- ```bash
152
- npm run test
153
- ```
154
-
155
- Unit tests use [Vitest](https://vitest.dev) and [Supertest](https://www.npmjs.com/package/supertest).
156
-
157
- ---
158
-
159
- ## 📁 Features
160
-
161
- - 📂 Multi-project access control
162
- - 🔐 Project-aware file uploads & listings
163
- - 🧹 Modular adapters per storage provider
164
- - 🧰 Built-in testability with mock auth & storage
165
- - ✨ Simple to extend for custom business logic
166
-
167
- ---
168
-
169
- ## 🗺️ API Endpoints
170
-
171
- Here's a summary of the key API endpoints provided by `@streamby/core`:
172
-
173
- ### `GET /streamby/projects`
174
-
175
- Lists all projects accessible by the authenticated user.
176
-
177
- **Query Parameters:**
178
- - `archived`: (Optional) Filter projects by their archived status.
179
- - `true`: Returns only archived projects.
180
- - `false`: Returns only unarchived projects.
181
- - (Omitted): Returns all projects (both archived and unarchived).
182
-
183
- **Example Response:**
184
- ```json
185
- {
186
- "projects": [
187
- {
188
- "id": "project1_id",
189
- "dbType": "nosql",
190
- "name": "My NoSQL Project",
191
- "image": "url_to_image",
192
- "archived": false
193
- },
194
- {
195
- "id": "project2_id",
196
- "dbType": "sql",
197
- "name": "My SQL Project",
198
- "image": "url_to_image",
199
- "archived": true
200
- }
201
- ]
202
- }
203
- ```
204
-
205
- ### `PATCH /streamby/projects/:id/archive`
206
-
207
- Archives a specific project. After archiving, it returns the complete list of projects for the user, including the updated project.
208
-
209
- **Example Response:**
210
- ```json
211
- {
212
- "success": true,
213
- "projects": [
214
- {
215
- "id": "project1_id",
216
- "dbType": "nosql",
217
- "name": "My NoSQL Project",
218
- "image": "url_to_image",
219
- "archived": true
220
- },
221
- {
222
- "id": "project2_id",
223
- "dbType": "sql",
224
- "name": "My SQL Project",
225
- "image": "url_to_image",
226
- "archived": false
227
- }
228
- ]
229
- }
230
- ```
231
-
232
- ### `PATCH /streamby/projects/:id/unarchive`
233
-
234
- Unarchives a specific project. After unarchiving, it returns the complete list of projects for the user, including the updated project.
235
-
236
- **Example Response:**
237
- ```json
238
- {
239
- "success": true,
240
- "projects": [
241
- {
242
- "id": "project1_id",
243
- "dbType": "nosql",
244
- "name": "My NoSQL Project",
245
- "image": "url_to_image",
246
- "archived": false
247
- },
248
- {
249
- "id": "project2_id",
250
- "dbType": "sql",
251
- "name": "My SQL Project",
252
- "image": "url_to_image",
253
- "archived": false
254
- }
255
- ]
256
- }
257
- ```
258
-
259
- ---
260
-
261
- ## 🚣 Roadmap
262
-
263
- - [ ] Support for Google Cloud Storage
264
- - [ ] CLI tool for uploading files
265
- - [ ] Role-based access control middleware
266
- - [ ] Plugin system for extended routes
267
- - [ ] Streaming support (HLS, audio, etc.)
268
-
269
- ---
270
-
271
- ## 🧑‍💻 Maintained by
272
-
273
- **TerminalCore Labs** – part of [Nhexa Entertainment](https://nhexa.cl)
274
- Developed as open-source infrastructure for creative and media-oriented apps.
275
-
276
- Visit us [here](https://terminalcore.cl)
277
-
278
- ---
279
-
280
- ## 📟 License
281
-
282
- [MIT](./LICENSE)
283
-
1
+ # @streamby/core
2
+
3
+ Middleware framework for building storage-agnostic media APIs.
4
+ Part of the [StreamBy](https://streamby.nhexa.cl) ecosystem by TerminalCore Labs.
5
+
6
+ ---
7
+
8
+ ## 🚀 What is it?
9
+
10
+ `@streamby/core` is a plug-and-play middleware for Express (or compatible frameworks) that enables file uploads, listings and **multi-project access** over services like:
11
+
12
+ - ✅ AWS S3
13
+ - ✅ Google Cloud Storage (soon)
14
+ - ✅ Cloudflare R2 (soon)
15
+ - ✅ Local/Personal servers (soon)
16
+
17
+ It's designed to be installed inside your existing API as a library — **no need to run a separate backend**.
18
+
19
+ ---
20
+
21
+ ## 📦 Installation
22
+
23
+ ```bash
24
+ npm install @streamby/core ws
25
+ npm install -D @types/ws
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 🧱 Basic usage
31
+
32
+ ```ts
33
+ import http from 'http';
34
+ import express from 'express';
35
+ import { WebSocketServer } from 'ws';
36
+ import { createStreamByRouter } from '@streamby/core';
37
+
38
+ const app = express();
39
+ const server = http.createServer(app);
40
+ const wss = new WebSocketServer({ server, path: '/streamby/ws' });
41
+
42
+ app.use('/streamby', express.json(), createStreamByRouter({
43
+ authProvider: async (req) => {
44
+ // validate token/session and return user info
45
+ return { userId: 'user-id', username: 'username', role: 'admin' };
46
+ },
47
+ databases: [
48
+ { id: 'mongo', type: 'nosql', connectionString: process.env.MONGO_URI! },
49
+ { id: 'postgres', type: 'sql', connectionString: process.env.POSTGRES_URI!, main: true },
50
+ ],
51
+ storageProviders: [
52
+ {
53
+ type: 's3',
54
+ config: {
55
+ bucket: process.env.AWS_BUCKET!,
56
+ region: process.env.AWS_REGION!,
57
+ accessKeyId: process.env.AWS_ACCESS_KEY!,
58
+ secretAccessKey: process.env.AWS_SECRET_KEY!,
59
+ },
60
+ },
61
+ ],
62
+ encrypt: process.env.STREAMBY_ENCRYPTION_KEY,
63
+ websocket: { server: wss },
64
+ }));
65
+
66
+ server.listen(3000);
67
+ ```
68
+
69
+ > **Important:** use `http.createServer(app)` and pass `server.listen()` instead of `app.listen()`.
70
+ > This is required so the `WebSocketServer` shares the same port as the HTTP server.
71
+
72
+ ---
73
+
74
+ ## 🗄️ Database Schema Setup
75
+
76
+ `@streamby/core` requires a pre-configured database schema for its internal operations. It does not manage database connections directly; instead, it expects the host application to provide initialized connection instances. This ensures full control and flexibility for the host environment.
77
+
78
+ The library will only interact with the specified schema and will not touch other schemas in your database. The `reset` option is opt-in and will only drop and recreate the specified `streamby` schema if explicitly set to `true`.
79
+
80
+ ### PostgreSQL Example
81
+
82
+ ```ts
83
+ import { Pool } from "pg";
84
+ import { setupStreambyPg } from "@streamby/core/pg/setup"; // Adjust path as per your package structure
85
+
86
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
+
88
+ // Call setupStreambyPg to ensure the schema and tables are created
89
+ // reset: false (default) ensures idempotency and no data loss
90
+ await setupStreambyPg({ pool, schema: "streamby", reset: false });
91
+ ```
92
+
93
+ ### MongoDB Example
94
+
95
+ ```ts
96
+ import { MongoClient } from "mongodb";
97
+ import { setupStreambyMongo } from "@streamby/core/mongo/setup"; // Adjust path as per your package structure
98
+
99
+ const client = new MongoClient(process.env.MONGO_URL!);
100
+
101
+ // Connect the MongoDB client
102
+ await client.connect();
103
+
104
+ // Call setupStreambyMongo to ensure collections and indexes are created
105
+ await setupStreambyMongo({ client, dbName: "streamby" });
106
+ ```
107
+
108
+ ```ts
109
+ import express from 'express';
110
+ import { createStreamByRouter } from '@streamby/core';
111
+
112
+ const app = express();
113
+
114
+ app.use('/streamby', createStreamByRouter({
115
+ storageProvider: {
116
+ type: 's3',
117
+ config: {
118
+ bucket: 'your-bucket-name',
119
+ region: 'your-region',
120
+ accessKeyId: 'YOUR_AWS_KEY',
121
+ secretAccessKey: 'YOUR_AWS_SECRET'
122
+ }
123
+ },
124
+ authProvider: async (req) => {
125
+ // extract token/cookie and validate
126
+ return {
127
+ userId: 'demo',
128
+ projects: ['demo-project'],
129
+ role: 'admin',
130
+ };
131
+ },
132
+ projectProvider: async (projectId) => {
133
+ // fetch project from your DB
134
+ return {
135
+ id: projectId,
136
+ name: 'Demo Project',
137
+ description: 'Demo for StreamBy integration',
138
+ rootFolders: [],
139
+ settings: { allowUpload: true }
140
+ };
141
+ }
142
+ }));
143
+
144
+ app.listen(3000);
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 🧪 Testing
150
+
151
+ ```bash
152
+ npm run test
153
+ ```
154
+
155
+ Unit tests use [Vitest](https://vitest.dev) and [Supertest](https://www.npmjs.com/package/supertest).
156
+
157
+ ---
158
+
159
+ ## 📁 Features
160
+
161
+ - 📂 Multi-project access control
162
+ - 🔐 Project-aware file uploads & listings
163
+ - 🧹 Modular adapters per storage provider
164
+ - 🧰 Built-in testability with mock auth & storage
165
+ - ✨ Simple to extend for custom business logic
166
+
167
+ ---
168
+
169
+ ## 🗺️ API Endpoints
170
+
171
+ Here's a summary of the key API endpoints provided by `@streamby/core`:
172
+
173
+ ### `GET /streamby/projects`
174
+
175
+ Lists all projects accessible by the authenticated user.
176
+
177
+ **Query Parameters:**
178
+ - `archived`: (Optional) Filter projects by their archived status.
179
+ - `true`: Returns only archived projects.
180
+ - `false`: Returns only unarchived projects.
181
+ - (Omitted): Returns all projects (both archived and unarchived).
182
+
183
+ **Example Response:**
184
+ ```json
185
+ {
186
+ "projects": [
187
+ {
188
+ "id": "project1_id",
189
+ "dbType": "nosql",
190
+ "name": "My NoSQL Project",
191
+ "image": "url_to_image",
192
+ "archived": false
193
+ },
194
+ {
195
+ "id": "project2_id",
196
+ "dbType": "sql",
197
+ "name": "My SQL Project",
198
+ "image": "url_to_image",
199
+ "archived": true
200
+ }
201
+ ]
202
+ }
203
+ ```
204
+
205
+ ### `PATCH /streamby/projects/:id/archive`
206
+
207
+ Archives a specific project. After archiving, it returns the complete list of projects for the user, including the updated project.
208
+
209
+ **Example Response:**
210
+ ```json
211
+ {
212
+ "success": true,
213
+ "projects": [
214
+ {
215
+ "id": "project1_id",
216
+ "dbType": "nosql",
217
+ "name": "My NoSQL Project",
218
+ "image": "url_to_image",
219
+ "archived": true
220
+ },
221
+ {
222
+ "id": "project2_id",
223
+ "dbType": "sql",
224
+ "name": "My SQL Project",
225
+ "image": "url_to_image",
226
+ "archived": false
227
+ }
228
+ ]
229
+ }
230
+ ```
231
+
232
+ ### `PATCH /streamby/projects/:id/unarchive`
233
+
234
+ Unarchives a specific project. After unarchiving, it returns the complete list of projects for the user, including the updated project.
235
+
236
+ **Example Response:**
237
+ ```json
238
+ {
239
+ "success": true,
240
+ "projects": [
241
+ {
242
+ "id": "project1_id",
243
+ "dbType": "nosql",
244
+ "name": "My NoSQL Project",
245
+ "image": "url_to_image",
246
+ "archived": false
247
+ },
248
+ {
249
+ "id": "project2_id",
250
+ "dbType": "sql",
251
+ "name": "My SQL Project",
252
+ "image": "url_to_image",
253
+ "archived": false
254
+ }
255
+ ]
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 🚣 Roadmap
262
+
263
+ - [ ] Support for Google Cloud Storage
264
+ - [ ] CLI tool for uploading files
265
+ - [ ] Role-based access control middleware
266
+ - [ ] Plugin system for extended routes
267
+ - [ ] Streaming support (HLS, audio, etc.)
268
+
269
+ ---
270
+
271
+ ## 🧑‍💻 Maintained by
272
+
273
+ **TerminalCore Labs** – part of [Nhexa Entertainment](https://nhexa.cl)
274
+ Developed as open-source infrastructure for creative and media-oriented apps.
275
+
276
+ Visit us [here](https://terminalcore.cl)
277
+
278
+ ---
279
+
280
+ ## 📟 License
281
+
282
+ [MIT](./LICENSE)
283
+
@@ -43,85 +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";
46
+ await pool.query(`
47
+ CREATE SCHEMA IF NOT EXISTS streamby;
48
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
49
49
  `);
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 $$;
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
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
+ 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
+ );
72
72
  `);
73
73
  console.log('✅ "projects" table ensured to exist.');
74
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 '[]';
81
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
82
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
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 '[]';
81
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
82
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
83
83
  `);
84
84
  // Ensure dbType exists with a non-null default
85
- await pool.query(`
86
- ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
85
+ await pool.query(`
86
+ ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
87
87
  `);
88
- await pool.query(`
89
- CREATE TABLE IF NOT EXISTS streamby.project_members (
90
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
91
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
92
- "userId" VARCHAR(255) NOT NULL,
93
- role VARCHAR(255) NOT NULL DEFAULT 'member',
94
- status VARCHAR(50) NOT NULL DEFAULT 'active',
95
- archived BOOLEAN NOT NULL DEFAULT FALSE,
96
- "archivedBy" VARCHAR(255),
97
- "archivedAt" TIMESTAMP WITH TIME ZONE,
98
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
99
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
100
- UNIQUE ("projectId", "userId")
101
- );
88
+ await pool.query(`
89
+ CREATE TABLE IF NOT EXISTS streamby.project_members (
90
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
91
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
92
+ "userId" VARCHAR(255) NOT NULL,
93
+ role VARCHAR(255) NOT NULL DEFAULT 'member',
94
+ status VARCHAR(50) NOT NULL DEFAULT 'active',
95
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
96
+ "archivedBy" VARCHAR(255),
97
+ "archivedAt" TIMESTAMP WITH TIME ZONE,
98
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
99
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
100
+ UNIQUE ("projectId", "userId")
101
+ );
102
102
  `);
103
- await pool.query(`
104
- ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
103
+ await pool.query(`
104
+ ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
105
105
  `);
106
106
  console.log('✅ "project_members" table ensured to exist.');
107
107
  // Migrate projectId FK column from UUID to TEXT if table predates this schema version
108
- await pool.query(`
109
- DO $$ BEGIN
110
- ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
111
- ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
112
- ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
113
- FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
114
- EXCEPTION WHEN OTHERS THEN NULL; END $$;
108
+ await pool.query(`
109
+ DO $$ BEGIN
110
+ ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
111
+ ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
112
+ ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
113
+ FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
114
+ EXCEPTION WHEN OTHERS THEN NULL; END $$;
115
115
  `);
116
- await pool.query(`
117
- CREATE TABLE IF NOT EXISTS streamby.exports (
118
- id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
119
- "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
120
- status VARCHAR(50) NOT NULL DEFAULT 'pending',
121
- "filePath" TEXT,
122
- "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
123
- "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
124
- );
116
+ await pool.query(`
117
+ CREATE TABLE IF NOT EXISTS streamby.exports (
118
+ id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
119
+ "projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
120
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
121
+ "filePath" TEXT,
122
+ "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
123
+ "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
124
+ );
125
125
  `);
126
126
  console.log('✅ "exports" table ensured to exist.');
127
127
  }
@@ -10,7 +10,7 @@ function serializeForPg(v) {
10
10
  }
11
11
  const createSQLExportTable = async (connection, projectId, exportName, nodeSchema) => {
12
12
  const exportId = (0, crypto_1.randomUUID)();
13
- await connection.query(`INSERT INTO "exports" (id, "projectId", node_schema, name, method, created_at, updated_at)
13
+ await connection.query(`INSERT INTO "exports" (id, "projectId", node_schema, name, method, created_at, updated_at)
14
14
  VALUES ($1, $2, $3, $4, 'GET', NOW(), NOW())`, [exportId, projectId, nodeSchema ? JSON.stringify(nodeSchema) : null, exportName]);
15
15
  return { exportId };
16
16
  };
@@ -32,6 +32,7 @@ const ai_1 = require("./routes/ai");
32
32
  const production_1 = require("./routes/production");
33
33
  const rights_1 = require("./routes/rights");
34
34
  const workflow_1 = require("./routes/workflow");
35
+ const pipeline_1 = require("./routes/pipeline");
35
36
  function createStreamByRouter(config) {
36
37
  const router = express_1.default.Router();
37
38
  if (config.encrypt)
@@ -104,5 +105,6 @@ function createStreamByRouter(config) {
104
105
  router.use((0, production_1.productionRouter)(config));
105
106
  router.use((0, rights_1.rightsRouter)(config));
106
107
  router.use((0, workflow_1.workflowRouter)(config));
108
+ router.use((0, pipeline_1.pipelineRouter)(config));
107
109
  return router;
108
110
  }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function pipelineRouter(_config: StreamByConfig): Router;
@@ -0,0 +1,171 @@
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.pipelineRouter = pipelineRouter;
7
+ const express_1 = require("express");
8
+ const auth_1 = require("../../utils/auth");
9
+ const connectionManager_1 = require("../../adapters/database/connectionManager");
10
+ const manager_1 = require("../../models/manager");
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ function getDb() {
13
+ const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
14
+ if (!ids.length)
15
+ return null;
16
+ return (0, connectionManager_1.getConnection)(ids[0]).client.db();
17
+ }
18
+ // A Pipeline is a sub-workflow scoped to a project. The `pipelines` collection is the
19
+ // source of truth (holds the full nodeSchema); ProjectInfo.pipelines mirrors lightweight
20
+ // refs so the sidebar can list them without loading every schema.
21
+ function pipelineRouter(_config) {
22
+ const router = (0, express_1.Router)();
23
+ const Project = (0, manager_1.getModel)('projects');
24
+ const toRef = (p) => ({ id: p.id, name: p.name, order: p.order });
25
+ // Rewrite ProjectInfo.pipelines from the current collection state, sorted by order.
26
+ const syncProjectRefs = async (projectId) => {
27
+ const db = getDb();
28
+ if (!db)
29
+ return;
30
+ const all = await db.collection('pipelines')
31
+ .find({ projectId })
32
+ .project({ _id: 0, id: 1, name: 1, order: 1 })
33
+ .sort({ order: 1, createdAt: 1 })
34
+ .toArray();
35
+ const refs = all.map(p => toRef(p));
36
+ await Project.update({ _id: projectId }, { pipelines: refs });
37
+ };
38
+ router.get('/projects/:projectId/pipelines', async (req, res) => {
39
+ try {
40
+ const auth = req.auth;
41
+ const { projectId } = req.params;
42
+ const project = await Project.findOne({ _id: projectId });
43
+ if (!project)
44
+ return res.status(404).json({ message: 'Project not found' });
45
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
46
+ return res.status(403).json({ message: 'Unauthorized' });
47
+ const db = getDb();
48
+ if (!db)
49
+ return res.status(500).json({ message: 'Database not available' });
50
+ const pipelines = await db.collection('pipelines')
51
+ .find({ projectId })
52
+ .sort({ order: 1, createdAt: 1 })
53
+ .toArray();
54
+ res.json({ pipelines });
55
+ }
56
+ catch (err) {
57
+ res.status(500).json({ message: err.message });
58
+ }
59
+ });
60
+ router.post('/projects/:projectId/pipelines', async (req, res) => {
61
+ try {
62
+ const auth = req.auth;
63
+ const { projectId } = req.params;
64
+ const { name, description, order } = req.body;
65
+ if (!name)
66
+ return res.status(400).json({ message: 'name is required' });
67
+ const project = await Project.findOne({ _id: projectId });
68
+ if (!project)
69
+ return res.status(404).json({ message: 'Project not found' });
70
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
71
+ return res.status(403).json({ message: 'Unauthorized' });
72
+ const db = getDb();
73
+ if (!db)
74
+ return res.status(500).json({ message: 'Database not available' });
75
+ const now = new Date();
76
+ const pipeline = {
77
+ id: crypto_1.default.randomUUID(),
78
+ projectId,
79
+ name,
80
+ description: description ?? null,
81
+ order: order ?? (project.pipelines?.length ?? 0),
82
+ nodeSchema: null,
83
+ createdBy: auth.userId,
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ };
87
+ await db.collection('pipelines').insertOne({ ...pipeline });
88
+ await syncProjectRefs(projectId);
89
+ res.status(201).json({ pipeline });
90
+ }
91
+ catch (err) {
92
+ res.status(500).json({ message: err.message });
93
+ }
94
+ });
95
+ router.get('/projects/:projectId/pipelines/:pipelineId', async (req, res) => {
96
+ try {
97
+ const auth = req.auth;
98
+ const { projectId, pipelineId } = req.params;
99
+ const project = await Project.findOne({ _id: projectId });
100
+ if (!project)
101
+ return res.status(404).json({ message: 'Project not found' });
102
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
103
+ return res.status(403).json({ message: 'Unauthorized' });
104
+ const db = getDb();
105
+ if (!db)
106
+ return res.status(500).json({ message: 'Database not available' });
107
+ const pipeline = await db.collection('pipelines').findOne({ id: pipelineId, projectId });
108
+ if (!pipeline)
109
+ return res.status(404).json({ message: 'Pipeline not found' });
110
+ res.json({ pipeline });
111
+ }
112
+ catch (err) {
113
+ res.status(500).json({ message: err.message });
114
+ }
115
+ });
116
+ router.patch('/projects/:projectId/pipelines/:pipelineId', async (req, res) => {
117
+ try {
118
+ const auth = req.auth;
119
+ const { projectId, pipelineId } = req.params;
120
+ const project = await Project.findOne({ _id: projectId });
121
+ if (!project)
122
+ return res.status(404).json({ message: 'Project not found' });
123
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
124
+ return res.status(403).json({ message: 'Unauthorized' });
125
+ const db = getDb();
126
+ if (!db)
127
+ return res.status(500).json({ message: 'Database not available' });
128
+ const { name, description, nodeSchema, order } = req.body;
129
+ const $set = { updatedAt: new Date() };
130
+ if (name !== undefined)
131
+ $set.name = name;
132
+ if (description !== undefined)
133
+ $set.description = description;
134
+ if (nodeSchema !== undefined)
135
+ $set.nodeSchema = nodeSchema;
136
+ if (order !== undefined)
137
+ $set.order = order;
138
+ const result = await db.collection('pipelines').findOneAndUpdate({ id: pipelineId, projectId }, { $set }, { returnDocument: 'after' });
139
+ if (!result)
140
+ return res.status(404).json({ message: 'Pipeline not found' });
141
+ // Only name/order affect the lightweight refs.
142
+ if (name !== undefined || order !== undefined)
143
+ await syncProjectRefs(projectId);
144
+ res.json({ pipeline: result });
145
+ }
146
+ catch (err) {
147
+ res.status(500).json({ message: err.message });
148
+ }
149
+ });
150
+ router.delete('/projects/:projectId/pipelines/:pipelineId', async (req, res) => {
151
+ try {
152
+ const auth = req.auth;
153
+ const { projectId, pipelineId } = req.params;
154
+ const project = await Project.findOne({ _id: projectId });
155
+ if (!project)
156
+ return res.status(404).json({ message: 'Project not found' });
157
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
158
+ return res.status(403).json({ message: 'Unauthorized' });
159
+ const db = getDb();
160
+ if (!db)
161
+ return res.status(500).json({ message: 'Database not available' });
162
+ await db.collection('pipelines').deleteOne({ id: pipelineId, projectId });
163
+ await syncProjectRefs(projectId);
164
+ res.json({ message: 'Pipeline deleted' });
165
+ }
166
+ catch (err) {
167
+ res.status(500).json({ message: err.message });
168
+ }
169
+ });
170
+ return router;
171
+ }
@@ -9,11 +9,9 @@ const mongodb_1 = require("mongodb");
9
9
  function workflowRouter(config) {
10
10
  const router = (0, express_1.Router)();
11
11
  const Project = (0, manager_1.getModel)('projects');
12
- // A project has exactly one Workflow (the central canvas). It is stored as the
13
- // single `project.workflow` object. Reads fall back to the legacy `workflows[0]`
14
- // array so pre-migration data keeps working; PATCH upserts and writes only the
15
- // new single field (and clears the legacy array on Mongo).
16
- const resolveWorkflow = (project) => project.workflow ?? (project.workflows ?? [])[0] ?? null;
12
+ // A project has exactly one Workflow (the central canvas), stored as the single
13
+ // `project.workflow` object. PATCH upserts it.
14
+ const resolveWorkflow = (project) => project.workflow ?? null;
17
15
  router.get('/projects/:id/workflow', async (req, res) => {
18
16
  try {
19
17
  const auth = await config.authProvider(req);
@@ -70,7 +68,7 @@ function workflowRouter(config) {
70
68
  await SqlProject.update({ _id: projectId }, { workflow: updated });
71
69
  }
72
70
  else {
73
- await Project.update({ _id: new mongodb_1.ObjectId(projectId) }, { $set: { workflow: updated }, $unset: { workflows: '' } });
71
+ await Project.update({ _id: new mongodb_1.ObjectId(projectId) }, { $set: { workflow: updated } });
74
72
  }
75
73
  res.json({ data: updated, message: 'Workflow updated successfully' });
76
74
  }
@@ -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.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
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 => ({
@@ -219,10 +219,10 @@ class Model {
219
219
  else {
220
220
  const matchKey = value.id !== undefined ? 'id' : Object.keys(value)[0];
221
221
  const matchVal = String(value[matchKey]);
222
- await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
- SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
- FROM jsonb_array_elements("${field}") elem
225
- WHERE elem->>'${matchKey}' != $1
222
+ await connection.query(`UPDATE ${tbl} SET "${field}" = (
223
+ SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
224
+ FROM jsonb_array_elements("${field}") elem
225
+ WHERE elem->>'${matchKey}' != $1
226
226
  ) WHERE id = $2`, [matchVal, id]);
227
227
  }
228
228
  }
@@ -73,6 +73,7 @@ export interface ProjectInfo {
73
73
  apiConnections?: ApiConnection[];
74
74
  dbConnections?: DbConnection[];
75
75
  storageConnections?: StorageConnection[];
76
+ pipelines?: PipelineRef[];
76
77
  }
77
78
  export interface Credential {
78
79
  id: string;
@@ -168,6 +169,23 @@ export interface NodeSchema {
168
169
  nodes: any[];
169
170
  edges: any[];
170
171
  }
172
+ export interface Pipeline {
173
+ _id?: string;
174
+ id: string;
175
+ projectId: string;
176
+ name: string;
177
+ description?: string | null;
178
+ order: number;
179
+ nodeSchema: NodeSchema | null;
180
+ createdBy: string;
181
+ createdAt: Date;
182
+ updatedAt: Date;
183
+ }
184
+ export interface PipelineRef {
185
+ id: string;
186
+ name: string;
187
+ order: number;
188
+ }
171
189
  export interface Export {
172
190
  _id?: string;
173
191
  id: string;
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "name": "streamby-core",
3
- "version": "0.29.6",
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.29.8",
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
- }