@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,324 @@
1
+ /**
2
+ * AST v2 wire format — canonical types and emitters.
3
+ * Parsers build {@link ParsedField}; only `emitField` / `emitModel` / `emitSchema` produce JSON.
4
+ */
5
+
6
+ export const AST_VERSION = 2 as const
7
+
8
+ export type DefaultAst =
9
+ | { kind: "value"; value: string | number | boolean | null }
10
+ | { kind: "now" }
11
+ | { kind: "genRandomUuid" }
12
+ | { kind: "expression"; expr: string }
13
+
14
+ export interface DbFieldAnnotations {
15
+ pgType?: string
16
+ unique?: boolean
17
+ index?: boolean
18
+ foreignKey?: string
19
+ serverGenerated?: boolean
20
+ elementType?: string
21
+ }
22
+
23
+ export interface PlatformFieldAnnotations {
24
+ editor?: string
25
+ readOnly?: boolean
26
+ }
27
+
28
+ export interface FieldAnnotations {
29
+ db?: DbFieldAnnotations
30
+ platform?: PlatformFieldAnnotations
31
+ }
32
+
33
+ /** Kernel facts only — never db/platform keys at this layer. */
34
+ export interface KernelFieldFacts {
35
+ required?: boolean
36
+ primaryKey?: boolean
37
+ default?: DefaultAst
38
+ localized?: boolean
39
+ cardinality?: string
40
+ target?: string
41
+ values?: string[]
42
+ from?: string
43
+ sources?: string[]
44
+ template?: string
45
+ bucket?: string
46
+ accessMode?: string
47
+ geoType?: string
48
+ srid?: number
49
+ dimensions?: number
50
+ blocks?: BlockDefinitionAst[]
51
+ check?: string
52
+ precision?: number
53
+ scale?: number
54
+ references?: string
55
+ through?: string
56
+ onDelete?: string
57
+ onUpdate?: string
58
+ uniqueFk?: boolean
59
+ plugin?: string
60
+ fieldType?: string
61
+ tsType?: string
62
+ index?: boolean
63
+ }
64
+
65
+ /** Internal parse result — not serialized. */
66
+ export interface ParsedField {
67
+ kind: string
68
+ kernel: KernelFieldFacts
69
+ db: DbFieldAnnotations
70
+ platform: PlatformFieldAnnotations
71
+ }
72
+
73
+ export type FieldAstV2 = {
74
+ kind: string
75
+ annotations?: FieldAnnotations
76
+ } & Record<string, unknown>
77
+
78
+ export interface BlockDefinitionAst {
79
+ name: string
80
+ label?: string
81
+ icon?: string
82
+ fields: Record<string, FieldAstV2>
83
+ }
84
+
85
+ export interface ModelAstV2 {
86
+ name: string
87
+ fields: Record<string, FieldAstV2>
88
+ options: Record<string, unknown>
89
+ annotations: {
90
+ db: { tableName: string; indexes: unknown[] }
91
+ platform: { access: Record<string, unknown> }
92
+ }
93
+ }
94
+
95
+ export interface ExtractedStorageBucketAst {
96
+ id: string
97
+ public: boolean
98
+ accessMode?: "public" | "private" | "custom"
99
+ allowedMimeTypes?: string[]
100
+ fileSizeLimit?: number
101
+ access?: Record<string, unknown>
102
+ s3BucketPolicy?: string
103
+ }
104
+
105
+ export interface ExtractedSchemaAstV2 {
106
+ astVersion: typeof AST_VERSION
107
+ models: ModelAstV2[]
108
+ storageBuckets?: ExtractedStorageBucketAst[]
109
+ locales?: string[]
110
+ defaultLocale?: string
111
+ }
112
+
113
+ const DEFAULT_DB_BY_KIND: Record<string, Partial<DbFieldAnnotations>> = {
114
+ text: { pgType: "TEXT" },
115
+ richText: { pgType: "JSONB" },
116
+ integer: { pgType: "INTEGER" },
117
+ smallInt: { pgType: "SMALLINT" },
118
+ bigInt: { pgType: "BIGINT" },
119
+ float: { pgType: "DOUBLE PRECISION" },
120
+ boolean: { pgType: "BOOLEAN" },
121
+ datetime: { pgType: "TIMESTAMPTZ" },
122
+ date: { pgType: "DATE" },
123
+ timestamp: { pgType: "TIMESTAMP WITH TIME ZONE" },
124
+ uuid: { pgType: "UUID" },
125
+ email: { pgType: "TEXT" },
126
+ url: { pgType: "TEXT" },
127
+ slug: { pgType: "TEXT" },
128
+ enum: { pgType: "TEXT" },
129
+ json: { pgType: "JSONB" },
130
+ decimal: { pgType: "TEXT" },
131
+ bytes: { pgType: "BYTEA" },
132
+ serial: { pgType: "SERIAL" },
133
+ bigSerial: { pgType: "BIGSERIAL" },
134
+ money: { pgType: "TEXT" },
135
+ ip: { pgType: "TEXT" },
136
+ cidr: { pgType: "TEXT" },
137
+ macaddr: { pgType: "TEXT" },
138
+ xml: { pgType: "TEXT" },
139
+ tsQuery: { pgType: "TEXT" },
140
+ tsVector: { pgType: "TEXT" },
141
+ color: { pgType: "TEXT" },
142
+ array: { pgType: "ARRAY" },
143
+ image: { pgType: "JSONB" },
144
+ file: { pgType: "JSONB" },
145
+ geo: { pgType: "GEOGRAPHY" },
146
+ vector: { pgType: "VECTOR" },
147
+ blocks: { pgType: "JSONB" },
148
+ }
149
+
150
+ const DEFAULT_PLATFORM_BY_KIND: Record<string, Partial<PlatformFieldAnnotations>> = {
151
+ richText: { editor: "rich" },
152
+ }
153
+
154
+ const COMPOSITE_KINDS = new Set(["timestamps", "publishable", "softDelete"])
155
+
156
+ function hasKeys(obj: Record<string, unknown>): boolean {
157
+ return Object.keys(obj).length > 0
158
+ }
159
+
160
+ function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
161
+ const out: Record<string, unknown> = {}
162
+ for (const [k, v] of Object.entries(obj)) {
163
+ if (v !== undefined) out[k] = v
164
+ }
165
+ return out as Partial<T>
166
+ }
167
+
168
+ /** Start a parsed field with kind defaults for db/platform namespaces. */
169
+ export function defaultPgTypeForKind(kind: string): string {
170
+ return DEFAULT_DB_BY_KIND[kind]?.pgType ?? "TEXT"
171
+ }
172
+
173
+ /** Flat wire shape for fields nested inside `blocks` definitions (engine FieldAst serde). */
174
+ export function emitBlockNestedField(field: FieldAstV2): FieldAstV2 {
175
+ const annotations = (field.annotations ?? {}) as FieldAnnotations
176
+ const db = annotations.db ?? {}
177
+ const platform = annotations.platform ?? {}
178
+
179
+ const wire: FieldAstV2 = {
180
+ kind: field.kind,
181
+ pgType: db.pgType ?? defaultPgTypeForKind(String(field.kind)),
182
+ required: field.required ?? false,
183
+ unique: db.unique ?? false,
184
+ localized: field.localized ?? false,
185
+ }
186
+
187
+ if (platform.readOnly) wire.readOnly = true
188
+ if (field.primaryKey) wire.primaryKey = true
189
+ if (field.default !== undefined) wire.default = field.default
190
+ if (field.from !== undefined) wire.from = field.from
191
+ if (field.values !== undefined) wire.values = field.values
192
+ if (field.bucket !== undefined) wire.bucket = field.bucket
193
+ if (field.accessMode !== undefined) wire.accessMode = field.accessMode
194
+ if (field.geoType !== undefined) wire.geoType = field.geoType
195
+ if (field.srid !== undefined) wire.srid = field.srid
196
+ if (field.dimensions !== undefined) wire.dimensions = field.dimensions
197
+ if (field.check !== undefined) wire.check = field.check
198
+ if (field.precision !== undefined) wire.precision = field.precision
199
+ if (field.scale !== undefined) wire.scale = field.scale
200
+ if (field.sources !== undefined) wire.sources = field.sources
201
+ if (field.template !== undefined) wire.template = field.template
202
+ if (field.plugin !== undefined) wire.plugin = field.plugin
203
+ if (field.fieldType !== undefined) wire.fieldType = field.fieldType
204
+ if (field.tsType !== undefined) wire.tsType = field.tsType
205
+
206
+ return wire
207
+ }
208
+
209
+ export function scalar(
210
+ kind: string,
211
+ extra?: {
212
+ kernel?: Partial<KernelFieldFacts>
213
+ db?: Partial<DbFieldAnnotations>
214
+ platform?: Partial<PlatformFieldAnnotations>
215
+ },
216
+ ): ParsedField {
217
+ return {
218
+ kind,
219
+ kernel: { ...(extra?.kernel ?? {}) },
220
+ db: { ...DEFAULT_DB_BY_KIND[kind], ...stripUndefined(extra?.db ?? {}) },
221
+ platform: { ...DEFAULT_PLATFORM_BY_KIND[kind], ...stripUndefined(extra?.platform ?? {}) },
222
+ }
223
+ }
224
+
225
+ export function emitField(parsed: ParsedField): FieldAstV2 {
226
+ if (COMPOSITE_KINDS.has(parsed.kind)) {
227
+ return { kind: parsed.kind }
228
+ }
229
+
230
+ const db = stripUndefined({
231
+ ...DEFAULT_DB_BY_KIND[parsed.kind],
232
+ ...parsed.db,
233
+ }) as DbFieldAnnotations
234
+
235
+ const platform = stripUndefined({
236
+ ...DEFAULT_PLATFORM_BY_KIND[parsed.kind],
237
+ ...parsed.platform,
238
+ }) as PlatformFieldAnnotations
239
+
240
+ const wire: FieldAstV2 = { kind: parsed.kind }
241
+
242
+ const kernel = parsed.kernel
243
+ if (kernel.required !== undefined) wire.required = kernel.required
244
+ if (kernel.primaryKey) wire.primaryKey = true
245
+ if (kernel.default !== undefined) wire.default = kernel.default
246
+ if (kernel.localized) wire.localized = true
247
+ if (kernel.cardinality !== undefined) wire.cardinality = kernel.cardinality
248
+ if (kernel.target !== undefined) wire.target = kernel.target
249
+ if (kernel.values !== undefined) wire.values = kernel.values
250
+ if (kernel.from !== undefined) wire.from = kernel.from
251
+ if (kernel.sources !== undefined && kernel.sources.length > 0) wire.sources = kernel.sources
252
+ if (kernel.template !== undefined) wire.template = kernel.template
253
+ if (kernel.bucket !== undefined) wire.bucket = kernel.bucket
254
+ if (kernel.accessMode !== undefined) wire.accessMode = kernel.accessMode
255
+ if (kernel.geoType !== undefined) wire.geoType = kernel.geoType
256
+ if (kernel.srid !== undefined) wire.srid = kernel.srid
257
+ if (kernel.dimensions !== undefined) wire.dimensions = kernel.dimensions
258
+ if (kernel.blocks !== undefined) {
259
+ wire.blocks = kernel.blocks.map((blockDef) => ({
260
+ ...blockDef,
261
+ fields: Object.fromEntries(
262
+ Object.entries(blockDef.fields).map(([name, nested]) => [
263
+ name,
264
+ emitBlockNestedField(nested),
265
+ ]),
266
+ ),
267
+ }))
268
+ }
269
+ if (kernel.check !== undefined) wire.check = kernel.check
270
+ if (kernel.precision !== undefined) wire.precision = kernel.precision
271
+ if (kernel.scale !== undefined) wire.scale = kernel.scale
272
+ if (kernel.references !== undefined) wire.references = kernel.references
273
+ if (kernel.through !== undefined) wire.through = kernel.through
274
+ if (kernel.onDelete !== undefined) wire.onDelete = kernel.onDelete
275
+ if (kernel.onUpdate !== undefined) wire.onUpdate = kernel.onUpdate
276
+ if (kernel.uniqueFk) wire.uniqueFk = true
277
+ if (kernel.plugin !== undefined) wire.plugin = kernel.plugin
278
+ if (kernel.fieldType !== undefined) wire.fieldType = kernel.fieldType
279
+ if (kernel.tsType !== undefined) wire.tsType = kernel.tsType
280
+ if (parsed.kind === "blocks" && kernel.index !== undefined) wire.index = kernel.index
281
+
282
+ const annotations: FieldAnnotations = {}
283
+ if (hasKeys(db as Record<string, unknown>)) annotations.db = db
284
+ if (hasKeys(platform as Record<string, unknown>)) annotations.platform = platform
285
+ if (hasKeys(annotations as Record<string, unknown>)) wire.annotations = annotations
286
+
287
+ return wire
288
+ }
289
+
290
+ export function emitModel(
291
+ name: string,
292
+ fields: Record<string, FieldAstV2>,
293
+ options: Record<string, unknown>,
294
+ tableName: string,
295
+ access: Record<string, unknown>,
296
+ ): ModelAstV2 {
297
+ return {
298
+ name,
299
+ fields,
300
+ options,
301
+ annotations: {
302
+ db: { tableName, indexes: [] },
303
+ platform: { access },
304
+ },
305
+ }
306
+ }
307
+
308
+ export function emitSchema(
309
+ models: ModelAstV2[],
310
+ extras?: {
311
+ storageBuckets?: ExtractedSchemaAstV2["storageBuckets"]
312
+ locales?: string[]
313
+ defaultLocale?: string
314
+ },
315
+ ): ExtractedSchemaAstV2 {
316
+ return {
317
+ astVersion: AST_VERSION,
318
+ models,
319
+ ...(extras?.storageBuckets !== undefined &&
320
+ extras.storageBuckets.length > 0 && { storageBuckets: extras.storageBuckets }),
321
+ ...(extras?.locales !== undefined && extras.locales.length > 0 && { locales: extras.locales }),
322
+ ...(extras?.defaultLocale !== undefined && { defaultLocale: extras.defaultLocale }),
323
+ }
324
+ }
package/src/seed.ts ADDED
@@ -0,0 +1,43 @@
1
+ import pg from "pg"
2
+
3
+ export interface SeedSql {
4
+ (strings: TemplateStringsArray, ...values: unknown[]): Promise<pg.QueryResult>
5
+ end(): Promise<void>
6
+ }
7
+
8
+ /** Lightweight tagged-template SQL helper for project `seed.ts` scripts. */
9
+ export function sql(connectionString: string): SeedSql {
10
+ const client = new pg.Client({ connectionString })
11
+ let connected = false
12
+
13
+ const ensureConnected = async (): Promise<void> => {
14
+ if (!connected) {
15
+ await client.connect()
16
+ connected = true
17
+ }
18
+ }
19
+
20
+ const tag = async (
21
+ strings: TemplateStringsArray,
22
+ ...values: unknown[]
23
+ ): Promise<pg.QueryResult> => {
24
+ await ensureConnected()
25
+ let text = ""
26
+ for (let i = 0; i < strings.length; i++) {
27
+ text += strings[i]
28
+ if (i < values.length) {
29
+ text += `$${i + 1}`
30
+ }
31
+ }
32
+ return client.query(text, values)
33
+ }
34
+
35
+ return Object.assign(tag, {
36
+ end: async (): Promise<void> => {
37
+ if (connected) {
38
+ await client.end()
39
+ connected = false
40
+ }
41
+ },
42
+ })
43
+ }
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs"
2
2
  import { dirname, join, relative, resolve } from "node:path"
3
3
  import { spawnSync } from "node:child_process"
4
- import type { SupatypeProjectConfig } from "./project-config.js"
4
+ import { preferredFunctionsPathFromProject, type SupatypeProjectConfig } from "./project-config.js"
5
+ import { hasEngineOverride, hasStudioOverride } from "./binary-cache.js"
5
6
  import { buildKongDeclarative } from "./kong-config.js"
6
7
 
7
8
  export interface SelfHostComposePaths {
@@ -33,43 +34,138 @@ export function staticDirForCompose(config: SupatypeProjectConfig): string | und
33
34
  return dir && dir.length > 0 ? dir : "./public"
34
35
  }
35
36
 
36
- function projectMountPath(cwd: string): string {
37
- const composeDir = resolve(cwd, ".supatype", "self-host")
38
- let rel = relative(composeDir, resolve(cwd)).replace(/\\/g, "/")
37
+ /**
38
+ * Bind-mount source for `/project` in generated compose files.
39
+ * Paths are resolved from `--project-directory` (always the project root in `runDockerCompose`),
40
+ * not from the compose file directory — use `.` not `../..`.
41
+ */
42
+ function projectMountPath(_cwd: string): string {
43
+ return "."
44
+ }
45
+
46
+ /** Paths in generated compose are resolved from `--project-directory` (project root). */
47
+ function relativeFromProjectRoot(cwd: string, target: string): string {
48
+ let rel = relative(resolve(cwd), resolve(target)).replace(/\\/g, "/")
39
49
  if (!rel.startsWith(".") && !rel.startsWith("/")) {
40
50
  rel = `./${rel}`
41
51
  }
42
- return rel || "../.."
52
+ return rel
53
+ }
54
+
55
+ function kongMountPath(_cwd: string): string {
56
+ return ".supatype/self-host/kong.yml"
43
57
  }
44
58
 
45
- function serverAppEnvForCompose(config: SupatypeProjectConfig): string {
59
+ /** Host Vite dev server as seen from Kong inside Docker Compose. */
60
+ export const COMPOSE_STUDIO_HOST_URL = "http://host.docker.internal:3002"
61
+
62
+ /** Local monorepo Studio image for `supatype dev` (dogfooding). */
63
+ function studioServiceBlock(cwd: string, devLocal: boolean): string {
64
+ if (!devLocal) {
65
+ return ` image: \${SUPATYPE_STUDIO_IMAGE:-supatype/studio:latest}`
66
+ }
67
+ const monorepoRoot = resolve(cwd, "..", "supatype")
68
+ const studioDockerfile = join(monorepoRoot, "packages", "studio", "Dockerfile")
69
+ if (!existsSync(studioDockerfile) || !existsSync(join(monorepoRoot, "pnpm-workspace.yaml"))) {
70
+ return ` image: \${SUPATYPE_STUDIO_IMAGE:-supatype/studio:latest}`
71
+ }
72
+ const context = relativeFromProjectRoot(cwd, monorepoRoot)
73
+ return ` build:
74
+ context: ${context}
75
+ dockerfile: packages/studio/Dockerfile
76
+ image: supatype/studio:dev-local`
77
+ }
78
+
79
+ /** Host dev app (Astro/Vite on the machine) as seen from inside compose services. */
80
+ function proxyUpstreamForCompose(upstream: string, devLocal: boolean): string {
81
+ const trimmed = upstream.trim()
82
+ if (!devLocal) return trimmed
83
+ try {
84
+ const url = new URL(trimmed)
85
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
86
+ url.hostname = "host.docker.internal"
87
+ return url.toString()
88
+ }
89
+ } catch {
90
+ // keep literal upstream when not a URL
91
+ }
92
+ return trimmed
93
+ }
94
+
95
+ function serverAppEnvForCompose(config: SupatypeProjectConfig, devLocal: boolean): string {
46
96
  const mode = config.app.mode ?? "none"
47
97
  const lines = [` SUPATYPE_APP_MODE: ${mode}`]
48
98
  if (mode === "static") {
49
99
  const dir = staticDirForCompose(config) ?? "./public"
50
100
  lines.push(` SUPATYPE_APP_STATIC_DIR: /project/${dir.replace(/^\.\//, "")}`)
51
101
  } else if (mode === "proxy" && config.app.upstream?.trim()) {
52
- lines.push(` SUPATYPE_APP_UPSTREAM: ${config.app.upstream.trim()}`)
102
+ lines.push(` SUPATYPE_APP_UPSTREAM: ${proxyUpstreamForCompose(config.app.upstream, devLocal)}`)
53
103
  }
54
104
  return lines.join("\n")
55
105
  }
56
106
 
57
- export function renderSelfHostCompose(config: SupatypeProjectConfig, cwd: string = process.cwd()): string {
107
+ export interface SelfHostComposeOptions {
108
+ /** `supatype dev` with provider docker: internal-only db/server; Kong on host :18473. */
109
+ devLocal?: boolean
110
+ }
111
+
112
+ export function renderSelfHostCompose(
113
+ config: SupatypeProjectConfig,
114
+ cwd: string = process.cwd(),
115
+ options?: SelfHostComposeOptions,
116
+ ): string {
58
117
  const projectMount = projectMountPath(cwd)
59
- const appEnv = serverAppEnvForCompose(config)
118
+ const kongMount = kongMountPath(cwd)
119
+ const devLocal = options?.devLocal === true
120
+ const studioHostDev = devLocal && hasStudioOverride(config)
121
+ const appEnv = serverAppEnvForCompose(config, devLocal)
122
+ const studioService = studioServiceBlock(cwd, devLocal)
123
+ const studioBlock = studioHostDev
124
+ ? ""
125
+ : `
126
+ studio:
127
+ ${studioService}
128
+ environment:
129
+ SUPATYPE_CLOUD_JSON: '{"url":"\${API_EXTERNAL_URL:-http://localhost:18473}","anonKey":"\${ANON_KEY:-}"}'
130
+ expose:
131
+ - "3002"
132
+ `
133
+ const kongDependsOn = studioHostDev
134
+ ? ` - server`
135
+ : ` - server
136
+ - studio`
137
+ const publishDbToHost = !devLocal || hasEngineOverride(config)
138
+ const dbPorts = publishDbToHost
139
+ ? devLocal
140
+ ? ` ports:
141
+ - "127.0.0.1:\${SUPATYPE_DEV_DB_PORT:-54329}:5432"
142
+ `
143
+ : ` ports:
144
+ - "5432:5432"
145
+ `
146
+ : ""
147
+ const serverPorts = devLocal
148
+ ? ""
149
+ : ` ports:
150
+ - "9999:9999"
151
+ `
152
+ const minioPorts = devLocal
153
+ ? ""
154
+ : ` ports:
155
+ - "9000:9000"
156
+ - "9001:9001"
157
+ `
60
158
 
61
159
  return `# Generated by supatype self-host compose
62
160
  # Kong → supatype-server (unified gateway) → internal PostgREST / storage / etc.
63
161
  services:
64
162
  db:
65
- image: \${SUPATYPE_POSTGRES_IMAGE:-supatype/postgres:17-latest}
163
+ image: \${SUPATYPE_POSTGRES_IMAGE:-supatype/postgres:latest}
66
164
  environment:
67
165
  POSTGRES_USER: \${POSTGRES_USER:-supatype_admin}
68
166
  POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
69
167
  POSTGRES_DB: \${POSTGRES_DB:-supatype}
70
- ports:
71
- - "5432:5432"
72
- volumes:
168
+ ${dbPorts} volumes:
73
169
  - db-data:/var/lib/postgresql/data
74
170
  healthcheck:
75
171
  test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-supatype_admin}"]
@@ -83,7 +179,7 @@ services:
83
179
  - "3000"
84
180
  environment:
85
181
  PGRST_DB_URI: postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}
86
- PGRST_DB_SCHEMA: "public, supatype"
182
+ PGRST_DB_SCHEMA: "public, supatype, graphql_public"
87
183
  PGRST_DB_ANON_ROLE: anon
88
184
  PGRST_JWT_SECRET: \${JWT_SECRET:-super-secret-jwt-token-change-in-production}
89
185
  PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
@@ -122,19 +218,20 @@ services:
122
218
  SUPATYPE_URL: \${API_EXTERNAL_URL:-http://localhost:18473}
123
219
  SUPATYPE_ANON_KEY: \${ANON_KEY:-}
124
220
  SUPATYPE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY:-}
221
+ STRIPE_SECRET_KEY: \${STRIPE_SECRET_KEY:-}
222
+ STRIPE_WEBHOOK_SECRET: \${STRIPE_WEBHOOK_SECRET:-}
223
+ SITE_URL: \${SITE_URL:-\${API_EXTERNAL_URL:-http://localhost:18473}}
125
224
  depends_on:
126
225
  db:
127
226
  condition: service_healthy
128
227
 
129
228
  server:
130
229
  image: \${SUPATYPE_SERVER_IMAGE:-\${SUPATYPE_AUTH_IMAGE:-supatype/server:latest}}
131
- ports:
132
- - "9999:9999"
133
- volumes:
230
+ ${serverPorts} volumes:
134
231
  - ${projectMount}:/project:ro
135
232
  working_dir: /project
136
233
  environment:
137
- SUPATYPE_MODE: standalone
234
+ SUPATYPE_MODE: ${devLocal ? "dev" : "standalone"}
138
235
  SUPATYPE_MANIFEST_PATH: /project/.supatype/manifest.json
139
236
  SUPATYPE_ADMIN_CONFIG_PATH: /project/.supatype/admin-config.json
140
237
  SUPATYPE_API_CONFIG_PATH: /project/.supatype/api-config.json
@@ -162,6 +259,7 @@ ${appEnv}
162
259
  GOTRUE_JWT_ADMIN_ROLES: service_role,supatype_admin
163
260
  GOTRUE_MAILER_AUTOCONFIRM: \${GOTRUE_MAILER_AUTOCONFIRM:-true}
164
261
  GOTRUE_DISABLE_SIGNUP: \${DISABLE_SIGNUP:-false}
262
+ ${devLocal ? " STUDIO_OPEN_DEV: \"1\"\n" : ""}
165
263
  depends_on:
166
264
  db:
167
265
  condition: service_healthy
@@ -178,17 +276,20 @@ ${appEnv}
178
276
  environment:
179
277
  MINIO_ROOT_USER: supatype
180
278
  MINIO_ROOT_PASSWORD: supatype-secret
181
- ports:
182
- - "9000:9000"
183
- - "9001:9001"
184
- volumes:
279
+ ${minioPorts} volumes:
185
280
  - minio-data:/data
186
281
 
187
- studio:
188
- image: \${SUPATYPE_STUDIO_IMAGE:-supatype/studio:latest}
189
- expose:
190
- - "3002"
191
-
282
+ schema-engine:
283
+ image: \${SUPATYPE_ENGINE_IMAGE:-supatype/schema-engine:latest}
284
+ profiles: ["tools"]
285
+ entrypoint: ["supatype-engine"]
286
+ volumes:
287
+ - ${projectMount}:/project
288
+ working_dir: /project
289
+ depends_on:
290
+ db:
291
+ condition: service_healthy
292
+ ${studioBlock}
192
293
  kong:
193
294
  image: kong:3.6
194
295
  environment:
@@ -199,12 +300,11 @@ ${appEnv}
199
300
  KONG_PROXY_ERROR_LOG: /dev/stderr
200
301
  KONG_ADMIN_ERROR_LOG: /dev/stderr
201
302
  volumes:
202
- - ./kong.yml:/etc/kong/kong.yml:ro
303
+ - ${kongMount}:/etc/kong/kong.yml:ro
203
304
  ports:
204
- - "18473:8000"
305
+ - "\${SUPATYPE_KONG_PORT:-18473}:8000"
205
306
  depends_on:
206
- - server
207
- - studio
307
+ ${kongDependsOn}
208
308
 
209
309
  volumes:
210
310
  db-data:
@@ -226,24 +326,50 @@ function ensureComposeManifest(cwd: string): void {
226
326
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
227
327
  }
228
328
 
229
- export function writeSelfHostCompose(cwd: string, config: SupatypeProjectConfig): SelfHostComposePaths {
329
+ function ensureProjectFunctionsDir(cwd: string, config: SupatypeProjectConfig): void {
330
+ mkdirSync(preferredFunctionsPathFromProject(config, cwd), { recursive: true })
331
+ }
332
+
333
+ export function writeSelfHostCompose(
334
+ cwd: string,
335
+ config: SupatypeProjectConfig,
336
+ options?: SelfHostComposeOptions,
337
+ ): SelfHostComposePaths {
230
338
  const paths = selfHostComposePaths(cwd)
231
339
  mkdirSync(paths.dir, { recursive: true })
340
+ ensureProjectFunctionsDir(cwd, config)
232
341
  ensureComposeManifest(cwd)
233
- writeFileSync(paths.composePath, renderSelfHostCompose(config, cwd), "utf8")
342
+ writeFileSync(paths.composePath, renderSelfHostCompose(config, cwd, options), "utf8")
343
+ const studioHostDev = options?.devLocal === true && hasStudioOverride(config)
234
344
  writeFileSync(
235
345
  paths.kongPath,
236
346
  buildKongDeclarative({
237
347
  unifiedGateway: true,
348
+ ...(studioHostDev && {
349
+ studioServiceUrl: COMPOSE_STUDIO_HOST_URL,
350
+ studioStripPath: false,
351
+ }),
238
352
  }),
239
353
  "utf8",
240
354
  )
241
355
  return paths
242
356
  }
243
357
 
244
- export function runDockerCompose(composePath: string, args: string[], projectRoot: string = process.cwd()): number {
358
+ export function runDockerCompose(
359
+ composePath: string,
360
+ args: string[],
361
+ projectRoot: string = process.cwd(),
362
+ composeProject?: string,
363
+ ): number {
245
364
  const envFile = resolve(projectRoot, ".env")
246
- const composeArgs = ["compose", "-f", composePath]
365
+ const composeArgs = ["compose"]
366
+ // Per-project name isolates containers/volumes/network so multiple Supatype
367
+ // projects on one machine never share a database (default would be the
368
+ // ".supatype/self-host" dir name, identical for every project).
369
+ if (composeProject) composeArgs.push("-p", composeProject)
370
+ // Resolve ${VAR} in compose.yml from the project root .env (not .supatype/self-host/).
371
+ composeArgs.push("--project-directory", projectRoot)
372
+ composeArgs.push("-f", composePath)
247
373
  if (existsSync(envFile)) {
248
374
  composeArgs.push("--env-file", envFile)
249
375
  }
@@ -251,7 +377,13 @@ export function runDockerCompose(composePath: string, args: string[], projectRoo
251
377
  const result = spawnSync(
252
378
  "docker",
253
379
  composeArgs,
254
- { stdio: "inherit", cwd: dirname(composePath) },
380
+ { stdio: "inherit", cwd: projectRoot },
255
381
  )
256
382
  return result.status ?? 1
257
383
  }
384
+
385
+ /** Compose project name for a Supatype project — isolates docker state per project. */
386
+ export function composeProjectName(projectName: string): string {
387
+ const slug = projectName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "")
388
+ return `supatype-${slug || "project"}`
389
+ }
@@ -0,0 +1,16 @@
1
+ import type { SupatypeProjectConfig } from "./project-config.js"
2
+
3
+ export const DEFAULT_STUDIO_ADMIN_ROLES = ["admin", "supatype_admin"] as const
4
+
5
+ /** Studio admin roles from `supatype.config.ts` `admin.roles` or defaults. */
6
+ export function studioAdminRoles(cfg: SupatypeProjectConfig): string[] {
7
+ const roles = cfg.admin?.roles
8
+ if (roles !== undefined && roles.length > 0) return roles
9
+ return [...DEFAULT_STUDIO_ADMIN_ROLES]
10
+ }
11
+
12
+ /** Merge `adminRoles` into engine admin-config JSON for Studio and supatype-server. */
13
+ export function withAdminRoles(admin: unknown, cfg: SupatypeProjectConfig): Record<string, unknown> {
14
+ const base = typeof admin === "object" && admin !== null ? (admin as Record<string, unknown>) : {}
15
+ return { ...base, adminRoles: studioAdminRoles(cfg) }
16
+ }