@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.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +67 -62
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/proxy-dev-app.d.ts +13 -0
  5. package/dist/app/proxy-dev-app.d.ts.map +1 -0
  6. package/dist/app/proxy-dev-app.js +53 -0
  7. package/dist/app/proxy-dev-app.js.map +1 -0
  8. package/dist/binary-cache.d.ts +5 -0
  9. package/dist/binary-cache.d.ts.map +1 -1
  10. package/dist/binary-cache.js +13 -0
  11. package/dist/binary-cache.js.map +1 -1
  12. package/dist/commands/cloud.d.ts +11 -3
  13. package/dist/commands/cloud.d.ts.map +1 -1
  14. package/dist/commands/cloud.js +33 -25
  15. package/dist/commands/cloud.js.map +1 -1
  16. package/dist/commands/deploy.d.ts.map +1 -1
  17. package/dist/commands/deploy.js +3 -17
  18. package/dist/commands/deploy.js.map +1 -1
  19. package/dist/commands/dev.d.ts +3 -3
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +66 -59
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/diff.d.ts.map +1 -1
  24. package/dist/commands/diff.js +11 -1
  25. package/dist/commands/diff.js.map +1 -1
  26. package/dist/commands/init.js +16 -3
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/push.d.ts.map +1 -1
  29. package/dist/commands/push.js +42 -12
  30. package/dist/commands/push.js.map +1 -1
  31. package/dist/commands/update.d.ts.map +1 -1
  32. package/dist/commands/update.js +16 -0
  33. package/dist/commands/update.js.map +1 -1
  34. package/dist/dev-compose.d.ts +17 -0
  35. package/dist/dev-compose.d.ts.map +1 -0
  36. package/dist/dev-compose.js +374 -0
  37. package/dist/dev-compose.js.map +1 -0
  38. package/dist/diff-output.d.ts +4 -0
  39. package/dist/diff-output.d.ts.map +1 -0
  40. package/dist/diff-output.js +12 -0
  41. package/dist/diff-output.js.map +1 -0
  42. package/dist/docker-postgres.d.ts +21 -3
  43. package/dist/docker-postgres.d.ts.map +1 -1
  44. package/dist/docker-postgres.js +130 -18
  45. package/dist/docker-postgres.js.map +1 -1
  46. package/dist/engine-client.d.ts +5 -3
  47. package/dist/engine-client.d.ts.map +1 -1
  48. package/dist/engine-client.js +2 -1
  49. package/dist/engine-client.js.map +1 -1
  50. package/dist/kong-config.d.ts +4 -0
  51. package/dist/kong-config.d.ts.map +1 -1
  52. package/dist/kong-config.js +12 -1
  53. package/dist/kong-config.js.map +1 -1
  54. package/dist/process-manager.d.ts +2 -0
  55. package/dist/process-manager.d.ts.map +1 -1
  56. package/dist/process-manager.js +16 -1
  57. package/dist/process-manager.js.map +1 -1
  58. package/dist/project-config.d.ts +21 -1
  59. package/dist/project-config.d.ts.map +1 -1
  60. package/dist/project-config.js +15 -0
  61. package/dist/project-config.js.map +1 -1
  62. package/dist/runtime-routes.d.ts +9 -0
  63. package/dist/runtime-routes.d.ts.map +1 -1
  64. package/dist/runtime-routes.js +75 -12
  65. package/dist/runtime-routes.js.map +1 -1
  66. package/dist/schema-ast-v2.d.ts +127 -0
  67. package/dist/schema-ast-v2.d.ts.map +1 -0
  68. package/dist/schema-ast-v2.js +226 -0
  69. package/dist/schema-ast-v2.js.map +1 -0
  70. package/dist/self-host-compose.d.ts +12 -4
  71. package/dist/self-host-compose.d.ts.map +1 -1
  72. package/dist/self-host-compose.js +146 -35
  73. package/dist/self-host-compose.js.map +1 -1
  74. package/dist/studio-admin-roles.d.ts +7 -0
  75. package/dist/studio-admin-roles.d.ts.map +1 -0
  76. package/dist/studio-admin-roles.js +14 -0
  77. package/dist/studio-admin-roles.js.map +1 -0
  78. package/dist/studio-dev-server.d.ts +22 -0
  79. package/dist/studio-dev-server.d.ts.map +1 -0
  80. package/dist/studio-dev-server.js +28 -0
  81. package/dist/studio-dev-server.js.map +1 -0
  82. package/dist/type-extractor.d.ts +3 -30
  83. package/dist/type-extractor.d.ts.map +1 -1
  84. package/dist/type-extractor.js +485 -148
  85. package/dist/type-extractor.js.map +1 -1
  86. package/dist/type-resolver.d.ts +33 -0
  87. package/dist/type-resolver.d.ts.map +1 -0
  88. package/dist/type-resolver.js +338 -0
  89. package/dist/type-resolver.js.map +1 -0
  90. package/package.json +1 -1
  91. package/src/TYPE-RESOLUTION.md +294 -0
  92. package/src/app/proxy-dev-app.ts +67 -0
  93. package/src/binary-cache.ts +20 -0
  94. package/src/commands/cloud.ts +40 -30
  95. package/src/commands/deploy.ts +3 -18
  96. package/src/commands/dev.ts +72 -69
  97. package/src/commands/diff.ts +11 -1
  98. package/src/commands/init.ts +16 -3
  99. package/src/commands/push.ts +49 -13
  100. package/src/commands/update.ts +17 -0
  101. package/src/dev-compose.ts +455 -0
  102. package/src/diff-output.ts +12 -0
  103. package/src/docker-postgres.ts +184 -27
  104. package/src/engine-client.ts +9 -4
  105. package/src/kong-config.ts +16 -1
  106. package/src/process-manager.ts +18 -1
  107. package/src/project-config.ts +34 -1
  108. package/src/runtime-routes.ts +87 -12
  109. package/src/schema-ast-v2.ts +324 -0
  110. package/src/self-host-compose.ts +168 -36
  111. package/src/studio-admin-roles.ts +16 -0
  112. package/src/studio-dev-server.ts +53 -0
  113. package/src/type-extractor.ts +649 -186
  114. package/src/type-resolver.ts +457 -0
  115. package/tests/config.test.ts +34 -3
  116. package/tests/docker-postgres.test.ts +39 -0
  117. package/tests/normalize-admin-config.test.ts +48 -0
  118. package/tests/proxy-dev-app.test.ts +33 -0
  119. package/tests/runtime-contract.test.ts +119 -4
  120. package/tests/studio-admin-roles.test.ts +27 -0
  121. package/tests/type-extractor.test.ts +607 -23
  122. package/tests/type-resolver.test.ts +59 -0
  123. package/tsconfig.tsbuildinfo +1 -1
@@ -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
- if (stmt.type.typeName.getText(sourceFile) !== "Model")
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
- models.push({
59
- name: stmt.name.text,
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
- return {
72
- models,
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
- if (!ts.isExportDeclaration(stmt))
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
- if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
122
+ }
123
+ if (!specifier.startsWith("."))
97
124
  continue;
98
- const nextPath = resolveTypeModulePath(baseDir, stmt.moduleSpecifier.text);
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 (typeNode.typeName.text === "WithTimestamps" ||
148
- typeNode.typeName.text === "WithSoftDelete" ||
149
- typeNode.typeName.text === "WithPublishable") {
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
- function parseFieldType(fieldName, typeNode, sourceFile, blockAliases, bucketAliases, bucketsById) {
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
- // Default<T, V> — unwrap to T so `Default<boolean, true>` resolves as boolean, not text.
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
- target: flags.relationTarget,
248
- foreignKey: relationForeignKeyFromField(fieldName),
249
- ...(flags.editorReadOnly && { readOnly: true }),
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
- target: flags.relationTarget,
258
- ...(flags.editorReadOnly && { readOnly: true }),
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
- target: flags.relationTarget,
268
- ...(flags.editorReadOnly && { readOnly: true }),
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 scalar = parseScalarType(current, sourceFile, blockAliases, bucketAliases, bucketsById);
276
- const parsed = {
277
- ...scalar,
278
- required: flags.required,
279
- unique: flags.unique,
280
- index: flags.index,
281
- ...(flags.primaryKey && { primaryKey: true }),
282
- ...(flags.editorReadOnly && { readOnly: true }),
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.kind = "serial";
286
- parsed.pgType = "SERIAL";
287
- }
288
- // RFC parity with existing examples: `id: UUID` should be the model PK unless
289
- // explicitly overridden via wrappers such as PrimaryKey<> in source types.
290
- if (fieldName === "id" &&
291
- parsed.kind === "uuid" &&
292
- flags.primaryKey === false) {
293
- parsed.primaryKey = true;
294
- parsed.unique = true;
295
- parsed.required = true;
296
- }
297
- // Align with engine fixtures: PK UUID is created by the database unless the author supplies one.
298
- if (parsed.primaryKey === true && parsed.kind === "uuid") {
299
- parsed.default = { kind: "genRandomUuid" };
300
- }
301
- else if (parsed.primaryKey === true && (parsed.kind === "serial" || parsed.kind === "bigSerial")) {
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 = true;
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 = true;
424
+ parsed = { ...parsed, db: { ...parsed.db, serverGenerated: true } };
314
425
  if ((parsed.kind === "datetime" || parsed.kind === "date") &&
315
- parsed.default === undefined) {
316
- parsed.default = { kind: "now" };
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.default = { kind: "now" };
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
- ...(hasCfSources && { sources: flags.computedFromSources }),
331
- ...(hasCfTemplate && { template: flags.computedFromTemplate }),
475
+ kernel: { ...parsed.kernel, localized: true },
476
+ db: { ...parsed.db, pgType: "JSONB" },
332
477
  };
333
478
  }
334
479
  return parsed;
335
480
  }
336
- function parseScalarType(typeNode, sourceFile, blockAliases, bucketAliases, bucketsById) {
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
- const elementKind = typeof element.kind === "string" ? element.kind : "text";
340
- // Keep arrays as native SQL arrays (old `arrayOf(...)` parity), not JSONB.
341
- return {
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
- kind: "enum",
352
- pgType: "TEXT",
353
- values: literals.map((lit) => lit.literal.text),
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.getText(sourceFile);
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 { kind: "uuid", pgType: "UUID" };
366
- case "RichText":
367
- return { kind: "richText", pgType: "JSONB" };
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
- const from = fromLiteral ?? "title";
372
- return { kind: "slug", pgType: "TEXT", from };
552
+ return scalar("slug", { kernel: { from: fromLiteral ?? "title" } });
373
553
  }
374
554
  case "Email":
375
- return { kind: "email", pgType: "TEXT" };
555
+ return scalar("email");
376
556
  case "URL":
377
- return { kind: "url", pgType: "TEXT" };
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 { kind: "text", pgType: "TEXT" };
560
+ return scalar("text");
561
+ case "Color":
562
+ return scalar("color");
384
563
  case "IPAddress":
385
- return { kind: "ip", pgType: "TEXT" };
564
+ return scalar("ip");
386
565
  case "CIDR":
387
- return { kind: "cidr", pgType: "TEXT" };
566
+ return scalar("cidr");
388
567
  case "MacAddress":
389
- return { kind: "macaddr", pgType: "TEXT" };
568
+ return scalar("macaddr");
390
569
  case "XML":
391
- return { kind: "xml", pgType: "TEXT" };
570
+ return scalar("xml");
392
571
  case "TSQuery":
393
- return { kind: "tsQuery", pgType: "TEXT" };
572
+ return scalar("tsQuery");
394
573
  case "TSVector":
395
- return { kind: "tsVector", pgType: "TEXT" };
574
+ return scalar("tsVector");
396
575
  case "Money":
397
- return { kind: "money", pgType: "TEXT" };
576
+ return scalar("money");
398
577
  case "Decimal":
399
- return { kind: "decimal", pgType: "TEXT" };
578
+ return scalar("decimal");
400
579
  case "DateOnly":
401
- return { kind: "date", pgType: "DATE" };
580
+ return scalar("date");
402
581
  case "Date":
403
582
  case "DateTime":
404
583
  case "Timestamp":
405
- return { kind: "datetime", pgType: "TIMESTAMP WITH TIME ZONE" };
584
+ return scalar("datetime", { db: { pgType: "TIMESTAMP WITH TIME ZONE" } });
406
585
  case "Int":
407
- return { kind: "integer", pgType: "INTEGER" };
586
+ return scalar("integer");
408
587
  case "SmallInt":
409
- return { kind: "smallInt", pgType: "SMALLINT" };
588
+ return scalar("smallInt");
410
589
  case "BigInt":
411
- return { kind: "bigInt", pgType: "BIGINT" };
590
+ return scalar("bigInt");
412
591
  case "Float":
413
- return { kind: "float", pgType: "DOUBLE PRECISION" };
592
+ return scalar("float");
414
593
  case "Bytea":
415
- return { kind: "bytes", pgType: "BYTEA" };
594
+ return scalar("bytes");
416
595
  case "JSON":
417
- return { kind: "json", pgType: "JSONB" };
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 { kind: "geo", pgType: "GEOGRAPHY", geoType: "point", srid: 4326 };
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
- return attachStorageFieldMeta({ kind: "file", pgType: "TEXT", bucket }, bucket, bucketsById);
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
- return attachStorageFieldMeta({ kind: "image", pgType: "TEXT", bucket }, bucket, bucketsById);
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
- kind: "blocks",
434
- pgType: "JSONB",
435
- blocks: parseBlocksTypeDefinitions(typeNode.typeArguments?.[0], sourceFile, blockAliases, bucketAliases, bucketsById),
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 { kind: "vector", pgType: "VECTOR", dimensions: Number(dimensions ?? "1536") };
630
+ return scalar("vector", {
631
+ kernel: { dimensions: Number(dimensions ?? "1536") },
632
+ });
440
633
  }
441
634
  default:
442
- return { kind: "text", pgType: "TEXT" };
635
+ throw unknownTypeError(ref, fieldName);
443
636
  }
444
637
  }
445
638
  switch (typeNode.kind) {
446
639
  case ts.SyntaxKind.StringKeyword:
447
- return { kind: "text", pgType: "TEXT" };
640
+ return scalar("text");
448
641
  case ts.SyntaxKind.NumberKeyword:
449
- return { kind: "float", pgType: "DOUBLE PRECISION" };
642
+ return scalar("float");
450
643
  case ts.SyntaxKind.BooleanKeyword:
451
- return { kind: "boolean", pgType: "BOOLEAN" };
644
+ return scalar("boolean");
452
645
  default:
453
- return { kind: "json", pgType: "JSONB" };
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
- ...(cfg.accessMode !== undefined && { accessMode: cfg.accessMode }),
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: relationForeignKeyFromField(relationField) };
1180
+ return { type: "owner", field: relationField };
844
1181
  }
845
1182
  case "Role": {
846
1183
  const roleArg = typeNode.typeArguments?.[0];