@supatype/cli 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +67 -62
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app/proxy-dev-app.d.ts +13 -0
- package/dist/app/proxy-dev-app.d.ts.map +1 -0
- package/dist/app/proxy-dev-app.js +53 -0
- package/dist/app/proxy-dev-app.js.map +1 -0
- package/dist/binary-cache.d.ts +5 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +13 -0
- package/dist/binary-cache.js.map +1 -1
- package/dist/commands/cloud.d.ts +11 -3
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +33 -25
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -17
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -3
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +66 -59
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +11 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/init.js +16 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +42 -12
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +16 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/dev-compose.d.ts +17 -0
- package/dist/dev-compose.d.ts.map +1 -0
- package/dist/dev-compose.js +374 -0
- package/dist/dev-compose.js.map +1 -0
- package/dist/diff-output.d.ts +4 -0
- package/dist/diff-output.d.ts.map +1 -0
- package/dist/diff-output.js +12 -0
- package/dist/diff-output.js.map +1 -0
- package/dist/docker-postgres.d.ts +21 -3
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +130 -18
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-client.d.ts +5 -3
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +2 -1
- package/dist/engine-client.js.map +1 -1
- package/dist/kong-config.d.ts +4 -0
- package/dist/kong-config.d.ts.map +1 -1
- package/dist/kong-config.js +12 -1
- package/dist/kong-config.js.map +1 -1
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +16 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/project-config.d.ts +21 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +15 -0
- package/dist/project-config.js.map +1 -1
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +75 -12
- package/dist/runtime-routes.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +127 -0
- package/dist/schema-ast-v2.d.ts.map +1 -0
- package/dist/schema-ast-v2.js +226 -0
- package/dist/schema-ast-v2.js.map +1 -0
- package/dist/self-host-compose.d.ts +12 -4
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +146 -35
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +7 -0
- package/dist/studio-admin-roles.d.ts.map +1 -0
- package/dist/studio-admin-roles.js +14 -0
- package/dist/studio-admin-roles.js.map +1 -0
- package/dist/studio-dev-server.d.ts +22 -0
- package/dist/studio-dev-server.d.ts.map +1 -0
- package/dist/studio-dev-server.js +28 -0
- package/dist/studio-dev-server.js.map +1 -0
- package/dist/type-extractor.d.ts +3 -30
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +485 -148
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-resolver.d.ts +33 -0
- package/dist/type-resolver.d.ts.map +1 -0
- package/dist/type-resolver.js +338 -0
- package/dist/type-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/TYPE-RESOLUTION.md +294 -0
- package/src/app/proxy-dev-app.ts +67 -0
- package/src/binary-cache.ts +20 -0
- package/src/commands/cloud.ts +40 -30
- package/src/commands/deploy.ts +3 -18
- package/src/commands/dev.ts +72 -69
- package/src/commands/diff.ts +11 -1
- package/src/commands/init.ts +16 -3
- package/src/commands/push.ts +49 -13
- package/src/commands/update.ts +17 -0
- package/src/dev-compose.ts +455 -0
- package/src/diff-output.ts +12 -0
- package/src/docker-postgres.ts +184 -27
- package/src/engine-client.ts +9 -4
- package/src/kong-config.ts +16 -1
- package/src/process-manager.ts +18 -1
- package/src/project-config.ts +34 -1
- package/src/runtime-routes.ts +87 -12
- package/src/schema-ast-v2.ts +324 -0
- package/src/self-host-compose.ts +168 -36
- package/src/studio-admin-roles.ts +16 -0
- package/src/studio-dev-server.ts +53 -0
- package/src/type-extractor.ts +649 -186
- package/src/type-resolver.ts +457 -0
- package/tests/config.test.ts +34 -3
- package/tests/docker-postgres.test.ts +39 -0
- package/tests/normalize-admin-config.test.ts +48 -0
- package/tests/proxy-dev-app.test.ts +33 -0
- package/tests/runtime-contract.test.ts +119 -4
- package/tests/studio-admin-roles.test.ts +27 -0
- package/tests/type-extractor.test.ts +607 -23
- package/tests/type-resolver.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,10 +2,19 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest"
|
|
5
|
+
import type { ModelAstV2 } from "../src/schema-ast-v2.js"
|
|
5
6
|
import { extractSchemaAstFromTypes } from "../src/type-extractor.js"
|
|
6
7
|
|
|
7
8
|
const dirs: string[] = []
|
|
8
9
|
|
|
10
|
+
function tableName(model: ModelAstV2 | undefined): string | undefined {
|
|
11
|
+
return model?.annotations.db.tableName
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function modelAccess(model: ModelAstV2 | undefined): Record<string, unknown> {
|
|
15
|
+
return model?.annotations.platform.access ?? {}
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
afterEach(() => {
|
|
10
19
|
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
11
20
|
})
|
|
@@ -42,27 +51,32 @@ export type Comment = Model<{
|
|
|
42
51
|
|
|
43
52
|
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
44
53
|
expect(ast).not.toBeNull()
|
|
54
|
+
expect(ast?.astVersion).toBe(2)
|
|
45
55
|
expect(ast?.models).toHaveLength(2)
|
|
46
56
|
const post = ast?.models.find((m) => m.name === "Post")
|
|
47
57
|
const comment = ast?.models.find((m) => m.name === "Comment")
|
|
48
|
-
expect(post
|
|
58
|
+
expect(tableName(post)).toBe("post")
|
|
49
59
|
expect(comment?.fields["post"]).toMatchObject({
|
|
50
60
|
kind: "relation",
|
|
51
61
|
cardinality: "belongsTo",
|
|
52
62
|
target: "Post",
|
|
53
|
-
foreignKey: "post_id",
|
|
63
|
+
annotations: { db: { foreignKey: "post_id" } },
|
|
54
64
|
})
|
|
55
|
-
expect(post?.fields["id"]).toMatchObject({ kind: "uuid", pgType: "UUID" })
|
|
56
65
|
expect(post?.fields["id"]).toMatchObject({
|
|
66
|
+
kind: "uuid",
|
|
67
|
+
annotations: { db: { pgType: "UUID", unique: true } },
|
|
57
68
|
primaryKey: true,
|
|
58
|
-
unique: true,
|
|
59
69
|
required: true,
|
|
60
70
|
default: { kind: "genRandomUuid" },
|
|
61
71
|
})
|
|
62
|
-
expect(post?.fields["slug"]).toMatchObject({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
expect(post?.fields["slug"]).toMatchObject({
|
|
73
|
+
kind: "slug",
|
|
74
|
+
from: "title",
|
|
75
|
+
annotations: { db: { unique: true } },
|
|
76
|
+
})
|
|
77
|
+
expect(modelAccess(post)["read"]).toEqual({ type: "public" })
|
|
78
|
+
expect(modelAccess(post)["update"]).toEqual({ type: "owner", field: "author_id" })
|
|
79
|
+
expect(modelAccess(post)["delete"]).toEqual({ type: "owner", field: "author_id" })
|
|
66
80
|
})
|
|
67
81
|
|
|
68
82
|
it("emits DEFAULT now for created_at / updated_at timestamp columns", () => {
|
|
@@ -83,12 +97,12 @@ export type Entry = Model<{ id: UUID; created_at: Timestamp; updated_at: Timesta
|
|
|
83
97
|
const entry = ast?.models.find((m) => m.name === "Entry")
|
|
84
98
|
expect(entry?.fields["created_at"]).toMatchObject({
|
|
85
99
|
kind: "datetime",
|
|
86
|
-
serverGenerated: true,
|
|
100
|
+
annotations: { db: { serverGenerated: true, pgType: "TIMESTAMP WITH TIME ZONE" } },
|
|
87
101
|
default: { kind: "now" },
|
|
88
102
|
})
|
|
89
103
|
expect(entry?.fields["updated_at"]).toMatchObject({
|
|
90
104
|
kind: "datetime",
|
|
91
|
-
serverGenerated: true,
|
|
105
|
+
annotations: { db: { serverGenerated: true, pgType: "TIMESTAMP WITH TIME ZONE" } },
|
|
92
106
|
default: { kind: "now" },
|
|
93
107
|
})
|
|
94
108
|
})
|
|
@@ -113,8 +127,8 @@ export type User = Model<{
|
|
|
113
127
|
|
|
114
128
|
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
115
129
|
const user = ast?.models.find((m) => m.name === "User")
|
|
116
|
-
expect(user
|
|
117
|
-
expect(user
|
|
130
|
+
expect(modelAccess(user)["update"]).toEqual({ type: "owner", field: "id" })
|
|
131
|
+
expect(modelAccess(user)["delete"]).toEqual({ type: "owner", field: "id" })
|
|
118
132
|
})
|
|
119
133
|
|
|
120
134
|
it("maps SupatypeAuthUser relations and OwnerFrom relation keys", () => {
|
|
@@ -142,10 +156,10 @@ export type Post = Model<{
|
|
|
142
156
|
kind: "relation",
|
|
143
157
|
cardinality: "belongsTo",
|
|
144
158
|
target: "supatype:user",
|
|
145
|
-
foreignKey: "auth_user_id",
|
|
159
|
+
annotations: { db: { foreignKey: "auth_user_id" } },
|
|
146
160
|
})
|
|
147
|
-
expect(post
|
|
148
|
-
expect(post
|
|
161
|
+
expect(modelAccess(post)["update"]).toEqual({ type: "owner", field: "authUser" })
|
|
162
|
+
expect(modelAccess(post)["delete"]).toEqual({ type: "owner", field: "authUser" })
|
|
149
163
|
})
|
|
150
164
|
|
|
151
165
|
it("unwraps Default<> so boolean fields stay boolean in the AST", () => {
|
|
@@ -165,7 +179,85 @@ export type Flags = Model<{
|
|
|
165
179
|
"utf8",
|
|
166
180
|
)
|
|
167
181
|
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
168
|
-
expect(ast?.models[0]?.fields["isActive"]).toMatchObject({
|
|
182
|
+
expect(ast?.models[0]?.fields["isActive"]).toMatchObject({
|
|
183
|
+
kind: "boolean",
|
|
184
|
+
annotations: { db: { pgType: "BOOLEAN" } },
|
|
185
|
+
default: { kind: "value", value: true },
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("extracts Default<> literal values for scalars and RichText plain-string defaults", () => {
|
|
190
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-defaults-"))
|
|
191
|
+
dirs.push(dir)
|
|
192
|
+
const schemaPath = join(dir, "schema.ts")
|
|
193
|
+
writeFileSync(
|
|
194
|
+
schemaPath,
|
|
195
|
+
`
|
|
196
|
+
import type { Model, UUID, Default, Int, RichText } from "@supatype/types"
|
|
197
|
+
|
|
198
|
+
export type Product = Model<{
|
|
199
|
+
id: UUID
|
|
200
|
+
stock: Default<Int, 0>
|
|
201
|
+
blurb: Default<RichText, "Welcome to our shop.">
|
|
202
|
+
}>
|
|
203
|
+
`,
|
|
204
|
+
"utf8",
|
|
205
|
+
)
|
|
206
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
207
|
+
expect(ast?.models[0]?.fields["stock"]).toMatchObject({
|
|
208
|
+
kind: "integer",
|
|
209
|
+
default: { kind: "value", value: 0 },
|
|
210
|
+
})
|
|
211
|
+
expect(ast?.models[0]?.fields["blurb"]).toMatchObject({
|
|
212
|
+
kind: "richText",
|
|
213
|
+
annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
|
|
214
|
+
default: { kind: "value", value: "Welcome to our shop." },
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it("extracts RichText<\"…\"> inline default (equivalent to Default<RichText, \"…\">)", () => {
|
|
219
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-richtext-inline-"))
|
|
220
|
+
dirs.push(dir)
|
|
221
|
+
const schemaPath = join(dir, "schema.ts")
|
|
222
|
+
writeFileSync(
|
|
223
|
+
schemaPath,
|
|
224
|
+
`
|
|
225
|
+
import type { Model, UUID, RichText } from "@supatype/types"
|
|
226
|
+
|
|
227
|
+
export type Page = Model<{
|
|
228
|
+
id: UUID
|
|
229
|
+
intro: RichText<"Welcome to Elmside.">
|
|
230
|
+
}>
|
|
231
|
+
`,
|
|
232
|
+
"utf8",
|
|
233
|
+
)
|
|
234
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
235
|
+
expect(ast?.models[0]?.fields["intro"]).toMatchObject({
|
|
236
|
+
kind: "richText",
|
|
237
|
+
annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
|
|
238
|
+
default: { kind: "value", value: "Welcome to Elmside." },
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("errors when RichText inline default and Default<> are both set", () => {
|
|
243
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-richtext-double-default-"))
|
|
244
|
+
dirs.push(dir)
|
|
245
|
+
const schemaPath = join(dir, "schema.ts")
|
|
246
|
+
writeFileSync(
|
|
247
|
+
schemaPath,
|
|
248
|
+
`
|
|
249
|
+
import type { Model, UUID, Default, RichText } from "@supatype/types"
|
|
250
|
+
|
|
251
|
+
export type Page = Model<{
|
|
252
|
+
id: UUID
|
|
253
|
+
intro: Default<RichText<"a">, "b">
|
|
254
|
+
}>
|
|
255
|
+
`,
|
|
256
|
+
"utf8",
|
|
257
|
+
)
|
|
258
|
+
expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(
|
|
259
|
+
/either Default<…> or an inline type default/,
|
|
260
|
+
)
|
|
169
261
|
})
|
|
170
262
|
|
|
171
263
|
it("extracts Bucket<> config into storageBuckets and field accessMode", () => {
|
|
@@ -279,9 +371,15 @@ export type Comment = Model<{
|
|
|
279
371
|
)
|
|
280
372
|
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
281
373
|
const comment = ast?.models.find((m) => m.name === "Comment")
|
|
282
|
-
expect(comment?.fields["author"]).toMatchObject({
|
|
283
|
-
|
|
284
|
-
|
|
374
|
+
expect(comment?.fields["author"]).toMatchObject({
|
|
375
|
+
annotations: { db: { foreignKey: "author_id" } },
|
|
376
|
+
})
|
|
377
|
+
expect(comment?.fields["userId"]).toMatchObject({
|
|
378
|
+
annotations: { db: { foreignKey: "user_id" } },
|
|
379
|
+
})
|
|
380
|
+
expect(comment?.fields["customerID"]).toMatchObject({
|
|
381
|
+
annotations: { db: { foreignKey: "customer_id" } },
|
|
382
|
+
})
|
|
285
383
|
})
|
|
286
384
|
|
|
287
385
|
it("extracts EditorReadOnly wrapper as readOnly field metadata", () => {
|
|
@@ -305,8 +403,14 @@ export type Doc = Model<{
|
|
|
305
403
|
)
|
|
306
404
|
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
307
405
|
const doc = ast?.models.find((m) => m.name === "Doc")
|
|
308
|
-
expect(doc?.fields["title"]).toMatchObject({
|
|
309
|
-
|
|
406
|
+
expect(doc?.fields["title"]).toMatchObject({
|
|
407
|
+
kind: "text",
|
|
408
|
+
annotations: { platform: { readOnly: true } },
|
|
409
|
+
})
|
|
410
|
+
expect(doc?.fields["owner"]).toMatchObject({
|
|
411
|
+
kind: "relation",
|
|
412
|
+
annotations: { platform: { readOnly: true } },
|
|
413
|
+
})
|
|
310
414
|
})
|
|
311
415
|
|
|
312
416
|
it("extracts Computed wrapper as readOnly + serverGenerated metadata", () => {
|
|
@@ -330,8 +434,7 @@ export type Doc = Model<{
|
|
|
330
434
|
expect(doc?.fields["summary"]).toMatchObject({
|
|
331
435
|
kind: "text",
|
|
332
436
|
required: false,
|
|
333
|
-
readOnly: true,
|
|
334
|
-
serverGenerated: true,
|
|
437
|
+
annotations: { db: { serverGenerated: true }, platform: { readOnly: true } },
|
|
335
438
|
})
|
|
336
439
|
})
|
|
337
440
|
|
|
@@ -398,4 +501,485 @@ export type Note = Model<{
|
|
|
398
501
|
})
|
|
399
502
|
expect(new Set(summary?.sources ?? [])).toEqual(new Set(["author", "published_at", "description"]))
|
|
400
503
|
})
|
|
504
|
+
|
|
505
|
+
it("extracts singleton: true with default _global_ table name", () => {
|
|
506
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-singleton-"))
|
|
507
|
+
dirs.push(dir)
|
|
508
|
+
const schemaPath = join(dir, "schema.ts")
|
|
509
|
+
writeFileSync(
|
|
510
|
+
schemaPath,
|
|
511
|
+
`
|
|
512
|
+
import type { Model, UUID, Public, Role, Timestamp } from "@supatype/types"
|
|
513
|
+
|
|
514
|
+
export type SiteSettings = Model<{
|
|
515
|
+
id: UUID
|
|
516
|
+
site_name: string
|
|
517
|
+
created_at: Timestamp
|
|
518
|
+
updated_at: Timestamp
|
|
519
|
+
}, {
|
|
520
|
+
singleton: true
|
|
521
|
+
access: { read: Public; update: Role<"supatype_admin"> }
|
|
522
|
+
}>
|
|
523
|
+
`,
|
|
524
|
+
"utf8",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
528
|
+
const settings = ast?.models.find((m) => m.name === "SiteSettings")
|
|
529
|
+
expect(tableName(settings)).toBe("_global_site_settings")
|
|
530
|
+
expect(settings?.options).toMatchObject({ singleton: true, timestamps: true })
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it("respects tableName override on singleton models", () => {
|
|
534
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-singleton-table-"))
|
|
535
|
+
dirs.push(dir)
|
|
536
|
+
const schemaPath = join(dir, "schema.ts")
|
|
537
|
+
writeFileSync(
|
|
538
|
+
schemaPath,
|
|
539
|
+
`
|
|
540
|
+
import type { Model, UUID, Public } from "@supatype/types"
|
|
541
|
+
|
|
542
|
+
export type Config = Model<{
|
|
543
|
+
id: UUID
|
|
544
|
+
}, {
|
|
545
|
+
singleton: true
|
|
546
|
+
tableName: "config"
|
|
547
|
+
access: { read: Public }
|
|
548
|
+
}>
|
|
549
|
+
`,
|
|
550
|
+
"utf8",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
554
|
+
const config = ast?.models.find((m) => m.name === "Config")
|
|
555
|
+
expect(tableName(config)).toBe("config")
|
|
556
|
+
expect(config?.options.singleton).toBe(true)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("infers timestamps from WithTimestamps wrapper", () => {
|
|
560
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-timestamps-"))
|
|
561
|
+
dirs.push(dir)
|
|
562
|
+
const schemaPath = join(dir, "schema.ts")
|
|
563
|
+
writeFileSync(
|
|
564
|
+
schemaPath,
|
|
565
|
+
`
|
|
566
|
+
import type { Model, UUID, WithTimestamps, Public } from "@supatype/types"
|
|
567
|
+
|
|
568
|
+
export type Post = Model<WithTimestamps<{
|
|
569
|
+
id: UUID
|
|
570
|
+
title: string
|
|
571
|
+
}>, {
|
|
572
|
+
access: { read: Public }
|
|
573
|
+
}>
|
|
574
|
+
`,
|
|
575
|
+
"utf8",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
579
|
+
const post = ast?.models.find((m) => m.name === "Post")
|
|
580
|
+
expect(post?.options.timestamps).toBe(true)
|
|
581
|
+
expect(post?.options.singleton).toBeUndefined()
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it("extracts LocaleConfig into schema AST locales", () => {
|
|
585
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-types-locale-config-"))
|
|
586
|
+
dirs.push(dir)
|
|
587
|
+
const schemaPath = join(dir, "schema.ts")
|
|
588
|
+
writeFileSync(
|
|
589
|
+
schemaPath,
|
|
590
|
+
`
|
|
591
|
+
import type { LocaleConfig, Model, UUID } from "@supatype/types"
|
|
592
|
+
|
|
593
|
+
export type localeConfig = LocaleConfig<["en", "de"], "en">
|
|
594
|
+
|
|
595
|
+
export type Page = Model<{ id: UUID; title: string }>
|
|
596
|
+
`,
|
|
597
|
+
"utf8",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
601
|
+
expect(ast?.locales).toEqual(["en", "de"])
|
|
602
|
+
expect(ast?.defaultLocale).toBe("en")
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it("marks Localized fields as JSONB with localized:true", () => {
|
|
606
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-field-"))
|
|
607
|
+
dirs.push(dir)
|
|
608
|
+
const schemaPath = join(dir, "schema.ts")
|
|
609
|
+
writeFileSync(
|
|
610
|
+
schemaPath,
|
|
611
|
+
`
|
|
612
|
+
import type { Localized, Model, Optional, RichText, UUID } from "@supatype/types"
|
|
613
|
+
|
|
614
|
+
export type Page = Model<{
|
|
615
|
+
id: UUID
|
|
616
|
+
title: Localized<string>
|
|
617
|
+
body: Localized<RichText>
|
|
618
|
+
subtitle: Optional<Localized<string>>
|
|
619
|
+
}>
|
|
620
|
+
`,
|
|
621
|
+
"utf8",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
625
|
+
const page = ast?.models.find((m) => m.name === "Page")
|
|
626
|
+
expect(page?.fields["title"]).toMatchObject({
|
|
627
|
+
kind: "text",
|
|
628
|
+
annotations: { db: { pgType: "JSONB" } },
|
|
629
|
+
localized: true,
|
|
630
|
+
required: true,
|
|
631
|
+
})
|
|
632
|
+
expect(page?.fields["body"]).toMatchObject({
|
|
633
|
+
kind: "richText",
|
|
634
|
+
annotations: { db: { pgType: "JSONB" }, platform: { editor: "rich" } },
|
|
635
|
+
localized: true,
|
|
636
|
+
})
|
|
637
|
+
expect(page?.fields["subtitle"]).toMatchObject({
|
|
638
|
+
kind: "text",
|
|
639
|
+
annotations: { db: { pgType: "JSONB" } },
|
|
640
|
+
localized: true,
|
|
641
|
+
required: false,
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it("extracts LocalizedModel with auto-localized copy fields", () => {
|
|
646
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-model-"))
|
|
647
|
+
dirs.push(dir)
|
|
648
|
+
const schemaPath = join(dir, "schema.ts")
|
|
649
|
+
writeFileSync(
|
|
650
|
+
schemaPath,
|
|
651
|
+
`
|
|
652
|
+
import type {
|
|
653
|
+
LocalizedModel,
|
|
654
|
+
UUID,
|
|
655
|
+
ImageAsset,
|
|
656
|
+
NotLocalized,
|
|
657
|
+
Blocks,
|
|
658
|
+
Block,
|
|
659
|
+
Bucket,
|
|
660
|
+
} from "@supatype/types"
|
|
661
|
+
|
|
662
|
+
export type marketing = Bucket<"marketing", { accessMode: "public" }>
|
|
663
|
+
export type RuleBlock = Block<"rule", { text: string }>
|
|
664
|
+
|
|
665
|
+
export type Homepage = LocalizedModel<{
|
|
666
|
+
id: UUID
|
|
667
|
+
hero_title: string
|
|
668
|
+
map_url: NotLocalized<string>
|
|
669
|
+
og_image: ImageAsset<marketing, { localized: true }>
|
|
670
|
+
hero_slides: Blocks<RuleBlock>
|
|
671
|
+
}>
|
|
672
|
+
`,
|
|
673
|
+
"utf8",
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
677
|
+
const homepage = ast?.models.find((m) => m.name === "Homepage")
|
|
678
|
+
expect(homepage?.fields["hero_title"]).toMatchObject({
|
|
679
|
+
kind: "text",
|
|
680
|
+
localized: true,
|
|
681
|
+
annotations: { db: { pgType: "JSONB" } },
|
|
682
|
+
})
|
|
683
|
+
expect(homepage?.fields["map_url"]?.localized).toBeUndefined()
|
|
684
|
+
expect(homepage?.fields["og_image"]).toMatchObject({
|
|
685
|
+
kind: "image",
|
|
686
|
+
localized: true,
|
|
687
|
+
})
|
|
688
|
+
const slides = homepage?.fields["hero_slides"] as { blocks?: { fields: Record<string, unknown> }[] }
|
|
689
|
+
expect(slides?.blocks?.[0]?.fields["text"]).toMatchObject({
|
|
690
|
+
kind: "text",
|
|
691
|
+
localized: true,
|
|
692
|
+
pgType: "JSONB",
|
|
693
|
+
})
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
it("marks Localized<Blocks<...>> as localized column", () => {
|
|
697
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-types-localized-blocks-col-"))
|
|
698
|
+
dirs.push(dir)
|
|
699
|
+
const schemaPath = join(dir, "schema.ts")
|
|
700
|
+
writeFileSync(
|
|
701
|
+
schemaPath,
|
|
702
|
+
`
|
|
703
|
+
import type { Localized, Model, UUID, Blocks, Block } from "@supatype/types"
|
|
704
|
+
|
|
705
|
+
export type Slide = Block<"slide", { image_path: string }>
|
|
706
|
+
|
|
707
|
+
export type Page = Model<{
|
|
708
|
+
id: UUID
|
|
709
|
+
slides: Localized<Blocks<Slide>>
|
|
710
|
+
}>
|
|
711
|
+
`,
|
|
712
|
+
"utf8",
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
716
|
+
const page = ast?.models.find((m) => m.name === "Page")
|
|
717
|
+
expect(page?.fields["slides"]).toMatchObject({
|
|
718
|
+
kind: "blocks",
|
|
719
|
+
localized: true,
|
|
720
|
+
annotations: { db: { pgType: "JSONB" } },
|
|
721
|
+
})
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it("resolves type alias Nullable<T> = Optional<T>", () => {
|
|
725
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-alias-nullable-"))
|
|
726
|
+
dirs.push(dir)
|
|
727
|
+
const schemaPath = join(dir, "schema.ts")
|
|
728
|
+
writeFileSync(
|
|
729
|
+
schemaPath,
|
|
730
|
+
`
|
|
731
|
+
import type { Model, UUID, Email, Optional } from "@supatype/types"
|
|
732
|
+
|
|
733
|
+
type Nullable<T> = Optional<T>
|
|
734
|
+
|
|
735
|
+
export type User = Model<{
|
|
736
|
+
id: UUID
|
|
737
|
+
email: Nullable<Email>
|
|
738
|
+
}>
|
|
739
|
+
`,
|
|
740
|
+
"utf8",
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
744
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
745
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
746
|
+
kind: "email",
|
|
747
|
+
required: false,
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it("resolves multi-hop type aliases", () => {
|
|
752
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-alias-multihop-"))
|
|
753
|
+
dirs.push(dir)
|
|
754
|
+
const schemaPath = join(dir, "schema.ts")
|
|
755
|
+
writeFileSync(
|
|
756
|
+
schemaPath,
|
|
757
|
+
`
|
|
758
|
+
import type { Model, UUID, Email, Optional } from "@supatype/types"
|
|
759
|
+
|
|
760
|
+
type Nullable<T> = Optional<T>
|
|
761
|
+
type A = Nullable<Email>
|
|
762
|
+
type B = A
|
|
763
|
+
|
|
764
|
+
export type User = Model<{ id: UUID; email: B }>
|
|
765
|
+
`,
|
|
766
|
+
"utf8",
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
770
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
771
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
772
|
+
kind: "email",
|
|
773
|
+
required: false,
|
|
774
|
+
})
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
it("resolves enum string-union type aliases", () => {
|
|
778
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-alias-enum-"))
|
|
779
|
+
dirs.push(dir)
|
|
780
|
+
const schemaPath = join(dir, "schema.ts")
|
|
781
|
+
writeFileSync(
|
|
782
|
+
schemaPath,
|
|
783
|
+
`
|
|
784
|
+
import type { Model, UUID } from "@supatype/types"
|
|
785
|
+
|
|
786
|
+
type Status = "draft" | "published" | "archived"
|
|
787
|
+
|
|
788
|
+
export type Post = Model<{ id: UUID; status: Status }>
|
|
789
|
+
`,
|
|
790
|
+
"utf8",
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
794
|
+
expect(ast?.models[0]?.fields["status"]).toMatchObject({
|
|
795
|
+
kind: "enum",
|
|
796
|
+
values: ["draft", "published", "archived"],
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
it("resolves import renames of @supatype/types primitives", () => {
|
|
801
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-import-rename-"))
|
|
802
|
+
dirs.push(dir)
|
|
803
|
+
const schemaPath = join(dir, "schema.ts")
|
|
804
|
+
writeFileSync(
|
|
805
|
+
schemaPath,
|
|
806
|
+
`
|
|
807
|
+
import type { Model, UUID, Email, Optional as Maybe } from "@supatype/types"
|
|
808
|
+
|
|
809
|
+
export type User = Model<{ id: UUID; email: Maybe<Email> }>
|
|
810
|
+
`,
|
|
811
|
+
"utf8",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
815
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
816
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
817
|
+
kind: "email",
|
|
818
|
+
required: false,
|
|
819
|
+
})
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
it("resolves cross-file type aliases via local import", () => {
|
|
823
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-cross-file-alias-"))
|
|
824
|
+
dirs.push(dir)
|
|
825
|
+
writeFileSync(
|
|
826
|
+
join(dir, "field-types.ts"),
|
|
827
|
+
`
|
|
828
|
+
import type { Optional } from "@supatype/types"
|
|
829
|
+
|
|
830
|
+
export type Nullable<T> = Optional<T>
|
|
831
|
+
`,
|
|
832
|
+
"utf8",
|
|
833
|
+
)
|
|
834
|
+
const schemaPath = join(dir, "schema.ts")
|
|
835
|
+
writeFileSync(
|
|
836
|
+
schemaPath,
|
|
837
|
+
`
|
|
838
|
+
import type { Model, UUID, Email } from "@supatype/types"
|
|
839
|
+
import type { Nullable } from "./field-types"
|
|
840
|
+
|
|
841
|
+
export type User = Model<{ id: UUID; email: Nullable<Email> }>
|
|
842
|
+
`,
|
|
843
|
+
"utf8",
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
847
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
848
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
849
|
+
kind: "email",
|
|
850
|
+
required: false,
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it("resolves import rename of a local type alias", () => {
|
|
855
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-rename-local-alias-"))
|
|
856
|
+
dirs.push(dir)
|
|
857
|
+
writeFileSync(
|
|
858
|
+
join(dir, "field-types.ts"),
|
|
859
|
+
`
|
|
860
|
+
import type { Optional } from "@supatype/types"
|
|
861
|
+
|
|
862
|
+
export type Nullable<T> = Optional<T>
|
|
863
|
+
`,
|
|
864
|
+
"utf8",
|
|
865
|
+
)
|
|
866
|
+
const schemaPath = join(dir, "schema.ts")
|
|
867
|
+
writeFileSync(
|
|
868
|
+
schemaPath,
|
|
869
|
+
`
|
|
870
|
+
import type { Model, UUID, Email } from "@supatype/types"
|
|
871
|
+
import type { Nullable as MaybeNull } from "./field-types"
|
|
872
|
+
|
|
873
|
+
export type User = Model<{ id: UUID; email: MaybeNull<Email> }>
|
|
874
|
+
`,
|
|
875
|
+
"utf8",
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
879
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
880
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
881
|
+
kind: "email",
|
|
882
|
+
required: false,
|
|
883
|
+
})
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
it("resolves conditional type aliases via type checker", () => {
|
|
887
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-conditional-alias-"))
|
|
888
|
+
dirs.push(dir)
|
|
889
|
+
const schemaPath = join(dir, "schema.ts")
|
|
890
|
+
writeFileSync(
|
|
891
|
+
schemaPath,
|
|
892
|
+
`
|
|
893
|
+
import type { Model, UUID, Email, Optional } from "@supatype/types"
|
|
894
|
+
|
|
895
|
+
type NullableStr<T> = T extends string ? Optional<T> : T
|
|
896
|
+
|
|
897
|
+
export type User = Model<{ id: UUID; email: NullableStr<Email> }>
|
|
898
|
+
`,
|
|
899
|
+
"utf8",
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
903
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
904
|
+
expect(user?.fields["email"]).toMatchObject({
|
|
905
|
+
kind: "email",
|
|
906
|
+
required: false,
|
|
907
|
+
})
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
it("resolves mapped type aliases as Model fields argument", () => {
|
|
911
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-mapped-fields-"))
|
|
912
|
+
dirs.push(dir)
|
|
913
|
+
const schemaPath = join(dir, "schema.ts")
|
|
914
|
+
writeFileSync(
|
|
915
|
+
schemaPath,
|
|
916
|
+
`
|
|
917
|
+
import type { Model, UUID, Email, Optional } from "@supatype/types"
|
|
918
|
+
|
|
919
|
+
type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
|
|
920
|
+
|
|
921
|
+
export type User = Model<AllOptional<{ email: Email; name: string }>>
|
|
922
|
+
`,
|
|
923
|
+
"utf8",
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
const ast = extractSchemaAstFromTypes(schemaPath, dir)
|
|
927
|
+
const user = ast?.models.find((m) => m.name === "User")
|
|
928
|
+
expect(user?.fields["email"]).toMatchObject({ kind: "email", required: false })
|
|
929
|
+
expect(user?.fields["name"]).toMatchObject({ kind: "text", required: false })
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it("throws on unknown Supatype types instead of silently mapping to TEXT", () => {
|
|
933
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-unknown-type-"))
|
|
934
|
+
dirs.push(dir)
|
|
935
|
+
const schemaPath = join(dir, "schema.ts")
|
|
936
|
+
writeFileSync(
|
|
937
|
+
schemaPath,
|
|
938
|
+
`
|
|
939
|
+
import type { Model, UUID } from "@supatype/types"
|
|
940
|
+
|
|
941
|
+
export type User = Model<{ id: UUID; email: SomeType }>
|
|
942
|
+
`,
|
|
943
|
+
"utf8",
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/Unknown Supatype type "SomeType"/)
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it("throws on circular type alias chains", () => {
|
|
950
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-circular-alias-"))
|
|
951
|
+
dirs.push(dir)
|
|
952
|
+
const schemaPath = join(dir, "schema.ts")
|
|
953
|
+
writeFileSync(
|
|
954
|
+
schemaPath,
|
|
955
|
+
`
|
|
956
|
+
import type { Model, UUID } from "@supatype/types"
|
|
957
|
+
|
|
958
|
+
type A = B
|
|
959
|
+
type B = A
|
|
960
|
+
|
|
961
|
+
export type User = Model<{ id: UUID; email: A }>
|
|
962
|
+
`,
|
|
963
|
+
"utf8",
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/circular alias chain/)
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
it("throws on TypeScript utility types used as field types", () => {
|
|
970
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-utility-type-"))
|
|
971
|
+
dirs.push(dir)
|
|
972
|
+
const schemaPath = join(dir, "schema.ts")
|
|
973
|
+
writeFileSync(
|
|
974
|
+
schemaPath,
|
|
975
|
+
`
|
|
976
|
+
import type { Model, UUID } from "@supatype/types"
|
|
977
|
+
|
|
978
|
+
export type User = Model<{ id: UUID; email: NonNullable<string> }>
|
|
979
|
+
`,
|
|
980
|
+
"utf8",
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
expect(() => extractSchemaAstFromTypes(schemaPath, dir)).toThrow(/Unknown Supatype type "NonNullable"/)
|
|
984
|
+
})
|
|
401
985
|
})
|