@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,152 +0,0 @@
1
- import { describe, beforeEach, it, expect } from 'vitest';
2
- import { createClient } from '@supabase/supabase-js';
3
- import { runStorageProviderConformance } from '@selvajs/platform/testing';
4
- import { definitionPaths } from '@selvajs/platform';
5
- import { SupabaseStorageProvider } from '../SupabaseStorageProvider.js';
6
-
7
- const SUPABASE_URL = process.env.SUPABASE_URL;
8
- const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
9
- const PUBLIC_BUCKET = process.env.SUPABASE_PUBLIC_BUCKET ?? 'selva-public';
10
- const PRIVATE_BUCKET = process.env.SUPABASE_PRIVATE_BUCKET ?? 'selva-private';
11
-
12
- // The conformance suite hits a live Supabase stack. If creds aren't present
13
- // (e.g. in CI without Docker, or a clean clone pre-`supabase start`), skip
14
- // the suite with a single explanatory test so the run surfaces the reason.
15
- // `createClient` transitively loads `@supabase/realtime-js`, which on Node < 22
16
- // throws at construction without native WebSocket — treat that as "no stack"
17
- // rather than crashing the suite during collection.
18
- function tryBuildAdminClient(): ReturnType<typeof createClient> | null {
19
- if (!SUPABASE_URL || !SERVICE_ROLE_KEY) return null;
20
- try {
21
- return createClient(SUPABASE_URL, SERVICE_ROLE_KEY, {
22
- auth: { persistSession: false, autoRefreshToken: false }
23
- });
24
- } catch (err) {
25
- const msg = err instanceof Error ? err.message : String(err);
26
- if (msg.includes('WebSocket')) return null;
27
- throw err;
28
- }
29
- }
30
- const _adminClient = tryBuildAdminClient();
31
-
32
- if (_adminClient === null) {
33
- describe.skip('SupabaseStorageProvider (skipped: no live stack)', () => {
34
- it('populate packages/providers/supabase/.env.test with SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (from `npx supabase start`) to run these tests', () => {});
35
- });
36
- } else {
37
- // Re-bind in this branch so closures below see the non-null narrowed type.
38
- const adminClient = _adminClient;
39
-
40
- /**
41
- * Delete every object under a prefix in a bucket. The conformance suite
42
- * assumes fresh state per test; Supabase Storage is shared across runs,
43
- * so we reset both buckets before each test.
44
- */
45
- async function emptyBucket(bucket: string): Promise<void> {
46
- const keys: string[] = [];
47
- const stack: string[] = [''];
48
- while (stack.length > 0) {
49
- const dir = stack.pop()!;
50
- const { data, error } = await adminClient.storage.from(bucket).list(dir, {
51
- limit: 1000,
52
- sortBy: { column: 'name', order: 'asc' }
53
- });
54
- if (error) throw error;
55
- for (const entry of data ?? []) {
56
- const full = dir ? `${dir}/${entry.name}` : entry.name;
57
- if (entry.id === null) stack.push(full);
58
- else keys.push(full);
59
- }
60
- }
61
- if (keys.length === 0) return;
62
- for (let i = 0; i < keys.length; i += 1000) {
63
- const { error } = await adminClient.storage.from(bucket).remove(keys.slice(i, i + 1000));
64
- if (error) throw error;
65
- }
66
- }
67
-
68
- describe('SupabaseStorageProvider', () => {
69
- beforeEach(async () => {
70
- await Promise.all([emptyBucket(PUBLIC_BUCKET), emptyBucket(PRIVATE_BUCKET)]);
71
- });
72
-
73
- runStorageProviderConformance({
74
- name: 'SupabaseStorageProvider',
75
- createStorage: () =>
76
- new SupabaseStorageProvider({
77
- supabaseUrl: SUPABASE_URL!,
78
- serviceRoleKey: SERVICE_ROLE_KEY!,
79
- publicBucket: PUBLIC_BUCKET,
80
- privateBucket: PRIVATE_BUCKET
81
- })
82
- });
83
-
84
- // ============================================================================
85
- // Bucket routing — Supabase-specific (the conformance suite is provider-
86
- // agnostic and can't see which bucket a file lands in). Without these tests
87
- // a regex regression in `bucketFor` could silently move .gh source files
88
- // into the public/CDN bucket.
89
- // ============================================================================
90
- describe('bucket routing', () => {
91
- const storage = new SupabaseStorageProvider({
92
- supabaseUrl: SUPABASE_URL!,
93
- serviceRoleKey: SERVICE_ROLE_KEY!,
94
- publicBucket: PUBLIC_BUCKET,
95
- privateBucket: PRIVATE_BUCKET
96
- });
97
-
98
- async function existsInBucket(bucket: string, path: string): Promise<boolean> {
99
- const { data, error } = await adminClient.storage.from(bucket).download(path);
100
- if (error) return false;
101
- return Boolean(data);
102
- }
103
-
104
- it('versioned .gh files land in the private bucket', async () => {
105
- const guid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
106
- const path = definitionPaths.version(guid, 1, 'gh');
107
- await storage.put(path, new TextEncoder().encode('GH_BYTES'));
108
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
109
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
110
- });
111
-
112
- it('versioned .ghx files land in the private bucket', async () => {
113
- const guid = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
114
- const path = definitionPaths.version(guid, 3, 'ghx');
115
- await storage.put(path, new TextEncoder().encode('GHX_BYTES'));
116
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
117
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
118
- });
119
-
120
- it('cover images land in the public bucket', async () => {
121
- const guid = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
122
- const path = definitionPaths.image(guid);
123
- // Real 1×1 transparent PNG. The provider runs every image through
124
- // transcodeImageIfNeeded → sharp, so a hand-crafted byte stub
125
- // would fail the decode. Routing (the test's actual concern) is
126
- // independent of which format the input arrives in.
127
- const tinyPng = Uint8Array.from([
128
- 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8,
129
- 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 3, 232, 0, 0, 3, 232, 1,
130
- 181, 123, 82, 107, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 96, 96, 96, 96, 0, 0, 0, 5,
131
- 0, 1, 165, 246, 69, 64, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
132
- ]);
133
- await storage.put(path, tinyPng, 'image/png');
134
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(true);
135
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(false);
136
- });
137
-
138
- it('getPublicUrl returns the proxy prefix for private files', () => {
139
- const path = definitionPaths.version('guid-1', 1, 'gh');
140
- const url = storage.getPublicUrl(path);
141
- expect(url.startsWith('/api/files/')).toBe(true);
142
- expect(url).not.toContain('/storage/v1/object/public/');
143
- });
144
-
145
- it('getPublicUrl returns the CDN URL for public files', () => {
146
- const path = definitionPaths.image('guid-1');
147
- const url = storage.getPublicUrl(path);
148
- expect(url).toContain(`/storage/v1/object/public/${PUBLIC_BUCKET}/`);
149
- });
150
- });
151
- });
152
- }
@@ -1,2 +0,0 @@
1
- export { SupabaseStorageProvider } from './SupabaseStorageProvider.js';
2
- export type { SupabaseStorageProviderConfig } from './SupabaseStorageProvider.js';
@@ -1,177 +0,0 @@
1
- import type {
2
- IUserProfileStore,
3
- RecentRun,
4
- RequestContext,
5
- UserManagementResult,
6
- UserProfile
7
- } from '@selvajs/platform';
8
- import { ProviderError, hasPermission } from '@selvajs/platform';
9
- import type { ClientBundle } from '../data/client.js';
10
-
11
- /**
12
- * User profile backed by `public.user_profiles` (one row per `auth.users.id`).
13
- * The row is auto-seeded by the `handle_new_auth_user` trigger on signup, so
14
- * the store never inserts — only reads/updates.
15
- *
16
- * Uses the service-role client throughout (RLS on `user_profiles` is
17
- * coarse-grained — server-side flows like the hooks.server.ts profile load
18
- * need access regardless of the caller's identity). The `assertCanAccess`
19
- * check enforces "you can only see/modify your own profile" at the app layer
20
- * to match the `IUserProfileStore` contract.
21
- */
22
- const MAX_RECENT_RUNS = 20;
23
-
24
- export class SupabaseUserProfileProvider implements IUserProfileStore {
25
- constructor(private readonly clients: ClientBundle) {}
26
-
27
- private client() {
28
- return this.clients.serviceClient;
29
- }
30
-
31
- async getProfile(ctx: RequestContext, userId: string): Promise<UserProfile | null> {
32
- assertCanAccess(ctx, userId);
33
- const { data, error } = await this.client()
34
- .from('user_profiles')
35
- .select('user_id, display_name, starred_definitions, recent_runs')
36
- .eq('user_id', userId)
37
- .maybeSingle();
38
- if (error) throw mapError(error);
39
- return data ? rowToProfile(data) : null;
40
- }
41
-
42
- async getProfiles(_ctx: RequestContext, userIds: readonly string[]): Promise<UserProfile[]> {
43
- // Batch read serves display-name lookups across the UI; scoping to one
44
- // user defeats the purpose. No assertCanAccess.
45
- if (userIds.length === 0) return [];
46
- const { data, error } = await this.client()
47
- .from('user_profiles')
48
- .select('user_id, display_name, starred_definitions, recent_runs')
49
- .in('user_id', [...userIds]);
50
- if (error) throw mapError(error);
51
- return (data ?? []).map(rowToProfile);
52
- }
53
-
54
- async updateProfile(
55
- ctx: RequestContext,
56
- userId: string,
57
- patch: { displayName?: string }
58
- ): Promise<UserManagementResult> {
59
- assertCanAccess(ctx, userId);
60
- const row: Record<string, unknown> = {};
61
- if (patch.displayName !== undefined) row.display_name = patch.displayName;
62
- if (Object.keys(row).length === 0) return 'ok';
63
-
64
- const { data, error } = await this.client()
65
- .from('user_profiles')
66
- .update(row)
67
- .eq('user_id', userId)
68
- .select('user_id');
69
- if (error) throw mapError(error);
70
- return data && data.length > 0 ? 'ok' : 'not_found';
71
- }
72
-
73
- async starDefinition(
74
- ctx: RequestContext,
75
- userId: string,
76
- definitionId: string
77
- ): Promise<UserManagementResult> {
78
- assertCanAccess(ctx, userId);
79
- // Read-modify-write is fine for the local scale here; if concurrent
80
- // starring ever becomes a hotspot, a SECURITY DEFINER RPC using
81
- // `array_append` + `array_distinct` closes the race.
82
- const existing = await this.getProfile(ctx, userId);
83
- if (!existing) return 'not_found';
84
- if (existing.starredDefinitions.includes(definitionId)) return 'ok';
85
-
86
- const next = [...existing.starredDefinitions, definitionId];
87
- const { data, error } = await this.client()
88
- .from('user_profiles')
89
- .update({ starred_definitions: next })
90
- .eq('user_id', userId)
91
- .select('user_id');
92
- if (error) throw mapError(error);
93
- return data && data.length > 0 ? 'ok' : 'not_found';
94
- }
95
-
96
- async unstarDefinition(
97
- ctx: RequestContext,
98
- userId: string,
99
- definitionId: string
100
- ): Promise<UserManagementResult> {
101
- assertCanAccess(ctx, userId);
102
- const existing = await this.getProfile(ctx, userId);
103
- if (!existing) return 'not_found';
104
- if (!existing.starredDefinitions.includes(definitionId)) return 'ok';
105
-
106
- const next = existing.starredDefinitions.filter((id) => id !== definitionId);
107
- const { data, error } = await this.client()
108
- .from('user_profiles')
109
- .update({ starred_definitions: next })
110
- .eq('user_id', userId)
111
- .select('user_id');
112
- if (error) throw mapError(error);
113
- return data && data.length > 0 ? 'ok' : 'not_found';
114
- }
115
-
116
- async recordRun(
117
- ctx: RequestContext,
118
- userId: string,
119
- run: RecentRun
120
- ): Promise<UserManagementResult> {
121
- assertCanAccess(ctx, userId);
122
- const existing = await this.getProfile(ctx, userId);
123
- if (!existing) return 'not_found';
124
-
125
- // Prepend newest, de-dupe by definitionId, cap at MAX_RECENT_RUNS.
126
- const filtered = existing.recentRuns.filter((r) => r.definitionId !== run.definitionId);
127
- const next = [run, ...filtered].slice(0, MAX_RECENT_RUNS);
128
-
129
- const { data, error } = await this.client()
130
- .from('user_profiles')
131
- .update({ recent_runs: next })
132
- .eq('user_id', userId)
133
- .select('user_id');
134
- if (error) throw mapError(error);
135
- return data && data.length > 0 ? 'ok' : 'not_found';
136
- }
137
- }
138
-
139
- function assertCanAccess(ctx: RequestContext, userId: string): void {
140
- if (ctx.system) return;
141
- if (ctx.userId === userId) return;
142
- if (hasPermission(ctx, 'instance_admin')) return;
143
- throw new ProviderError('Forbidden: cannot access another user’s profile', 403);
144
- }
145
-
146
- interface ProfileRow {
147
- user_id: string;
148
- display_name: string | null;
149
- starred_definitions: string[] | null;
150
- recent_runs: RecentRun[] | null;
151
- }
152
-
153
- function rowToProfile(row: ProfileRow): UserProfile {
154
- return {
155
- userId: row.user_id,
156
- displayName: row.display_name ?? undefined,
157
- starredDefinitions: row.starred_definitions ?? [],
158
- recentRuns: row.recent_runs ?? []
159
- };
160
- }
161
-
162
- interface PostgrestError {
163
- code?: string;
164
- message?: string;
165
- }
166
-
167
- function mapError(e: unknown): Error {
168
- const pg = e as PostgrestError;
169
- if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
170
- if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
171
- if (e instanceof Error) return e;
172
- if (e && typeof e === 'object') {
173
- const obj = e as { message?: string; code?: string };
174
- return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
175
- }
176
- return new Error(String(e));
177
- }
@@ -1,39 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runUserProfileStoreConformance } from '@selvajs/platform/testing';
3
- import type { AuthUser } from '@selvajs/platform';
4
- import { SupabaseUserProfileProvider } from '../SupabaseUserProfileProvider.js';
5
- import { readEnv, resetAllData } from '../../data/__tests__/test-helpers.js';
6
-
7
- const envCtx = readEnv();
8
-
9
- if (!envCtx) {
10
- describe.skip('SupabaseUserProfileProvider (skipped: no live stack)', () => {
11
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
12
- });
13
- } else {
14
- describe('SupabaseUserProfileProvider', () => {
15
- beforeEach(async () => {
16
- await resetAllData(envCtx);
17
- });
18
-
19
- runUserProfileStoreConformance({
20
- name: 'SupabaseUserProfileProvider',
21
- createStore: async () => {
22
- const store = new SupabaseUserProfileProvider(envCtx.bundle);
23
- const seedUser = async (email: string): Promise<AuthUser> => {
24
- const { data, error } = await envCtx.adminClient.auth.admin.createUser({
25
- email,
26
- password: 'conformance-test-password-1234',
27
- email_confirm: true
28
- });
29
- if (error) throw error;
30
- return {
31
- id: data.user.id,
32
- email: data.user.email
33
- };
34
- };
35
- return { store, seedUser };
36
- }
37
- });
38
- });
39
- }
@@ -1 +0,0 @@
1
- export { SupabaseUserProfileProvider } from './SupabaseUserProfileProvider.js';