@agent-foundry/studio 1.0.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/README.md +96 -0
- package/dist/db/client.d.ts +59 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +51 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/deployments.d.ts +65 -0
- package/dist/db/deployments.d.ts.map +1 -0
- package/dist/db/deployments.js +249 -0
- package/dist/db/deployments.js.map +1 -0
- package/dist/db/index.d.ts +7 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +7 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/projects.d.ts +48 -0
- package/dist/db/projects.d.ts.map +1 -0
- package/dist/db/projects.js +192 -0
- package/dist/db/projects.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/oss/client.d.ts +65 -0
- package/dist/oss/client.d.ts.map +1 -0
- package/dist/oss/client.js +146 -0
- package/dist/oss/client.js.map +1 -0
- package/dist/oss/index.d.ts +7 -0
- package/dist/oss/index.d.ts.map +1 -0
- package/dist/oss/index.js +7 -0
- package/dist/oss/index.js.map +1 -0
- package/dist/oss/types.d.ts +96 -0
- package/dist/oss/types.d.ts.map +1 -0
- package/dist/oss/types.js +5 -0
- package/dist/oss/types.js.map +1 -0
- package/dist/oss/uploader.d.ts +72 -0
- package/dist/oss/uploader.d.ts.map +1 -0
- package/dist/oss/uploader.js +185 -0
- package/dist/oss/uploader.js.map +1 -0
- package/dist/types/deployment.d.ts +112 -0
- package/dist/types/deployment.d.ts.map +1 -0
- package/dist/types/deployment.js +7 -0
- package/dist/types/deployment.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +90 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +8 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/user.d.ts +71 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +8 -0
- package/dist/types/user.js.map +1 -0
- package/dist/types/workspace.d.ts +88 -0
- package/dist/types/workspace.d.ts.map +1 -0
- package/dist/types/workspace.js +27 -0
- package/dist/types/workspace.js.map +1 -0
- package/dist/utils/build.d.ts +78 -0
- package/dist/utils/build.d.ts.map +1 -0
- package/dist/utils/build.js +148 -0
- package/dist/utils/build.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/manifest.d.ts +106 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +109 -0
- package/dist/utils/manifest.js.map +1 -0
- package/package.json +62 -0
- package/src/db/client.ts +92 -0
- package/src/db/deployments.ts +316 -0
- package/src/db/index.ts +7 -0
- package/src/db/projects.ts +246 -0
- package/src/db/schema.sql +156 -0
- package/src/index.ts +18 -0
- package/src/oss/client.ts +183 -0
- package/src/oss/index.ts +7 -0
- package/src/oss/types.ts +126 -0
- package/src/oss/uploader.ts +254 -0
- package/src/types/deployment.ts +147 -0
- package/src/types/index.ts +8 -0
- package/src/types/project.ts +114 -0
- package/src/types/user.ts +91 -0
- package/src/types/workspace.ts +124 -0
- package/src/utils/build.ts +199 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/manifest.ts +224 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployments Repository
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for studio_deployments table.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
8
|
+
import type {
|
|
9
|
+
Deployment,
|
|
10
|
+
CreateDeploymentInput,
|
|
11
|
+
UpdateDeploymentInput,
|
|
12
|
+
DeploymentListFilters,
|
|
13
|
+
DeploymentStatus,
|
|
14
|
+
DeploymentMetadata,
|
|
15
|
+
} from '../types/deployment';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Database row type (snake_case)
|
|
19
|
+
*/
|
|
20
|
+
interface DeploymentRow {
|
|
21
|
+
id: string;
|
|
22
|
+
project_id: string;
|
|
23
|
+
user_id: string;
|
|
24
|
+
version: string;
|
|
25
|
+
status: DeploymentStatus;
|
|
26
|
+
oss_bucket: string | null;
|
|
27
|
+
oss_key: string | null;
|
|
28
|
+
oss_url: string | null;
|
|
29
|
+
bundle_size_bytes: number | null;
|
|
30
|
+
build_log: string | null;
|
|
31
|
+
error_message: string | null;
|
|
32
|
+
metadata: DeploymentMetadata;
|
|
33
|
+
created_at: string;
|
|
34
|
+
published_at: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert database row to Deployment
|
|
39
|
+
*/
|
|
40
|
+
function rowToDeployment(row: DeploymentRow): Deployment {
|
|
41
|
+
return {
|
|
42
|
+
id: row.id,
|
|
43
|
+
projectId: row.project_id,
|
|
44
|
+
userId: row.user_id,
|
|
45
|
+
version: row.version,
|
|
46
|
+
status: row.status,
|
|
47
|
+
ossBucket: row.oss_bucket ?? undefined,
|
|
48
|
+
ossKey: row.oss_key ?? undefined,
|
|
49
|
+
ossUrl: row.oss_url ?? undefined,
|
|
50
|
+
bundleSizeBytes: row.bundle_size_bytes ?? undefined,
|
|
51
|
+
buildLog: row.build_log ?? undefined,
|
|
52
|
+
errorMessage: row.error_message ?? undefined,
|
|
53
|
+
metadata: row.metadata,
|
|
54
|
+
createdAt: row.created_at,
|
|
55
|
+
publishedAt: row.published_at ?? undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a version string based on timestamp
|
|
61
|
+
*/
|
|
62
|
+
function generateVersion(): string {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const year = now.getFullYear();
|
|
65
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
66
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
67
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
68
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
69
|
+
return `${year}.${month}.${day}-${hours}${minutes}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deployments Repository - manages studio_deployments table
|
|
74
|
+
*/
|
|
75
|
+
export class DeploymentsRepository {
|
|
76
|
+
private readonly TABLE = 'studio_deployments';
|
|
77
|
+
|
|
78
|
+
constructor(private readonly supabase: SupabaseClient) {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a new deployment
|
|
82
|
+
*/
|
|
83
|
+
async create(input: CreateDeploymentInput): Promise<Deployment> {
|
|
84
|
+
const { data: { user } } = await this.supabase.auth.getUser();
|
|
85
|
+
if (!user) {
|
|
86
|
+
throw new Error('Not authenticated');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const version = input.version ?? generateVersion();
|
|
90
|
+
|
|
91
|
+
const { data, error } = await this.supabase
|
|
92
|
+
.from(this.TABLE)
|
|
93
|
+
.insert({
|
|
94
|
+
project_id: input.projectId,
|
|
95
|
+
user_id: user.id,
|
|
96
|
+
version,
|
|
97
|
+
status: 'pending',
|
|
98
|
+
metadata: input.metadata ?? {},
|
|
99
|
+
})
|
|
100
|
+
.select()
|
|
101
|
+
.single();
|
|
102
|
+
|
|
103
|
+
if (error) {
|
|
104
|
+
throw new Error(`Failed to create deployment: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return rowToDeployment(data as DeploymentRow);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get a deployment by ID
|
|
112
|
+
*/
|
|
113
|
+
async getById(id: string): Promise<Deployment | null> {
|
|
114
|
+
const { data, error } = await this.supabase
|
|
115
|
+
.from(this.TABLE)
|
|
116
|
+
.select()
|
|
117
|
+
.eq('id', id)
|
|
118
|
+
.single();
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
if (error.code === 'PGRST116') {
|
|
122
|
+
return null; // Not found
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Failed to get deployment: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return rowToDeployment(data as DeploymentRow);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* List deployments with optional filters
|
|
132
|
+
*/
|
|
133
|
+
async list(filters: DeploymentListFilters = {}): Promise<Deployment[]> {
|
|
134
|
+
let query = this.supabase.from(this.TABLE).select();
|
|
135
|
+
|
|
136
|
+
// Apply filters
|
|
137
|
+
if (filters.projectId) {
|
|
138
|
+
query = query.eq('project_id', filters.projectId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (filters.status) {
|
|
142
|
+
query = query.eq('status', filters.status);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ordering
|
|
146
|
+
const orderBy = filters.orderBy ?? 'createdAt';
|
|
147
|
+
const orderColumn = orderBy === 'createdAt' ? 'created_at' :
|
|
148
|
+
orderBy === 'publishedAt' ? 'published_at' : 'version';
|
|
149
|
+
const ascending = filters.orderDir === 'asc';
|
|
150
|
+
query = query.order(orderColumn, { ascending, nullsFirst: false });
|
|
151
|
+
|
|
152
|
+
// Pagination
|
|
153
|
+
if (filters.limit) {
|
|
154
|
+
query = query.limit(filters.limit);
|
|
155
|
+
}
|
|
156
|
+
if (filters.offset) {
|
|
157
|
+
query = query.range(filters.offset, filters.offset + (filters.limit ?? 50) - 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { data, error } = await query;
|
|
161
|
+
|
|
162
|
+
if (error) {
|
|
163
|
+
throw new Error(`Failed to list deployments: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (data as DeploymentRow[]).map(rowToDeployment);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update a deployment
|
|
171
|
+
*/
|
|
172
|
+
async update(id: string, input: UpdateDeploymentInput): Promise<Deployment> {
|
|
173
|
+
const updateData: Record<string, unknown> = {};
|
|
174
|
+
|
|
175
|
+
if (input.status !== undefined) {
|
|
176
|
+
updateData.status = input.status;
|
|
177
|
+
}
|
|
178
|
+
if (input.ossBucket !== undefined) {
|
|
179
|
+
updateData.oss_bucket = input.ossBucket;
|
|
180
|
+
}
|
|
181
|
+
if (input.ossKey !== undefined) {
|
|
182
|
+
updateData.oss_key = input.ossKey;
|
|
183
|
+
}
|
|
184
|
+
if (input.ossUrl !== undefined) {
|
|
185
|
+
updateData.oss_url = input.ossUrl;
|
|
186
|
+
}
|
|
187
|
+
if (input.bundleSizeBytes !== undefined) {
|
|
188
|
+
updateData.bundle_size_bytes = input.bundleSizeBytes;
|
|
189
|
+
}
|
|
190
|
+
if (input.buildLog !== undefined) {
|
|
191
|
+
updateData.build_log = input.buildLog;
|
|
192
|
+
}
|
|
193
|
+
if (input.errorMessage !== undefined) {
|
|
194
|
+
updateData.error_message = input.errorMessage;
|
|
195
|
+
}
|
|
196
|
+
if (input.metadata !== undefined) {
|
|
197
|
+
// Merge with existing metadata
|
|
198
|
+
const existing = await this.getById(id);
|
|
199
|
+
if (!existing) {
|
|
200
|
+
throw new Error('Deployment not found');
|
|
201
|
+
}
|
|
202
|
+
updateData.metadata = { ...existing.metadata, ...input.metadata };
|
|
203
|
+
}
|
|
204
|
+
if (input.publishedAt !== undefined) {
|
|
205
|
+
updateData.published_at = input.publishedAt;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { data, error } = await this.supabase
|
|
209
|
+
.from(this.TABLE)
|
|
210
|
+
.update(updateData)
|
|
211
|
+
.eq('id', id)
|
|
212
|
+
.select()
|
|
213
|
+
.single();
|
|
214
|
+
|
|
215
|
+
if (error) {
|
|
216
|
+
throw new Error(`Failed to update deployment: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return rowToDeployment(data as DeploymentRow);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Mark deployment as building
|
|
224
|
+
*/
|
|
225
|
+
async markBuilding(id: string): Promise<Deployment> {
|
|
226
|
+
return this.update(id, { status: 'building' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Mark deployment as uploading
|
|
231
|
+
*/
|
|
232
|
+
async markUploading(id: string): Promise<Deployment> {
|
|
233
|
+
return this.update(id, { status: 'uploading' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Mark deployment as published
|
|
238
|
+
*/
|
|
239
|
+
async markPublished(
|
|
240
|
+
id: string,
|
|
241
|
+
ossInfo: { bucket: string; key: string; url: string; sizeBytes: number }
|
|
242
|
+
): Promise<Deployment> {
|
|
243
|
+
return this.update(id, {
|
|
244
|
+
status: 'published',
|
|
245
|
+
ossBucket: ossInfo.bucket,
|
|
246
|
+
ossKey: ossInfo.key,
|
|
247
|
+
ossUrl: ossInfo.url,
|
|
248
|
+
bundleSizeBytes: ossInfo.sizeBytes,
|
|
249
|
+
publishedAt: new Date().toISOString(),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Mark deployment as failed
|
|
255
|
+
*/
|
|
256
|
+
async markFailed(id: string, errorMessage: string, buildLog?: string): Promise<Deployment> {
|
|
257
|
+
return this.update(id, {
|
|
258
|
+
status: 'failed',
|
|
259
|
+
errorMessage,
|
|
260
|
+
buildLog,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the latest successful deployment for a project
|
|
266
|
+
*/
|
|
267
|
+
async getLatestPublished(projectId: string): Promise<Deployment | null> {
|
|
268
|
+
const { data, error } = await this.supabase
|
|
269
|
+
.from(this.TABLE)
|
|
270
|
+
.select()
|
|
271
|
+
.eq('project_id', projectId)
|
|
272
|
+
.eq('status', 'published')
|
|
273
|
+
.order('published_at', { ascending: false })
|
|
274
|
+
.limit(1)
|
|
275
|
+
.single();
|
|
276
|
+
|
|
277
|
+
if (error) {
|
|
278
|
+
if (error.code === 'PGRST116') {
|
|
279
|
+
return null; // Not found
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`Failed to get latest deployment: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return rowToDeployment(data as DeploymentRow);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Delete a deployment
|
|
289
|
+
*/
|
|
290
|
+
async delete(id: string): Promise<void> {
|
|
291
|
+
const { error } = await this.supabase
|
|
292
|
+
.from(this.TABLE)
|
|
293
|
+
.delete()
|
|
294
|
+
.eq('id', id);
|
|
295
|
+
|
|
296
|
+
if (error) {
|
|
297
|
+
throw new Error(`Failed to delete deployment: ${error.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Append to build log
|
|
303
|
+
*/
|
|
304
|
+
async appendBuildLog(id: string, logLine: string): Promise<void> {
|
|
305
|
+
const existing = await this.getById(id);
|
|
306
|
+
if (!existing) {
|
|
307
|
+
throw new Error('Deployment not found');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const newLog = existing.buildLog
|
|
311
|
+
? `${existing.buildLog}\n${logLine}`
|
|
312
|
+
: logLine;
|
|
313
|
+
|
|
314
|
+
await this.update(id, { buildLog: newLog });
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects Repository
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for studio_projects table.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
8
|
+
import type {
|
|
9
|
+
StudioProject,
|
|
10
|
+
CreateProjectInput,
|
|
11
|
+
UpdateProjectInput,
|
|
12
|
+
ProjectListFilters,
|
|
13
|
+
ProjectConfig,
|
|
14
|
+
} from '../types/project';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Database row type (snake_case)
|
|
18
|
+
*/
|
|
19
|
+
interface ProjectRow {
|
|
20
|
+
id: string;
|
|
21
|
+
user_id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
description: string | null;
|
|
25
|
+
root_path: string;
|
|
26
|
+
framework: string;
|
|
27
|
+
config: ProjectConfig;
|
|
28
|
+
parent_project_id: string | null;
|
|
29
|
+
created_at: string;
|
|
30
|
+
updated_at: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert database row to StudioProject
|
|
35
|
+
*/
|
|
36
|
+
function rowToProject(row: ProjectRow): StudioProject {
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
userId: row.user_id,
|
|
40
|
+
name: row.name,
|
|
41
|
+
slug: row.slug,
|
|
42
|
+
description: row.description ?? undefined,
|
|
43
|
+
rootPath: row.root_path,
|
|
44
|
+
framework: row.framework as StudioProject['framework'],
|
|
45
|
+
config: row.config,
|
|
46
|
+
parentProjectId: row.parent_project_id ?? undefined,
|
|
47
|
+
createdAt: row.created_at,
|
|
48
|
+
updatedAt: row.updated_at,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Projects Repository - manages studio_projects table
|
|
54
|
+
*/
|
|
55
|
+
export class ProjectsRepository {
|
|
56
|
+
private readonly TABLE = 'studio_projects';
|
|
57
|
+
|
|
58
|
+
constructor(private readonly supabase: SupabaseClient) {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new project
|
|
62
|
+
*/
|
|
63
|
+
async create(input: CreateProjectInput): Promise<StudioProject> {
|
|
64
|
+
const { data: { user } } = await this.supabase.auth.getUser();
|
|
65
|
+
if (!user) {
|
|
66
|
+
throw new Error('Not authenticated');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { data, error } = await this.supabase
|
|
70
|
+
.from(this.TABLE)
|
|
71
|
+
.insert({
|
|
72
|
+
user_id: user.id,
|
|
73
|
+
name: input.name,
|
|
74
|
+
slug: input.slug,
|
|
75
|
+
description: input.description,
|
|
76
|
+
root_path: input.rootPath,
|
|
77
|
+
framework: input.framework ?? 'vite-react',
|
|
78
|
+
config: input.config ?? {},
|
|
79
|
+
parent_project_id: input.parentProjectId,
|
|
80
|
+
})
|
|
81
|
+
.select()
|
|
82
|
+
.single();
|
|
83
|
+
|
|
84
|
+
if (error) {
|
|
85
|
+
throw new Error(`Failed to create project: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return rowToProject(data as ProjectRow);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a project by ID
|
|
93
|
+
*/
|
|
94
|
+
async getById(id: string): Promise<StudioProject | null> {
|
|
95
|
+
const { data, error } = await this.supabase
|
|
96
|
+
.from(this.TABLE)
|
|
97
|
+
.select()
|
|
98
|
+
.eq('id', id)
|
|
99
|
+
.single();
|
|
100
|
+
|
|
101
|
+
if (error) {
|
|
102
|
+
if (error.code === 'PGRST116') {
|
|
103
|
+
return null; // Not found
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Failed to get project: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return rowToProject(data as ProjectRow);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get a project by slug (for current user)
|
|
113
|
+
*/
|
|
114
|
+
async getBySlug(slug: string): Promise<StudioProject | null> {
|
|
115
|
+
const { data, error } = await this.supabase
|
|
116
|
+
.from(this.TABLE)
|
|
117
|
+
.select()
|
|
118
|
+
.eq('slug', slug)
|
|
119
|
+
.single();
|
|
120
|
+
|
|
121
|
+
if (error) {
|
|
122
|
+
if (error.code === 'PGRST116') {
|
|
123
|
+
return null; // Not found
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to get project: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return rowToProject(data as ProjectRow);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List projects with optional filters
|
|
133
|
+
*/
|
|
134
|
+
async list(filters: ProjectListFilters = {}): Promise<StudioProject[]> {
|
|
135
|
+
let query = this.supabase.from(this.TABLE).select();
|
|
136
|
+
|
|
137
|
+
// Apply filters
|
|
138
|
+
if (filters.framework) {
|
|
139
|
+
query = query.eq('framework', filters.framework);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (filters.search) {
|
|
143
|
+
query = query.ilike('name', `%${filters.search}%`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Ordering
|
|
147
|
+
const orderBy = filters.orderBy ?? 'createdAt';
|
|
148
|
+
const orderColumn = orderBy === 'createdAt' ? 'created_at' :
|
|
149
|
+
orderBy === 'updatedAt' ? 'updated_at' : 'name';
|
|
150
|
+
const ascending = filters.orderDir === 'asc';
|
|
151
|
+
query = query.order(orderColumn, { ascending });
|
|
152
|
+
|
|
153
|
+
// Pagination
|
|
154
|
+
if (filters.limit) {
|
|
155
|
+
query = query.limit(filters.limit);
|
|
156
|
+
}
|
|
157
|
+
if (filters.offset) {
|
|
158
|
+
query = query.range(filters.offset, filters.offset + (filters.limit ?? 50) - 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { data, error } = await query;
|
|
162
|
+
|
|
163
|
+
if (error) {
|
|
164
|
+
throw new Error(`Failed to list projects: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (data as ProjectRow[]).map(rowToProject);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update a project
|
|
172
|
+
*/
|
|
173
|
+
async update(id: string, input: UpdateProjectInput): Promise<StudioProject> {
|
|
174
|
+
const updateData: Record<string, unknown> = {};
|
|
175
|
+
|
|
176
|
+
if (input.name !== undefined) {
|
|
177
|
+
updateData.name = input.name;
|
|
178
|
+
}
|
|
179
|
+
if (input.description !== undefined) {
|
|
180
|
+
updateData.description = input.description;
|
|
181
|
+
}
|
|
182
|
+
if (input.config !== undefined) {
|
|
183
|
+
// Merge with existing config
|
|
184
|
+
const existing = await this.getById(id);
|
|
185
|
+
if (!existing) {
|
|
186
|
+
throw new Error('Project not found');
|
|
187
|
+
}
|
|
188
|
+
updateData.config = { ...existing.config, ...input.config };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { data, error } = await this.supabase
|
|
192
|
+
.from(this.TABLE)
|
|
193
|
+
.update(updateData)
|
|
194
|
+
.eq('id', id)
|
|
195
|
+
.select()
|
|
196
|
+
.single();
|
|
197
|
+
|
|
198
|
+
if (error) {
|
|
199
|
+
throw new Error(`Failed to update project: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return rowToProject(data as ProjectRow);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Delete a project
|
|
207
|
+
*/
|
|
208
|
+
async delete(id: string): Promise<void> {
|
|
209
|
+
const { error } = await this.supabase
|
|
210
|
+
.from(this.TABLE)
|
|
211
|
+
.delete()
|
|
212
|
+
.eq('id', id);
|
|
213
|
+
|
|
214
|
+
if (error) {
|
|
215
|
+
throw new Error(`Failed to delete project: ${error.message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Fork a project (create a copy)
|
|
221
|
+
*/
|
|
222
|
+
async fork(id: string, newSlug: string, newRootPath: string): Promise<StudioProject> {
|
|
223
|
+
const original = await this.getById(id);
|
|
224
|
+
if (!original) {
|
|
225
|
+
throw new Error('Original project not found');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return this.create({
|
|
229
|
+
name: `${original.name} (Fork)`,
|
|
230
|
+
slug: newSlug,
|
|
231
|
+
description: original.description,
|
|
232
|
+
rootPath: newRootPath,
|
|
233
|
+
framework: original.framework,
|
|
234
|
+
config: original.config,
|
|
235
|
+
parentProjectId: original.id,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if a slug is available for current user
|
|
241
|
+
*/
|
|
242
|
+
async isSlugAvailable(slug: string): Promise<boolean> {
|
|
243
|
+
const existing = await this.getBySlug(slug);
|
|
244
|
+
return existing === null;
|
|
245
|
+
}
|
|
246
|
+
}
|