@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,177 @@
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
+ }
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1 @@
1
+ export { SupabaseUserProfileProvider } from './SupabaseUserProfileProvider.js';
@@ -0,0 +1 @@
1
+ main
@@ -0,0 +1 @@
1
+ v2.95.4
@@ -0,0 +1,68 @@
1
+ # Supabase local stack for @selvajs/supabase-provider.
2
+ # Run from the package root: `npx supabase start` (Docker Desktop required).
3
+ # The output prints SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY
4
+ # — copy those into `.env.test` for the conformance tests.
5
+
6
+ project_id = "selva-supabase-provider"
7
+
8
+ [api]
9
+ enabled = true
10
+ port = 54321
11
+ # Exposed REST schemas. `public` is where all Selva tables live; `storage`
12
+ # stays private (only the storage-js client reaches it via the dedicated path).
13
+ schemas = ["public", "graphql_public"]
14
+ extra_search_path = ["public", "extensions"]
15
+ max_rows = 1000
16
+
17
+ [db]
18
+ port = 54322
19
+ shadow_port = 54320
20
+ major_version = 15
21
+
22
+ [db.pooler]
23
+ enabled = false
24
+
25
+ [db.seed]
26
+ enabled = true
27
+ sql_paths = ["./seed.sql"]
28
+
29
+ [studio]
30
+ enabled = true
31
+ port = 54323
32
+
33
+ [inbucket]
34
+ enabled = true
35
+ port = 54324
36
+
37
+ [storage]
38
+ enabled = true
39
+ file_size_limit = "50MiB"
40
+
41
+ [auth]
42
+ enabled = true
43
+ site_url = "http://127.0.0.1:54321"
44
+ additional_redirect_urls = ["http://127.0.0.1:5173"]
45
+ jwt_expiry = 3600
46
+ enable_signup = true
47
+ enable_anonymous_sign_ins = false
48
+ enable_manual_linking = false
49
+ minimum_password_length = 8
50
+
51
+ [auth.email]
52
+ enable_signup = true
53
+ double_confirm_changes = false
54
+ enable_confirmations = false
55
+ secure_password_change = false
56
+ max_frequency = "1s"
57
+
58
+ [auth.sms]
59
+ enable_signup = false
60
+ enable_confirmations = false
61
+
62
+ [edge_runtime]
63
+ enabled = false
64
+
65
+ [analytics]
66
+ enabled = false
67
+
68
+ [experimental]