streamby-core 0.28.4 → 0.28.6

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.
@@ -24,6 +24,11 @@ const dbConnection_1 = require("./routes/dbConnection");
24
24
  const storageConnection_1 = require("./routes/storageConnection");
25
25
  const notification_1 = require("./routes/notification");
26
26
  const subscription_1 = require("./routes/subscription");
27
+ const media_1 = require("./routes/media");
28
+ const renderFarm_1 = require("./routes/renderFarm");
29
+ const review_1 = require("./routes/review");
30
+ const distribution_1 = require("./routes/distribution");
31
+ const ai_1 = require("./routes/ai");
27
32
  function createStreamByRouter(config) {
28
33
  const router = express_1.default.Router();
29
34
  if (config.encrypt)
@@ -39,6 +44,13 @@ function createStreamByRouter(config) {
39
44
  if (nosqlDbIds.length > 0) {
40
45
  (0, manager_1.registerModel)('notifications', nosqlDbIds, 'notifications', streambySchema);
41
46
  (0, manager_1.registerModel)('storage_files', nosqlDbIds, 'storage_files');
47
+ (0, manager_1.registerModel)('media_metadata', nosqlDbIds, 'media_metadata');
48
+ (0, manager_1.registerModel)('asset_versions', nosqlDbIds, 'asset_versions');
49
+ (0, manager_1.registerModel)('asset_dependency_graphs', nosqlDbIds, 'asset_dependency_graphs');
50
+ (0, manager_1.registerModel)('review_sessions', nosqlDbIds, 'review_sessions');
51
+ (0, manager_1.registerModel)('annotations', nosqlDbIds, 'annotations');
52
+ (0, manager_1.registerModel)('qc_reports', nosqlDbIds, 'qc_reports');
53
+ (0, manager_1.registerModel)('generative_jobs', nosqlDbIds, 'generative_jobs');
42
54
  }
43
55
  const mainDb = config.databases.find(db => db.main);
44
56
  if (mainDb) {
@@ -64,5 +76,10 @@ function createStreamByRouter(config) {
64
76
  router.use((0, storageConnection_1.storageConnectionRouter)(config));
65
77
  router.use((0, notification_1.notificationRouter)(config));
66
78
  router.use((0, subscription_1.subscriptionRouter)(config));
79
+ router.use((0, media_1.mediaRouter)(config));
80
+ router.use((0, renderFarm_1.renderFarmRouter)(config));
81
+ router.use((0, review_1.reviewRouter)(config));
82
+ router.use((0, distribution_1.distributionRouter)(config));
83
+ router.use((0, ai_1.aiRouter)(config));
67
84
  return router;
68
85
  }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function aiRouter(_config: StreamByConfig): Router;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aiRouter = aiRouter;
4
+ const express_1 = require("express");
5
+ const manager_1 = require("../../models/manager");
6
+ const auth_1 = require("../../utils/auth");
7
+ const jobQueue_1 = require("../../services/jobQueue");
8
+ const aiProcessor_1 = require("../../services/aiProcessor");
9
+ const AVAILABLE_NODE_TYPES = [
10
+ 'dataSourceNode', 'jsonInputNode', 'apiConnectionNode',
11
+ 'processNode', 'filterNode',
12
+ 'ingestNode', 'transcodeNode', 'captionNode', 'thumbnailNode',
13
+ 'renderJobNode', 'formatConvertNode', 'lodNode', 'assetDependencyNode',
14
+ 'reviewGateNode', 'annotationNode',
15
+ 'qcCheckNode', 'deliverableNode', 'distributionNode',
16
+ 'transcriptionNode', 'upscaleNode', 'proceduralAssetNode', 'pipelineSuggestNode',
17
+ ];
18
+ function aiRouter(_config) {
19
+ const router = (0, express_1.Router)();
20
+ const Project = (0, manager_1.getModel)('projects');
21
+ // POST /projects/:projectId/jobs/transcription
22
+ router.post('/projects/:projectId/jobs/transcription', async (req, res) => {
23
+ try {
24
+ const auth = req.auth;
25
+ const { projectId } = req.params;
26
+ const { fileId, model, sourceLanguage, outputFormats, provider, credentialId } = req.body;
27
+ if (!fileId)
28
+ return res.status(400).json({ message: 'fileId is required' });
29
+ const project = await Project.findOne({ _id: projectId });
30
+ if (!project)
31
+ return res.status(404).json({ message: 'Project not found' });
32
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
33
+ return res.status(403).json({ message: 'Unauthorized' });
34
+ const job = (0, jobQueue_1.createJob)('transcription', auth.userId, projectId, { fileId, model, sourceLanguage, outputFormats, provider, credentialId });
35
+ setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, { model, sourceLanguage, outputFormats, provider, credentialId }));
36
+ res.status(202).json({ jobId: job.jobId, status: job.status });
37
+ }
38
+ catch (err) {
39
+ res.status(500).json({ message: err.message });
40
+ }
41
+ });
42
+ // POST /projects/:projectId/jobs/upscale
43
+ router.post('/projects/:projectId/jobs/upscale', async (req, res) => {
44
+ try {
45
+ const auth = req.auth;
46
+ const { projectId } = req.params;
47
+ const { fileId, scale, model, mode, credentialId } = req.body;
48
+ if (!fileId)
49
+ return res.status(400).json({ message: 'fileId is required' });
50
+ const project = await Project.findOne({ _id: projectId });
51
+ if (!project)
52
+ return res.status(404).json({ message: 'Project not found' });
53
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
54
+ return res.status(403).json({ message: 'Unauthorized' });
55
+ const job = (0, jobQueue_1.createJob)('upscale', auth.userId, projectId, { fileId, scale, model, mode, credentialId });
56
+ setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, { scale, model, mode, credentialId }));
57
+ res.status(202).json({ jobId: job.jobId, status: job.status });
58
+ }
59
+ catch (err) {
60
+ res.status(500).json({ message: err.message });
61
+ }
62
+ });
63
+ // POST /projects/:projectId/jobs/generate-asset
64
+ router.post('/projects/:projectId/jobs/generate-asset', async (req, res) => {
65
+ try {
66
+ const auth = req.auth;
67
+ const { projectId } = req.params;
68
+ const { assetType, provider, prompt, seed, credentialId } = req.body;
69
+ if (!prompt)
70
+ return res.status(400).json({ message: 'prompt is required' });
71
+ const project = await Project.findOne({ _id: projectId });
72
+ if (!project)
73
+ return res.status(404).json({ message: 'Project not found' });
74
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
75
+ return res.status(403).json({ message: 'Unauthorized' });
76
+ const job = (0, jobQueue_1.createJob)('generate-asset', auth.userId, projectId, { assetType, provider, prompt, seed, credentialId });
77
+ setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, { assetType, provider, prompt, seed, credentialId }));
78
+ res.status(202).json({ jobId: job.jobId, status: job.status });
79
+ }
80
+ catch (err) {
81
+ res.status(500).json({ message: err.message });
82
+ }
83
+ });
84
+ // POST /projects/:projectId/pipeline-suggest
85
+ router.post('/projects/:projectId/pipeline-suggest', async (req, res) => {
86
+ try {
87
+ const auth = req.auth;
88
+ const { projectId } = req.params;
89
+ const { nodeSchema } = req.body;
90
+ if (!nodeSchema?.nodes)
91
+ return res.status(400).json({ message: 'nodeSchema is required' });
92
+ const project = await Project.findOne({ _id: projectId });
93
+ if (!project)
94
+ return res.status(404).json({ message: 'Project not found' });
95
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
96
+ return res.status(403).json({ message: 'Unauthorized' });
97
+ const suggestion = await (0, aiProcessor_1.buildPipelineSuggestion)(nodeSchema, AVAILABLE_NODE_TYPES);
98
+ res.json({ suggestion });
99
+ }
100
+ catch (err) {
101
+ res.status(500).json({ message: err.message });
102
+ }
103
+ });
104
+ return router;
105
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function distributionRouter(_config: StreamByConfig): Router;
@@ -0,0 +1,192 @@
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.distributionRouter = distributionRouter;
7
+ const express_1 = require("express");
8
+ const manager_1 = require("../../models/manager");
9
+ const auth_1 = require("../../utils/auth");
10
+ const wsHub_1 = require("../../services/wsHub");
11
+ const connectionManager_1 = require("../../adapters/database/connectionManager");
12
+ const crypto_1 = __importDefault(require("crypto"));
13
+ function getDb() {
14
+ const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
15
+ if (!ids.length)
16
+ return null;
17
+ return (0, connectionManager_1.getConnection)(ids[0]).client.db();
18
+ }
19
+ function broadcastDeliveryUpdate(userId, payload) {
20
+ (0, wsHub_1.emitToUser)(userId, { type: 'jobEvent', data: payload });
21
+ }
22
+ function distributionRouter(_config) {
23
+ const router = (0, express_1.Router)();
24
+ const Project = (0, manager_1.getModel)('projects');
25
+ const Export = (0, manager_1.getModel)('exports');
26
+ // ─── Distribution Connections ─────────────────────────────────────────────────
27
+ router.get('/projects/:projectId/distribution-connections', async (req, res) => {
28
+ try {
29
+ const auth = req.auth;
30
+ const { projectId } = req.params;
31
+ const project = await Project.findOne({ _id: projectId });
32
+ if (!project)
33
+ return res.status(404).json({ message: 'Project not found' });
34
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
35
+ return res.status(403).json({ message: 'Unauthorized' });
36
+ res.json({ connections: project.distributionConnections ?? [] });
37
+ }
38
+ catch (err) {
39
+ res.status(500).json({ message: err.message });
40
+ }
41
+ });
42
+ router.post('/projects/:projectId/distribution-connections', async (req, res) => {
43
+ try {
44
+ const auth = req.auth;
45
+ const { projectId } = req.params;
46
+ const { name, target, credentialId, config, description } = req.body;
47
+ if (!name || !target)
48
+ return res.status(400).json({ message: 'name and target are required' });
49
+ const project = await Project.findOne({ _id: projectId });
50
+ if (!project)
51
+ return res.status(404).json({ message: 'Project not found' });
52
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
53
+ return res.status(403).json({ message: 'Unauthorized' });
54
+ const connection = {
55
+ id: crypto_1.default.randomUUID(),
56
+ name,
57
+ target,
58
+ credentialId,
59
+ config: config ?? {},
60
+ description,
61
+ projectId,
62
+ createdAt: new Date(),
63
+ };
64
+ await Project.update({ _id: projectId }, { $push: { distributionConnections: connection } });
65
+ res.status(201).json({ connection });
66
+ }
67
+ catch (err) {
68
+ res.status(500).json({ message: err.message });
69
+ }
70
+ });
71
+ router.delete('/projects/:projectId/distribution-connections/:connectionId', async (req, res) => {
72
+ try {
73
+ const auth = req.auth;
74
+ const { projectId, connectionId } = req.params;
75
+ const project = await Project.findOne({ _id: projectId });
76
+ if (!project)
77
+ return res.status(404).json({ message: 'Project not found' });
78
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
79
+ return res.status(403).json({ message: 'Unauthorized' });
80
+ await Project.update({ _id: projectId }, { $pull: { distributionConnections: { id: connectionId } } });
81
+ res.json({ message: 'Connection deleted' });
82
+ }
83
+ catch (err) {
84
+ res.status(500).json({ message: err.message });
85
+ }
86
+ });
87
+ // ─── Publish Deliverable ──────────────────────────────────────────────────────
88
+ router.post('/projects/:projectId/deliverables/:exportId/publish', async (req, res) => {
89
+ try {
90
+ const auth = req.auth;
91
+ const { projectId, exportId } = req.params;
92
+ const { targets } = req.body;
93
+ if (!targets?.length)
94
+ return res.status(400).json({ message: 'targets array is required' });
95
+ const project = await Project.findOne({ _id: projectId });
96
+ if (!project)
97
+ return res.status(404).json({ message: 'Project not found' });
98
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
99
+ return res.status(403).json({ message: 'Unauthorized' });
100
+ const exportDoc = await Export.findOne({ id: exportId, projectId });
101
+ if (!exportDoc)
102
+ return res.status(404).json({ message: 'Deliverable not found' });
103
+ const connections = project.distributionConnections ?? [];
104
+ const deliveryTargets = targets.map(t => {
105
+ const conn = connections.find(c => c.id === t.connectionId);
106
+ return {
107
+ connectionId: t.connectionId,
108
+ target: conn?.target ?? 'customWebhook',
109
+ channel: t.channel,
110
+ status: 'pending',
111
+ };
112
+ });
113
+ const now = new Date();
114
+ await Export.update({ id: exportId, projectId }, {
115
+ $set: {
116
+ deliverableTargets: deliveryTargets,
117
+ publishedAt: now,
118
+ publishedBy: auth.userId,
119
+ updatedAt: now,
120
+ },
121
+ });
122
+ // Dispatch async — actual upload/API call happens in the host worker
123
+ setImmediate(() => {
124
+ for (const dt of deliveryTargets) {
125
+ broadcastDeliveryUpdate(auth.userId, {
126
+ jobId: crypto_1.default.randomUUID(),
127
+ jobType: 'distribute',
128
+ stage: 'queued',
129
+ progress: 5,
130
+ assetId: exportId,
131
+ message: `Queued publish to ${dt.target}`,
132
+ });
133
+ }
134
+ });
135
+ res.status(202).json({ deliverableTargets: deliveryTargets, publishedAt: now });
136
+ }
137
+ catch (err) {
138
+ res.status(500).json({ message: err.message });
139
+ }
140
+ });
141
+ // ─── QC Reports ───────────────────────────────────────────────────────────────
142
+ router.get('/projects/:projectId/assets/:assetId/qc-report', async (req, res) => {
143
+ try {
144
+ const auth = req.auth;
145
+ const { projectId, assetId } = req.params;
146
+ const project = await Project.findOne({ _id: projectId });
147
+ if (!project)
148
+ return res.status(404).json({ message: 'Project not found' });
149
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
150
+ return res.status(403).json({ message: 'Unauthorized' });
151
+ const db = getDb();
152
+ if (!db)
153
+ return res.status(500).json({ message: 'Database not available' });
154
+ const report = await db.collection('qc_reports').findOne({ assetId, projectId }, { sort: { generatedAt: -1 } });
155
+ res.json({ report: report ?? null });
156
+ }
157
+ catch (err) {
158
+ res.status(500).json({ message: err.message });
159
+ }
160
+ });
161
+ router.post('/projects/:projectId/assets/:assetId/qc-report', async (req, res) => {
162
+ try {
163
+ const auth = req.auth;
164
+ const { projectId, assetId } = req.params;
165
+ const { checks } = req.body;
166
+ if (!Array.isArray(checks))
167
+ return res.status(400).json({ message: 'checks array is required' });
168
+ const project = await Project.findOne({ _id: projectId });
169
+ if (!project)
170
+ return res.status(404).json({ message: 'Project not found' });
171
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
172
+ return res.status(403).json({ message: 'Unauthorized' });
173
+ const db = getDb();
174
+ if (!db)
175
+ return res.status(500).json({ message: 'Database not available' });
176
+ const overallPassed = checks.every((c) => c.passed === true);
177
+ const report = {
178
+ assetId,
179
+ projectId,
180
+ checks,
181
+ overallPassed,
182
+ generatedAt: new Date(),
183
+ };
184
+ await db.collection('qc_reports').insertOne({ ...report });
185
+ res.status(201).json({ report });
186
+ }
187
+ catch (err) {
188
+ res.status(500).json({ message: err.message });
189
+ }
190
+ });
191
+ return router;
192
+ }
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig, StorageAdapter } from '../../types';
3
+ export declare function mediaRouter(config: StreamByConfig & {
4
+ adapter?: StorageAdapter;
5
+ }): Router;
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mediaRouter = mediaRouter;
4
+ const express_1 = require("express");
5
+ const manager_1 = require("../../models/manager");
6
+ const auth_1 = require("../../utils/auth");
7
+ const jobQueue_1 = require("../../services/jobQueue");
8
+ const mediaProcessor_1 = require("../../services/mediaProcessor");
9
+ const storage_1 = require("../../providers/storage");
10
+ const connectionManager_1 = require("../../adapters/database/connectionManager");
11
+ function getMetadataCollection() {
12
+ const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
13
+ if (!ids.length)
14
+ return null;
15
+ return (0, connectionManager_1.getConnection)(ids[0]).client.db().collection('media_metadata');
16
+ }
17
+ function getVersionsCollection() {
18
+ const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
19
+ if (!ids.length)
20
+ return null;
21
+ return (0, connectionManager_1.getConnection)(ids[0]).client.db().collection('asset_versions');
22
+ }
23
+ function mediaRouter(config) {
24
+ const router = (0, express_1.Router)();
25
+ const adapter = config.adapter || (0, storage_1.createStorageProvider)(config.storageProviders);
26
+ const Project = (0, manager_1.getModel)('projects');
27
+ // GET /jobs/:jobId — poll job status
28
+ router.get('/jobs/:jobId', (req, res) => {
29
+ const job = (0, jobQueue_1.getJob)(req.params.jobId);
30
+ if (!job)
31
+ return res.status(404).json({ message: 'Job not found' });
32
+ res.json({ job });
33
+ });
34
+ // POST /projects/:projectId/jobs/ingest — queue an ingest job for an already-uploaded file
35
+ router.post('/projects/:projectId/jobs/ingest', async (req, res) => {
36
+ try {
37
+ const auth = req.auth;
38
+ const { projectId } = req.params;
39
+ const { fileId } = req.body;
40
+ if (!fileId)
41
+ return res.status(400).json({ message: 'fileId is required' });
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 job = (0, jobQueue_1.createJob)('ingest', auth.userId, projectId, { fileId });
48
+ // Run async — response returns immediately with jobId
49
+ setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
50
+ res.status(202).json({ jobId: job.jobId, status: job.status });
51
+ }
52
+ catch (err) {
53
+ res.status(500).json({ message: err.message });
54
+ }
55
+ });
56
+ // POST /projects/:projectId/jobs/transcode
57
+ router.post('/projects/:projectId/jobs/transcode', async (req, res) => {
58
+ try {
59
+ const auth = req.auth;
60
+ const { projectId } = req.params;
61
+ const { fileId, codec, resolution, outputFormat, bitrate, audioCodec } = req.body;
62
+ if (!fileId)
63
+ return res.status(400).json({ message: 'fileId is required' });
64
+ const project = await Project.findOne({ _id: projectId });
65
+ if (!project)
66
+ return res.status(404).json({ message: 'Project not found' });
67
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
68
+ return res.status(403).json({ message: 'Unauthorized' });
69
+ const job = (0, jobQueue_1.createJob)('transcode', auth.userId, projectId, {
70
+ fileId, codec, resolution, outputFormat, bitrate, audioCodec,
71
+ });
72
+ setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, { codec, resolution, outputFormat, bitrate, audioCodec }, adapter));
73
+ res.status(202).json({ jobId: job.jobId, status: job.status });
74
+ }
75
+ catch (err) {
76
+ res.status(500).json({ message: err.message });
77
+ }
78
+ });
79
+ // POST /projects/:projectId/jobs/thumbnail
80
+ router.post('/projects/:projectId/jobs/thumbnail', async (req, res) => {
81
+ try {
82
+ const auth = req.auth;
83
+ const { projectId } = req.params;
84
+ const { fileId, timecode, resolution, strategy } = req.body;
85
+ if (!fileId)
86
+ return res.status(400).json({ message: 'fileId is required' });
87
+ const project = await Project.findOne({ _id: projectId });
88
+ if (!project)
89
+ return res.status(404).json({ message: 'Project not found' });
90
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
91
+ return res.status(403).json({ message: 'Unauthorized' });
92
+ const job = (0, jobQueue_1.createJob)('thumbnail', auth.userId, projectId, { fileId, timecode, resolution, strategy });
93
+ setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, { timecode, resolution, strategy }));
94
+ res.status(202).json({ jobId: job.jobId, status: job.status });
95
+ }
96
+ catch (err) {
97
+ res.status(500).json({ message: err.message });
98
+ }
99
+ });
100
+ // POST /projects/:projectId/jobs/caption
101
+ router.post('/projects/:projectId/jobs/caption', async (req, res) => {
102
+ try {
103
+ const auth = req.auth;
104
+ const { projectId } = req.params;
105
+ const { fileId, sourceLanguage, outputFormat, provider } = req.body;
106
+ if (!fileId)
107
+ return res.status(400).json({ message: 'fileId is required' });
108
+ const project = await Project.findOne({ _id: projectId });
109
+ if (!project)
110
+ return res.status(404).json({ message: 'Project not found' });
111
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
112
+ return res.status(403).json({ message: 'Unauthorized' });
113
+ const job = (0, jobQueue_1.createJob)('caption', auth.userId, projectId, { fileId, sourceLanguage, outputFormat, provider });
114
+ setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, { sourceLanguage, outputFormat, provider }));
115
+ res.status(202).json({ jobId: job.jobId, status: job.status });
116
+ }
117
+ catch (err) {
118
+ res.status(500).json({ message: err.message });
119
+ }
120
+ });
121
+ // GET /projects/:projectId/assets/:fileId/metadata
122
+ router.get('/projects/:projectId/assets/:fileId/metadata', async (req, res) => {
123
+ try {
124
+ const auth = req.auth;
125
+ const { projectId, fileId } = req.params;
126
+ const project = await Project.findOne({ _id: projectId });
127
+ if (!project)
128
+ return res.status(404).json({ message: 'Project not found' });
129
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
130
+ return res.status(403).json({ message: 'Unauthorized' });
131
+ const col = getMetadataCollection();
132
+ if (!col)
133
+ return res.status(500).json({ message: 'Database not available' });
134
+ const meta = await col.findOne({ assetId: fileId, projectId });
135
+ res.json({ metadata: meta ?? null });
136
+ }
137
+ catch (err) {
138
+ res.status(500).json({ message: err.message });
139
+ }
140
+ });
141
+ // PATCH /projects/:projectId/assets/:fileId/metadata — update custom tags
142
+ router.patch('/projects/:projectId/assets/:fileId/metadata', async (req, res) => {
143
+ try {
144
+ const auth = req.auth;
145
+ const { projectId, fileId } = req.params;
146
+ const { customTags } = req.body;
147
+ if (!customTags || typeof customTags !== 'object') {
148
+ return res.status(400).json({ message: 'customTags object is required' });
149
+ }
150
+ const project = await Project.findOne({ _id: projectId });
151
+ if (!project)
152
+ return res.status(404).json({ message: 'Project not found' });
153
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
154
+ return res.status(403).json({ message: 'Unauthorized' });
155
+ const col = getMetadataCollection();
156
+ if (!col)
157
+ return res.status(500).json({ message: 'Database not available' });
158
+ await col.updateOne({ assetId: fileId, projectId }, { $set: { customTags, updatedAt: new Date() } }, { upsert: true });
159
+ res.json({ message: 'Metadata updated' });
160
+ }
161
+ catch (err) {
162
+ res.status(500).json({ message: err.message });
163
+ }
164
+ });
165
+ // GET /projects/:projectId/assets/:fileId/versions
166
+ router.get('/projects/:projectId/assets/:fileId/versions', async (req, res) => {
167
+ try {
168
+ const auth = req.auth;
169
+ const { projectId, fileId } = req.params;
170
+ const project = await Project.findOne({ _id: projectId });
171
+ if (!project)
172
+ return res.status(404).json({ message: 'Project not found' });
173
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
174
+ return res.status(403).json({ message: 'Unauthorized' });
175
+ const col = getVersionsCollection();
176
+ if (!col)
177
+ return res.status(500).json({ message: 'Database not available' });
178
+ const versions = await col
179
+ .find({ assetId: fileId, projectId })
180
+ .sort({ version: -1 })
181
+ .toArray();
182
+ res.json({ versions });
183
+ }
184
+ catch (err) {
185
+ res.status(500).json({ message: err.message });
186
+ }
187
+ });
188
+ return router;
189
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function renderFarmRouter(_config: StreamByConfig): Router;