@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +98 -96
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app-vite-env.d.ts +4 -0
- package/dist/app-vite-env.d.ts.map +1 -0
- package/dist/app-vite-env.js +22 -0
- package/dist/app-vite-env.js.map +1 -0
- package/dist/binary-cache.d.ts +7 -5
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +28 -10
- package/dist/binary-cache.js.map +1 -1
- package/dist/cli-standalone.d.ts +10 -0
- package/dist/cli-standalone.d.ts.map +1 -0
- package/dist/cli-standalone.js +56 -0
- package/dist/cli-standalone.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +34 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +14 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/self-update.d.ts +1 -1
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +33 -9
- package/dist/commands/self-update.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/components.d.ts +2 -2
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +2 -2
- package/dist/components.js.map +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +120 -12
- package/dist/dev-compose.js.map +1 -1
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js.map +1 -1
- package/dist/realtime-launch.d.ts +11 -0
- package/dist/realtime-launch.d.ts.map +1 -0
- package/dist/realtime-launch.js +39 -0
- package/dist/realtime-launch.js.map +1 -0
- package/dist/required-host-components.js +1 -1
- package/dist/required-host-components.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +15 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/schema-push-lock.d.ts +17 -0
- package/dist/schema-push-lock.d.ts.map +1 -0
- package/dist/schema-push-lock.js +106 -0
- package/dist/schema-push-lock.js.map +1 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +18 -0
- package/dist/self-host-compose.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +125 -13
- package/dist/type-extractor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/build-release-bundle.mjs +27 -0
- package/src/app-vite-env.ts +27 -0
- package/src/binary-cache.ts +29 -11
- package/src/cli-standalone.ts +62 -0
- package/src/commands/dev.ts +38 -0
- package/src/commands/init.ts +3 -1
- package/src/commands/keys.ts +14 -0
- package/src/commands/self-update.ts +34 -13
- package/src/commands/update.ts +9 -1
- package/src/components.ts +2 -2
- package/src/dev-compose.ts +144 -14
- package/src/project-config.ts +2 -0
- package/src/realtime-launch.ts +52 -0
- package/src/required-host-components.ts +1 -1
- package/src/route-manifest.ts +17 -0
- package/src/schema-push-lock.ts +131 -0
- package/src/self-host-compose.ts +18 -0
- package/src/type-extractor.ts +156 -13
- package/tests/realtime-launch.test.ts +25 -0
- package/tests/required-host-components.test.ts +2 -0
- package/tests/route-manifest.test.ts +29 -0
- package/tests/type-extractor.test.ts +52 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/type-extractor.ts
CHANGED
|
@@ -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
|
-
|
|
486
|
-
|
|
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: {
|
|
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
|
-
|
|
550
|
+
}
|
|
551
|
+
case "HasOne": {
|
|
509
552
|
flags.relationCardinality = "one"
|
|
510
|
-
flags.relationTarget = current.typeArguments?.[0]
|
|
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: {
|
|
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
|
-
|
|
518
|
-
case "ManyToMany":
|
|
580
|
+
}
|
|
581
|
+
case "ManyToMany": {
|
|
519
582
|
flags.relationCardinality = "many"
|
|
520
|
-
flags.relationTarget = current.typeArguments?.[0]
|
|
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: {
|
|
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
|
|