@supatype/cli 0.1.0-alpha.7 → 0.1.0-alpha.9
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 +66 -61
- 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/seed.d.ts +8 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +32 -0
- package/dist/seed.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 +7 -3
- 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/seed.ts +43 -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
package/src/type-extractor.ts
CHANGED
|
@@ -1,53 +1,46 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path"
|
|
3
3
|
import ts from "typescript"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
access?: Record<string, unknown>
|
|
32
|
-
/** Raw S3 bucket policy JSON; overrides default public-read when `public` is true if set. */
|
|
33
|
-
s3BucketPolicy?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ExtractedSchemaAst {
|
|
37
|
-
models: ModelAst[]
|
|
38
|
-
storageBuckets?: ExtractedStorageBucketAst[]
|
|
4
|
+
import {
|
|
5
|
+
applyImportRename,
|
|
6
|
+
createResolveContext,
|
|
7
|
+
needsChecker,
|
|
8
|
+
resolveTypeNode,
|
|
9
|
+
tryResolveTypeReference,
|
|
10
|
+
unknownTypeError,
|
|
11
|
+
type ResolveContext,
|
|
12
|
+
} from "./type-resolver.js"
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
emitField,
|
|
16
|
+
emitModel,
|
|
17
|
+
emitSchema,
|
|
18
|
+
defaultPgTypeForKind,
|
|
19
|
+
scalar,
|
|
20
|
+
type BlockDefinitionAst,
|
|
21
|
+
type ExtractedSchemaAstV2,
|
|
22
|
+
type ExtractedStorageBucketAst,
|
|
23
|
+
type FieldAstV2,
|
|
24
|
+
type ParsedField,
|
|
25
|
+
} from "./schema-ast-v2.js"
|
|
26
|
+
|
|
27
|
+
export type { ExtractedSchemaAstV2 as ExtractedSchemaAst, ExtractedStorageBucketAst } from "./schema-ast-v2.js"
|
|
28
|
+
|
|
29
|
+
interface FieldParseContext {
|
|
30
|
+
autoLocalize?: boolean
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
export function extractSchemaAstFromTypes(
|
|
42
34
|
schemaPath: string,
|
|
43
35
|
cwd: string = process.cwd(),
|
|
44
|
-
):
|
|
36
|
+
): ExtractedSchemaAstV2 | null {
|
|
45
37
|
const absPath = resolve(cwd, schemaPath)
|
|
46
38
|
if (!existsSync(absPath)) {
|
|
47
39
|
throw new Error(`Schema file not found: ${absPath}`)
|
|
48
40
|
}
|
|
49
41
|
|
|
50
42
|
const sourceFiles = loadSchemaSourceFiles(absPath)
|
|
43
|
+
const resolveCtx = createResolveContext(sourceFiles)
|
|
51
44
|
const bucketAliases = new Map<string, string>()
|
|
52
45
|
const bucketsById = new Map<string, ExtractedStorageBucketAst>()
|
|
53
46
|
for (const sourceFile of sourceFiles) {
|
|
@@ -68,26 +61,32 @@ export function extractSchemaAstFromTypes(
|
|
|
68
61
|
|
|
69
62
|
const blockAliases = new Map<string, BlockDefinitionAst>()
|
|
70
63
|
for (const sourceFile of sourceFiles) {
|
|
71
|
-
const next = collectBlockAliases(sourceFile, bucketAliases, bucketsById)
|
|
64
|
+
const next = collectBlockAliases(sourceFile, bucketAliases, bucketsById, resolveCtx)
|
|
72
65
|
for (const [name, block] of next) {
|
|
73
66
|
blockAliases.set(name, block)
|
|
74
67
|
}
|
|
75
68
|
}
|
|
76
69
|
|
|
77
|
-
const models:
|
|
70
|
+
const models: ExtractedSchemaAstV2["models"] = []
|
|
78
71
|
|
|
79
72
|
for (const sourceFile of sourceFiles) {
|
|
80
73
|
for (const stmt of sourceFile.statements) {
|
|
81
74
|
if (!ts.isTypeAliasDeclaration(stmt)) continue
|
|
82
75
|
if (!hasExportModifier(stmt)) continue
|
|
83
76
|
if (!ts.isTypeReferenceNode(stmt.type)) continue
|
|
84
|
-
|
|
77
|
+
const modelTypeName = stmt.type.typeName.getText(sourceFile)
|
|
78
|
+
if (modelTypeName !== "Model" && modelTypeName !== "LocalizedModel") continue
|
|
85
79
|
const [fieldsArg, metaArg] = stmt.type.typeArguments ?? []
|
|
86
80
|
if (!fieldsArg) continue
|
|
87
|
-
const fieldsLiteral = unwrapModelFields(fieldsArg)
|
|
81
|
+
const fieldsLiteral = unwrapModelFields(fieldsArg, sourceFile, resolveCtx)
|
|
88
82
|
if (!fieldsLiteral) continue
|
|
89
83
|
|
|
90
|
-
const
|
|
84
|
+
const metaHints = parseMetaLiteral(metaArg, sourceFile)
|
|
85
|
+
const fieldContext: FieldParseContext = {
|
|
86
|
+
autoLocalize: modelTypeName === "LocalizedModel" || metaHints.autoLocalize === true,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fields: Record<string, FieldAstV2> = {}
|
|
91
90
|
for (const member of fieldsLiteral.members) {
|
|
92
91
|
if (!ts.isPropertySignature(member) || !member.type) continue
|
|
93
92
|
const name = getPropertyName(member.name)
|
|
@@ -99,17 +98,22 @@ export function extractSchemaAstFromTypes(
|
|
|
99
98
|
blockAliases,
|
|
100
99
|
bucketAliases,
|
|
101
100
|
bucketsById,
|
|
101
|
+
fieldContext,
|
|
102
|
+
resolveCtx,
|
|
102
103
|
)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const { tableName, access, options } = parseModelMeta(
|
|
107
|
+
metaArg,
|
|
108
|
+
sourceFile,
|
|
109
|
+
stmt.name.text,
|
|
110
|
+
fieldsArg,
|
|
108
111
|
fields,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
models.push(
|
|
115
|
+
emitModel(stmt.name.text, fields, options, tableName, access),
|
|
116
|
+
)
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -118,10 +122,25 @@ export function extractSchemaAstFromTypes(
|
|
|
118
122
|
const storageBuckets =
|
|
119
123
|
bucketsById.size > 0 ? [...bucketsById.values()].sort((a, b) => a.id.localeCompare(b.id)) : undefined
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
let localeConfig: { locales: string[]; defaultLocale: string } | undefined
|
|
126
|
+
for (const sourceFile of sourceFiles) {
|
|
127
|
+
const found = collectLocaleConfig(sourceFile)
|
|
128
|
+
if (!found) continue
|
|
129
|
+
if (localeConfig !== undefined) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Conflicting LocaleConfig declarations. Export at most one `localeConfig` type alias.",
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
localeConfig = found
|
|
124
135
|
}
|
|
136
|
+
|
|
137
|
+
return emitSchema(models, {
|
|
138
|
+
...(storageBuckets !== undefined && storageBuckets.length > 0 && { storageBuckets }),
|
|
139
|
+
...(localeConfig !== undefined && {
|
|
140
|
+
locales: localeConfig.locales,
|
|
141
|
+
defaultLocale: localeConfig.defaultLocale,
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
|
|
@@ -142,9 +161,18 @@ function loadSchemaSourceFiles(entryPath: string): ts.SourceFile[] {
|
|
|
142
161
|
|
|
143
162
|
const baseDir = dirname(currentPath)
|
|
144
163
|
for (const stmt of sourceFile.statements) {
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
164
|
+
let specifier: string | undefined
|
|
165
|
+
if (ts.isExportDeclaration(stmt)) {
|
|
166
|
+
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
167
|
+
specifier = stmt.moduleSpecifier.text
|
|
168
|
+
} else if (ts.isImportDeclaration(stmt)) {
|
|
169
|
+
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue
|
|
170
|
+
specifier = stmt.moduleSpecifier.text
|
|
171
|
+
} else {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (!specifier.startsWith(".")) continue
|
|
175
|
+
const nextPath = resolveTypeModulePath(baseDir, specifier)
|
|
148
176
|
if (!nextPath) continue
|
|
149
177
|
if (!visited.has(nextPath)) queue.push(nextPath)
|
|
150
178
|
}
|
|
@@ -188,24 +216,69 @@ function getPropertyName(name: ts.PropertyName): string | null {
|
|
|
188
216
|
return null
|
|
189
217
|
}
|
|
190
218
|
|
|
191
|
-
function unwrapModelFields(
|
|
219
|
+
function unwrapModelFields(
|
|
220
|
+
typeNode: ts.TypeNode,
|
|
221
|
+
sourceFile: ts.SourceFile,
|
|
222
|
+
resolveCtx: ResolveContext,
|
|
223
|
+
depth = 0,
|
|
224
|
+
): ts.TypeLiteralNode | null {
|
|
225
|
+
if (depth > 16) return null
|
|
192
226
|
if (ts.isTypeLiteralNode(typeNode)) return typeNode
|
|
227
|
+
|
|
228
|
+
if (needsChecker(typeNode)) {
|
|
229
|
+
const resolved = resolveTypeNode(typeNode, sourceFile, resolveCtx)
|
|
230
|
+
if (ts.isTypeLiteralNode(resolved)) return resolved
|
|
231
|
+
return unwrapModelFields(resolved, sourceFile, resolveCtx, depth + 1)
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName)) return null
|
|
194
235
|
|
|
236
|
+
const typeName = applyImportRename(typeNode.typeName.text, sourceFile, resolveCtx.renameMap)
|
|
237
|
+
|
|
195
238
|
// Composite helpers in @supatype/types wrap the concrete field object.
|
|
196
239
|
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
240
|
+
typeName === "WithTimestamps" ||
|
|
241
|
+
typeName === "WithSoftDelete" ||
|
|
242
|
+
typeName === "WithPublishable"
|
|
200
243
|
) {
|
|
201
244
|
const inner = typeNode.typeArguments?.[0]
|
|
202
245
|
if (!inner) return null
|
|
203
|
-
return unwrapModelFields(inner)
|
|
246
|
+
return unwrapModelFields(inner, sourceFile, resolveCtx, depth + 1)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const expanded = tryResolveTypeReference(typeNode, sourceFile, resolveCtx)
|
|
250
|
+
if (expanded) {
|
|
251
|
+
if (ts.isTypeLiteralNode(expanded)) return expanded
|
|
252
|
+
return unwrapModelFields(expanded, sourceFile, resolveCtx, depth + 1)
|
|
204
253
|
}
|
|
205
254
|
|
|
206
255
|
return null
|
|
207
256
|
}
|
|
208
257
|
|
|
258
|
+
/** Parse `Default<T, V>` second type argument into a JSON-serializable literal. */
|
|
259
|
+
function parseDefaultLiteral(
|
|
260
|
+
node: ts.TypeNode,
|
|
261
|
+
sourceFile: ts.SourceFile,
|
|
262
|
+
): string | number | boolean | null | undefined {
|
|
263
|
+
if (ts.isLiteralTypeNode(node)) {
|
|
264
|
+
const lit = node.literal
|
|
265
|
+
if (ts.isStringLiteral(lit) || ts.isNoSubstitutionTemplateLiteral(lit)) return lit.text
|
|
266
|
+
if (ts.isNumericLiteral(lit)) return Number(lit.text)
|
|
267
|
+
if (lit.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
268
|
+
if (lit.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
269
|
+
if (lit.kind === ts.SyntaxKind.NullKeyword) return null
|
|
270
|
+
}
|
|
271
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
272
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
273
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null
|
|
274
|
+
// Negative numeric literals appear as PrefixUnaryExpression in some TS versions.
|
|
275
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
|
|
276
|
+
const inner = parseDefaultLiteral(node.operand as unknown as ts.TypeNode, sourceFile)
|
|
277
|
+
if (typeof inner === "number") return -inner
|
|
278
|
+
}
|
|
279
|
+
return undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
209
282
|
function parseFieldType(
|
|
210
283
|
fieldName: string,
|
|
211
284
|
typeNode: ts.TypeNode,
|
|
@@ -213,7 +286,9 @@ function parseFieldType(
|
|
|
213
286
|
blockAliases: Map<string, BlockDefinitionAst>,
|
|
214
287
|
bucketAliases: Map<string, string>,
|
|
215
288
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
216
|
-
|
|
289
|
+
context: FieldParseContext = {},
|
|
290
|
+
resolveCtx: ResolveContext,
|
|
291
|
+
): FieldAstV2 {
|
|
217
292
|
const flags = {
|
|
218
293
|
required: true,
|
|
219
294
|
unique: false,
|
|
@@ -224,15 +299,17 @@ function parseFieldType(
|
|
|
224
299
|
relationCardinality: undefined as "one" | "many" | undefined,
|
|
225
300
|
relationTarget: undefined as string | undefined,
|
|
226
301
|
editorReadOnly: false,
|
|
227
|
-
/** When set from `ComputedFrom`, Studio previews from these sources until edited on create */
|
|
228
302
|
computedFromSources: undefined as string[] | undefined,
|
|
229
|
-
/** When set, second arg was a template literal with `{field}` / `{truncate(f, n)}` */
|
|
230
303
|
computedFromTemplate: undefined as string | undefined,
|
|
304
|
+
fieldDefault: undefined as string | number | boolean | null | undefined,
|
|
305
|
+
localized: false,
|
|
306
|
+
notLocalized: false,
|
|
231
307
|
}
|
|
232
308
|
|
|
309
|
+
const resolving = new Set<string>()
|
|
233
310
|
let current = typeNode
|
|
234
311
|
while (ts.isTypeReferenceNode(current) && ts.isIdentifier(current.typeName)) {
|
|
235
|
-
const typeName = current.typeName.text
|
|
312
|
+
const typeName = applyImportRename(current.typeName.text, sourceFile, resolveCtx.renameMap)
|
|
236
313
|
switch (typeName) {
|
|
237
314
|
case "Optional":
|
|
238
315
|
flags.required = false
|
|
@@ -261,10 +338,18 @@ function parseFieldType(
|
|
|
261
338
|
flags.unique = true
|
|
262
339
|
current = current.typeArguments?.[0] ?? current
|
|
263
340
|
continue
|
|
264
|
-
case "Default":
|
|
265
|
-
|
|
341
|
+
case "Default": {
|
|
342
|
+
const valueArg = current.typeArguments?.[1]
|
|
343
|
+
if (valueArg !== undefined) {
|
|
344
|
+
const literal = parseDefaultLiteral(valueArg, sourceFile)
|
|
345
|
+
if (literal !== undefined) {
|
|
346
|
+
flags.fieldDefault = literal
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Unwrap to T so `Default<boolean, true>` resolves as boolean, not text.
|
|
266
350
|
current = current.typeArguments?.[0] ?? current
|
|
267
351
|
continue
|
|
352
|
+
}
|
|
268
353
|
case "Searchable":
|
|
269
354
|
current = current.typeArguments?.[0] ?? current
|
|
270
355
|
continue
|
|
@@ -295,251 +380,438 @@ function parseFieldType(
|
|
|
295
380
|
case "Between":
|
|
296
381
|
current = current.typeArguments?.[0] ?? current
|
|
297
382
|
continue
|
|
383
|
+
case "Localized":
|
|
384
|
+
flags.localized = true
|
|
385
|
+
current = current.typeArguments?.[0] ?? current
|
|
386
|
+
continue
|
|
387
|
+
case "NotLocalized":
|
|
388
|
+
flags.notLocalized = true
|
|
389
|
+
current = current.typeArguments?.[0] ?? current
|
|
390
|
+
continue
|
|
298
391
|
case "RelatedTo":
|
|
299
392
|
flags.relationCardinality = "one"
|
|
300
393
|
flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile)
|
|
301
394
|
// `target` must match `ModelAst.name` to satisfy validator resolution.
|
|
302
395
|
// FK column follows the field name (two relations to the same model need distinct columns).
|
|
303
|
-
return {
|
|
396
|
+
return emitField({
|
|
304
397
|
kind: "relation",
|
|
305
|
-
cardinality: "belongsTo",
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
398
|
+
kernel: { cardinality: "belongsTo", target: flags.relationTarget! },
|
|
399
|
+
db: { foreignKey: relationForeignKeyFromField(fieldName) },
|
|
400
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
401
|
+
})
|
|
310
402
|
case "HasOne":
|
|
311
403
|
flags.relationCardinality = "one"
|
|
312
404
|
flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown"
|
|
313
|
-
return {
|
|
405
|
+
return emitField({
|
|
314
406
|
kind: "relation",
|
|
315
|
-
cardinality: "hasOne",
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
407
|
+
kernel: { cardinality: "hasOne", target: flags.relationTarget },
|
|
408
|
+
db: {},
|
|
409
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
410
|
+
})
|
|
319
411
|
case "HasMany":
|
|
320
412
|
case "ManyToMany":
|
|
321
413
|
flags.relationCardinality = "many"
|
|
322
414
|
flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown"
|
|
323
|
-
return {
|
|
415
|
+
return emitField({
|
|
324
416
|
kind: "relation",
|
|
325
|
-
cardinality: "hasMany",
|
|
326
|
-
|
|
327
|
-
|
|
417
|
+
kernel: { cardinality: "hasMany", target: flags.relationTarget },
|
|
418
|
+
db: {},
|
|
419
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
420
|
+
})
|
|
421
|
+
default: {
|
|
422
|
+
const resolved = tryResolveTypeReference(current, sourceFile, resolveCtx, { fieldName, resolving })
|
|
423
|
+
if (resolved) {
|
|
424
|
+
current = resolved
|
|
425
|
+
continue
|
|
328
426
|
}
|
|
329
|
-
default:
|
|
330
427
|
break
|
|
428
|
+
}
|
|
331
429
|
}
|
|
332
430
|
break
|
|
333
431
|
}
|
|
334
432
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
433
|
+
const scalarBase = parseScalarType(
|
|
434
|
+
current,
|
|
435
|
+
sourceFile,
|
|
436
|
+
blockAliases,
|
|
437
|
+
bucketAliases,
|
|
438
|
+
bucketsById,
|
|
439
|
+
context,
|
|
440
|
+
resolveCtx,
|
|
441
|
+
fieldName,
|
|
442
|
+
resolving,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
let parsed: ParsedField = {
|
|
446
|
+
kind: scalarBase.kind,
|
|
447
|
+
kernel: {
|
|
448
|
+
...scalarBase.kernel,
|
|
449
|
+
required: flags.required,
|
|
450
|
+
...(flags.primaryKey && { primaryKey: true }),
|
|
451
|
+
},
|
|
452
|
+
db: {
|
|
453
|
+
...scalarBase.db,
|
|
454
|
+
unique: flags.unique,
|
|
455
|
+
index: flags.index,
|
|
456
|
+
},
|
|
457
|
+
platform: {
|
|
458
|
+
...scalarBase.platform,
|
|
459
|
+
...(flags.editorReadOnly && { readOnly: true }),
|
|
460
|
+
},
|
|
343
461
|
}
|
|
344
462
|
|
|
345
463
|
if (flags.autoIncrement && parsed.kind === "integer") {
|
|
346
|
-
parsed
|
|
347
|
-
parsed.pgType = "SERIAL"
|
|
464
|
+
parsed = { ...parsed, kind: "serial", db: { ...parsed.db, pgType: "SERIAL" } }
|
|
348
465
|
}
|
|
349
466
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
parsed.
|
|
467
|
+
if (fieldName === "id" && parsed.kind === "uuid" && flags.primaryKey === false) {
|
|
468
|
+
parsed = {
|
|
469
|
+
...parsed,
|
|
470
|
+
kernel: { ...parsed.kernel, primaryKey: true, required: true },
|
|
471
|
+
db: { ...parsed.db, unique: true },
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (flags.fieldDefault !== undefined) {
|
|
476
|
+
if (parsed.kernel.default !== undefined) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Field "${fieldName}": use either Default<…> or an inline type default (e.g. RichText<"…">), not both.`,
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
parsed = {
|
|
482
|
+
...parsed,
|
|
483
|
+
kernel: { ...parsed.kernel, default: { kind: "value", value: flags.fieldDefault } },
|
|
484
|
+
}
|
|
360
485
|
}
|
|
361
486
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
487
|
+
if (parsed.kernel.primaryKey === true && parsed.kind === "uuid" && parsed.kernel.default === undefined) {
|
|
488
|
+
parsed = {
|
|
489
|
+
...parsed,
|
|
490
|
+
kernel: { ...parsed.kernel, default: { kind: "genRandomUuid" } },
|
|
491
|
+
}
|
|
492
|
+
} else if (
|
|
493
|
+
parsed.kernel.primaryKey === true &&
|
|
494
|
+
(parsed.kind === "serial" || parsed.kind === "bigSerial")
|
|
495
|
+
) {
|
|
366
496
|
flags.serverGenerated = true
|
|
367
497
|
}
|
|
368
498
|
|
|
369
499
|
if (flags.serverGenerated === true) {
|
|
370
|
-
parsed.serverGenerated
|
|
500
|
+
parsed = { ...parsed, db: { ...parsed.db, serverGenerated: true } }
|
|
371
501
|
}
|
|
372
502
|
|
|
373
|
-
// Convention: standard audit columns are filled by the DB on insert/update.
|
|
374
503
|
const auditTs =
|
|
375
504
|
fieldName === "created_at" ||
|
|
376
505
|
fieldName === "updated_at" ||
|
|
377
506
|
fieldName === "createdAt" ||
|
|
378
507
|
fieldName === "updatedAt"
|
|
379
508
|
if (auditTs) {
|
|
380
|
-
parsed.serverGenerated
|
|
509
|
+
parsed = { ...parsed, db: { ...parsed.db, serverGenerated: true } }
|
|
381
510
|
if (
|
|
382
511
|
(parsed.kind === "datetime" || parsed.kind === "date") &&
|
|
383
|
-
parsed.default === undefined
|
|
512
|
+
parsed.kernel.default === undefined
|
|
384
513
|
) {
|
|
385
|
-
parsed
|
|
514
|
+
parsed = { ...parsed, kernel: { ...parsed.kernel, default: { kind: "now" } } }
|
|
386
515
|
}
|
|
387
516
|
}
|
|
388
517
|
|
|
389
|
-
// `ServerDefault<Date>` etc. → DEFAULT NOW() for column types Postgres handles with NOW().
|
|
390
518
|
if (
|
|
391
519
|
flags.serverGenerated &&
|
|
392
520
|
(parsed.kind === "datetime" || parsed.kind === "date") &&
|
|
393
|
-
parsed.default === undefined
|
|
521
|
+
parsed.kernel.default === undefined
|
|
394
522
|
) {
|
|
395
|
-
parsed
|
|
523
|
+
parsed = { ...parsed, kernel: { ...parsed.kernel, default: { kind: "now" } } }
|
|
396
524
|
}
|
|
397
525
|
|
|
398
526
|
const hasCfTemplate = flags.computedFromTemplate !== undefined
|
|
399
527
|
const hasCfSources = Boolean(flags.computedFromSources && flags.computedFromSources.length > 0)
|
|
400
528
|
if (parsed.kind === "text" && (hasCfTemplate || hasCfSources)) {
|
|
529
|
+
const kernel: ParsedField["kernel"] = { ...parsed.kernel }
|
|
530
|
+
if (hasCfSources && flags.computedFromSources) {
|
|
531
|
+
kernel.sources = flags.computedFromSources
|
|
532
|
+
}
|
|
533
|
+
if (hasCfTemplate && flags.computedFromTemplate !== undefined) {
|
|
534
|
+
kernel.template = flags.computedFromTemplate
|
|
535
|
+
}
|
|
536
|
+
parsed = { ...parsed, kernel }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return emitField(finalizeParsedField(parsed, flags, context))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function finalizeParsedField(
|
|
543
|
+
parsed: ParsedField,
|
|
544
|
+
flags: { localized: boolean; notLocalized: boolean },
|
|
545
|
+
context: FieldParseContext,
|
|
546
|
+
): ParsedField {
|
|
547
|
+
let localized = flags.localized
|
|
548
|
+
|
|
549
|
+
if (
|
|
550
|
+
!localized &&
|
|
551
|
+
!flags.notLocalized &&
|
|
552
|
+
context.autoLocalize &&
|
|
553
|
+
shouldAutoLocalizeFieldKind(parsed.kind)
|
|
554
|
+
) {
|
|
555
|
+
localized = true
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (parsed.kind === "blocks" && parsed.kernel.blocks && context.autoLocalize && !localized) {
|
|
401
559
|
return {
|
|
402
560
|
...parsed,
|
|
403
|
-
|
|
404
|
-
|
|
561
|
+
kernel: {
|
|
562
|
+
...parsed.kernel,
|
|
563
|
+
blocks: parsed.kernel.blocks.map((blockDef) => ({
|
|
564
|
+
...blockDef,
|
|
565
|
+
fields: Object.fromEntries(
|
|
566
|
+
Object.entries(blockDef.fields).map(([name, fieldWire]) => [
|
|
567
|
+
name,
|
|
568
|
+
localizeFieldWire(fieldWire),
|
|
569
|
+
]),
|
|
570
|
+
),
|
|
571
|
+
})),
|
|
572
|
+
},
|
|
405
573
|
}
|
|
406
574
|
}
|
|
407
575
|
|
|
576
|
+
if (localized) {
|
|
577
|
+
return {
|
|
578
|
+
...parsed,
|
|
579
|
+
kernel: { ...parsed.kernel, localized: true },
|
|
580
|
+
db: { ...parsed.db, pgType: "JSONB" },
|
|
581
|
+
}
|
|
582
|
+
}
|
|
408
583
|
return parsed
|
|
409
584
|
}
|
|
410
585
|
|
|
586
|
+
function shouldAutoLocalizeFieldKind(kind: unknown): boolean {
|
|
587
|
+
return kind === "text" || kind === "richText"
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function localizeFieldWire(field: FieldAstV2): FieldAstV2 {
|
|
591
|
+
if (field.localized === true) return field
|
|
592
|
+
if (!shouldAutoLocalizeFieldKind(field.kind)) return field
|
|
593
|
+
const annotations = (field.annotations ?? {}) as { db?: Record<string, unknown>; platform?: Record<string, unknown> }
|
|
594
|
+
return {
|
|
595
|
+
...field,
|
|
596
|
+
localized: true,
|
|
597
|
+
annotations: {
|
|
598
|
+
...annotations,
|
|
599
|
+
db: { ...annotations.db, pgType: "JSONB" },
|
|
600
|
+
},
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
411
604
|
function parseScalarType(
|
|
412
605
|
typeNode: ts.TypeNode,
|
|
413
606
|
sourceFile: ts.SourceFile,
|
|
414
607
|
blockAliases: Map<string, BlockDefinitionAst>,
|
|
415
608
|
bucketAliases: Map<string, string>,
|
|
416
609
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
417
|
-
|
|
610
|
+
context: FieldParseContext = {},
|
|
611
|
+
resolveCtx: ResolveContext,
|
|
612
|
+
fieldName = "?",
|
|
613
|
+
resolving: Set<string> = new Set(),
|
|
614
|
+
): ParsedField {
|
|
418
615
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
419
|
-
const element = parseScalarType(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
616
|
+
const element = parseScalarType(
|
|
617
|
+
typeNode.elementType,
|
|
618
|
+
sourceFile,
|
|
619
|
+
blockAliases,
|
|
620
|
+
bucketAliases,
|
|
621
|
+
bucketsById,
|
|
622
|
+
context,
|
|
623
|
+
resolveCtx,
|
|
624
|
+
fieldName,
|
|
625
|
+
resolving,
|
|
626
|
+
)
|
|
627
|
+
return scalar("array", {
|
|
628
|
+
db: { elementType: defaultPgTypeForKind(element.kind) },
|
|
629
|
+
})
|
|
427
630
|
}
|
|
428
631
|
|
|
429
632
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
430
633
|
const literals = typeNode.types.filter(ts.isLiteralTypeNode)
|
|
431
634
|
if (literals.length === typeNode.types.length && literals.every((lit) => ts.isStringLiteral(lit.literal))) {
|
|
432
|
-
return {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
635
|
+
return scalar("enum", {
|
|
636
|
+
kernel: {
|
|
637
|
+
values: literals.map((lit) => (lit.literal as ts.StringLiteral).text),
|
|
638
|
+
},
|
|
639
|
+
})
|
|
437
640
|
}
|
|
438
641
|
const nonNull = typeNode.types.find((t) => t.kind !== ts.SyntaxKind.NullKeyword)
|
|
439
|
-
if (nonNull)
|
|
642
|
+
if (nonNull) {
|
|
643
|
+
return parseScalarType(
|
|
644
|
+
nonNull,
|
|
645
|
+
sourceFile,
|
|
646
|
+
blockAliases,
|
|
647
|
+
bucketAliases,
|
|
648
|
+
bucketsById,
|
|
649
|
+
context,
|
|
650
|
+
resolveCtx,
|
|
651
|
+
fieldName,
|
|
652
|
+
resolving,
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
658
|
+
const resolved = tryResolveTypeReference(typeNode, sourceFile, resolveCtx, { fieldName, resolving })
|
|
659
|
+
if (resolved) {
|
|
660
|
+
return parseScalarType(
|
|
661
|
+
resolved,
|
|
662
|
+
sourceFile,
|
|
663
|
+
blockAliases,
|
|
664
|
+
bucketAliases,
|
|
665
|
+
bucketsById,
|
|
666
|
+
context,
|
|
667
|
+
resolveCtx,
|
|
668
|
+
fieldName,
|
|
669
|
+
resolving,
|
|
670
|
+
)
|
|
671
|
+
}
|
|
440
672
|
}
|
|
441
673
|
|
|
442
674
|
if (ts.isTypeReferenceNode(typeNode)) {
|
|
443
|
-
const ref = typeNode.typeName
|
|
675
|
+
const ref = ts.isIdentifier(typeNode.typeName)
|
|
676
|
+
? applyImportRename(typeNode.typeName.text, sourceFile, resolveCtx.renameMap)
|
|
677
|
+
: typeNode.typeName.getText(sourceFile)
|
|
444
678
|
switch (ref) {
|
|
445
679
|
case "UUID":
|
|
446
680
|
case "SupatypeAuthUserId":
|
|
447
|
-
return
|
|
448
|
-
case "RichText":
|
|
449
|
-
|
|
681
|
+
return scalar("uuid")
|
|
682
|
+
case "RichText": {
|
|
683
|
+
const defaultArg = typeNode.typeArguments?.[0]
|
|
684
|
+
if (!defaultArg) return scalar("richText")
|
|
685
|
+
const literal = parseDefaultLiteral(defaultArg, sourceFile)
|
|
686
|
+
if (literal === undefined) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`RichText default must be a string literal (plain text or Lexical JSON string), not HTML.`,
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
if (typeof literal !== "string") {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`RichText<…> default must be a string literal (plain text or Lexical JSON string).`,
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
return scalar("richText", {
|
|
697
|
+
kernel: { default: { kind: "value", value: literal } },
|
|
698
|
+
})
|
|
699
|
+
}
|
|
450
700
|
case "Slug": {
|
|
451
701
|
const fromArg = typeNode.typeArguments?.[0]
|
|
452
702
|
const fromLiteral = fromArg ? literalStringType(fromArg) : null
|
|
453
|
-
|
|
454
|
-
return { kind: "slug", pgType: "TEXT", from }
|
|
703
|
+
return scalar("slug", { kernel: { from: fromLiteral ?? "title" } })
|
|
455
704
|
}
|
|
456
705
|
case "Email":
|
|
457
|
-
return
|
|
706
|
+
return scalar("email")
|
|
458
707
|
case "URL":
|
|
459
|
-
return
|
|
708
|
+
return scalar("url")
|
|
460
709
|
case "Markdown":
|
|
461
|
-
return { kind: "text", pgType: "TEXT" }
|
|
462
|
-
case "Color":
|
|
463
|
-
return { kind: "color", pgType: "TEXT" }
|
|
464
710
|
case "PhoneNumber":
|
|
465
|
-
return
|
|
711
|
+
return scalar("text")
|
|
712
|
+
case "Color":
|
|
713
|
+
return scalar("color")
|
|
466
714
|
case "IPAddress":
|
|
467
|
-
return
|
|
715
|
+
return scalar("ip")
|
|
468
716
|
case "CIDR":
|
|
469
|
-
return
|
|
717
|
+
return scalar("cidr")
|
|
470
718
|
case "MacAddress":
|
|
471
|
-
return
|
|
719
|
+
return scalar("macaddr")
|
|
472
720
|
case "XML":
|
|
473
|
-
return
|
|
721
|
+
return scalar("xml")
|
|
474
722
|
case "TSQuery":
|
|
475
|
-
return
|
|
723
|
+
return scalar("tsQuery")
|
|
476
724
|
case "TSVector":
|
|
477
|
-
return
|
|
725
|
+
return scalar("tsVector")
|
|
478
726
|
case "Money":
|
|
479
|
-
return
|
|
727
|
+
return scalar("money")
|
|
480
728
|
case "Decimal":
|
|
481
|
-
return
|
|
729
|
+
return scalar("decimal")
|
|
482
730
|
case "DateOnly":
|
|
483
|
-
return
|
|
731
|
+
return scalar("date")
|
|
484
732
|
case "Date":
|
|
485
733
|
case "DateTime":
|
|
486
734
|
case "Timestamp":
|
|
487
|
-
return
|
|
735
|
+
return scalar("datetime", { db: { pgType: "TIMESTAMP WITH TIME ZONE" } })
|
|
488
736
|
case "Int":
|
|
489
|
-
return
|
|
737
|
+
return scalar("integer")
|
|
490
738
|
case "SmallInt":
|
|
491
|
-
return
|
|
739
|
+
return scalar("smallInt")
|
|
492
740
|
case "BigInt":
|
|
493
|
-
return
|
|
741
|
+
return scalar("bigInt")
|
|
494
742
|
case "Float":
|
|
495
|
-
return
|
|
743
|
+
return scalar("float")
|
|
496
744
|
case "Bytea":
|
|
497
|
-
return
|
|
745
|
+
return scalar("bytes")
|
|
498
746
|
case "JSON":
|
|
499
|
-
return
|
|
747
|
+
return scalar("json")
|
|
748
|
+
case "Button":
|
|
749
|
+
return scalar("button", { db: { pgType: "JSONB" } })
|
|
750
|
+
case "Duration":
|
|
751
|
+
return scalar("json", { db: { pgType: "JSONB" } })
|
|
500
752
|
case "GeoPoint":
|
|
501
|
-
return { kind: "geo", pgType: "GEOGRAPHY", geoType: "point", srid: 4326 }
|
|
502
753
|
case "Geo":
|
|
503
|
-
return
|
|
754
|
+
return scalar("geo", { kernel: { geoType: "point", srid: 4326 } })
|
|
504
755
|
case "Asset":
|
|
505
756
|
case "FileAsset": {
|
|
506
757
|
const bucket = resolveBucketName(typeNode.typeArguments?.[0], sourceFile, bucketAliases, "assets")
|
|
507
|
-
|
|
758
|
+
const assetOpts = parseAssetFieldOptions(typeNode.typeArguments?.[1], sourceFile)
|
|
759
|
+
return attachStorageFieldMeta(
|
|
760
|
+
scalar("file", {
|
|
761
|
+
db: { pgType: "TEXT" },
|
|
762
|
+
kernel: { bucket, ...(assetOpts.localized && { localized: true }) },
|
|
763
|
+
}),
|
|
764
|
+
bucket,
|
|
765
|
+
bucketsById,
|
|
766
|
+
)
|
|
508
767
|
}
|
|
509
768
|
case "ImageAsset": {
|
|
510
769
|
const bucket = resolveBucketName(typeNode.typeArguments?.[0], sourceFile, bucketAliases, "images")
|
|
511
|
-
|
|
770
|
+
const assetOpts = parseAssetFieldOptions(typeNode.typeArguments?.[1], sourceFile)
|
|
771
|
+
return attachStorageFieldMeta(
|
|
772
|
+
scalar("image", {
|
|
773
|
+
db: { pgType: "TEXT" },
|
|
774
|
+
kernel: { bucket, ...(assetOpts.localized && { localized: true }) },
|
|
775
|
+
}),
|
|
776
|
+
bucket,
|
|
777
|
+
bucketsById,
|
|
778
|
+
)
|
|
512
779
|
}
|
|
513
780
|
case "Blocks":
|
|
514
|
-
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
781
|
+
return scalar("blocks", {
|
|
782
|
+
kernel: {
|
|
783
|
+
index: true,
|
|
784
|
+
blocks: parseBlocksTypeDefinitions(
|
|
785
|
+
typeNode.typeArguments?.[0],
|
|
786
|
+
sourceFile,
|
|
787
|
+
blockAliases,
|
|
788
|
+
bucketAliases,
|
|
789
|
+
bucketsById,
|
|
790
|
+
context,
|
|
791
|
+
resolveCtx,
|
|
792
|
+
),
|
|
793
|
+
},
|
|
794
|
+
})
|
|
525
795
|
case "Vector": {
|
|
526
796
|
const dimensions = typeNode.typeArguments?.[0]?.getText(sourceFile)
|
|
527
|
-
return
|
|
797
|
+
return scalar("vector", {
|
|
798
|
+
kernel: { dimensions: Number(dimensions ?? "1536") },
|
|
799
|
+
})
|
|
528
800
|
}
|
|
529
801
|
default:
|
|
530
|
-
|
|
802
|
+
throw unknownTypeError(ref, fieldName)
|
|
531
803
|
}
|
|
532
804
|
}
|
|
533
805
|
|
|
534
806
|
switch (typeNode.kind) {
|
|
535
807
|
case ts.SyntaxKind.StringKeyword:
|
|
536
|
-
return
|
|
808
|
+
return scalar("text")
|
|
537
809
|
case ts.SyntaxKind.NumberKeyword:
|
|
538
|
-
return
|
|
810
|
+
return scalar("float")
|
|
539
811
|
case ts.SyntaxKind.BooleanKeyword:
|
|
540
|
-
return
|
|
812
|
+
return scalar("boolean")
|
|
541
813
|
default:
|
|
542
|
-
return
|
|
814
|
+
return scalar("json")
|
|
543
815
|
}
|
|
544
816
|
}
|
|
545
817
|
|
|
@@ -547,19 +819,71 @@ function collectBlockAliases(
|
|
|
547
819
|
sourceFile: ts.SourceFile,
|
|
548
820
|
bucketAliases: Map<string, string>,
|
|
549
821
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
822
|
+
resolveCtx: ResolveContext,
|
|
550
823
|
): Map<string, BlockDefinitionAst> {
|
|
551
824
|
const blocks = new Map<string, BlockDefinitionAst>()
|
|
552
825
|
for (const stmt of sourceFile.statements) {
|
|
553
826
|
if (!ts.isTypeAliasDeclaration(stmt)) continue
|
|
554
827
|
if (!ts.isTypeReferenceNode(stmt.type)) continue
|
|
555
828
|
if (!ts.isIdentifier(stmt.type.typeName) || stmt.type.typeName.text !== "Block") continue
|
|
556
|
-
const block = parseInlineBlockDefinition(
|
|
829
|
+
const block = parseInlineBlockDefinition(
|
|
830
|
+
stmt.type,
|
|
831
|
+
sourceFile,
|
|
832
|
+
new Map(),
|
|
833
|
+
bucketAliases,
|
|
834
|
+
bucketsById,
|
|
835
|
+
{},
|
|
836
|
+
resolveCtx,
|
|
837
|
+
)
|
|
557
838
|
if (!block) continue
|
|
558
839
|
blocks.set(stmt.name.text, block)
|
|
559
840
|
}
|
|
560
841
|
return blocks
|
|
561
842
|
}
|
|
562
843
|
|
|
844
|
+
function collectLocaleConfig(
|
|
845
|
+
sourceFile: ts.SourceFile,
|
|
846
|
+
): { locales: string[]; defaultLocale: string } | undefined {
|
|
847
|
+
for (const stmt of sourceFile.statements) {
|
|
848
|
+
if (!ts.isTypeAliasDeclaration(stmt)) continue
|
|
849
|
+
if (!hasExportModifier(stmt)) continue
|
|
850
|
+
if (!ts.isTypeReferenceNode(stmt.type)) continue
|
|
851
|
+
if (stmt.type.typeName.getText(sourceFile) !== "LocaleConfig") continue
|
|
852
|
+
const parsed = parseLocaleConfigTypeRef(stmt.type, sourceFile)
|
|
853
|
+
if (parsed) return parsed
|
|
854
|
+
}
|
|
855
|
+
return undefined
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function parseLocaleConfigTypeRef(
|
|
859
|
+
typeRef: ts.TypeReferenceNode,
|
|
860
|
+
sourceFile: ts.SourceFile,
|
|
861
|
+
): { locales: string[]; defaultLocale: string } | null {
|
|
862
|
+
const [localesArg, defaultArg] = typeRef.typeArguments ?? []
|
|
863
|
+
if (!localesArg || !defaultArg) return null
|
|
864
|
+
|
|
865
|
+
const locales = parseStringLiteralTuple(localesArg, sourceFile)
|
|
866
|
+
const defaultLocale = literalStringType(defaultArg)
|
|
867
|
+
if (!locales || locales.length === 0 || !defaultLocale) return null
|
|
868
|
+
if (!locales.includes(defaultLocale)) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
`LocaleConfig defaultLocale "${defaultLocale}" must be one of: ${locales.join(", ")}`,
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
return { locales, defaultLocale }
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function parseStringLiteralTuple(node: ts.TypeNode, sourceFile: ts.SourceFile): string[] | null {
|
|
877
|
+
if (!ts.isTupleTypeNode(node)) return null
|
|
878
|
+
const out: string[] = []
|
|
879
|
+
for (const el of node.elements) {
|
|
880
|
+
const lit = literalStringType(el)
|
|
881
|
+
if (!lit) return null
|
|
882
|
+
out.push(lit)
|
|
883
|
+
}
|
|
884
|
+
return out
|
|
885
|
+
}
|
|
886
|
+
|
|
563
887
|
function collectBucketContext(sourceFile: ts.SourceFile): {
|
|
564
888
|
aliases: Map<string, string>
|
|
565
889
|
bucketsById: Map<string, ExtractedStorageBucketAst>
|
|
@@ -761,15 +1085,15 @@ function bucketsEqual(a: ExtractedStorageBucketAst, b: ExtractedStorageBucketAst
|
|
|
761
1085
|
}
|
|
762
1086
|
|
|
763
1087
|
function attachStorageFieldMeta(
|
|
764
|
-
field:
|
|
1088
|
+
field: ParsedField,
|
|
765
1089
|
bucketId: string,
|
|
766
1090
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
767
|
-
):
|
|
1091
|
+
): ParsedField {
|
|
768
1092
|
const cfg = bucketsById.get(bucketId)
|
|
769
1093
|
if (cfg?.accessMode !== undefined) {
|
|
770
1094
|
return {
|
|
771
1095
|
...field,
|
|
772
|
-
...
|
|
1096
|
+
kernel: { ...field.kernel, accessMode: cfg.accessMode },
|
|
773
1097
|
}
|
|
774
1098
|
}
|
|
775
1099
|
return field
|
|
@@ -781,6 +1105,8 @@ function parseBlocksTypeDefinitions(
|
|
|
781
1105
|
blockAliases: Map<string, BlockDefinitionAst>,
|
|
782
1106
|
bucketAliases: Map<string, string>,
|
|
783
1107
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
1108
|
+
context: FieldParseContext = {},
|
|
1109
|
+
resolveCtx: ResolveContext,
|
|
784
1110
|
): BlockDefinitionAst[] {
|
|
785
1111
|
if (!blocksArg) return []
|
|
786
1112
|
const parts = ts.isUnionTypeNode(blocksArg) ? blocksArg.types : [blocksArg]
|
|
@@ -788,7 +1114,15 @@ function parseBlocksTypeDefinitions(
|
|
|
788
1114
|
for (const part of parts) {
|
|
789
1115
|
if (ts.isTypeReferenceNode(part) && ts.isIdentifier(part.typeName)) {
|
|
790
1116
|
if (part.typeName.text === "Block") {
|
|
791
|
-
const inline = parseInlineBlockDefinition(
|
|
1117
|
+
const inline = parseInlineBlockDefinition(
|
|
1118
|
+
part,
|
|
1119
|
+
sourceFile,
|
|
1120
|
+
blockAliases,
|
|
1121
|
+
bucketAliases,
|
|
1122
|
+
bucketsById,
|
|
1123
|
+
context,
|
|
1124
|
+
resolveCtx,
|
|
1125
|
+
)
|
|
792
1126
|
if (inline) out.push(inline)
|
|
793
1127
|
continue
|
|
794
1128
|
}
|
|
@@ -805,12 +1139,14 @@ function parseInlineBlockDefinition(
|
|
|
805
1139
|
blockAliases: Map<string, BlockDefinitionAst>,
|
|
806
1140
|
bucketAliases: Map<string, string>,
|
|
807
1141
|
bucketsById: Map<string, ExtractedStorageBucketAst>,
|
|
1142
|
+
context: FieldParseContext = {},
|
|
1143
|
+
resolveCtx: ResolveContext,
|
|
808
1144
|
): BlockDefinitionAst | null {
|
|
809
1145
|
const [nameArg, fieldsArg, metaArg] = ref.typeArguments ?? []
|
|
810
1146
|
const name = literalStringType(nameArg)
|
|
811
1147
|
if (!name || !fieldsArg || !ts.isTypeLiteralNode(fieldsArg)) return null
|
|
812
1148
|
|
|
813
|
-
const fields: Record<string,
|
|
1149
|
+
const fields: Record<string, FieldAstV2> = {}
|
|
814
1150
|
for (const member of fieldsArg.members) {
|
|
815
1151
|
if (!ts.isPropertySignature(member) || !member.type) continue
|
|
816
1152
|
const fieldName = getPropertyName(member.name)
|
|
@@ -822,6 +1158,8 @@ function parseInlineBlockDefinition(
|
|
|
822
1158
|
blockAliases,
|
|
823
1159
|
bucketAliases,
|
|
824
1160
|
bucketsById,
|
|
1161
|
+
context,
|
|
1162
|
+
resolveCtx,
|
|
825
1163
|
)
|
|
826
1164
|
}
|
|
827
1165
|
|
|
@@ -937,6 +1275,131 @@ function resolveBucketName(
|
|
|
937
1275
|
return typeArg.getText(sourceFile).replace(/^['"]|['"]$/g, "") || fallback
|
|
938
1276
|
}
|
|
939
1277
|
|
|
1278
|
+
function isBooleanLiteralType(typeNode: ts.TypeNode, value: boolean): boolean {
|
|
1279
|
+
if (value) {
|
|
1280
|
+
if (typeNode.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
1281
|
+
if (ts.isLiteralTypeNode(typeNode) && typeNode.literal.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1282
|
+
return true
|
|
1283
|
+
}
|
|
1284
|
+
return false
|
|
1285
|
+
}
|
|
1286
|
+
if (typeNode.kind === ts.SyntaxKind.FalseKeyword) return true
|
|
1287
|
+
if (ts.isLiteralTypeNode(typeNode) && typeNode.literal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1288
|
+
return true
|
|
1289
|
+
}
|
|
1290
|
+
return false
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function parseAssetFieldOptions(
|
|
1294
|
+
optionsArg: ts.TypeNode | undefined,
|
|
1295
|
+
sourceFile: ts.SourceFile,
|
|
1296
|
+
): { localized: boolean } {
|
|
1297
|
+
if (!optionsArg || !ts.isTypeLiteralNode(optionsArg)) return { localized: false }
|
|
1298
|
+
for (const member of optionsArg.members) {
|
|
1299
|
+
if (!ts.isPropertySignature(member) || !member.type) continue
|
|
1300
|
+
const key = getPropertyName(member.name)
|
|
1301
|
+
if (key === "localized" && isBooleanLiteralType(member.type, true)) {
|
|
1302
|
+
return { localized: true }
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return { localized: false }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function parseMetaLiteral(
|
|
1309
|
+
metaArg: ts.TypeNode | undefined,
|
|
1310
|
+
sourceFile: ts.SourceFile,
|
|
1311
|
+
): {
|
|
1312
|
+
tableName?: string
|
|
1313
|
+
singleton?: boolean
|
|
1314
|
+
timestamps?: boolean
|
|
1315
|
+
softDelete?: boolean
|
|
1316
|
+
autoLocalize?: boolean
|
|
1317
|
+
} {
|
|
1318
|
+
const result: {
|
|
1319
|
+
tableName?: string
|
|
1320
|
+
singleton?: boolean
|
|
1321
|
+
timestamps?: boolean
|
|
1322
|
+
softDelete?: boolean
|
|
1323
|
+
autoLocalize?: boolean
|
|
1324
|
+
} = {}
|
|
1325
|
+
|
|
1326
|
+
if (!metaArg || !ts.isTypeLiteralNode(metaArg)) return result
|
|
1327
|
+
|
|
1328
|
+
for (const member of metaArg.members) {
|
|
1329
|
+
if (!ts.isPropertySignature(member) || !member.type) continue
|
|
1330
|
+
const key = getPropertyName(member.name)
|
|
1331
|
+
if (!key) continue
|
|
1332
|
+
|
|
1333
|
+
if (key === "singleton" && isBooleanLiteralType(member.type, true)) {
|
|
1334
|
+
result.singleton = true
|
|
1335
|
+
} else if (key === "timestamps") {
|
|
1336
|
+
if (isBooleanLiteralType(member.type, true)) result.timestamps = true
|
|
1337
|
+
if (isBooleanLiteralType(member.type, false)) result.timestamps = false
|
|
1338
|
+
} else if (key === "softDelete") {
|
|
1339
|
+
if (isBooleanLiteralType(member.type, true)) result.softDelete = true
|
|
1340
|
+
if (isBooleanLiteralType(member.type, false)) result.softDelete = false
|
|
1341
|
+
} else if (key === "autoLocalize" && isBooleanLiteralType(member.type, true)) {
|
|
1342
|
+
result.autoLocalize = true
|
|
1343
|
+
} else if (
|
|
1344
|
+
key === "tableName" &&
|
|
1345
|
+
ts.isLiteralTypeNode(member.type) &&
|
|
1346
|
+
ts.isStringLiteral(member.type.literal)
|
|
1347
|
+
) {
|
|
1348
|
+
result.tableName = member.type.literal.text
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return result
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function hasCompositeWrapper(typeNode: ts.TypeNode, wrapperName: string): boolean {
|
|
1356
|
+
if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName)) return false
|
|
1357
|
+
if (typeNode.typeName.text === wrapperName) return true
|
|
1358
|
+
if (
|
|
1359
|
+
typeNode.typeName.text === "WithTimestamps" ||
|
|
1360
|
+
typeNode.typeName.text === "WithSoftDelete" ||
|
|
1361
|
+
typeNode.typeName.text === "WithPublishable"
|
|
1362
|
+
) {
|
|
1363
|
+
const inner = typeNode.typeArguments?.[0]
|
|
1364
|
+
if (inner) return hasCompositeWrapper(inner, wrapperName)
|
|
1365
|
+
}
|
|
1366
|
+
return false
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function parseModelMeta(
|
|
1370
|
+
metaArg: ts.TypeNode | undefined,
|
|
1371
|
+
sourceFile: ts.SourceFile,
|
|
1372
|
+
modelName: string,
|
|
1373
|
+
fieldsArg: ts.TypeNode,
|
|
1374
|
+
fields: Record<string, FieldAstV2>,
|
|
1375
|
+
): { tableName: string; access: Record<string, unknown>; options: Record<string, unknown> } {
|
|
1376
|
+
const literal = parseMetaLiteral(metaArg, sourceFile)
|
|
1377
|
+
const singleton = literal.singleton === true
|
|
1378
|
+
const tableName =
|
|
1379
|
+
literal.tableName ?? (singleton ? `_global_${toSnakeCase(modelName)}` : toSnakeCase(modelName))
|
|
1380
|
+
|
|
1381
|
+
const timestamps =
|
|
1382
|
+
literal.timestamps ??
|
|
1383
|
+
(hasCompositeWrapper(fieldsArg, "WithTimestamps") ||
|
|
1384
|
+
(fields["created_at"] !== undefined && fields["updated_at"] !== undefined))
|
|
1385
|
+
|
|
1386
|
+
const softDelete =
|
|
1387
|
+
literal.softDelete ??
|
|
1388
|
+
(hasCompositeWrapper(fieldsArg, "WithSoftDelete") || fields["deleted_at"] !== undefined)
|
|
1389
|
+
|
|
1390
|
+
const options: Record<string, unknown> = {}
|
|
1391
|
+
if (singleton) options.singleton = true
|
|
1392
|
+
if (timestamps) options.timestamps = true
|
|
1393
|
+
if (softDelete) options.softDelete = true
|
|
1394
|
+
if (literal.autoLocalize === true) options.autoLocalize = true
|
|
1395
|
+
|
|
1396
|
+
return {
|
|
1397
|
+
tableName,
|
|
1398
|
+
access: parseModelAccess(metaArg, sourceFile),
|
|
1399
|
+
options,
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
940
1403
|
function parseModelAccess(metaArg: ts.TypeNode | undefined, sourceFile: ts.SourceFile): Record<string, unknown> {
|
|
941
1404
|
if (!metaArg || !ts.isTypeLiteralNode(metaArg)) return {}
|
|
942
1405
|
const accessProp = metaArg.members.find(
|
|
@@ -980,7 +1443,7 @@ function parseAccessRule(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): Reco
|
|
|
980
1443
|
case "OwnerFrom": {
|
|
981
1444
|
const relationArg = typeNode.typeArguments?.[0]
|
|
982
1445
|
const relationField = relationArg?.getText(sourceFile).replace(/['"]/g, "") ?? "owner"
|
|
983
|
-
return { type: "owner", field:
|
|
1446
|
+
return { type: "owner", field: relationField }
|
|
984
1447
|
}
|
|
985
1448
|
case "Role": {
|
|
986
1449
|
const roleArg = typeNode.typeArguments?.[0]
|