@selvajs/supabase-provider 0.14.4-beta.0 → 0.15.0-beta.2
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/LICENSE +1 -1
- package/README.md +2 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +7 -14
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/data/SupabaseComputeServerStore.d.ts +14 -3
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +67 -16
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDataProvider.d.ts +36 -8
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
- package/dist/data/SupabaseDataProvider.js +119 -15
- package/dist/data/SupabaseDataProvider.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.d.ts +1 -0
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +41 -4
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseEventSink.d.ts +5 -2
- package/dist/data/SupabaseEventSink.d.ts.map +1 -1
- package/dist/data/SupabaseEventSink.js +20 -3
- package/dist/data/SupabaseEventSink.js.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.d.ts +49 -4
- package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.js +109 -19
- package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/migrationHead.d.ts +14 -0
- package/dist/data/migrationHead.d.ts.map +1 -0
- package/dist/data/migrationHead.js +14 -0
- package/dist/data/migrationHead.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +3 -0
- package/dist/storage/SupabaseStorageProvider.js.map +1 -1
- package/package.json +23 -14
- package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
- package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
- package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
- package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
- package/supabase/migrations/20260716120000_definition_status_and_audit_version.sql +36 -0
- package/supabase/migrations/20260716130000_erase_user_audit_email.sql +36 -0
- package/supabase/migrations/20260717120000_compute_server_has_api_key.sql +20 -0
- package/dist/auth/__tests__/conformance.test.d.ts +0 -2
- package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/auth/__tests__/conformance.test.js +0 -76
- package/dist/auth/__tests__/conformance.test.js.map +0 -1
- package/dist/auth/__tests__/token-verification.test.d.ts +0 -13
- package/dist/auth/__tests__/token-verification.test.d.ts.map +0 -1
- package/dist/auth/__tests__/token-verification.test.js +0 -135
- package/dist/auth/__tests__/token-verification.test.js.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.js +0 -45
- package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +0 -12
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +0 -1
- package/dist/data/__tests__/compute-server-encryption.test.js +0 -143
- package/dist/data/__tests__/compute-server-encryption.test.js.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.js +0 -85
- package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
- package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.js +0 -45
- package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
- package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/org-conformance.test.js +0 -29
- package/dist/data/__tests__/org-conformance.test.js.map +0 -1
- package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/project-conformance.test.js +0 -57
- package/dist/data/__tests__/project-conformance.test.js.map +0 -1
- package/dist/data/__tests__/share-link-concurrency.test.d.ts +0 -22
- package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +0 -1
- package/dist/data/__tests__/share-link-concurrency.test.js +0 -115
- package/dist/data/__tests__/share-link-concurrency.test.js.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.js +0 -88
- package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/test-helpers.d.ts +0 -52
- package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
- package/dist/data/__tests__/test-helpers.js +0 -177
- package/dist/data/__tests__/test-helpers.js.map +0 -1
- package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
- package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/permissions/__tests__/conformance.test.js +0 -27
- package/dist/permissions/__tests__/conformance.test.js.map +0 -1
- package/dist/storage/__tests__/conformance.test.d.ts +0 -2
- package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/storage/__tests__/conformance.test.js +0 -149
- package/dist/storage/__tests__/conformance.test.js.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.js +0 -38
- package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
- package/src/auth/SupabaseAuthProvider.ts +0 -600
- package/src/auth/__tests__/conformance.test.ts +0 -78
- package/src/auth/__tests__/token-verification.test.ts +0 -158
- package/src/auth/index.ts +0 -2
- package/src/cli/sync-migrations.ts +0 -133
- package/src/data/SupabaseAuditQuery.ts +0 -103
- package/src/data/SupabaseComputeServerStore.ts +0 -382
- package/src/data/SupabaseDataProvider.ts +0 -153
- package/src/data/SupabaseDefinitionStore.ts +0 -527
- package/src/data/SupabaseEventSink.ts +0 -30
- package/src/data/SupabaseInviteStore.ts +0 -171
- package/src/data/SupabaseOrgStore.ts +0 -508
- package/src/data/SupabaseProjectStore.ts +0 -431
- package/src/data/SupabaseShareLinkStore.ts +0 -176
- package/src/data/SupabaseSolveMetricSink.ts +0 -38
- package/src/data/__tests__/compute-server-conformance.test.ts +0 -46
- package/src/data/__tests__/compute-server-encryption.test.ts +0 -173
- package/src/data/__tests__/definition-conformance.test.ts +0 -89
- package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
- package/src/data/__tests__/invite-conformance.test.ts +0 -46
- package/src/data/__tests__/org-conformance.test.ts +0 -30
- package/src/data/__tests__/project-conformance.test.ts +0 -57
- package/src/data/__tests__/share-link-concurrency.test.ts +0 -124
- package/src/data/__tests__/share-link-conformance.test.ts +0 -87
- package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
- package/src/data/__tests__/test-helpers.ts +0 -188
- package/src/data/client.ts +0 -173
- package/src/data/errors.ts +0 -45
- package/src/data/index.ts +0 -29
- package/src/data/pagination.ts +0 -68
- package/src/data/query.ts +0 -22
- package/src/data/rowStamp.ts +0 -48
- package/src/index.ts +0 -42
- package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
- package/src/permissions/__tests__/conformance.test.ts +0 -28
- package/src/permissions/index.ts +0 -1
- package/src/storage/SupabaseStorageProvider.ts +0 -182
- package/src/storage/__tests__/conformance.test.ts +0 -152
- package/src/storage/index.ts +0 -2
- package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
- package/src/userProfile/__tests__/conformance.test.ts +0 -39
- package/src/userProfile/index.ts +0 -1
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IProjectStore,
|
|
3
|
-
IEventSink,
|
|
4
|
-
Project,
|
|
5
|
-
ProjectMember,
|
|
6
|
-
ProjectRole,
|
|
7
|
-
RequestContext,
|
|
8
|
-
ListOptions,
|
|
9
|
-
Page
|
|
10
|
-
} from '@selvajs/platform';
|
|
11
|
-
import { ProviderError, actorFrom, NoopEventSink } from '@selvajs/platform';
|
|
12
|
-
import type { ClientBundle } from './client.js';
|
|
13
|
-
import { mapPostgrestError } from './errors.js';
|
|
14
|
-
import { nextCursorFromRange, orderColumn, toRange } from './pagination.js';
|
|
15
|
-
import { stampSoftDelete, stampUpdate } from './rowStamp.js';
|
|
16
|
-
|
|
17
|
-
/** Explicit column list for `projects` — every field `rowToProject` consumes. */
|
|
18
|
-
const PROJECT_COLUMNS =
|
|
19
|
-
'id, org_id, name, slug, description, visibility, owner_id, created_by, updated_by, auto_join_on_upload, created_at, updated_at, deleted_at';
|
|
20
|
-
/** Explicit column list for `project_members` — every field `rowToProjectMember` consumes. */
|
|
21
|
-
const PROJECT_MEMBER_COLUMNS =
|
|
22
|
-
'project_id, user_id, role, joined_at, updated_at, updated_by, deleted_at';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Project + project-membership store backed by Postgres. Visibility semantics
|
|
26
|
-
* (public / org / private) are enforced by the `visible_project()` helper in
|
|
27
|
-
* RLS. Access predicates (canEdit / canManage / canView etc.) live as pure
|
|
28
|
-
* functions in `@selvajs/platform/access`; the route layer composes them with
|
|
29
|
-
* pre-loaded entities. See `packages/selva/src/lib/server/access.server.ts`.
|
|
30
|
-
*
|
|
31
|
-
* `createProject` atomically seeds the creator as the project `owner` in
|
|
32
|
-
* `project_members` so subsequent user-scoped reads can see it.
|
|
33
|
-
*/
|
|
34
|
-
export class SupabaseProjectStore implements IProjectStore {
|
|
35
|
-
private readonly events: IEventSink;
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
38
|
-
private readonly clients: ClientBundle,
|
|
39
|
-
events: IEventSink = new NoopEventSink()
|
|
40
|
-
) {
|
|
41
|
-
this.events = events;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async listProjects(
|
|
45
|
-
ctx: RequestContext,
|
|
46
|
-
orgId: string,
|
|
47
|
-
opts?: ListOptions
|
|
48
|
-
): Promise<Page<Project>> {
|
|
49
|
-
const range = toRange(opts);
|
|
50
|
-
const direction = opts?.orderDir ?? 'desc';
|
|
51
|
-
const { data, error, count } = await this.clients
|
|
52
|
-
.forRequest(ctx)
|
|
53
|
-
.from('projects')
|
|
54
|
-
.select(PROJECT_COLUMNS, { count: 'exact' })
|
|
55
|
-
.eq('org_id', orgId)
|
|
56
|
-
.is('deleted_at', null)
|
|
57
|
-
.order(orderColumn(opts?.orderBy), { ascending: direction === 'asc' })
|
|
58
|
-
.range(range.from, range.to);
|
|
59
|
-
if (error) throw mapPostgrestError(error);
|
|
60
|
-
const items = (data ?? []).map(rowToProject);
|
|
61
|
-
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async getProject(ctx: RequestContext, id: string): Promise<Project | null> {
|
|
65
|
-
const { data, error } = await this.clients
|
|
66
|
-
.forRequest(ctx)
|
|
67
|
-
.from('projects')
|
|
68
|
-
.select(PROJECT_COLUMNS)
|
|
69
|
-
.eq('id', id)
|
|
70
|
-
.is('deleted_at', null)
|
|
71
|
-
.maybeSingle();
|
|
72
|
-
if (error) throw mapPostgrestError(error);
|
|
73
|
-
return data ? rowToProject(data) : null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async getProjectBySlug(
|
|
77
|
-
ctx: RequestContext,
|
|
78
|
-
orgId: string,
|
|
79
|
-
slug: string
|
|
80
|
-
): Promise<Project | null> {
|
|
81
|
-
const { data, error } = await this.clients
|
|
82
|
-
.forRequest(ctx)
|
|
83
|
-
.from('projects')
|
|
84
|
-
.select(PROJECT_COLUMNS)
|
|
85
|
-
.eq('org_id', orgId)
|
|
86
|
-
.eq('slug', slug)
|
|
87
|
-
.is('deleted_at', null)
|
|
88
|
-
.maybeSingle();
|
|
89
|
-
if (error) throw mapPostgrestError(error);
|
|
90
|
-
return data ? rowToProject(data) : null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async createProject(ctx: RequestContext, project: Project): Promise<void> {
|
|
94
|
-
const client = this.clients.forRequest(ctx);
|
|
95
|
-
const { error } = await client.from('projects').insert(projectToRow(project));
|
|
96
|
-
if (error) throw mapPostgrestError(error);
|
|
97
|
-
|
|
98
|
-
// Seed owner membership so user-scoped reads see the project post-create.
|
|
99
|
-
// Upsert so a stale soft-deleted row from a prior project with the same id
|
|
100
|
-
// is reactivated rather than blocking creation.
|
|
101
|
-
const { error: memberError } = await client.from('project_members').upsert(
|
|
102
|
-
{
|
|
103
|
-
project_id: project.id,
|
|
104
|
-
user_id: project.ownerId,
|
|
105
|
-
role: 'owner',
|
|
106
|
-
joined_at: new Date().toISOString(),
|
|
107
|
-
deleted_at: null
|
|
108
|
-
},
|
|
109
|
-
{ onConflict: 'project_id,user_id' }
|
|
110
|
-
);
|
|
111
|
-
if (memberError) throw mapPostgrestError(memberError);
|
|
112
|
-
await this.events.emit({
|
|
113
|
-
type: 'project.created',
|
|
114
|
-
projectId: project.id,
|
|
115
|
-
orgId: project.orgId,
|
|
116
|
-
actorId: actorFrom(ctx)
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async updateProject(
|
|
121
|
-
ctx: RequestContext,
|
|
122
|
-
id: string,
|
|
123
|
-
patch: Partial<
|
|
124
|
-
Pick<Project, 'name' | 'slug' | 'description' | 'visibility' | 'autoJoinOnUpload'>
|
|
125
|
-
>
|
|
126
|
-
): Promise<void> {
|
|
127
|
-
const row: Record<string, unknown> = {};
|
|
128
|
-
if (patch.name !== undefined) row.name = patch.name;
|
|
129
|
-
if (patch.slug !== undefined) row.slug = patch.slug;
|
|
130
|
-
if (patch.description !== undefined) row.description = patch.description;
|
|
131
|
-
if (patch.visibility !== undefined) row.visibility = patch.visibility;
|
|
132
|
-
if (patch.autoJoinOnUpload !== undefined) row.auto_join_on_upload = patch.autoJoinOnUpload;
|
|
133
|
-
if (Object.keys(row).length === 0) return;
|
|
134
|
-
Object.assign(row, stampUpdate(ctx));
|
|
135
|
-
|
|
136
|
-
const { data, error } = await this.clients
|
|
137
|
-
.forRequest(ctx)
|
|
138
|
-
.from('projects')
|
|
139
|
-
.update(row)
|
|
140
|
-
.eq('id', id)
|
|
141
|
-
.is('deleted_at', null)
|
|
142
|
-
.select('id');
|
|
143
|
-
if (error) throw mapPostgrestError(error);
|
|
144
|
-
if (!data || data.length === 0) throw new ProviderError(`Project '${id}' not found`, 404);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async deleteProject(ctx: RequestContext, id: string): Promise<void> {
|
|
148
|
-
// §9 soft-delete with cascade. Mirrors LocalProjectStore: project →
|
|
149
|
-
// project_members, definitions. FK CASCADE doesn't fire on soft-delete,
|
|
150
|
-
// so the cascade is in app code.
|
|
151
|
-
const client = this.clients.forRequest(ctx);
|
|
152
|
-
const stampRow = stampSoftDelete(ctx);
|
|
153
|
-
|
|
154
|
-
const { data, error } = await client
|
|
155
|
-
.from('projects')
|
|
156
|
-
.update(stampRow)
|
|
157
|
-
.eq('id', id)
|
|
158
|
-
.is('deleted_at', null)
|
|
159
|
-
.select('id');
|
|
160
|
-
if (error) throw mapPostgrestError(error);
|
|
161
|
-
if (!data || data.length === 0) throw new ProviderError(`Project '${id}' not found`, 404);
|
|
162
|
-
|
|
163
|
-
const { error: pmErr } = await client
|
|
164
|
-
.from('project_members')
|
|
165
|
-
.update(stampRow)
|
|
166
|
-
.eq('project_id', id)
|
|
167
|
-
.is('deleted_at', null);
|
|
168
|
-
if (pmErr) throw mapPostgrestError(pmErr);
|
|
169
|
-
|
|
170
|
-
// Cascade to definitions. `select('guid')` returns the tombstoned rows so
|
|
171
|
-
// we emit one `definition.deleted` per record — parity with the single
|
|
172
|
-
// `delete` path and with `SupabaseDefinitionStore.deleteByProject` /
|
|
173
|
-
// `LocalProjectStore.deleteProject`. (Kept inline rather than delegating to
|
|
174
|
-
// the definition store to avoid a cross-store constructor dependency; the
|
|
175
|
-
// behavior must stay identical to `deleteByProject`.)
|
|
176
|
-
const { data: deletedDefs, error: defErr } = await client
|
|
177
|
-
.from('definitions')
|
|
178
|
-
.update(stampRow)
|
|
179
|
-
.eq('project_id', id)
|
|
180
|
-
.is('deleted_at', null)
|
|
181
|
-
.select('guid');
|
|
182
|
-
if (defErr) throw mapPostgrestError(defErr);
|
|
183
|
-
for (const row of deletedDefs ?? []) {
|
|
184
|
-
await this.events.emit({
|
|
185
|
-
type: 'definition.deleted',
|
|
186
|
-
definitionId: row.guid,
|
|
187
|
-
actorId: actorFrom(ctx)
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
await this.events.emit({ type: 'project.deleted', projectId: id, actorId: actorFrom(ctx) });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async reactivateProject(
|
|
195
|
-
ctx: RequestContext,
|
|
196
|
-
orgId: string,
|
|
197
|
-
slug: string
|
|
198
|
-
): Promise<Project | null> {
|
|
199
|
-
const client = this.clients.forRequest(ctx);
|
|
200
|
-
|
|
201
|
-
// Clear the tombstone. The WHERE deleted_at IS NOT NULL ensures we never
|
|
202
|
-
// accidentally clobber a live row, and the unique constraint means there
|
|
203
|
-
// can be at most one tombstone per (org_id, slug) at any time.
|
|
204
|
-
const { data, error } = await client
|
|
205
|
-
.from('projects')
|
|
206
|
-
.update({ deleted_at: null })
|
|
207
|
-
.eq('org_id', orgId)
|
|
208
|
-
.eq('slug', slug)
|
|
209
|
-
.not('deleted_at', 'is', null)
|
|
210
|
-
.select(PROJECT_COLUMNS)
|
|
211
|
-
.maybeSingle();
|
|
212
|
-
if (error) throw mapPostgrestError(error);
|
|
213
|
-
if (!data) return null;
|
|
214
|
-
|
|
215
|
-
// Reactivate the owner's project_members row (deleteProject cascades to it).
|
|
216
|
-
const { error: pmErr } = await client
|
|
217
|
-
.from('project_members')
|
|
218
|
-
.update({ deleted_at: null })
|
|
219
|
-
.eq('project_id', data.id)
|
|
220
|
-
.eq('user_id', data.owner_id)
|
|
221
|
-
.not('deleted_at', 'is', null);
|
|
222
|
-
if (pmErr) throw mapPostgrestError(pmErr);
|
|
223
|
-
|
|
224
|
-
const project = rowToProject(data);
|
|
225
|
-
await this.events.emit({
|
|
226
|
-
type: 'project.created',
|
|
227
|
-
projectId: project.id,
|
|
228
|
-
orgId: project.orgId,
|
|
229
|
-
actorId: actorFrom(ctx)
|
|
230
|
-
});
|
|
231
|
-
return project;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ============================================================================
|
|
235
|
-
// Project members
|
|
236
|
-
// ============================================================================
|
|
237
|
-
async listProjectMembers(
|
|
238
|
-
ctx: RequestContext,
|
|
239
|
-
projectId: string,
|
|
240
|
-
opts?: ListOptions
|
|
241
|
-
): Promise<Page<ProjectMember>> {
|
|
242
|
-
const range = toRange(opts);
|
|
243
|
-
const { data, error, count } = await this.clients
|
|
244
|
-
.forRequest(ctx)
|
|
245
|
-
.from('project_members')
|
|
246
|
-
.select(PROJECT_MEMBER_COLUMNS, { count: 'exact' })
|
|
247
|
-
.eq('project_id', projectId)
|
|
248
|
-
.is('deleted_at', null)
|
|
249
|
-
.order('joined_at', { ascending: (opts?.orderDir ?? 'desc') === 'asc' })
|
|
250
|
-
.range(range.from, range.to);
|
|
251
|
-
if (error) throw mapPostgrestError(error);
|
|
252
|
-
const items = (data ?? []).map(rowToProjectMember);
|
|
253
|
-
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async getProjectMember(
|
|
257
|
-
ctx: RequestContext,
|
|
258
|
-
projectId: string,
|
|
259
|
-
userId: string
|
|
260
|
-
): Promise<ProjectMember | null> {
|
|
261
|
-
const { data, error } = await this.clients
|
|
262
|
-
.forRequest(ctx)
|
|
263
|
-
.from('project_members')
|
|
264
|
-
.select(PROJECT_MEMBER_COLUMNS)
|
|
265
|
-
.eq('project_id', projectId)
|
|
266
|
-
.eq('user_id', userId)
|
|
267
|
-
.is('deleted_at', null)
|
|
268
|
-
.maybeSingle();
|
|
269
|
-
if (error) throw mapPostgrestError(error);
|
|
270
|
-
return data ? rowToProjectMember(data) : null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async addProjectMember(ctx: RequestContext, member: ProjectMember): Promise<void> {
|
|
274
|
-
// Upsert reactivates a prior soft-deleted row instead of throwing
|
|
275
|
-
// duplicate-key. Mirrors LocalProjectStore.addProjectMember.
|
|
276
|
-
const row: Record<string, unknown> = {
|
|
277
|
-
...projectMemberToRow(member),
|
|
278
|
-
deleted_at: null,
|
|
279
|
-
...stampUpdate(ctx)
|
|
280
|
-
};
|
|
281
|
-
const { error } = await this.clients
|
|
282
|
-
.forRequest(ctx)
|
|
283
|
-
.from('project_members')
|
|
284
|
-
.upsert(row, { onConflict: 'project_id,user_id' });
|
|
285
|
-
if (error) throw mapPostgrestError(error);
|
|
286
|
-
await this.events.emit({
|
|
287
|
-
type: 'project_member.added',
|
|
288
|
-
projectId: member.projectId,
|
|
289
|
-
userId: member.userId,
|
|
290
|
-
actorId: actorFrom(ctx)
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async updateProjectMemberRole(
|
|
295
|
-
ctx: RequestContext,
|
|
296
|
-
projectId: string,
|
|
297
|
-
userId: string,
|
|
298
|
-
role: ProjectRole
|
|
299
|
-
): Promise<void> {
|
|
300
|
-
const row: Record<string, unknown> = { role, ...stampUpdate(ctx) };
|
|
301
|
-
const { data, error } = await this.clients
|
|
302
|
-
.forRequest(ctx)
|
|
303
|
-
.from('project_members')
|
|
304
|
-
.update(row)
|
|
305
|
-
.eq('project_id', projectId)
|
|
306
|
-
.eq('user_id', userId)
|
|
307
|
-
.is('deleted_at', null)
|
|
308
|
-
.select('user_id');
|
|
309
|
-
if (error) throw mapPostgrestError(error);
|
|
310
|
-
if (!data || data.length === 0)
|
|
311
|
-
throw new ProviderError(`Project member '${userId}' not found`, 404);
|
|
312
|
-
await this.events.emit({
|
|
313
|
-
type: 'project_member.role_changed',
|
|
314
|
-
projectId,
|
|
315
|
-
userId,
|
|
316
|
-
role,
|
|
317
|
-
actorId: actorFrom(ctx)
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async removeProjectMember(ctx: RequestContext, projectId: string, userId: string): Promise<void> {
|
|
322
|
-
const { error } = await this.clients
|
|
323
|
-
.forRequest(ctx)
|
|
324
|
-
.from('project_members')
|
|
325
|
-
.update(stampSoftDelete(ctx))
|
|
326
|
-
.eq('project_id', projectId)
|
|
327
|
-
.eq('user_id', userId)
|
|
328
|
-
.is('deleted_at', null);
|
|
329
|
-
if (error) throw mapPostgrestError(error);
|
|
330
|
-
await this.events.emit({
|
|
331
|
-
type: 'project_member.removed',
|
|
332
|
-
projectId,
|
|
333
|
-
userId,
|
|
334
|
-
actorId: actorFrom(ctx)
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ============================================================================
|
|
340
|
-
// Row ↔ domain mappers
|
|
341
|
-
// ============================================================================
|
|
342
|
-
//
|
|
343
|
-
// Audit columns (`created_by` / `updated_by`) FK to `auth.users(id)` with
|
|
344
|
-
// `ON DELETE SET NULL`; they can legitimately become NULL when the user is
|
|
345
|
-
// deleted (spec §8). Mappers fall back to `owner_id` so the domain type
|
|
346
|
-
// stays non-nullable. Also true for ProjectMember audit columns below.
|
|
347
|
-
// that haven't applied the latest migration — the mapper falls back to
|
|
348
|
-
// owner_id / user_id / joined_at in those cases.
|
|
349
|
-
|
|
350
|
-
interface ProjectRow {
|
|
351
|
-
id: string;
|
|
352
|
-
org_id: string;
|
|
353
|
-
name: string;
|
|
354
|
-
slug: string;
|
|
355
|
-
description: string | null;
|
|
356
|
-
visibility: Project['visibility'];
|
|
357
|
-
owner_id: string;
|
|
358
|
-
created_by?: string | null;
|
|
359
|
-
updated_by?: string | null;
|
|
360
|
-
auto_join_on_upload?: boolean | null;
|
|
361
|
-
created_at: string;
|
|
362
|
-
updated_at: string;
|
|
363
|
-
deleted_at?: string | null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
interface ProjectMemberRow {
|
|
367
|
-
project_id: string;
|
|
368
|
-
user_id: string;
|
|
369
|
-
role: ProjectRole;
|
|
370
|
-
joined_at: string;
|
|
371
|
-
updated_at?: string | null;
|
|
372
|
-
updated_by?: string | null;
|
|
373
|
-
deleted_at?: string | null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function rowToProject(row: ProjectRow): Project {
|
|
377
|
-
return {
|
|
378
|
-
id: row.id,
|
|
379
|
-
orgId: row.org_id,
|
|
380
|
-
name: row.name,
|
|
381
|
-
slug: row.slug,
|
|
382
|
-
description: row.description ?? undefined,
|
|
383
|
-
visibility: row.visibility,
|
|
384
|
-
ownerId: row.owner_id,
|
|
385
|
-
createdBy: row.created_by ?? row.owner_id,
|
|
386
|
-
updatedBy: row.updated_by ?? row.owner_id,
|
|
387
|
-
autoJoinOnUpload: row.auto_join_on_upload ?? false,
|
|
388
|
-
createdAt: row.created_at,
|
|
389
|
-
updatedAt: row.updated_at,
|
|
390
|
-
deletedAt: row.deleted_at ?? null
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function projectToRow(p: Project): ProjectRow {
|
|
395
|
-
return {
|
|
396
|
-
id: p.id,
|
|
397
|
-
org_id: p.orgId,
|
|
398
|
-
name: p.name,
|
|
399
|
-
slug: p.slug,
|
|
400
|
-
description: p.description ?? null,
|
|
401
|
-
visibility: p.visibility,
|
|
402
|
-
owner_id: p.ownerId,
|
|
403
|
-
created_by: p.createdBy,
|
|
404
|
-
updated_by: p.updatedBy,
|
|
405
|
-
auto_join_on_upload: p.autoJoinOnUpload,
|
|
406
|
-
created_at: p.createdAt,
|
|
407
|
-
updated_at: p.updatedAt,
|
|
408
|
-
deleted_at: p.deletedAt ?? null
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function rowToProjectMember(row: ProjectMemberRow): ProjectMember {
|
|
413
|
-
return {
|
|
414
|
-
projectId: row.project_id,
|
|
415
|
-
userId: row.user_id,
|
|
416
|
-
role: row.role,
|
|
417
|
-
joinedAt: row.joined_at,
|
|
418
|
-
updatedAt: row.updated_at ?? row.joined_at,
|
|
419
|
-
updatedBy: row.updated_by ?? row.user_id,
|
|
420
|
-
deletedAt: row.deleted_at ?? null
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function projectMemberToRow(m: ProjectMember): ProjectMemberRow {
|
|
425
|
-
return {
|
|
426
|
-
project_id: m.projectId,
|
|
427
|
-
user_id: m.userId,
|
|
428
|
-
role: m.role,
|
|
429
|
-
joined_at: m.joinedAt
|
|
430
|
-
};
|
|
431
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IShareLinkStore,
|
|
3
|
-
IEventSink,
|
|
4
|
-
ShareLink,
|
|
5
|
-
RequestContext,
|
|
6
|
-
ListOptions,
|
|
7
|
-
Page
|
|
8
|
-
} from '@selvajs/platform';
|
|
9
|
-
import { actorFrom, NoopEventSink } from '@selvajs/platform';
|
|
10
|
-
import type { ClientBundle } from './client.js';
|
|
11
|
-
import { mapPostgrestError } from './errors.js';
|
|
12
|
-
import { nextCursorFromRange, toRange } from './pagination.js';
|
|
13
|
-
|
|
14
|
-
/** Explicit column list for `share_links` — every field `rowToLink` consumes. */
|
|
15
|
-
const SHARE_LINK_COLUMNS =
|
|
16
|
-
'id, definition_guid, channel, token_hash, name, created_by, created_at, expires_at, revoked_at, allow_solve, max_solves, solve_count';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Spec §7 — share-link store backed by Postgres.
|
|
20
|
-
*
|
|
21
|
-
* Token resolution (`getByTokenHash`) and atomic increment use the
|
|
22
|
-
* SECURITY DEFINER `try_increment_share_link_solve_count` RPC + service-
|
|
23
|
-
* role client to bypass RLS — the token IS the credential, not the
|
|
24
|
-
* authenticated user identity.
|
|
25
|
-
*
|
|
26
|
-
* `tryIncrementSolveCount` returns a single number on success or null when
|
|
27
|
-
* the cap is hit. The RPC enforces "increment iff under cap and live" in
|
|
28
|
-
* one statement, so even concurrent solves can't overshoot the cap.
|
|
29
|
-
*/
|
|
30
|
-
export class SupabaseShareLinkStore implements IShareLinkStore {
|
|
31
|
-
private readonly events: IEventSink;
|
|
32
|
-
|
|
33
|
-
constructor(
|
|
34
|
-
private readonly clients: ClientBundle,
|
|
35
|
-
events: IEventSink = new NoopEventSink()
|
|
36
|
-
) {
|
|
37
|
-
this.events = events;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async create(ctx: RequestContext, link: ShareLink): Promise<void> {
|
|
41
|
-
const { error } = await this.clients
|
|
42
|
-
.forRequest(ctx)
|
|
43
|
-
.from('share_links')
|
|
44
|
-
.insert(linkToRow(link));
|
|
45
|
-
if (error) throw mapPostgrestError(error);
|
|
46
|
-
await this.events.emit({
|
|
47
|
-
type: 'share_link.minted',
|
|
48
|
-
linkId: link.id,
|
|
49
|
-
definitionId: link.definitionId,
|
|
50
|
-
actorId: actorFrom(ctx)
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async listByDefinition(
|
|
55
|
-
ctx: RequestContext,
|
|
56
|
-
definitionId: string,
|
|
57
|
-
opts?: ListOptions
|
|
58
|
-
): Promise<Page<ShareLink>> {
|
|
59
|
-
const range = toRange(opts);
|
|
60
|
-
const { data, error, count } = await this.clients
|
|
61
|
-
.forRequest(ctx)
|
|
62
|
-
.from('share_links')
|
|
63
|
-
.select(SHARE_LINK_COLUMNS, { count: 'exact' })
|
|
64
|
-
.eq('definition_guid', definitionId)
|
|
65
|
-
.is('revoked_at', null)
|
|
66
|
-
.order('created_at', { ascending: false })
|
|
67
|
-
.range(range.from, range.to);
|
|
68
|
-
if (error) throw mapPostgrestError(error);
|
|
69
|
-
const items = (data ?? []).map(rowToLink);
|
|
70
|
-
return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async getById(ctx: RequestContext, id: string): Promise<ShareLink | null> {
|
|
74
|
-
const { data, error } = await this.clients
|
|
75
|
-
.forRequest(ctx)
|
|
76
|
-
.from('share_links')
|
|
77
|
-
.select(SHARE_LINK_COLUMNS)
|
|
78
|
-
.eq('id', id)
|
|
79
|
-
.maybeSingle();
|
|
80
|
-
if (error) throw mapPostgrestError(error);
|
|
81
|
-
return data ? rowToLink(data) : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async getByTokenHash(_ctx: RequestContext, tokenHash: string): Promise<ShareLink | null> {
|
|
85
|
-
// Service-role: token resolution must work for anonymous requests too.
|
|
86
|
-
// RLS would scope the query down to "links the current user can see"
|
|
87
|
-
// — exactly the wrong semantic for token-credentialed access.
|
|
88
|
-
const { data, error } = await this.clients.serviceClient
|
|
89
|
-
.from('share_links')
|
|
90
|
-
.select(`${SHARE_LINK_COLUMNS}, definitions!inner(deleted_at)`)
|
|
91
|
-
.eq('token_hash', tokenHash)
|
|
92
|
-
.is('revoked_at', null)
|
|
93
|
-
.is('definitions.deleted_at', null)
|
|
94
|
-
.maybeSingle();
|
|
95
|
-
if (error) throw mapPostgrestError(error);
|
|
96
|
-
return data ? rowToLink(data) : null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async revoke(ctx: RequestContext, id: string): Promise<void> {
|
|
100
|
-
// Idempotent: succeeds whether or not the row exists / is already
|
|
101
|
-
// revoked. The `is null` predicate on revoked_at means a re-revoke
|
|
102
|
-
// is a no-op (no row updated).
|
|
103
|
-
const { error } = await this.clients
|
|
104
|
-
.forRequest(ctx)
|
|
105
|
-
.from('share_links')
|
|
106
|
-
.update({ revoked_at: new Date().toISOString() })
|
|
107
|
-
.eq('id', id)
|
|
108
|
-
.is('revoked_at', null);
|
|
109
|
-
if (error) throw mapPostgrestError(error);
|
|
110
|
-
await this.events.emit({ type: 'share_link.revoked', linkId: id, actorId: actorFrom(ctx) });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async tryIncrementSolveCount(_ctx: RequestContext, id: string): Promise<number | null> {
|
|
114
|
-
// SECURITY DEFINER RPC — bypasses RLS. Atomic check-and-increment.
|
|
115
|
-
// Returns NULL when the link is missing/revoked/expired/capped; the
|
|
116
|
-
// row update is the only way to get a numeric return.
|
|
117
|
-
const { data, error } = await this.clients.serviceClient.rpc(
|
|
118
|
-
'try_increment_share_link_solve_count',
|
|
119
|
-
{ link_id: id }
|
|
120
|
-
);
|
|
121
|
-
if (error) throw mapPostgrestError(error);
|
|
122
|
-
return typeof data === 'number' ? data : null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ============================================================================
|
|
127
|
-
// Row ↔ domain mappers
|
|
128
|
-
// ============================================================================
|
|
129
|
-
interface ShareLinkRow {
|
|
130
|
-
id: string;
|
|
131
|
-
definition_guid: string;
|
|
132
|
-
channel: 'live' | 'draft';
|
|
133
|
-
token_hash: string;
|
|
134
|
-
name: string | null;
|
|
135
|
-
created_by: string;
|
|
136
|
-
created_at: string;
|
|
137
|
-
expires_at: string | null;
|
|
138
|
-
revoked_at: string | null;
|
|
139
|
-
allow_solve: boolean;
|
|
140
|
-
max_solves: number | null;
|
|
141
|
-
solve_count: number;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function rowToLink(row: ShareLinkRow): ShareLink {
|
|
145
|
-
return {
|
|
146
|
-
id: row.id,
|
|
147
|
-
definitionId: row.definition_guid,
|
|
148
|
-
channel: row.channel,
|
|
149
|
-
tokenHash: row.token_hash,
|
|
150
|
-
name: row.name ?? undefined,
|
|
151
|
-
createdBy: row.created_by,
|
|
152
|
-
createdAt: row.created_at,
|
|
153
|
-
expiresAt: row.expires_at,
|
|
154
|
-
revokedAt: row.revoked_at,
|
|
155
|
-
allowSolve: row.allow_solve,
|
|
156
|
-
maxSolves: row.max_solves,
|
|
157
|
-
solveCount: row.solve_count
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function linkToRow(l: ShareLink): Record<string, unknown> {
|
|
162
|
-
return {
|
|
163
|
-
id: l.id,
|
|
164
|
-
definition_guid: l.definitionId,
|
|
165
|
-
channel: l.channel,
|
|
166
|
-
token_hash: l.tokenHash,
|
|
167
|
-
name: l.name ?? null,
|
|
168
|
-
created_by: l.createdBy,
|
|
169
|
-
created_at: l.createdAt,
|
|
170
|
-
expires_at: l.expiresAt ?? null,
|
|
171
|
-
revoked_at: l.revokedAt ?? null,
|
|
172
|
-
allow_solve: l.allowSolve,
|
|
173
|
-
max_solves: l.maxSolves ?? null,
|
|
174
|
-
solve_count: l.solveCount
|
|
175
|
-
};
|
|
176
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ISolveMetricSink, RequestContext, SolveMetric } from '@selvajs/platform';
|
|
2
|
-
import type { ClientBundle } from './client.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Persists every solve's timing to `selva.solve_metrics`. Writes use the
|
|
6
|
-
* service-role client so RLS does not gate the telemetry — the sink runs as a
|
|
7
|
-
* system process attached to the compute route, not the requesting user.
|
|
8
|
-
*
|
|
9
|
-
* Per the `ISolveMetricSink` contract, `record` MUST NOT throw. It sits on the
|
|
10
|
-
* hot path of every solve and the response has already been produced by the
|
|
11
|
-
* time we're here, so a failed insert is logged and swallowed.
|
|
12
|
-
*/
|
|
13
|
-
export class SupabaseSolveMetricSink implements ISolveMetricSink {
|
|
14
|
-
constructor(private readonly clients: ClientBundle) {}
|
|
15
|
-
|
|
16
|
-
async record(_ctx: RequestContext, metric: SolveMetric): Promise<void> {
|
|
17
|
-
try {
|
|
18
|
-
const { error } = await this.clients.serviceClient.from('solve_metrics').insert({
|
|
19
|
-
actor_id: _ctx.userId || 'system',
|
|
20
|
-
definition_url: metric.definitionUrl,
|
|
21
|
-
definition_id: metric.definitionId,
|
|
22
|
-
version_id: metric.versionId,
|
|
23
|
-
channel: metric.channel,
|
|
24
|
-
org_id: metric.orgId,
|
|
25
|
-
duration_ms: metric.durationMs,
|
|
26
|
-
ok: metric.ok,
|
|
27
|
-
failure_kind: metric.failureKind,
|
|
28
|
-
error_count: metric.errorCount,
|
|
29
|
-
warning_count: metric.warningCount
|
|
30
|
-
});
|
|
31
|
-
if (error) {
|
|
32
|
-
console.error('[SupabaseSolveMetricSink] insert failed:', error.message, { metric });
|
|
33
|
-
}
|
|
34
|
-
} catch (err) {
|
|
35
|
-
console.error('[SupabaseSolveMetricSink] unexpected error:', err, { metric });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, beforeEach, it } from 'vitest';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { runComputeServerStoreConformance } from '@selvajs/platform/testing';
|
|
4
|
-
import { SupabaseComputeServerStore } from '../SupabaseComputeServerStore.js';
|
|
5
|
-
import { readEnv, resetAllData, seedUser } from './test-helpers.js';
|
|
6
|
-
|
|
7
|
-
const envCtx = readEnv();
|
|
8
|
-
|
|
9
|
-
// A throwaway at-rest key so the store encrypts apiKeys during the suite. The
|
|
10
|
-
// round-trip (encrypt on write, decrypt on read) must be transparent to the
|
|
11
|
-
// conformance assertions, which compare plaintext apiKeys.
|
|
12
|
-
const TEST_SECRET_KEY = randomBytes(32);
|
|
13
|
-
|
|
14
|
-
if (!envCtx) {
|
|
15
|
-
describe.skip('SupabaseComputeServerStore (skipped: no live stack)', () => {
|
|
16
|
-
it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
|
|
17
|
-
});
|
|
18
|
-
} else {
|
|
19
|
-
describe('SupabaseComputeServerStore', () => {
|
|
20
|
-
beforeEach(async () => {
|
|
21
|
-
await resetAllData(envCtx);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
runComputeServerStoreConformance({
|
|
25
|
-
name: 'SupabaseComputeServerStore',
|
|
26
|
-
createStore: () => new SupabaseComputeServerStore(envCtx.bundle, TEST_SECRET_KEY),
|
|
27
|
-
seedOrg: async (orgId) => {
|
|
28
|
-
// `compute_servers.owner_org_id` and `compute_server_org_defaults.org_id`
|
|
29
|
-
// FK to `orgs(id)`; the suite invents an `orgId` per org-scoped test,
|
|
30
|
-
// so we materialize the parent org here. Owner is a throwaway user
|
|
31
|
-
// (the org-scoped tests use SYSTEM_CONTEXT for writes anyway).
|
|
32
|
-
const { userId } = await seedUser(envCtx, '');
|
|
33
|
-
const now = new Date().toISOString();
|
|
34
|
-
const { error } = await envCtx.adminClient.from('orgs').insert({
|
|
35
|
-
id: orgId,
|
|
36
|
-
name: 'Compute Test Org',
|
|
37
|
-
slug: `compute-${orgId.slice(0, 8)}`,
|
|
38
|
-
owner_id: userId,
|
|
39
|
-
created_at: now,
|
|
40
|
-
updated_at: now
|
|
41
|
-
});
|
|
42
|
-
if (error) throw error;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|