@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.
- 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 +1 -1
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
- package/dist/data/SupabaseOrgStore.js +54 -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/dist/storage/SupabaseStorageProvider.d.ts +7 -5
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +14 -7
- package/dist/storage/SupabaseStorageProvider.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 +57 -96
- 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/src/storage/SupabaseStorageProvider.ts +12 -7
- package/supabase/config.toml +6 -6
- package/supabase/migrations/20260630120000_org_assets.sql +19 -0
- 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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; `
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|