@sdods/db 0.2.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.
Files changed (87) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/col.d.ts +32 -0
  4. package/dist/col.js +58 -0
  5. package/dist/create-db.d.ts +14 -0
  6. package/dist/create-db.js +58 -0
  7. package/dist/driver.d.ts +10 -0
  8. package/dist/driver.js +16 -0
  9. package/dist/ids.d.ts +2 -0
  10. package/dist/ids.js +2 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/ingest/attachments.d.ts +40 -0
  14. package/dist/ingest/attachments.js +154 -0
  15. package/dist/ingest/cucumber-ingest.d.ts +40 -0
  16. package/dist/ingest/cucumber-ingest.js +543 -0
  17. package/dist/ingest/finalize.d.ts +28 -0
  18. package/dist/ingest/finalize.js +132 -0
  19. package/dist/ingest/index.d.ts +26 -0
  20. package/dist/ingest/index.js +193 -0
  21. package/dist/ingest/message-index.d.ts +64 -0
  22. package/dist/ingest/message-index.js +146 -0
  23. package/dist/ingest/parse-ndjson.d.ts +10 -0
  24. package/dist/ingest/parse-ndjson.js +35 -0
  25. package/dist/ingest/pw-json-ingest.d.ts +14 -0
  26. package/dist/ingest/pw-json-ingest.js +259 -0
  27. package/dist/ingest/runner-json-ingest.d.ts +14 -0
  28. package/dist/ingest/runner-json-ingest.js +259 -0
  29. package/dist/ingest/types.d.ts +62 -0
  30. package/dist/ingest/types.js +89 -0
  31. package/dist/insights.d.ts +62 -0
  32. package/dist/insights.js +184 -0
  33. package/dist/migrate.d.ts +30 -0
  34. package/dist/migrate.js +114 -0
  35. package/dist/migrations/0001_platform_init.d.ts +5 -0
  36. package/dist/migrations/0001_platform_init.js +48 -0
  37. package/dist/migrations/0002_results.d.ts +5 -0
  38. package/dist/migrations/0002_results.js +124 -0
  39. package/dist/migrations/0003_improvement.d.ts +5 -0
  40. package/dist/migrations/0003_improvement.js +56 -0
  41. package/dist/migrations/0004_test_data.d.ts +5 -0
  42. package/dist/migrations/0004_test_data.js +48 -0
  43. package/dist/migrations/0005_integrations_agents_audit.d.ts +5 -0
  44. package/dist/migrations/0005_integrations_agents_audit.js +100 -0
  45. package/dist/migrations/0006_schedules.d.ts +5 -0
  46. package/dist/migrations/0006_schedules.js +34 -0
  47. package/dist/migrations/0007_hierarchy.d.ts +5 -0
  48. package/dist/migrations/0007_hierarchy.js +76 -0
  49. package/dist/migrations/0008_runner_naming.d.ts +14 -0
  50. package/dist/migrations/0008_runner_naming.js +35 -0
  51. package/dist/migrations/helpers.d.ts +15 -0
  52. package/dist/migrations/helpers.js +30 -0
  53. package/dist/migrations/index.d.ts +6 -0
  54. package/dist/migrations/index.js +22 -0
  55. package/dist/repos/auth.d.ts +109 -0
  56. package/dist/repos/auth.js +258 -0
  57. package/dist/repos/hierarchy.d.ts +151 -0
  58. package/dist/repos/hierarchy.js +465 -0
  59. package/dist/repos/improvement.d.ts +84 -0
  60. package/dist/repos/improvement.js +242 -0
  61. package/dist/repos/index.d.ts +8 -0
  62. package/dist/repos/index.js +8 -0
  63. package/dist/repos/platform.d.ts +250 -0
  64. package/dist/repos/platform.js +429 -0
  65. package/dist/repos/projects.d.ts +87 -0
  66. package/dist/repos/projects.js +118 -0
  67. package/dist/repos/runs.d.ts +306 -0
  68. package/dist/repos/runs.js +321 -0
  69. package/dist/repos/test-data.d.ts +84 -0
  70. package/dist/repos/test-data.js +291 -0
  71. package/dist/schema.d.ts +514 -0
  72. package/dist/schema.js +37 -0
  73. package/dist/scopes.d.ts +2 -0
  74. package/dist/scopes.js +2 -0
  75. package/dist/switch/export.d.ts +15 -0
  76. package/dist/switch/export.js +67 -0
  77. package/dist/switch/import.d.ts +16 -0
  78. package/dist/switch/import.js +106 -0
  79. package/dist/switch/index.d.ts +5 -0
  80. package/dist/switch/index.js +5 -0
  81. package/dist/switch/prune.d.ts +19 -0
  82. package/dist/switch/prune.js +50 -0
  83. package/dist/switch/switch.d.ts +34 -0
  84. package/dist/switch/switch.js +133 -0
  85. package/dist/test-data.d.ts +22 -0
  86. package/dist/test-data.js +79 -0
  87. package/package.json +50 -0
@@ -0,0 +1,429 @@
1
+ import { enc, nowIso, readBool, readJson, readTs } from '../col.js';
2
+ import { newId } from '../ids.js';
3
+ // ── integrations ──────────────────────────────────────────────────────────
4
+ export async function upsertIntegration(db, driver, input) {
5
+ const existing = await db
6
+ .selectFrom('integrations')
7
+ .select(['id'])
8
+ .where('project_id', '=', input.projectId)
9
+ .where('provider', '=', input.provider)
10
+ .executeTakeFirst();
11
+ const values = {
12
+ config_json: enc.json(input.config),
13
+ secret_env_json: enc.json(input.secretEnv ?? {}),
14
+ enabled: enc.bool(driver, input.enabled ?? false),
15
+ updated_at: nowIso(),
16
+ };
17
+ if (existing) {
18
+ await db.updateTable('integrations').set(values).where('id', '=', existing.id).execute();
19
+ return existing.id;
20
+ }
21
+ const id = newId();
22
+ await db
23
+ .insertInto('integrations')
24
+ .values({
25
+ id,
26
+ project_id: input.projectId,
27
+ provider: input.provider,
28
+ last_sync_at: null,
29
+ created_at: nowIso(),
30
+ ...values,
31
+ })
32
+ .execute();
33
+ return id;
34
+ }
35
+ export async function listIntegrations(db, projectId) {
36
+ const rows = await db
37
+ .selectFrom('integrations')
38
+ .selectAll()
39
+ .where('project_id', '=', projectId)
40
+ .orderBy('provider')
41
+ .execute();
42
+ return rows.map((r) => ({
43
+ id: r.id,
44
+ provider: r.provider,
45
+ config: readJson(r.config_json) ?? {},
46
+ secretEnv: readJson(r.secret_env_json) ?? {},
47
+ enabled: readBool(r.enabled),
48
+ lastSyncAt: readTs(r.last_sync_at),
49
+ }));
50
+ }
51
+ // ── issue links ───────────────────────────────────────────────────────────
52
+ export async function upsertIssueLink(db, input) {
53
+ const existing = await db
54
+ .selectFrom('issue_links')
55
+ .select(['id'])
56
+ .where('project_id', '=', input.projectId)
57
+ .where('provider', '=', input.provider)
58
+ .where('fingerprint', '=', input.fingerprint)
59
+ .where('external_key', '=', input.externalKey)
60
+ .executeTakeFirst();
61
+ const values = {
62
+ integration_id: input.integrationId ?? null,
63
+ scenario_name: input.scenarioName ?? null,
64
+ external_url: input.externalUrl ?? null,
65
+ status: input.status ?? 'unknown',
66
+ source: input.source ?? 'auto',
67
+ last_run_id: input.lastRunId ?? null,
68
+ last_synced_at: nowIso(),
69
+ updated_at: nowIso(),
70
+ };
71
+ if (existing) {
72
+ await db.updateTable('issue_links').set(values).where('id', '=', existing.id).execute();
73
+ return existing.id;
74
+ }
75
+ const id = newId();
76
+ await db
77
+ .insertInto('issue_links')
78
+ .values({
79
+ id,
80
+ project_id: input.projectId,
81
+ provider: input.provider,
82
+ fingerprint: input.fingerprint,
83
+ external_key: input.externalKey,
84
+ created_at: nowIso(),
85
+ ...values,
86
+ })
87
+ .execute();
88
+ return id;
89
+ }
90
+ export async function findOpenIssueLink(db, projectId, provider, fingerprint) {
91
+ const row = await db
92
+ .selectFrom('issue_links')
93
+ .selectAll()
94
+ .where('project_id', '=', projectId)
95
+ .where('provider', '=', provider)
96
+ .where('fingerprint', '=', fingerprint)
97
+ .where('status', '=', 'open')
98
+ .executeTakeFirst();
99
+ return row ? mapIssueLink(row) : null;
100
+ }
101
+ export async function listIssueLinks(db, opts) {
102
+ let q = db.selectFrom('issue_links').selectAll().orderBy('updated_at', 'desc');
103
+ if (opts.projectId)
104
+ q = q.where('project_id', '=', opts.projectId);
105
+ if (opts.fingerprint)
106
+ q = q.where('fingerprint', '=', opts.fingerprint);
107
+ if (opts.provider)
108
+ q = q.where('provider', '=', opts.provider);
109
+ return (await q.execute()).map(mapIssueLink);
110
+ }
111
+ export async function setIssueLinkStatus(db, id, status) {
112
+ await db
113
+ .updateTable('issue_links')
114
+ .set({ status, last_synced_at: nowIso(), updated_at: nowIso() })
115
+ .where('id', '=', id)
116
+ .execute();
117
+ }
118
+ function mapIssueLink(r) {
119
+ return {
120
+ id: r.id,
121
+ projectId: r.project_id,
122
+ integrationId: r.integration_id,
123
+ provider: r.provider,
124
+ fingerprint: r.fingerprint,
125
+ scenarioName: r.scenario_name,
126
+ externalKey: r.external_key,
127
+ externalUrl: r.external_url,
128
+ status: r.status,
129
+ source: r.source,
130
+ lastRunId: r.last_run_id,
131
+ lastSyncedAt: readTs(r.last_synced_at),
132
+ };
133
+ }
134
+ // ── proposals ─────────────────────────────────────────────────────────────
135
+ export async function upsertProposal(db, input) {
136
+ const existing = await db
137
+ .selectFrom('proposals')
138
+ .select(['id'])
139
+ .where('id', '=', input.id)
140
+ .executeTakeFirst();
141
+ const values = {
142
+ role: input.role,
143
+ status: input.status ?? 'pending',
144
+ summary: input.summary ?? null,
145
+ manifest_json: enc.json(input.manifest),
146
+ cost_usd: input.costUsd ?? null,
147
+ model: input.model ?? null,
148
+ };
149
+ if (existing) {
150
+ await db.updateTable('proposals').set(values).where('id', '=', input.id).execute();
151
+ return;
152
+ }
153
+ await db
154
+ .insertInto('proposals')
155
+ .values({
156
+ id: input.id,
157
+ project_id: input.projectId,
158
+ created_by: input.createdBy ?? null,
159
+ reviewed_by: null,
160
+ created_at: nowIso(),
161
+ reviewed_at: null,
162
+ ...values,
163
+ })
164
+ .execute();
165
+ }
166
+ export async function reviewProposal(db, id, status, reviewedBy) {
167
+ await db
168
+ .updateTable('proposals')
169
+ .set({ status, reviewed_by: reviewedBy ?? null, reviewed_at: nowIso() })
170
+ .where('id', '=', id)
171
+ .execute();
172
+ }
173
+ export async function listProposals(db, opts = {}) {
174
+ let q = db
175
+ .selectFrom('proposals')
176
+ .selectAll()
177
+ .orderBy('created_at', 'desc')
178
+ .limit(opts.limit ?? 100);
179
+ if (opts.projectId)
180
+ q = q.where('project_id', '=', opts.projectId);
181
+ if (opts.status)
182
+ q = q.where('status', '=', opts.status);
183
+ return (await q.execute()).map((p) => ({
184
+ id: p.id,
185
+ projectId: p.project_id,
186
+ role: p.role,
187
+ status: p.status,
188
+ summary: p.summary,
189
+ manifest: readJson(p.manifest_json) ?? {},
190
+ costUsd: p.cost_usd,
191
+ model: p.model,
192
+ createdBy: p.created_by,
193
+ reviewedBy: p.reviewed_by,
194
+ createdAt: readTs(p.created_at),
195
+ reviewedAt: readTs(p.reviewed_at),
196
+ }));
197
+ }
198
+ // ── agent jobs ────────────────────────────────────────────────────────────
199
+ export async function createAgentJob(db, input) {
200
+ const id = newId();
201
+ await db
202
+ .insertInto('agent_jobs')
203
+ .values({
204
+ id,
205
+ project_id: input.projectId,
206
+ kind: input.kind,
207
+ goal: input.goal ?? null,
208
+ status: 'queued',
209
+ input_json: enc.json(input.input ?? {}),
210
+ output_json: null,
211
+ diff_text: null,
212
+ log_rel: null,
213
+ provider: input.provider ?? null,
214
+ model: input.model ?? null,
215
+ tokens_in: null,
216
+ tokens_out: null,
217
+ cost_usd: null,
218
+ turns: null,
219
+ proposal_id: null,
220
+ started_by: input.startedBy ?? null,
221
+ reviewed_by: null,
222
+ started_at: null,
223
+ finished_at: null,
224
+ reviewed_at: null,
225
+ error: null,
226
+ created_at: nowIso(),
227
+ })
228
+ .execute();
229
+ return id;
230
+ }
231
+ export async function updateAgentJob(db, id, patch) {
232
+ await db
233
+ .updateTable('agent_jobs')
234
+ .set({
235
+ ...(patch.status !== undefined ? { status: patch.status } : {}),
236
+ ...(patch.output !== undefined ? { output_json: enc.json(patch.output) } : {}),
237
+ ...(patch.diffText !== undefined ? { diff_text: patch.diffText } : {}),
238
+ ...(patch.logRel !== undefined ? { log_rel: patch.logRel } : {}),
239
+ ...(patch.tokensIn !== undefined ? { tokens_in: patch.tokensIn } : {}),
240
+ ...(patch.tokensOut !== undefined ? { tokens_out: patch.tokensOut } : {}),
241
+ ...(patch.costUsd !== undefined ? { cost_usd: patch.costUsd } : {}),
242
+ ...(patch.turns !== undefined ? { turns: patch.turns } : {}),
243
+ ...(patch.proposalId !== undefined ? { proposal_id: patch.proposalId } : {}),
244
+ ...(patch.startedAt !== undefined ? { started_at: patch.startedAt } : {}),
245
+ ...(patch.finishedAt !== undefined ? { finished_at: patch.finishedAt } : {}),
246
+ ...(patch.error !== undefined ? { error: patch.error } : {}),
247
+ ...(patch.reviewedBy !== undefined ? { reviewed_by: patch.reviewedBy } : {}),
248
+ ...(patch.reviewedAt !== undefined ? { reviewed_at: patch.reviewedAt } : {}),
249
+ })
250
+ .where('id', '=', id)
251
+ .execute();
252
+ }
253
+ export async function getAgentJob(db, id) {
254
+ const j = await db.selectFrom('agent_jobs').selectAll().where('id', '=', id).executeTakeFirst();
255
+ return j ? mapAgentJob(j) : null;
256
+ }
257
+ export async function listAgentJobs(db, opts = {}) {
258
+ let q = db
259
+ .selectFrom('agent_jobs')
260
+ .selectAll()
261
+ .orderBy('created_at', 'desc')
262
+ .limit(opts.limit ?? 50);
263
+ if (opts.projectId)
264
+ q = q.where('project_id', '=', opts.projectId);
265
+ if (opts.status)
266
+ q = q.where('status', '=', opts.status);
267
+ return (await q.execute()).map(mapAgentJob);
268
+ }
269
+ function mapAgentJob(j) {
270
+ return {
271
+ id: j.id,
272
+ projectId: j.project_id,
273
+ kind: j.kind,
274
+ goal: j.goal,
275
+ status: j.status,
276
+ input: readJson(j.input_json) ?? {},
277
+ output: readJson(j.output_json),
278
+ diffText: j.diff_text,
279
+ logRel: j.log_rel,
280
+ provider: j.provider,
281
+ model: j.model,
282
+ tokensIn: j.tokens_in,
283
+ tokensOut: j.tokens_out,
284
+ costUsd: j.cost_usd,
285
+ turns: j.turns,
286
+ proposalId: j.proposal_id,
287
+ startedBy: j.started_by,
288
+ startedAt: readTs(j.started_at),
289
+ finishedAt: readTs(j.finished_at),
290
+ error: j.error,
291
+ createdAt: readTs(j.created_at),
292
+ };
293
+ }
294
+ export async function upsertSchedule(db, driver, s) {
295
+ const existing = await db
296
+ .selectFrom('schedules')
297
+ .select(['id'])
298
+ .where('project_id', '=', s.projectId)
299
+ .where('name', '=', s.name)
300
+ .executeTakeFirst();
301
+ const values = {
302
+ environment_id: s.environmentId ?? null,
303
+ cron_expr: s.cronExpr,
304
+ timezone: s.timezone ?? 'UTC',
305
+ run_input_json: enc.json(s.runInput),
306
+ overlap_policy: s.overlapPolicy ?? 'skip',
307
+ jitter_seconds: s.jitterSeconds ?? 0,
308
+ catch_up: enc.bool(driver, s.catchUp ?? false),
309
+ enabled: enc.bool(driver, s.enabled ?? true),
310
+ notify_json: enc.json(s.notify ?? []),
311
+ retention_runs: s.retentionRuns ?? null,
312
+ next_run_at: s.nextRunAt ?? null,
313
+ updated_at: nowIso(),
314
+ };
315
+ if (existing) {
316
+ await db.updateTable('schedules').set(values).where('id', '=', existing.id).execute();
317
+ return existing.id;
318
+ }
319
+ const id = newId();
320
+ await db
321
+ .insertInto('schedules')
322
+ .values({
323
+ id,
324
+ project_id: s.projectId,
325
+ name: s.name,
326
+ last_run_id: null,
327
+ last_status: null,
328
+ created_by: s.createdBy ?? null,
329
+ created_at: nowIso(),
330
+ ...values,
331
+ })
332
+ .execute();
333
+ return id;
334
+ }
335
+ export async function listSchedules(db, projectId) {
336
+ let q = db.selectFrom('schedules').selectAll().orderBy('name');
337
+ if (projectId)
338
+ q = q.where('project_id', '=', projectId);
339
+ return (await q.execute()).map((s) => ({
340
+ id: s.id,
341
+ projectId: s.project_id,
342
+ environmentId: s.environment_id,
343
+ name: s.name,
344
+ cronExpr: s.cron_expr,
345
+ timezone: s.timezone,
346
+ runInput: readJson(s.run_input_json) ?? {},
347
+ overlapPolicy: s.overlap_policy,
348
+ jitterSeconds: s.jitter_seconds,
349
+ catchUp: readBool(s.catch_up),
350
+ enabled: readBool(s.enabled),
351
+ notify: readJson(s.notify_json) ?? [],
352
+ retentionRuns: s.retention_runs,
353
+ nextRunAt: readTs(s.next_run_at),
354
+ lastRunId: s.last_run_id,
355
+ lastStatus: s.last_status,
356
+ }));
357
+ }
358
+ export async function updateScheduleState(db, id, patch, driver) {
359
+ await db
360
+ .updateTable('schedules')
361
+ .set({
362
+ ...(patch.nextRunAt !== undefined ? { next_run_at: patch.nextRunAt } : {}),
363
+ ...(patch.lastRunId !== undefined ? { last_run_id: patch.lastRunId } : {}),
364
+ ...(patch.lastStatus !== undefined ? { last_status: patch.lastStatus } : {}),
365
+ ...(patch.enabled !== undefined
366
+ ? { enabled: enc.bool(driver, patch.enabled) }
367
+ : {}),
368
+ updated_at: nowIso(),
369
+ })
370
+ .where('id', '=', id)
371
+ .execute();
372
+ }
373
+ export async function deleteSchedule(db, id) {
374
+ await db.deleteFrom('schedules').where('id', '=', id).execute();
375
+ }
376
+ export async function recordScheduleRun(db, input) {
377
+ await db
378
+ .insertInto('schedule_runs')
379
+ .values({
380
+ id: newId(),
381
+ schedule_id: input.scheduleId,
382
+ run_id: input.runId ?? null,
383
+ fired_at: nowIso(),
384
+ status: input.status,
385
+ note: input.note ?? null,
386
+ })
387
+ .execute();
388
+ }
389
+ export async function listScheduleRuns(db, scheduleId, limit = 50) {
390
+ return db
391
+ .selectFrom('schedule_runs')
392
+ .selectAll()
393
+ .where('schedule_id', '=', scheduleId)
394
+ .orderBy('fired_at', 'desc')
395
+ .limit(limit)
396
+ .execute();
397
+ }
398
+ // ── audit ─────────────────────────────────────────────────────────────────
399
+ export async function audit(db, input) {
400
+ await db
401
+ .insertInto('audit_log')
402
+ .values({
403
+ id: newId(),
404
+ actor_user_id: input.actorUserId ?? null,
405
+ actor_type: input.actorType ?? 'system',
406
+ action: input.action,
407
+ target_type: input.targetType ?? null,
408
+ target_id: input.targetId ?? null,
409
+ details_json: input.details ? enc.json(input.details) : null,
410
+ ip: input.ip ?? null,
411
+ created_at: nowIso(),
412
+ })
413
+ .execute();
414
+ }
415
+ export async function listAudit(db, opts = {}) {
416
+ let q = db
417
+ .selectFrom('audit_log')
418
+ .selectAll()
419
+ .orderBy('created_at', 'desc')
420
+ .limit(opts.limit ?? 100);
421
+ if (opts.before)
422
+ q = q.where('created_at', '<', opts.before);
423
+ return (await q.execute()).map((a) => ({
424
+ ...a,
425
+ details: readJson(a.details_json),
426
+ createdAt: readTs(a.created_at),
427
+ }));
428
+ }
429
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1,87 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { Driver } from '../driver.js';
3
+ import type { Database } from '../schema.js';
4
+ export interface ProjectUpsert {
5
+ slug: string;
6
+ name: string;
7
+ workspaceId?: string | null;
8
+ description?: string | null;
9
+ rootPath?: string | null;
10
+ config: Record<string, unknown>;
11
+ layers: string[];
12
+ browsers: string[];
13
+ tagsPolicy?: Record<string, unknown>;
14
+ screenshotPolicy?: Record<string, unknown>;
15
+ }
16
+ export declare function configHash(config: unknown): string;
17
+ /** Insert or update a project by slug; returns its id. */
18
+ export declare function upsertProject(db: Kysely<Database>, driver: Driver, p: ProjectUpsert): Promise<string>;
19
+ /** Ensure a minimal project row exists (used by ingest when the registry is unavailable). */
20
+ export declare function ensureProject(db: Kysely<Database>, driver: Driver, slug: string, name?: string): Promise<string>;
21
+ export declare function getProjectBySlug(db: Kysely<Database>, slug: string): Promise<{
22
+ id: string;
23
+ workspaceId: string | null;
24
+ slug: string;
25
+ name: string;
26
+ description: string | null;
27
+ rootPath: string | null;
28
+ config: Record<string, unknown>;
29
+ configHash: string | null;
30
+ layers: string[];
31
+ browsers: string[];
32
+ archived: boolean;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ } | null>;
36
+ export declare function listProjects(db: Kysely<Database>): Promise<{
37
+ id: string;
38
+ workspaceId: string | null;
39
+ slug: string;
40
+ name: string;
41
+ description: string | null;
42
+ rootPath: string | null;
43
+ config: Record<string, unknown>;
44
+ configHash: string | null;
45
+ layers: string[];
46
+ browsers: string[];
47
+ archived: boolean;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ }[]>;
51
+ export declare function mapProject(row: any): {
52
+ id: string;
53
+ workspaceId: string | null;
54
+ slug: string;
55
+ name: string;
56
+ description: string | null;
57
+ rootPath: string | null;
58
+ config: Record<string, unknown>;
59
+ configHash: string | null;
60
+ layers: string[];
61
+ browsers: string[];
62
+ archived: boolean;
63
+ createdAt: string;
64
+ updatedAt: string;
65
+ };
66
+ export declare function upsertEnvironment(db: Kysely<Database>, driver: Driver, input: {
67
+ projectId: string;
68
+ name: string;
69
+ baseUrl?: string | null;
70
+ apiBaseUrl?: string | null;
71
+ config?: Record<string, unknown>;
72
+ secretKeys?: string[];
73
+ isDefault?: boolean;
74
+ }): Promise<string>;
75
+ export declare function getEnvironment(db: Kysely<Database>, projectId: string, name: string): Promise<{
76
+ id: string;
77
+ name: string;
78
+ config_json: Record<string, unknown>;
79
+ created_at: string;
80
+ updated_at: string;
81
+ project_id: string;
82
+ base_url: string | null;
83
+ api_base_url: string | null;
84
+ secret_keys_json: string[];
85
+ is_default: number | boolean;
86
+ } | undefined>;
87
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1,118 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { enc, nowIso, readBool, readJson } from '../col.js';
3
+ import { newId } from '../ids.js';
4
+ export function configHash(config) {
5
+ return createHash('sha256').update(JSON.stringify(config)).digest('hex').slice(0, 16);
6
+ }
7
+ /** Insert or update a project by slug; returns its id. */
8
+ export async function upsertProject(db, driver, p) {
9
+ const existing = await db
10
+ .selectFrom('projects')
11
+ .select(['id'])
12
+ .where('slug', '=', p.slug)
13
+ .executeTakeFirst();
14
+ const values = {
15
+ name: p.name,
16
+ workspace_id: p.workspaceId ?? null,
17
+ description: p.description ?? null,
18
+ root_path: p.rootPath ?? null,
19
+ config_json: enc.json(p.config),
20
+ config_hash: configHash(p.config),
21
+ layers_json: enc.json(p.layers),
22
+ browsers_json: enc.json(p.browsers),
23
+ tags_policy_json: enc.json(p.tagsPolicy ?? {}),
24
+ screenshot_policy_json: enc.json(p.screenshotPolicy ?? {}),
25
+ updated_at: nowIso(),
26
+ };
27
+ if (existing) {
28
+ await db.updateTable('projects').set(values).where('id', '=', existing.id).execute();
29
+ return existing.id;
30
+ }
31
+ const id = newId();
32
+ await db
33
+ .insertInto('projects')
34
+ .values({
35
+ id,
36
+ slug: p.slug,
37
+ archived: enc.bool(driver, false),
38
+ created_at: nowIso(),
39
+ ...values,
40
+ })
41
+ .execute();
42
+ return id;
43
+ }
44
+ /** Ensure a minimal project row exists (used by ingest when the registry is unavailable). */
45
+ export async function ensureProject(db, driver, slug, name = slug) {
46
+ const existing = await db
47
+ .selectFrom('projects')
48
+ .select(['id'])
49
+ .where('slug', '=', slug)
50
+ .executeTakeFirst();
51
+ if (existing)
52
+ return existing.id;
53
+ return upsertProject(db, driver, { slug, name, config: {}, layers: [], browsers: [] });
54
+ }
55
+ export async function getProjectBySlug(db, slug) {
56
+ const row = await db
57
+ .selectFrom('projects')
58
+ .selectAll()
59
+ .where('slug', '=', slug)
60
+ .executeTakeFirst();
61
+ return row ? mapProject(row) : null;
62
+ }
63
+ export async function listProjects(db) {
64
+ const rows = await db.selectFrom('projects').selectAll().orderBy('slug').execute();
65
+ return rows.map(mapProject);
66
+ }
67
+ export function mapProject(row) {
68
+ return {
69
+ id: row.id,
70
+ workspaceId: (row.workspace_id ?? null),
71
+ slug: row.slug,
72
+ name: row.name,
73
+ description: row.description,
74
+ rootPath: row.root_path,
75
+ config: readJson(row.config_json) ?? {},
76
+ configHash: row.config_hash,
77
+ layers: readJson(row.layers_json) ?? [],
78
+ browsers: readJson(row.browsers_json) ?? [],
79
+ archived: readBool(row.archived),
80
+ createdAt: String(row.created_at),
81
+ updatedAt: String(row.updated_at),
82
+ };
83
+ }
84
+ export async function upsertEnvironment(db, driver, input) {
85
+ const existing = await db
86
+ .selectFrom('environments')
87
+ .select(['id'])
88
+ .where('project_id', '=', input.projectId)
89
+ .where('name', '=', input.name)
90
+ .executeTakeFirst();
91
+ const values = {
92
+ base_url: input.baseUrl ?? null,
93
+ api_base_url: input.apiBaseUrl ?? null,
94
+ config_json: enc.json(input.config ?? {}),
95
+ secret_keys_json: enc.json(input.secretKeys ?? []),
96
+ is_default: enc.bool(driver, input.isDefault ?? false),
97
+ updated_at: nowIso(),
98
+ };
99
+ if (existing) {
100
+ await db.updateTable('environments').set(values).where('id', '=', existing.id).execute();
101
+ return existing.id;
102
+ }
103
+ const id = newId();
104
+ await db
105
+ .insertInto('environments')
106
+ .values({ id, project_id: input.projectId, name: input.name, created_at: nowIso(), ...values })
107
+ .execute();
108
+ return id;
109
+ }
110
+ export async function getEnvironment(db, projectId, name) {
111
+ return db
112
+ .selectFrom('environments')
113
+ .selectAll()
114
+ .where('project_id', '=', projectId)
115
+ .where('name', '=', name)
116
+ .executeTakeFirst();
117
+ }
118
+ //# sourceMappingURL=projects.js.map