@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,78 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runAuthProviderConformance, runEmailLinkAuthConformance } from '@selvajs/platform/testing';
3
- import { SupabaseAuthProvider } from '../SupabaseAuthProvider.js';
4
- import { readEnv, resetAllData } from '../../data/__tests__/test-helpers.js';
5
-
6
- const envCtx = readEnv();
7
-
8
- if (!envCtx) {
9
- describe.skip('SupabaseAuthProvider (skipped: no live stack)', () => {
10
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
11
- });
12
- } else {
13
- const ctx = envCtx; // non-null in this branch
14
- // The auth conformance suite expects `verifyLogin('', adminPassword)` to
15
- // succeed. Supabase requires a non-empty email, so we seed a deterministic
16
- // admin email and have the suite use that via the `adminEmail` option.
17
- const ADMIN_EMAIL = 'admin@conformance.test';
18
- const ADMIN_PASSWORD = 'conformance-admin-password-1234';
19
-
20
- async function ensureAdmin() {
21
- const { data: existing } = await ctx.adminClient.auth.admin.listUsers({ perPage: 1000 });
22
- const match = existing.users.find((u) => u.email === ADMIN_EMAIL);
23
- if (match) return match.id;
24
- const { data, error } = await ctx.adminClient.auth.admin.createUser({
25
- email: ADMIN_EMAIL,
26
- password: ADMIN_PASSWORD,
27
- email_confirm: true
28
- });
29
- if (error) throw error;
30
- return data.user.id;
31
- }
32
-
33
- async function makeProvider() {
34
- const provider = new SupabaseAuthProvider({
35
- supabaseUrl: ctx.url,
36
- anonKey: ctx.anonKey,
37
- serviceRoleKey: ctx.serviceRoleKey
38
- });
39
- // Ensure the admin account exists + has instance_admin.
40
- const id = await ensureAdmin();
41
- const { error: upError } = await ctx.adminClient
42
- .from('user_profiles')
43
- .update({ platform_permissions: ['instance_admin'] })
44
- .eq('user_id', id);
45
- if (upError) throw upError;
46
- return { provider, adminPassword: ADMIN_PASSWORD };
47
- }
48
-
49
- describe('SupabaseAuthProvider', () => {
50
- beforeEach(async () => {
51
- await resetAllData(ctx);
52
- });
53
-
54
- runAuthProviderConformance({
55
- name: 'SupabaseAuthProvider',
56
- createProvider: async () => {
57
- const { provider, adminPassword } = await makeProvider();
58
- return { provider, adminPassword, adminEmail: ADMIN_EMAIL };
59
- },
60
- userManagement: true
61
- });
62
-
63
- runEmailLinkAuthConformance({
64
- name: 'SupabaseEmailLinkAuth',
65
- createAdapter: async () => {
66
- const { provider } = await makeProvider();
67
- return {
68
- adapter: provider.emailLink,
69
- // `validEmail` isn't used by the suite for live mail delivery;
70
- // it just needs to look like a real address. The malformed
71
- // case is what we actually assert on.
72
- validEmail: 'real-looking@conformance.test',
73
- invalidEmail: 'not-an-email-address'
74
- };
75
- }
76
- });
77
- });
78
- }
package/src/auth/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { SupabaseAuthProvider } from './SupabaseAuthProvider.js';
2
- export type { SupabaseAuthProviderConfig } from './SupabaseAuthProvider.js';
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env node
2
- // ============================================================================
3
- // sync-migrations — copy this package's Supabase migrations into a consuming
4
- // app's supabase/migrations/ directory.
5
- //
6
- // Migrations ship with deterministic timestamp-prefixed filenames
7
- // (<UTCtimestamp>_selva_<name>.sql) so they slot into a consuming app's own
8
- // migration timeline without colliding, and so every machine/CI produces a
9
- // byte-identical migration history. We copy verbatim — no rename at copy time,
10
- // which is what keeps the history stable.
11
- //
12
- // Usage:
13
- // npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]
14
- //
15
- // --dir <path> Target migrations dir (default: ./supabase/migrations)
16
- // --force Overwrite files that exist with differing content
17
- // ============================================================================
18
-
19
- import { fileURLToPath } from 'node:url';
20
- import { dirname, join, resolve } from 'node:path';
21
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
22
-
23
- const here = dirname(fileURLToPath(import.meta.url));
24
-
25
- // dist/cli/ -> package root is two levels up; migrations ship alongside src/dist.
26
- const packageRoot = resolve(here, '..', '..');
27
- const sourceDir = join(packageRoot, 'supabase', 'migrations');
28
-
29
- interface Options {
30
- targetDir: string;
31
- force: boolean;
32
- }
33
-
34
- function parseArgs(argv: string[]): Options {
35
- let targetDir = resolve(process.cwd(), 'supabase', 'migrations');
36
- let force = false;
37
-
38
- for (let i = 0; i < argv.length; i++) {
39
- const arg = argv[i];
40
- if (arg === '--dir') {
41
- const value = argv[++i];
42
- if (!value) fail('--dir requires a path argument');
43
- targetDir = resolve(process.cwd(), value);
44
- } else if (arg === '--force') {
45
- force = true;
46
- } else if (arg === '--help' || arg === '-h') {
47
- printHelp();
48
- process.exit(0);
49
- } else {
50
- fail(`unknown argument: ${arg}`);
51
- }
52
- }
53
-
54
- return { targetDir, force };
55
- }
56
-
57
- function printHelp(): void {
58
- console.log(
59
- [
60
- 'sync-migrations — copy @selvajs/supabase-provider migrations into your app.',
61
- '',
62
- 'Usage:',
63
- ' npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]',
64
- '',
65
- ' --dir <path> Target migrations dir (default: ./supabase/migrations)',
66
- ' --force Overwrite files that exist with differing content',
67
- '',
68
- 'After syncing, apply with: npx supabase db push'
69
- ].join('\n')
70
- );
71
- }
72
-
73
- function fail(message: string): never {
74
- console.error(`error: ${message}`);
75
- process.exit(1);
76
- }
77
-
78
- function main(): void {
79
- const { targetDir, force } = parseArgs(process.argv.slice(2));
80
-
81
- if (!existsSync(sourceDir)) {
82
- fail(`could not locate packaged migrations at ${sourceDir}`);
83
- }
84
-
85
- const files = readdirSync(sourceDir)
86
- .filter((name) => name.endsWith('.sql'))
87
- .sort();
88
-
89
- if (files.length === 0) {
90
- fail(`no .sql migrations found in ${sourceDir}`);
91
- }
92
-
93
- mkdirSync(targetDir, { recursive: true });
94
-
95
- let copied = 0;
96
- let skipped = 0;
97
- let conflicts = 0;
98
-
99
- for (const name of files) {
100
- const from = join(sourceDir, name);
101
- const to = join(targetDir, name);
102
- const contents = readFileSync(from);
103
-
104
- if (existsSync(to)) {
105
- const existing = readFileSync(to);
106
- if (existing.equals(contents)) {
107
- skipped++;
108
- continue;
109
- }
110
- if (!force) {
111
- console.warn(
112
- ` conflict ${name} exists with different content (use --force to overwrite)`
113
- );
114
- conflicts++;
115
- continue;
116
- }
117
- }
118
-
119
- writeFileSync(to, contents);
120
- console.log(` copied ${name}`);
121
- copied++;
122
- }
123
-
124
- console.log(`\n${copied} copied, ${skipped} unchanged, ${conflicts} conflict(s) -> ${targetDir}`);
125
-
126
- if (conflicts > 0) {
127
- process.exit(1);
128
- }
129
-
130
- console.log('\nNext: review the files, then run npx supabase db push');
131
- }
132
-
133
- main();
@@ -1,103 +0,0 @@
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
- }
@@ -1,262 +0,0 @@
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
- }