@elevasis/core 0.12.0 → 0.13.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/test-utils/index.d.ts +17 -12
- package/dist/test-utils/index.js +19 -0
- package/package.json +1 -1
- package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -216
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +5 -19
- package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +3 -13
- package/src/auth/multi-tenancy/permissions.ts +12 -5
- package/src/business/acquisition/activity-events.ts +142 -0
- package/src/business/acquisition/api-schemas.ts +694 -689
- package/src/business/acquisition/derive-actions.ts +90 -0
- package/src/business/acquisition/index.ts +111 -109
- package/src/execution/engine/index.ts +434 -434
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +1 -2
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +0 -1
- package/src/execution/engine/tools/lead-service-types.ts +882 -879
- package/src/execution/engine/tools/registry.ts +699 -700
- package/src/execution/engine/tools/tool-maps.ts +777 -780
- package/src/organization-model/organization-graph.mdx +2 -2
- package/src/platform/constants/versions.ts +1 -1
- package/src/scaffold-registry/index.ts +10 -9
- package/src/scaffold-registry/schema.ts +68 -62
- package/src/supabase/database.types.ts +9 -7
- package/src/test-utils/rls/RLSTestContext.ts +585 -553
|
@@ -1,556 +1,588 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RLS Test Context Manager
|
|
3
|
-
* Manages isolated test data and user context switching for RLS integration tests
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* ```typescript
|
|
7
|
-
* describe('RLS Policies - API Keys', () => {
|
|
8
|
-
* let ctx: RLSTestContext
|
|
9
|
-
*
|
|
10
|
-
* beforeAll(async () => {
|
|
11
|
-
* ctx = new RLSTestContext()
|
|
12
|
-
* // Create test data
|
|
13
|
-
* })
|
|
14
|
-
*
|
|
15
|
-
* afterAll(async () => {
|
|
16
|
-
* await ctx.cleanup()
|
|
17
|
-
* })
|
|
18
|
-
* })
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
|
|
1
|
+
/**
|
|
2
|
+
* RLS Test Context Manager
|
|
3
|
+
* Manages isolated test data and user context switching for RLS integration tests
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```typescript
|
|
7
|
+
* describe('RLS Policies - API Keys', () => {
|
|
8
|
+
* let ctx: RLSTestContext
|
|
9
|
+
*
|
|
10
|
+
* beforeAll(async () => {
|
|
11
|
+
* ctx = new RLSTestContext()
|
|
12
|
+
* // Create test data
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* afterAll(async () => {
|
|
16
|
+
* await ctx.cleanup()
|
|
17
|
+
* })
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
22
|
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
|
23
23
|
import jwt from 'jsonwebtoken'
|
|
24
24
|
import type { Database } from '../../supabase/database.types'
|
|
25
|
-
|
|
26
|
-
type Role = 'admin' | 'member'
|
|
27
|
-
|
|
28
|
-
interface UserWithWorkosId {
|
|
29
|
-
id: string
|
|
30
|
-
workos_user_id: string
|
|
31
|
-
email: string
|
|
32
|
-
is_platform_admin: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface PreProvisionedUser {
|
|
36
|
-
id: string
|
|
37
|
-
email: string
|
|
38
|
-
is_platform_admin: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface Organization {
|
|
42
|
-
id: string
|
|
43
|
-
name: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface Membership {
|
|
47
|
-
id: string
|
|
48
|
-
user_id: string
|
|
49
|
-
organization_id: string
|
|
50
|
-
role_slug: Role
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class RLSTestContext {
|
|
54
|
-
adminClient: SupabaseClient<Database>
|
|
55
|
-
testPrefix: string
|
|
56
|
-
createdIds: {
|
|
57
|
-
users: string[]
|
|
58
|
-
organizations: string[]
|
|
59
|
-
memberships: string[]
|
|
60
|
-
apiKeys: string[]
|
|
61
|
-
invitations: string[]
|
|
62
|
-
taskSchedules: string[]
|
|
63
|
-
commandQueue: string[]
|
|
64
|
-
sessions: string[]
|
|
65
|
-
sessionMessages: string[]
|
|
66
|
-
executionLogs: string[]
|
|
67
|
-
executionMetrics: string[]
|
|
68
|
-
notifications: string[]
|
|
69
|
-
credentials: string[]
|
|
70
|
-
activities: string[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
constructor() {
|
|
74
|
-
// Verify test environment is configured
|
|
75
|
-
if (!process.env.SUPABASE_URL) {
|
|
76
|
-
throw new Error('SUPABASE_URL not configured in .env.development')
|
|
77
|
-
}
|
|
78
|
-
if (!process.env.SUPABASE_SERVICE_KEY) {
|
|
79
|
-
throw new Error('SUPABASE_SERVICE_KEY not configured in .env.development')
|
|
80
|
-
}
|
|
81
|
-
if (!process.env.SUPABASE_JWT_SECRET) {
|
|
82
|
-
throw new Error('SUPABASE_JWT_SECRET not configured in .env.development')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Create admin client (bypasses RLS with service_role key)
|
|
86
|
-
this.adminClient = createClient<Database>(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY)
|
|
87
|
-
|
|
88
|
-
// Generate unique prefix for this test run to prevent collisions
|
|
89
|
-
this.testPrefix = `test_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
|
90
|
-
|
|
91
|
-
// Track created resources for cleanup
|
|
92
|
-
this.createdIds = {
|
|
93
|
-
users: [],
|
|
94
|
-
organizations: [],
|
|
95
|
-
memberships: [],
|
|
96
|
-
apiKeys: [],
|
|
97
|
-
invitations: [],
|
|
98
|
-
taskSchedules: [],
|
|
99
|
-
commandQueue: [],
|
|
100
|
-
sessions: [],
|
|
101
|
-
sessionMessages: [],
|
|
102
|
-
executionLogs: [],
|
|
103
|
-
executionMetrics: [],
|
|
104
|
-
notifications: [],
|
|
105
|
-
credentials: [],
|
|
106
|
-
activities: []
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Create a test organization
|
|
112
|
-
*/
|
|
113
|
-
async createOrganization(name: string): Promise<Organization> {
|
|
114
|
-
// Generate a unique WorkOS org ID for test organizations
|
|
115
|
-
const workosOrgId = `org_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
116
|
-
|
|
117
|
-
const { data, error } = await this.adminClient
|
|
118
|
-
.from('organizations')
|
|
119
|
-
.insert({
|
|
120
|
-
workos_org_id: workosOrgId,
|
|
121
|
-
name: `${this.testPrefix}_${name}`,
|
|
122
|
-
is_test: true
|
|
123
|
-
})
|
|
124
|
-
.select()
|
|
125
|
-
.single()
|
|
126
|
-
|
|
127
|
-
if (error) {
|
|
128
|
-
throw new Error(`Failed to create organization: ${error.message}`)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
this.createdIds.organizations.push(data.id)
|
|
132
|
-
return data
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create a test user with a WorkOS user ID
|
|
137
|
-
*/
|
|
138
|
-
async createUser(email: string, isPlatformAdmin = false): Promise<UserWithWorkosId> {
|
|
139
|
-
// Generate a unique WorkOS user ID for this test user
|
|
140
|
-
const workosUserId = `user_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
141
|
-
|
|
142
|
-
const { data, error } = await this.adminClient
|
|
143
|
-
.from('users')
|
|
144
|
-
.insert({
|
|
145
|
-
workos_user_id: workosUserId,
|
|
146
|
-
email: `${this.testPrefix}_${email}`,
|
|
147
|
-
first_name: 'Test',
|
|
148
|
-
last_name: 'User',
|
|
149
|
-
is_platform_admin: isPlatformAdmin
|
|
150
|
-
})
|
|
151
|
-
.select('*')
|
|
152
|
-
.single()
|
|
153
|
-
|
|
154
|
-
if (error) {
|
|
155
|
-
throw new Error(`Failed to create user: ${error.message}`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
this.createdIds.users.push(data.id)
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
id: data.id,
|
|
162
|
-
workos_user_id: workosUserId,
|
|
163
|
-
email: data.email,
|
|
164
|
-
is_platform_admin: (data as { is_platform_admin?: boolean }).is_platform_admin ?? false
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Create a pre-provisioned test user (without WorkOS user ID)
|
|
170
|
-
* Used for testing invitation flows where users are created before they sign up
|
|
171
|
-
*/
|
|
172
|
-
async createPreProvisionedUser(email: string, isPlatformAdmin = false): Promise<PreProvisionedUser> {
|
|
173
|
-
const { data, error } = await this.adminClient
|
|
174
|
-
.from('users')
|
|
175
|
-
.insert({
|
|
176
|
-
workos_user_id: null, // Key difference: NULL for pre-provisioned
|
|
177
|
-
email: `${this.testPrefix}_${email}`,
|
|
178
|
-
first_name: 'Test',
|
|
179
|
-
last_name: 'PreProvisioned',
|
|
180
|
-
is_platform_admin: isPlatformAdmin
|
|
181
|
-
})
|
|
182
|
-
.select('*')
|
|
183
|
-
.single()
|
|
184
|
-
|
|
185
|
-
if (error) {
|
|
186
|
-
throw new Error(`Failed to create pre-provisioned user: ${error.message}`)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
this.createdIds.users.push(data.id)
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
id: data.id,
|
|
193
|
-
email: data.email,
|
|
194
|
-
is_platform_admin: (data as { is_platform_admin?: boolean }).is_platform_admin ?? false
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Create an organization membership
|
|
200
|
-
*/
|
|
201
|
-
async createMembership(userId: string, organizationId: string, role: Role): Promise<Membership> {
|
|
202
|
-
// Generate a unique WorkOS membership ID
|
|
203
|
-
const workosMembershipId = `om_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
204
|
-
|
|
205
|
-
const { data, error } = await this.adminClient
|
|
206
|
-
.from('org_memberships')
|
|
207
|
-
.insert({
|
|
208
|
-
user_id: userId,
|
|
209
|
-
organization_id: organizationId,
|
|
210
|
-
workos_membership_id: workosMembershipId,
|
|
211
|
-
role_slug: role,
|
|
212
|
-
membership_status: 'active'
|
|
213
|
-
})
|
|
214
|
-
.select()
|
|
215
|
-
.single()
|
|
216
|
-
|
|
217
|
-
if (error) {
|
|
218
|
-
throw new Error(`Failed to create membership: ${error.message}`)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.createdIds.memberships.push(data.id)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
.single()
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
throw new Error(`Failed to
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (error) {
|
|
462
|
-
errors.push(`Failed to delete
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
25
|
+
|
|
26
|
+
type Role = 'admin' | 'member'
|
|
27
|
+
|
|
28
|
+
interface UserWithWorkosId {
|
|
29
|
+
id: string
|
|
30
|
+
workos_user_id: string
|
|
31
|
+
email: string
|
|
32
|
+
is_platform_admin: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PreProvisionedUser {
|
|
36
|
+
id: string
|
|
37
|
+
email: string
|
|
38
|
+
is_platform_admin: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Organization {
|
|
42
|
+
id: string
|
|
43
|
+
name: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Membership {
|
|
47
|
+
id: string
|
|
48
|
+
user_id: string
|
|
49
|
+
organization_id: string
|
|
50
|
+
role_slug: Role
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class RLSTestContext {
|
|
54
|
+
adminClient: SupabaseClient<Database>
|
|
55
|
+
testPrefix: string
|
|
56
|
+
createdIds: {
|
|
57
|
+
users: string[]
|
|
58
|
+
organizations: string[]
|
|
59
|
+
memberships: string[]
|
|
60
|
+
apiKeys: string[]
|
|
61
|
+
invitations: string[]
|
|
62
|
+
taskSchedules: string[]
|
|
63
|
+
commandQueue: string[]
|
|
64
|
+
sessions: string[]
|
|
65
|
+
sessionMessages: string[]
|
|
66
|
+
executionLogs: string[]
|
|
67
|
+
executionMetrics: string[]
|
|
68
|
+
notifications: string[]
|
|
69
|
+
credentials: string[]
|
|
70
|
+
activities: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor() {
|
|
74
|
+
// Verify test environment is configured
|
|
75
|
+
if (!process.env.SUPABASE_URL) {
|
|
76
|
+
throw new Error('SUPABASE_URL not configured in .env.development')
|
|
77
|
+
}
|
|
78
|
+
if (!process.env.SUPABASE_SERVICE_KEY) {
|
|
79
|
+
throw new Error('SUPABASE_SERVICE_KEY not configured in .env.development')
|
|
80
|
+
}
|
|
81
|
+
if (!process.env.SUPABASE_JWT_SECRET) {
|
|
82
|
+
throw new Error('SUPABASE_JWT_SECRET not configured in .env.development')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create admin client (bypasses RLS with service_role key)
|
|
86
|
+
this.adminClient = createClient<Database>(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY)
|
|
87
|
+
|
|
88
|
+
// Generate unique prefix for this test run to prevent collisions
|
|
89
|
+
this.testPrefix = `test_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
|
90
|
+
|
|
91
|
+
// Track created resources for cleanup
|
|
92
|
+
this.createdIds = {
|
|
93
|
+
users: [],
|
|
94
|
+
organizations: [],
|
|
95
|
+
memberships: [],
|
|
96
|
+
apiKeys: [],
|
|
97
|
+
invitations: [],
|
|
98
|
+
taskSchedules: [],
|
|
99
|
+
commandQueue: [],
|
|
100
|
+
sessions: [],
|
|
101
|
+
sessionMessages: [],
|
|
102
|
+
executionLogs: [],
|
|
103
|
+
executionMetrics: [],
|
|
104
|
+
notifications: [],
|
|
105
|
+
credentials: [],
|
|
106
|
+
activities: []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a test organization
|
|
112
|
+
*/
|
|
113
|
+
async createOrganization(name: string): Promise<Organization> {
|
|
114
|
+
// Generate a unique WorkOS org ID for test organizations
|
|
115
|
+
const workosOrgId = `org_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
116
|
+
|
|
117
|
+
const { data, error } = await this.adminClient
|
|
118
|
+
.from('organizations')
|
|
119
|
+
.insert({
|
|
120
|
+
workos_org_id: workosOrgId,
|
|
121
|
+
name: `${this.testPrefix}_${name}`,
|
|
122
|
+
is_test: true
|
|
123
|
+
})
|
|
124
|
+
.select()
|
|
125
|
+
.single()
|
|
126
|
+
|
|
127
|
+
if (error) {
|
|
128
|
+
throw new Error(`Failed to create organization: ${error.message}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.createdIds.organizations.push(data.id)
|
|
132
|
+
return data
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a test user with a WorkOS user ID
|
|
137
|
+
*/
|
|
138
|
+
async createUser(email: string, isPlatformAdmin = false): Promise<UserWithWorkosId> {
|
|
139
|
+
// Generate a unique WorkOS user ID for this test user
|
|
140
|
+
const workosUserId = `user_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
141
|
+
|
|
142
|
+
const { data, error } = await this.adminClient
|
|
143
|
+
.from('users')
|
|
144
|
+
.insert({
|
|
145
|
+
workos_user_id: workosUserId,
|
|
146
|
+
email: `${this.testPrefix}_${email}`,
|
|
147
|
+
first_name: 'Test',
|
|
148
|
+
last_name: 'User',
|
|
149
|
+
is_platform_admin: isPlatformAdmin
|
|
150
|
+
})
|
|
151
|
+
.select('*')
|
|
152
|
+
.single()
|
|
153
|
+
|
|
154
|
+
if (error) {
|
|
155
|
+
throw new Error(`Failed to create user: ${error.message}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.createdIds.users.push(data.id)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: data.id,
|
|
162
|
+
workos_user_id: workosUserId,
|
|
163
|
+
email: data.email,
|
|
164
|
+
is_platform_admin: (data as { is_platform_admin?: boolean }).is_platform_admin ?? false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create a pre-provisioned test user (without WorkOS user ID)
|
|
170
|
+
* Used for testing invitation flows where users are created before they sign up
|
|
171
|
+
*/
|
|
172
|
+
async createPreProvisionedUser(email: string, isPlatformAdmin = false): Promise<PreProvisionedUser> {
|
|
173
|
+
const { data, error } = await this.adminClient
|
|
174
|
+
.from('users')
|
|
175
|
+
.insert({
|
|
176
|
+
workos_user_id: null, // Key difference: NULL for pre-provisioned
|
|
177
|
+
email: `${this.testPrefix}_${email}`,
|
|
178
|
+
first_name: 'Test',
|
|
179
|
+
last_name: 'PreProvisioned',
|
|
180
|
+
is_platform_admin: isPlatformAdmin
|
|
181
|
+
})
|
|
182
|
+
.select('*')
|
|
183
|
+
.single()
|
|
184
|
+
|
|
185
|
+
if (error) {
|
|
186
|
+
throw new Error(`Failed to create pre-provisioned user: ${error.message}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.createdIds.users.push(data.id)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: data.id,
|
|
193
|
+
email: data.email,
|
|
194
|
+
is_platform_admin: (data as { is_platform_admin?: boolean }).is_platform_admin ?? false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create an organization membership
|
|
200
|
+
*/
|
|
201
|
+
async createMembership(userId: string, organizationId: string, role: Role): Promise<Membership> {
|
|
202
|
+
// Generate a unique WorkOS membership ID
|
|
203
|
+
const workosMembershipId = `om_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
204
|
+
|
|
205
|
+
const { data, error } = await this.adminClient
|
|
206
|
+
.from('org_memberships')
|
|
207
|
+
.insert({
|
|
208
|
+
user_id: userId,
|
|
209
|
+
organization_id: organizationId,
|
|
210
|
+
workos_membership_id: workosMembershipId,
|
|
211
|
+
role_slug: role,
|
|
212
|
+
membership_status: 'active'
|
|
213
|
+
})
|
|
214
|
+
.select()
|
|
215
|
+
.single()
|
|
216
|
+
|
|
217
|
+
if (error) {
|
|
218
|
+
throw new Error(`Failed to create membership: ${error.message}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.createdIds.memberships.push(data.id)
|
|
222
|
+
|
|
223
|
+
await this.assignSystemRole(data.id, role)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
id: data.id,
|
|
227
|
+
user_id: data.user_id,
|
|
228
|
+
organization_id: data.organization_id,
|
|
229
|
+
role_slug: data.role_slug as Role
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Assign a system role to a membership via org_rol_assignments.
|
|
235
|
+
* After the 2026-04-25 auth refactor, RLS policies read `effective_permissions[]`
|
|
236
|
+
* (materialized by trigger from org_rol_assignments → org_rol_grants).
|
|
237
|
+
* Without this assignment, role_slug is informational only and the membership
|
|
238
|
+
* has zero permissions, causing all has_org_permission() checks to deny.
|
|
239
|
+
*/
|
|
240
|
+
private async assignSystemRole(membershipId: string, slug: Role): Promise<void> {
|
|
241
|
+
const { data: roleDef, error: roleErr } = await this.adminClient
|
|
242
|
+
.from('org_rol_definitions')
|
|
243
|
+
.select('id')
|
|
244
|
+
.is('organization_id', null)
|
|
245
|
+
.eq('slug', slug)
|
|
246
|
+
.single()
|
|
247
|
+
|
|
248
|
+
if (roleErr || !roleDef) {
|
|
249
|
+
throw new Error(`Failed to look up system role '${slug}': ${roleErr?.message ?? 'not found'}`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const { error: assignErr } = await this.adminClient
|
|
253
|
+
.from('org_rol_assignments')
|
|
254
|
+
.insert({ membership_id: membershipId, role_id: roleDef.id })
|
|
255
|
+
|
|
256
|
+
if (assignErr) {
|
|
257
|
+
throw new Error(`Failed to assign system role '${slug}' to membership: ${assignErr.message}`)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a pre-provisioned organization membership (without WorkOS membership ID)
|
|
263
|
+
* Used for testing invitation flows where memberships are created before user accepts
|
|
264
|
+
*/
|
|
265
|
+
async createPreProvisionedMembership(userId: string, organizationId: string, role: Role): Promise<Membership> {
|
|
266
|
+
const { data, error } = await this.adminClient
|
|
267
|
+
.from('org_memberships')
|
|
268
|
+
.insert({
|
|
269
|
+
user_id: userId,
|
|
270
|
+
organization_id: organizationId,
|
|
271
|
+
workos_membership_id: null, // Key difference: NULL for pre-provisioned
|
|
272
|
+
role_slug: role,
|
|
273
|
+
membership_status: 'active' // Pre-provisioned memberships are active from creation
|
|
274
|
+
})
|
|
275
|
+
.select()
|
|
276
|
+
.single()
|
|
277
|
+
|
|
278
|
+
if (error) {
|
|
279
|
+
throw new Error(`Failed to create pre-provisioned membership: ${error.message}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.createdIds.memberships.push(data.id)
|
|
283
|
+
|
|
284
|
+
await this.assignSystemRole(data.id, role)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
id: data.id,
|
|
288
|
+
user_id: data.user_id,
|
|
289
|
+
organization_id: data.organization_id,
|
|
290
|
+
role_slug: data.role_slug as Role
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generate a JWT token for a test user
|
|
296
|
+
* Uses Supabase JWT secret so auth.jwt() in RLS policies can decode it
|
|
297
|
+
*/
|
|
298
|
+
generateJWT(workosUserId: string, email?: string): string {
|
|
299
|
+
if (!process.env.SUPABASE_JWT_SECRET) {
|
|
300
|
+
throw new Error('SUPABASE_JWT_SECRET not configured')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const payload = {
|
|
304
|
+
sub: workosUserId, // Supabase RLS policies use auth.jwt() ->> 'sub'
|
|
305
|
+
aud: 'authenticated',
|
|
306
|
+
role: 'authenticated',
|
|
307
|
+
iat: Math.floor(Date.now() / 1000),
|
|
308
|
+
exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour expiry
|
|
309
|
+
...(email && { email }) // Add email claim if provided (for pre-provisioned user RLS)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return jwt.sign(payload, process.env.SUPABASE_JWT_SECRET, {
|
|
313
|
+
algorithm: 'HS256'
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create a Supabase client for a specific user (respects RLS)
|
|
319
|
+
* This client will have the user's JWT token, so RLS policies will apply
|
|
320
|
+
*/
|
|
321
|
+
createUserClient(workosUserId: string): SupabaseClient<Database> {
|
|
322
|
+
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
|
|
323
|
+
throw new Error('Test environment not configured')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const token = this.generateJWT(workosUserId)
|
|
327
|
+
|
|
328
|
+
// Create client with anon key and set auth token
|
|
329
|
+
const client = createClient<Database>(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
|
|
330
|
+
global: {
|
|
331
|
+
headers: {
|
|
332
|
+
Authorization: `Bearer ${token}`
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
return client
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a Supabase client for a pre-provisioned user (respects RLS)
|
|
342
|
+
* Uses a dummy workos_user_id but includes the email claim for RLS matching
|
|
343
|
+
* The email claim is what matters for pre-provisioned user RLS policies
|
|
344
|
+
*/
|
|
345
|
+
createPreProvisionedUserClient(email: string): SupabaseClient<Database> {
|
|
346
|
+
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
|
|
347
|
+
throw new Error('Test environment not configured')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Use a dummy workos_user_id that won't match any real user
|
|
351
|
+
// The email claim is what matters for pre-provisioned user RLS
|
|
352
|
+
const dummyWorkosUserId = `preprov_${this.testPrefix}_${Math.random().toString(36).substring(7)}`
|
|
353
|
+
const token = this.generateJWT(dummyWorkosUserId, email)
|
|
354
|
+
|
|
355
|
+
// Create client with anon key and set auth token
|
|
356
|
+
const client = createClient<Database>(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
|
|
357
|
+
global: {
|
|
358
|
+
headers: {
|
|
359
|
+
Authorization: `Bearer ${token}`
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
return client
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Create an organization with an admin user and authenticated client. */
|
|
368
|
+
async createOrgWithAdmin(
|
|
369
|
+
name: string,
|
|
370
|
+
email: string
|
|
371
|
+
): Promise<{
|
|
372
|
+
org: Organization
|
|
373
|
+
user: UserWithWorkosId
|
|
374
|
+
membership: Membership
|
|
375
|
+
client: SupabaseClient<Database>
|
|
376
|
+
}> {
|
|
377
|
+
const org = await this.createOrganization(name)
|
|
378
|
+
const user = await this.createUser(email, false)
|
|
379
|
+
const membership = await this.createMembership(user.id, org.id, 'admin')
|
|
380
|
+
const client = this.createUserClient(user.workos_user_id)
|
|
381
|
+
return { org, user, membership, client }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Create two isolated organizations with admin users for cross-org isolation tests. */
|
|
385
|
+
async createCrossOrgFixture(
|
|
386
|
+
nameA = 'OrgA',
|
|
387
|
+
nameB = 'OrgB'
|
|
388
|
+
): Promise<{
|
|
389
|
+
orgA: Organization
|
|
390
|
+
orgB: Organization
|
|
391
|
+
userA: UserWithWorkosId
|
|
392
|
+
userB: UserWithWorkosId
|
|
393
|
+
membershipA: Membership
|
|
394
|
+
membershipB: Membership
|
|
395
|
+
clientA: SupabaseClient<Database>
|
|
396
|
+
clientB: SupabaseClient<Database>
|
|
397
|
+
}> {
|
|
398
|
+
const orgA = await this.createOrganization(nameA)
|
|
399
|
+
const orgB = await this.createOrganization(nameB)
|
|
400
|
+
const userA = await this.createUser(`${nameA.toLowerCase()}Admin@test.com`, false)
|
|
401
|
+
const userB = await this.createUser(`${nameB.toLowerCase()}Admin@test.com`, false)
|
|
402
|
+
const membershipA = await this.createMembership(userA.id, orgA.id, 'admin')
|
|
403
|
+
const membershipB = await this.createMembership(userB.id, orgB.id, 'admin')
|
|
404
|
+
const clientA = this.createUserClient(userA.workos_user_id)
|
|
405
|
+
const clientB = this.createUserClient(userB.workos_user_id)
|
|
406
|
+
return { orgA, orgB, userA, userB, membershipA, membershipB, clientA, clientB }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Clean up all test data created during the test run
|
|
411
|
+
* Called in afterAll() to prevent test pollution
|
|
412
|
+
*
|
|
413
|
+
* Cleanup order respects foreign key dependencies (child tables before parent tables):
|
|
414
|
+
*
|
|
415
|
+
* Level 1 (Deepest dependencies):
|
|
416
|
+
* - execution_metrics (FK: execution_logs)
|
|
417
|
+
* - session_messages (FK: sessions)
|
|
418
|
+
*
|
|
419
|
+
* Level 2 (Mid-level dependencies):
|
|
420
|
+
* - execution_logs (FK: sessions, users)
|
|
421
|
+
* - task_schedules (FK: organizations)
|
|
422
|
+
* - command_queue (FK: organizations, users)
|
|
423
|
+
* - notifications (FK: organizations, users)
|
|
424
|
+
* - credentials (FK: organizations, users)
|
|
425
|
+
* - sessions (FK: organizations, users)
|
|
426
|
+
* - activities (FK: organizations)
|
|
427
|
+
*
|
|
428
|
+
* Level 3 (Organization/User dependencies):
|
|
429
|
+
* - invitations (FK: organizations, users)
|
|
430
|
+
* - api_keys (FK: organizations)
|
|
431
|
+
* - memberships (FK: organizations, users)
|
|
432
|
+
*
|
|
433
|
+
* Level 4 (Base tables):
|
|
434
|
+
* - users
|
|
435
|
+
* - organizations
|
|
436
|
+
*/
|
|
437
|
+
async cleanup(): Promise<void> {
|
|
438
|
+
const errors: string[] = []
|
|
439
|
+
|
|
440
|
+
// LEVEL 1: Delete deepest child tables first
|
|
441
|
+
|
|
442
|
+
// Delete execution_metrics (FK: execution_logs)
|
|
443
|
+
if (this.createdIds.executionMetrics.length > 0) {
|
|
444
|
+
const { error } = await this.adminClient
|
|
445
|
+
.from('execution_metrics')
|
|
446
|
+
.delete()
|
|
447
|
+
.in('execution_id', this.createdIds.executionMetrics)
|
|
448
|
+
|
|
449
|
+
if (error) {
|
|
450
|
+
errors.push(`Failed to delete execution_metrics: ${error.message}`)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Delete session_messages (FK: sessions)
|
|
455
|
+
if (this.createdIds.sessionMessages.length > 0) {
|
|
456
|
+
const { error } = await this.adminClient
|
|
457
|
+
.from('session_messages')
|
|
458
|
+
.delete()
|
|
459
|
+
.in('id', this.createdIds.sessionMessages)
|
|
460
|
+
|
|
461
|
+
if (error) {
|
|
462
|
+
errors.push(`Failed to delete session_messages: ${error.message}`)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// LEVEL 2: Delete mid-level tables
|
|
467
|
+
|
|
468
|
+
// Delete execution_logs (FK: sessions, users)
|
|
469
|
+
if (this.createdIds.executionLogs.length > 0) {
|
|
470
|
+
const { error } = await this.adminClient
|
|
471
|
+
.from('execution_logs')
|
|
472
|
+
.delete()
|
|
473
|
+
.in('execution_id', this.createdIds.executionLogs)
|
|
474
|
+
|
|
475
|
+
if (error) {
|
|
476
|
+
errors.push(`Failed to delete execution_logs: ${error.message}`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Delete task_schedules (FK: organizations)
|
|
481
|
+
if (this.createdIds.taskSchedules.length > 0) {
|
|
482
|
+
const { error } = await this.adminClient.from('task_schedules').delete().in('id', this.createdIds.taskSchedules)
|
|
483
|
+
|
|
484
|
+
if (error) {
|
|
485
|
+
errors.push(`Failed to delete task_schedules: ${error.message}`)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Delete command_queue (FK: organizations, users)
|
|
490
|
+
if (this.createdIds.commandQueue.length > 0) {
|
|
491
|
+
const { error } = await this.adminClient.from('command_queue').delete().in('id', this.createdIds.commandQueue)
|
|
492
|
+
|
|
493
|
+
if (error) {
|
|
494
|
+
errors.push(`Failed to delete command_queue: ${error.message}`)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Delete notifications (FK: organizations, users)
|
|
499
|
+
if (this.createdIds.notifications.length > 0) {
|
|
500
|
+
const { error } = await this.adminClient.from('notifications').delete().in('id', this.createdIds.notifications)
|
|
501
|
+
|
|
502
|
+
if (error) {
|
|
503
|
+
errors.push(`Failed to delete notifications: ${error.message}`)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Delete credentials (FK: organizations, users)
|
|
508
|
+
if (this.createdIds.credentials.length > 0) {
|
|
509
|
+
const { error } = await this.adminClient.from('credentials').delete().in('id', this.createdIds.credentials)
|
|
510
|
+
|
|
511
|
+
if (error) {
|
|
512
|
+
errors.push(`Failed to delete credentials: ${error.message}`)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Delete sessions (FK: organizations, users)
|
|
517
|
+
if (this.createdIds.sessions.length > 0) {
|
|
518
|
+
const { error } = await this.adminClient.from('sessions').delete().in('session_id', this.createdIds.sessions)
|
|
519
|
+
|
|
520
|
+
if (error) {
|
|
521
|
+
errors.push(`Failed to delete sessions: ${error.message}`)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Delete activities (FK: organizations)
|
|
526
|
+
if (this.createdIds.activities.length > 0) {
|
|
527
|
+
const { error } = await this.adminClient.from('activities').delete().in('id', this.createdIds.activities)
|
|
528
|
+
|
|
529
|
+
if (error) {
|
|
530
|
+
errors.push(`Failed to delete activities: ${error.message}`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// LEVEL 3: Delete organization/user relationship tables
|
|
535
|
+
|
|
536
|
+
// Delete invitations
|
|
537
|
+
if (this.createdIds.invitations.length > 0) {
|
|
538
|
+
const { error } = await this.adminClient.from('org_invitations').delete().in('id', this.createdIds.invitations)
|
|
539
|
+
|
|
540
|
+
if (error) {
|
|
541
|
+
errors.push(`Failed to delete invitations: ${error.message}`)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Delete API keys
|
|
546
|
+
if (this.createdIds.apiKeys.length > 0) {
|
|
547
|
+
const { error } = await this.adminClient.from('api_keys').delete().in('id', this.createdIds.apiKeys)
|
|
548
|
+
|
|
549
|
+
if (error) {
|
|
550
|
+
errors.push(`Failed to delete API keys: ${error.message}`)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Delete memberships
|
|
555
|
+
if (this.createdIds.memberships.length > 0) {
|
|
556
|
+
const { error } = await this.adminClient.from('org_memberships').delete().in('id', this.createdIds.memberships)
|
|
557
|
+
|
|
558
|
+
if (error) {
|
|
559
|
+
errors.push(`Failed to delete memberships: ${error.message}`)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// LEVEL 4: Delete base tables
|
|
564
|
+
|
|
565
|
+
// Delete users
|
|
566
|
+
if (this.createdIds.users.length > 0) {
|
|
567
|
+
const { error } = await this.adminClient.from('users').delete().in('id', this.createdIds.users)
|
|
568
|
+
|
|
569
|
+
if (error) {
|
|
570
|
+
errors.push(`Failed to delete users: ${error.message}`)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Delete organizations
|
|
575
|
+
if (this.createdIds.organizations.length > 0) {
|
|
576
|
+
const { error } = await this.adminClient.from('organizations').delete().in('id', this.createdIds.organizations)
|
|
577
|
+
|
|
578
|
+
if (error) {
|
|
579
|
+
errors.push(`Failed to delete organizations: ${error.message}`)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Log any cleanup errors but don't throw
|
|
584
|
+
if (errors.length > 0) {
|
|
585
|
+
console.warn('\n⚠️ Cleanup warnings:', errors.join('\n'))
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|