streamby-core 0.28.5 → 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.
@@ -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;
@@ -0,0 +1,158 @@
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.renderFarmRouter = renderFarmRouter;
7
+ const express_1 = require("express");
8
+ const manager_1 = require("../../models/manager");
9
+ const auth_1 = require("../../utils/auth");
10
+ const jobQueue_1 = require("../../services/jobQueue");
11
+ const vfxProcessor_1 = require("../../services/vfxProcessor");
12
+ const crypto_1 = __importDefault(require("crypto"));
13
+ function renderFarmRouter(_config) {
14
+ const router = (0, express_1.Router)();
15
+ const Project = (0, manager_1.getModel)('projects');
16
+ // ─── Render Farm Connections ─────────────────────────────────────────────────
17
+ router.get('/projects/:projectId/render-farm-connections', async (req, res) => {
18
+ try {
19
+ const auth = req.auth;
20
+ const { projectId } = req.params;
21
+ const project = await Project.findOne({ _id: projectId });
22
+ if (!project)
23
+ return res.status(404).json({ message: 'Project not found' });
24
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
25
+ return res.status(403).json({ message: 'Unauthorized' });
26
+ res.json({ connections: project.renderFarmConnections ?? [] });
27
+ }
28
+ catch (err) {
29
+ res.status(500).json({ message: err.message });
30
+ }
31
+ });
32
+ router.post('/projects/:projectId/render-farm-connections', async (req, res) => {
33
+ try {
34
+ const auth = req.auth;
35
+ const { projectId } = req.params;
36
+ const { name, provider, apiUrl, credentialId, description } = req.body;
37
+ if (!name || !provider || !apiUrl) {
38
+ return res.status(400).json({ message: 'name, provider and apiUrl are required' });
39
+ }
40
+ const project = await Project.findOne({ _id: projectId });
41
+ if (!project)
42
+ return res.status(404).json({ message: 'Project not found' });
43
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
44
+ return res.status(403).json({ message: 'Unauthorized' });
45
+ const connection = {
46
+ id: crypto_1.default.randomUUID(),
47
+ name,
48
+ provider,
49
+ apiUrl,
50
+ credentialId,
51
+ description,
52
+ projectId,
53
+ createdAt: new Date(),
54
+ };
55
+ await Project.update({ _id: projectId }, { $push: { renderFarmConnections: connection } });
56
+ res.status(201).json({ connection });
57
+ }
58
+ catch (err) {
59
+ res.status(500).json({ message: err.message });
60
+ }
61
+ });
62
+ router.delete('/projects/:projectId/render-farm-connections/:connectionId', async (req, res) => {
63
+ try {
64
+ const auth = req.auth;
65
+ const { projectId, connectionId } = req.params;
66
+ const project = await Project.findOne({ _id: projectId });
67
+ if (!project)
68
+ return res.status(404).json({ message: 'Project not found' });
69
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
70
+ return res.status(403).json({ message: 'Unauthorized' });
71
+ await Project.update({ _id: projectId }, { $pull: { renderFarmConnections: { id: connectionId } } });
72
+ res.json({ message: 'Connection deleted' });
73
+ }
74
+ catch (err) {
75
+ res.status(500).json({ message: err.message });
76
+ }
77
+ });
78
+ // ─── VFX Jobs ────────────────────────────────────────────────────────────────
79
+ router.post('/projects/:projectId/jobs/render', async (req, res) => {
80
+ try {
81
+ const auth = req.auth;
82
+ const { projectId } = req.params;
83
+ const { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat } = req.body;
84
+ if (!fileId)
85
+ return res.status(400).json({ message: 'fileId is required' });
86
+ const project = await Project.findOne({ _id: projectId });
87
+ if (!project)
88
+ return res.status(404).json({ message: 'Project not found' });
89
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
90
+ return res.status(403).json({ message: 'Unauthorized' });
91
+ const job = (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
92
+ setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, { renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat }));
93
+ res.status(202).json({ jobId: job.jobId, status: job.status });
94
+ }
95
+ catch (err) {
96
+ res.status(500).json({ message: err.message });
97
+ }
98
+ });
99
+ router.post('/projects/:projectId/jobs/format-convert', async (req, res) => {
100
+ try {
101
+ const auth = req.auth;
102
+ const { projectId } = req.params;
103
+ const { fileId, inputFormat, outputFormat, applyTransforms, embedTextures } = req.body;
104
+ if (!fileId)
105
+ return res.status(400).json({ message: 'fileId is required' });
106
+ const project = await Project.findOne({ _id: projectId });
107
+ if (!project)
108
+ return res.status(404).json({ message: 'Project not found' });
109
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
110
+ return res.status(403).json({ message: 'Unauthorized' });
111
+ const job = (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
112
+ setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, { inputFormat, outputFormat, applyTransforms, embedTextures }));
113
+ res.status(202).json({ jobId: job.jobId, status: job.status });
114
+ }
115
+ catch (err) {
116
+ res.status(500).json({ message: err.message });
117
+ }
118
+ });
119
+ router.post('/projects/:projectId/jobs/lod', async (req, res) => {
120
+ try {
121
+ const auth = req.auth;
122
+ const { projectId } = req.params;
123
+ const { fileId, levels, reductionRatios, algorithm, outputFormat } = req.body;
124
+ if (!fileId)
125
+ return res.status(400).json({ message: 'fileId is required' });
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 job = (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
132
+ setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, { levels, reductionRatios, algorithm, outputFormat }));
133
+ res.status(202).json({ jobId: job.jobId, status: job.status });
134
+ }
135
+ catch (err) {
136
+ res.status(500).json({ message: err.message });
137
+ }
138
+ });
139
+ // ─── Asset Dependency Graph ──────────────────────────────────────────────────
140
+ router.get('/projects/:projectId/assets/:fileId/dependency-graph', async (req, res) => {
141
+ try {
142
+ const auth = req.auth;
143
+ const { projectId, fileId } = req.params;
144
+ const maxDepth = Number(req.query.maxDepth ?? 5);
145
+ const project = await Project.findOne({ _id: projectId });
146
+ if (!project)
147
+ return res.status(404).json({ message: 'Project not found' });
148
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
149
+ return res.status(403).json({ message: 'Unauthorized' });
150
+ const graph = await (0, vfxProcessor_1.resolveAssetDependencyGraph)(projectId, fileId, maxDepth);
151
+ res.json({ graph });
152
+ }
153
+ catch (err) {
154
+ res.status(500).json({ message: err.message });
155
+ }
156
+ });
157
+ return router;
158
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function reviewRouter(_config: StreamByConfig): Router;
@@ -0,0 +1,240 @@
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.reviewRouter = reviewRouter;
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 broadcastReviewUpdate(review, project) {
20
+ const members = project.members ?? [];
21
+ for (const m of members) {
22
+ (0, wsHub_1.emitToUser)(m.userId, { type: 'reviewEvent', data: review });
23
+ }
24
+ }
25
+ function reviewRouter(_config) {
26
+ const router = (0, express_1.Router)();
27
+ const Project = (0, manager_1.getModel)('projects');
28
+ // GET /projects/:projectId/reviews
29
+ router.get('/projects/:projectId/reviews', async (req, res) => {
30
+ try {
31
+ const auth = req.auth;
32
+ const { projectId } = req.params;
33
+ const project = await Project.findOne({ _id: projectId });
34
+ if (!project)
35
+ return res.status(404).json({ message: 'Project not found' });
36
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
37
+ return res.status(403).json({ message: 'Unauthorized' });
38
+ const db = getDb();
39
+ if (!db)
40
+ return res.status(500).json({ message: 'Database not available' });
41
+ const reviews = await db.collection('review_sessions')
42
+ .find({ projectId })
43
+ .sort({ createdAt: -1 })
44
+ .toArray();
45
+ res.json({ reviews });
46
+ }
47
+ catch (err) {
48
+ res.status(500).json({ message: err.message });
49
+ }
50
+ });
51
+ // POST /projects/:projectId/reviews
52
+ router.post('/projects/:projectId/reviews', async (req, res) => {
53
+ try {
54
+ const auth = req.auth;
55
+ const { projectId } = req.params;
56
+ const { assetId, assetVersionId, requiredApprovers, deadline } = req.body;
57
+ if (!assetId || !assetVersionId) {
58
+ return res.status(400).json({ message: 'assetId and assetVersionId are required' });
59
+ }
60
+ const project = await Project.findOne({ _id: projectId });
61
+ if (!project)
62
+ return res.status(404).json({ message: 'Project not found' });
63
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
64
+ return res.status(403).json({ message: 'Unauthorized' });
65
+ const db = getDb();
66
+ if (!db)
67
+ return res.status(500).json({ message: 'Database not available' });
68
+ const now = new Date();
69
+ const review = {
70
+ id: crypto_1.default.randomUUID(),
71
+ projectId,
72
+ assetId,
73
+ assetVersionId,
74
+ status: 'open',
75
+ requiredApprovers: requiredApprovers ?? 1,
76
+ approvals: [],
77
+ deadline: deadline ? new Date(deadline) : undefined,
78
+ createdAt: now,
79
+ updatedAt: now,
80
+ };
81
+ await db.collection('review_sessions').insertOne({ ...review });
82
+ broadcastReviewUpdate(review, project);
83
+ res.status(201).json({ review });
84
+ }
85
+ catch (err) {
86
+ res.status(500).json({ message: err.message });
87
+ }
88
+ });
89
+ // GET /projects/:projectId/reviews/:reviewId
90
+ router.get('/projects/:projectId/reviews/:reviewId', async (req, res) => {
91
+ try {
92
+ const auth = req.auth;
93
+ const { projectId, reviewId } = req.params;
94
+ const project = await Project.findOne({ _id: projectId });
95
+ if (!project)
96
+ return res.status(404).json({ message: 'Project not found' });
97
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
98
+ return res.status(403).json({ message: 'Unauthorized' });
99
+ const db = getDb();
100
+ if (!db)
101
+ return res.status(500).json({ message: 'Database not available' });
102
+ const review = await db.collection('review_sessions').findOne({ id: reviewId, projectId });
103
+ if (!review)
104
+ return res.status(404).json({ message: 'Review not found' });
105
+ res.json({ review });
106
+ }
107
+ catch (err) {
108
+ res.status(500).json({ message: err.message });
109
+ }
110
+ });
111
+ // POST /projects/:projectId/reviews/:reviewId/decision
112
+ router.post('/projects/:projectId/reviews/:reviewId/decision', async (req, res) => {
113
+ try {
114
+ const auth = req.auth;
115
+ const { projectId, reviewId } = req.params;
116
+ const { decision, comment } = req.body;
117
+ if (decision !== 'approve' && decision !== 'reject') {
118
+ return res.status(400).json({ message: 'decision must be approve or reject' });
119
+ }
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 col = db.collection('review_sessions');
129
+ const review = await col.findOne({ id: reviewId, projectId });
130
+ if (!review)
131
+ return res.status(404).json({ message: 'Review not found' });
132
+ if (review.status !== 'open')
133
+ return res.status(409).json({ message: `Review is already ${review.status}` });
134
+ // Remove any prior decision from this user, then add the new one
135
+ const approvals = (review.approvals ?? []).filter(a => a.userId !== auth.userId);
136
+ approvals.push({
137
+ userId: auth.userId,
138
+ username: auth.username ?? auth.userId,
139
+ decision,
140
+ comment,
141
+ at: new Date(),
142
+ });
143
+ const approveCount = approvals.filter(a => a.decision === 'approve').length;
144
+ const hasRejection = approvals.some(a => a.decision === 'reject');
145
+ let status = 'open';
146
+ if (hasRejection)
147
+ status = 'rejected';
148
+ else if (approveCount >= review.requiredApprovers)
149
+ status = 'approved';
150
+ const updatedAt = new Date();
151
+ await col.updateOne({ id: reviewId, projectId }, { $set: { approvals, status, updatedAt } });
152
+ const updated = { ...review, approvals, status, updatedAt };
153
+ broadcastReviewUpdate(updated, project);
154
+ res.json({ review: updated });
155
+ }
156
+ catch (err) {
157
+ res.status(500).json({ message: err.message });
158
+ }
159
+ });
160
+ // ─── Annotations ─────────────────────────────────────────────────────────────
161
+ router.get('/projects/:projectId/assets/:assetId/versions/:versionId/annotations', async (req, res) => {
162
+ try {
163
+ const auth = req.auth;
164
+ const { projectId, assetId, versionId } = req.params;
165
+ const project = await Project.findOne({ _id: projectId });
166
+ if (!project)
167
+ return res.status(404).json({ message: 'Project not found' });
168
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
169
+ return res.status(403).json({ message: 'Unauthorized' });
170
+ const db = getDb();
171
+ if (!db)
172
+ return res.status(500).json({ message: 'Database not available' });
173
+ const annotations = await db.collection('annotations')
174
+ .find({ assetId, assetVersionId: versionId, projectId })
175
+ .sort({ createdAt: 1 })
176
+ .toArray();
177
+ res.json({ annotations });
178
+ }
179
+ catch (err) {
180
+ res.status(500).json({ message: err.message });
181
+ }
182
+ });
183
+ router.post('/projects/:projectId/assets/:assetId/versions/:versionId/annotations', async (req, res) => {
184
+ try {
185
+ const auth = req.auth;
186
+ const { projectId, assetId, versionId } = req.params;
187
+ const { type, timecode, position3d, regionRect, text } = req.body;
188
+ if (!type || !text)
189
+ return res.status(400).json({ message: 'type and text are required' });
190
+ const project = await Project.findOne({ _id: projectId });
191
+ if (!project)
192
+ return res.status(404).json({ message: 'Project not found' });
193
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
194
+ return res.status(403).json({ message: 'Unauthorized' });
195
+ const db = getDb();
196
+ if (!db)
197
+ return res.status(500).json({ message: 'Database not available' });
198
+ const annotation = {
199
+ id: crypto_1.default.randomUUID(),
200
+ assetId,
201
+ assetVersionId: versionId,
202
+ projectId,
203
+ authorId: auth.userId,
204
+ authorUsername: auth.username ?? auth.userId,
205
+ type,
206
+ timecode,
207
+ position3d,
208
+ regionRect,
209
+ text,
210
+ resolved: false,
211
+ createdAt: new Date(),
212
+ };
213
+ await db.collection('annotations').insertOne({ ...annotation });
214
+ res.status(201).json({ annotation });
215
+ }
216
+ catch (err) {
217
+ res.status(500).json({ message: err.message });
218
+ }
219
+ });
220
+ router.patch('/projects/:projectId/assets/:assetId/versions/:versionId/annotations/:annotationId/resolve', async (req, res) => {
221
+ try {
222
+ const auth = req.auth;
223
+ const { projectId, assetId, versionId, annotationId } = req.params;
224
+ const project = await Project.findOne({ _id: projectId });
225
+ if (!project)
226
+ return res.status(404).json({ message: 'Project not found' });
227
+ if (!(0, auth_1.isProjectMember)(project, auth.userId))
228
+ return res.status(403).json({ message: 'Unauthorized' });
229
+ const db = getDb();
230
+ if (!db)
231
+ return res.status(500).json({ message: 'Database not available' });
232
+ await db.collection('annotations').updateOne({ id: annotationId, assetId, assetVersionId: versionId, projectId }, { $set: { resolved: true, resolvedBy: auth.userId } });
233
+ res.json({ message: 'Annotation resolved' });
234
+ }
235
+ catch (err) {
236
+ res.status(500).json({ message: err.message });
237
+ }
238
+ });
239
+ return router;
240
+ }
@@ -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
  }
@@ -0,0 +1,25 @@
1
+ import { PipelineSuggestion } from '../types';
2
+ export declare function runTranscriptionJob(jobId: string, fileId: string, projectId: string, options: {
3
+ model?: string;
4
+ sourceLanguage?: string;
5
+ outputFormats?: string[];
6
+ provider?: string;
7
+ credentialId?: string;
8
+ }): Promise<void>;
9
+ export declare function runUpscaleJob(jobId: string, fileId: string, projectId: string, options: {
10
+ scale?: number;
11
+ model?: string;
12
+ mode?: 'images' | 'video';
13
+ credentialId?: string;
14
+ }): Promise<void>;
15
+ export declare function runGenerateAssetJob(jobId: string, projectId: string, options: {
16
+ assetType?: 'mesh' | 'texture' | 'audio';
17
+ provider?: string;
18
+ prompt: string;
19
+ seed?: number;
20
+ credentialId?: string;
21
+ }): Promise<void>;
22
+ export declare function buildPipelineSuggestion(nodeSchema: {
23
+ nodes: any[];
24
+ edges: any[];
25
+ }, availableNodeTypes: string[]): Promise<PipelineSuggestion>;