@selvajs/supabase-provider 0.14.4-beta.0 → 0.15.0-beta.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 (158) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  4. package/dist/auth/SupabaseAuthProvider.js +7 -14
  5. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  6. package/dist/data/SupabaseComputeServerStore.d.ts +14 -3
  7. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  8. package/dist/data/SupabaseComputeServerStore.js +67 -16
  9. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  10. package/dist/data/SupabaseDataProvider.d.ts +36 -8
  11. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  12. package/dist/data/SupabaseDataProvider.js +119 -15
  13. package/dist/data/SupabaseDataProvider.js.map +1 -1
  14. package/dist/data/SupabaseDefinitionStore.d.ts +1 -0
  15. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  16. package/dist/data/SupabaseDefinitionStore.js +41 -4
  17. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  18. package/dist/data/SupabaseEventSink.d.ts +5 -2
  19. package/dist/data/SupabaseEventSink.d.ts.map +1 -1
  20. package/dist/data/SupabaseEventSink.js +20 -3
  21. package/dist/data/SupabaseEventSink.js.map +1 -1
  22. package/dist/data/SupabaseSolveMetricSink.d.ts +49 -4
  23. package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
  24. package/dist/data/SupabaseSolveMetricSink.js +109 -19
  25. package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.d.ts.map +1 -1
  28. package/dist/data/index.js +1 -0
  29. package/dist/data/index.js.map +1 -1
  30. package/dist/data/migrationHead.d.ts +14 -0
  31. package/dist/data/migrationHead.d.ts.map +1 -0
  32. package/dist/data/migrationHead.js +14 -0
  33. package/dist/data/migrationHead.js.map +1 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
  39. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  40. package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
  41. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  42. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  43. package/dist/storage/SupabaseStorageProvider.js +3 -0
  44. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  45. package/package.json +23 -14
  46. package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
  47. package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
  48. package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
  49. package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
  50. package/supabase/migrations/20260716120000_definition_status_and_audit_version.sql +36 -0
  51. package/supabase/migrations/20260716130000_erase_user_audit_email.sql +36 -0
  52. package/supabase/migrations/20260717120000_compute_server_has_api_key.sql +20 -0
  53. package/dist/auth/__tests__/conformance.test.d.ts +0 -2
  54. package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
  55. package/dist/auth/__tests__/conformance.test.js +0 -76
  56. package/dist/auth/__tests__/conformance.test.js.map +0 -1
  57. package/dist/auth/__tests__/token-verification.test.d.ts +0 -13
  58. package/dist/auth/__tests__/token-verification.test.d.ts.map +0 -1
  59. package/dist/auth/__tests__/token-verification.test.js +0 -135
  60. package/dist/auth/__tests__/token-verification.test.js.map +0 -1
  61. package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
  62. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
  63. package/dist/data/__tests__/compute-server-conformance.test.js +0 -45
  64. package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
  65. package/dist/data/__tests__/compute-server-encryption.test.d.ts +0 -12
  66. package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +0 -1
  67. package/dist/data/__tests__/compute-server-encryption.test.js +0 -143
  68. package/dist/data/__tests__/compute-server-encryption.test.js.map +0 -1
  69. package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
  70. package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
  71. package/dist/data/__tests__/definition-conformance.test.js +0 -85
  72. package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
  73. package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
  74. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
  75. package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
  76. package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
  77. package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
  78. package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
  79. package/dist/data/__tests__/invite-conformance.test.js +0 -45
  80. package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
  81. package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
  82. package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
  83. package/dist/data/__tests__/org-conformance.test.js +0 -29
  84. package/dist/data/__tests__/org-conformance.test.js.map +0 -1
  85. package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
  86. package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
  87. package/dist/data/__tests__/project-conformance.test.js +0 -57
  88. package/dist/data/__tests__/project-conformance.test.js.map +0 -1
  89. package/dist/data/__tests__/share-link-concurrency.test.d.ts +0 -22
  90. package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +0 -1
  91. package/dist/data/__tests__/share-link-concurrency.test.js +0 -115
  92. package/dist/data/__tests__/share-link-concurrency.test.js.map +0 -1
  93. package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
  94. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
  95. package/dist/data/__tests__/share-link-conformance.test.js +0 -88
  96. package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
  97. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
  98. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
  99. package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
  100. package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
  101. package/dist/data/__tests__/test-helpers.d.ts +0 -52
  102. package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
  103. package/dist/data/__tests__/test-helpers.js +0 -177
  104. package/dist/data/__tests__/test-helpers.js.map +0 -1
  105. package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
  106. package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
  107. package/dist/permissions/__tests__/conformance.test.js +0 -27
  108. package/dist/permissions/__tests__/conformance.test.js.map +0 -1
  109. package/dist/storage/__tests__/conformance.test.d.ts +0 -2
  110. package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
  111. package/dist/storage/__tests__/conformance.test.js +0 -149
  112. package/dist/storage/__tests__/conformance.test.js.map +0 -1
  113. package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
  114. package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
  115. package/dist/userProfile/__tests__/conformance.test.js +0 -38
  116. package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
  117. package/src/auth/SupabaseAuthProvider.ts +0 -600
  118. package/src/auth/__tests__/conformance.test.ts +0 -78
  119. package/src/auth/__tests__/token-verification.test.ts +0 -158
  120. package/src/auth/index.ts +0 -2
  121. package/src/cli/sync-migrations.ts +0 -133
  122. package/src/data/SupabaseAuditQuery.ts +0 -103
  123. package/src/data/SupabaseComputeServerStore.ts +0 -382
  124. package/src/data/SupabaseDataProvider.ts +0 -153
  125. package/src/data/SupabaseDefinitionStore.ts +0 -527
  126. package/src/data/SupabaseEventSink.ts +0 -30
  127. package/src/data/SupabaseInviteStore.ts +0 -171
  128. package/src/data/SupabaseOrgStore.ts +0 -508
  129. package/src/data/SupabaseProjectStore.ts +0 -431
  130. package/src/data/SupabaseShareLinkStore.ts +0 -176
  131. package/src/data/SupabaseSolveMetricSink.ts +0 -38
  132. package/src/data/__tests__/compute-server-conformance.test.ts +0 -46
  133. package/src/data/__tests__/compute-server-encryption.test.ts +0 -173
  134. package/src/data/__tests__/definition-conformance.test.ts +0 -89
  135. package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
  136. package/src/data/__tests__/invite-conformance.test.ts +0 -46
  137. package/src/data/__tests__/org-conformance.test.ts +0 -30
  138. package/src/data/__tests__/project-conformance.test.ts +0 -57
  139. package/src/data/__tests__/share-link-concurrency.test.ts +0 -124
  140. package/src/data/__tests__/share-link-conformance.test.ts +0 -87
  141. package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
  142. package/src/data/__tests__/test-helpers.ts +0 -188
  143. package/src/data/client.ts +0 -173
  144. package/src/data/errors.ts +0 -45
  145. package/src/data/index.ts +0 -29
  146. package/src/data/pagination.ts +0 -68
  147. package/src/data/query.ts +0 -22
  148. package/src/data/rowStamp.ts +0 -48
  149. package/src/index.ts +0 -42
  150. package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
  151. package/src/permissions/__tests__/conformance.test.ts +0 -28
  152. package/src/permissions/index.ts +0 -1
  153. package/src/storage/SupabaseStorageProvider.ts +0 -182
  154. package/src/storage/__tests__/conformance.test.ts +0 -152
  155. package/src/storage/index.ts +0 -2
  156. package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
  157. package/src/userProfile/__tests__/conformance.test.ts +0 -39
  158. package/src/userProfile/index.ts +0 -1
@@ -1,527 +0,0 @@
1
- import type {
2
- IDefinitionStore,
3
- IEventSink,
4
- DefinitionRecord,
5
- DefinitionRecordPatch,
6
- DefinitionStatus,
7
- DefinitionFileExt,
8
- DefinitionVersion,
9
- RequestContext,
10
- DefinitionListOptions,
11
- ListOptions,
12
- Page,
13
- UISchema
14
- } from '@selvajs/platform';
15
- import { ProviderError, actorFrom, NoopEventSink } from '@selvajs/platform';
16
- import type { ClientBundle } from './client.js';
17
- import { mapPostgrestError } from './errors.js';
18
- import { nextCursorFromRange, toRange } from './pagination.js';
19
- import { stampSoftDelete, stampUpdate } from './rowStamp.js';
20
-
21
- /** Explicit column list for `definitions` — every field `rowToRecord` consumes. */
22
- const DEFINITION_COLUMNS =
23
- 'guid, project_id, owner_id, created_by, updated_by, compute_server_id, display_name, description, category, tags, cover_image, status, run_count, live_version_id, draft_version_id, created_at, updated_at, deleted_at';
24
- /** Explicit column list for `definition_versions` — every field `rowToVersion` consumes. */
25
- const DEFINITION_VERSION_COLUMNS =
26
- 'id, definition_guid, version_number, file_ext, file_key, original_filename, uploaded_by, uploaded_at, change_note, schema, schema_extracted_at';
27
-
28
- /**
29
- * Definition metadata + version store backed by Postgres. Spec §6 versioning:
30
- * `definitions` carries the parent record and the `live`/`draft` channel
31
- * pointers; `definition_versions` carries the immutable per-upload rows.
32
- *
33
- * Deletion protection (§6) is enforced by FK constraints — `live_version_id`
34
- * and `draft_version_id` are ON DELETE RESTRICT. Trying to delete a
35
- * referenced version raises 23503 which `mapPostgrestError` turns into a 409.
36
- *
37
- * `incrementSolveCount` uses a SQL function for atomic UPDATE — no
38
- * read-modify-write race like the local provider has.
39
- */
40
- export class SupabaseDefinitionStore implements IDefinitionStore {
41
- private readonly events: IEventSink;
42
-
43
- constructor(
44
- private readonly clients: ClientBundle,
45
- events: IEventSink = new NoopEventSink()
46
- ) {
47
- this.events = events;
48
- }
49
-
50
- // ============================================================================
51
- // Definitions
52
- // ============================================================================
53
-
54
- async list(ctx: RequestContext, opts?: DefinitionListOptions): Promise<Page<DefinitionRecord>> {
55
- return this.runList(ctx, undefined, opts);
56
- }
57
-
58
- async listByProject(
59
- ctx: RequestContext,
60
- projectId: string,
61
- opts?: DefinitionListOptions
62
- ): Promise<Page<DefinitionRecord>> {
63
- return this.runList(ctx, { projectId }, opts);
64
- }
65
-
66
- async listPublic(
67
- ctx: RequestContext,
68
- opts?: DefinitionListOptions & { orgId?: string }
69
- ): Promise<Page<DefinitionRecord>> {
70
- return this.runList(ctx, { publicOnly: true, orgId: opts?.orgId }, opts);
71
- }
72
-
73
- private async runList(
74
- ctx: RequestContext,
75
- filter: { projectId?: string; publicOnly?: boolean; orgId?: string } | undefined,
76
- opts?: DefinitionListOptions
77
- ): Promise<Page<DefinitionRecord>> {
78
- const range = toRange(opts);
79
- const direction = opts?.orderDir ?? 'desc';
80
-
81
- // The public path embeds `project` to filter on its visibility/org; the
82
- // non-public path selects the definition columns alone. Pick the select
83
- // string up front so there's a single `.select()` call and the builder
84
- // keeps one type across both branches.
85
- const selectColumns = filter?.publicOnly
86
- ? `${DEFINITION_COLUMNS}, project:projects!inner(visibility, org_id)`
87
- : DEFINITION_COLUMNS;
88
-
89
- let query = this.clients
90
- .forRequest(ctx)
91
- .from('definitions')
92
- .select(selectColumns, { count: 'exact' })
93
- .is('deleted_at', null);
94
-
95
- if (filter?.publicOnly) {
96
- query = query.eq('project.visibility', 'public');
97
- if (filter.orgId) query = query.eq('project.org_id', filter.orgId);
98
- }
99
-
100
- if (filter?.projectId) query = query.eq('project_id', filter.projectId);
101
-
102
- if (opts?.statuses?.length) {
103
- query = query.in('status', opts.statuses);
104
- } else {
105
- const excluded: string[] = [];
106
- if (!opts?.includePending) excluded.push('pending');
107
- if (!opts?.includeArchived) excluded.push('archived');
108
- if (excluded.length) {
109
- query = query.not('status', 'in', `(${excluded.join(',')})`);
110
- }
111
- }
112
-
113
- query = query
114
- .order(definitionOrderColumn(opts?.orderBy), { ascending: direction === 'asc' })
115
- .range(range.from, range.to);
116
-
117
- const { data, error, count } = await query;
118
- if (error) throw mapPostgrestError(error);
119
- // `data` is typed against the dynamic select string, which the PostgREST
120
- // parser can't resolve to a row shape; the definition columns are all
121
- // present regardless, so map through the known `DefinitionRow`.
122
- const items = ((data ?? []) as unknown as DefinitionRow[]).map(rowToRecord);
123
- return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
124
- }
125
-
126
- async get(ctx: RequestContext, guid: string): Promise<DefinitionRecord | null> {
127
- const { data, error } = await this.clients
128
- .forRequest(ctx)
129
- .from('definitions')
130
- .select(DEFINITION_COLUMNS)
131
- .eq('guid', guid)
132
- .is('deleted_at', null)
133
- .maybeSingle();
134
- if (error) throw mapPostgrestError(error);
135
- return data ? rowToRecord(data) : null;
136
- }
137
-
138
- async create(ctx: RequestContext, record: DefinitionRecord): Promise<void> {
139
- const { error } = await this.clients
140
- .forRequest(ctx)
141
- .from('definitions')
142
- .insert(recordToRow(record));
143
- if (error) throw mapPostgrestError(error);
144
- await this.events.emit({
145
- type: 'definition.created',
146
- definitionId: record.guid,
147
- projectId: record.projectId,
148
- actorId: actorFrom(ctx)
149
- });
150
- }
151
-
152
- async update(ctx: RequestContext, guid: string, patch: DefinitionRecordPatch): Promise<void> {
153
- const row = patchToRow(patch);
154
- if (Object.keys(row).length === 0) return;
155
- Object.assign(row, stampUpdate(ctx));
156
-
157
- const { data, error } = await this.clients
158
- .forRequest(ctx)
159
- .from('definitions')
160
- .update(row)
161
- .eq('guid', guid)
162
- .is('deleted_at', null)
163
- .select('guid');
164
- if (error) throw mapPostgrestError(error);
165
- if (!data || data.length === 0) throw new ProviderError(`Definition '${guid}' not found`, 404);
166
- }
167
-
168
- async delete(ctx: RequestContext, guid: string): Promise<void> {
169
- // Soft-delete (matches the local provider and spec §9). A retention
170
- // sweep running as service-role hard-deletes later, which cascades
171
- // versions + share_links via FK.
172
- const { error } = await this.clients
173
- .forRequest(ctx)
174
- .from('definitions')
175
- .update(stampSoftDelete(ctx))
176
- .eq('guid', guid)
177
- .is('deleted_at', null);
178
- if (error) throw mapPostgrestError(error);
179
- await this.events.emit({
180
- type: 'definition.deleted',
181
- definitionId: guid,
182
- actorId: actorFrom(ctx)
183
- });
184
- }
185
-
186
- async deleteByProject(ctx: RequestContext, projectId: string): Promise<void> {
187
- // Cascade from `deleteProject` — soft-delete every live definition in the
188
- // project in one statement. `select('guid')` returns the affected rows so
189
- // we can emit a `definition.deleted` per record (parity with the local
190
- // provider and the single-`delete` path above).
191
- const { data, error } = await this.clients
192
- .forRequest(ctx)
193
- .from('definitions')
194
- .update(stampSoftDelete(ctx))
195
- .eq('project_id', projectId)
196
- .is('deleted_at', null)
197
- .select('guid');
198
- if (error) throw mapPostgrestError(error);
199
- for (const row of data ?? []) {
200
- await this.events.emit({
201
- type: 'definition.deleted',
202
- definitionId: row.guid,
203
- actorId: actorFrom(ctx)
204
- });
205
- }
206
- }
207
-
208
- async incrementSolveCount(ctx: RequestContext, guid: string): Promise<void> {
209
- const { error } = await this.clients.forRequest(ctx).rpc('increment_run_count', { g: guid });
210
- if (error) throw mapPostgrestError(error);
211
- }
212
-
213
- // ============================================================================
214
- // Versions (spec §6)
215
- // ============================================================================
216
-
217
- async createVersion(ctx: RequestContext, version: DefinitionVersion): Promise<void> {
218
- const { error } = await this.clients
219
- .forRequest(ctx)
220
- .from('definition_versions')
221
- .insert(versionToRow(version));
222
- if (error) throw mapPostgrestError(error);
223
- await this.events.emit({
224
- type: 'definition_version.created',
225
- versionId: version.id,
226
- definitionId: version.definitionId,
227
- actorId: actorFrom(ctx)
228
- });
229
- }
230
-
231
- async listVersions(
232
- ctx: RequestContext,
233
- definitionId: string,
234
- opts?: ListOptions
235
- ): Promise<Page<DefinitionVersion>> {
236
- const range = toRange(opts);
237
- const { data, error, count } = await this.clients
238
- .forRequest(ctx)
239
- .from('definition_versions')
240
- .select(DEFINITION_VERSION_COLUMNS, { count: 'exact' })
241
- .eq('definition_guid', definitionId)
242
- .order('version_number', { ascending: false })
243
- .range(range.from, range.to);
244
- if (error) throw mapPostgrestError(error);
245
- const items = (data ?? []).map(rowToVersion);
246
- return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
247
- }
248
-
249
- async getVersion(ctx: RequestContext, versionId: string): Promise<DefinitionVersion | null> {
250
- const { data, error } = await this.clients
251
- .forRequest(ctx)
252
- .from('definition_versions')
253
- .select(DEFINITION_VERSION_COLUMNS)
254
- .eq('id', versionId)
255
- .maybeSingle();
256
- if (error) throw mapPostgrestError(error);
257
- return data ? rowToVersion(data) : null;
258
- }
259
-
260
- async setVersionSchema(ctx: RequestContext, versionId: string, schema: UISchema): Promise<void> {
261
- const { error } = await this.clients
262
- .forRequest(ctx)
263
- .from('definition_versions')
264
- .update({ schema, schema_extracted_at: new Date().toISOString() })
265
- .eq('id', versionId);
266
- if (error) throw mapPostgrestError(error);
267
- }
268
-
269
- async deleteVersion(ctx: RequestContext, versionId: string): Promise<void> {
270
- // FK enforcement at the DB layer: live_version_id / draft_version_id
271
- // are ON DELETE RESTRICT. If the version is referenced by either
272
- // channel, Postgres raises 23503 → mapPostgrestError → ProviderError(409).
273
- const { error } = await this.clients
274
- .forRequest(ctx)
275
- .from('definition_versions')
276
- .delete()
277
- .eq('id', versionId);
278
- if (error) throw mapPostgrestError(error);
279
- await this.events.emit({
280
- type: 'definition_version.deleted',
281
- versionId,
282
- actorId: actorFrom(ctx)
283
- });
284
- }
285
-
286
- async setLiveVersion(
287
- ctx: RequestContext,
288
- definitionId: string,
289
- versionId: string
290
- ): Promise<void> {
291
- await this.repointChannel(ctx, definitionId, versionId, 'live_version_id');
292
- // Only `live` advancement is the published-event trigger. Draft
293
- // repointing is the editor's working pointer, not a publication signal.
294
- await this.events.emit({
295
- type: 'definition.published',
296
- definitionId,
297
- versionId,
298
- actorId: actorFrom(ctx)
299
- });
300
- }
301
-
302
- async setDraftVersion(
303
- ctx: RequestContext,
304
- definitionId: string,
305
- versionId: string
306
- ): Promise<void> {
307
- await this.repointChannel(ctx, definitionId, versionId, 'draft_version_id');
308
- }
309
-
310
- async attachInitialVersion(
311
- ctx: RequestContext,
312
- definitionId: string,
313
- versionId: string
314
- ): Promise<void> {
315
- const client = this.clients.forRequest(ctx);
316
- // Validate the version belongs to this definition before repointing —
317
- // matches `repointChannel` so the bootstrap surface enforces the same
318
- // invariant.
319
- const { data: version, error: vError } = await client
320
- .from('definition_versions')
321
- .select('id, definition_guid')
322
- .eq('id', versionId)
323
- .maybeSingle();
324
- if (vError) throw mapPostgrestError(vError);
325
- if (!version || version.definition_guid !== definitionId) {
326
- throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
327
- }
328
-
329
- // Single UPDATE — both channel pointers and status flip in one row
330
- // write, so a mid-flight failure can't leave a half-promoted record.
331
- const row: Record<string, unknown> = {
332
- live_version_id: versionId,
333
- draft_version_id: versionId,
334
- status: 'draft',
335
- ...stampUpdate(ctx)
336
- };
337
-
338
- const { data, error } = await client
339
- .from('definitions')
340
- .update(row)
341
- .eq('guid', definitionId)
342
- .is('deleted_at', null)
343
- .select('guid');
344
- if (error) throw mapPostgrestError(error);
345
- if (!data || data.length === 0) {
346
- throw new ProviderError(`Definition '${definitionId}' not found`, 404);
347
- }
348
- // No `definition.published` event — see interface doc.
349
- }
350
-
351
- private async repointChannel(
352
- ctx: RequestContext,
353
- definitionId: string,
354
- versionId: string,
355
- column: 'live_version_id' | 'draft_version_id'
356
- ): Promise<void> {
357
- const client = this.clients.forRequest(ctx);
358
- // Validate the version belongs to this definition before repointing.
359
- const { data: version, error: vError } = await client
360
- .from('definition_versions')
361
- .select('id, definition_guid')
362
- .eq('id', versionId)
363
- .maybeSingle();
364
- if (vError) throw mapPostgrestError(vError);
365
- if (!version || version.definition_guid !== definitionId) {
366
- throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
367
- }
368
-
369
- const row: Record<string, unknown> = { [column]: versionId, ...stampUpdate(ctx) };
370
- const { data, error } = await client
371
- .from('definitions')
372
- .update(row)
373
- .eq('guid', definitionId)
374
- .is('deleted_at', null)
375
- .select('guid');
376
- if (error) throw mapPostgrestError(error);
377
- if (!data || data.length === 0) {
378
- throw new ProviderError(`Definition '${definitionId}' not found`, 404);
379
- }
380
- }
381
- }
382
-
383
- // ============================================================================
384
- // Row ↔ domain mappers
385
- // ============================================================================
386
- interface DefinitionRow {
387
- guid: string;
388
- project_id: string;
389
- owner_id: string;
390
- created_by?: string | null;
391
- updated_by?: string | null;
392
- compute_server_id: string | null;
393
- display_name: string;
394
- description: string | null;
395
- category: string | null;
396
- tags: string[] | null;
397
- cover_image: string | null;
398
- status: DefinitionStatus;
399
- run_count: number | string;
400
- live_version_id: string | null;
401
- draft_version_id: string | null;
402
- created_at: string;
403
- updated_at: string;
404
- deleted_at?: string | null;
405
- }
406
-
407
- interface DefinitionVersionRow {
408
- id: string;
409
- definition_guid: string;
410
- version_number: number;
411
- file_ext: DefinitionFileExt;
412
- file_key: string;
413
- original_filename: string | null;
414
- uploaded_by: string;
415
- uploaded_at: string;
416
- change_note?: string | null;
417
- schema?: UISchema | null;
418
- schema_extracted_at?: string | null;
419
- }
420
-
421
- function rowToRecord(row: DefinitionRow): DefinitionRecord {
422
- return {
423
- guid: row.guid,
424
- projectId: row.project_id,
425
- ownerId: row.owner_id,
426
- createdBy: row.created_by ?? row.owner_id,
427
- updatedBy: row.updated_by ?? row.owner_id,
428
- liveVersionId: row.live_version_id,
429
- draftVersionId: row.draft_version_id,
430
- computeServerId: row.compute_server_id ?? undefined,
431
- displayName: row.display_name,
432
- description: row.description ?? undefined,
433
- category: row.category ?? undefined,
434
- tags: row.tags ?? undefined,
435
- coverImage: row.cover_image ?? undefined,
436
- status: row.status,
437
- solveCount: typeof row.run_count === 'string' ? Number(row.run_count) : row.run_count,
438
- createdAt: row.created_at,
439
- updatedAt: row.updated_at,
440
- deletedAt: row.deleted_at ?? null
441
- };
442
- }
443
-
444
- function recordToRow(r: DefinitionRecord): Record<string, unknown> {
445
- const row: Record<string, unknown> = {
446
- guid: r.guid,
447
- project_id: r.projectId,
448
- owner_id: r.ownerId,
449
- created_by: r.createdBy,
450
- updated_by: r.updatedBy,
451
- live_version_id: r.liveVersionId,
452
- draft_version_id: r.draftVersionId,
453
- compute_server_id: r.computeServerId ?? null,
454
- display_name: r.displayName,
455
- description: r.description ?? null,
456
- category: r.category ?? null,
457
- cover_image: r.coverImage ?? null,
458
- status: r.status,
459
- run_count: r.solveCount,
460
- created_at: r.createdAt,
461
- updated_at: r.updatedAt,
462
- deleted_at: r.deletedAt ?? null
463
- };
464
- if (r.tags !== undefined) row.tags = r.tags;
465
- return row;
466
- }
467
-
468
- function patchToRow(patch: DefinitionRecordPatch): Record<string, unknown> {
469
- const row: Record<string, unknown> = {};
470
- if (patch.displayName !== undefined) row.display_name = patch.displayName;
471
- if (patch.description !== undefined) row.description = patch.description;
472
- if (patch.category !== undefined) row.category = patch.category;
473
- if (patch.tags !== undefined) row.tags = patch.tags;
474
- if (patch.coverImage !== undefined) row.cover_image = patch.coverImage;
475
- if (patch.projectId !== undefined) row.project_id = patch.projectId;
476
- if (patch.computeServerId !== undefined) row.compute_server_id = patch.computeServerId;
477
- if (patch.status !== undefined) row.status = patch.status;
478
- if (patch.ownerId !== undefined) row.owner_id = patch.ownerId;
479
- return row;
480
- }
481
-
482
- function rowToVersion(row: DefinitionVersionRow): DefinitionVersion {
483
- return {
484
- id: row.id,
485
- definitionId: row.definition_guid,
486
- versionNumber: row.version_number,
487
- fileExt: row.file_ext,
488
- fileKey: row.file_key,
489
- originalFilename: row.original_filename ?? undefined,
490
- uploadedBy: row.uploaded_by,
491
- uploadedAt: row.uploaded_at,
492
- changeNote: row.change_note ?? undefined,
493
- schema: row.schema ?? undefined,
494
- schemaExtractedAt: row.schema_extracted_at ?? undefined
495
- };
496
- }
497
-
498
- function versionToRow(v: DefinitionVersion): Record<string, unknown> {
499
- const row: Record<string, unknown> = {
500
- id: v.id,
501
- definition_guid: v.definitionId,
502
- version_number: v.versionNumber,
503
- file_ext: v.fileExt,
504
- file_key: v.fileKey,
505
- original_filename: v.originalFilename ?? null,
506
- uploaded_by: v.uploadedBy,
507
- uploaded_at: v.uploadedAt
508
- };
509
- if (v.changeNote !== undefined) row.change_note = v.changeNote;
510
- if (v.schema !== undefined) row.schema = v.schema;
511
- if (v.schemaExtractedAt !== undefined) row.schema_extracted_at = v.schemaExtractedAt;
512
- return row;
513
- }
514
-
515
- function definitionOrderColumn(orderBy: DefinitionListOptions['orderBy'] | undefined): string {
516
- switch (orderBy) {
517
- case 'name':
518
- return 'display_name';
519
- case 'solveCount':
520
- return 'run_count';
521
- case 'updatedAt':
522
- return 'updated_at';
523
- case 'createdAt':
524
- default:
525
- return 'created_at';
526
- }
527
- }
@@ -1,30 +0,0 @@
1
- import type { DomainEvent, IEventSink } from '@selvajs/platform';
2
- import type { ClientBundle } from './client.js';
3
-
4
- /**
5
- * Persists every domain event to `public.audit_events`. Writes use the
6
- * service-role client so RLS does not gate the audit trail — the sink runs
7
- * as a system process attached to whichever store emitted the event.
8
- *
9
- * Per the `IEventSink` contract, `emit` MUST NOT throw. The user-facing
10
- * mutation has already succeeded by the time we're here; a failed audit
11
- * write is logged and swallowed so the response still completes.
12
- */
13
- export class SupabaseEventSink implements IEventSink {
14
- constructor(private readonly clients: ClientBundle) {}
15
-
16
- async emit(event: DomainEvent): Promise<void> {
17
- try {
18
- const { error } = await this.clients.serviceClient.from('audit_events').insert({
19
- type: event.type,
20
- actor_id: event.actorId,
21
- data: event
22
- });
23
- if (error) {
24
- console.error('[SupabaseEventSink] insert failed:', error.message, { event });
25
- }
26
- } catch (err) {
27
- console.error('[SupabaseEventSink] unexpected error:', err, { event });
28
- }
29
- }
30
- }