streamby-core 0.37.0 → 0.39.0
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/CHANGELOG.md +75 -0
- package/LICENSE +661 -661
- package/README.md +573 -512
- package/dist/adapters/database/connectionManager.js +124 -124
- package/dist/adapters/database/sql.js +1 -1
- package/dist/adapters/storage/s3.d.ts +3 -2
- package/dist/adapters/storage/s3.js +18 -5
- package/dist/mcp/server.js +12 -0
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +107 -14
- package/dist/mcp/workflowNodes.d.ts +24 -0
- package/dist/mcp/workflowNodes.js +48 -0
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/apiConnection.d.ts +3 -0
- package/dist/middleware/routes/apiConnection.js +100 -0
- package/dist/middleware/routes/credential.d.ts +3 -0
- package/dist/middleware/routes/credential.js +80 -0
- package/dist/middleware/routes/deliverable.d.ts +3 -0
- package/dist/middleware/routes/deliverable.js +131 -0
- package/dist/middleware/routes/export.js +18 -0
- package/dist/middleware/routes/production.js +60 -67
- package/dist/middleware/routes/project.js +39 -11
- package/dist/middleware/routes/storage.js +115 -24
- package/dist/models/Model.js +9 -9
- package/dist/services/apiConnection.d.ts +18 -0
- package/dist/services/apiConnection.js +65 -0
- package/dist/services/apiTokenPermissions.js +7 -3
- package/dist/services/assetVersions.d.ts +48 -0
- package/dist/services/assetVersions.js +84 -0
- package/dist/services/credential.d.ts +8 -0
- package/dist/services/credential.js +70 -0
- package/dist/services/deliverables.d.ts +26 -0
- package/dist/services/deliverables.js +55 -0
- package/dist/services/mediaProcessor.js +3 -1
- package/dist/services/pipeline.d.ts +2 -0
- package/dist/services/pipeline.js +292 -0
- package/dist/services/production.d.ts +2 -0
- package/dist/services/production.js +13 -0
- package/dist/services/productionFields.d.ts +16 -0
- package/dist/services/productionFields.js +80 -0
- package/dist/services/signedUrls.d.ts +6 -0
- package/dist/services/signedUrls.js +9 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.js +32 -0
- package/dist/utils/nosqlDb.d.ts +2 -0
- package/dist/utils/nosqlDb.js +11 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +14 -0
- package/package.json +1 -1
|
@@ -43,157 +43,157 @@ 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
|
-
connections JSONB DEFAULT '[]',
|
|
66
|
-
"dbConnections" JSONB DEFAULT '[]',
|
|
67
|
-
"dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
|
|
68
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
69
|
-
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
70
|
-
);
|
|
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
|
+
connections JSONB DEFAULT '[]',
|
|
66
|
+
"dbConnections" JSONB DEFAULT '[]',
|
|
67
|
+
"dbType" VARCHAR(50) NOT NULL DEFAULT 'sql',
|
|
68
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
69
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
70
|
+
);
|
|
71
71
|
`);
|
|
72
72
|
console.log('✅ "projects" table ensured to exist.');
|
|
73
73
|
// Add JSONB columns to existing tables that predate this schema version
|
|
74
|
-
await pool.query(`
|
|
75
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
|
|
76
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
|
|
77
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS connections JSONB DEFAULT '[]';
|
|
78
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
|
|
79
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
|
|
80
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
|
|
81
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
|
|
82
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
|
|
83
|
-
-- Written when a project is made (routes/project) and by its connection routes.
|
|
84
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
|
|
85
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
|
|
86
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "integrationConnections" JSONB DEFAULT '[]';
|
|
87
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "distributionConnections" JSONB DEFAULT '[]';
|
|
88
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "renderFarmConnections" JSONB DEFAULT '[]';
|
|
74
|
+
await pool.query(`
|
|
75
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "allowedOrigin" JSONB DEFAULT '[]';
|
|
76
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS exports JSONB DEFAULT '[]';
|
|
77
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS connections JSONB DEFAULT '[]';
|
|
78
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbConnections" JSONB DEFAULT '[]';
|
|
79
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "storageConnections" JSONB DEFAULT '[]';
|
|
80
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS workflow JSONB;
|
|
81
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS pipelines JSONB DEFAULT '[]';
|
|
82
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "builtinBackfilledAt" TIMESTAMP WITH TIME ZONE;
|
|
83
|
+
-- Written when a project is made (routes/project) and by its connection routes.
|
|
84
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS public BOOLEAN DEFAULT TRUE;
|
|
85
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS category TEXT;
|
|
86
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "integrationConnections" JSONB DEFAULT '[]';
|
|
87
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "distributionConnections" JSONB DEFAULT '[]';
|
|
88
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "renderFarmConnections" JSONB DEFAULT '[]';
|
|
89
89
|
`);
|
|
90
90
|
// Ensure dbType exists with a non-null default
|
|
91
|
-
await pool.query(`
|
|
92
|
-
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
|
|
91
|
+
await pool.query(`
|
|
92
|
+
ALTER TABLE streamby.projects ADD COLUMN IF NOT EXISTS "dbType" VARCHAR(50) NOT NULL DEFAULT 'sql';
|
|
93
93
|
`);
|
|
94
|
-
await pool.query(`
|
|
95
|
-
CREATE TABLE IF NOT EXISTS streamby.project_members (
|
|
96
|
-
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
97
|
-
"projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
|
|
98
|
-
"userId" VARCHAR(255) NOT NULL,
|
|
99
|
-
role VARCHAR(255) NOT NULL DEFAULT 'member',
|
|
100
|
-
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
|
101
|
-
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
|
102
|
-
"archivedBy" VARCHAR(255),
|
|
103
|
-
"archivedAt" TIMESTAMP WITH TIME ZONE,
|
|
104
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
105
|
-
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
106
|
-
UNIQUE ("projectId", "userId")
|
|
107
|
-
);
|
|
94
|
+
await pool.query(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS streamby.project_members (
|
|
96
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
97
|
+
"projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
|
|
98
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
99
|
+
role VARCHAR(255) NOT NULL DEFAULT 'member',
|
|
100
|
+
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
|
101
|
+
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
|
102
|
+
"archivedBy" VARCHAR(255),
|
|
103
|
+
"archivedAt" TIMESTAMP WITH TIME ZONE,
|
|
104
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
105
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
106
|
+
UNIQUE ("projectId", "userId")
|
|
107
|
+
);
|
|
108
108
|
`);
|
|
109
|
-
await pool.query(`
|
|
110
|
-
ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
|
|
109
|
+
await pool.query(`
|
|
110
|
+
ALTER TABLE streamby.project_members ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active';
|
|
111
111
|
`);
|
|
112
112
|
console.log('✅ "project_members" table ensured to exist.');
|
|
113
113
|
// Migrate projectId FK column from UUID to TEXT if table predates this schema version
|
|
114
|
-
await pool.query(`
|
|
115
|
-
DO $$ BEGIN
|
|
116
|
-
ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
|
|
117
|
-
ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
|
|
118
|
-
ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
|
|
119
|
-
FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
|
|
120
|
-
EXCEPTION WHEN OTHERS THEN NULL; END $$;
|
|
114
|
+
await pool.query(`
|
|
115
|
+
DO $$ BEGIN
|
|
116
|
+
ALTER TABLE streamby.project_members DROP CONSTRAINT IF EXISTS project_members_projectId_fkey;
|
|
117
|
+
ALTER TABLE streamby.project_members ALTER COLUMN "projectId" TYPE TEXT;
|
|
118
|
+
ALTER TABLE streamby.project_members ADD CONSTRAINT project_members_projectid_fkey
|
|
119
|
+
FOREIGN KEY ("projectId") REFERENCES streamby.projects(id) ON DELETE CASCADE;
|
|
120
|
+
EXCEPTION WHEN OTHERS THEN NULL; END $$;
|
|
121
121
|
`);
|
|
122
|
-
await pool.query(`
|
|
123
|
-
CREATE TABLE IF NOT EXISTS streamby.exports (
|
|
124
|
-
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
125
|
-
"projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
|
|
126
|
-
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
|
127
|
-
"filePath" TEXT,
|
|
128
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
129
|
-
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
130
|
-
);
|
|
122
|
+
await pool.query(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS streamby.exports (
|
|
124
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
125
|
+
"projectId" TEXT NOT NULL REFERENCES streamby.projects(id) ON DELETE CASCADE,
|
|
126
|
+
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
|
127
|
+
"filePath" TEXT,
|
|
128
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
129
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
130
|
+
);
|
|
131
131
|
`);
|
|
132
132
|
console.log('✅ "exports" table ensured to exist.');
|
|
133
133
|
// user_integrations is registered against the main db only (see createRouter.ts), but
|
|
134
134
|
// this function runs per-SQL-connection like projects/exports above — harmless on
|
|
135
135
|
// non-main connections since the model never queries them there.
|
|
136
|
-
await pool.query(`
|
|
137
|
-
CREATE TABLE IF NOT EXISTS streamby.user_integrations (
|
|
138
|
-
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
139
|
-
"userId" VARCHAR(255) NOT NULL,
|
|
140
|
-
kind VARCHAR(20) NOT NULL,
|
|
141
|
-
provider VARCHAR(20) NOT NULL,
|
|
142
|
-
name VARCHAR(255) NOT NULL,
|
|
143
|
-
description TEXT,
|
|
144
|
-
"encryptedCredential" TEXT NOT NULL,
|
|
145
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
146
|
-
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
147
|
-
);
|
|
148
|
-
CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
|
|
136
|
+
await pool.query(`
|
|
137
|
+
CREATE TABLE IF NOT EXISTS streamby.user_integrations (
|
|
138
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
139
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
140
|
+
kind VARCHAR(20) NOT NULL,
|
|
141
|
+
provider VARCHAR(20) NOT NULL,
|
|
142
|
+
name VARCHAR(255) NOT NULL,
|
|
143
|
+
description TEXT,
|
|
144
|
+
"encryptedCredential" TEXT NOT NULL,
|
|
145
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
146
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
147
|
+
);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS user_integrations_userid_idx ON streamby.user_integrations ("userId");
|
|
149
149
|
`);
|
|
150
150
|
console.log('✅ "user_integrations" table ensured to exist.');
|
|
151
151
|
// API tokens: only their hash is kept (see services/apiTokens). Main db only, like
|
|
152
152
|
// user_integrations above. A table from before tokens had a kind and permissions gets the
|
|
153
153
|
// new columns; its old tokens no longer work (they have no kind) and aren't listed.
|
|
154
|
-
await pool.query(`
|
|
155
|
-
CREATE TABLE IF NOT EXISTS streamby.api_tokens (
|
|
156
|
-
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
157
|
-
kind VARCHAR(10),
|
|
158
|
-
"userId" VARCHAR(255) NOT NULL,
|
|
159
|
-
"createdByName" VARCHAR(255),
|
|
160
|
-
name VARCHAR(255) NOT NULL,
|
|
161
|
-
prefix VARCHAR(32) NOT NULL,
|
|
162
|
-
hash CHAR(64) NOT NULL UNIQUE,
|
|
163
|
-
permissions JSONB NOT NULL DEFAULT '[]',
|
|
164
|
-
"projectIds" JSONB NOT NULL,
|
|
165
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
166
|
-
"expiresAt" TIMESTAMP WITH TIME ZONE,
|
|
167
|
-
"lastUsedAt" TIMESTAMP WITH TIME ZONE
|
|
168
|
-
);
|
|
169
|
-
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
|
|
170
|
-
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
|
|
171
|
-
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
|
|
172
|
-
DO $$ BEGIN
|
|
173
|
-
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
174
|
-
WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
|
|
175
|
-
ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
|
|
176
|
-
END IF;
|
|
177
|
-
END $$;
|
|
178
|
-
CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
|
|
179
|
-
CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
|
|
154
|
+
await pool.query(`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS streamby.api_tokens (
|
|
156
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
157
|
+
kind VARCHAR(10),
|
|
158
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
159
|
+
"createdByName" VARCHAR(255),
|
|
160
|
+
name VARCHAR(255) NOT NULL,
|
|
161
|
+
prefix VARCHAR(32) NOT NULL,
|
|
162
|
+
hash CHAR(64) NOT NULL UNIQUE,
|
|
163
|
+
permissions JSONB NOT NULL DEFAULT '[]',
|
|
164
|
+
"projectIds" JSONB NOT NULL,
|
|
165
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
166
|
+
"expiresAt" TIMESTAMP WITH TIME ZONE,
|
|
167
|
+
"lastUsedAt" TIMESTAMP WITH TIME ZONE
|
|
168
|
+
);
|
|
169
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS kind VARCHAR(10);
|
|
170
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS "createdByName" VARCHAR(255);
|
|
171
|
+
ALTER TABLE streamby.api_tokens ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]';
|
|
172
|
+
DO $$ BEGIN
|
|
173
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
174
|
+
WHERE table_schema = 'streamby' AND table_name = 'api_tokens' AND column_name = 'access') THEN
|
|
175
|
+
ALTER TABLE streamby.api_tokens ALTER COLUMN access DROP NOT NULL;
|
|
176
|
+
END IF;
|
|
177
|
+
END $$;
|
|
178
|
+
CREATE INDEX IF NOT EXISTS api_tokens_userid_idx ON streamby.api_tokens ("userId");
|
|
179
|
+
CREATE INDEX IF NOT EXISTS api_tokens_kind_idx ON streamby.api_tokens (kind);
|
|
180
180
|
`);
|
|
181
181
|
console.log('✅ "api_tokens" table ensured to exist.');
|
|
182
182
|
// The MCP server's audit log: every tool call (see mcp/audit). Main db only.
|
|
183
|
-
await pool.query(`
|
|
184
|
-
CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
|
|
185
|
-
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
186
|
-
"userId" VARCHAR(255) NOT NULL,
|
|
187
|
-
"tokenId" VARCHAR(255) NOT NULL,
|
|
188
|
-
"projectId" VARCHAR(255),
|
|
189
|
-
tool VARCHAR(100) NOT NULL,
|
|
190
|
-
ok BOOLEAN NOT NULL,
|
|
191
|
-
status INTEGER,
|
|
192
|
-
error TEXT,
|
|
193
|
-
"durationMs" INTEGER NOT NULL,
|
|
194
|
-
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
195
|
-
);
|
|
196
|
-
CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
|
|
183
|
+
await pool.query(`
|
|
184
|
+
CREATE TABLE IF NOT EXISTS streamby.mcp_audit (
|
|
185
|
+
id TEXT PRIMARY KEY DEFAULT uuid_generate_v4()::text,
|
|
186
|
+
"userId" VARCHAR(255) NOT NULL,
|
|
187
|
+
"tokenId" VARCHAR(255) NOT NULL,
|
|
188
|
+
"projectId" VARCHAR(255),
|
|
189
|
+
tool VARCHAR(100) NOT NULL,
|
|
190
|
+
ok BOOLEAN NOT NULL,
|
|
191
|
+
status INTEGER,
|
|
192
|
+
error TEXT,
|
|
193
|
+
"durationMs" INTEGER NOT NULL,
|
|
194
|
+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
|
|
197
197
|
`);
|
|
198
198
|
console.log('✅ "mcp_audit" table ensured to exist.');
|
|
199
199
|
}
|
|
@@ -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
|
};
|
|
@@ -8,8 +8,9 @@ export declare class S3Adapter implements StorageAdapter {
|
|
|
8
8
|
deleteProjectDirectory(projectId: string): Promise<any>;
|
|
9
9
|
getPresignedUrl(contentType: string, projectId: string): Promise<any>;
|
|
10
10
|
getPresignedProjectImageUrl(projectId: string): Promise<any>;
|
|
11
|
-
getPresignedGetUrl(key: string): Promise<string>;
|
|
12
|
-
getPresignedUploadUrl(key: string, contentType: string): Promise<string>;
|
|
11
|
+
getPresignedGetUrl(key: string, expiresIn?: number, downloadName?: string): Promise<string>;
|
|
12
|
+
getPresignedUploadUrl(key: string, contentType: string, expiresIn?: number): Promise<string>;
|
|
13
|
+
copyFile(from: string, to: string): Promise<void>;
|
|
13
14
|
deleteFile(key: string): Promise<void>;
|
|
14
15
|
listFilesByCategory(projectId: string, category: string): Promise<StorageFileInfo[]>;
|
|
15
16
|
private inferMimeType;
|
|
@@ -64,13 +64,21 @@ class S3Adapter {
|
|
|
64
64
|
const publicUrl = `https://${this.bucket}.s3.amazonaws.com/${key}`;
|
|
65
65
|
return { url, publicUrl };
|
|
66
66
|
}
|
|
67
|
-
async getPresignedGetUrl(key) {
|
|
68
|
-
const command = new client_s3_1.GetObjectCommand({
|
|
69
|
-
|
|
67
|
+
async getPresignedGetUrl(key, expiresIn = 86400, downloadName) {
|
|
68
|
+
const command = new client_s3_1.GetObjectCommand({
|
|
69
|
+
Bucket: this.bucket,
|
|
70
|
+
Key: key,
|
|
71
|
+
...(downloadName ? { ResponseContentDisposition: contentDisposition(downloadName) } : {}),
|
|
72
|
+
});
|
|
73
|
+
return await (0, s3_request_presigner_1.getSignedUrl)(this.s3, command, { expiresIn });
|
|
70
74
|
}
|
|
71
|
-
async getPresignedUploadUrl(key, contentType) {
|
|
75
|
+
async getPresignedUploadUrl(key, contentType, expiresIn = 3600) {
|
|
72
76
|
const command = new client_s3_1.PutObjectCommand({ Bucket: this.bucket, Key: key, ContentType: contentType });
|
|
73
|
-
return await (0, s3_request_presigner_1.getSignedUrl)(this.s3, command, { expiresIn
|
|
77
|
+
return await (0, s3_request_presigner_1.getSignedUrl)(this.s3, command, { expiresIn });
|
|
78
|
+
}
|
|
79
|
+
async copyFile(from, to) {
|
|
80
|
+
const source = `${this.bucket}/${from.split('/').map(encodeURIComponent).join('/')}`;
|
|
81
|
+
await this.s3.send(new client_s3_1.CopyObjectCommand({ Bucket: this.bucket, CopySource: source, Key: to }));
|
|
74
82
|
}
|
|
75
83
|
async deleteFile(key) {
|
|
76
84
|
const command = new client_s3_1.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
|
|
@@ -114,3 +122,8 @@ class S3Adapter {
|
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
exports.S3Adapter = S3Adapter;
|
|
125
|
+
// An attachment with its name: ASCII in `filename`, the full name in `filename*` (RFC 6266).
|
|
126
|
+
const contentDisposition = (name) => {
|
|
127
|
+
const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, '_');
|
|
128
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
129
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.createMcpServer = createMcpServer;
|
|
|
5
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
6
|
const tools_1 = require("./tools");
|
|
7
7
|
const audit_1 = require("./audit");
|
|
8
|
+
const workflowNodes_1 = require("./workflowNodes");
|
|
8
9
|
exports.MCP_SERVER_NAME = 'streamby';
|
|
9
10
|
const errorText = (body, status) => {
|
|
10
11
|
const message = body?.message ?? body?.error;
|
|
@@ -34,6 +35,17 @@ function createMcpServer(auth, api, config, version) {
|
|
|
34
35
|
const projectId = tool.projectOf?.(args) ?? tokenProject;
|
|
35
36
|
let status = null;
|
|
36
37
|
try {
|
|
38
|
+
// What the workflow needs first, as a user would add it.
|
|
39
|
+
if (tool.workflowNode) {
|
|
40
|
+
const refused = await (0, workflowNodes_1.ensureWorkflowNode)(tool.workflowNode, tool.projectOf?.(args), auth, api);
|
|
41
|
+
if (refused) {
|
|
42
|
+
await (0, audit_1.recordMcpCall)({
|
|
43
|
+
userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
|
|
44
|
+
ok: false, status: null, error: refused, durationMs: Date.now() - started,
|
|
45
|
+
}, config);
|
|
46
|
+
return { isError: true, content: [{ type: 'text', text: refused }] };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
const answer = await api(tool.call(args));
|
|
38
50
|
status = answer.status;
|
|
39
51
|
const ok = status >= 200 && status < 300;
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ApiTokenPermission } from '../services/apiTokenPermissions';
|
|
3
|
+
import type { WorkflowNodeRequirement } from './workflowNodes';
|
|
3
4
|
export interface ToolCall {
|
|
4
5
|
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
5
6
|
path: string;
|
|
@@ -12,6 +13,7 @@ export interface ToolDefinition<S extends z.ZodRawShape = z.ZodRawShape> {
|
|
|
12
13
|
permissions: ApiTokenPermission[];
|
|
13
14
|
write?: boolean;
|
|
14
15
|
destructive?: boolean;
|
|
16
|
+
workflowNode?: WorkflowNodeRequirement;
|
|
15
17
|
input: S;
|
|
16
18
|
projectOf?: (args: z.infer<z.ZodObject<S>>) => string | undefined;
|
|
17
19
|
call: (args: z.infer<z.ZodObject<S>>) => ToolCall;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -8,19 +8,20 @@ const projectId = id('project');
|
|
|
8
8
|
const enc = encodeURIComponent;
|
|
9
9
|
const byProject = ({ projectId }) => projectId;
|
|
10
10
|
const CATEGORIES = ['images', 'audios', 'videos', '3d-models'];
|
|
11
|
+
const STATUSES = ['todo', 'in_progress', 'review', 'done'];
|
|
11
12
|
const shotFields = {
|
|
12
13
|
description: zod_1.z.string().optional(),
|
|
13
|
-
status: zod_1.z.
|
|
14
|
-
assignedTo: zod_1.z.string().optional().describe("The
|
|
14
|
+
status: zod_1.z.enum(STATUSES).optional(),
|
|
15
|
+
assignedTo: zod_1.z.array(zod_1.z.string()).optional().describe("The assignees' user ids: members of the project"),
|
|
15
16
|
assetId: zod_1.z.string().optional(),
|
|
16
17
|
exportId: zod_1.z.string().optional(),
|
|
17
18
|
dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
|
|
18
19
|
order: zod_1.z.number().optional(),
|
|
19
20
|
};
|
|
20
21
|
const taskFields = {
|
|
21
|
-
status: zod_1.z.
|
|
22
|
-
priority: zod_1.z.
|
|
23
|
-
assignedTo: zod_1.z.string().optional().describe("The assignee's user id"),
|
|
22
|
+
status: zod_1.z.enum(STATUSES).optional(),
|
|
23
|
+
priority: zod_1.z.enum(['low', 'medium', 'high']).optional(),
|
|
24
|
+
assignedTo: zod_1.z.string().optional().describe("The assignee's user id: a member of the project"),
|
|
24
25
|
dueDate: zod_1.z.string().optional().describe('An ISO 8601 date'),
|
|
25
26
|
notes: zod_1.z.string().optional(),
|
|
26
27
|
};
|
|
@@ -156,21 +157,21 @@ exports.TOOLS = [
|
|
|
156
157
|
// ── Writing ──
|
|
157
158
|
tool({
|
|
158
159
|
name: 'update_asset_metadata', title: "Update an asset's custom tags", permissions: ['assets:write'], write: true,
|
|
159
|
-
description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had.",
|
|
160
|
+
description: "Sets an asset's custom tags (key/value pairs), replacing the ones it had. They show in the file's Metadata tab, in Storage.",
|
|
160
161
|
input: { projectId, fileId: id('asset'), customTags: zod_1.z.record(zod_1.z.string(), zod_1.z.string()) },
|
|
161
162
|
projectOf: byProject,
|
|
162
163
|
call: ({ projectId, fileId, customTags }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/assets/${enc(fileId)}/metadata`, body: { customTags } }),
|
|
163
164
|
}),
|
|
164
165
|
tool({
|
|
165
166
|
name: 'set_asset_rights', title: "Set an asset's rights", permissions: ['rights:write'], write: true,
|
|
166
|
-
description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes.",
|
|
167
|
+
description: "Sets an asset's rights: holder, license, territory, expiry, restrictions and notes. They show in the file's Rights tab, in Storage.",
|
|
167
168
|
input: { projectId, assetId: id('asset'), ...rightsFields },
|
|
168
169
|
projectOf: byProject,
|
|
169
170
|
call: ({ projectId, assetId, ...rights }) => ({ method: 'PUT', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/rights`, body: defined(rights) }),
|
|
170
171
|
}),
|
|
171
172
|
tool({
|
|
172
173
|
name: 'create_pipeline', title: 'Create a pipeline', permissions: ['workflow:write'], write: true,
|
|
173
|
-
description:
|
|
174
|
+
description: "Creates a pipeline in a project. It shows under Pipelines, and the workflow canvas draws its node.",
|
|
174
175
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
175
176
|
projectOf: byProject,
|
|
176
177
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/pipelines`, body: defined(fields) }),
|
|
@@ -183,8 +184,8 @@ exports.TOOLS = [
|
|
|
183
184
|
call: ({ projectId, pipelineId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/pipelines/${enc(pipelineId)}`, body: defined(fields) }),
|
|
184
185
|
}),
|
|
185
186
|
tool({
|
|
186
|
-
name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write'], write: true,
|
|
187
|
-
description:
|
|
187
|
+
name: 'create_sequence', title: 'Create a production sequence', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
188
|
+
description: "Creates a production sequence in a project, shown in its Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
188
189
|
input: { projectId, name: zod_1.z.string().min(1), description: zod_1.z.string().optional(), order: zod_1.z.number().optional() },
|
|
189
190
|
projectOf: byProject,
|
|
190
191
|
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences`, body: defined(fields) }),
|
|
@@ -197,8 +198,8 @@ exports.TOOLS = [
|
|
|
197
198
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}`, body: defined(fields) }),
|
|
198
199
|
}),
|
|
199
200
|
tool({
|
|
200
|
-
name: 'create_shot', title: 'Create a shot', permissions: ['production:write'], write: true,
|
|
201
|
-
description:
|
|
201
|
+
name: 'create_shot', title: 'Create a shot', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
202
|
+
description: "Creates a shot in a production sequence, shown in its Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
202
203
|
input: { projectId, sequenceId: id('sequence'), name: zod_1.z.string().min(1), ...shotFields },
|
|
203
204
|
projectOf: byProject,
|
|
204
205
|
call: ({ projectId, sequenceId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/sequences/${enc(sequenceId)}/shots`, body: defined(fields) }),
|
|
@@ -211,8 +212,8 @@ exports.TOOLS = [
|
|
|
211
212
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}`, body: defined(fields) }),
|
|
212
213
|
}),
|
|
213
214
|
tool({
|
|
214
|
-
name: 'create_task', title: 'Create a shot task', permissions: ['production:write'], write: true,
|
|
215
|
-
description:
|
|
215
|
+
name: 'create_task', title: 'Create a shot task', permissions: ['production:write', 'workflow:write'], write: true, workflowNode: 'production',
|
|
216
|
+
description: "Creates a task in a shot, shown in the shot's panel in the Production board. If the project's workflow has no production node yet, it first adds one (a Shot node, as dropping it on the canvas does), so the project's Production section shows it: that's why it also needs workflow:write.",
|
|
216
217
|
input: { projectId, shotId: id('shot'), name: zod_1.z.string().min(1), ...taskFields },
|
|
217
218
|
projectOf: byProject,
|
|
218
219
|
call: ({ projectId, shotId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks`, body: defined(fields) }),
|
|
@@ -224,6 +225,98 @@ exports.TOOLS = [
|
|
|
224
225
|
projectOf: byProject,
|
|
225
226
|
call: ({ projectId, shotId, taskId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/production/shots/${enc(shotId)}/tasks/${enc(taskId)}`, body: defined(fields) }),
|
|
226
227
|
}),
|
|
228
|
+
// ── Files: uploading, downloading, deliverables ──
|
|
229
|
+
// The file itself never goes through the conversation: these tools give a signed URL, and
|
|
230
|
+
// the user's own tool (Blender's or Godot's MCP, a script) uploads or downloads it directly.
|
|
231
|
+
tool({
|
|
232
|
+
name: 'list_asset_versions', title: "List an asset's versions", permissions: ['assets:read'],
|
|
233
|
+
description: "An asset's versions, newest first: each one's id, number, file name and when it was made. Any of them can be downloaded (get_asset_download_url).",
|
|
234
|
+
input: { projectId, assetId: id('asset') },
|
|
235
|
+
projectOf: byProject,
|
|
236
|
+
call: ({ projectId, assetId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/versions` }),
|
|
237
|
+
pick: (data) => (data?.versions ?? []).map((v) => ({
|
|
238
|
+
versionId: v.versionId, version: v.version, fileName: v.fileName ?? null, contentType: v.contentType ?? null, createdAt: v.createdAt,
|
|
239
|
+
})),
|
|
240
|
+
}),
|
|
241
|
+
tool({
|
|
242
|
+
name: 'request_asset_upload', title: 'Request an upload URL', permissions: ['assets:write'], write: true,
|
|
243
|
+
description: [
|
|
244
|
+
"Makes a new asset (or, with assetId, a new version of one) and gives a signed URL to upload its file to, as the UI does.",
|
|
245
|
+
"It doesn't upload anything: the file is sent by the tool that has it (for example Blender's MCP, running Python), with an HTTP PUT of its bytes to `upload.url` and the `upload.headers` given, before `expiresAt`.",
|
|
246
|
+
"The URL works for that file only. A new version keeps the asset's URL (it replaces what it serves); the earlier ones are archived and can still be downloaded.",
|
|
247
|
+
"Categories: images, audios, videos, 3d-models (.glb, .gltf, .obj, .fbx, .stl, .ply); contentType must match it.",
|
|
248
|
+
].join(' '),
|
|
249
|
+
input: {
|
|
250
|
+
projectId,
|
|
251
|
+
category: zod_1.z.enum(CATEGORIES),
|
|
252
|
+
fileName: zod_1.z.string().min(1).describe('The file name, with its extension (e.g. shot010_layout.glb)'),
|
|
253
|
+
contentType: zod_1.z.string().min(1).describe('Its MIME type (e.g. model/gltf-binary, image/png, video/mp4)'),
|
|
254
|
+
assetId: zod_1.z.string().optional().describe('An existing asset to upload a new version of; a new asset if not given'),
|
|
255
|
+
connectionId: zod_1.z.string().optional().describe("A storage connection to store it in; the project's built-in storage if not given"),
|
|
256
|
+
},
|
|
257
|
+
projectOf: byProject,
|
|
258
|
+
call: ({ projectId, category, fileName, contentType, assetId, connectionId }) => {
|
|
259
|
+
const query = new URLSearchParams({ fileName, contentType, ...(connectionId && !assetId ? { category } : {}) });
|
|
260
|
+
const files = connectionId ? `/projects/${enc(projectId)}/connections/storage/${enc(connectionId)}` : `/projects/${enc(projectId)}/storage`;
|
|
261
|
+
const path = assetId
|
|
262
|
+
? `${files}/files/${enc(assetId)}/replace-url`
|
|
263
|
+
: connectionId ? `${files}/upload-url` : `${files}/${category}/upload-url`;
|
|
264
|
+
return { method: 'GET', path: `${path}?${query}` };
|
|
265
|
+
},
|
|
266
|
+
pick: (data, { contentType }) => ({
|
|
267
|
+
assetId: data?.fileId,
|
|
268
|
+
...(data?.versionId ? { versionId: data.versionId, version: data.version } : { version: 1 }),
|
|
269
|
+
upload: { method: 'PUT', url: data?.url, headers: { 'Content-Type': contentType } },
|
|
270
|
+
expiresAt: data?.expiresAt ?? null,
|
|
271
|
+
}),
|
|
272
|
+
}),
|
|
273
|
+
tool({
|
|
274
|
+
name: 'get_asset_download_url', title: 'Get a download URL', permissions: ['assets:read'],
|
|
275
|
+
description: "A short-lived signed URL to download an asset's file (its latest version, or the one given), for the tool that will use it (for example Blender's or Godot's MCP) to fetch with an HTTP GET before `expiresAt`. It works for that file only.",
|
|
276
|
+
input: { projectId, assetId: id('asset'), versionId: zod_1.z.string().optional().describe('A version (list_asset_versions); the latest if not given') },
|
|
277
|
+
projectOf: byProject,
|
|
278
|
+
call: ({ projectId, assetId, versionId }) => ({
|
|
279
|
+
method: 'GET',
|
|
280
|
+
path: `/projects/${enc(projectId)}/assets/${enc(assetId)}/download-url${versionId ? `?${new URLSearchParams({ versionId })}` : ''}`,
|
|
281
|
+
}),
|
|
282
|
+
}),
|
|
283
|
+
tool({
|
|
284
|
+
name: 'list_deliverables', title: 'List deliverables', permissions: ['assets:read'],
|
|
285
|
+
description: "A project's deliverables (assets delivered for a shot or an export), of one shot or export if given: each one's asset, version, note and what it's for.",
|
|
286
|
+
input: {
|
|
287
|
+
projectId,
|
|
288
|
+
shotId: zod_1.z.string().optional().describe("A shot's id: only its deliverables"),
|
|
289
|
+
exportId: zod_1.z.string().optional().describe("An export's id: only its deliverables"),
|
|
290
|
+
},
|
|
291
|
+
projectOf: byProject,
|
|
292
|
+
call: ({ projectId, shotId, exportId }) => {
|
|
293
|
+
const query = new URLSearchParams(defined({ shotId, exportId })).toString();
|
|
294
|
+
return { method: 'GET', path: `/projects/${enc(projectId)}/deliverables${query ? `?${query}` : ''}` };
|
|
295
|
+
},
|
|
296
|
+
pick: (data) => data?.deliverables ?? [],
|
|
297
|
+
}),
|
|
298
|
+
tool({
|
|
299
|
+
name: 'mark_deliverable', title: 'Mark a deliverable', permissions: ['assets:write'], write: true,
|
|
300
|
+
description: "Marks an asset (or one of its versions) as a deliverable of a production shot or of an export. It shows in the shot's panel in the Production board, or in the export's Deliverables tab. Give shotId or exportId, one of them. Upload the file first (request_asset_upload).",
|
|
301
|
+
input: {
|
|
302
|
+
projectId,
|
|
303
|
+
assetId: id('asset'),
|
|
304
|
+
versionId: zod_1.z.string().optional().describe('A version of it; without one, the asset as it is (its latest version)'),
|
|
305
|
+
shotId: zod_1.z.string().optional().describe("The shot it's delivered for"),
|
|
306
|
+
exportId: zod_1.z.string().optional().describe("The export it's delivered for"),
|
|
307
|
+
note: zod_1.z.string().max(1000).optional(),
|
|
308
|
+
},
|
|
309
|
+
projectOf: byProject,
|
|
310
|
+
call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/deliverables`, body: defined(fields) }),
|
|
311
|
+
pick: (data) => data?.deliverable,
|
|
312
|
+
}),
|
|
313
|
+
tool({
|
|
314
|
+
name: 'remove_deliverable', title: 'Remove a deliverable', permissions: ['assets:write'], write: true,
|
|
315
|
+
description: 'Unmarks a deliverable. Its asset stays where it is.',
|
|
316
|
+
input: { projectId, deliverableId: id('deliverable') },
|
|
317
|
+
projectOf: byProject,
|
|
318
|
+
call: ({ projectId, deliverableId }) => ({ method: 'DELETE', path: `/projects/${enc(projectId)}/deliverables/${enc(deliverableId)}` }),
|
|
319
|
+
}),
|
|
227
320
|
// ── Projects ──
|
|
228
321
|
tool({
|
|
229
322
|
name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Auth } from '../types';
|
|
2
|
+
import type { ApiCaller } from './server';
|
|
3
|
+
export type WorkflowNodeRequirement = 'production';
|
|
4
|
+
/** A Shot node, as the UI makes one dropped on the canvas without a position. */
|
|
5
|
+
export declare const newShotNode: () => {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
position: {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
};
|
|
12
|
+
data: {
|
|
13
|
+
label: string;
|
|
14
|
+
subtitle: string;
|
|
15
|
+
bgColor: string;
|
|
16
|
+
iconColor: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Makes sure a project's workflow has what `requirement` needs, adding it (through the REST
|
|
21
|
+
* API, as the caller) if it's missing. An error to answer with, or null to go on. A project
|
|
22
|
+
* the caller doesn't reach is left alone: the call that follows answers for it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureWorkflowNode(requirement: WorkflowNodeRequirement, projectId: string | undefined, auth: Auth, api: ApiCaller): Promise<string | null>;
|