@agent-foundry/studio 1.0.0 → 1.0.2

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.
Files changed (44) hide show
  1. package/README.md +180 -47
  2. package/dist/bff/client.d.ts +168 -0
  3. package/dist/bff/client.d.ts.map +1 -0
  4. package/dist/bff/client.js +288 -0
  5. package/dist/bff/client.js.map +1 -0
  6. package/dist/bff/index.d.ts +27 -0
  7. package/dist/bff/index.d.ts.map +1 -0
  8. package/dist/bff/index.js +26 -0
  9. package/dist/bff/index.js.map +1 -0
  10. package/dist/bff/types.d.ts +168 -0
  11. package/dist/bff/types.d.ts.map +1 -0
  12. package/dist/bff/types.js +8 -0
  13. package/dist/bff/types.js.map +1 -0
  14. package/dist/db/deployments.d.ts +67 -1
  15. package/dist/db/deployments.d.ts.map +1 -1
  16. package/dist/db/deployments.js +92 -1
  17. package/dist/db/deployments.js.map +1 -1
  18. package/dist/db/index.d.ts +17 -0
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +17 -0
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/projects.d.ts +54 -1
  23. package/dist/db/projects.d.ts.map +1 -1
  24. package/dist/db/projects.js +79 -1
  25. package/dist/db/projects.js.map +1 -1
  26. package/dist/index.d.ts +14 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +16 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/types/deployment.d.ts +56 -0
  31. package/dist/types/deployment.d.ts.map +1 -1
  32. package/dist/types/project.d.ts +6 -0
  33. package/dist/types/project.d.ts.map +1 -1
  34. package/package.json +14 -4
  35. package/src/bff/client.ts +412 -0
  36. package/src/bff/index.ts +32 -0
  37. package/src/bff/types.ts +212 -0
  38. package/src/db/deployments.ts +99 -1
  39. package/src/db/index.ts +17 -0
  40. package/src/db/projects.ts +86 -1
  41. package/src/db/schema.sql +35 -52
  42. package/src/index.ts +18 -2
  43. package/src/types/deployment.ts +75 -0
  44. package/src/types/project.ts +7 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * BFF API Types
3
+ *
4
+ * Response types from the BFF Studio API endpoints.
5
+ * These match the Pydantic models in services/bff/app/models/studio.py
6
+ */
7
+
8
+ import type {
9
+ StudioProject,
10
+ ProjectConfig,
11
+ ProjectFramework,
12
+ } from '../types/project';
13
+ import type {
14
+ DeploymentStatus,
15
+ DeploymentMetadata,
16
+ AppManifest,
17
+ } from '../types/deployment';
18
+
19
+ // =============================================================================
20
+ // Project API Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Request body for creating a project via BFF
25
+ */
26
+ export interface CreateProjectRequest {
27
+ name: string;
28
+ slug: string;
29
+ description?: string;
30
+ rootPath: string;
31
+ framework?: ProjectFramework;
32
+ config?: ProjectConfig;
33
+ parentProjectId?: string;
34
+ }
35
+
36
+ /**
37
+ * Request body for updating a project via BFF
38
+ */
39
+ export interface UpdateProjectRequest {
40
+ name?: string;
41
+ description?: string;
42
+ config?: Partial<ProjectConfig>;
43
+ }
44
+
45
+ /**
46
+ * Request body for forking a project via BFF
47
+ */
48
+ export interface ForkProjectRequest {
49
+ newSlug: string;
50
+ newRootPath: string;
51
+ newName?: string;
52
+ }
53
+
54
+ /**
55
+ * Project response from BFF API
56
+ */
57
+ export interface ProjectResponse extends StudioProject {
58
+ latestDeployment?: DeploymentSummary;
59
+ }
60
+
61
+ /**
62
+ * Paginated project list response
63
+ */
64
+ export interface ProjectListResponse {
65
+ projects: ProjectResponse[];
66
+ total: number;
67
+ page: number;
68
+ pageSize: number;
69
+ }
70
+
71
+ // =============================================================================
72
+ // Deployment API Types
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Deployment summary (for lists)
77
+ */
78
+ export interface DeploymentSummary {
79
+ id: string;
80
+ version: string;
81
+ status: DeploymentStatus;
82
+ ossUrl?: string;
83
+ bundleSizeBytes?: number;
84
+ createdAt: string;
85
+ publishedAt?: string;
86
+ }
87
+
88
+ /**
89
+ * Request body for starting a deployment
90
+ */
91
+ export interface StartDeploymentRequest {
92
+ projectId: string;
93
+ version?: string;
94
+ metadata?: Partial<DeploymentMetadata>;
95
+ }
96
+
97
+ /**
98
+ * STS credentials for OSS upload
99
+ */
100
+ export interface STSCredentials {
101
+ accessKeyId: string;
102
+ accessKeySecret: string;
103
+ securityToken: string;
104
+ expiration: string;
105
+ }
106
+
107
+ /**
108
+ * Response from starting a deployment
109
+ */
110
+ export interface StartDeploymentResponse {
111
+ deploymentId: string;
112
+ credentials: STSCredentials;
113
+ bucket: string;
114
+ region: string;
115
+ keyPrefix: string;
116
+ }
117
+
118
+ /**
119
+ * Request body for updating deployment status
120
+ */
121
+ export interface UpdateDeploymentStatusRequest {
122
+ status: DeploymentStatus;
123
+ buildLog?: string;
124
+ errorMessage?: string;
125
+ metadata?: Partial<DeploymentMetadata>;
126
+ }
127
+
128
+ /**
129
+ * Request body for completing a deployment
130
+ */
131
+ export interface CompleteDeploymentRequest {
132
+ bucket: string;
133
+ keyPrefix: string;
134
+ ossUrl: string;
135
+ totalBytes: number;
136
+ fileCount: number;
137
+ }
138
+
139
+ /**
140
+ * Response from completing a deployment
141
+ */
142
+ export interface CompleteDeploymentResponse {
143
+ deploymentId: string;
144
+ ossUrl: string;
145
+ version: string;
146
+ }
147
+
148
+ /**
149
+ * Request body for failing a deployment
150
+ */
151
+ export interface FailDeploymentRequest {
152
+ errorMessage: string;
153
+ buildLog?: string;
154
+ }
155
+
156
+ /**
157
+ * Deployment list response
158
+ */
159
+ export interface DeploymentListResponse {
160
+ deployments: DeploymentSummary[];
161
+ total: number;
162
+ }
163
+
164
+ // =============================================================================
165
+ // Publish API Types
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Request body for publishing to Feed
170
+ */
171
+ export interface PublishRequest {
172
+ deploymentId?: string;
173
+ manifest?: AppManifest;
174
+ status?: 'canary' | 'stable';
175
+ }
176
+
177
+ /**
178
+ * Response from publishing to Feed
179
+ */
180
+ export interface PublishResponse {
181
+ appId: string;
182
+ artifactId: string;
183
+ deploymentId: string;
184
+ shareUrl: string;
185
+ entryUrl: string;
186
+ }
187
+
188
+ // =============================================================================
189
+ // Error Types
190
+ // =============================================================================
191
+
192
+ /**
193
+ * API error response
194
+ */
195
+ export interface APIError {
196
+ detail: string;
197
+ code?: string;
198
+ }
199
+
200
+ /**
201
+ * BFF client configuration
202
+ */
203
+ export interface BFFClientConfig {
204
+ /** BFF base URL (e.g., "http://localhost:11001") */
205
+ baseUrl: string;
206
+
207
+ /** Supabase JWT token for authentication */
208
+ authToken: string;
209
+
210
+ /** Request timeout in milliseconds (default: 30000) */
211
+ timeout?: number;
212
+ }
@@ -1,7 +1,38 @@
1
1
  /**
2
2
  * Deployments Repository
3
3
  *
4
- * CRUD operations for studio_deployments table.
4
+ * Provides access to studio_deployments table via Supabase client.
5
+ *
6
+ * ## Important: Hybrid Access Pattern
7
+ *
8
+ * Due to RLS (Row Level Security) restrictions, this repository should
9
+ * primarily be used for **read operations only**.
10
+ *
11
+ * **For write operations, use the BFF client or DirectUploader:**
12
+ *
13
+ * ```typescript
14
+ * import { DirectUploader } from '@agent-foundry/studio/oss';
15
+ *
16
+ * // DirectUploader handles the complete deployment workflow via BFF
17
+ * const uploader = new DirectUploader({
18
+ * bffBaseUrl: 'http://localhost:11001',
19
+ * authToken: 'your-jwt-token',
20
+ * });
21
+ *
22
+ * const result = await uploader.upload({
23
+ * projectId: 'project-uuid',
24
+ * files: distFiles,
25
+ * });
26
+ * ```
27
+ *
28
+ * Or use the BFF client directly:
29
+ *
30
+ * ```typescript
31
+ * import { createBFFClient } from '@agent-foundry/studio/bff';
32
+ *
33
+ * const bff = createBFFClient({ ... });
34
+ * const deployment = await bff.deployments.start({ projectId: '...' });
35
+ * ```
5
36
  */
6
37
 
7
38
  import { SupabaseClient } from '@supabase/supabase-js';
@@ -26,6 +57,7 @@ interface DeploymentRow {
26
57
  oss_bucket: string | null;
27
58
  oss_key: string | null;
28
59
  oss_url: string | null;
60
+ artifact_id: string | null;
29
61
  bundle_size_bytes: number | null;
30
62
  build_log: string | null;
31
63
  error_message: string | null;
@@ -47,6 +79,7 @@ function rowToDeployment(row: DeploymentRow): Deployment {
47
79
  ossBucket: row.oss_bucket ?? undefined,
48
80
  ossKey: row.oss_key ?? undefined,
49
81
  ossUrl: row.oss_url ?? undefined,
82
+ artifactId: row.artifact_id ?? undefined,
50
83
  bundleSizeBytes: row.bundle_size_bytes ?? undefined,
51
84
  buildLog: row.build_log ?? undefined,
52
85
  errorMessage: row.error_message ?? undefined,
@@ -79,6 +112,12 @@ export class DeploymentsRepository {
79
112
 
80
113
  /**
81
114
  * Create a new deployment
115
+ *
116
+ * @deprecated Use `DirectUploader.upload()` or `createBFFClient().deployments.start()` instead.
117
+ *
118
+ * This method may fail with RLS errors. The recommended workflow is:
119
+ * 1. Use DirectUploader which handles the complete deployment lifecycle
120
+ * 2. Or use BFF client's deployments.start() to get STS credentials
82
121
  */
83
122
  async create(input: CreateDeploymentInput): Promise<Deployment> {
84
123
  const { data: { user } } = await this.supabase.auth.getUser();
@@ -168,6 +207,10 @@ export class DeploymentsRepository {
168
207
 
169
208
  /**
170
209
  * Update a deployment
210
+ *
211
+ * @deprecated Use `createBFFClient().deployments.updateStatus()` instead.
212
+ *
213
+ * This method may fail with RLS errors. Use BFF client for write operations.
171
214
  */
172
215
  async update(id: string, input: UpdateDeploymentInput): Promise<Deployment> {
173
216
  const updateData: Record<string, unknown> = {};
@@ -221,6 +264,8 @@ export class DeploymentsRepository {
221
264
 
222
265
  /**
223
266
  * Mark deployment as building
267
+ *
268
+ * @deprecated Use `createBFFClient().deployments.updateStatus()` instead.
224
269
  */
225
270
  async markBuilding(id: string): Promise<Deployment> {
226
271
  return this.update(id, { status: 'building' });
@@ -228,6 +273,8 @@ export class DeploymentsRepository {
228
273
 
229
274
  /**
230
275
  * Mark deployment as uploading
276
+ *
277
+ * @deprecated Use `createBFFClient().deployments.updateStatus()` instead.
231
278
  */
232
279
  async markUploading(id: string): Promise<Deployment> {
233
280
  return this.update(id, { status: 'uploading' });
@@ -235,6 +282,8 @@ export class DeploymentsRepository {
235
282
 
236
283
  /**
237
284
  * Mark deployment as published
285
+ *
286
+ * @deprecated Use `createBFFClient().deployments.complete()` instead.
238
287
  */
239
288
  async markPublished(
240
289
  id: string,
@@ -252,6 +301,8 @@ export class DeploymentsRepository {
252
301
 
253
302
  /**
254
303
  * Mark deployment as failed
304
+ *
305
+ * @deprecated Use `createBFFClient().deployments.fail()` instead.
255
306
  */
256
307
  async markFailed(id: string, errorMessage: string, buildLog?: string): Promise<Deployment> {
257
308
  return this.update(id, {
@@ -286,6 +337,9 @@ export class DeploymentsRepository {
286
337
 
287
338
  /**
288
339
  * Delete a deployment
340
+ *
341
+ * @deprecated Deployment deletion should be handled server-side.
342
+ * This method may fail with RLS errors.
289
343
  */
290
344
  async delete(id: string): Promise<void> {
291
345
  const { error } = await this.supabase
@@ -300,6 +354,9 @@ export class DeploymentsRepository {
300
354
 
301
355
  /**
302
356
  * Append to build log
357
+ *
358
+ * @deprecated Use `createBFFClient().deployments.updateStatus()` with buildLog field.
359
+ * This method may fail with RLS errors.
303
360
  */
304
361
  async appendBuildLog(id: string, logLine: string): Promise<void> {
305
362
  const existing = await this.getById(id);
@@ -313,4 +370,45 @@ export class DeploymentsRepository {
313
370
 
314
371
  await this.update(id, { buildLog: newLog });
315
372
  }
373
+
374
+ /**
375
+ * Link deployment to an artifact (called after publishing to Feed)
376
+ *
377
+ * @deprecated This is handled automatically by BFF's publish workflow.
378
+ * Use `createBFFClient().projects.publish()` instead.
379
+ */
380
+ async linkToArtifact(id: string, artifactId: string): Promise<Deployment> {
381
+ const { data, error } = await this.supabase
382
+ .from(this.TABLE)
383
+ .update({ artifact_id: artifactId })
384
+ .eq('id', id)
385
+ .select()
386
+ .single();
387
+
388
+ if (error) {
389
+ throw new Error(`Failed to link deployment to artifact: ${error.message}`);
390
+ }
391
+
392
+ return rowToDeployment(data as DeploymentRow);
393
+ }
394
+
395
+ /**
396
+ * Get deployment by artifact ID
397
+ */
398
+ async getByArtifactId(artifactId: string): Promise<Deployment | null> {
399
+ const { data, error } = await this.supabase
400
+ .from(this.TABLE)
401
+ .select()
402
+ .eq('artifact_id', artifactId)
403
+ .single();
404
+
405
+ if (error) {
406
+ if (error.code === 'PGRST116') {
407
+ return null; // Not found
408
+ }
409
+ throw new Error(`Failed to get deployment: ${error.message}`);
410
+ }
411
+
412
+ return rowToDeployment(data as DeploymentRow);
413
+ }
316
414
  }
package/src/db/index.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  /**
2
2
  * Database module - Supabase client for Studio
3
+ *
4
+ * ## Important: Read-Only Usage Recommended
5
+ *
6
+ * Due to RLS (Row Level Security), this module should primarily be used
7
+ * for **read operations only**. For write operations, use the BFF client:
8
+ *
9
+ * ```typescript
10
+ * // Read operations - use this module
11
+ * import { createStudioClient } from '@agent-foundry/studio/db';
12
+ * const db = createStudioClient({ supabaseUrl: '...', supabaseKey: '...' });
13
+ * const projects = await db.projects.list();
14
+ *
15
+ * // Write operations - use BFF client
16
+ * import { createBFFClient } from '@agent-foundry/studio/bff';
17
+ * const bff = createBFFClient({ baseUrl: '...', authToken: '...' });
18
+ * const project = await bff.projects.create({ ... });
19
+ * ```
3
20
  */
4
21
 
5
22
  export { createStudioClient, type StudioClientConfig } from './client';
@@ -1,7 +1,29 @@
1
1
  /**
2
2
  * Projects Repository
3
3
  *
4
- * CRUD operations for studio_projects table.
4
+ * Provides access to studio_projects table via Supabase client.
5
+ *
6
+ * ## Important: Hybrid Access Pattern
7
+ *
8
+ * Due to RLS (Row Level Security) restrictions, this repository should
9
+ * primarily be used for **read operations only**.
10
+ *
11
+ * **For write operations (create, update, delete, fork), use the BFF client:**
12
+ *
13
+ * ```typescript
14
+ * import { createBFFClient } from '@agent-foundry/studio/bff';
15
+ *
16
+ * const bff = createBFFClient({
17
+ * baseUrl: 'http://localhost:11001',
18
+ * authToken: 'your-jwt-token',
19
+ * });
20
+ *
21
+ * // Create project via BFF (recommended)
22
+ * const project = await bff.projects.create({ ... });
23
+ * ```
24
+ *
25
+ * The BFF uses service_role credentials which bypass RLS, ensuring
26
+ * consistent and secure write operations.
5
27
  */
6
28
 
7
29
  import { SupabaseClient } from '@supabase/supabase-js';
@@ -22,6 +44,7 @@ interface ProjectRow {
22
44
  name: string;
23
45
  slug: string;
24
46
  description: string | null;
47
+ app_id: string | null;
25
48
  root_path: string;
26
49
  framework: string;
27
50
  config: ProjectConfig;
@@ -40,6 +63,7 @@ function rowToProject(row: ProjectRow): StudioProject {
40
63
  name: row.name,
41
64
  slug: row.slug,
42
65
  description: row.description ?? undefined,
66
+ appId: row.app_id ?? undefined,
43
67
  rootPath: row.root_path,
44
68
  framework: row.framework as StudioProject['framework'],
45
69
  config: row.config,
@@ -59,6 +83,11 @@ export class ProjectsRepository {
59
83
 
60
84
  /**
61
85
  * Create a new project
86
+ *
87
+ * @deprecated Use BFF client instead: `createBFFClient().projects.create()`
88
+ *
89
+ * This method may fail with RLS errors when using anon key.
90
+ * The BFF client uses service_role credentials which bypass RLS.
62
91
  */
63
92
  async create(input: CreateProjectInput): Promise<StudioProject> {
64
93
  const { data: { user } } = await this.supabase.auth.getUser();
@@ -169,6 +198,11 @@ export class ProjectsRepository {
169
198
 
170
199
  /**
171
200
  * Update a project
201
+ *
202
+ * @deprecated Use BFF client instead: `createBFFClient().projects.update()`
203
+ *
204
+ * This method may fail with RLS errors when using anon key.
205
+ * The BFF client uses service_role credentials which bypass RLS.
172
206
  */
173
207
  async update(id: string, input: UpdateProjectInput): Promise<StudioProject> {
174
208
  const updateData: Record<string, unknown> = {};
@@ -204,6 +238,11 @@ export class ProjectsRepository {
204
238
 
205
239
  /**
206
240
  * Delete a project
241
+ *
242
+ * @deprecated Use BFF client instead: `createBFFClient().projects.delete()`
243
+ *
244
+ * This method may fail with RLS errors when using anon key.
245
+ * The BFF client uses service_role credentials which bypass RLS.
207
246
  */
208
247
  async delete(id: string): Promise<void> {
209
248
  const { error } = await this.supabase
@@ -218,6 +257,11 @@ export class ProjectsRepository {
218
257
 
219
258
  /**
220
259
  * Fork a project (create a copy)
260
+ *
261
+ * @deprecated Use BFF client instead: `createBFFClient().projects.fork()`
262
+ *
263
+ * This method may fail with RLS errors when using anon key.
264
+ * The BFF client uses service_role credentials which bypass RLS.
221
265
  */
222
266
  async fork(id: string, newSlug: string, newRootPath: string): Promise<StudioProject> {
223
267
  const original = await this.getById(id);
@@ -243,4 +287,45 @@ export class ProjectsRepository {
243
287
  const existing = await this.getBySlug(slug);
244
288
  return existing === null;
245
289
  }
290
+
291
+ /**
292
+ * Link project to a registered app (called after publishing to Feed)
293
+ *
294
+ * @deprecated This is handled automatically by BFF's publish workflow.
295
+ * Use `createBFFClient().projects.publish()` instead.
296
+ */
297
+ async linkToApp(id: string, appId: string): Promise<StudioProject> {
298
+ const { data, error } = await this.supabase
299
+ .from(this.TABLE)
300
+ .update({ app_id: appId })
301
+ .eq('id', id)
302
+ .select()
303
+ .single();
304
+
305
+ if (error) {
306
+ throw new Error(`Failed to link project to app: ${error.message}`);
307
+ }
308
+
309
+ return rowToProject(data as ProjectRow);
310
+ }
311
+
312
+ /**
313
+ * Get project by app ID
314
+ */
315
+ async getByAppId(appId: string): Promise<StudioProject | null> {
316
+ const { data, error } = await this.supabase
317
+ .from(this.TABLE)
318
+ .select()
319
+ .eq('app_id', appId)
320
+ .single();
321
+
322
+ if (error) {
323
+ if (error.code === 'PGRST116') {
324
+ return null; // Not found
325
+ }
326
+ throw new Error(`Failed to get project: ${error.message}`);
327
+ }
328
+
329
+ return rowToProject(data as ProjectRow);
330
+ }
246
331
  }
package/src/db/schema.sql CHANGED
@@ -1,22 +1,18 @@
1
1
  -- =============================================================================
2
- -- Agent Foundry Studio Database Schema
2
+ -- Agent Foundry Studio Database Schema (Reference)
3
3
  -- =============================================================================
4
- -- This schema defines tables for managing Studio projects and deployments.
5
- -- Run this migration against your Supabase PostgreSQL database.
4
+ -- This is a REFERENCE file showing the schema design.
5
+ -- Actual migrations are managed via Alembic in services/bff/alembic/versions/
6
6
  --
7
- -- Prerequisites:
8
- -- 1. Enable uuid-ossp extension: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
9
- -- 2. Supabase Auth must be configured (auth.users table exists)
7
+ -- See: 005_add_studio_tables.py for the actual migration
10
8
  -- =============================================================================
11
9
 
12
- -- Enable UUID extension if not already enabled
13
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
14
-
15
10
  -- =============================================================================
16
11
  -- Studio Projects
17
12
  -- =============================================================================
18
13
  -- Represents a React/Vite project created and managed in Build Studio.
19
14
  -- Each project belongs to a user and can optionally be forked from another project.
15
+ -- After first publish to Feed, links to an apps entry via app_id.
20
16
 
21
17
  CREATE TABLE IF NOT EXISTS studio_projects (
22
18
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
@@ -29,6 +25,9 @@ CREATE TABLE IF NOT EXISTS studio_projects (
29
25
  slug VARCHAR(255) NOT NULL,
30
26
  description TEXT,
31
27
 
28
+ -- Link to registered app (set after first publish to Feed)
29
+ app_id VARCHAR(255) REFERENCES apps(id) ON DELETE SET NULL,
30
+
32
31
  -- Local development
33
32
  root_path TEXT NOT NULL, -- Local filesystem path to project root
34
33
 
@@ -44,35 +43,21 @@ CREATE TABLE IF NOT EXISTS studio_projects (
44
43
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
45
44
 
46
45
  -- Ensure slug is unique per user
47
- CONSTRAINT unique_user_slug UNIQUE (user_id, slug)
46
+ CONSTRAINT uq_studio_projects_user_slug UNIQUE (user_id, slug)
48
47
  );
49
48
 
50
- -- Index for user's projects
49
+ -- Indexes
51
50
  CREATE INDEX IF NOT EXISTS idx_studio_projects_user_id ON studio_projects(user_id);
52
-
53
- -- Index for finding forks
54
- CREATE INDEX IF NOT EXISTS idx_studio_projects_parent ON studio_projects(parent_project_id);
55
-
56
- -- Trigger to auto-update updated_at
57
- CREATE OR REPLACE FUNCTION update_studio_projects_updated_at()
58
- RETURNS TRIGGER AS $$
59
- BEGIN
60
- NEW.updated_at = NOW();
61
- RETURN NEW;
62
- END;
63
- $$ LANGUAGE plpgsql;
64
-
65
- DROP TRIGGER IF EXISTS trigger_studio_projects_updated_at ON studio_projects;
66
- CREATE TRIGGER trigger_studio_projects_updated_at
67
- BEFORE UPDATE ON studio_projects
68
- FOR EACH ROW
69
- EXECUTE FUNCTION update_studio_projects_updated_at();
51
+ CREATE INDEX IF NOT EXISTS idx_studio_projects_app_id ON studio_projects(app_id) WHERE app_id IS NOT NULL;
52
+ CREATE INDEX IF NOT EXISTS idx_studio_projects_parent ON studio_projects(parent_project_id) WHERE parent_project_id IS NOT NULL;
53
+ CREATE INDEX IF NOT EXISTS idx_studio_projects_created ON studio_projects(created_at DESC);
70
54
 
71
55
  -- =============================================================================
72
56
  -- Studio Deployments
73
57
  -- =============================================================================
74
58
  -- Represents a build and upload of a project to Alibaba Cloud OSS.
75
59
  -- Tracks the full lifecycle: pending -> building -> uploading -> published/failed
60
+ -- When published to Feed, links to an artifact entry via artifact_id.
76
61
 
77
62
  CREATE TABLE IF NOT EXISTS studio_deployments (
78
63
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
@@ -93,6 +78,9 @@ CREATE TABLE IF NOT EXISTS studio_deployments (
93
78
  oss_key TEXT,
94
79
  oss_url TEXT,
95
80
 
81
+ -- Link to artifact (set when publishing to Feed)
82
+ artifact_id UUID REFERENCES artifacts(id) ON DELETE SET NULL,
83
+
96
84
  -- Metrics
97
85
  bundle_size_bytes BIGINT,
98
86
 
@@ -108,49 +96,44 @@ CREATE TABLE IF NOT EXISTS studio_deployments (
108
96
  published_at TIMESTAMPTZ
109
97
  );
110
98
 
111
- -- Index for project's deployments
99
+ -- Indexes
112
100
  CREATE INDEX IF NOT EXISTS idx_studio_deployments_project_id ON studio_deployments(project_id);
113
-
114
- -- Index for user's deployments
115
101
  CREATE INDEX IF NOT EXISTS idx_studio_deployments_user_id ON studio_deployments(user_id);
116
-
117
- -- Index for filtering by status
118
102
  CREATE INDEX IF NOT EXISTS idx_studio_deployments_status ON studio_deployments(status);
119
-
120
- -- Composite index for recent deployments by project
121
- CREATE INDEX IF NOT EXISTS idx_studio_deployments_project_created
122
- ON studio_deployments(project_id, created_at DESC);
103
+ CREATE INDEX IF NOT EXISTS idx_studio_deployments_artifact ON studio_deployments(artifact_id) WHERE artifact_id IS NOT NULL;
104
+ CREATE INDEX IF NOT EXISTS idx_studio_deployments_project_created ON studio_deployments(project_id, created_at DESC);
123
105
 
124
106
  -- =============================================================================
125
107
  -- Row Level Security (RLS) Policies
126
108
  -- =============================================================================
127
- -- Enable RLS to ensure users can only access their own data.
128
109
 
129
- -- Enable RLS on both tables
130
110
  ALTER TABLE studio_projects ENABLE ROW LEVEL SECURITY;
131
111
  ALTER TABLE studio_deployments ENABLE ROW LEVEL SECURITY;
132
112
 
133
113
  -- Projects: Users can only see/modify their own projects
134
- DROP POLICY IF EXISTS studio_projects_user_policy ON studio_projects;
135
- CREATE POLICY studio_projects_user_policy ON studio_projects
114
+ CREATE POLICY studio_projects_owner_all ON studio_projects
136
115
  FOR ALL
137
116
  USING (auth.uid() = user_id)
138
117
  WITH CHECK (auth.uid() = user_id);
139
118
 
140
119
  -- Deployments: Users can only see/modify their own deployments
141
- DROP POLICY IF EXISTS studio_deployments_user_policy ON studio_deployments;
142
- CREATE POLICY studio_deployments_user_policy ON studio_deployments
120
+ CREATE POLICY studio_deployments_owner_all ON studio_deployments
143
121
  FOR ALL
144
122
  USING (auth.uid() = user_id)
145
123
  WITH CHECK (auth.uid() = user_id);
146
124
 
147
125
  -- =============================================================================
148
- -- Comments
126
+ -- Integration with Existing Tables
127
+ -- =============================================================================
128
+ --
129
+ -- When a project is published to Feed:
130
+ -- 1. Create/update entry in `apps` table
131
+ -- 2. Create `app_versions` record
132
+ -- 3. Create `artifacts` record with:
133
+ -- - type = 'bundle'
134
+ -- - bucket = 'external'
135
+ -- - object_path = OSS URL
136
+ -- 4. Link deployment to artifact via artifact_id
137
+ -- 5. Link project to app via app_id
138
+ --
149
139
  -- =============================================================================
150
-
151
- COMMENT ON TABLE studio_projects IS 'React/Vite projects created in Agent Foundry Build Studio';
152
- COMMENT ON COLUMN studio_projects.root_path IS 'Local filesystem path - only valid on the machine where project was created';
153
- COMMENT ON COLUMN studio_projects.config IS 'JSON config: buildCommand, outputDir, envVars, baseUrl, viteConfig';
154
-
155
- COMMENT ON TABLE studio_deployments IS 'Build and deployment history to Alibaba Cloud OSS';
156
- COMMENT ON COLUMN studio_deployments.metadata IS 'JSON metadata: commitHash, branch, buildDurationMs, uploadDurationMs, fileCount';