@supatype/cli 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +98 -96
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app-vite-env.d.ts +4 -0
  5. package/dist/app-vite-env.d.ts.map +1 -0
  6. package/dist/app-vite-env.js +22 -0
  7. package/dist/app-vite-env.js.map +1 -0
  8. package/dist/binary-cache.d.ts +7 -5
  9. package/dist/binary-cache.d.ts.map +1 -1
  10. package/dist/binary-cache.js +28 -10
  11. package/dist/binary-cache.js.map +1 -1
  12. package/dist/cli-standalone.d.ts +10 -0
  13. package/dist/cli-standalone.d.ts.map +1 -0
  14. package/dist/cli-standalone.js +56 -0
  15. package/dist/cli-standalone.js.map +1 -0
  16. package/dist/commands/dev.d.ts.map +1 -1
  17. package/dist/commands/dev.js +34 -0
  18. package/dist/commands/dev.js.map +1 -1
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +3 -1
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/keys.d.ts.map +1 -1
  23. package/dist/commands/keys.js +14 -0
  24. package/dist/commands/keys.js.map +1 -1
  25. package/dist/commands/self-update.d.ts +1 -1
  26. package/dist/commands/self-update.d.ts.map +1 -1
  27. package/dist/commands/self-update.js +33 -9
  28. package/dist/commands/self-update.js.map +1 -1
  29. package/dist/commands/update.d.ts.map +1 -1
  30. package/dist/commands/update.js +9 -1
  31. package/dist/commands/update.js.map +1 -1
  32. package/dist/components.d.ts +2 -2
  33. package/dist/components.d.ts.map +1 -1
  34. package/dist/components.js +2 -2
  35. package/dist/components.js.map +1 -1
  36. package/dist/dev-compose.d.ts.map +1 -1
  37. package/dist/dev-compose.js +120 -12
  38. package/dist/dev-compose.js.map +1 -1
  39. package/dist/project-config.d.ts +2 -0
  40. package/dist/project-config.d.ts.map +1 -1
  41. package/dist/project-config.js.map +1 -1
  42. package/dist/realtime-launch.d.ts +11 -0
  43. package/dist/realtime-launch.d.ts.map +1 -0
  44. package/dist/realtime-launch.js +39 -0
  45. package/dist/realtime-launch.js.map +1 -0
  46. package/dist/required-host-components.js +1 -1
  47. package/dist/required-host-components.js.map +1 -1
  48. package/dist/route-manifest.d.ts +3 -0
  49. package/dist/route-manifest.d.ts.map +1 -0
  50. package/dist/route-manifest.js +15 -0
  51. package/dist/route-manifest.js.map +1 -0
  52. package/dist/schema-push-lock.d.ts +17 -0
  53. package/dist/schema-push-lock.d.ts.map +1 -0
  54. package/dist/schema-push-lock.js +106 -0
  55. package/dist/schema-push-lock.js.map +1 -0
  56. package/dist/self-host-compose.d.ts.map +1 -1
  57. package/dist/self-host-compose.js +18 -0
  58. package/dist/self-host-compose.js.map +1 -1
  59. package/dist/type-extractor.d.ts.map +1 -1
  60. package/dist/type-extractor.js +125 -13
  61. package/dist/type-extractor.js.map +1 -1
  62. package/package.json +1 -1
  63. package/scripts/build-release-bundle.mjs +27 -0
  64. package/src/app-vite-env.ts +27 -0
  65. package/src/binary-cache.ts +29 -11
  66. package/src/cli-standalone.ts +62 -0
  67. package/src/commands/dev.ts +38 -0
  68. package/src/commands/init.ts +3 -1
  69. package/src/commands/keys.ts +14 -0
  70. package/src/commands/self-update.ts +34 -13
  71. package/src/commands/update.ts +9 -1
  72. package/src/components.ts +2 -2
  73. package/src/dev-compose.ts +144 -14
  74. package/src/project-config.ts +2 -0
  75. package/src/realtime-launch.ts +52 -0
  76. package/src/required-host-components.ts +1 -1
  77. package/src/route-manifest.ts +17 -0
  78. package/src/schema-push-lock.ts +131 -0
  79. package/src/self-host-compose.ts +18 -0
  80. package/src/type-extractor.ts +156 -13
  81. package/tests/realtime-launch.test.ts +25 -0
  82. package/tests/required-host-components.test.ts +2 -0
  83. package/tests/route-manifest.test.ts +29 -0
  84. package/tests/type-extractor.test.ts +52 -0
  85. package/tsconfig.tsbuildinfo +1 -1
@@ -22,6 +22,7 @@ import {
22
22
  type ExtractedSchemaAstV2,
23
23
  type ExtractedStorageBucketAst,
24
24
  type FieldAstV2,
25
+ type KernelFieldFacts,
25
26
  type ParsedField,
26
27
  } from "./schema-ast-v2.js"
27
28
 
@@ -410,6 +411,7 @@ function parseFieldType(
410
411
  fieldDefault: undefined as string | number | boolean | null | undefined,
411
412
  localized: false,
412
413
  notLocalized: false,
414
+ checkConstraint: undefined as string | undefined,
413
415
  }
414
416
 
415
417
  const resolving = new Set<string>()
@@ -481,11 +483,46 @@ function parseFieldType(
481
483
  current = valueArg ?? current
482
484
  continue
483
485
  }
484
- case "MaxLength":
485
- case "MinLength":
486
- case "Between":
486
+ case "MaxLength": {
487
+ const max = parseNumericTypeArg(current.typeArguments?.[1], sourceFile)
488
+ if (max !== undefined) {
489
+ flags.checkConstraint = mergeCheckConstraint(
490
+ flags.checkConstraint,
491
+ `char_length("{name}") <= ${max}`,
492
+ )
493
+ }
494
+ current = current.typeArguments?.[0] ?? current
495
+ continue
496
+ }
497
+ case "MinLength": {
498
+ const min = parseNumericTypeArg(current.typeArguments?.[1], sourceFile)
499
+ if (min !== undefined) {
500
+ flags.checkConstraint = mergeCheckConstraint(
501
+ flags.checkConstraint,
502
+ `char_length("{name}") >= ${min}`,
503
+ )
504
+ }
505
+ current = current.typeArguments?.[0] ?? current
506
+ continue
507
+ }
508
+ case "Between": {
509
+ const min = parseNumericTypeArg(current.typeArguments?.[1], sourceFile)
510
+ const max = parseNumericTypeArg(current.typeArguments?.[2], sourceFile)
511
+ if (min !== undefined) {
512
+ flags.checkConstraint = mergeCheckConstraint(
513
+ flags.checkConstraint,
514
+ `"{name}"::numeric >= ${min}`,
515
+ )
516
+ }
517
+ if (max !== undefined) {
518
+ flags.checkConstraint = mergeCheckConstraint(
519
+ flags.checkConstraint,
520
+ `"{name}"::numeric <= ${max}`,
521
+ )
522
+ }
487
523
  current = current.typeArguments?.[0] ?? current
488
524
  continue
525
+ }
489
526
  case "Localized":
490
527
  flags.localized = true
491
528
  current = current.typeArguments?.[0] ?? current
@@ -494,36 +531,68 @@ function parseFieldType(
494
531
  flags.notLocalized = true
495
532
  current = current.typeArguments?.[0] ?? current
496
533
  continue
497
- case "RelatedTo":
534
+ case "RelatedTo": {
498
535
  flags.relationCardinality = "one"
499
536
  flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile)
537
+ const relOpts = parseRelationOptions(current.typeArguments?.[1], sourceFile)
500
538
  // `target` must match `ModelAst.name` to satisfy validator resolution.
501
539
  // FK column follows the field name (two relations to the same model need distinct columns).
502
540
  return emitField({
503
541
  kind: "relation",
504
- kernel: { cardinality: "belongsTo", target: flags.relationTarget! },
542
+ kernel: {
543
+ cardinality: "belongsTo",
544
+ target: flags.relationTarget!,
545
+ ...relationOptionsKernel(relOpts),
546
+ },
505
547
  db: { foreignKey: relationForeignKeyFromField(fieldName) },
506
548
  platform: flags.editorReadOnly ? { readOnly: true } : {},
507
549
  })
508
- case "HasOne":
550
+ }
551
+ case "HasOne": {
509
552
  flags.relationCardinality = "one"
510
- flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown"
553
+ flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile)
554
+ const relOpts = parseRelationOptions(current.typeArguments?.[1], sourceFile)
555
+ return emitField({
556
+ kind: "relation",
557
+ kernel: {
558
+ cardinality: "hasOne",
559
+ target: flags.relationTarget,
560
+ ...relationOptionsKernel(relOpts),
561
+ },
562
+ db: {},
563
+ platform: flags.editorReadOnly ? { readOnly: true } : {},
564
+ })
565
+ }
566
+ case "HasMany": {
567
+ flags.relationCardinality = "many"
568
+ flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile)
569
+ const relOpts = parseRelationOptions(current.typeArguments?.[1], sourceFile)
511
570
  return emitField({
512
571
  kind: "relation",
513
- kernel: { cardinality: "hasOne", target: flags.relationTarget },
572
+ kernel: {
573
+ cardinality: "hasMany",
574
+ target: flags.relationTarget,
575
+ ...relationOptionsKernel(relOpts),
576
+ },
514
577
  db: {},
515
578
  platform: flags.editorReadOnly ? { readOnly: true } : {},
516
579
  })
517
- case "HasMany":
518
- case "ManyToMany":
580
+ }
581
+ case "ManyToMany": {
519
582
  flags.relationCardinality = "many"
520
- flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown"
583
+ flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile)
584
+ const relOpts = parseRelationOptions(current.typeArguments?.[1], sourceFile)
521
585
  return emitField({
522
586
  kind: "relation",
523
- kernel: { cardinality: "hasMany", target: flags.relationTarget },
587
+ kernel: {
588
+ cardinality: "manyToMany",
589
+ target: flags.relationTarget,
590
+ ...relationOptionsKernel(relOpts),
591
+ },
524
592
  db: {},
525
593
  platform: flags.editorReadOnly ? { readOnly: true } : {},
526
594
  })
595
+ }
527
596
  default: {
528
597
  const resolved = tryResolveTypeReference(current, sourceFile, resolveCtx, { fieldName, resolving })
529
598
  if (resolved) {
@@ -642,6 +711,13 @@ function parseFieldType(
642
711
  parsed = { ...parsed, kernel }
643
712
  }
644
713
 
714
+ if (flags.checkConstraint !== undefined) {
715
+ parsed = {
716
+ ...parsed,
717
+ kernel: { ...parsed.kernel, check: flags.checkConstraint },
718
+ }
719
+ }
720
+
645
721
  return emitField(finalizeParsedField(parsed, flags, context))
646
722
  }
647
723
 
@@ -1534,7 +1610,7 @@ function parseModelIndexes(
1534
1610
  const indexes: unknown[] = []
1535
1611
  for (const element of indexesProp.type.elements) {
1536
1612
  if (!ts.isTypeLiteralNode(element)) continue
1537
- const indexDef: Record<string, unknown> = { using: "btree" }
1613
+ const indexDef: Record<string, unknown> = { using: "btree", unique: false }
1538
1614
  for (const member of element.members) {
1539
1615
  if (!ts.isPropertySignature(member) || !member.type) continue
1540
1616
  const key = getPropertyName(member.name)
@@ -1628,6 +1704,73 @@ function parseAccessRule(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): Reco
1628
1704
  }
1629
1705
  }
1630
1706
 
1707
+ interface ParsedRelationOptions {
1708
+ required?: boolean
1709
+ onDelete?: string
1710
+ onUpdate?: string
1711
+ through?: string
1712
+ }
1713
+
1714
+ function parseRelationOptions(
1715
+ optsArg: ts.TypeNode | undefined,
1716
+ sourceFile: ts.SourceFile,
1717
+ ): ParsedRelationOptions {
1718
+ const out: ParsedRelationOptions = {}
1719
+ if (!optsArg || !ts.isTypeLiteralNode(optsArg)) return out
1720
+
1721
+ for (const member of optsArg.members) {
1722
+ if (!ts.isPropertySignature(member) || !member.name || !member.type) continue
1723
+ const key = getPropertyName(member.name)
1724
+ if (!key) continue
1725
+
1726
+ if (key === "required" && isBooleanLiteralType(member.type, true)) {
1727
+ out.required = true
1728
+ continue
1729
+ }
1730
+
1731
+ if (
1732
+ (key === "onDelete" || key === "onUpdate" || key === "through") &&
1733
+ ts.isLiteralTypeNode(member.type) &&
1734
+ ts.isStringLiteral(member.type.literal)
1735
+ ) {
1736
+ out[key] = member.type.literal.text
1737
+ }
1738
+ }
1739
+
1740
+ return out
1741
+ }
1742
+
1743
+ function relationOptionsKernel(
1744
+ opts: ParsedRelationOptions,
1745
+ ): Pick<KernelFieldFacts, "required" | "onDelete" | "onUpdate" | "through"> {
1746
+ return {
1747
+ ...(opts.required === true && { required: true }),
1748
+ ...(opts.onDelete !== undefined && { onDelete: opts.onDelete }),
1749
+ ...(opts.onUpdate !== undefined && { onUpdate: opts.onUpdate }),
1750
+ ...(opts.through !== undefined && { through: opts.through }),
1751
+ }
1752
+ }
1753
+
1754
+ function parseNumericTypeArg(typeArg: ts.TypeNode | undefined, sourceFile: ts.SourceFile): number | undefined {
1755
+ if (!typeArg) return undefined
1756
+ if (ts.isLiteralTypeNode(typeArg) && ts.isNumericLiteral(typeArg.literal)) {
1757
+ const value = Number(typeArg.literal.text)
1758
+ return Number.isFinite(value) ? value : undefined
1759
+ }
1760
+ if (ts.isLiteralTypeNode(typeArg) && ts.isStringLiteral(typeArg.literal)) {
1761
+ const value = Number(typeArg.literal.text)
1762
+ return Number.isFinite(value) ? value : undefined
1763
+ }
1764
+ const raw = typeArg.getText(sourceFile).trim()
1765
+ const value = Number(raw)
1766
+ return Number.isFinite(value) ? value : undefined
1767
+ }
1768
+
1769
+ function mergeCheckConstraint(existing: string | undefined, next: string): string {
1770
+ if (!existing) return next
1771
+ return `(${existing}) AND (${next})`
1772
+ }
1773
+
1631
1774
  function relationTargetFromTypeArg(typeArg: ts.TypeNode | undefined, sourceFile: ts.SourceFile): string {
1632
1775
  if (!typeArg) return "unknown"
1633
1776
  const raw = typeArg.getText(sourceFile).replace(/\s/g, "")
@@ -0,0 +1,25 @@
1
+ import { mkdtempSync, rmSync } from "node:fs"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { describe, expect, it } from "vitest"
5
+ import { resolveRealtimeLaunch } from "../src/realtime-launch.js"
6
+ import type { SupatypeProjectConfig } from "../src/project-config.js"
7
+
8
+ describe("resolveRealtimeLaunch", () => {
9
+ it("uses overrides.realtime .js entry via node", async () => {
10
+ const dir = mkdtempSync(join(tmpdir(), "supatype-rt-launch-"))
11
+ try {
12
+ const entry = join(dir, "fake-realtime.js")
13
+ const { writeFileSync } = await import("node:fs")
14
+ writeFileSync(entry, "export {}\n")
15
+ const config = {
16
+ overrides: { realtime: entry },
17
+ } as SupatypeProjectConfig
18
+ const spec = await resolveRealtimeLaunch(config, dir)
19
+ expect(spec.bin).toBe(process.execPath)
20
+ expect(spec.args).toEqual([entry])
21
+ } finally {
22
+ rmSync(dir, { recursive: true, force: true })
23
+ }
24
+ })
25
+ })
@@ -33,6 +33,7 @@ describe("requiredHostComponents", () => {
33
33
  "engine",
34
34
  "server",
35
35
  "postgres",
36
+ "realtime",
36
37
  ])
37
38
  rmSync(dir, { recursive: true, force: true })
38
39
  })
@@ -44,6 +45,7 @@ describe("requiredHostComponents", () => {
44
45
  "engine",
45
46
  "server",
46
47
  "postgres",
48
+ "realtime",
47
49
  "deno",
48
50
  ])
49
51
  rmSync(dir, { recursive: true, force: true })
@@ -0,0 +1,29 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { describe, expect, it } from "vitest"
5
+ import { patchRouteManifest } from "../src/route-manifest.js"
6
+
7
+ describe("patchRouteManifest", () => {
8
+ it("merges realtime fields into existing manifest", () => {
9
+ const dir = mkdtempSync(join(tmpdir(), "supatype-manifest-"))
10
+ const path = join(dir, "manifest.json")
11
+ try {
12
+ patchRouteManifest(path, { realtime_enabled: true, realtime_url: "http://127.0.0.1:4000" })
13
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>
14
+ expect(parsed).toMatchObject({
15
+ realtime_enabled: true,
16
+ realtime_url: "http://127.0.0.1:4000",
17
+ })
18
+ patchRouteManifest(path, { schema: "public" })
19
+ const again = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>
20
+ expect(again).toMatchObject({
21
+ schema: "public",
22
+ realtime_enabled: true,
23
+ realtime_url: "http://127.0.0.1:4000",
24
+ })
25
+ } finally {
26
+ rmSync(dir, { recursive: true, force: true })
27
+ }
28
+ })
29
+ })
@@ -635,6 +635,7 @@ export type Message = Model<{
635
635
  kind: "relation",
636
636
  cardinality: "belongsTo",
637
637
  target: "Profile",
638
+ required: true,
638
639
  annotations: { db: { foreignKey: "created_by_id" } },
639
640
  })
640
641
  expect(room?.fields["created_at"]).toMatchObject({ kind: "datetime" })
@@ -647,12 +648,63 @@ export type Message = Model<{
647
648
  kind: "relation",
648
649
  cardinality: "belongsTo",
649
650
  target: "Room",
651
+ required: true,
652
+ onDelete: "cascade",
650
653
  annotations: { db: { foreignKey: "room_id" } },
651
654
  })
652
655
  expect(message?.fields["author"]).toMatchObject({
653
656
  kind: "relation",
654
657
  cardinality: "belongsTo",
655
658
  target: "Profile",
659
+ required: true,
660
+ })
661
+ expect(message?.fields["body"]).toMatchObject({
662
+ kind: "text",
663
+ check: 'char_length("{name}") <= 2000',
664
+ })
665
+ const messageIndexes = message?.annotations.db.indexes as Array<Record<string, unknown>>
666
+ expect(messageIndexes).toHaveLength(1)
667
+ expect(messageIndexes?.[0]).toMatchObject({
668
+ using: "btree",
669
+ unique: false,
670
+ fields: ["room_id", "created_at"],
671
+ })
672
+ })
673
+
674
+ it("extracts ManyToMany relations with through table option", () => {
675
+ const dir = mkdtempSync(join(tmpdir(), "supatype-many-to-many-"))
676
+ dirs.push(dir)
677
+ const schemaPath = join(dir, "schema.ts")
678
+ writeFileSync(
679
+ schemaPath,
680
+ `
681
+ import type { Model, UUID, ManyToMany, Public, LoggedIn } from "@supatype/types"
682
+
683
+ export type Course = Model<{
684
+ id: UUID
685
+ title: string
686
+ }, {
687
+ access: { read: Public; create: LoggedIn }
688
+ }>
689
+
690
+ export type Student = Model<{
691
+ id: UUID
692
+ name: string
693
+ courses: ManyToMany<Course, { through: "enrollments" }>
694
+ }, {
695
+ access: { read: Public; create: LoggedIn }
696
+ }>
697
+ `,
698
+ "utf8",
699
+ )
700
+
701
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
702
+ const student = ast?.models.find((m) => m.name === "Student")
703
+ expect(student?.fields["courses"]).toMatchObject({
704
+ kind: "relation",
705
+ cardinality: "manyToMany",
706
+ target: "Course",
707
+ through: "enrollments",
656
708
  })
657
709
  })
658
710