@selvajs/supabase-provider 0.14.2 → 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.
Files changed (94) hide show
  1. package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
  2. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  3. package/dist/auth/SupabaseAuthProvider.js +116 -4
  4. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  5. package/dist/auth/__tests__/token-verification.test.d.ts +13 -0
  6. package/dist/auth/__tests__/token-verification.test.d.ts.map +1 -0
  7. package/dist/auth/__tests__/token-verification.test.js +135 -0
  8. package/dist/auth/__tests__/token-verification.test.js.map +1 -0
  9. package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
  10. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  11. package/dist/data/SupabaseComputeServerStore.js +146 -39
  12. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  13. package/dist/data/SupabaseDataProvider.d.ts +5 -2
  14. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  15. package/dist/data/SupabaseDataProvider.js +24 -18
  16. package/dist/data/SupabaseDataProvider.js.map +1 -1
  17. package/dist/data/SupabaseDefinitionStore.d.ts +2 -1
  18. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  19. package/dist/data/SupabaseDefinitionStore.js +68 -67
  20. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  21. package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
  22. package/dist/data/SupabaseInviteStore.js +11 -22
  23. package/dist/data/SupabaseInviteStore.js.map +1 -1
  24. package/dist/data/SupabaseOrgStore.d.ts +1 -1
  25. package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
  26. package/dist/data/SupabaseOrgStore.js +54 -86
  27. package/dist/data/SupabaseOrgStore.js.map +1 -1
  28. package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
  29. package/dist/data/SupabaseProjectStore.js +51 -70
  30. package/dist/data/SupabaseProjectStore.js.map +1 -1
  31. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
  32. package/dist/data/SupabaseShareLinkStore.js +13 -27
  33. package/dist/data/SupabaseShareLinkStore.js.map +1 -1
  34. package/dist/data/__tests__/compute-server-conformance.test.js +6 -1
  35. package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -1
  36. package/dist/data/__tests__/compute-server-encryption.test.d.ts +12 -0
  37. package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
  38. package/dist/data/__tests__/compute-server-encryption.test.js +143 -0
  39. package/dist/data/__tests__/compute-server-encryption.test.js.map +1 -0
  40. package/dist/data/__tests__/share-link-concurrency.test.d.ts +22 -0
  41. package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +1 -0
  42. package/dist/data/__tests__/share-link-concurrency.test.js +115 -0
  43. package/dist/data/__tests__/share-link-concurrency.test.js.map +1 -0
  44. package/dist/data/client.d.ts +35 -3
  45. package/dist/data/client.d.ts.map +1 -1
  46. package/dist/data/client.js +67 -16
  47. package/dist/data/client.js.map +1 -1
  48. package/dist/data/errors.d.ts +2 -0
  49. package/dist/data/errors.d.ts.map +1 -0
  50. package/dist/data/errors.js +23 -0
  51. package/dist/data/errors.js.map +1 -0
  52. package/dist/data/index.d.ts +7 -2
  53. package/dist/data/index.d.ts.map +1 -1
  54. package/dist/data/index.js +5 -1
  55. package/dist/data/index.js.map +1 -1
  56. package/dist/data/query.d.ts +20 -0
  57. package/dist/data/query.d.ts.map +1 -0
  58. package/dist/data/query.js +13 -0
  59. package/dist/data/query.js.map +1 -0
  60. package/dist/data/rowStamp.d.ts +38 -0
  61. package/dist/data/rowStamp.d.ts.map +1 -0
  62. package/dist/data/rowStamp.js +43 -0
  63. package/dist/data/rowStamp.js.map +1 -0
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/storage/SupabaseStorageProvider.d.ts +7 -5
  69. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  70. package/dist/storage/SupabaseStorageProvider.js +14 -7
  71. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/auth/SupabaseAuthProvider.ts +140 -4
  74. package/src/auth/__tests__/token-verification.test.ts +158 -0
  75. package/src/data/SupabaseComputeServerStore.ts +163 -43
  76. package/src/data/SupabaseDataProvider.ts +35 -15
  77. package/src/data/SupabaseDefinitionStore.ts +73 -66
  78. package/src/data/SupabaseInviteStore.ts +13 -25
  79. package/src/data/SupabaseOrgStore.ts +57 -96
  80. package/src/data/SupabaseProjectStore.ts +55 -78
  81. package/src/data/SupabaseShareLinkStore.ts +15 -30
  82. package/src/data/__tests__/compute-server-conformance.test.ts +7 -1
  83. package/src/data/__tests__/compute-server-encryption.test.ts +173 -0
  84. package/src/data/__tests__/share-link-concurrency.test.ts +124 -0
  85. package/src/data/client.ts +95 -20
  86. package/src/data/errors.ts +45 -0
  87. package/src/data/index.ts +19 -2
  88. package/src/data/query.ts +22 -0
  89. package/src/data/rowStamp.ts +48 -0
  90. package/src/index.ts +20 -2
  91. package/src/storage/SupabaseStorageProvider.ts +12 -7
  92. package/supabase/config.toml +6 -6
  93. package/supabase/migrations/20260630120000_org_assets.sql +19 -0
  94. package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
@@ -12,12 +12,20 @@ import type {
12
12
  import {
13
13
  DEFAULT_ORG_PERMISSIONS,
14
14
  ProviderError,
15
- auditSoftDelete,
16
15
  actorFrom,
17
16
  NoopEventSink
18
17
  } from '@selvajs/platform';
19
18
  import type { ClientBundle } from './client.js';
19
+ import { mapPostgrestError } from './errors.js';
20
20
  import { nextCursorFromRange, orderColumn, toRange } from './pagination.js';
21
+ import { stampSoftDelete, stampUpdate } from './rowStamp.js';
22
+
23
+ /** Explicit column list for `orgs` — every field `rowToOrg` consumes. */
24
+ const ORG_COLUMNS =
25
+ 'id, name, slug, owner_id, assets, created_by, updated_by, created_at, updated_at, deleted_at';
26
+ /** Explicit column list for `org_members` — every field `rowToOrgMember` consumes. */
27
+ const ORG_MEMBER_COLUMNS =
28
+ 'org_id, user_id, role, permissions, joined_at, updated_at, updated_by, deleted_at';
21
29
 
22
30
  /**
23
31
  * Org + org-membership store backed by Postgres. The queries rely on RLS
@@ -44,11 +52,11 @@ export class SupabaseOrgStore implements IOrgStore {
44
52
  const { data, error, count } = await this.clients
45
53
  .forRequest(ctx)
46
54
  .from('orgs')
47
- .select('*', { count: 'exact' })
55
+ .select(ORG_COLUMNS, { count: 'exact' })
48
56
  .is('deleted_at', null)
49
57
  .order(orderColumn(opts?.orderBy), { ascending: direction === 'asc' })
50
58
  .range(range.from, range.to);
51
- if (error) throw mapError(error);
59
+ if (error) throw mapPostgrestError(error);
52
60
  const items = (data ?? []).map(rowToOrg);
53
61
  return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
54
62
  }
@@ -57,11 +65,11 @@ export class SupabaseOrgStore implements IOrgStore {
57
65
  const { data, error } = await this.clients
58
66
  .forRequest(ctx)
59
67
  .from('orgs')
60
- .select('*')
68
+ .select(ORG_COLUMNS)
61
69
  .eq('id', id)
62
70
  .is('deleted_at', null)
63
71
  .maybeSingle();
64
- if (error) throw mapError(error);
72
+ if (error) throw mapPostgrestError(error);
65
73
  return data ? rowToOrg(data) : null;
66
74
  }
67
75
 
@@ -69,18 +77,18 @@ export class SupabaseOrgStore implements IOrgStore {
69
77
  const { data, error } = await this.clients
70
78
  .forRequest(ctx)
71
79
  .from('orgs')
72
- .select('*')
80
+ .select(ORG_COLUMNS)
73
81
  .eq('slug', slug)
74
82
  .is('deleted_at', null)
75
83
  .maybeSingle();
76
- if (error) throw mapError(error);
84
+ if (error) throw mapPostgrestError(error);
77
85
  return data ? rowToOrg(data) : null;
78
86
  }
79
87
 
80
88
  async createOrg(ctx: RequestContext, org: Organization): Promise<void> {
81
89
  const client = this.clients.forRequest(ctx);
82
90
  const { error } = await client.from('orgs').insert(orgToRow(org));
83
- if (error) throw mapError(error);
91
+ if (error) throw mapPostgrestError(error);
84
92
 
85
93
  // Seed owner membership so user-scoped queries can see the org post-create.
86
94
  // Service-role bypasses RLS, but the same code path runs for user-scoped
@@ -98,24 +106,25 @@ export class SupabaseOrgStore implements IOrgStore {
98
106
  },
99
107
  { onConflict: 'org_id,user_id' }
100
108
  );
101
- if (memberError) throw mapError(memberError);
109
+ if (memberError) throw mapPostgrestError(memberError);
102
110
  await this.events.emit({ type: 'org.created', orgId: org.id, actorId: actorFrom(ctx) });
103
111
  }
104
112
 
105
113
  async updateOrg(
106
114
  ctx: RequestContext,
107
115
  id: string,
108
- patch: Partial<Pick<Organization, 'name' | 'slug'>>
116
+ patch: Partial<Pick<Organization, 'name' | 'slug' | 'assets'>>
109
117
  ): Promise<void> {
110
118
  const row: Record<string, unknown> = {};
111
119
  if (patch.name !== undefined) row.name = patch.name;
112
120
  if (patch.slug !== undefined) row.slug = patch.slug;
121
+ // Callers pass the full merged map; store it wholesale (JSONB column).
122
+ if (patch.assets !== undefined) row.assets = patch.assets;
113
123
  if (Object.keys(row).length === 0) return;
114
- // `updated_at` is set by the trg_orgs_updated_at trigger; `updated_by`
115
- // is not, so stamp it here. ctx.userId can be empty in system contexts —
116
- // the DB FK is `on delete set null` so passing empty would fail; let it
117
- // fall back to whatever's already on the row by omitting in that case.
118
- if (ctx.userId) row.updated_by = ctx.userId;
124
+ // `updated_at` is set by the trg_orgs_updated_at trigger; `stampUpdate`
125
+ // only adds `updated_by`, and only when ctx.userId is set (empty on
126
+ // system contexts would violate the `on delete set null` FK).
127
+ Object.assign(row, stampUpdate(ctx));
119
128
 
120
129
  const { error, data } = await this.clients
121
130
  .forRequest(ctx)
@@ -124,7 +133,7 @@ export class SupabaseOrgStore implements IOrgStore {
124
133
  .eq('id', id)
125
134
  .is('deleted_at', null)
126
135
  .select('id');
127
- if (error) throw mapError(error);
136
+ if (error) throw mapPostgrestError(error);
128
137
  if (!data || data.length === 0) throw new ProviderError(`Org '${id}' not found`, 404);
129
138
  }
130
139
 
@@ -133,8 +142,7 @@ export class SupabaseOrgStore implements IOrgStore {
133
142
  // projects → project_members, definitions. Hard delete is reserved for
134
143
  // the background janitor; user-facing deletes preserve the audit trail.
135
144
  const client = this.clients.forRequest(ctx);
136
- const stamp = auditSoftDelete(ctx, ctx.userId);
137
- const stampRow = stampToRow(stamp);
145
+ const stampRow = stampSoftDelete(ctx);
138
146
 
139
147
  // Org itself.
140
148
  const { error: orgErr, data: orgData } = await client
@@ -143,7 +151,7 @@ export class SupabaseOrgStore implements IOrgStore {
143
151
  .eq('id', id)
144
152
  .is('deleted_at', null)
145
153
  .select('id');
146
- if (orgErr) throw mapError(orgErr);
154
+ if (orgErr) throw mapPostgrestError(orgErr);
147
155
  if (!orgData || orgData.length === 0) throw new ProviderError(`Org '${id}' not found`, 404);
148
156
 
149
157
  // Cascade: org_members.
@@ -152,7 +160,7 @@ export class SupabaseOrgStore implements IOrgStore {
152
160
  .update(stampRow)
153
161
  .eq('org_id', id)
154
162
  .is('deleted_at', null);
155
- if (omErr) throw mapError(omErr);
163
+ if (omErr) throw mapPostgrestError(omErr);
156
164
 
157
165
  // Cascade: projects in this org. Fetch IDs first so we can cascade to
158
166
  // project_members and definitions in app code (Postgres FK CASCADE only
@@ -162,7 +170,7 @@ export class SupabaseOrgStore implements IOrgStore {
162
170
  .select('id')
163
171
  .eq('org_id', id)
164
172
  .is('deleted_at', null);
165
- if (projFetchErr) throw mapError(projFetchErr);
173
+ if (projFetchErr) throw mapPostgrestError(projFetchErr);
166
174
  const projectIds = (orgProjects ?? []).map((p) => p.id as string);
167
175
 
168
176
  const { error: projErr } = await client
@@ -170,7 +178,7 @@ export class SupabaseOrgStore implements IOrgStore {
170
178
  .update(stampRow)
171
179
  .eq('org_id', id)
172
180
  .is('deleted_at', null);
173
- if (projErr) throw mapError(projErr);
181
+ if (projErr) throw mapPostgrestError(projErr);
174
182
 
175
183
  if (projectIds.length > 0) {
176
184
  const { error: pmErr } = await client
@@ -178,14 +186,14 @@ export class SupabaseOrgStore implements IOrgStore {
178
186
  .update(stampRow)
179
187
  .in('project_id', projectIds)
180
188
  .is('deleted_at', null);
181
- if (pmErr) throw mapError(pmErr);
189
+ if (pmErr) throw mapPostgrestError(pmErr);
182
190
 
183
191
  const { error: defErr } = await client
184
192
  .from('definitions')
185
193
  .update(stampRow)
186
194
  .in('project_id', projectIds)
187
195
  .is('deleted_at', null);
188
- if (defErr) throw mapError(defErr);
196
+ if (defErr) throw mapPostgrestError(defErr);
189
197
  }
190
198
 
191
199
  // Hard-delete tables that have no `deleted_at` column. SQL CASCADE on
@@ -193,23 +201,23 @@ export class SupabaseOrgStore implements IOrgStore {
193
201
  // clean these up here. Pending invites to a dead org are unredeemable;
194
202
  // stale compute config is operational state with no audit need.
195
203
  const { error: invErr } = await client.from('invites').delete().eq('org_id', id);
196
- if (invErr) throw mapError(invErr);
204
+ if (invErr) throw mapPostgrestError(invErr);
197
205
 
198
206
  const { error: cdErr } = await client
199
207
  .from('compute_server_org_defaults')
200
208
  .delete()
201
209
  .eq('org_id', id);
202
- if (cdErr) throw mapError(cdErr);
210
+ if (cdErr) throw mapPostgrestError(cdErr);
203
211
 
204
212
  const { error: shErr } = await client.from('compute_server_shares').delete().eq('org_id', id);
205
- if (shErr) throw mapError(shErr);
213
+ if (shErr) throw mapPostgrestError(shErr);
206
214
 
207
215
  const { error: csErr } = await client
208
216
  .from('compute_servers')
209
217
  .delete()
210
218
  .eq('scope', 'org')
211
219
  .eq('owner_org_id', id);
212
- if (csErr) throw mapError(csErr);
220
+ if (csErr) throw mapPostgrestError(csErr);
213
221
 
214
222
  await this.events.emit({ type: 'org.deleted', orgId: id, actorId: actorFrom(ctx) });
215
223
  }
@@ -226,12 +234,12 @@ export class SupabaseOrgStore implements IOrgStore {
226
234
  const { data, error, count } = await this.clients
227
235
  .forRequest(ctx)
228
236
  .from('org_members')
229
- .select('*', { count: 'exact' })
237
+ .select(ORG_MEMBER_COLUMNS, { count: 'exact' })
230
238
  .eq('org_id', orgId)
231
239
  .is('deleted_at', null)
232
240
  .order('joined_at', { ascending: (opts?.orderDir ?? 'desc') === 'asc' })
233
241
  .range(range.from, range.to);
234
- if (error) throw mapError(error);
242
+ if (error) throw mapPostgrestError(error);
235
243
  const items = (data ?? []).map(rowToOrgMember);
236
244
  return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
237
245
  }
@@ -244,12 +252,12 @@ export class SupabaseOrgStore implements IOrgStore {
244
252
  const { data, error } = await this.clients
245
253
  .forRequest(ctx)
246
254
  .from('org_members')
247
- .select('*')
255
+ .select(ORG_MEMBER_COLUMNS)
248
256
  .eq('org_id', orgId)
249
257
  .eq('user_id', userId)
250
258
  .is('deleted_at', null)
251
259
  .maybeSingle();
252
- if (error) throw mapError(error);
260
+ if (error) throw mapPostgrestError(error);
253
261
  return data ? rowToOrgMember(data) : null;
254
262
  }
255
263
 
@@ -264,14 +272,14 @@ export class SupabaseOrgStore implements IOrgStore {
264
272
  const { data, error } = await this.clients
265
273
  .forRequest(ctx)
266
274
  .from('org_members')
267
- .select('*, org:orgs!inner(*)')
275
+ .select(`${ORG_MEMBER_COLUMNS}, org:orgs!inner(${ORG_COLUMNS})`)
268
276
  .eq('user_id', userId)
269
277
  .is('deleted_at', null)
270
278
  .is('org.deleted_at', null)
271
279
  .order('joined_at', { ascending: true })
272
280
  .limit(1)
273
281
  .maybeSingle();
274
- if (error) throw mapError(error);
282
+ if (error) throw mapPostgrestError(error);
275
283
  if (!data) return null;
276
284
 
277
285
  // PostgREST embeds the related row as `org` per the alias above. Strip
@@ -291,14 +299,14 @@ export class SupabaseOrgStore implements IOrgStore {
291
299
  // a duplicate-key error. Mirrors LocalOrgStore.addOrgMember.
292
300
  const row: Record<string, unknown> = {
293
301
  ...memberToRow(member),
294
- deleted_at: null
302
+ deleted_at: null,
303
+ ...stampUpdate(ctx)
295
304
  };
296
- if (ctx.userId) row.updated_by = ctx.userId;
297
305
  const { error } = await this.clients
298
306
  .forRequest(ctx)
299
307
  .from('org_members')
300
308
  .upsert(row, { onConflict: 'org_id,user_id' });
301
- if (error) throw mapError(error);
309
+ if (error) throw mapPostgrestError(error);
302
310
  await this.events.emit({
303
311
  type: 'org_member.added',
304
312
  orgId: member.orgId,
@@ -316,9 +324,9 @@ export class SupabaseOrgStore implements IOrgStore {
316
324
  // Role change re-seeds default permissions. Matches LocalOrgStore.
317
325
  const row: Record<string, unknown> = {
318
326
  role,
319
- permissions: [...DEFAULT_ORG_PERMISSIONS[role]]
327
+ permissions: [...DEFAULT_ORG_PERMISSIONS[role]],
328
+ ...stampUpdate(ctx)
320
329
  };
321
- if (ctx.userId) row.updated_by = ctx.userId;
322
330
  const { data, error } = await this.clients
323
331
  .forRequest(ctx)
324
332
  .from('org_members')
@@ -327,7 +335,7 @@ export class SupabaseOrgStore implements IOrgStore {
327
335
  .eq('user_id', userId)
328
336
  .is('deleted_at', null)
329
337
  .select('user_id');
330
- if (error) throw mapError(error);
338
+ if (error) throw mapPostgrestError(error);
331
339
  if (!data || data.length === 0)
332
340
  throw new ProviderError(`Org member '${userId}' not found`, 404);
333
341
  await this.events.emit({
@@ -348,8 +356,7 @@ export class SupabaseOrgStore implements IOrgStore {
348
356
  // Replace permissions only. Matches LocalOrgStore.updateOrgMemberPermissions —
349
357
  // distinct from role change so callers can grant a finer-grained set without
350
358
  // re-seeding defaults from the role.
351
- const row: Record<string, unknown> = { permissions: [...permissions] };
352
- if (ctx.userId) row.updated_by = ctx.userId;
359
+ const row: Record<string, unknown> = { permissions: [...permissions], ...stampUpdate(ctx) };
353
360
  const { data, error } = await this.clients
354
361
  .forRequest(ctx)
355
362
  .from('org_members')
@@ -358,7 +365,7 @@ export class SupabaseOrgStore implements IOrgStore {
358
365
  .eq('user_id', userId)
359
366
  .is('deleted_at', null)
360
367
  .select('user_id');
361
- if (error) throw mapError(error);
368
+ if (error) throw mapPostgrestError(error);
362
369
  if (!data || data.length === 0)
363
370
  throw new ProviderError(`Org member '${userId}' not found`, 404);
364
371
  await this.events.emit({
@@ -377,15 +384,14 @@ export class SupabaseOrgStore implements IOrgStore {
377
384
  // cascade BEFORE the org_member update so RLS policies that gate
378
385
  // project_members on org_member existence still pass for the duration.
379
386
  const client = this.clients.forRequest(ctx);
380
- const stamp = auditSoftDelete(ctx, ctx.userId);
381
- const stampRow = stampToRow(stamp);
387
+ const stampRow = stampSoftDelete(ctx);
382
388
 
383
389
  const { data: orgProjects, error: projError } = await client
384
390
  .from('projects')
385
391
  .select('id')
386
392
  .eq('org_id', orgId)
387
393
  .is('deleted_at', null);
388
- if (projError) throw mapError(projError);
394
+ if (projError) throw mapPostgrestError(projError);
389
395
 
390
396
  const projectIds = (orgProjects ?? []).map((p) => p.id as string);
391
397
  if (projectIds.length > 0) {
@@ -395,7 +401,7 @@ export class SupabaseOrgStore implements IOrgStore {
395
401
  .in('project_id', projectIds)
396
402
  .eq('user_id', userId)
397
403
  .is('deleted_at', null);
398
- if (pmError) throw mapError(pmError);
404
+ if (pmError) throw mapPostgrestError(pmError);
399
405
  }
400
406
 
401
407
  const { error } = await client
@@ -404,7 +410,7 @@ export class SupabaseOrgStore implements IOrgStore {
404
410
  .eq('org_id', orgId)
405
411
  .eq('user_id', userId)
406
412
  .is('deleted_at', null);
407
- if (error) throw mapError(error);
413
+ if (error) throw mapPostgrestError(error);
408
414
  await this.events.emit({
409
415
  type: 'org_member.removed',
410
416
  orgId,
@@ -414,26 +420,6 @@ export class SupabaseOrgStore implements IOrgStore {
414
420
  }
415
421
  }
416
422
 
417
- /**
418
- * Build a row payload from an audit-soft-delete stamp. `updated_by` only
419
- * goes into the row when ctx.userId is set — passing `''` would violate the
420
- * `references auth.users(id)` FK on system contexts. The DB trigger sets
421
- * `updated_at`, but for soft-delete we set it explicitly so it matches
422
- * `deleted_at` (single timestamp for the deletion event).
423
- */
424
- function stampToRow(stamp: {
425
- updatedAt: string;
426
- updatedBy: string;
427
- deletedAt: string;
428
- }): Record<string, unknown> {
429
- const row: Record<string, unknown> = {
430
- deleted_at: stamp.deletedAt,
431
- updated_at: stamp.updatedAt
432
- };
433
- if (stamp.updatedBy) row.updated_by = stamp.updatedBy;
434
- return row;
435
- }
436
-
437
423
  // ============================================================================
438
424
  // Row ↔ domain mappers
439
425
  // ============================================================================
@@ -449,6 +435,7 @@ interface OrgRow {
449
435
  name: string;
450
436
  slug: string;
451
437
  owner_id: string;
438
+ assets?: Record<string, string> | null;
452
439
  created_by?: string | null;
453
440
  updated_by?: string | null;
454
441
  created_at: string;
@@ -473,6 +460,7 @@ function rowToOrg(row: OrgRow): Organization {
473
460
  name: row.name,
474
461
  slug: row.slug,
475
462
  ownerId: row.owner_id,
463
+ assets: row.assets ?? undefined,
476
464
  createdBy: row.created_by ?? row.owner_id,
477
465
  updatedBy: row.updated_by ?? row.owner_id,
478
466
  createdAt: row.created_at,
@@ -487,6 +475,7 @@ function orgToRow(org: Organization): OrgRow {
487
475
  name: org.name,
488
476
  slug: org.slug,
489
477
  owner_id: org.ownerId,
478
+ assets: org.assets ?? null,
490
479
  created_by: org.createdBy,
491
480
  updated_by: org.updatedBy,
492
481
  created_at: org.createdAt,
@@ -517,31 +506,3 @@ function memberToRow(m: OrgMember): OrgMemberRow {
517
506
  joined_at: m.joinedAt
518
507
  };
519
508
  }
520
-
521
- // ============================================================================
522
- // Error translation
523
- // ============================================================================
524
- interface PostgrestError {
525
- code?: string;
526
- message?: string;
527
- details?: string;
528
- }
529
-
530
- function mapError(e: unknown): Error {
531
- const pg = e as PostgrestError;
532
- if (pg?.code === '23505') {
533
- return new ProviderError(pg.message ?? 'Duplicate record', 409);
534
- }
535
- if (pg?.code === '23503') {
536
- return new ProviderError(pg.message ?? 'Foreign key violation', 409);
537
- }
538
- if (e instanceof Error) return e;
539
- if (e && typeof e === 'object') {
540
- const obj = e as { message?: string; details?: string; hint?: string; code?: string };
541
- const msg = obj.message ?? obj.details ?? obj.hint ?? 'Unknown Postgres error';
542
- const err = new Error(obj.code ? `[${obj.code}] ${msg}` : msg);
543
- Object.assign(err, obj);
544
- return err;
545
- }
546
- return new Error(String(e));
547
- }