@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.
Files changed (128) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +66 -61
  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/seed.d.ts +8 -0
  71. package/dist/seed.d.ts.map +1 -0
  72. package/dist/seed.js +32 -0
  73. package/dist/seed.js.map +1 -0
  74. package/dist/self-host-compose.d.ts +12 -4
  75. package/dist/self-host-compose.d.ts.map +1 -1
  76. package/dist/self-host-compose.js +146 -35
  77. package/dist/self-host-compose.js.map +1 -1
  78. package/dist/studio-admin-roles.d.ts +7 -0
  79. package/dist/studio-admin-roles.d.ts.map +1 -0
  80. package/dist/studio-admin-roles.js +14 -0
  81. package/dist/studio-admin-roles.js.map +1 -0
  82. package/dist/studio-dev-server.d.ts +22 -0
  83. package/dist/studio-dev-server.d.ts.map +1 -0
  84. package/dist/studio-dev-server.js +28 -0
  85. package/dist/studio-dev-server.js.map +1 -0
  86. package/dist/type-extractor.d.ts +3 -30
  87. package/dist/type-extractor.d.ts.map +1 -1
  88. package/dist/type-extractor.js +485 -148
  89. package/dist/type-extractor.js.map +1 -1
  90. package/dist/type-resolver.d.ts +33 -0
  91. package/dist/type-resolver.d.ts.map +1 -0
  92. package/dist/type-resolver.js +338 -0
  93. package/dist/type-resolver.js.map +1 -0
  94. package/package.json +7 -3
  95. package/src/TYPE-RESOLUTION.md +294 -0
  96. package/src/app/proxy-dev-app.ts +67 -0
  97. package/src/binary-cache.ts +20 -0
  98. package/src/commands/cloud.ts +40 -30
  99. package/src/commands/deploy.ts +3 -18
  100. package/src/commands/dev.ts +72 -69
  101. package/src/commands/diff.ts +11 -1
  102. package/src/commands/init.ts +16 -3
  103. package/src/commands/push.ts +49 -13
  104. package/src/commands/update.ts +17 -0
  105. package/src/dev-compose.ts +455 -0
  106. package/src/diff-output.ts +12 -0
  107. package/src/docker-postgres.ts +184 -27
  108. package/src/engine-client.ts +9 -4
  109. package/src/kong-config.ts +16 -1
  110. package/src/process-manager.ts +18 -1
  111. package/src/project-config.ts +34 -1
  112. package/src/runtime-routes.ts +87 -12
  113. package/src/schema-ast-v2.ts +324 -0
  114. package/src/seed.ts +43 -0
  115. package/src/self-host-compose.ts +168 -36
  116. package/src/studio-admin-roles.ts +16 -0
  117. package/src/studio-dev-server.ts +53 -0
  118. package/src/type-extractor.ts +649 -186
  119. package/src/type-resolver.ts +457 -0
  120. package/tests/config.test.ts +34 -3
  121. package/tests/docker-postgres.test.ts +39 -0
  122. package/tests/normalize-admin-config.test.ts +48 -0
  123. package/tests/proxy-dev-app.test.ts +33 -0
  124. package/tests/runtime-contract.test.ts +119 -4
  125. package/tests/studio-admin-roles.test.ts +27 -0
  126. package/tests/type-extractor.test.ts +607 -23
  127. package/tests/type-resolver.test.ts +59 -0
  128. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,457 @@
1
+ import { resolve } from "node:path"
2
+ import ts from "typescript"
3
+
4
+ export type AliasEntry = {
5
+ typeParams: string[]
6
+ body: ts.TypeNode
7
+ sourceFile: ts.SourceFile
8
+ }
9
+
10
+ /** fileName → (localName → canonicalName) for explicit `import { X as Y }` renames. */
11
+ export type ImportRenameMap = Map<string, Map<string, string>>
12
+
13
+ export type CheckerContext = {
14
+ program: ts.Program
15
+ checker: ts.TypeChecker
16
+ }
17
+
18
+ export type ResolveContext = {
19
+ aliasRegistry: Map<string, AliasEntry>
20
+ renameMap: ImportRenameMap
21
+ sourceFiles: ts.SourceFile[]
22
+ getChecker: () => CheckerContext
23
+ }
24
+
25
+ const MODEL_ALIAS_NAMES = new Set(["Model", "LocalizedModel"])
26
+
27
+ export function createResolveContext(sourceFiles: ts.SourceFile[]): ResolveContext {
28
+ let checkerCtx: CheckerContext | undefined
29
+ return {
30
+ aliasRegistry: buildAliasRegistry(sourceFiles),
31
+ renameMap: buildImportRenameMap(sourceFiles),
32
+ sourceFiles,
33
+ getChecker: () => {
34
+ if (!checkerCtx) {
35
+ checkerCtx = createCheckerContext(sourceFiles)
36
+ }
37
+ return checkerCtx
38
+ },
39
+ }
40
+ }
41
+
42
+ export function buildAliasRegistry(sourceFiles: ts.SourceFile[]): Map<string, AliasEntry> {
43
+ const registry = new Map<string, AliasEntry>()
44
+ for (const sourceFile of sourceFiles) {
45
+ for (const stmt of sourceFile.statements) {
46
+ if (!ts.isTypeAliasDeclaration(stmt)) continue
47
+ if (ts.isTypeReferenceNode(stmt.type) && ts.isIdentifier(stmt.type.typeName)) {
48
+ if (MODEL_ALIAS_NAMES.has(stmt.type.typeName.text)) continue
49
+ }
50
+ const typeParams = stmt.typeParameters?.map((p) => p.name.text) ?? []
51
+ registry.set(stmt.name.text, {
52
+ typeParams,
53
+ body: stmt.type,
54
+ sourceFile,
55
+ })
56
+ }
57
+ }
58
+ return registry
59
+ }
60
+
61
+ export function buildImportRenameMap(sourceFiles: ts.SourceFile[]): ImportRenameMap {
62
+ const renameMap: ImportRenameMap = new Map()
63
+ for (const sourceFile of sourceFiles) {
64
+ const fileRenames = new Map<string, string>()
65
+ for (const stmt of sourceFile.statements) {
66
+ if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
67
+ const bindings = stmt.importClause.namedBindings
68
+ if (!bindings || !ts.isNamedImports(bindings)) continue
69
+ for (const el of bindings.elements) {
70
+ const localName = el.name.text
71
+ const canonicalName = el.propertyName?.text ?? localName
72
+ if (localName !== canonicalName) {
73
+ fileRenames.set(localName, canonicalName)
74
+ }
75
+ }
76
+ }
77
+ if (fileRenames.size > 0) {
78
+ renameMap.set(sourceFile.fileName, fileRenames)
79
+ }
80
+ }
81
+ return renameMap
82
+ }
83
+
84
+ export function applyImportRename(
85
+ name: string,
86
+ sourceFile: ts.SourceFile,
87
+ renameMap: ImportRenameMap,
88
+ ): string {
89
+ return renameMap.get(sourceFile.fileName)?.get(name) ?? name
90
+ }
91
+
92
+ export function needsChecker(node: ts.TypeNode): boolean {
93
+ return containsConditionalOrMapped(node)
94
+ }
95
+
96
+ export function resolveTypeNode(
97
+ typeNode: ts.TypeNode,
98
+ sourceFile: ts.SourceFile,
99
+ ctx: ResolveContext,
100
+ options: { fieldName?: string; resolving?: Set<string> } = {},
101
+ ): ts.TypeNode {
102
+ if (needsChecker(typeNode)) {
103
+ return resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
104
+ }
105
+
106
+ if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
107
+ const renamed = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
108
+ if (renamed !== typeNode.typeName.text) {
109
+ return resolveTypeNode(
110
+ ts.factory.createTypeReferenceNode(renamed, typeNode.typeArguments),
111
+ sourceFile,
112
+ ctx,
113
+ options,
114
+ )
115
+ }
116
+ const expanded = tryExpandAlias(typeNode, sourceFile, ctx, options)
117
+ if (expanded) {
118
+ return resolveTypeNode(expanded, sourceFile, ctx, options)
119
+ }
120
+ }
121
+
122
+ return typeNode
123
+ }
124
+
125
+ export function tryResolveTypeReference(
126
+ typeNode: ts.TypeReferenceNode,
127
+ sourceFile: ts.SourceFile,
128
+ ctx: ResolveContext,
129
+ options: { fieldName?: string; resolving?: Set<string> } = {},
130
+ ): ts.TypeNode | null {
131
+ if (!ts.isIdentifier(typeNode.typeName)) return null
132
+
133
+ const renamed = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
134
+ if (renamed !== typeNode.typeName.text) {
135
+ return ts.factory.createTypeReferenceNode(renamed, typeNode.typeArguments)
136
+ }
137
+
138
+ return tryExpandAlias(typeNode, sourceFile, ctx, options)
139
+ }
140
+
141
+ function tryExpandAlias(
142
+ typeNode: ts.TypeReferenceNode,
143
+ sourceFile: ts.SourceFile,
144
+ ctx: ResolveContext,
145
+ options: { fieldName?: string; resolving?: Set<string> },
146
+ ): ts.TypeNode | null {
147
+ if (!ts.isIdentifier(typeNode.typeName)) return null
148
+ const aliasName = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
149
+ const entry = ctx.aliasRegistry.get(aliasName)
150
+ if (!entry) return null
151
+
152
+ const resolving = options.resolving ?? new Set<string>()
153
+ if (resolving.has(aliasName)) {
154
+ throw new Error(
155
+ `Field "${options.fieldName ?? "?"}": circular alias chain detected resolving "${aliasName}".`,
156
+ )
157
+ }
158
+
159
+ resolving.add(aliasName)
160
+
161
+ const typeArgs = typeNode.typeArguments ?? []
162
+ const synthetic =
163
+ tryEvaluateConditionalAlias(entry, typeArgs) ?? tryEvaluateMappedOptionalFields(entry, typeArgs)
164
+ if (synthetic) {
165
+ return expandAliasChain(synthetic, sourceFile, ctx, options, resolving)
166
+ }
167
+
168
+ let expanded: ts.TypeNode
169
+ if (needsChecker(entry.body)) {
170
+ expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
171
+ } else {
172
+ expanded = substituteAndParse(entry, typeArgs)
173
+ if (needsChecker(expanded)) {
174
+ expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
175
+ }
176
+ }
177
+
178
+ return expandAliasChain(expanded, sourceFile, ctx, options, resolving)
179
+ }
180
+
181
+ function expandAliasChain(
182
+ typeNode: ts.TypeNode,
183
+ sourceFile: ts.SourceFile,
184
+ ctx: ResolveContext,
185
+ options: { fieldName?: string; resolving?: Set<string> },
186
+ resolving: Set<string>,
187
+ ): ts.TypeNode {
188
+ if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName)) {
189
+ return typeNode
190
+ }
191
+
192
+ const chainName = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
193
+ if (!ctx.aliasRegistry.has(chainName) || resolving.has(chainName)) {
194
+ return typeNode
195
+ }
196
+
197
+ const entry = ctx.aliasRegistry.get(chainName)!
198
+ resolving.add(chainName)
199
+
200
+ const typeArgs = typeNode.typeArguments ?? []
201
+ const synthetic =
202
+ tryEvaluateConditionalAlias(entry, typeArgs) ?? tryEvaluateMappedOptionalFields(entry, typeArgs)
203
+ if (synthetic) {
204
+ return expandAliasChain(synthetic, sourceFile, ctx, options, resolving)
205
+ }
206
+
207
+ let expanded: ts.TypeNode
208
+ if (needsChecker(entry.body)) {
209
+ expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
210
+ } else {
211
+ expanded = substituteAndParse(entry, typeArgs)
212
+ if (needsChecker(expanded)) {
213
+ expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
214
+ }
215
+ }
216
+
217
+ return expandAliasChain(expanded, sourceFile, ctx, options, resolving)
218
+ }
219
+
220
+ function substituteAndParse(
221
+ entry: AliasEntry,
222
+ typeArgs: readonly ts.TypeNode[],
223
+ ): ts.TypeNode {
224
+ let text = entry.body.getText(entry.sourceFile)
225
+ for (let i = 0; i < entry.typeParams.length; i++) {
226
+ const param = entry.typeParams[i]
227
+ const arg = typeArgs[i]
228
+ if (!param || !arg) continue
229
+ const argSource = arg.getSourceFile()
230
+ const argText = arg.getText(argSource)
231
+ text = text.replace(new RegExp(`\\b${escapeRegExp(param)}\\b`, "g"), argText)
232
+ }
233
+ const parsed = ts.createSourceFile(
234
+ "__alias__.ts",
235
+ `type __A__ = ${text}`,
236
+ ts.ScriptTarget.Latest,
237
+ true,
238
+ ts.ScriptKind.TS,
239
+ )
240
+ const decl = parsed.statements[0]
241
+ if (!decl || !ts.isTypeAliasDeclaration(decl)) {
242
+ throw new Error("Internal error: failed to parse substituted type alias body.")
243
+ }
244
+ return decl.type
245
+ }
246
+
247
+ function resolveViaChecker(
248
+ node: ts.TypeNode,
249
+ sourceFile: ts.SourceFile,
250
+ ctx: ResolveContext,
251
+ fieldName?: string,
252
+ ): ts.TypeNode {
253
+ const { program, checker } = ctx.getChecker()
254
+ const nodeFileName = resolve(node.getSourceFile().fileName)
255
+ const programSf = program.getSourceFile(nodeFileName) ?? program.getSourceFile(resolve(sourceFile.fileName))
256
+ if (!programSf) {
257
+ throw checkerResolutionError(fieldName)
258
+ }
259
+
260
+ const programNode = findNodeAtPosition(programSf, node.pos, node.end) ?? node
261
+
262
+ const type = checker.getTypeAtLocation(programNode)
263
+ let typeText = checker.typeToString(
264
+ type,
265
+ programNode,
266
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
267
+ )
268
+ if (!typeText.trim()) {
269
+ throw checkerResolutionError(fieldName)
270
+ }
271
+
272
+ let parsed = parseResolvedTypeText(typeText)
273
+ if (needsChecker(parsed)) {
274
+ typeText = checker.typeToString(type, programNode)
275
+ parsed = parseResolvedTypeText(typeText)
276
+ }
277
+ if (needsChecker(parsed)) {
278
+ throw checkerResolutionError(fieldName)
279
+ }
280
+ return parsed
281
+ }
282
+
283
+ function parseResolvedTypeText(typeText: string): ts.TypeNode {
284
+ const parsed = ts.createSourceFile(
285
+ "__resolved__.ts",
286
+ `type __R__ = ${typeText}`,
287
+ ts.ScriptTarget.Latest,
288
+ true,
289
+ ts.ScriptKind.TS,
290
+ )
291
+ const decl = parsed.statements[0]
292
+ if (!decl || !ts.isTypeAliasDeclaration(decl)) {
293
+ throw new Error("Internal error: failed to parse checker output.")
294
+ }
295
+ return decl.type
296
+ }
297
+
298
+ /** `T extends string ? Optional<T> : T` and similar — pick the Optional branch for scalar args. */
299
+ function tryEvaluateConditionalAlias(entry: AliasEntry, typeArgs: readonly ts.TypeNode[]): ts.TypeNode | null {
300
+ const substituted = substituteAndParse(entry, typeArgs)
301
+ if (!ts.isConditionalTypeNode(substituted)) return null
302
+ if (substituted.extendsType.getText() !== "string") return null
303
+ const trueType = substituted.trueType
304
+ if (
305
+ ts.isTypeReferenceNode(trueType) &&
306
+ ts.isIdentifier(trueType.typeName) &&
307
+ trueType.typeName.text === "Optional"
308
+ ) {
309
+ return trueType
310
+ }
311
+ return null
312
+ }
313
+
314
+ /** `{ [K in keyof T]: Optional<T[K]> }` with a concrete `T` type literal. */
315
+ function tryEvaluateMappedOptionalFields(entry: AliasEntry, typeArgs: readonly ts.TypeNode[]): ts.TypeNode | null {
316
+ if (!ts.isMappedTypeNode(entry.body)) return null
317
+ const template = entry.body.type
318
+ if (
319
+ template === undefined ||
320
+ !ts.isTypeReferenceNode(template) ||
321
+ template.typeName.getText() !== "Optional"
322
+ ) {
323
+ return null
324
+ }
325
+
326
+ const arg = typeArgs[0]
327
+ if (!arg || !ts.isTypeLiteralNode(arg)) return null
328
+
329
+ const argSource = arg.getSourceFile()
330
+ const parts: string[] = []
331
+ for (const member of arg.members) {
332
+ if (!ts.isPropertySignature(member) || !member.type) continue
333
+ const name = member.name.getText(argSource)
334
+ const typeText = member.type.getText(argSource)
335
+ parts.push(`${name}: Optional<${typeText}>`)
336
+ }
337
+ if (parts.length === 0) return null
338
+
339
+ const parsed = ts.createSourceFile(
340
+ "__mapped__.ts",
341
+ `type __M__ = { ${parts.join("; ")} }`,
342
+ ts.ScriptTarget.Latest,
343
+ true,
344
+ ts.ScriptKind.TS,
345
+ )
346
+ const decl = parsed.statements[0]
347
+ if (!decl || !ts.isTypeAliasDeclaration(decl) || !ts.isTypeLiteralNode(decl.type)) return null
348
+ return decl.type
349
+ }
350
+
351
+ function checkerResolutionError(fieldName?: string): Error {
352
+ const prefix = fieldName ? `Field "${fieldName}": ` : ""
353
+ return new Error(`${prefix}could not resolve conditional/mapped type via type checker.`)
354
+ }
355
+
356
+ const SUPATYPE_CHECKER_STUB_PATH = resolve(process.cwd(), "__supatype_checker_stubs__.ts")
357
+ const SUPATYPE_CHECKER_STUB_SOURCE = `
358
+ export type Optional<T> = T;
359
+ export type Unique<T> = T;
360
+ export type PrimaryKey<T> = T;
361
+ export type Localized<T> = T;
362
+ export type NotLocalized<T> = T;
363
+ export type Default<T, V> = T;
364
+ export type Email = string & { readonly __supatypeEmailBrand: unique symbol };
365
+ export type UUID = string & { readonly __supatypeUuidBrand: unique symbol };
366
+ export type RichText = unknown;
367
+ export type Int = number;
368
+ export type SmallInt = number;
369
+ export type BigInt = number;
370
+ export type Timestamp = string;
371
+ export type Date = string;
372
+ export type DateTime = string;
373
+ export type DateOnly = string;
374
+ export type Slug<S extends string = string> = string;
375
+ export type RelatedTo<T> = string;
376
+ export type Text = string;
377
+ `
378
+
379
+ function createCheckerStubFile(): ts.SourceFile {
380
+ return ts.createSourceFile(
381
+ SUPATYPE_CHECKER_STUB_PATH,
382
+ SUPATYPE_CHECKER_STUB_SOURCE,
383
+ ts.ScriptTarget.Latest,
384
+ true,
385
+ ts.ScriptKind.TS,
386
+ )
387
+ }
388
+
389
+ function createCheckerContext(sourceFiles: ts.SourceFile[]): CheckerContext {
390
+ const stubFile = createCheckerStubFile()
391
+ const fileMap = new Map<string, ts.SourceFile>([
392
+ [resolve(stubFile.fileName), stubFile],
393
+ ...sourceFiles.map((sf) => [resolve(sf.fileName), sf] as const),
394
+ ])
395
+ const options: ts.CompilerOptions = {
396
+ target: ts.ScriptTarget.Latest,
397
+ skipLibCheck: true,
398
+ baseUrl: process.cwd(),
399
+ paths: {
400
+ "@supatype/types": ["__supatype_checker_stubs__.ts"],
401
+ },
402
+ }
403
+
404
+ const host: ts.CompilerHost = {
405
+ getSourceFile: (fileName, languageVersion) => {
406
+ const existing = fileMap.get(resolve(fileName))
407
+ if (existing) return existing
408
+ const libContent = ts.sys.readFile(fileName)
409
+ if (libContent) {
410
+ return ts.createSourceFile(fileName, libContent, languageVersion, true)
411
+ }
412
+ return undefined
413
+ },
414
+ getDefaultLibFileName: (opts) => ts.getDefaultLibFileName(opts ?? options),
415
+ writeFile: () => {},
416
+ getCurrentDirectory: () => process.cwd(),
417
+ getCanonicalFileName: (f) => f,
418
+ useCaseSensitiveFileNames: () => true,
419
+ getNewLine: () => ts.sys.newLine,
420
+ fileExists: (fileName) => fileMap.has(resolve(fileName)) || ts.sys.fileExists(fileName),
421
+ readFile: (fileName) => fileMap.get(resolve(fileName))?.getFullText() ?? ts.sys.readFile(fileName),
422
+ }
423
+
424
+ const program = ts.createProgram([...fileMap.keys()], options, host)
425
+ return { program, checker: program.getTypeChecker() }
426
+ }
427
+
428
+ function findNodeAtPosition(root: ts.Node, pos: number, end: number): ts.Node | undefined {
429
+ const queue: ts.Node[] = [root]
430
+ while (queue.length > 0) {
431
+ const current = queue.shift()
432
+ if (!current) continue
433
+ if (current.pos === pos && current.end === end) return current
434
+ ts.forEachChild(current, (child) => queue.push(child))
435
+ }
436
+ return undefined
437
+ }
438
+
439
+ function containsConditionalOrMapped(node: ts.Node): boolean {
440
+ if (ts.isConditionalTypeNode(node) || ts.isMappedTypeNode(node)) return true
441
+ let found = false
442
+ ts.forEachChild(node, (child) => {
443
+ if (!found && containsConditionalOrMapped(child)) found = true
444
+ })
445
+ return found
446
+ }
447
+
448
+ function escapeRegExp(value: string): string {
449
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
450
+ }
451
+
452
+ export function unknownTypeError(typeName: string, fieldName: string): Error {
453
+ return new Error(
454
+ `Unknown Supatype type "${typeName}" in field "${fieldName}". ` +
455
+ "If this is a type alias, confirm the file defining it is reachable from your schema entry point.",
456
+ )
457
+ }
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { tmpdir } from "node:os"
5
5
  import { defineConfig, loadConfig } from "../src/config.js"
6
- import { mergeProjectConfig, type SupatypeProjectConfig } from "../src/project-config.js"
6
+ import { mergeProjectConfig, resolveRuntimeProvider, type SupatypeProjectConfig } from "../src/project-config.js"
7
7
  import { DENO_RELEASE_PIN } from "../src/release-pins.js"
8
8
 
9
9
  let counter = 0
@@ -180,6 +180,26 @@ describe("loadConfig()", () => {
180
180
  })
181
181
  })
182
182
 
183
+ describe("resolveRuntimeProvider()", () => {
184
+ it("prefers top-level provider over database.provider", () => {
185
+ const cfg = defineConfig({
186
+ ...minimalProject("p"),
187
+ provider: "docker",
188
+ database: { provider: "native" },
189
+ })
190
+ expect(resolveRuntimeProvider(cfg)).toBe("docker")
191
+ })
192
+
193
+ it("falls back to database.provider then native", () => {
194
+ expect(resolveRuntimeProvider(defineConfig({ ...minimalProject("p"), database: { provider: "docker" } }))).toBe(
195
+ "docker",
196
+ )
197
+ expect(resolveRuntimeProvider(defineConfig({ ...minimalProject("p"), database: { provider: "native" } }))).toBe(
198
+ "native",
199
+ )
200
+ })
201
+ })
202
+
183
203
  describe("mergeProjectConfig()", () => {
184
204
  it("deep-merges email.smtp between base and local", () => {
185
205
  const base = defineConfig({
@@ -213,8 +233,19 @@ describe("mergeProjectConfig()", () => {
213
233
  app: { mode: "static", static_dir: "./dist", vite_dev_url: "http://127.0.0.1:1111" },
214
234
  })
215
235
  const merged = mergeProjectConfig(base, { app: { vite_dev_url: "http://127.0.0.1:5173" } })
216
- expect(merged.app.vite_dev_url).toBe("http://127.0.0.1:5173")
217
- expect(merged.app.mode).toBe("static")
236
+ expect(merged.app?.vite_dev_url).toBe("http://127.0.0.1:5173")
237
+ })
238
+
239
+ it("merges app.start for proxy dev script override", () => {
240
+ const base = defineConfig({
241
+ ...minimalProject("p"),
242
+ app: { mode: "static", static_dir: "./dist" },
243
+ })
244
+ const merged = mergeProjectConfig(base, {
245
+ app: { mode: "proxy", upstream: "http://127.0.0.1:4321", start: "dev:site" },
246
+ })
247
+ expect(merged.app.mode).toBe("proxy")
248
+ expect(merged.app.start).toBe("dev:site")
218
249
  expect(merged.app.static_dir).toBe("./dist")
219
250
  })
220
251
  })
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ containerName,
4
+ dockerPgLoopbackDbUrl,
5
+ dockerPgPostInitServing,
6
+ } from "../src/docker-postgres.js"
7
+
8
+ describe("docker-postgres", () => {
9
+ it("derives container name from project name", () => {
10
+ expect(containerName("supatype-integration")).toBe("supatype-supatype-integration")
11
+ })
12
+
13
+ it("detects post-init serving after first-boot init", () => {
14
+ const logs = [
15
+ "PostgreSQL init process complete; ready for start up.",
16
+ "database system is ready to accept connections",
17
+ ].join("\n")
18
+ expect(dockerPgPostInitServing(logs)).toBe(true)
19
+ })
20
+
21
+ it("detects post-init serving on reused volume (no init banner this run)", () => {
22
+ const logs = "database system is ready to accept connections\n"
23
+ expect(dockerPgPostInitServing(logs)).toBe(true)
24
+ })
25
+
26
+ it("builds loopback DB URL for in-container migrate", () => {
27
+ expect(dockerPgLoopbackDbUrl("my-project")).toBe(
28
+ "postgres://supatype_admin:postgres@127.0.0.1:5432/my-project?sslmode=disable",
29
+ )
30
+ })
31
+
32
+ it("rejects ready line before init complete on first boot", () => {
33
+ const logs = [
34
+ "database system is ready to accept connections",
35
+ "PostgreSQL init process complete; ready for start up.",
36
+ ].join("\n")
37
+ expect(dockerPgPostInitServing(logs)).toBe(false)
38
+ })
39
+ })
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { normalizeAdminConfig } from "../../studio/src/lib/normalize-admin-config.js"
3
+
4
+ describe("normalizeAdminConfig", () => {
5
+ it("maps global fields and Settings navigation items", () => {
6
+ const config = normalizeAdminConfig({
7
+ localization: { locales: ["en"], defaultLocale: "en" },
8
+ navigation: [
9
+ {
10
+ group: "Content",
11
+ items: [{ label: "Post", model: "posts" }],
12
+ },
13
+ {
14
+ group: "Settings",
15
+ items: [{ label: "Site Settings", global: "_global_site_settings" }],
16
+ },
17
+ ],
18
+ models: [
19
+ {
20
+ name: "Post",
21
+ tableName: "posts",
22
+ fields: [{ name: "title", widget: "text", required: true }],
23
+ },
24
+ ],
25
+ globals: [
26
+ {
27
+ name: "SiteSettings",
28
+ tableName: "_global_site_settings",
29
+ singleton: true,
30
+ fields: [{ name: "site_name", widget: "text", required: true }],
31
+ },
32
+ ],
33
+ })
34
+
35
+ expect(config.globals).toHaveLength(1)
36
+ expect(config.globals[0]?.tableName).toBe("_global_site_settings")
37
+ expect(config.globals[0]?.apiPath).toBe("/rest/v1/_global_site_settings")
38
+ expect(config.globals[0]?.fields).toHaveLength(1)
39
+ expect(config.globals[0]?.fields[0]?.name).toBe("site_name")
40
+
41
+ const settingsNav = config.navigation.find((g) => g.label === "Settings")
42
+ expect(settingsNav?.items[0]).toMatchObject({
43
+ type: "global",
44
+ href: "/models/globals/SiteSettings",
45
+ label: "Site Settings",
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { resolveProxyDevScript } from "../src/app/proxy-dev-app.js"
3
+ import type { SupatypeProjectConfig } from "../src/project-config.js"
4
+
5
+ const base = {
6
+ project: { name: "test" },
7
+ database: { provider: "native" as const },
8
+ server: { mode: "dev" as const },
9
+ versions: { engine: "latest", server: "latest", postgres: "latest", deno: "latest" },
10
+ }
11
+
12
+ describe("resolveProxyDevScript()", () => {
13
+ it("returns null when app.mode is not proxy", () => {
14
+ const config = { ...base, app: { mode: "static" as const } } satisfies SupatypeProjectConfig
15
+ expect(resolveProxyDevScript(config)).toBeNull()
16
+ })
17
+
18
+ it("defaults to start when proxy mode and app.start omitted", () => {
19
+ const config = {
20
+ ...base,
21
+ app: { mode: "proxy" as const, upstream: "http://127.0.0.1:4321" },
22
+ } satisfies SupatypeProjectConfig
23
+ expect(resolveProxyDevScript(config)).toBe("start")
24
+ })
25
+
26
+ it("uses app.start when set", () => {
27
+ const config = {
28
+ ...base,
29
+ app: { mode: "proxy" as const, upstream: "http://127.0.0.1:4321", start: "dev:site" },
30
+ } satisfies SupatypeProjectConfig
31
+ expect(resolveProxyDevScript(config)).toBe("dev:site")
32
+ })
33
+ })