@selvajs/supabase-provider 0.14.3 → 0.14.4-beta.1

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 (160) hide show
  1. package/LICENSE +1 -1
  2. package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  4. package/dist/auth/SupabaseAuthProvider.js +123 -18
  5. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  6. package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
  7. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  8. package/dist/data/SupabaseComputeServerStore.js +146 -39
  9. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  10. package/dist/data/SupabaseDataProvider.d.ts +18 -3
  11. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  12. package/dist/data/SupabaseDataProvider.js +83 -18
  13. package/dist/data/SupabaseDataProvider.js.map +1 -1
  14. package/dist/data/SupabaseDefinitionStore.d.ts +3 -1
  15. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  16. package/dist/data/SupabaseDefinitionStore.js +94 -67
  17. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  18. package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
  19. package/dist/data/SupabaseInviteStore.js +11 -22
  20. package/dist/data/SupabaseInviteStore.js.map +1 -1
  21. package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
  22. package/dist/data/SupabaseOrgStore.js +49 -86
  23. package/dist/data/SupabaseOrgStore.js.map +1 -1
  24. package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
  25. package/dist/data/SupabaseProjectStore.js +51 -70
  26. package/dist/data/SupabaseProjectStore.js.map +1 -1
  27. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
  28. package/dist/data/SupabaseShareLinkStore.js +13 -27
  29. package/dist/data/SupabaseShareLinkStore.js.map +1 -1
  30. package/dist/data/client.d.ts +35 -3
  31. package/dist/data/client.d.ts.map +1 -1
  32. package/dist/data/client.js +67 -16
  33. package/dist/data/client.js.map +1 -1
  34. package/dist/data/errors.d.ts +2 -0
  35. package/dist/data/errors.d.ts.map +1 -0
  36. package/dist/data/errors.js +23 -0
  37. package/dist/data/errors.js.map +1 -0
  38. package/dist/data/index.d.ts +8 -2
  39. package/dist/data/index.d.ts.map +1 -1
  40. package/dist/data/index.js +6 -1
  41. package/dist/data/index.js.map +1 -1
  42. package/dist/data/migrationHead.d.ts +14 -0
  43. package/dist/data/migrationHead.d.ts.map +1 -0
  44. package/dist/data/migrationHead.js +14 -0
  45. package/dist/data/migrationHead.js.map +1 -0
  46. package/dist/data/query.d.ts +20 -0
  47. package/dist/data/query.d.ts.map +1 -0
  48. package/dist/data/query.js +13 -0
  49. package/dist/data/query.js.map +1 -0
  50. package/dist/data/rowStamp.d.ts +38 -0
  51. package/dist/data/rowStamp.d.ts.map +1 -0
  52. package/dist/data/rowStamp.js +43 -0
  53. package/dist/data/rowStamp.js.map +1 -0
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
  59. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  60. package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
  61. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  62. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  63. package/dist/storage/SupabaseStorageProvider.js +3 -0
  64. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  65. package/package.json +13 -14
  66. package/supabase/.branches/_current_branch +1 -0
  67. package/supabase/.temp/cli-latest +1 -0
  68. package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
  69. package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
  70. package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
  71. package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
  72. package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
  73. package/dist/auth/__tests__/conformance.test.d.ts +0 -2
  74. package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
  75. package/dist/auth/__tests__/conformance.test.js +0 -76
  76. package/dist/auth/__tests__/conformance.test.js.map +0 -1
  77. package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
  78. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
  79. package/dist/data/__tests__/compute-server-conformance.test.js +0 -40
  80. package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
  81. package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
  82. package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
  83. package/dist/data/__tests__/definition-conformance.test.js +0 -85
  84. package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
  85. package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
  86. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
  87. package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
  88. package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
  89. package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
  90. package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
  91. package/dist/data/__tests__/invite-conformance.test.js +0 -45
  92. package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
  93. package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
  94. package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
  95. package/dist/data/__tests__/org-conformance.test.js +0 -29
  96. package/dist/data/__tests__/org-conformance.test.js.map +0 -1
  97. package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
  98. package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
  99. package/dist/data/__tests__/project-conformance.test.js +0 -57
  100. package/dist/data/__tests__/project-conformance.test.js.map +0 -1
  101. package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
  102. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
  103. package/dist/data/__tests__/share-link-conformance.test.js +0 -88
  104. package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
  105. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
  106. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
  107. package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
  108. package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
  109. package/dist/data/__tests__/test-helpers.d.ts +0 -52
  110. package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
  111. package/dist/data/__tests__/test-helpers.js +0 -177
  112. package/dist/data/__tests__/test-helpers.js.map +0 -1
  113. package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
  114. package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
  115. package/dist/permissions/__tests__/conformance.test.js +0 -27
  116. package/dist/permissions/__tests__/conformance.test.js.map +0 -1
  117. package/dist/storage/__tests__/conformance.test.d.ts +0 -2
  118. package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
  119. package/dist/storage/__tests__/conformance.test.js +0 -149
  120. package/dist/storage/__tests__/conformance.test.js.map +0 -1
  121. package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
  122. package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
  123. package/dist/userProfile/__tests__/conformance.test.js +0 -38
  124. package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
  125. package/src/auth/SupabaseAuthProvider.ts +0 -464
  126. package/src/auth/__tests__/conformance.test.ts +0 -78
  127. package/src/auth/index.ts +0 -2
  128. package/src/cli/sync-migrations.ts +0 -133
  129. package/src/data/SupabaseAuditQuery.ts +0 -103
  130. package/src/data/SupabaseComputeServerStore.ts +0 -262
  131. package/src/data/SupabaseDataProvider.ts +0 -133
  132. package/src/data/SupabaseDefinitionStore.ts +0 -520
  133. package/src/data/SupabaseEventSink.ts +0 -30
  134. package/src/data/SupabaseInviteStore.ts +0 -183
  135. package/src/data/SupabaseOrgStore.ts +0 -552
  136. package/src/data/SupabaseProjectStore.ts +0 -454
  137. package/src/data/SupabaseShareLinkStore.ts +0 -191
  138. package/src/data/SupabaseSolveMetricSink.ts +0 -38
  139. package/src/data/__tests__/compute-server-conformance.test.ts +0 -40
  140. package/src/data/__tests__/definition-conformance.test.ts +0 -89
  141. package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
  142. package/src/data/__tests__/invite-conformance.test.ts +0 -46
  143. package/src/data/__tests__/org-conformance.test.ts +0 -30
  144. package/src/data/__tests__/project-conformance.test.ts +0 -57
  145. package/src/data/__tests__/share-link-conformance.test.ts +0 -87
  146. package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
  147. package/src/data/__tests__/test-helpers.ts +0 -188
  148. package/src/data/client.ts +0 -98
  149. package/src/data/index.ts +0 -12
  150. package/src/data/pagination.ts +0 -68
  151. package/src/index.ts +0 -24
  152. package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
  153. package/src/permissions/__tests__/conformance.test.ts +0 -28
  154. package/src/permissions/index.ts +0 -1
  155. package/src/storage/SupabaseStorageProvider.ts +0 -182
  156. package/src/storage/__tests__/conformance.test.ts +0 -152
  157. package/src/storage/index.ts +0 -2
  158. package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
  159. package/src/userProfile/__tests__/conformance.test.ts +0 -39
  160. package/src/userProfile/index.ts +0 -1
@@ -1,133 +0,0 @@
1
- import type { IDataProvider, IEventSink, IPlatformProjectGrantStore } from '@selvajs/platform';
2
- import { ProviderError } from '@selvajs/platform';
3
- import type { ClientBundle, BuildClientOptions } from './client.js';
4
- import { buildClientBundle } from './client.js';
5
- import { SupabaseEventSink } from './SupabaseEventSink.js';
6
- import { SupabaseSolveMetricSink } from './SupabaseSolveMetricSink.js';
7
- import { SupabaseAuditQuery } from './SupabaseAuditQuery.js';
8
- import { SupabaseOrgStore } from './SupabaseOrgStore.js';
9
- import { SupabaseProjectStore } from './SupabaseProjectStore.js';
10
- import { SupabaseDefinitionStore } from './SupabaseDefinitionStore.js';
11
- import { SupabaseInviteStore } from './SupabaseInviteStore.js';
12
- import { SupabaseComputeServerStore } from './SupabaseComputeServerStore.js';
13
- import { SupabaseShareLinkStore } from './SupabaseShareLinkStore.js';
14
- import { SupabaseUserProfileProvider } from '../userProfile/SupabaseUserProfileProvider.js';
15
- import { SupabasePlatformPermissionStore } from '../permissions/SupabasePlatformPermissionStore.js';
16
-
17
- /**
18
- * Stub until the Supabase implementation lands. Reads and mutations surface a
19
- * clear 501; cascade hooks are silent no-ops so deleting an org or project on
20
- * a Supabase deployment without the feature wired does not blow up.
21
- */
22
- const NOT_IMPLEMENTED_MSG = 'Platform projects are not supported on this deployment yet.';
23
- const notImplementedGrantStore: IPlatformProjectGrantStore = {
24
- listByProject: async () => {
25
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
26
- },
27
- create: async () => {
28
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
29
- },
30
- delete: async () => {
31
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
32
- },
33
- deleteByProject: async () => {
34
- // No-op: nothing to clean up on a deployment that never accepted grants.
35
- },
36
- deleteByGranteeOrg: async () => {
37
- // No-op: same reasoning.
38
- }
39
- };
40
-
41
- /**
42
- * Composition of every data store for the Supabase backend. Instantiates one
43
- * `ClientBundle` (service-role + per-request factory) and wires each store
44
- * against it so every store in a single process shares the same pooled clients.
45
- */
46
- export class SupabaseDataProvider implements IDataProvider {
47
- readonly orgs: SupabaseOrgStore;
48
- readonly projects: SupabaseProjectStore;
49
- readonly definitions: SupabaseDefinitionStore;
50
- readonly invites: SupabaseInviteStore;
51
- readonly computeServer: SupabaseComputeServerStore;
52
- readonly shareLinks: SupabaseShareLinkStore;
53
- readonly userProfile: SupabaseUserProfileProvider;
54
- readonly permissions: SupabasePlatformPermissionStore;
55
- readonly platformProjectGrants: IPlatformProjectGrantStore;
56
- readonly auditQuery: SupabaseAuditQuery;
57
- /**
58
- * Per-solve timing sink, built from the same client bundle. Exposed so the
59
- * app's provider wiring can hand it to `SelvaConfig.solveMetrics` without
60
- * constructing a second Supabase client.
61
- */
62
- readonly solveMetrics: SupabaseSolveMetricSink;
63
-
64
- private constructor(
65
- private readonly clients: ClientBundle,
66
- events: IEventSink
67
- ) {
68
- this.solveMetrics = new SupabaseSolveMetricSink(clients);
69
- this.orgs = new SupabaseOrgStore(clients, events);
70
- this.projects = new SupabaseProjectStore(clients, events);
71
- this.definitions = new SupabaseDefinitionStore(clients, events);
72
- this.invites = new SupabaseInviteStore(clients, events);
73
- this.computeServer = new SupabaseComputeServerStore(clients);
74
- this.shareLinks = new SupabaseShareLinkStore(clients, events);
75
- this.userProfile = new SupabaseUserProfileProvider(clients);
76
- this.permissions = new SupabasePlatformPermissionStore(clients);
77
- this.platformProjectGrants = notImplementedGrantStore;
78
- this.auditQuery = new SupabaseAuditQuery(clients);
79
- }
80
-
81
- static fromEnv(
82
- env: Record<string, string | undefined>,
83
- events?: IEventSink
84
- ): SupabaseDataProvider {
85
- const supabaseUrl = env.SUPABASE_URL;
86
- const anonKey = env.SUPABASE_ANON_KEY;
87
- const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY;
88
- if (!supabaseUrl) throw new Error('Missing required env var: SUPABASE_URL');
89
- if (!anonKey) throw new Error('Missing required env var: SUPABASE_ANON_KEY');
90
- if (!serviceRoleKey) throw new Error('Missing required env var: SUPABASE_SERVICE_ROLE_KEY');
91
- const bundle = buildClientBundle({ supabaseUrl, anonKey, serviceRoleKey });
92
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
93
- }
94
-
95
- static create(opts: BuildClientOptions, events?: IEventSink): SupabaseDataProvider {
96
- const bundle = buildClientBundle(opts);
97
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
98
- }
99
-
100
- /**
101
- * Build from a pre-existing `ClientBundle`. Useful for tests or advanced
102
- * cases that need to inject a custom event sink or share a bundle externally.
103
- */
104
- static fromBundle(bundle: ClientBundle, events?: IEventSink): SupabaseDataProvider {
105
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
106
- }
107
-
108
- /**
109
- * Expose the underlying bundle so other providers in the same package
110
- * (storage, user profile, auth) can share one set of clients per process.
111
- */
112
- getClientBundle(): ClientBundle {
113
- return this.clients;
114
- }
115
-
116
- /**
117
- * No-op: `public.user_profiles` is auto-seeded by the `handle_new_auth_user`
118
- * trigger on every `auth.users` insert (see `0001_initial.sql`). The data
119
- * layer always has a row for any authenticated user without per-request work.
120
- */
121
- async ensureUser(): Promise<void> {
122
- // Trigger handles it.
123
- }
124
-
125
- /**
126
- * No-op: `public.user_profiles.user_id` references `auth.users(id)` with
127
- * `on delete cascade`. Deleting the auth user removes the profile row
128
- * automatically.
129
- */
130
- async onUserDeleted(): Promise<void> {
131
- // FK cascade handles it.
132
- }
133
- }
@@ -1,520 +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, auditSoftDelete, actorFrom, NoopEventSink } from '@selvajs/platform';
16
- import type { ClientBundle } from './client.js';
17
- import { nextCursorFromRange, toRange } from './pagination.js';
18
-
19
- /**
20
- * Definition metadata + version store backed by Postgres. Spec §6 versioning:
21
- * `definitions` carries the parent record and the `live`/`draft` channel
22
- * pointers; `definition_versions` carries the immutable per-upload rows.
23
- *
24
- * Deletion protection (§6) is enforced by FK constraints — `live_version_id`
25
- * and `draft_version_id` are ON DELETE RESTRICT. Trying to delete a
26
- * referenced version raises 23503 which `mapError` turns into a 409.
27
- *
28
- * `incrementSolveCount` uses a SQL function for atomic UPDATE — no
29
- * read-modify-write race like the local provider has.
30
- */
31
- export class SupabaseDefinitionStore implements IDefinitionStore {
32
- private readonly events: IEventSink;
33
-
34
- constructor(
35
- private readonly clients: ClientBundle,
36
- events: IEventSink = new NoopEventSink()
37
- ) {
38
- this.events = events;
39
- }
40
-
41
- // ============================================================================
42
- // Definitions
43
- // ============================================================================
44
-
45
- async list(ctx: RequestContext, opts?: DefinitionListOptions): Promise<Page<DefinitionRecord>> {
46
- return this.runList(ctx, undefined, opts);
47
- }
48
-
49
- async listByProject(
50
- ctx: RequestContext,
51
- projectId: string,
52
- opts?: DefinitionListOptions
53
- ): Promise<Page<DefinitionRecord>> {
54
- return this.runList(ctx, { projectId }, opts);
55
- }
56
-
57
- async listPublic(
58
- ctx: RequestContext,
59
- opts?: DefinitionListOptions & { orgId?: string }
60
- ): Promise<Page<DefinitionRecord>> {
61
- return this.runList(ctx, { publicOnly: true, orgId: opts?.orgId }, opts);
62
- }
63
-
64
- private async runList(
65
- ctx: RequestContext,
66
- filter: { projectId?: string; publicOnly?: boolean; orgId?: string } | undefined,
67
- opts?: DefinitionListOptions
68
- ): Promise<Page<DefinitionRecord>> {
69
- const range = toRange(opts);
70
- const direction = opts?.orderDir ?? 'desc';
71
-
72
- let query = this.clients
73
- .forRequest(ctx)
74
- .from('definitions')
75
- .select('*', { count: 'exact' })
76
- .is('deleted_at', null);
77
-
78
- if (filter?.projectId) query = query.eq('project_id', filter.projectId);
79
-
80
- if (filter?.publicOnly) {
81
- query = this.clients
82
- .forRequest(ctx)
83
- .from('definitions')
84
- .select('*, project:projects!inner(visibility, org_id)', {
85
- count: 'exact'
86
- })
87
- .is('deleted_at', null)
88
- .eq('project.visibility', 'public');
89
- if (filter.orgId) query = query.eq('project.org_id', filter.orgId);
90
- if (filter.projectId) query = query.eq('project_id', filter.projectId);
91
- }
92
-
93
- if (opts?.statuses?.length) {
94
- query = query.in('status', opts.statuses);
95
- } else {
96
- const excluded: string[] = [];
97
- if (!opts?.includePending) excluded.push('pending');
98
- if (!opts?.includeArchived) excluded.push('archived');
99
- if (excluded.length) {
100
- query = query.not('status', 'in', `(${excluded.join(',')})`);
101
- }
102
- }
103
-
104
- query = query
105
- .order(definitionOrderColumn(opts?.orderBy), { ascending: direction === 'asc' })
106
- .range(range.from, range.to);
107
-
108
- const { data, error, count } = await query;
109
- if (error) throw mapError(error);
110
- const items = (data ?? []).map(rowToRecord);
111
- return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
112
- }
113
-
114
- async get(ctx: RequestContext, guid: string): Promise<DefinitionRecord | null> {
115
- const { data, error } = await this.clients
116
- .forRequest(ctx)
117
- .from('definitions')
118
- .select('*')
119
- .eq('guid', guid)
120
- .is('deleted_at', null)
121
- .maybeSingle();
122
- if (error) throw mapError(error);
123
- return data ? rowToRecord(data) : null;
124
- }
125
-
126
- async create(ctx: RequestContext, record: DefinitionRecord): Promise<void> {
127
- const { error } = await this.clients
128
- .forRequest(ctx)
129
- .from('definitions')
130
- .insert(recordToRow(record));
131
- if (error) throw mapError(error);
132
- await this.events.emit({
133
- type: 'definition.created',
134
- definitionId: record.guid,
135
- projectId: record.projectId,
136
- actorId: actorFrom(ctx)
137
- });
138
- }
139
-
140
- async update(ctx: RequestContext, guid: string, patch: DefinitionRecordPatch): Promise<void> {
141
- const row = patchToRow(patch);
142
- if (Object.keys(row).length === 0) return;
143
- if (ctx.userId) row.updated_by = ctx.userId;
144
-
145
- const { data, error } = await this.clients
146
- .forRequest(ctx)
147
- .from('definitions')
148
- .update(row)
149
- .eq('guid', guid)
150
- .is('deleted_at', null)
151
- .select('guid');
152
- if (error) throw mapError(error);
153
- if (!data || data.length === 0) throw new ProviderError(`Definition '${guid}' not found`, 404);
154
- }
155
-
156
- async delete(ctx: RequestContext, guid: string): Promise<void> {
157
- // Soft-delete (matches the local provider and spec §9). A retention
158
- // sweep running as service-role hard-deletes later, which cascades
159
- // versions + share_links via FK.
160
- const stamp = auditSoftDelete(ctx, ctx.userId);
161
- const row: Record<string, unknown> = {
162
- deleted_at: stamp.deletedAt,
163
- updated_at: stamp.updatedAt
164
- };
165
- if (stamp.updatedBy) row.updated_by = stamp.updatedBy;
166
- const { error } = await this.clients
167
- .forRequest(ctx)
168
- .from('definitions')
169
- .update(row)
170
- .eq('guid', guid)
171
- .is('deleted_at', null);
172
- if (error) throw mapError(error);
173
- await this.events.emit({
174
- type: 'definition.deleted',
175
- definitionId: guid,
176
- actorId: actorFrom(ctx)
177
- });
178
- }
179
-
180
- async incrementSolveCount(ctx: RequestContext, guid: string): Promise<void> {
181
- const { error } = await this.clients.forRequest(ctx).rpc('increment_run_count', { g: guid });
182
- if (error) throw mapError(error);
183
- }
184
-
185
- // ============================================================================
186
- // Versions (spec §6)
187
- // ============================================================================
188
-
189
- async createVersion(ctx: RequestContext, version: DefinitionVersion): Promise<void> {
190
- const { error } = await this.clients
191
- .forRequest(ctx)
192
- .from('definition_versions')
193
- .insert(versionToRow(version));
194
- if (error) throw mapError(error);
195
- await this.events.emit({
196
- type: 'definition_version.created',
197
- versionId: version.id,
198
- definitionId: version.definitionId,
199
- actorId: actorFrom(ctx)
200
- });
201
- }
202
-
203
- async listVersions(
204
- ctx: RequestContext,
205
- definitionId: string,
206
- opts?: ListOptions
207
- ): Promise<Page<DefinitionVersion>> {
208
- const range = toRange(opts);
209
- const { data, error, count } = await this.clients
210
- .forRequest(ctx)
211
- .from('definition_versions')
212
- .select('*', { count: 'exact' })
213
- .eq('definition_guid', definitionId)
214
- .order('version_number', { ascending: false })
215
- .range(range.from, range.to);
216
- if (error) throw mapError(error);
217
- const items = (data ?? []).map(rowToVersion);
218
- return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
219
- }
220
-
221
- async getVersion(ctx: RequestContext, versionId: string): Promise<DefinitionVersion | null> {
222
- const { data, error } = await this.clients
223
- .forRequest(ctx)
224
- .from('definition_versions')
225
- .select('*')
226
- .eq('id', versionId)
227
- .maybeSingle();
228
- if (error) throw mapError(error);
229
- return data ? rowToVersion(data) : null;
230
- }
231
-
232
- async setVersionSchema(ctx: RequestContext, versionId: string, schema: UISchema): Promise<void> {
233
- const { error } = await this.clients
234
- .forRequest(ctx)
235
- .from('definition_versions')
236
- .update({ schema, schema_extracted_at: new Date().toISOString() })
237
- .eq('id', versionId);
238
- if (error) throw mapError(error);
239
- }
240
-
241
- async deleteVersion(ctx: RequestContext, versionId: string): Promise<void> {
242
- // FK enforcement at the DB layer: live_version_id / draft_version_id
243
- // are ON DELETE RESTRICT. If the version is referenced by either
244
- // channel, Postgres raises 23503 → mapError → ProviderError(409).
245
- const { error } = await this.clients
246
- .forRequest(ctx)
247
- .from('definition_versions')
248
- .delete()
249
- .eq('id', versionId);
250
- if (error) throw mapError(error);
251
- await this.events.emit({
252
- type: 'definition_version.deleted',
253
- versionId,
254
- actorId: actorFrom(ctx)
255
- });
256
- }
257
-
258
- async setLiveVersion(
259
- ctx: RequestContext,
260
- definitionId: string,
261
- versionId: string
262
- ): Promise<void> {
263
- await this.repointChannel(ctx, definitionId, versionId, 'live_version_id');
264
- // Only `live` advancement is the published-event trigger. Draft
265
- // repointing is the editor's working pointer, not a publication signal.
266
- await this.events.emit({
267
- type: 'definition.published',
268
- definitionId,
269
- versionId,
270
- actorId: actorFrom(ctx)
271
- });
272
- }
273
-
274
- async setDraftVersion(
275
- ctx: RequestContext,
276
- definitionId: string,
277
- versionId: string
278
- ): Promise<void> {
279
- await this.repointChannel(ctx, definitionId, versionId, 'draft_version_id');
280
- }
281
-
282
- async attachInitialVersion(
283
- ctx: RequestContext,
284
- definitionId: string,
285
- versionId: string
286
- ): Promise<void> {
287
- const client = this.clients.forRequest(ctx);
288
- // Validate the version belongs to this definition before repointing —
289
- // matches `repointChannel` so the bootstrap surface enforces the same
290
- // invariant.
291
- const { data: version, error: vError } = await client
292
- .from('definition_versions')
293
- .select('id, definition_guid')
294
- .eq('id', versionId)
295
- .maybeSingle();
296
- if (vError) throw mapError(vError);
297
- if (!version || version.definition_guid !== definitionId) {
298
- throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
299
- }
300
-
301
- // Single UPDATE — both channel pointers and status flip in one row
302
- // write, so a mid-flight failure can't leave a half-promoted record.
303
- const row: Record<string, unknown> = {
304
- live_version_id: versionId,
305
- draft_version_id: versionId,
306
- status: 'draft'
307
- };
308
- if (ctx.userId) row.updated_by = ctx.userId;
309
-
310
- const { data, error } = await client
311
- .from('definitions')
312
- .update(row)
313
- .eq('guid', definitionId)
314
- .is('deleted_at', null)
315
- .select('guid');
316
- if (error) throw mapError(error);
317
- if (!data || data.length === 0) {
318
- throw new ProviderError(`Definition '${definitionId}' not found`, 404);
319
- }
320
- // No `definition.published` event — see interface doc.
321
- }
322
-
323
- private async repointChannel(
324
- ctx: RequestContext,
325
- definitionId: string,
326
- versionId: string,
327
- column: 'live_version_id' | 'draft_version_id'
328
- ): Promise<void> {
329
- const client = this.clients.forRequest(ctx);
330
- // Validate the version belongs to this definition before repointing.
331
- const { data: version, error: vError } = await client
332
- .from('definition_versions')
333
- .select('id, definition_guid')
334
- .eq('id', versionId)
335
- .maybeSingle();
336
- if (vError) throw mapError(vError);
337
- if (!version || version.definition_guid !== definitionId) {
338
- throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
339
- }
340
-
341
- const row: Record<string, unknown> = { [column]: versionId };
342
- if (ctx.userId) row.updated_by = ctx.userId;
343
- const { data, error } = await client
344
- .from('definitions')
345
- .update(row)
346
- .eq('guid', definitionId)
347
- .is('deleted_at', null)
348
- .select('guid');
349
- if (error) throw mapError(error);
350
- if (!data || data.length === 0) {
351
- throw new ProviderError(`Definition '${definitionId}' not found`, 404);
352
- }
353
- }
354
- }
355
-
356
- // ============================================================================
357
- // Row ↔ domain mappers
358
- // ============================================================================
359
- interface DefinitionRow {
360
- guid: string;
361
- project_id: string;
362
- owner_id: string;
363
- created_by?: string | null;
364
- updated_by?: string | null;
365
- compute_server_id: string | null;
366
- display_name: string;
367
- description: string | null;
368
- category: string | null;
369
- tags: string[] | null;
370
- cover_image: string | null;
371
- status: DefinitionStatus;
372
- run_count: number | string;
373
- live_version_id: string | null;
374
- draft_version_id: string | null;
375
- created_at: string;
376
- updated_at: string;
377
- deleted_at?: string | null;
378
- }
379
-
380
- interface DefinitionVersionRow {
381
- id: string;
382
- definition_guid: string;
383
- version_number: number;
384
- file_ext: DefinitionFileExt;
385
- file_key: string;
386
- original_filename: string | null;
387
- uploaded_by: string;
388
- uploaded_at: string;
389
- change_note?: string | null;
390
- schema?: UISchema | null;
391
- schema_extracted_at?: string | null;
392
- }
393
-
394
- function rowToRecord(row: DefinitionRow): DefinitionRecord {
395
- return {
396
- guid: row.guid,
397
- projectId: row.project_id,
398
- ownerId: row.owner_id,
399
- createdBy: row.created_by ?? row.owner_id,
400
- updatedBy: row.updated_by ?? row.owner_id,
401
- liveVersionId: row.live_version_id,
402
- draftVersionId: row.draft_version_id,
403
- computeServerId: row.compute_server_id ?? undefined,
404
- displayName: row.display_name,
405
- description: row.description ?? undefined,
406
- category: row.category ?? undefined,
407
- tags: row.tags ?? undefined,
408
- coverImage: row.cover_image ?? undefined,
409
- status: row.status,
410
- solveCount: typeof row.run_count === 'string' ? Number(row.run_count) : row.run_count,
411
- createdAt: row.created_at,
412
- updatedAt: row.updated_at,
413
- deletedAt: row.deleted_at ?? null
414
- };
415
- }
416
-
417
- function recordToRow(r: DefinitionRecord): Record<string, unknown> {
418
- const row: Record<string, unknown> = {
419
- guid: r.guid,
420
- project_id: r.projectId,
421
- owner_id: r.ownerId,
422
- created_by: r.createdBy,
423
- updated_by: r.updatedBy,
424
- live_version_id: r.liveVersionId,
425
- draft_version_id: r.draftVersionId,
426
- compute_server_id: r.computeServerId ?? null,
427
- display_name: r.displayName,
428
- description: r.description ?? null,
429
- category: r.category ?? null,
430
- cover_image: r.coverImage ?? null,
431
- status: r.status,
432
- run_count: r.solveCount,
433
- created_at: r.createdAt,
434
- updated_at: r.updatedAt,
435
- deleted_at: r.deletedAt ?? null
436
- };
437
- if (r.tags !== undefined) row.tags = r.tags;
438
- return row;
439
- }
440
-
441
- function patchToRow(patch: DefinitionRecordPatch): Record<string, unknown> {
442
- const row: Record<string, unknown> = {};
443
- if (patch.displayName !== undefined) row.display_name = patch.displayName;
444
- if (patch.description !== undefined) row.description = patch.description;
445
- if (patch.category !== undefined) row.category = patch.category;
446
- if (patch.tags !== undefined) row.tags = patch.tags;
447
- if (patch.coverImage !== undefined) row.cover_image = patch.coverImage;
448
- if (patch.projectId !== undefined) row.project_id = patch.projectId;
449
- if (patch.computeServerId !== undefined) row.compute_server_id = patch.computeServerId;
450
- if (patch.status !== undefined) row.status = patch.status;
451
- if (patch.ownerId !== undefined) row.owner_id = patch.ownerId;
452
- return row;
453
- }
454
-
455
- function rowToVersion(row: DefinitionVersionRow): DefinitionVersion {
456
- return {
457
- id: row.id,
458
- definitionId: row.definition_guid,
459
- versionNumber: row.version_number,
460
- fileExt: row.file_ext,
461
- fileKey: row.file_key,
462
- originalFilename: row.original_filename ?? undefined,
463
- uploadedBy: row.uploaded_by,
464
- uploadedAt: row.uploaded_at,
465
- changeNote: row.change_note ?? undefined,
466
- schema: row.schema ?? undefined,
467
- schemaExtractedAt: row.schema_extracted_at ?? undefined
468
- };
469
- }
470
-
471
- function versionToRow(v: DefinitionVersion): Record<string, unknown> {
472
- const row: Record<string, unknown> = {
473
- id: v.id,
474
- definition_guid: v.definitionId,
475
- version_number: v.versionNumber,
476
- file_ext: v.fileExt,
477
- file_key: v.fileKey,
478
- original_filename: v.originalFilename ?? null,
479
- uploaded_by: v.uploadedBy,
480
- uploaded_at: v.uploadedAt
481
- };
482
- if (v.changeNote !== undefined) row.change_note = v.changeNote;
483
- if (v.schema !== undefined) row.schema = v.schema;
484
- if (v.schemaExtractedAt !== undefined) row.schema_extracted_at = v.schemaExtractedAt;
485
- return row;
486
- }
487
-
488
- function definitionOrderColumn(orderBy: DefinitionListOptions['orderBy'] | undefined): string {
489
- switch (orderBy) {
490
- case 'name':
491
- return 'display_name';
492
- case 'solveCount':
493
- return 'run_count';
494
- case 'updatedAt':
495
- return 'updated_at';
496
- case 'createdAt':
497
- default:
498
- return 'created_at';
499
- }
500
- }
501
-
502
- interface PostgrestError {
503
- code?: string;
504
- message?: string;
505
- }
506
-
507
- function mapError(e: unknown): Error {
508
- const pg = e as PostgrestError;
509
- if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
510
- if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
511
- if (e instanceof Error) return e;
512
- if (e && typeof e === 'object') {
513
- const obj = e as { message?: string; details?: string; hint?: string; code?: string };
514
- const msg = obj.message ?? obj.details ?? obj.hint ?? 'Unknown Postgres error';
515
- const err = new Error(obj.code ? `[${obj.code}] ${msg}` : msg);
516
- Object.assign(err, obj);
517
- return err;
518
- }
519
- return new Error(String(e));
520
- }
@@ -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
- }