@selvajs/supabase-provider 0.14.3 → 0.14.4-beta.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.
- package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +116 -4
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/auth/__tests__/token-verification.test.d.ts +13 -0
- package/dist/auth/__tests__/token-verification.test.d.ts.map +1 -0
- package/dist/auth/__tests__/token-verification.test.js +135 -0
- package/dist/auth/__tests__/token-verification.test.js.map +1 -0
- package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +146 -39
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDataProvider.d.ts +5 -2
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
- package/dist/data/SupabaseDataProvider.js +24 -18
- package/dist/data/SupabaseDataProvider.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.d.ts +2 -1
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +68 -67
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
- package/dist/data/SupabaseInviteStore.js +11 -22
- package/dist/data/SupabaseInviteStore.js.map +1 -1
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
- package/dist/data/SupabaseOrgStore.js +49 -86
- package/dist/data/SupabaseOrgStore.js.map +1 -1
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
- package/dist/data/SupabaseProjectStore.js +51 -70
- package/dist/data/SupabaseProjectStore.js.map +1 -1
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
- package/dist/data/SupabaseShareLinkStore.js +13 -27
- package/dist/data/SupabaseShareLinkStore.js.map +1 -1
- package/dist/data/__tests__/compute-server-conformance.test.js +6 -1
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -1
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +12 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.js +143 -0
- package/dist/data/__tests__/compute-server-encryption.test.js.map +1 -0
- package/dist/data/__tests__/share-link-concurrency.test.d.ts +22 -0
- package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +1 -0
- package/dist/data/__tests__/share-link-concurrency.test.js +115 -0
- package/dist/data/__tests__/share-link-concurrency.test.js.map +1 -0
- package/dist/data/client.d.ts +35 -3
- package/dist/data/client.d.ts.map +1 -1
- package/dist/data/client.js +67 -16
- package/dist/data/client.js.map +1 -1
- package/dist/data/errors.d.ts +2 -0
- package/dist/data/errors.d.ts.map +1 -0
- package/dist/data/errors.js +23 -0
- package/dist/data/errors.js.map +1 -0
- package/dist/data/index.d.ts +7 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +5 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/query.d.ts +20 -0
- package/dist/data/query.d.ts.map +1 -0
- package/dist/data/query.js +13 -0
- package/dist/data/query.js.map +1 -0
- package/dist/data/rowStamp.d.ts +38 -0
- package/dist/data/rowStamp.d.ts.map +1 -0
- package/dist/data/rowStamp.js +43 -0
- package/dist/data/rowStamp.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/SupabaseAuthProvider.ts +140 -4
- package/src/auth/__tests__/token-verification.test.ts +158 -0
- package/src/data/SupabaseComputeServerStore.ts +163 -43
- package/src/data/SupabaseDataProvider.ts +35 -15
- package/src/data/SupabaseDefinitionStore.ts +73 -66
- package/src/data/SupabaseInviteStore.ts +13 -25
- package/src/data/SupabaseOrgStore.ts +51 -95
- package/src/data/SupabaseProjectStore.ts +55 -78
- package/src/data/SupabaseShareLinkStore.ts +15 -30
- package/src/data/__tests__/compute-server-conformance.test.ts +7 -1
- package/src/data/__tests__/compute-server-encryption.test.ts +173 -0
- package/src/data/__tests__/share-link-concurrency.test.ts +124 -0
- package/src/data/client.ts +95 -20
- package/src/data/errors.ts +45 -0
- package/src/data/index.ts +19 -2
- package/src/data/query.ts +22 -0
- package/src/data/rowStamp.ts +48 -0
- package/src/index.ts +20 -2
- package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
|
@@ -12,9 +12,18 @@ import type {
|
|
|
12
12
|
Page,
|
|
13
13
|
UISchema
|
|
14
14
|
} from '@selvajs/platform';
|
|
15
|
-
import { ProviderError,
|
|
15
|
+
import { ProviderError, actorFrom, NoopEventSink } from '@selvajs/platform';
|
|
16
16
|
import type { ClientBundle } from './client.js';
|
|
17
|
+
import { mapPostgrestError } from './errors.js';
|
|
17
18
|
import { nextCursorFromRange, toRange } from './pagination.js';
|
|
19
|
+
import { stampSoftDelete, stampUpdate } from './rowStamp.js';
|
|
20
|
+
|
|
21
|
+
/** Explicit column list for `definitions` — every field `rowToRecord` consumes. */
|
|
22
|
+
const DEFINITION_COLUMNS =
|
|
23
|
+
'guid, project_id, owner_id, created_by, updated_by, compute_server_id, display_name, description, category, tags, cover_image, status, run_count, live_version_id, draft_version_id, created_at, updated_at, deleted_at';
|
|
24
|
+
/** Explicit column list for `definition_versions` — every field `rowToVersion` consumes. */
|
|
25
|
+
const DEFINITION_VERSION_COLUMNS =
|
|
26
|
+
'id, definition_guid, version_number, file_ext, file_key, original_filename, uploaded_by, uploaded_at, change_note, schema, schema_extracted_at';
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
29
|
* Definition metadata + version store backed by Postgres. Spec §6 versioning:
|
|
@@ -23,7 +32,7 @@ import { nextCursorFromRange, toRange } from './pagination.js';
|
|
|
23
32
|
*
|
|
24
33
|
* Deletion protection (§6) is enforced by FK constraints — `live_version_id`
|
|
25
34
|
* and `draft_version_id` are ON DELETE RESTRICT. Trying to delete a
|
|
26
|
-
* referenced version raises 23503 which `
|
|
35
|
+
* referenced version raises 23503 which `mapPostgrestError` turns into a 409.
|
|
27
36
|
*
|
|
28
37
|
* `incrementSolveCount` uses a SQL function for atomic UPDATE — no
|
|
29
38
|
* read-modify-write race like the local provider has.
|
|
@@ -69,27 +78,27 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
69
78
|
const range = toRange(opts);
|
|
70
79
|
const direction = opts?.orderDir ?? 'desc';
|
|
71
80
|
|
|
81
|
+
// The public path embeds `project` to filter on its visibility/org; the
|
|
82
|
+
// non-public path selects the definition columns alone. Pick the select
|
|
83
|
+
// string up front so there's a single `.select()` call and the builder
|
|
84
|
+
// keeps one type across both branches.
|
|
85
|
+
const selectColumns = filter?.publicOnly
|
|
86
|
+
? `${DEFINITION_COLUMNS}, project:projects!inner(visibility, org_id)`
|
|
87
|
+
: DEFINITION_COLUMNS;
|
|
88
|
+
|
|
72
89
|
let query = this.clients
|
|
73
90
|
.forRequest(ctx)
|
|
74
91
|
.from('definitions')
|
|
75
|
-
.select(
|
|
92
|
+
.select(selectColumns, { count: 'exact' })
|
|
76
93
|
.is('deleted_at', null);
|
|
77
94
|
|
|
78
|
-
if (filter?.projectId) query = query.eq('project_id', filter.projectId);
|
|
79
|
-
|
|
80
95
|
if (filter?.publicOnly) {
|
|
81
|
-
query =
|
|
82
|
-
.forRequest(ctx)
|
|
83
|
-
.from('definitions')
|
|
84
|
-
.select('*, project:projects!inner(visibility, org_id)', {
|
|
85
|
-
count: 'exact'
|
|
86
|
-
})
|
|
87
|
-
.is('deleted_at', null)
|
|
88
|
-
.eq('project.visibility', 'public');
|
|
96
|
+
query = query.eq('project.visibility', 'public');
|
|
89
97
|
if (filter.orgId) query = query.eq('project.org_id', filter.orgId);
|
|
90
|
-
if (filter.projectId) query = query.eq('project_id', filter.projectId);
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
if (filter?.projectId) query = query.eq('project_id', filter.projectId);
|
|
101
|
+
|
|
93
102
|
if (opts?.statuses?.length) {
|
|
94
103
|
query = query.in('status', opts.statuses);
|
|
95
104
|
} else {
|
|
@@ -106,8 +115,11 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
106
115
|
.range(range.from, range.to);
|
|
107
116
|
|
|
108
117
|
const { data, error, count } = await query;
|
|
109
|
-
if (error) throw
|
|
110
|
-
|
|
118
|
+
if (error) throw mapPostgrestError(error);
|
|
119
|
+
// `data` is typed against the dynamic select string, which the PostgREST
|
|
120
|
+
// parser can't resolve to a row shape; the definition columns are all
|
|
121
|
+
// present regardless, so map through the known `DefinitionRow`.
|
|
122
|
+
const items = ((data ?? []) as unknown as DefinitionRow[]).map(rowToRecord);
|
|
111
123
|
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -115,11 +127,11 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
115
127
|
const { data, error } = await this.clients
|
|
116
128
|
.forRequest(ctx)
|
|
117
129
|
.from('definitions')
|
|
118
|
-
.select(
|
|
130
|
+
.select(DEFINITION_COLUMNS)
|
|
119
131
|
.eq('guid', guid)
|
|
120
132
|
.is('deleted_at', null)
|
|
121
133
|
.maybeSingle();
|
|
122
|
-
if (error) throw
|
|
134
|
+
if (error) throw mapPostgrestError(error);
|
|
123
135
|
return data ? rowToRecord(data) : null;
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -128,7 +140,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
128
140
|
.forRequest(ctx)
|
|
129
141
|
.from('definitions')
|
|
130
142
|
.insert(recordToRow(record));
|
|
131
|
-
if (error) throw
|
|
143
|
+
if (error) throw mapPostgrestError(error);
|
|
132
144
|
await this.events.emit({
|
|
133
145
|
type: 'definition.created',
|
|
134
146
|
definitionId: record.guid,
|
|
@@ -140,7 +152,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
140
152
|
async update(ctx: RequestContext, guid: string, patch: DefinitionRecordPatch): Promise<void> {
|
|
141
153
|
const row = patchToRow(patch);
|
|
142
154
|
if (Object.keys(row).length === 0) return;
|
|
143
|
-
|
|
155
|
+
Object.assign(row, stampUpdate(ctx));
|
|
144
156
|
|
|
145
157
|
const { data, error } = await this.clients
|
|
146
158
|
.forRequest(ctx)
|
|
@@ -149,7 +161,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
149
161
|
.eq('guid', guid)
|
|
150
162
|
.is('deleted_at', null)
|
|
151
163
|
.select('guid');
|
|
152
|
-
if (error) throw
|
|
164
|
+
if (error) throw mapPostgrestError(error);
|
|
153
165
|
if (!data || data.length === 0) throw new ProviderError(`Definition '${guid}' not found`, 404);
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -157,19 +169,13 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
157
169
|
// Soft-delete (matches the local provider and spec §9). A retention
|
|
158
170
|
// sweep running as service-role hard-deletes later, which cascades
|
|
159
171
|
// versions + share_links via FK.
|
|
160
|
-
const stamp = auditSoftDelete(ctx, ctx.userId);
|
|
161
|
-
const row: Record<string, unknown> = {
|
|
162
|
-
deleted_at: stamp.deletedAt,
|
|
163
|
-
updated_at: stamp.updatedAt
|
|
164
|
-
};
|
|
165
|
-
if (stamp.updatedBy) row.updated_by = stamp.updatedBy;
|
|
166
172
|
const { error } = await this.clients
|
|
167
173
|
.forRequest(ctx)
|
|
168
174
|
.from('definitions')
|
|
169
|
-
.update(
|
|
175
|
+
.update(stampSoftDelete(ctx))
|
|
170
176
|
.eq('guid', guid)
|
|
171
177
|
.is('deleted_at', null);
|
|
172
|
-
if (error) throw
|
|
178
|
+
if (error) throw mapPostgrestError(error);
|
|
173
179
|
await this.events.emit({
|
|
174
180
|
type: 'definition.deleted',
|
|
175
181
|
definitionId: guid,
|
|
@@ -177,9 +183,31 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
177
183
|
});
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
async deleteByProject(ctx: RequestContext, projectId: string): Promise<void> {
|
|
187
|
+
// Cascade from `deleteProject` — soft-delete every live definition in the
|
|
188
|
+
// project in one statement. `select('guid')` returns the affected rows so
|
|
189
|
+
// we can emit a `definition.deleted` per record (parity with the local
|
|
190
|
+
// provider and the single-`delete` path above).
|
|
191
|
+
const { data, error } = await this.clients
|
|
192
|
+
.forRequest(ctx)
|
|
193
|
+
.from('definitions')
|
|
194
|
+
.update(stampSoftDelete(ctx))
|
|
195
|
+
.eq('project_id', projectId)
|
|
196
|
+
.is('deleted_at', null)
|
|
197
|
+
.select('guid');
|
|
198
|
+
if (error) throw mapPostgrestError(error);
|
|
199
|
+
for (const row of data ?? []) {
|
|
200
|
+
await this.events.emit({
|
|
201
|
+
type: 'definition.deleted',
|
|
202
|
+
definitionId: row.guid,
|
|
203
|
+
actorId: actorFrom(ctx)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
async incrementSolveCount(ctx: RequestContext, guid: string): Promise<void> {
|
|
181
209
|
const { error } = await this.clients.forRequest(ctx).rpc('increment_run_count', { g: guid });
|
|
182
|
-
if (error) throw
|
|
210
|
+
if (error) throw mapPostgrestError(error);
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
// ============================================================================
|
|
@@ -191,7 +219,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
191
219
|
.forRequest(ctx)
|
|
192
220
|
.from('definition_versions')
|
|
193
221
|
.insert(versionToRow(version));
|
|
194
|
-
if (error) throw
|
|
222
|
+
if (error) throw mapPostgrestError(error);
|
|
195
223
|
await this.events.emit({
|
|
196
224
|
type: 'definition_version.created',
|
|
197
225
|
versionId: version.id,
|
|
@@ -209,11 +237,11 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
209
237
|
const { data, error, count } = await this.clients
|
|
210
238
|
.forRequest(ctx)
|
|
211
239
|
.from('definition_versions')
|
|
212
|
-
.select(
|
|
240
|
+
.select(DEFINITION_VERSION_COLUMNS, { count: 'exact' })
|
|
213
241
|
.eq('definition_guid', definitionId)
|
|
214
242
|
.order('version_number', { ascending: false })
|
|
215
243
|
.range(range.from, range.to);
|
|
216
|
-
if (error) throw
|
|
244
|
+
if (error) throw mapPostgrestError(error);
|
|
217
245
|
const items = (data ?? []).map(rowToVersion);
|
|
218
246
|
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
219
247
|
}
|
|
@@ -222,10 +250,10 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
222
250
|
const { data, error } = await this.clients
|
|
223
251
|
.forRequest(ctx)
|
|
224
252
|
.from('definition_versions')
|
|
225
|
-
.select(
|
|
253
|
+
.select(DEFINITION_VERSION_COLUMNS)
|
|
226
254
|
.eq('id', versionId)
|
|
227
255
|
.maybeSingle();
|
|
228
|
-
if (error) throw
|
|
256
|
+
if (error) throw mapPostgrestError(error);
|
|
229
257
|
return data ? rowToVersion(data) : null;
|
|
230
258
|
}
|
|
231
259
|
|
|
@@ -235,19 +263,19 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
235
263
|
.from('definition_versions')
|
|
236
264
|
.update({ schema, schema_extracted_at: new Date().toISOString() })
|
|
237
265
|
.eq('id', versionId);
|
|
238
|
-
if (error) throw
|
|
266
|
+
if (error) throw mapPostgrestError(error);
|
|
239
267
|
}
|
|
240
268
|
|
|
241
269
|
async deleteVersion(ctx: RequestContext, versionId: string): Promise<void> {
|
|
242
270
|
// FK enforcement at the DB layer: live_version_id / draft_version_id
|
|
243
271
|
// are ON DELETE RESTRICT. If the version is referenced by either
|
|
244
|
-
// channel, Postgres raises 23503 →
|
|
272
|
+
// channel, Postgres raises 23503 → mapPostgrestError → ProviderError(409).
|
|
245
273
|
const { error } = await this.clients
|
|
246
274
|
.forRequest(ctx)
|
|
247
275
|
.from('definition_versions')
|
|
248
276
|
.delete()
|
|
249
277
|
.eq('id', versionId);
|
|
250
|
-
if (error) throw
|
|
278
|
+
if (error) throw mapPostgrestError(error);
|
|
251
279
|
await this.events.emit({
|
|
252
280
|
type: 'definition_version.deleted',
|
|
253
281
|
versionId,
|
|
@@ -293,7 +321,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
293
321
|
.select('id, definition_guid')
|
|
294
322
|
.eq('id', versionId)
|
|
295
323
|
.maybeSingle();
|
|
296
|
-
if (vError) throw
|
|
324
|
+
if (vError) throw mapPostgrestError(vError);
|
|
297
325
|
if (!version || version.definition_guid !== definitionId) {
|
|
298
326
|
throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
|
|
299
327
|
}
|
|
@@ -303,9 +331,9 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
303
331
|
const row: Record<string, unknown> = {
|
|
304
332
|
live_version_id: versionId,
|
|
305
333
|
draft_version_id: versionId,
|
|
306
|
-
status: 'draft'
|
|
334
|
+
status: 'draft',
|
|
335
|
+
...stampUpdate(ctx)
|
|
307
336
|
};
|
|
308
|
-
if (ctx.userId) row.updated_by = ctx.userId;
|
|
309
337
|
|
|
310
338
|
const { data, error } = await client
|
|
311
339
|
.from('definitions')
|
|
@@ -313,7 +341,7 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
313
341
|
.eq('guid', definitionId)
|
|
314
342
|
.is('deleted_at', null)
|
|
315
343
|
.select('guid');
|
|
316
|
-
if (error) throw
|
|
344
|
+
if (error) throw mapPostgrestError(error);
|
|
317
345
|
if (!data || data.length === 0) {
|
|
318
346
|
throw new ProviderError(`Definition '${definitionId}' not found`, 404);
|
|
319
347
|
}
|
|
@@ -333,20 +361,19 @@ export class SupabaseDefinitionStore implements IDefinitionStore {
|
|
|
333
361
|
.select('id, definition_guid')
|
|
334
362
|
.eq('id', versionId)
|
|
335
363
|
.maybeSingle();
|
|
336
|
-
if (vError) throw
|
|
364
|
+
if (vError) throw mapPostgrestError(vError);
|
|
337
365
|
if (!version || version.definition_guid !== definitionId) {
|
|
338
366
|
throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
|
|
339
367
|
}
|
|
340
368
|
|
|
341
|
-
const row: Record<string, unknown> = { [column]: versionId };
|
|
342
|
-
if (ctx.userId) row.updated_by = ctx.userId;
|
|
369
|
+
const row: Record<string, unknown> = { [column]: versionId, ...stampUpdate(ctx) };
|
|
343
370
|
const { data, error } = await client
|
|
344
371
|
.from('definitions')
|
|
345
372
|
.update(row)
|
|
346
373
|
.eq('guid', definitionId)
|
|
347
374
|
.is('deleted_at', null)
|
|
348
375
|
.select('guid');
|
|
349
|
-
if (error) throw
|
|
376
|
+
if (error) throw mapPostgrestError(error);
|
|
350
377
|
if (!data || data.length === 0) {
|
|
351
378
|
throw new ProviderError(`Definition '${definitionId}' not found`, 404);
|
|
352
379
|
}
|
|
@@ -498,23 +525,3 @@ function definitionOrderColumn(orderBy: DefinitionListOptions['orderBy'] | undef
|
|
|
498
525
|
return 'created_at';
|
|
499
526
|
}
|
|
500
527
|
}
|
|
501
|
-
|
|
502
|
-
interface PostgrestError {
|
|
503
|
-
code?: string;
|
|
504
|
-
message?: string;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function mapError(e: unknown): Error {
|
|
508
|
-
const pg = e as PostgrestError;
|
|
509
|
-
if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
|
|
510
|
-
if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
|
|
511
|
-
if (e instanceof Error) return e;
|
|
512
|
-
if (e && typeof e === 'object') {
|
|
513
|
-
const obj = e as { message?: string; details?: string; hint?: string; code?: string };
|
|
514
|
-
const msg = obj.message ?? obj.details ?? obj.hint ?? 'Unknown Postgres error';
|
|
515
|
-
const err = new Error(obj.code ? `[${obj.code}] ${msg}` : msg);
|
|
516
|
-
Object.assign(err, obj);
|
|
517
|
-
return err;
|
|
518
|
-
}
|
|
519
|
-
return new Error(String(e));
|
|
520
|
-
}
|
|
@@ -6,10 +6,15 @@ import type {
|
|
|
6
6
|
ListOptions,
|
|
7
7
|
Page
|
|
8
8
|
} from '@selvajs/platform';
|
|
9
|
-
import { NoopEventSink,
|
|
9
|
+
import { NoopEventSink, actorFrom } from '@selvajs/platform';
|
|
10
10
|
import type { ClientBundle } from './client.js';
|
|
11
|
+
import { mapPostgrestError } from './errors.js';
|
|
11
12
|
import { nextCursorFromRange, toRange } from './pagination.js';
|
|
12
13
|
|
|
14
|
+
/** Explicit column list for `invites` — every field `rowToInvite` consumes. */
|
|
15
|
+
const INVITE_COLUMNS =
|
|
16
|
+
'id, token_hash, email, org_id, org_role, org_permissions, invited_by, created_at, expires_at, accepted_at, accepted_by_user_id';
|
|
17
|
+
|
|
13
18
|
/**
|
|
14
19
|
* Invite store. `getByTokenHash` routes through a SECURITY DEFINER RPC so
|
|
15
20
|
* unauthenticated / just-signed-up callers can validate and consume the
|
|
@@ -29,7 +34,7 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
29
34
|
.forRequest(ctx)
|
|
30
35
|
.from('invites')
|
|
31
36
|
.insert(inviteToRow(invite));
|
|
32
|
-
if (error) throw
|
|
37
|
+
if (error) throw mapPostgrestError(error);
|
|
33
38
|
await this.events.emit({
|
|
34
39
|
type: 'invite.created',
|
|
35
40
|
inviteId: invite.id,
|
|
@@ -47,7 +52,7 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
47
52
|
const { data, error } = await this.clients
|
|
48
53
|
.forRequest(ctx)
|
|
49
54
|
.rpc('get_invite_by_token_hash', { h: tokenHash });
|
|
50
|
-
if (error) throw
|
|
55
|
+
if (error) throw mapPostgrestError(error);
|
|
51
56
|
// rpc returns the matching row (or null). PostgREST may wrap it in
|
|
52
57
|
// an array depending on the signature — normalize both shapes.
|
|
53
58
|
const row = Array.isArray(data) ? data[0] : data;
|
|
@@ -59,11 +64,11 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
59
64
|
const { data, error, count } = await this.clients
|
|
60
65
|
.forRequest(ctx)
|
|
61
66
|
.from('invites')
|
|
62
|
-
.select(
|
|
67
|
+
.select(INVITE_COLUMNS, { count: 'exact' })
|
|
63
68
|
.eq('org_id', orgId)
|
|
64
69
|
.order('created_at', { ascending: (opts?.orderDir ?? 'desc') === 'asc' })
|
|
65
70
|
.range(range.from, range.to);
|
|
66
|
-
if (error) throw
|
|
71
|
+
if (error) throw mapPostgrestError(error);
|
|
67
72
|
const items = (data ?? []).map(rowToInvite);
|
|
68
73
|
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
69
74
|
}
|
|
@@ -78,7 +83,7 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
78
83
|
.eq('id', id)
|
|
79
84
|
.is('accepted_at', null)
|
|
80
85
|
.select('org_id');
|
|
81
|
-
if (error) throw
|
|
86
|
+
if (error) throw mapPostgrestError(error);
|
|
82
87
|
const row = data?.[0];
|
|
83
88
|
if (!row) return;
|
|
84
89
|
await this.events.emit({
|
|
@@ -98,7 +103,7 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
98
103
|
.eq('id', id)
|
|
99
104
|
.is('accepted_at', null)
|
|
100
105
|
.select('org_id');
|
|
101
|
-
if (error) throw
|
|
106
|
+
if (error) throw mapPostgrestError(error);
|
|
102
107
|
const row = data?.[0];
|
|
103
108
|
if (!row) return;
|
|
104
109
|
await this.events.emit({
|
|
@@ -115,7 +120,7 @@ export class SupabaseInviteStore implements IInviteStore {
|
|
|
115
120
|
.from('invites')
|
|
116
121
|
.delete()
|
|
117
122
|
.eq('org_id', orgId);
|
|
118
|
-
if (error) throw
|
|
123
|
+
if (error) throw mapPostgrestError(error);
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
@@ -164,20 +169,3 @@ function inviteToRow(i: Invite): InviteRow {
|
|
|
164
169
|
accepted_by_user_id: i.acceptedByUserId ?? null
|
|
165
170
|
};
|
|
166
171
|
}
|
|
167
|
-
|
|
168
|
-
interface PostgrestError {
|
|
169
|
-
code?: string;
|
|
170
|
-
message?: string;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function mapError(e: unknown): Error {
|
|
174
|
-
const pg = e as PostgrestError;
|
|
175
|
-
if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
|
|
176
|
-
if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
|
|
177
|
-
if (e instanceof Error) return e;
|
|
178
|
-
if (e && typeof e === 'object') {
|
|
179
|
-
const obj = e as { message?: string; code?: string };
|
|
180
|
-
return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
|
|
181
|
-
}
|
|
182
|
-
return new Error(String(e));
|
|
183
|
-
}
|