spine-framework 0.3.95 → 0.3.97

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.
@@ -22,6 +22,7 @@ import { existsSync, readFileSync, mkdirSync, cpSync, writeFileSync } from 'fs'
22
22
  import { resolve, dirname, join, basename } from 'path'
23
23
  import { fileURLToPath } from 'url'
24
24
  import { execSync } from 'child_process'
25
+ import * as readline from 'readline'
25
26
  import { createClient } from '@supabase/supabase-js'
26
27
  import { runMigrations } from './migrate.js'
27
28
 
@@ -388,6 +389,9 @@ async function seedApp(appDir: string, appSlug: string) {
388
389
  table: string
389
390
  conflict: string
390
391
  inject_app_id?: boolean
392
+ // When true: only patch design_schema.record_permissions on existing types.
393
+ // The record must have { kind, slug, record_permissions } — no app_id injection.
394
+ permissions_patch?: boolean
391
395
  }
392
396
 
393
397
  const seedEntries: SeedEntry[] = Array.isArray(manifest.seed)
@@ -399,6 +403,18 @@ async function seedApp(appDir: string, appSlug: string) {
399
403
  { file: 'seed/link-types.json', table: 'link_types', conflict: 'app_id,slug' },
400
404
  ]
401
405
 
406
+ // Always append the permissions patch file if it exists, unless already declared
407
+ const hasPermissionsPatch = seedEntries.some(e => e.permissions_patch)
408
+ if (!hasPermissionsPatch) {
409
+ seedEntries.push({
410
+ file: 'seed/type_permissions.json',
411
+ table: 'types',
412
+ conflict: 'kind,slug',
413
+ inject_app_id: false,
414
+ permissions_patch: true,
415
+ })
416
+ }
417
+
402
418
  for (const entry of seedEntries) {
403
419
  const filePath = join(appDir, entry.file)
404
420
  if (!existsSync(filePath)) {
@@ -419,6 +435,70 @@ async function seedApp(appDir: string, appSlug: string) {
419
435
  continue
420
436
  }
421
437
 
438
+ // ── type_permissions.json: targeted record_permissions patch with check-and-choose ──
439
+ if (entry.permissions_patch) {
440
+ console.log(` Patching record_permissions for ${records.length} type(s) from ${entry.file}...`)
441
+ for (const record of records) {
442
+ const { kind, slug, record_permissions: newPerms } = record
443
+ if (!kind || !slug || !newPerms) {
444
+ console.warn(` ⚠ Skipping malformed entry (requires kind, slug, record_permissions)`)
445
+ continue
446
+ }
447
+
448
+ // Fetch the existing type (match on kind+slug regardless of app_id)
449
+ const { data: existing } = await db
450
+ .from('types')
451
+ .select('id, design_schema')
452
+ .eq('kind', kind)
453
+ .eq('slug', slug)
454
+ .maybeSingle()
455
+
456
+ if (!existing) {
457
+ console.log(` ○ ${kind}/${slug} not found in DB — skipping (install the owning app first)`)
458
+ continue
459
+ }
460
+
461
+ const existingSchema = (existing.design_schema ?? {}) as Record<string, any>
462
+ const existingPerms = existingSchema.record_permissions ?? null
463
+
464
+ // Deep-equal check
465
+ if (JSON.stringify(existingPerms) === JSON.stringify(newPerms)) {
466
+ console.log(` ✓ ${kind}/${slug} record_permissions unchanged — skipped`)
467
+ continue
468
+ }
469
+
470
+ // Show diff and prompt
471
+ console.log(`\n Type: ${kind}/${slug}`)
472
+ console.log(` Existing record_permissions:`)
473
+ console.log(' ' + JSON.stringify(existingPerms, null, 2).replace(/\n/g, '\n '))
474
+ console.log(` New record_permissions:`)
475
+ console.log(' ' + JSON.stringify(newPerms, null, 2).replace(/\n/g, '\n '))
476
+
477
+ const choice = await prompt(' Replace? [R]eplace / [K]eep → ')
478
+ if (choice.trim().toLowerCase() !== 'r') {
479
+ console.log(` ○ Kept existing`)
480
+ continue
481
+ }
482
+
483
+ // Targeted JSON merge: only update record_permissions key in design_schema
484
+ const { error: patchErr } = await db
485
+ .from('types')
486
+ .update({
487
+ design_schema: { ...existingSchema, record_permissions: newPerms },
488
+ })
489
+ .eq('id', existing.id)
490
+
491
+ if (patchErr) {
492
+ console.warn(` ⚠ ${kind}/${slug}: ${patchErr.message}`)
493
+ } else {
494
+ console.log(` ✓ ${kind}/${slug} record_permissions updated`)
495
+ }
496
+ }
497
+ console.log(` ✓ type_permissions patch complete`)
498
+ continue
499
+ }
500
+
501
+ // ── Standard seed: upsert whole record ────────────────────────────────────
422
502
  const injectAppId = entry.inject_app_id !== false // default true
423
503
  console.log(` Seeding ${records.length} record(s) from ${entry.file} → ${entry.table}...`)
424
504
 
@@ -437,6 +517,18 @@ async function seedApp(appDir: string, appSlug: string) {
437
517
  }
438
518
  }
439
519
 
520
+ // ─── PROMPT HELPER ──────────────────────────────────────────────────────────
521
+
522
+ function prompt(question: string): Promise<string> {
523
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
524
+ return new Promise(resolve => {
525
+ rl.question(question, answer => {
526
+ rl.close()
527
+ resolve(answer)
528
+ })
529
+ })
530
+ }
531
+
440
532
  // ─── SPINE CONFIG HELPERS ───────────────────────────────────────────────────
441
533
 
442
534
  interface RegistrationConfig {
@@ -152,7 +152,7 @@ export const validateToken = createHandler(async (_ctx, _body) => {
152
152
 
153
153
  const { data: person } = await adminDb
154
154
  .from('people')
155
- .select('id, email, account_id, role_id, status, data, account:accounts(display_name), role:roles(slug, name)')
155
+ .select('id, email, account_id, role_id, status, data, account:accounts!people_account_id_fkey(display_name), role:roles(slug, name)')
156
156
  .filter('data->>invite_token', 'eq', token)
157
157
  .in('status', ['invited', 'pending'])
158
158
  .single()
@@ -602,7 +602,7 @@ export const listInvites = createHandler(async (ctx, _body) => {
602
602
  .select(`
603
603
  id, email, full_name, status, created_at, updated_at,
604
604
  account_id, role_id,
605
- account:accounts(id, slug, display_name),
605
+ account:accounts!people_account_id_fkey(id, slug, display_name),
606
606
  role:roles(id, slug, name),
607
607
  data
608
608
  `)
@@ -87,9 +87,11 @@
87
87
  }
88
88
  },
89
89
  "record_permissions": {
90
- "system-admin": ["create", "read", "update", "delete"],
91
- "support": ["create", "read", "update"],
92
- "member": ["create", "read"]
90
+ "system_admin": ["create", "read", "update", "delete"],
91
+ "support_admin": ["create", "read", "update"],
92
+ "support": ["create", "read", "update"],
93
+ "member_admin": ["create", "read", "update"],
94
+ "member": ["create", "read", "update"]
93
95
  }
94
96
  },
95
97
  "validation_schema": {},
@@ -98,9 +98,11 @@
98
98
  }
99
99
  },
100
100
  "record_permissions": {
101
- "system-admin": ["create", "read", "update", "delete"],
102
- "support": ["create", "read", "update"],
103
- "member": ["create", "read", "update"]
101
+ "system_admin": ["create", "read", "update", "delete"],
102
+ "support_admin": ["create", "read", "update"],
103
+ "support": ["create", "read", "update"],
104
+ "member_admin": ["create", "read", "update"],
105
+ "member": ["create", "read", "update"]
104
106
  }
105
107
  },
106
108
  "validation_schema": {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.95",
3
+ "version": "0.3.97",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {