@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
package/dist/type-extractor.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import { applyImportRename, createResolveContext, needsChecker, resolveTypeNode, tryResolveTypeReference, unknownTypeError, } from "./type-resolver.js";
|
|
5
|
+
import { emitField, emitModel, emitSchema, defaultPgTypeForKind, scalar, } from "./schema-ast-v2.js";
|
|
4
6
|
export function extractSchemaAstFromTypes(schemaPath, cwd = process.cwd()) {
|
|
5
7
|
const absPath = resolve(cwd, schemaPath);
|
|
6
8
|
if (!existsSync(absPath)) {
|
|
7
9
|
throw new Error(`Schema file not found: ${absPath}`);
|
|
8
10
|
}
|
|
9
11
|
const sourceFiles = loadSchemaSourceFiles(absPath);
|
|
12
|
+
const resolveCtx = createResolveContext(sourceFiles);
|
|
10
13
|
const bucketAliases = new Map();
|
|
11
14
|
const bucketsById = new Map();
|
|
12
15
|
for (const sourceFile of sourceFiles) {
|
|
@@ -24,7 +27,7 @@ export function extractSchemaAstFromTypes(schemaPath, cwd = process.cwd()) {
|
|
|
24
27
|
}
|
|
25
28
|
const blockAliases = new Map();
|
|
26
29
|
for (const sourceFile of sourceFiles) {
|
|
27
|
-
const next = collectBlockAliases(sourceFile, bucketAliases, bucketsById);
|
|
30
|
+
const next = collectBlockAliases(sourceFile, bucketAliases, bucketsById, resolveCtx);
|
|
28
31
|
for (const [name, block] of next) {
|
|
29
32
|
blockAliases.set(name, block);
|
|
30
33
|
}
|
|
@@ -38,14 +41,19 @@ export function extractSchemaAstFromTypes(schemaPath, cwd = process.cwd()) {
|
|
|
38
41
|
continue;
|
|
39
42
|
if (!ts.isTypeReferenceNode(stmt.type))
|
|
40
43
|
continue;
|
|
41
|
-
|
|
44
|
+
const modelTypeName = stmt.type.typeName.getText(sourceFile);
|
|
45
|
+
if (modelTypeName !== "Model" && modelTypeName !== "LocalizedModel")
|
|
42
46
|
continue;
|
|
43
47
|
const [fieldsArg, metaArg] = stmt.type.typeArguments ?? [];
|
|
44
48
|
if (!fieldsArg)
|
|
45
49
|
continue;
|
|
46
|
-
const fieldsLiteral = unwrapModelFields(fieldsArg);
|
|
50
|
+
const fieldsLiteral = unwrapModelFields(fieldsArg, sourceFile, resolveCtx);
|
|
47
51
|
if (!fieldsLiteral)
|
|
48
52
|
continue;
|
|
53
|
+
const metaHints = parseMetaLiteral(metaArg, sourceFile);
|
|
54
|
+
const fieldContext = {
|
|
55
|
+
autoLocalize: modelTypeName === "LocalizedModel" || metaHints.autoLocalize === true,
|
|
56
|
+
};
|
|
49
57
|
const fields = {};
|
|
50
58
|
for (const member of fieldsLiteral.members) {
|
|
51
59
|
if (!ts.isPropertySignature(member) || !member.type)
|
|
@@ -53,25 +61,32 @@ export function extractSchemaAstFromTypes(schemaPath, cwd = process.cwd()) {
|
|
|
53
61
|
const name = getPropertyName(member.name);
|
|
54
62
|
if (!name)
|
|
55
63
|
continue;
|
|
56
|
-
fields[name] = parseFieldType(name, member.type, sourceFile, blockAliases, bucketAliases, bucketsById);
|
|
64
|
+
fields[name] = parseFieldType(name, member.type, sourceFile, blockAliases, bucketAliases, bucketsById, fieldContext, resolveCtx);
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
tableName: toSnakeCase(stmt.name.text),
|
|
61
|
-
fields,
|
|
62
|
-
access: parseModelAccess(metaArg, sourceFile),
|
|
63
|
-
indexes: [],
|
|
64
|
-
options: {},
|
|
65
|
-
});
|
|
66
|
+
const { tableName, access, options } = parseModelMeta(metaArg, sourceFile, stmt.name.text, fieldsArg, fields);
|
|
67
|
+
models.push(emitModel(stmt.name.text, fields, options, tableName, access));
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
if (models.length === 0)
|
|
69
71
|
return null;
|
|
70
72
|
const storageBuckets = bucketsById.size > 0 ? [...bucketsById.values()].sort((a, b) => a.id.localeCompare(b.id)) : undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
let localeConfig;
|
|
74
|
+
for (const sourceFile of sourceFiles) {
|
|
75
|
+
const found = collectLocaleConfig(sourceFile);
|
|
76
|
+
if (!found)
|
|
77
|
+
continue;
|
|
78
|
+
if (localeConfig !== undefined) {
|
|
79
|
+
throw new Error("Conflicting LocaleConfig declarations. Export at most one `localeConfig` type alias.");
|
|
80
|
+
}
|
|
81
|
+
localeConfig = found;
|
|
82
|
+
}
|
|
83
|
+
return emitSchema(models, {
|
|
73
84
|
...(storageBuckets !== undefined && storageBuckets.length > 0 && { storageBuckets }),
|
|
74
|
-
|
|
85
|
+
...(localeConfig !== undefined && {
|
|
86
|
+
locales: localeConfig.locales,
|
|
87
|
+
defaultLocale: localeConfig.defaultLocale,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
75
90
|
}
|
|
76
91
|
function loadSchemaSourceFiles(entryPath) {
|
|
77
92
|
const visited = new Set();
|
|
@@ -91,11 +106,23 @@ function loadSchemaSourceFiles(entryPath) {
|
|
|
91
106
|
sourceFiles.push(sourceFile);
|
|
92
107
|
const baseDir = dirname(currentPath);
|
|
93
108
|
for (const stmt of sourceFile.statements) {
|
|
94
|
-
|
|
109
|
+
let specifier;
|
|
110
|
+
if (ts.isExportDeclaration(stmt)) {
|
|
111
|
+
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
112
|
+
continue;
|
|
113
|
+
specifier = stmt.moduleSpecifier.text;
|
|
114
|
+
}
|
|
115
|
+
else if (ts.isImportDeclaration(stmt)) {
|
|
116
|
+
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
117
|
+
continue;
|
|
118
|
+
specifier = stmt.moduleSpecifier.text;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
95
121
|
continue;
|
|
96
|
-
|
|
122
|
+
}
|
|
123
|
+
if (!specifier.startsWith("."))
|
|
97
124
|
continue;
|
|
98
|
-
const nextPath = resolveTypeModulePath(baseDir,
|
|
125
|
+
const nextPath = resolveTypeModulePath(baseDir, specifier);
|
|
99
126
|
if (!nextPath)
|
|
100
127
|
continue;
|
|
101
128
|
if (!visited.has(nextPath))
|
|
@@ -138,23 +165,67 @@ function getPropertyName(name) {
|
|
|
138
165
|
return name.text;
|
|
139
166
|
return null;
|
|
140
167
|
}
|
|
141
|
-
function unwrapModelFields(typeNode) {
|
|
168
|
+
function unwrapModelFields(typeNode, sourceFile, resolveCtx, depth = 0) {
|
|
169
|
+
if (depth > 16)
|
|
170
|
+
return null;
|
|
142
171
|
if (ts.isTypeLiteralNode(typeNode))
|
|
143
172
|
return typeNode;
|
|
173
|
+
if (needsChecker(typeNode)) {
|
|
174
|
+
const resolved = resolveTypeNode(typeNode, sourceFile, resolveCtx);
|
|
175
|
+
if (ts.isTypeLiteralNode(resolved))
|
|
176
|
+
return resolved;
|
|
177
|
+
return unwrapModelFields(resolved, sourceFile, resolveCtx, depth + 1);
|
|
178
|
+
}
|
|
144
179
|
if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName))
|
|
145
180
|
return null;
|
|
181
|
+
const typeName = applyImportRename(typeNode.typeName.text, sourceFile, resolveCtx.renameMap);
|
|
146
182
|
// Composite helpers in @supatype/types wrap the concrete field object.
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
if (typeName === "WithTimestamps" ||
|
|
184
|
+
typeName === "WithSoftDelete" ||
|
|
185
|
+
typeName === "WithPublishable") {
|
|
150
186
|
const inner = typeNode.typeArguments?.[0];
|
|
151
187
|
if (!inner)
|
|
152
188
|
return null;
|
|
153
|
-
return unwrapModelFields(inner);
|
|
189
|
+
return unwrapModelFields(inner, sourceFile, resolveCtx, depth + 1);
|
|
190
|
+
}
|
|
191
|
+
const expanded = tryResolveTypeReference(typeNode, sourceFile, resolveCtx);
|
|
192
|
+
if (expanded) {
|
|
193
|
+
if (ts.isTypeLiteralNode(expanded))
|
|
194
|
+
return expanded;
|
|
195
|
+
return unwrapModelFields(expanded, sourceFile, resolveCtx, depth + 1);
|
|
154
196
|
}
|
|
155
197
|
return null;
|
|
156
198
|
}
|
|
157
|
-
|
|
199
|
+
/** Parse `Default<T, V>` second type argument into a JSON-serializable literal. */
|
|
200
|
+
function parseDefaultLiteral(node, sourceFile) {
|
|
201
|
+
if (ts.isLiteralTypeNode(node)) {
|
|
202
|
+
const lit = node.literal;
|
|
203
|
+
if (ts.isStringLiteral(lit) || ts.isNoSubstitutionTemplateLiteral(lit))
|
|
204
|
+
return lit.text;
|
|
205
|
+
if (ts.isNumericLiteral(lit))
|
|
206
|
+
return Number(lit.text);
|
|
207
|
+
if (lit.kind === ts.SyntaxKind.TrueKeyword)
|
|
208
|
+
return true;
|
|
209
|
+
if (lit.kind === ts.SyntaxKind.FalseKeyword)
|
|
210
|
+
return false;
|
|
211
|
+
if (lit.kind === ts.SyntaxKind.NullKeyword)
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
215
|
+
return true;
|
|
216
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
217
|
+
return false;
|
|
218
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
219
|
+
return null;
|
|
220
|
+
// Negative numeric literals appear as PrefixUnaryExpression in some TS versions.
|
|
221
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
|
|
222
|
+
const inner = parseDefaultLiteral(node.operand, sourceFile);
|
|
223
|
+
if (typeof inner === "number")
|
|
224
|
+
return -inner;
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
function parseFieldType(fieldName, typeNode, sourceFile, blockAliases, bucketAliases, bucketsById, context = {}, resolveCtx) {
|
|
158
229
|
const flags = {
|
|
159
230
|
required: true,
|
|
160
231
|
unique: false,
|
|
@@ -165,14 +236,16 @@ function parseFieldType(fieldName, typeNode, sourceFile, blockAliases, bucketAli
|
|
|
165
236
|
relationCardinality: undefined,
|
|
166
237
|
relationTarget: undefined,
|
|
167
238
|
editorReadOnly: false,
|
|
168
|
-
/** When set from `ComputedFrom`, Studio previews from these sources until edited on create */
|
|
169
239
|
computedFromSources: undefined,
|
|
170
|
-
/** When set, second arg was a template literal with `{field}` / `{truncate(f, n)}` */
|
|
171
240
|
computedFromTemplate: undefined,
|
|
241
|
+
fieldDefault: undefined,
|
|
242
|
+
localized: false,
|
|
243
|
+
notLocalized: false,
|
|
172
244
|
};
|
|
245
|
+
const resolving = new Set();
|
|
173
246
|
let current = typeNode;
|
|
174
247
|
while (ts.isTypeReferenceNode(current) && ts.isIdentifier(current.typeName)) {
|
|
175
|
-
const typeName = current.typeName.text;
|
|
248
|
+
const typeName = applyImportRename(current.typeName.text, sourceFile, resolveCtx.renameMap);
|
|
176
249
|
switch (typeName) {
|
|
177
250
|
case "Optional":
|
|
178
251
|
flags.required = false;
|
|
@@ -201,10 +274,18 @@ function parseFieldType(fieldName, typeNode, sourceFile, blockAliases, bucketAli
|
|
|
201
274
|
flags.unique = true;
|
|
202
275
|
current = current.typeArguments?.[0] ?? current;
|
|
203
276
|
continue;
|
|
204
|
-
case "Default":
|
|
205
|
-
|
|
277
|
+
case "Default": {
|
|
278
|
+
const valueArg = current.typeArguments?.[1];
|
|
279
|
+
if (valueArg !== undefined) {
|
|
280
|
+
const literal = parseDefaultLiteral(valueArg, sourceFile);
|
|
281
|
+
if (literal !== undefined) {
|
|
282
|
+
flags.fieldDefault = literal;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Unwrap to T so `Default<boolean, true>` resolves as boolean, not text.
|
|
206
286
|
current = current.typeArguments?.[0] ?? current;
|
|
207
287
|
continue;
|
|
288
|
+
}
|
|
208
289
|
case "Searchable":
|
|
209
290
|
current = current.typeArguments?.[0] ?? current;
|
|
210
291
|
continue;
|
|
@@ -236,224 +317,336 @@ function parseFieldType(fieldName, typeNode, sourceFile, blockAliases, bucketAli
|
|
|
236
317
|
case "Between":
|
|
237
318
|
current = current.typeArguments?.[0] ?? current;
|
|
238
319
|
continue;
|
|
320
|
+
case "Localized":
|
|
321
|
+
flags.localized = true;
|
|
322
|
+
current = current.typeArguments?.[0] ?? current;
|
|
323
|
+
continue;
|
|
324
|
+
case "NotLocalized":
|
|
325
|
+
flags.notLocalized = true;
|
|
326
|
+
current = current.typeArguments?.[0] ?? current;
|
|
327
|
+
continue;
|
|
239
328
|
case "RelatedTo":
|
|
240
329
|
flags.relationCardinality = "one";
|
|
241
330
|
flags.relationTarget = relationTargetFromTypeArg(current.typeArguments?.[0], sourceFile);
|
|
242
331
|
// `target` must match `ModelAst.name` to satisfy validator resolution.
|
|
243
332
|
// FK column follows the field name (two relations to the same model need distinct columns).
|
|
244
|
-
return {
|
|
333
|
+
return emitField({
|
|
245
334
|
kind: "relation",
|
|
246
|
-
cardinality: "belongsTo",
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
335
|
+
kernel: { cardinality: "belongsTo", target: flags.relationTarget },
|
|
336
|
+
db: { foreignKey: relationForeignKeyFromField(fieldName) },
|
|
337
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
338
|
+
});
|
|
251
339
|
case "HasOne":
|
|
252
340
|
flags.relationCardinality = "one";
|
|
253
341
|
flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown";
|
|
254
|
-
return {
|
|
342
|
+
return emitField({
|
|
255
343
|
kind: "relation",
|
|
256
|
-
cardinality: "hasOne",
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
344
|
+
kernel: { cardinality: "hasOne", target: flags.relationTarget },
|
|
345
|
+
db: {},
|
|
346
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
347
|
+
});
|
|
260
348
|
case "HasMany":
|
|
261
349
|
case "ManyToMany":
|
|
262
350
|
flags.relationCardinality = "many";
|
|
263
351
|
flags.relationTarget = current.typeArguments?.[0]?.getText(sourceFile).replace(/\W/g, "") ?? "unknown";
|
|
264
|
-
return {
|
|
352
|
+
return emitField({
|
|
265
353
|
kind: "relation",
|
|
266
|
-
cardinality: "hasMany",
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
};
|
|
270
|
-
default:
|
|
354
|
+
kernel: { cardinality: "hasMany", target: flags.relationTarget },
|
|
355
|
+
db: {},
|
|
356
|
+
platform: flags.editorReadOnly ? { readOnly: true } : {},
|
|
357
|
+
});
|
|
358
|
+
default: {
|
|
359
|
+
const resolved = tryResolveTypeReference(current, sourceFile, resolveCtx, { fieldName, resolving });
|
|
360
|
+
if (resolved) {
|
|
361
|
+
current = resolved;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
271
364
|
break;
|
|
365
|
+
}
|
|
272
366
|
}
|
|
273
367
|
break;
|
|
274
368
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
369
|
+
const scalarBase = parseScalarType(current, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx, fieldName, resolving);
|
|
370
|
+
let parsed = {
|
|
371
|
+
kind: scalarBase.kind,
|
|
372
|
+
kernel: {
|
|
373
|
+
...scalarBase.kernel,
|
|
374
|
+
required: flags.required,
|
|
375
|
+
...(flags.primaryKey && { primaryKey: true }),
|
|
376
|
+
},
|
|
377
|
+
db: {
|
|
378
|
+
...scalarBase.db,
|
|
379
|
+
unique: flags.unique,
|
|
380
|
+
index: flags.index,
|
|
381
|
+
},
|
|
382
|
+
platform: {
|
|
383
|
+
...scalarBase.platform,
|
|
384
|
+
...(flags.editorReadOnly && { readOnly: true }),
|
|
385
|
+
},
|
|
283
386
|
};
|
|
284
387
|
if (flags.autoIncrement && parsed.kind === "integer") {
|
|
285
|
-
parsed
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
parsed.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
388
|
+
parsed = { ...parsed, kind: "serial", db: { ...parsed.db, pgType: "SERIAL" } };
|
|
389
|
+
}
|
|
390
|
+
if (fieldName === "id" && parsed.kind === "uuid" && flags.primaryKey === false) {
|
|
391
|
+
parsed = {
|
|
392
|
+
...parsed,
|
|
393
|
+
kernel: { ...parsed.kernel, primaryKey: true, required: true },
|
|
394
|
+
db: { ...parsed.db, unique: true },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (flags.fieldDefault !== undefined) {
|
|
398
|
+
if (parsed.kernel.default !== undefined) {
|
|
399
|
+
throw new Error(`Field "${fieldName}": use either Default<…> or an inline type default (e.g. RichText<"…">), not both.`);
|
|
400
|
+
}
|
|
401
|
+
parsed = {
|
|
402
|
+
...parsed,
|
|
403
|
+
kernel: { ...parsed.kernel, default: { kind: "value", value: flags.fieldDefault } },
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (parsed.kernel.primaryKey === true && parsed.kind === "uuid" && parsed.kernel.default === undefined) {
|
|
407
|
+
parsed = {
|
|
408
|
+
...parsed,
|
|
409
|
+
kernel: { ...parsed.kernel, default: { kind: "genRandomUuid" } },
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
else if (parsed.kernel.primaryKey === true &&
|
|
413
|
+
(parsed.kind === "serial" || parsed.kind === "bigSerial")) {
|
|
302
414
|
flags.serverGenerated = true;
|
|
303
415
|
}
|
|
304
416
|
if (flags.serverGenerated === true) {
|
|
305
|
-
parsed.serverGenerated
|
|
417
|
+
parsed = { ...parsed, db: { ...parsed.db, serverGenerated: true } };
|
|
306
418
|
}
|
|
307
|
-
// Convention: standard audit columns are filled by the DB on insert/update.
|
|
308
419
|
const auditTs = fieldName === "created_at" ||
|
|
309
420
|
fieldName === "updated_at" ||
|
|
310
421
|
fieldName === "createdAt" ||
|
|
311
422
|
fieldName === "updatedAt";
|
|
312
423
|
if (auditTs) {
|
|
313
|
-
parsed.serverGenerated
|
|
424
|
+
parsed = { ...parsed, db: { ...parsed.db, serverGenerated: true } };
|
|
314
425
|
if ((parsed.kind === "datetime" || parsed.kind === "date") &&
|
|
315
|
-
parsed.default === undefined) {
|
|
316
|
-
parsed
|
|
426
|
+
parsed.kernel.default === undefined) {
|
|
427
|
+
parsed = { ...parsed, kernel: { ...parsed.kernel, default: { kind: "now" } } };
|
|
317
428
|
}
|
|
318
429
|
}
|
|
319
|
-
// `ServerDefault<Date>` etc. → DEFAULT NOW() for column types Postgres handles with NOW().
|
|
320
430
|
if (flags.serverGenerated &&
|
|
321
431
|
(parsed.kind === "datetime" || parsed.kind === "date") &&
|
|
322
|
-
parsed.default === undefined) {
|
|
323
|
-
parsed
|
|
432
|
+
parsed.kernel.default === undefined) {
|
|
433
|
+
parsed = { ...parsed, kernel: { ...parsed.kernel, default: { kind: "now" } } };
|
|
324
434
|
}
|
|
325
435
|
const hasCfTemplate = flags.computedFromTemplate !== undefined;
|
|
326
436
|
const hasCfSources = Boolean(flags.computedFromSources && flags.computedFromSources.length > 0);
|
|
327
437
|
if (parsed.kind === "text" && (hasCfTemplate || hasCfSources)) {
|
|
438
|
+
const kernel = { ...parsed.kernel };
|
|
439
|
+
if (hasCfSources && flags.computedFromSources) {
|
|
440
|
+
kernel.sources = flags.computedFromSources;
|
|
441
|
+
}
|
|
442
|
+
if (hasCfTemplate && flags.computedFromTemplate !== undefined) {
|
|
443
|
+
kernel.template = flags.computedFromTemplate;
|
|
444
|
+
}
|
|
445
|
+
parsed = { ...parsed, kernel };
|
|
446
|
+
}
|
|
447
|
+
return emitField(finalizeParsedField(parsed, flags, context));
|
|
448
|
+
}
|
|
449
|
+
function finalizeParsedField(parsed, flags, context) {
|
|
450
|
+
let localized = flags.localized;
|
|
451
|
+
if (!localized &&
|
|
452
|
+
!flags.notLocalized &&
|
|
453
|
+
context.autoLocalize &&
|
|
454
|
+
shouldAutoLocalizeFieldKind(parsed.kind)) {
|
|
455
|
+
localized = true;
|
|
456
|
+
}
|
|
457
|
+
if (parsed.kind === "blocks" && parsed.kernel.blocks && context.autoLocalize && !localized) {
|
|
458
|
+
return {
|
|
459
|
+
...parsed,
|
|
460
|
+
kernel: {
|
|
461
|
+
...parsed.kernel,
|
|
462
|
+
blocks: parsed.kernel.blocks.map((blockDef) => ({
|
|
463
|
+
...blockDef,
|
|
464
|
+
fields: Object.fromEntries(Object.entries(blockDef.fields).map(([name, fieldWire]) => [
|
|
465
|
+
name,
|
|
466
|
+
localizeFieldWire(fieldWire),
|
|
467
|
+
])),
|
|
468
|
+
})),
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (localized) {
|
|
328
473
|
return {
|
|
329
474
|
...parsed,
|
|
330
|
-
|
|
331
|
-
|
|
475
|
+
kernel: { ...parsed.kernel, localized: true },
|
|
476
|
+
db: { ...parsed.db, pgType: "JSONB" },
|
|
332
477
|
};
|
|
333
478
|
}
|
|
334
479
|
return parsed;
|
|
335
480
|
}
|
|
336
|
-
function
|
|
481
|
+
function shouldAutoLocalizeFieldKind(kind) {
|
|
482
|
+
return kind === "text" || kind === "richText";
|
|
483
|
+
}
|
|
484
|
+
function localizeFieldWire(field) {
|
|
485
|
+
if (field.localized === true)
|
|
486
|
+
return field;
|
|
487
|
+
if (!shouldAutoLocalizeFieldKind(field.kind))
|
|
488
|
+
return field;
|
|
489
|
+
const annotations = (field.annotations ?? {});
|
|
490
|
+
return {
|
|
491
|
+
...field,
|
|
492
|
+
localized: true,
|
|
493
|
+
annotations: {
|
|
494
|
+
...annotations,
|
|
495
|
+
db: { ...annotations.db, pgType: "JSONB" },
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function parseScalarType(typeNode, sourceFile, blockAliases, bucketAliases, bucketsById, context = {}, resolveCtx, fieldName = "?", resolving = new Set()) {
|
|
337
500
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
338
|
-
const element = parseScalarType(typeNode.elementType, sourceFile, blockAliases, bucketAliases, bucketsById);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
kind: "array",
|
|
343
|
-
pgType: "ARRAY",
|
|
344
|
-
elementType: elementKind,
|
|
345
|
-
};
|
|
501
|
+
const element = parseScalarType(typeNode.elementType, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx, fieldName, resolving);
|
|
502
|
+
return scalar("array", {
|
|
503
|
+
db: { elementType: defaultPgTypeForKind(element.kind) },
|
|
504
|
+
});
|
|
346
505
|
}
|
|
347
506
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
348
507
|
const literals = typeNode.types.filter(ts.isLiteralTypeNode);
|
|
349
508
|
if (literals.length === typeNode.types.length && literals.every((lit) => ts.isStringLiteral(lit.literal))) {
|
|
350
|
-
return {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
};
|
|
509
|
+
return scalar("enum", {
|
|
510
|
+
kernel: {
|
|
511
|
+
values: literals.map((lit) => lit.literal.text),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
355
514
|
}
|
|
356
515
|
const nonNull = typeNode.types.find((t) => t.kind !== ts.SyntaxKind.NullKeyword);
|
|
357
|
-
if (nonNull)
|
|
358
|
-
return parseScalarType(nonNull, sourceFile, blockAliases, bucketAliases, bucketsById);
|
|
516
|
+
if (nonNull) {
|
|
517
|
+
return parseScalarType(nonNull, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx, fieldName, resolving);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
521
|
+
const resolved = tryResolveTypeReference(typeNode, sourceFile, resolveCtx, { fieldName, resolving });
|
|
522
|
+
if (resolved) {
|
|
523
|
+
return parseScalarType(resolved, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx, fieldName, resolving);
|
|
524
|
+
}
|
|
359
525
|
}
|
|
360
526
|
if (ts.isTypeReferenceNode(typeNode)) {
|
|
361
|
-
const ref = typeNode.typeName
|
|
527
|
+
const ref = ts.isIdentifier(typeNode.typeName)
|
|
528
|
+
? applyImportRename(typeNode.typeName.text, sourceFile, resolveCtx.renameMap)
|
|
529
|
+
: typeNode.typeName.getText(sourceFile);
|
|
362
530
|
switch (ref) {
|
|
363
531
|
case "UUID":
|
|
364
532
|
case "SupatypeAuthUserId":
|
|
365
|
-
return
|
|
366
|
-
case "RichText":
|
|
367
|
-
|
|
533
|
+
return scalar("uuid");
|
|
534
|
+
case "RichText": {
|
|
535
|
+
const defaultArg = typeNode.typeArguments?.[0];
|
|
536
|
+
if (!defaultArg)
|
|
537
|
+
return scalar("richText");
|
|
538
|
+
const literal = parseDefaultLiteral(defaultArg, sourceFile);
|
|
539
|
+
if (literal === undefined) {
|
|
540
|
+
throw new Error(`RichText default must be a string literal (plain text or Lexical JSON string), not HTML.`);
|
|
541
|
+
}
|
|
542
|
+
if (typeof literal !== "string") {
|
|
543
|
+
throw new Error(`RichText<…> default must be a string literal (plain text or Lexical JSON string).`);
|
|
544
|
+
}
|
|
545
|
+
return scalar("richText", {
|
|
546
|
+
kernel: { default: { kind: "value", value: literal } },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
368
549
|
case "Slug": {
|
|
369
550
|
const fromArg = typeNode.typeArguments?.[0];
|
|
370
551
|
const fromLiteral = fromArg ? literalStringType(fromArg) : null;
|
|
371
|
-
|
|
372
|
-
return { kind: "slug", pgType: "TEXT", from };
|
|
552
|
+
return scalar("slug", { kernel: { from: fromLiteral ?? "title" } });
|
|
373
553
|
}
|
|
374
554
|
case "Email":
|
|
375
|
-
return
|
|
555
|
+
return scalar("email");
|
|
376
556
|
case "URL":
|
|
377
|
-
return
|
|
557
|
+
return scalar("url");
|
|
378
558
|
case "Markdown":
|
|
379
|
-
return { kind: "text", pgType: "TEXT" };
|
|
380
|
-
case "Color":
|
|
381
|
-
return { kind: "color", pgType: "TEXT" };
|
|
382
559
|
case "PhoneNumber":
|
|
383
|
-
return
|
|
560
|
+
return scalar("text");
|
|
561
|
+
case "Color":
|
|
562
|
+
return scalar("color");
|
|
384
563
|
case "IPAddress":
|
|
385
|
-
return
|
|
564
|
+
return scalar("ip");
|
|
386
565
|
case "CIDR":
|
|
387
|
-
return
|
|
566
|
+
return scalar("cidr");
|
|
388
567
|
case "MacAddress":
|
|
389
|
-
return
|
|
568
|
+
return scalar("macaddr");
|
|
390
569
|
case "XML":
|
|
391
|
-
return
|
|
570
|
+
return scalar("xml");
|
|
392
571
|
case "TSQuery":
|
|
393
|
-
return
|
|
572
|
+
return scalar("tsQuery");
|
|
394
573
|
case "TSVector":
|
|
395
|
-
return
|
|
574
|
+
return scalar("tsVector");
|
|
396
575
|
case "Money":
|
|
397
|
-
return
|
|
576
|
+
return scalar("money");
|
|
398
577
|
case "Decimal":
|
|
399
|
-
return
|
|
578
|
+
return scalar("decimal");
|
|
400
579
|
case "DateOnly":
|
|
401
|
-
return
|
|
580
|
+
return scalar("date");
|
|
402
581
|
case "Date":
|
|
403
582
|
case "DateTime":
|
|
404
583
|
case "Timestamp":
|
|
405
|
-
return
|
|
584
|
+
return scalar("datetime", { db: { pgType: "TIMESTAMP WITH TIME ZONE" } });
|
|
406
585
|
case "Int":
|
|
407
|
-
return
|
|
586
|
+
return scalar("integer");
|
|
408
587
|
case "SmallInt":
|
|
409
|
-
return
|
|
588
|
+
return scalar("smallInt");
|
|
410
589
|
case "BigInt":
|
|
411
|
-
return
|
|
590
|
+
return scalar("bigInt");
|
|
412
591
|
case "Float":
|
|
413
|
-
return
|
|
592
|
+
return scalar("float");
|
|
414
593
|
case "Bytea":
|
|
415
|
-
return
|
|
594
|
+
return scalar("bytes");
|
|
416
595
|
case "JSON":
|
|
417
|
-
return
|
|
596
|
+
return scalar("json");
|
|
597
|
+
case "Button":
|
|
598
|
+
return scalar("button", { db: { pgType: "JSONB" } });
|
|
599
|
+
case "Duration":
|
|
600
|
+
return scalar("json", { db: { pgType: "JSONB" } });
|
|
418
601
|
case "GeoPoint":
|
|
419
|
-
return { kind: "geo", pgType: "GEOGRAPHY", geoType: "point", srid: 4326 };
|
|
420
602
|
case "Geo":
|
|
421
|
-
return
|
|
603
|
+
return scalar("geo", { kernel: { geoType: "point", srid: 4326 } });
|
|
422
604
|
case "Asset":
|
|
423
605
|
case "FileAsset": {
|
|
424
606
|
const bucket = resolveBucketName(typeNode.typeArguments?.[0], sourceFile, bucketAliases, "assets");
|
|
425
|
-
|
|
607
|
+
const assetOpts = parseAssetFieldOptions(typeNode.typeArguments?.[1], sourceFile);
|
|
608
|
+
return attachStorageFieldMeta(scalar("file", {
|
|
609
|
+
db: { pgType: "TEXT" },
|
|
610
|
+
kernel: { bucket, ...(assetOpts.localized && { localized: true }) },
|
|
611
|
+
}), bucket, bucketsById);
|
|
426
612
|
}
|
|
427
613
|
case "ImageAsset": {
|
|
428
614
|
const bucket = resolveBucketName(typeNode.typeArguments?.[0], sourceFile, bucketAliases, "images");
|
|
429
|
-
|
|
615
|
+
const assetOpts = parseAssetFieldOptions(typeNode.typeArguments?.[1], sourceFile);
|
|
616
|
+
return attachStorageFieldMeta(scalar("image", {
|
|
617
|
+
db: { pgType: "TEXT" },
|
|
618
|
+
kernel: { bucket, ...(assetOpts.localized && { localized: true }) },
|
|
619
|
+
}), bucket, bucketsById);
|
|
430
620
|
}
|
|
431
621
|
case "Blocks":
|
|
432
|
-
return {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
622
|
+
return scalar("blocks", {
|
|
623
|
+
kernel: {
|
|
624
|
+
index: true,
|
|
625
|
+
blocks: parseBlocksTypeDefinitions(typeNode.typeArguments?.[0], sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx),
|
|
626
|
+
},
|
|
627
|
+
});
|
|
437
628
|
case "Vector": {
|
|
438
629
|
const dimensions = typeNode.typeArguments?.[0]?.getText(sourceFile);
|
|
439
|
-
return
|
|
630
|
+
return scalar("vector", {
|
|
631
|
+
kernel: { dimensions: Number(dimensions ?? "1536") },
|
|
632
|
+
});
|
|
440
633
|
}
|
|
441
634
|
default:
|
|
442
|
-
|
|
635
|
+
throw unknownTypeError(ref, fieldName);
|
|
443
636
|
}
|
|
444
637
|
}
|
|
445
638
|
switch (typeNode.kind) {
|
|
446
639
|
case ts.SyntaxKind.StringKeyword:
|
|
447
|
-
return
|
|
640
|
+
return scalar("text");
|
|
448
641
|
case ts.SyntaxKind.NumberKeyword:
|
|
449
|
-
return
|
|
642
|
+
return scalar("float");
|
|
450
643
|
case ts.SyntaxKind.BooleanKeyword:
|
|
451
|
-
return
|
|
644
|
+
return scalar("boolean");
|
|
452
645
|
default:
|
|
453
|
-
return
|
|
646
|
+
return scalar("json");
|
|
454
647
|
}
|
|
455
648
|
}
|
|
456
|
-
function collectBlockAliases(sourceFile, bucketAliases, bucketsById) {
|
|
649
|
+
function collectBlockAliases(sourceFile, bucketAliases, bucketsById, resolveCtx) {
|
|
457
650
|
const blocks = new Map();
|
|
458
651
|
for (const stmt of sourceFile.statements) {
|
|
459
652
|
if (!ts.isTypeAliasDeclaration(stmt))
|
|
@@ -462,13 +655,54 @@ function collectBlockAliases(sourceFile, bucketAliases, bucketsById) {
|
|
|
462
655
|
continue;
|
|
463
656
|
if (!ts.isIdentifier(stmt.type.typeName) || stmt.type.typeName.text !== "Block")
|
|
464
657
|
continue;
|
|
465
|
-
const block = parseInlineBlockDefinition(stmt.type, sourceFile, new Map(), bucketAliases, bucketsById);
|
|
658
|
+
const block = parseInlineBlockDefinition(stmt.type, sourceFile, new Map(), bucketAliases, bucketsById, {}, resolveCtx);
|
|
466
659
|
if (!block)
|
|
467
660
|
continue;
|
|
468
661
|
blocks.set(stmt.name.text, block);
|
|
469
662
|
}
|
|
470
663
|
return blocks;
|
|
471
664
|
}
|
|
665
|
+
function collectLocaleConfig(sourceFile) {
|
|
666
|
+
for (const stmt of sourceFile.statements) {
|
|
667
|
+
if (!ts.isTypeAliasDeclaration(stmt))
|
|
668
|
+
continue;
|
|
669
|
+
if (!hasExportModifier(stmt))
|
|
670
|
+
continue;
|
|
671
|
+
if (!ts.isTypeReferenceNode(stmt.type))
|
|
672
|
+
continue;
|
|
673
|
+
if (stmt.type.typeName.getText(sourceFile) !== "LocaleConfig")
|
|
674
|
+
continue;
|
|
675
|
+
const parsed = parseLocaleConfigTypeRef(stmt.type, sourceFile);
|
|
676
|
+
if (parsed)
|
|
677
|
+
return parsed;
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
function parseLocaleConfigTypeRef(typeRef, sourceFile) {
|
|
682
|
+
const [localesArg, defaultArg] = typeRef.typeArguments ?? [];
|
|
683
|
+
if (!localesArg || !defaultArg)
|
|
684
|
+
return null;
|
|
685
|
+
const locales = parseStringLiteralTuple(localesArg, sourceFile);
|
|
686
|
+
const defaultLocale = literalStringType(defaultArg);
|
|
687
|
+
if (!locales || locales.length === 0 || !defaultLocale)
|
|
688
|
+
return null;
|
|
689
|
+
if (!locales.includes(defaultLocale)) {
|
|
690
|
+
throw new Error(`LocaleConfig defaultLocale "${defaultLocale}" must be one of: ${locales.join(", ")}`);
|
|
691
|
+
}
|
|
692
|
+
return { locales, defaultLocale };
|
|
693
|
+
}
|
|
694
|
+
function parseStringLiteralTuple(node, sourceFile) {
|
|
695
|
+
if (!ts.isTupleTypeNode(node))
|
|
696
|
+
return null;
|
|
697
|
+
const out = [];
|
|
698
|
+
for (const el of node.elements) {
|
|
699
|
+
const lit = literalStringType(el);
|
|
700
|
+
if (!lit)
|
|
701
|
+
return null;
|
|
702
|
+
out.push(lit);
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
472
706
|
function collectBucketContext(sourceFile) {
|
|
473
707
|
const aliases = new Map();
|
|
474
708
|
const bucketsById = new Map();
|
|
@@ -648,12 +882,12 @@ function attachStorageFieldMeta(field, bucketId, bucketsById) {
|
|
|
648
882
|
if (cfg?.accessMode !== undefined) {
|
|
649
883
|
return {
|
|
650
884
|
...field,
|
|
651
|
-
...
|
|
885
|
+
kernel: { ...field.kernel, accessMode: cfg.accessMode },
|
|
652
886
|
};
|
|
653
887
|
}
|
|
654
888
|
return field;
|
|
655
889
|
}
|
|
656
|
-
function parseBlocksTypeDefinitions(blocksArg, sourceFile, blockAliases, bucketAliases, bucketsById) {
|
|
890
|
+
function parseBlocksTypeDefinitions(blocksArg, sourceFile, blockAliases, bucketAliases, bucketsById, context = {}, resolveCtx) {
|
|
657
891
|
if (!blocksArg)
|
|
658
892
|
return [];
|
|
659
893
|
const parts = ts.isUnionTypeNode(blocksArg) ? blocksArg.types : [blocksArg];
|
|
@@ -661,7 +895,7 @@ function parseBlocksTypeDefinitions(blocksArg, sourceFile, blockAliases, bucketA
|
|
|
661
895
|
for (const part of parts) {
|
|
662
896
|
if (ts.isTypeReferenceNode(part) && ts.isIdentifier(part.typeName)) {
|
|
663
897
|
if (part.typeName.text === "Block") {
|
|
664
|
-
const inline = parseInlineBlockDefinition(part, sourceFile, blockAliases, bucketAliases, bucketsById);
|
|
898
|
+
const inline = parseInlineBlockDefinition(part, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx);
|
|
665
899
|
if (inline)
|
|
666
900
|
out.push(inline);
|
|
667
901
|
continue;
|
|
@@ -673,7 +907,7 @@ function parseBlocksTypeDefinitions(blocksArg, sourceFile, blockAliases, bucketA
|
|
|
673
907
|
}
|
|
674
908
|
return out;
|
|
675
909
|
}
|
|
676
|
-
function parseInlineBlockDefinition(ref, sourceFile, blockAliases, bucketAliases, bucketsById) {
|
|
910
|
+
function parseInlineBlockDefinition(ref, sourceFile, blockAliases, bucketAliases, bucketsById, context = {}, resolveCtx) {
|
|
677
911
|
const [nameArg, fieldsArg, metaArg] = ref.typeArguments ?? [];
|
|
678
912
|
const name = literalStringType(nameArg);
|
|
679
913
|
if (!name || !fieldsArg || !ts.isTypeLiteralNode(fieldsArg))
|
|
@@ -685,7 +919,7 @@ function parseInlineBlockDefinition(ref, sourceFile, blockAliases, bucketAliases
|
|
|
685
919
|
const fieldName = getPropertyName(member.name);
|
|
686
920
|
if (!fieldName)
|
|
687
921
|
continue;
|
|
688
|
-
fields[fieldName] = parseFieldType(fieldName, member.type, sourceFile, blockAliases, bucketAliases, bucketsById);
|
|
922
|
+
fields[fieldName] = parseFieldType(fieldName, member.type, sourceFile, blockAliases, bucketAliases, bucketsById, context, resolveCtx);
|
|
689
923
|
}
|
|
690
924
|
let label;
|
|
691
925
|
let icon;
|
|
@@ -797,6 +1031,109 @@ function resolveBucketName(typeArg, sourceFile, bucketAliases, fallback) {
|
|
|
797
1031
|
}
|
|
798
1032
|
return typeArg.getText(sourceFile).replace(/^['"]|['"]$/g, "") || fallback;
|
|
799
1033
|
}
|
|
1034
|
+
function isBooleanLiteralType(typeNode, value) {
|
|
1035
|
+
if (value) {
|
|
1036
|
+
if (typeNode.kind === ts.SyntaxKind.TrueKeyword)
|
|
1037
|
+
return true;
|
|
1038
|
+
if (ts.isLiteralTypeNode(typeNode) && typeNode.literal.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (typeNode.kind === ts.SyntaxKind.FalseKeyword)
|
|
1044
|
+
return true;
|
|
1045
|
+
if (ts.isLiteralTypeNode(typeNode) && typeNode.literal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
function parseAssetFieldOptions(optionsArg, sourceFile) {
|
|
1051
|
+
if (!optionsArg || !ts.isTypeLiteralNode(optionsArg))
|
|
1052
|
+
return { localized: false };
|
|
1053
|
+
for (const member of optionsArg.members) {
|
|
1054
|
+
if (!ts.isPropertySignature(member) || !member.type)
|
|
1055
|
+
continue;
|
|
1056
|
+
const key = getPropertyName(member.name);
|
|
1057
|
+
if (key === "localized" && isBooleanLiteralType(member.type, true)) {
|
|
1058
|
+
return { localized: true };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return { localized: false };
|
|
1062
|
+
}
|
|
1063
|
+
function parseMetaLiteral(metaArg, sourceFile) {
|
|
1064
|
+
const result = {};
|
|
1065
|
+
if (!metaArg || !ts.isTypeLiteralNode(metaArg))
|
|
1066
|
+
return result;
|
|
1067
|
+
for (const member of metaArg.members) {
|
|
1068
|
+
if (!ts.isPropertySignature(member) || !member.type)
|
|
1069
|
+
continue;
|
|
1070
|
+
const key = getPropertyName(member.name);
|
|
1071
|
+
if (!key)
|
|
1072
|
+
continue;
|
|
1073
|
+
if (key === "singleton" && isBooleanLiteralType(member.type, true)) {
|
|
1074
|
+
result.singleton = true;
|
|
1075
|
+
}
|
|
1076
|
+
else if (key === "timestamps") {
|
|
1077
|
+
if (isBooleanLiteralType(member.type, true))
|
|
1078
|
+
result.timestamps = true;
|
|
1079
|
+
if (isBooleanLiteralType(member.type, false))
|
|
1080
|
+
result.timestamps = false;
|
|
1081
|
+
}
|
|
1082
|
+
else if (key === "softDelete") {
|
|
1083
|
+
if (isBooleanLiteralType(member.type, true))
|
|
1084
|
+
result.softDelete = true;
|
|
1085
|
+
if (isBooleanLiteralType(member.type, false))
|
|
1086
|
+
result.softDelete = false;
|
|
1087
|
+
}
|
|
1088
|
+
else if (key === "autoLocalize" && isBooleanLiteralType(member.type, true)) {
|
|
1089
|
+
result.autoLocalize = true;
|
|
1090
|
+
}
|
|
1091
|
+
else if (key === "tableName" &&
|
|
1092
|
+
ts.isLiteralTypeNode(member.type) &&
|
|
1093
|
+
ts.isStringLiteral(member.type.literal)) {
|
|
1094
|
+
result.tableName = member.type.literal.text;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
function hasCompositeWrapper(typeNode, wrapperName) {
|
|
1100
|
+
if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName))
|
|
1101
|
+
return false;
|
|
1102
|
+
if (typeNode.typeName.text === wrapperName)
|
|
1103
|
+
return true;
|
|
1104
|
+
if (typeNode.typeName.text === "WithTimestamps" ||
|
|
1105
|
+
typeNode.typeName.text === "WithSoftDelete" ||
|
|
1106
|
+
typeNode.typeName.text === "WithPublishable") {
|
|
1107
|
+
const inner = typeNode.typeArguments?.[0];
|
|
1108
|
+
if (inner)
|
|
1109
|
+
return hasCompositeWrapper(inner, wrapperName);
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
function parseModelMeta(metaArg, sourceFile, modelName, fieldsArg, fields) {
|
|
1114
|
+
const literal = parseMetaLiteral(metaArg, sourceFile);
|
|
1115
|
+
const singleton = literal.singleton === true;
|
|
1116
|
+
const tableName = literal.tableName ?? (singleton ? `_global_${toSnakeCase(modelName)}` : toSnakeCase(modelName));
|
|
1117
|
+
const timestamps = literal.timestamps ??
|
|
1118
|
+
(hasCompositeWrapper(fieldsArg, "WithTimestamps") ||
|
|
1119
|
+
(fields["created_at"] !== undefined && fields["updated_at"] !== undefined));
|
|
1120
|
+
const softDelete = literal.softDelete ??
|
|
1121
|
+
(hasCompositeWrapper(fieldsArg, "WithSoftDelete") || fields["deleted_at"] !== undefined);
|
|
1122
|
+
const options = {};
|
|
1123
|
+
if (singleton)
|
|
1124
|
+
options.singleton = true;
|
|
1125
|
+
if (timestamps)
|
|
1126
|
+
options.timestamps = true;
|
|
1127
|
+
if (softDelete)
|
|
1128
|
+
options.softDelete = true;
|
|
1129
|
+
if (literal.autoLocalize === true)
|
|
1130
|
+
options.autoLocalize = true;
|
|
1131
|
+
return {
|
|
1132
|
+
tableName,
|
|
1133
|
+
access: parseModelAccess(metaArg, sourceFile),
|
|
1134
|
+
options,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
800
1137
|
function parseModelAccess(metaArg, sourceFile) {
|
|
801
1138
|
if (!metaArg || !ts.isTypeLiteralNode(metaArg))
|
|
802
1139
|
return {};
|
|
@@ -840,7 +1177,7 @@ function parseAccessRule(typeNode, sourceFile) {
|
|
|
840
1177
|
case "OwnerFrom": {
|
|
841
1178
|
const relationArg = typeNode.typeArguments?.[0];
|
|
842
1179
|
const relationField = relationArg?.getText(sourceFile).replace(/['"]/g, "") ?? "owner";
|
|
843
|
-
return { type: "owner", field:
|
|
1180
|
+
return { type: "owner", field: relationField };
|
|
844
1181
|
}
|
|
845
1182
|
case "Role": {
|
|
846
1183
|
const roleArg = typeNode.typeArguments?.[0];
|