@selvajs/supabase-provider 0.11.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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +271 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts +81 -0
  4. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/SupabaseAuthProvider.js +383 -0
  6. package/dist/auth/SupabaseAuthProvider.js.map +1 -0
  7. package/dist/auth/__tests__/conformance.test.d.ts +2 -0
  8. package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/conformance.test.js +76 -0
  10. package/dist/auth/__tests__/conformance.test.js.map +1 -0
  11. package/dist/auth/index.d.ts +3 -0
  12. package/dist/auth/index.d.ts.map +1 -0
  13. package/dist/auth/index.js +2 -0
  14. package/dist/auth/index.js.map +1 -0
  15. package/dist/data/SupabaseAuditQuery.d.ts +16 -0
  16. package/dist/data/SupabaseAuditQuery.d.ts.map +1 -0
  17. package/dist/data/SupabaseAuditQuery.js +83 -0
  18. package/dist/data/SupabaseAuditQuery.js.map +1 -0
  19. package/dist/data/SupabaseComputeServerStore.d.ts +26 -0
  20. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -0
  21. package/dist/data/SupabaseComputeServerStore.js +227 -0
  22. package/dist/data/SupabaseComputeServerStore.js.map +1 -0
  23. package/dist/data/SupabaseDataProvider.d.ts +55 -0
  24. package/dist/data/SupabaseDataProvider.d.ts.map +1 -0
  25. package/dist/data/SupabaseDataProvider.js +114 -0
  26. package/dist/data/SupabaseDataProvider.js.map +1 -0
  27. package/dist/data/SupabaseDefinitionStore.d.ts +39 -0
  28. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -0
  29. package/dist/data/SupabaseDefinitionStore.js +419 -0
  30. package/dist/data/SupabaseDefinitionStore.js.map +1 -0
  31. package/dist/data/SupabaseEventSink.d.ts +17 -0
  32. package/dist/data/SupabaseEventSink.d.ts.map +1 -0
  33. package/dist/data/SupabaseEventSink.js +31 -0
  34. package/dist/data/SupabaseEventSink.js.map +1 -0
  35. package/dist/data/SupabaseInviteStore.d.ts +22 -0
  36. package/dist/data/SupabaseInviteStore.d.ts.map +1 -0
  37. package/dist/data/SupabaseInviteStore.js +159 -0
  38. package/dist/data/SupabaseInviteStore.js.map +1 -0
  39. package/dist/data/SupabaseOrgStore.d.ts +33 -0
  40. package/dist/data/SupabaseOrgStore.d.ts.map +1 -0
  41. package/dist/data/SupabaseOrgStore.js +455 -0
  42. package/dist/data/SupabaseOrgStore.js.map +1 -0
  43. package/dist/data/SupabaseProjectStore.d.ts +29 -0
  44. package/dist/data/SupabaseProjectStore.d.ts.map +1 -0
  45. package/dist/data/SupabaseProjectStore.js +328 -0
  46. package/dist/data/SupabaseProjectStore.js.map +1 -0
  47. package/dist/data/SupabaseShareLinkStore.d.ts +26 -0
  48. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -0
  49. package/dist/data/SupabaseShareLinkStore.js +150 -0
  50. package/dist/data/SupabaseShareLinkStore.js.map +1 -0
  51. package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
  52. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
  53. package/dist/data/__tests__/compute-server-conformance.test.js +40 -0
  54. package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
  55. package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
  56. package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
  57. package/dist/data/__tests__/definition-conformance.test.js +85 -0
  58. package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
  59. package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
  60. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
  61. package/dist/data/__tests__/event-sink-conformance.test.js +27 -0
  62. package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
  63. package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
  64. package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
  65. package/dist/data/__tests__/invite-conformance.test.js +45 -0
  66. package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
  67. package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
  68. package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
  69. package/dist/data/__tests__/org-conformance.test.js +29 -0
  70. package/dist/data/__tests__/org-conformance.test.js.map +1 -0
  71. package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
  72. package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
  73. package/dist/data/__tests__/project-conformance.test.js +57 -0
  74. package/dist/data/__tests__/project-conformance.test.js.map +1 -0
  75. package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
  76. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
  77. package/dist/data/__tests__/share-link-conformance.test.js +88 -0
  78. package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
  79. package/dist/data/__tests__/test-helpers.d.ts +53 -0
  80. package/dist/data/__tests__/test-helpers.d.ts.map +1 -0
  81. package/dist/data/__tests__/test-helpers.js +175 -0
  82. package/dist/data/__tests__/test-helpers.js.map +1 -0
  83. package/dist/data/client.d.ts +44 -0
  84. package/dist/data/client.d.ts.map +1 -0
  85. package/dist/data/client.js +40 -0
  86. package/dist/data/client.js.map +1 -0
  87. package/dist/data/index.d.ts +12 -0
  88. package/dist/data/index.d.ts.map +1 -0
  89. package/dist/data/index.js +11 -0
  90. package/dist/data/index.js.map +1 -0
  91. package/dist/data/pagination.d.ts +30 -0
  92. package/dist/data/pagination.d.ts.map +1 -0
  93. package/dist/data/pagination.js +53 -0
  94. package/dist/data/pagination.js.map +1 -0
  95. package/dist/index.d.ts +9 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +6 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +25 -0
  100. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -0
  101. package/dist/permissions/SupabasePlatformPermissionStore.js +153 -0
  102. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -0
  103. package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
  104. package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
  105. package/dist/permissions/__tests__/conformance.test.js +27 -0
  106. package/dist/permissions/__tests__/conformance.test.js.map +1 -0
  107. package/dist/permissions/index.d.ts +2 -0
  108. package/dist/permissions/index.d.ts.map +1 -0
  109. package/dist/permissions/index.js +2 -0
  110. package/dist/permissions/index.js.map +1 -0
  111. package/dist/storage/SupabaseStorageProvider.d.ts +67 -0
  112. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -0
  113. package/dist/storage/SupabaseStorageProvider.js +136 -0
  114. package/dist/storage/SupabaseStorageProvider.js.map +1 -0
  115. package/dist/storage/__tests__/conformance.test.d.ts +2 -0
  116. package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
  117. package/dist/storage/__tests__/conformance.test.js +149 -0
  118. package/dist/storage/__tests__/conformance.test.js.map +1 -0
  119. package/dist/storage/index.d.ts +3 -0
  120. package/dist/storage/index.d.ts.map +1 -0
  121. package/dist/storage/index.js +2 -0
  122. package/dist/storage/index.js.map +1 -0
  123. package/dist/userProfile/SupabaseUserProfileProvider.d.ts +16 -0
  124. package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -0
  125. package/dist/userProfile/SupabaseUserProfileProvider.js +148 -0
  126. package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -0
  127. package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
  128. package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
  129. package/dist/userProfile/__tests__/conformance.test.js +38 -0
  130. package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
  131. package/dist/userProfile/index.d.ts +2 -0
  132. package/dist/userProfile/index.d.ts.map +1 -0
  133. package/dist/userProfile/index.js +2 -0
  134. package/dist/userProfile/index.js.map +1 -0
  135. package/package.json +75 -0
  136. package/src/auth/SupabaseAuthProvider.ts +464 -0
  137. package/src/auth/__tests__/conformance.test.ts +78 -0
  138. package/src/auth/index.ts +2 -0
  139. package/src/data/SupabaseAuditQuery.ts +103 -0
  140. package/src/data/SupabaseComputeServerStore.ts +262 -0
  141. package/src/data/SupabaseDataProvider.ts +125 -0
  142. package/src/data/SupabaseDefinitionStore.ts +504 -0
  143. package/src/data/SupabaseEventSink.ts +30 -0
  144. package/src/data/SupabaseInviteStore.ts +183 -0
  145. package/src/data/SupabaseOrgStore.ts +547 -0
  146. package/src/data/SupabaseProjectStore.ts +414 -0
  147. package/src/data/SupabaseShareLinkStore.ts +191 -0
  148. package/src/data/__tests__/compute-server-conformance.test.ts +40 -0
  149. package/src/data/__tests__/definition-conformance.test.ts +89 -0
  150. package/src/data/__tests__/event-sink-conformance.test.ts +32 -0
  151. package/src/data/__tests__/invite-conformance.test.ts +46 -0
  152. package/src/data/__tests__/org-conformance.test.ts +30 -0
  153. package/src/data/__tests__/project-conformance.test.ts +57 -0
  154. package/src/data/__tests__/share-link-conformance.test.ts +87 -0
  155. package/src/data/__tests__/test-helpers.ts +186 -0
  156. package/src/data/client.ts +86 -0
  157. package/src/data/index.ts +11 -0
  158. package/src/data/pagination.ts +68 -0
  159. package/src/index.ts +23 -0
  160. package/src/permissions/SupabasePlatformPermissionStore.ts +161 -0
  161. package/src/permissions/__tests__/conformance.test.ts +28 -0
  162. package/src/permissions/index.ts +1 -0
  163. package/src/storage/SupabaseStorageProvider.ts +177 -0
  164. package/src/storage/__tests__/conformance.test.ts +152 -0
  165. package/src/storage/index.ts +2 -0
  166. package/src/userProfile/SupabaseUserProfileProvider.ts +177 -0
  167. package/src/userProfile/__tests__/conformance.test.ts +39 -0
  168. package/src/userProfile/index.ts +1 -0
  169. package/supabase/.branches/_current_branch +1 -0
  170. package/supabase/.temp/cli-latest +1 -0
  171. package/supabase/config.toml +68 -0
  172. package/supabase/migrations/0001_initial.sql +1084 -0
  173. package/supabase/seed.sql +13 -0
@@ -0,0 +1,103 @@
1
+ import type {
2
+ AuditEventRow,
3
+ AuditQueryFilters,
4
+ AuditQueryResult,
5
+ DomainEvent,
6
+ IAuditQuery,
7
+ RequestContext
8
+ } from '@selvajs/platform';
9
+ import type { ClientBundle } from './client.js';
10
+
11
+ const MAX_LIMIT = 200;
12
+ const DEFAULT_LIMIT = 100;
13
+
14
+ // Cursor values are interpolated into PostgREST filter strings — both formats
15
+ // must be validated before any string concatenation reaches the query.
16
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
17
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
18
+
19
+ /**
20
+ * Read-side for `audit_events`. Always uses the service-role client — RLS
21
+ * blocks all read access by design, so the route layer is the single trust
22
+ * boundary (callers must gate on `instance_admin`).
23
+ *
24
+ * Pagination is keyset on `(occurred_at desc, id desc)`. The `id` tie-breaker
25
+ * prevents skipped rows when multiple events land in the same millisecond.
26
+ */
27
+ export class SupabaseAuditQuery implements IAuditQuery {
28
+ constructor(private readonly clients: ClientBundle) {}
29
+
30
+ async list(_ctx: RequestContext, filters: AuditQueryFilters): Promise<AuditQueryResult> {
31
+ const limit = clampLimit(filters.limit);
32
+
33
+ let q = this.clients.serviceClient
34
+ .from('audit_events')
35
+ .select('id, type, actor_id, occurred_at, data')
36
+ .order('occurred_at', { ascending: false })
37
+ .order('id', { ascending: false })
38
+ // Fetch one extra row to detect "has next page" without a count query.
39
+ .limit(limit + 1);
40
+
41
+ if (filters.types && filters.types.length > 0) {
42
+ q = q.in('type', [...filters.types]);
43
+ }
44
+ if (filters.actorId) {
45
+ q = q.eq('actor_id', filters.actorId);
46
+ }
47
+ if (filters.sinceIso) {
48
+ q = q.gte('occurred_at', filters.sinceIso);
49
+ }
50
+ if (filters.untilIso) {
51
+ q = q.lte('occurred_at', filters.untilIso);
52
+ }
53
+ if (filters.cursor) {
54
+ // `(occurred_at, id) < (cursorTs, cursorId)` expressed for PostgREST:
55
+ // strictly older OR same timestamp with a strictly smaller id.
56
+ //
57
+ // Both values are interpolated into a `.or()` filter string. Validate
58
+ // shape before interpolation — without these guards a malformed cursor
59
+ // (or a hand-crafted URL by a route caller) could break out of the
60
+ // filter and combine with arbitrary PostgREST operators.
61
+ const ts = filters.cursor.occurredAt;
62
+ const id = filters.cursor.id;
63
+ if (!ISO_RE.test(ts) || !UUID_RE.test(id)) {
64
+ throw new Error('audit_events query: malformed cursor');
65
+ }
66
+ q = q.or(`occurred_at.lt.${ts},and(occurred_at.eq.${ts},id.lt.${id})`);
67
+ }
68
+
69
+ const { data, error } = await q;
70
+ if (error) {
71
+ throw new Error(`audit_events query failed: ${error.message}`);
72
+ }
73
+
74
+ const fetched = (data ?? []) as Array<{
75
+ id: string;
76
+ type: string;
77
+ actor_id: string;
78
+ occurred_at: string;
79
+ data: DomainEvent;
80
+ }>;
81
+
82
+ const hasMore = fetched.length > limit;
83
+ const page = hasMore ? fetched.slice(0, limit) : fetched;
84
+ const last = page[page.length - 1];
85
+
86
+ return {
87
+ rows: page.map((r) => ({
88
+ id: r.id,
89
+ type: r.type as DomainEvent['type'],
90
+ actorId: r.actor_id,
91
+ occurredAt: r.occurred_at,
92
+ data: r.data
93
+ })) satisfies AuditEventRow[],
94
+ nextCursor: hasMore && last ? { occurredAt: last.occurred_at, id: last.id } : null
95
+ };
96
+ }
97
+ }
98
+
99
+ function clampLimit(requested: number | undefined): number {
100
+ if (!requested || requested <= 0) return DEFAULT_LIMIT;
101
+ if (requested > MAX_LIMIT) return MAX_LIMIT;
102
+ return Math.floor(requested);
103
+ }
@@ -0,0 +1,262 @@
1
+ import {
2
+ isOrgServer,
3
+ isPlatformServer,
4
+ type IComputeServerStore,
5
+ type ComputeConfig,
6
+ type ComputeServerConfig,
7
+ type RequestContext
8
+ } from '@selvajs/platform';
9
+ import { ProviderError } from '@selvajs/platform';
10
+ import type { ClientBundle } from './client.js';
11
+
12
+ /**
13
+ * Compute-server config backed by three tables:
14
+ * - `compute_servers`: one row per server. `scope = 'platform' | 'org'`.
15
+ * Platform rows carry a `shared_with_all` flag + `compute_server_shares`
16
+ * allowlist; org rows carry `owner_org_id`.
17
+ * - `compute_server_shares`: many-to-many for platform servers shared
18
+ * with specific orgs (when `shared_with_all = false`).
19
+ * - `compute_server_org_defaults`: per-org `orgDefaults[orgId]` choice.
20
+ * - `compute_server_platform_default`: single-row sentinel for the global
21
+ * `defaultServerId`.
22
+ *
23
+ * `getConfig` returns the full doc; the visibility predicate is applied by
24
+ * callers (resolver, page loaders) using helpers in `@selvajs/platform`.
25
+ */
26
+ export class SupabaseComputeServerStore implements IComputeServerStore {
27
+ constructor(private readonly clients: ClientBundle) {}
28
+
29
+ async getConfig(ctx: RequestContext): Promise<ComputeConfig> {
30
+ const client = this.clients.forRequest(ctx);
31
+
32
+ const [serversRes, sharesRes, orgDefRes, platDefRes] = await Promise.all([
33
+ client.from('compute_servers').select('*'),
34
+ client.from('compute_server_shares').select('*'),
35
+ client.from('compute_server_org_defaults').select('*'),
36
+ client
37
+ .from('compute_server_platform_default')
38
+ .select('default_server_id')
39
+ .eq('singleton', true)
40
+ .maybeSingle()
41
+ ]);
42
+
43
+ if (serversRes.error) throw mapError(serversRes.error);
44
+ if (sharesRes.error) throw mapError(sharesRes.error);
45
+ if (orgDefRes.error) throw mapError(orgDefRes.error);
46
+ if (platDefRes.error) throw mapError(platDefRes.error);
47
+
48
+ // Build per-server `sharedWith` allowlists from the join table.
49
+ const sharedByServer = new Map<string, string[]>();
50
+ for (const row of sharesRes.data ?? []) {
51
+ const list = sharedByServer.get(row.server_id) ?? [];
52
+ list.push(row.org_id);
53
+ sharedByServer.set(row.server_id, list);
54
+ }
55
+
56
+ const servers = (serversRes.data ?? []).map((row) => rowToServer(row, sharedByServer));
57
+
58
+ const orgDefaults: Record<string, string> = {};
59
+ for (const row of orgDefRes.data ?? []) {
60
+ if (row.default_server_id) orgDefaults[row.org_id] = row.default_server_id;
61
+ }
62
+
63
+ return {
64
+ servers,
65
+ defaultServerId: platDefRes.data?.default_server_id ?? undefined,
66
+ orgDefaults
67
+ };
68
+ }
69
+
70
+ async savePlatformServers(
71
+ ctx: RequestContext,
72
+ servers: ComputeServerConfig[],
73
+ defaultServerId: string | undefined
74
+ ): Promise<void> {
75
+ const client = this.clients.forRequest(ctx);
76
+ const platformOnly = servers.filter(isPlatformServer);
77
+
78
+ // Replace-all of platform rows.
79
+ const { error: delErr } = await client.from('compute_servers').delete().eq('scope', 'platform');
80
+ if (delErr) throw mapError(delErr);
81
+
82
+ if (platformOnly.length > 0) {
83
+ const { error: insErr } = await client
84
+ .from('compute_servers')
85
+ .insert(platformOnly.map(serverToRow));
86
+ if (insErr) throw mapError(insErr);
87
+
88
+ // Rebuild the share rows for any non-`all` platform servers.
89
+ const shareRows = platformOnly.flatMap((s) =>
90
+ s.sharedWith === 'all'
91
+ ? []
92
+ : s.sharedWith.map((orgId) => ({ server_id: s.id, org_id: orgId }))
93
+ );
94
+ if (shareRows.length > 0) {
95
+ const { error: shErr } = await client.from('compute_server_shares').insert(shareRows);
96
+ if (shErr) throw mapError(shErr);
97
+ }
98
+ }
99
+
100
+ const { error: defErr } = await client
101
+ .from('compute_server_platform_default')
102
+ .update({ default_server_id: defaultServerId ?? null })
103
+ .eq('singleton', true);
104
+ if (defErr) throw mapError(defErr);
105
+ }
106
+
107
+ async saveOrgServers(
108
+ ctx: RequestContext,
109
+ orgId: string,
110
+ servers: ComputeServerConfig[],
111
+ defaultServerId?: string | null
112
+ ): Promise<void> {
113
+ const client = this.clients.forRequest(ctx);
114
+ const orgOnly = servers.filter(isOrgServer).map((s) => ({ ...s, ownerOrgId: orgId }));
115
+
116
+ const { error: delErr } = await client
117
+ .from('compute_servers')
118
+ .delete()
119
+ .eq('scope', 'org')
120
+ .eq('owner_org_id', orgId);
121
+ if (delErr) throw mapError(delErr);
122
+
123
+ if (orgOnly.length > 0) {
124
+ const { error: insErr } = await client
125
+ .from('compute_servers')
126
+ .insert(orgOnly.map(serverToRow));
127
+ if (insErr) throw mapError(insErr);
128
+ }
129
+
130
+ if (defaultServerId === null) {
131
+ const { error } = await client
132
+ .from('compute_server_org_defaults')
133
+ .delete()
134
+ .eq('org_id', orgId);
135
+ if (error) throw mapError(error);
136
+ } else if (typeof defaultServerId === 'string') {
137
+ const { error } = await client
138
+ .from('compute_server_org_defaults')
139
+ .upsert({ org_id: orgId, default_server_id: defaultServerId });
140
+ if (error) throw mapError(error);
141
+ }
142
+ }
143
+
144
+ async setOrgDefault(ctx: RequestContext, orgId: string, serverId: string | null): Promise<void> {
145
+ const client = this.clients.forRequest(ctx);
146
+ if (serverId === null) {
147
+ const { error } = await client
148
+ .from('compute_server_org_defaults')
149
+ .delete()
150
+ .eq('org_id', orgId);
151
+ if (error) throw mapError(error);
152
+ return;
153
+ }
154
+ const { error } = await client
155
+ .from('compute_server_org_defaults')
156
+ .upsert({ org_id: orgId, default_server_id: serverId });
157
+ if (error) throw mapError(error);
158
+ }
159
+
160
+ async deleteByOrg(ctx: RequestContext, orgId: string): Promise<void> {
161
+ const client = this.clients.forRequest(ctx);
162
+
163
+ // orgDefaults entry — FK on org_id cascades from orgs(id), but org
164
+ // soft-delete doesn't trigger the cascade, so do it explicitly.
165
+ const { error: defErr } = await client
166
+ .from('compute_server_org_defaults')
167
+ .delete()
168
+ .eq('org_id', orgId);
169
+ if (defErr) throw mapError(defErr);
170
+
171
+ // Org-private servers owned by this org.
172
+ const { error: srvErr } = await client
173
+ .from('compute_servers')
174
+ .delete()
175
+ .eq('scope', 'org')
176
+ .eq('owner_org_id', orgId);
177
+ if (srvErr) throw mapError(srvErr);
178
+
179
+ // Strip this org from any platform server share allowlist.
180
+ const { error: shErr } = await client
181
+ .from('compute_server_shares')
182
+ .delete()
183
+ .eq('org_id', orgId);
184
+ if (shErr) throw mapError(shErr);
185
+ }
186
+ }
187
+
188
+ interface ServerRow {
189
+ id: string;
190
+ scope: 'platform' | 'org';
191
+ owner_org_id: string | null;
192
+ shared_with_all: boolean;
193
+ label: string;
194
+ server_url: string;
195
+ api_key: string | null;
196
+ timeout_ms: number | null;
197
+ retry_count: number | null;
198
+ }
199
+
200
+ function rowToServer(row: ServerRow, sharedByServer: Map<string, string[]>): ComputeServerConfig {
201
+ const common = {
202
+ id: row.id,
203
+ label: row.label,
204
+ serverUrl: row.server_url,
205
+ apiKey: row.api_key ?? undefined,
206
+ timeoutMs: row.timeout_ms ?? undefined,
207
+ retryCount: row.retry_count ?? undefined
208
+ };
209
+ if (row.scope === 'platform') {
210
+ return {
211
+ ...common,
212
+ scope: 'platform',
213
+ sharedWith: row.shared_with_all ? 'all' : (sharedByServer.get(row.id) ?? [])
214
+ };
215
+ }
216
+ if (!row.owner_org_id) {
217
+ throw new Error(`compute_servers row ${row.id} has scope='org' but null owner_org_id`);
218
+ }
219
+ return { ...common, scope: 'org', ownerOrgId: row.owner_org_id };
220
+ }
221
+
222
+ function serverToRow(s: ComputeServerConfig): ServerRow {
223
+ const base = {
224
+ id: s.id,
225
+ label: s.label,
226
+ server_url: s.serverUrl,
227
+ api_key: s.apiKey ?? null,
228
+ timeout_ms: s.timeoutMs ?? null,
229
+ retry_count: s.retryCount ?? null
230
+ };
231
+ if (isPlatformServer(s)) {
232
+ return {
233
+ ...base,
234
+ scope: 'platform',
235
+ owner_org_id: null,
236
+ shared_with_all: s.sharedWith === 'all'
237
+ };
238
+ }
239
+ return {
240
+ ...base,
241
+ scope: 'org',
242
+ owner_org_id: s.ownerOrgId,
243
+ shared_with_all: false
244
+ };
245
+ }
246
+
247
+ interface PostgrestError {
248
+ code?: string;
249
+ message?: string;
250
+ }
251
+
252
+ function mapError(e: unknown): Error {
253
+ const pg = e as PostgrestError;
254
+ if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
255
+ if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
256
+ if (e instanceof Error) return e;
257
+ if (e && typeof e === 'object') {
258
+ const obj = e as { message?: string; code?: string };
259
+ return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
260
+ }
261
+ return new Error(String(e));
262
+ }
@@ -0,0 +1,125 @@
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 { SupabaseAuditQuery } from './SupabaseAuditQuery.js';
7
+ import { SupabaseOrgStore } from './SupabaseOrgStore.js';
8
+ import { SupabaseProjectStore } from './SupabaseProjectStore.js';
9
+ import { SupabaseDefinitionStore } from './SupabaseDefinitionStore.js';
10
+ import { SupabaseInviteStore } from './SupabaseInviteStore.js';
11
+ import { SupabaseComputeServerStore } from './SupabaseComputeServerStore.js';
12
+ import { SupabaseShareLinkStore } from './SupabaseShareLinkStore.js';
13
+ import { SupabaseUserProfileProvider } from '../userProfile/SupabaseUserProfileProvider.js';
14
+ import { SupabasePlatformPermissionStore } from '../permissions/SupabasePlatformPermissionStore.js';
15
+
16
+ /**
17
+ * Stub until the Supabase implementation lands. Reads and mutations surface a
18
+ * clear 501; cascade hooks are silent no-ops so deleting an org or project on
19
+ * a Supabase deployment without the feature wired does not blow up.
20
+ */
21
+ const NOT_IMPLEMENTED_MSG = 'Platform projects are not supported on this deployment yet.';
22
+ const notImplementedGrantStore: IPlatformProjectGrantStore = {
23
+ listByProject: async () => {
24
+ throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
25
+ },
26
+ create: async () => {
27
+ throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
28
+ },
29
+ delete: async () => {
30
+ throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
31
+ },
32
+ deleteByProject: async () => {
33
+ // No-op: nothing to clean up on a deployment that never accepted grants.
34
+ },
35
+ deleteByGranteeOrg: async () => {
36
+ // No-op: same reasoning.
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Composition of every data store for the Supabase backend. Instantiates one
42
+ * `ClientBundle` (service-role + per-request factory) and wires each store
43
+ * against it so every store in a single process shares the same pooled clients.
44
+ */
45
+ export class SupabaseDataProvider implements IDataProvider {
46
+ readonly orgs: SupabaseOrgStore;
47
+ readonly projects: SupabaseProjectStore;
48
+ readonly definitions: SupabaseDefinitionStore;
49
+ readonly invites: SupabaseInviteStore;
50
+ readonly computeServer: SupabaseComputeServerStore;
51
+ readonly shareLinks: SupabaseShareLinkStore;
52
+ readonly userProfile: SupabaseUserProfileProvider;
53
+ readonly permissions: SupabasePlatformPermissionStore;
54
+ readonly platformProjectGrants: IPlatformProjectGrantStore;
55
+ readonly auditQuery: SupabaseAuditQuery;
56
+
57
+ private constructor(
58
+ private readonly clients: ClientBundle,
59
+ events: IEventSink
60
+ ) {
61
+ this.orgs = new SupabaseOrgStore(clients, events);
62
+ this.projects = new SupabaseProjectStore(clients, events);
63
+ this.definitions = new SupabaseDefinitionStore(clients, events);
64
+ this.invites = new SupabaseInviteStore(clients, events);
65
+ this.computeServer = new SupabaseComputeServerStore(clients);
66
+ this.shareLinks = new SupabaseShareLinkStore(clients, events);
67
+ this.userProfile = new SupabaseUserProfileProvider(clients);
68
+ this.permissions = new SupabasePlatformPermissionStore(clients);
69
+ this.platformProjectGrants = notImplementedGrantStore;
70
+ this.auditQuery = new SupabaseAuditQuery(clients);
71
+ }
72
+
73
+ static fromEnv(
74
+ env: Record<string, string | undefined>,
75
+ events?: IEventSink
76
+ ): SupabaseDataProvider {
77
+ const supabaseUrl = env.SUPABASE_URL;
78
+ const anonKey = env.SUPABASE_ANON_KEY;
79
+ const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY;
80
+ if (!supabaseUrl) throw new Error('Missing required env var: SUPABASE_URL');
81
+ if (!anonKey) throw new Error('Missing required env var: SUPABASE_ANON_KEY');
82
+ if (!serviceRoleKey) throw new Error('Missing required env var: SUPABASE_SERVICE_ROLE_KEY');
83
+ const bundle = buildClientBundle({ supabaseUrl, anonKey, serviceRoleKey });
84
+ return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
85
+ }
86
+
87
+ static create(opts: BuildClientOptions, events?: IEventSink): SupabaseDataProvider {
88
+ const bundle = buildClientBundle(opts);
89
+ return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
90
+ }
91
+
92
+ /**
93
+ * Build from a pre-existing `ClientBundle`. Useful for tests or advanced
94
+ * cases that need to inject a custom event sink or share a bundle externally.
95
+ */
96
+ static fromBundle(bundle: ClientBundle, events?: IEventSink): SupabaseDataProvider {
97
+ return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
98
+ }
99
+
100
+ /**
101
+ * Expose the underlying bundle so other providers in the same package
102
+ * (storage, user profile, auth) can share one set of clients per process.
103
+ */
104
+ getClientBundle(): ClientBundle {
105
+ return this.clients;
106
+ }
107
+
108
+ /**
109
+ * No-op: `public.user_profiles` is auto-seeded by the `handle_new_auth_user`
110
+ * trigger on every `auth.users` insert (see `0001_initial.sql`). The data
111
+ * layer always has a row for any authenticated user without per-request work.
112
+ */
113
+ async ensureUser(): Promise<void> {
114
+ // Trigger handles it.
115
+ }
116
+
117
+ /**
118
+ * No-op: `public.user_profiles.user_id` references `auth.users(id)` with
119
+ * `on delete cascade`. Deleting the auth user removes the profile row
120
+ * automatically.
121
+ */
122
+ async onUserDeleted(): Promise<void> {
123
+ // FK cascade handles it.
124
+ }
125
+ }