@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
@@ -9,7 +9,7 @@
9
9
  import { spawnSync } from "node:child_process"
10
10
 
11
11
  export interface DockerPgOptions {
12
- /** Docker image to run. Defaults to supatype/postgres:17-latest. */
12
+ /** Docker image to run. Defaults to supatype/postgres:latest. */
13
13
  image: string
14
14
  /** Project name — used to derive the container and volume names. */
15
15
  projectName: string
@@ -20,6 +20,26 @@ export interface DockerPgOptions {
20
20
  }
21
21
 
22
22
  const PG_USER = "supatype_admin"
23
+ /** Must match `dockerPgStart` default `POSTGRES_PASSWORD`. */
24
+ const DEFAULT_DEV_PASSWORD = "postgres"
25
+
26
+ function dockerPgPsql(
27
+ name: string,
28
+ db: string,
29
+ sql: string,
30
+ password = DEFAULT_DEV_PASSWORD,
31
+ ) {
32
+ return spawnSync(
33
+ "docker",
34
+ [
35
+ "exec",
36
+ "-e", `PGPASSWORD=${password}`,
37
+ name,
38
+ "psql", "--no-password", "-U", PG_USER, "-d", db, "-tAc", sql,
39
+ ],
40
+ { encoding: "utf8", stdio: "pipe" },
41
+ )
42
+ }
23
43
 
24
44
  /** Derived container name for a project. */
25
45
  export function containerName(projectName: string): string {
@@ -72,6 +92,72 @@ export function dockerPgStop(projectName: string): void {
72
92
  spawnSync("docker", ["stop", containerName(projectName)], { encoding: "utf8" })
73
93
  }
74
94
 
95
+ function dockerPgLogsTail(name: string, tail = 120): string {
96
+ const logs = spawnSync(
97
+ "docker",
98
+ ["logs", "--tail", String(tail), name],
99
+ { encoding: "utf8" },
100
+ )
101
+ return `${logs.stdout ?? ""}${logs.stderr ?? ""}`
102
+ }
103
+
104
+ function dockerPgHealthStatus(name: string): string | undefined {
105
+ const inspect = spawnSync(
106
+ "docker",
107
+ ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", name],
108
+ { encoding: "utf8", stdio: "pipe" },
109
+ )
110
+ if (inspect.status !== 0) return undefined
111
+ const status = inspect.stdout?.trim()
112
+ return status === "" ? undefined : status
113
+ }
114
+
115
+ /** True when the final post-init Postgres process is accepting connections. */
116
+ export function dockerPgPostInitServing(logs: string): boolean {
117
+ const ready = "database system is ready to accept connections"
118
+ const lastReady = logs.lastIndexOf(ready)
119
+ if (lastReady === -1) return false
120
+
121
+ const initDone = logs.lastIndexOf("PostgreSQL init process complete")
122
+ if (initDone === -1) {
123
+ // Reused data volume — this run did not re-run entrypoint init.
124
+ return true
125
+ }
126
+ return initDone < lastReady
127
+ }
128
+
129
+ function migrateStillRunning(logs: string): boolean {
130
+ return /99-supatype-migrate\.sh: running .+\.sql/.test(logs)
131
+ }
132
+
133
+ function psqlTruthy(stdout: string | undefined): boolean {
134
+ const v = (stdout ?? "").replace(/\r/g, "").trim().toLowerCase()
135
+ return v === "t" || v === "true"
136
+ }
137
+
138
+ const ANON_ROLE_SQL = "SELECT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon')"
139
+
140
+ function dockerPgHasAnonRole(
141
+ name: string,
142
+ projectName: string,
143
+ password = DEFAULT_DEV_PASSWORD,
144
+ ): boolean {
145
+ for (const db of [projectName, "postgres"]) {
146
+ const result = dockerPgPsql(name, db, ANON_ROLE_SQL, password)
147
+ if (result.status === 0 && psqlTruthy(result.stdout)) return true
148
+ }
149
+ return false
150
+ }
151
+
152
+ function dockerPgExecReady(name: string): boolean {
153
+ const ready = spawnSync(
154
+ "docker",
155
+ ["exec", name, "pg_isready", "-U", PG_USER, "-q"],
156
+ { encoding: "utf8", stdio: "pipe" },
157
+ )
158
+ return ready.status === 0
159
+ }
160
+
75
161
  /**
76
162
  * Poll until Postgres accepts connections and the image entrypoint init has
77
163
  * finished (anon/authenticated/service_role come from supatype-postgres
@@ -79,50 +165,121 @@ export function dockerPgStop(projectName: string): void {
79
165
  */
80
166
  export async function dockerPgWaitReady(
81
167
  projectName: string,
82
- timeoutMs = 30_000,
168
+ timeoutMs = 180_000,
169
+ password = DEFAULT_DEV_PASSWORD,
83
170
  ): Promise<void> {
84
171
  const name = containerName(projectName)
85
172
  const deadline = Date.now() + timeoutMs
173
+ let servingWithoutAnonMs = 0
174
+ let lastPsqlDetail = ""
86
175
 
87
176
  while (Date.now() < deadline) {
88
- const ready = spawnSync(
89
- "docker",
90
- ["exec", name, "pg_isready", "-U", PG_USER, "-q"],
91
- { encoding: "utf8" },
92
- )
93
- if (ready.status === 0) {
94
- const initDone = spawnSync(
95
- "docker",
96
- [
97
- "exec", name,
98
- "psql", "-U", PG_USER, "-d", projectName, "-tAc",
99
- "SELECT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon')",
100
- ],
101
- { encoding: "utf8", stdio: "pipe" },
102
- )
103
- if (initDone.status === 0 && initDone.stdout?.trim() === "t") return
177
+ const health = dockerPgHealthStatus(name)
178
+ const logs = dockerPgLogsTail(name)
179
+ const serving =
180
+ health === "healthy" ||
181
+ dockerPgPostInitServing(logs) ||
182
+ dockerPgExecReady(name)
183
+
184
+ if (serving && !migrateStillRunning(logs)) {
185
+ if (dockerPgHasAnonRole(name, projectName, password)) return
186
+
187
+ const probe = dockerPgPsql(name, projectName, ANON_ROLE_SQL, password)
188
+ lastPsqlDetail = [
189
+ probe.status !== 0 ? `psql exit ${probe.status}` : "",
190
+ probe.stderr?.trim(),
191
+ probe.stdout?.trim() ? `stdout=${probe.stdout.trim()}` : "",
192
+ ]
193
+ .filter(Boolean)
194
+ .join("; ")
195
+
196
+ const reusedVolume =
197
+ logs.includes("database system is ready to accept connections") &&
198
+ !logs.includes("PostgreSQL init process complete")
199
+
200
+ if (reusedVolume) {
201
+ servingWithoutAnonMs += 500
202
+ if (servingWithoutAnonMs >= 5_000) throw staleVolumeError(name)
203
+ } else {
204
+ servingWithoutAnonMs = 0
205
+ }
206
+ } else {
207
+ servingWithoutAnonMs = 0
104
208
  }
105
- await sleep(300)
209
+
210
+ await sleep(500)
106
211
  }
107
212
 
108
- const logs = spawnSync("docker", ["logs", "--tail", "30", name], {
109
- encoding: "utf8",
110
- })
213
+ const logs = dockerPgLogsTail(name, 80)
111
214
  throw new Error(
112
215
  `Docker Postgres "${name}" did not finish image init within ${timeoutMs}ms.\n` +
113
216
  " API roles (anon, authenticated, service_role) are created by the supatype/postgres\n" +
114
217
  " entrypoint (99-supatype-migrate.sh), not by the CLI.\n" +
115
218
  " If you upgraded the image, remove the stale volume:\n" +
116
219
  ` docker volume rm ${name}-data\n` +
117
- (logs.stdout ? ` stdout:\n${indent(logs.stdout)}\n` : "") +
118
- (logs.stderr ? ` stderr:\n${indent(logs.stderr)}\n` : ""),
220
+ (lastPsqlDetail ? ` Last anon probe: ${lastPsqlDetail}\n` : "") +
221
+ (logs ? ` logs (tail):\n${indent(logs)}\n` : ""),
222
+ )
223
+ }
224
+
225
+ function staleVolumeError(name: string): Error {
226
+ return new Error(
227
+ `Docker Postgres "${name}" is up but API roles are missing.\n` +
228
+ " The data volume was initialised without supatype/postgres migrations (stale or wrong image).\n" +
229
+ " Remove the volume so first-boot 99-supatype-migrate.sh runs again:\n" +
230
+ ` docker volume rm ${name}-data`,
119
231
  )
120
232
  }
121
233
 
122
234
  /** Connection string for the Docker container (local dev credentials). */
123
- export function dockerDbUrl(projectName: string, port: number): string {
124
- // Local image has no TLS; sqlx/libpq default "prefer" can mis-handle the SSLRequest on some hosts.
125
- return `postgres://${PG_USER}:postgres@127.0.0.1:${port}/${projectName}?sslmode=disable`
235
+ export function dockerDbUrl(projectName: string, port: number, password = DEFAULT_DEV_PASSWORD): string {
236
+ // Host published port. sqlx/libpq "prefer" can mis-handle SSL on some hosts (e.g. Docker Desktop on Windows).
237
+ return `postgres://${PG_USER}:${password}@127.0.0.1:${port}/${projectName}?sslmode=disable`
238
+ }
239
+
240
+ /**
241
+ * DB URL for processes sharing the Postgres container network namespace
242
+ * (supatype-server migrate in a one-shot container). Uses loopback inside the
243
+ * container where pg_hba grants trust — avoids host-published-port SCRAM/SSL issues.
244
+ */
245
+ export function dockerPgLoopbackDbUrl(projectName: string, password = DEFAULT_DEV_PASSWORD): string {
246
+ return `postgres://${PG_USER}:${password}@127.0.0.1:5432/${projectName}?sslmode=disable`
247
+ }
248
+
249
+ /**
250
+ * Published Hub tag for local dev when CDN server version is not on Docker Hub yet.
251
+ * Keep in sync with tests/integration/scripts/compose-smoke.sh.
252
+ */
253
+ export const DEFAULT_SERVER_DOCKER_IMAGE = "supatype/server:latest"
254
+
255
+ /**
256
+ * Run `supatype-server migrate` on the Postgres container network (loopback trust).
257
+ * Used on Windows + database.provider docker — host-published :5432 breaks libpq TLS there.
258
+ */
259
+ export function runGotrueMigrationsViaDocker(
260
+ pgContainerName: string,
261
+ serverImage: string,
262
+ migrateEnv: Record<string, string>,
263
+ ): void {
264
+ const envArgs = Object.entries(migrateEnv).flatMap(([k, v]) => ["-e", `${k}=${v}`])
265
+ const result = spawnSync(
266
+ "docker",
267
+ [
268
+ "run", "--rm",
269
+ "--network", `container:${pgContainerName}`,
270
+ ...envArgs,
271
+ serverImage,
272
+ "migrate",
273
+ ],
274
+ { encoding: "utf8", stdio: "pipe" },
275
+ )
276
+ if (result.status !== 0) {
277
+ const detail = (result.stderr ?? result.stdout ?? "").trim()
278
+ throw new Error(
279
+ `GoTrue migrations failed in Docker (exit ${result.status ?? "unknown"})` +
280
+ (detail ? `:\n${detail}` : ""),
281
+ )
282
+ }
126
283
  }
127
284
 
128
285
  function sleep(ms: number): Promise<void> {
@@ -21,9 +21,12 @@ import { resolveBinary, currentPlatform, cachePath } from "./binary-cache.js"
21
21
 
22
22
  export interface Operation {
23
23
  kind: "create_table" | "alter_table" | "drop_table" | "create_index" | "drop_index" |
24
- "create_policy" | "drop_policy" | "add_column" | "drop_column" | "alter_column"
25
- description: string
26
- risk?: "safe" | "warn" | "danger"
24
+ "create_policy" | "drop_policy" | "add_column" | "drop_column" | "alter_column" |
25
+ "recreate_column"
26
+ type?: string
27
+ description?: string
28
+ risk?: "safe" | "warn" | "danger" | "cautious" | "destructive"
29
+ warning?: string
27
30
  sql?: string
28
31
  }
29
32
 
@@ -196,13 +199,15 @@ function endpointToArgs(
196
199
  const dbUrl = (body["database_url"] as string | undefined) ?? ""
197
200
  const schema = (body["schema"] as string | undefined) ?? "public"
198
201
  const force = body["force"] ? ["--force"] : []
202
+ const nonInteractive =
203
+ body["non_interactive"] === true || body["force"] === true ? ["--non-interactive"] : []
199
204
 
200
205
  switch (endpoint) {
201
206
  case "/diff":
202
207
  return ["diff", "--input", reqFile, "--database-url", dbUrl, "--schema", schema]
203
208
 
204
209
  case "/push":
205
- return ["push", "--input", reqFile, "--database-url", dbUrl, "--schema", schema, ...force]
210
+ return ["push", "--input", reqFile, "--database-url", dbUrl, "--schema", schema, ...force, ...nonInteractive]
206
211
 
207
212
  case "/parse":
208
213
  return ["parse", "--input", reqFile]
@@ -14,6 +14,10 @@ export interface KongDeclarativeOptions {
14
14
  functionsServiceUrl?: string | undefined
15
15
  /** Self-host: route API paths through supatype-server (see runtime-routes). */
16
16
  unifiedGateway?: boolean | undefined
17
+ /** Studio UI upstream (default: in-compose `studio:3002`). */
18
+ studioServiceUrl?: string | undefined
19
+ /** See {@link RuntimeRouteOptions.studioStripPath}. */
20
+ studioStripPath?: boolean | undefined
17
21
  }
18
22
 
19
23
  /** Escape a string for use inside YAML double quotes. */
@@ -35,6 +39,8 @@ export function buildKongDeclarative(opts: KongDeclarativeOptions = {}): string
35
39
  ...(opts.unifiedGateway !== true &&
36
40
  opts.staticAppServiceUrl !== undefined && { staticAppServiceUrl: opts.staticAppServiceUrl }),
37
41
  ...(opts.functionsServiceUrl !== undefined && { functionsServiceUrl: opts.functionsServiceUrl }),
42
+ ...(opts.studioServiceUrl !== undefined && { studioServiceUrl: opts.studioServiceUrl }),
43
+ ...(opts.studioStripPath === false && { studioStripPath: false }),
38
44
  })
39
45
 
40
46
  const consumersBlock = secured
@@ -60,6 +66,15 @@ consumers:
60
66
  ? ` protocols:\n${route.protocols.map((p) => ` - ${p}`).join("\n")}\n`
61
67
  : ""
62
68
  const stripPath = route.stripPath ?? false
69
+ const graphqlPlugins = route.graphqlPostgrest
70
+ ? ` plugins:
71
+ - name: request-transformer
72
+ config:
73
+ add:
74
+ headers:
75
+ - Content-Profile:graphql_public
76
+ `
77
+ : ""
63
78
  return ` - name: ${route.serviceName}
64
79
  url: ${route.serviceUrl}
65
80
  routes:
@@ -67,7 +82,7 @@ consumers:
67
82
  strip_path: ${stripPath}
68
83
  paths:
69
84
  ${route.paths.map((path) => ` - ${path}`).join("\n")}
70
- ${protocols}${routePlugins}`
85
+ ${protocols}${routePlugins}${graphqlPlugins}`
71
86
  }).join("\n")
72
87
 
73
88
  return `_format_version: "3.0"
@@ -25,6 +25,8 @@ export interface ProcessOptions {
25
25
  maxBackoffMs?: number
26
26
  /** Called when the process exits cleanly (code 0). */
27
27
  onExit?: () => void
28
+ /** Use shell to spawn (required for pnpm/npm/yarn .cmd shims on Windows). */
29
+ shell?: boolean
28
30
  }
29
31
 
30
32
  const RESET = "\x1b[0m"
@@ -47,6 +49,7 @@ export class ProcessManager {
47
49
  initialBackoffMs: 1_000,
48
50
  maxBackoffMs: 30_000,
49
51
  onExit: () => {},
52
+ shell: false,
50
53
  ...opts,
51
54
  }
52
55
  this.backoffMs = this.opts.initialBackoffMs
@@ -82,7 +85,12 @@ export class ProcessManager {
82
85
  if (this.stopped) return
83
86
 
84
87
  const env = { ...process.env, ...this.opts.env } as NodeJS.ProcessEnv
85
- this.child = spawn(this.bin, this.args, { env, cwd: this.opts.cwd, stdio: "pipe" })
88
+ this.child = spawn(this.bin, this.args, {
89
+ env,
90
+ cwd: this.opts.cwd,
91
+ stdio: "pipe",
92
+ ...(this.opts.shell ? { shell: true } : {}),
93
+ })
86
94
 
87
95
  const pid = this.child.pid
88
96
  if (pid) this.writePid(pid)
@@ -103,6 +111,15 @@ export class ProcessManager {
103
111
  }
104
112
  })
105
113
 
114
+ this.child.once("error", (err) => {
115
+ if (this.stopped) return
116
+ process.stderr.write(`${prefix}failed to start: ${err.message}\n`)
117
+ setTimeout(() => {
118
+ this.backoffMs = Math.min(this.backoffMs * 2, this.opts.maxBackoffMs)
119
+ this.spawn()
120
+ }, this.backoffMs)
121
+ })
122
+
106
123
  this.child.once("exit", (code, signal) => {
107
124
  if (this.stopped) return
108
125
 
@@ -7,6 +7,12 @@ import type { ComponentVersions } from "./components.js"
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
9
  export interface SupatypeProjectConfig {
10
+ /**
11
+ * Runtime stack for local dev and `supatype update`.
12
+ * "native" = host binaries (default). "docker" = self-host Compose stack.
13
+ * Falls back to `database.provider` when omitted (deprecated).
14
+ */
15
+ provider?: "native" | "docker"
10
16
  supatype?: {
11
17
  /**
12
18
  * Base directory for Supatype project assets (schema, functions, etc).
@@ -34,7 +40,7 @@ export interface SupatypeProjectConfig {
34
40
  data_dir?: string
35
41
  /**
36
42
  * Docker image to use (provider=docker).
37
- * Defaults to supatype/postgres:17-latest.
43
+ * Defaults to supatype/postgres:latest.
38
44
  * Override in supatype.local.config.ts for local builds.
39
45
  */
40
46
  image?: string
@@ -72,6 +78,11 @@ export interface SupatypeProjectConfig {
72
78
  * When omitted, dev still falls back to `SUPATYPE_APP_UPSTREAM` for non-proxy app modes.
73
79
  */
74
80
  vite_dev_url?: string
81
+ /**
82
+ * package.json script name for `supatype dev` to run when mode is proxy.
83
+ * Default: `"start"`. Ignored for static/none modes.
84
+ */
85
+ start?: string
75
86
  }
76
87
  /**
77
88
  * Pinned binary versions per component. Use **`"local"`** with the matching **`overrides.*`**
@@ -192,6 +203,11 @@ export interface SupatypeProjectConfig {
192
203
  * When omitted, `DATABASE_URL` from the environment is used, then a local default DSN.
193
204
  */
194
205
  connection?: string
206
+ /** Studio admin panel access (Gap Appendices task 47). */
207
+ admin?: {
208
+ /** JWT `app_metadata.role` values allowed to use Studio. Default: admin, supatype_admin */
209
+ roles?: string[]
210
+ }
195
211
  }
196
212
 
197
213
  // ---------------------------------------------------------------------------
@@ -207,6 +223,9 @@ export function mergeProjectConfig(
207
223
  override: Partial<SupatypeProjectConfig>,
208
224
  ): SupatypeProjectConfig {
209
225
  return {
226
+ ...(base.provider !== undefined || override.provider !== undefined
227
+ ? { provider: override.provider ?? base.provider }
228
+ : {}),
210
229
  ...(base.supatype !== undefined || override.supatype !== undefined
211
230
  ? { supatype: { ...base.supatype, ...override.supatype } as NonNullable<SupatypeProjectConfig["supatype"]> }
212
231
  : {}),
@@ -263,6 +282,9 @@ export function mergeProjectConfig(
263
282
  ...(base.connection !== undefined || override.connection !== undefined
264
283
  ? { connection: override.connection ?? base.connection }
265
284
  : {}),
285
+ ...(base.admin !== undefined || override.admin !== undefined
286
+ ? { admin: { ...base.admin, ...override.admin } as NonNullable<SupatypeProjectConfig["admin"]> }
287
+ : {}),
266
288
  }
267
289
  }
268
290
 
@@ -330,6 +352,9 @@ export function serverBaseUrl(cfg: SupatypeProjectConfig): string | undefined {
330
352
  switch (cfg.server.mode) {
331
353
  case "dev":
332
354
  case "standalone":
355
+ if (cfg.server.mode === "dev" && resolveRuntimeProvider(cfg) === "docker") {
356
+ return `http://localhost:${COMPOSE_DEV_KONG_PORT}`
357
+ }
333
358
  return cfg.server.domain
334
359
  ? `https://${cfg.server.domain}`
335
360
  : `http://localhost:${port}`
@@ -338,6 +363,14 @@ export function serverBaseUrl(cfg: SupatypeProjectConfig): string | undefined {
338
363
  }
339
364
  }
340
365
 
366
+ /** Resolved runtime provider (`config.provider` ?? `database.provider` ?? native). */
367
+ export function resolveRuntimeProvider(cfg: SupatypeProjectConfig): "native" | "docker" {
368
+ return cfg.provider ?? cfg.database.provider ?? "native"
369
+ }
370
+
371
+ /** Kong gateway port when `provider: docker` (self-host compose dev). */
372
+ export const COMPOSE_DEV_KONG_PORT = 18473
373
+
341
374
  /** The local Postgres DSN derived from project name (dev default). */
342
375
  export function localDSN(cfg: SupatypeProjectConfig): string {
343
376
  const port = 5432 // standard; per-project state dir isolates data dirs
@@ -6,6 +6,8 @@ export interface RuntimeRoute {
6
6
  stripPath?: boolean
7
7
  protocols?: string[]
8
8
  engineProtected?: boolean
9
+ /** Route /graphql/v1 to PostgREST /rpc/graphql with graphql_public Content-Profile. */
10
+ graphqlPostgrest?: boolean
9
11
  }
10
12
 
11
13
  export interface RuntimeRouteOptions {
@@ -18,6 +20,24 @@ export interface RuntimeRouteOptions {
18
20
  * which proxies to internal services — same model as `supatype dev`.
19
21
  */
20
22
  unifiedGateway?: boolean
23
+ /** Studio UI upstream (default: in-compose `studio:3002`). */
24
+ studioServiceUrl?: string
25
+ /**
26
+ * Strip `/studio/` before proxying to the Studio upstream.
27
+ * False for host Vite dev (`host.docker.internal`) where `base` is `/studio/`.
28
+ */
29
+ studioStripPath?: boolean
30
+ }
31
+
32
+ const DEFAULT_STUDIO_SERVICE_URL = "http://studio:3002"
33
+
34
+ function studioServiceUrl(opts: RuntimeRouteOptions): string {
35
+ const url = opts.studioServiceUrl?.trim()
36
+ return url && url.length > 0 ? url : DEFAULT_STUDIO_SERVICE_URL
37
+ }
38
+
39
+ function studioStripPath(opts: RuntimeRouteOptions): boolean {
40
+ return opts.studioStripPath !== false
21
41
  }
22
42
 
23
43
  const SERVER_GATEWAY = "http://server:9999"
@@ -25,7 +45,9 @@ const SERVER_GATEWAY = "http://server:9999"
25
45
  /**
26
46
  * Kong routes when self-host uses supatype-server as the single API gateway.
27
47
  */
28
- function runtimeRouteSpecUnified(): RuntimeRoute[] {
48
+ function runtimeRouteSpecUnified(opts: RuntimeRouteOptions): RuntimeRoute[] {
49
+ const studioUrl = studioServiceUrl(opts)
50
+ const stripStudio = studioStripPath(opts)
29
51
  return [
30
52
  {
31
53
  name: "rest-v1",
@@ -61,7 +83,7 @@ function runtimeRouteSpecUnified(): RuntimeRoute[] {
61
83
  serviceUrl: SERVER_GATEWAY,
62
84
  paths: ["/realtime/v1/"],
63
85
  stripPath: false,
64
- protocols: ["http", "https", "ws", "wss"],
86
+ protocols: ["http", "https"],
65
87
  },
66
88
  {
67
89
  name: "functions-v1",
@@ -72,10 +94,11 @@ function runtimeRouteSpecUnified(): RuntimeRoute[] {
72
94
  },
73
95
  {
74
96
  name: "graphql-v1",
75
- serviceName: "supatype-server-graphql",
76
- serviceUrl: SERVER_GATEWAY,
77
- paths: ["/graphql/v1/"],
78
- stripPath: false,
97
+ serviceName: "postgrest-graphql",
98
+ serviceUrl: "http://postgrest:3000/rpc/graphql",
99
+ paths: ["/graphql/v1"],
100
+ stripPath: true,
101
+ graphqlPostgrest: true,
79
102
  },
80
103
  {
81
104
  name: "studio-config-route",
@@ -91,12 +114,33 @@ function runtimeRouteSpecUnified(): RuntimeRoute[] {
91
114
  paths: ["/sql"],
92
115
  stripPath: false,
93
116
  },
117
+ {
118
+ name: "studio-auth",
119
+ serviceName: "supatype-server-studio-auth",
120
+ serviceUrl: SERVER_GATEWAY,
121
+ paths: ["/studio/auth/"],
122
+ stripPath: false,
123
+ },
124
+ {
125
+ name: "studio-proxy",
126
+ serviceName: "supatype-server-studio-proxy",
127
+ serviceUrl: SERVER_GATEWAY,
128
+ paths: ["/studio/proxy/"],
129
+ stripPath: false,
130
+ },
131
+ {
132
+ name: "studio-exact",
133
+ serviceName: "studio-exact",
134
+ serviceUrl: studioUrl,
135
+ paths: ["~/studio$"],
136
+ stripPath: stripStudio,
137
+ },
94
138
  {
95
139
  name: "studio",
96
140
  serviceName: "studio",
97
- serviceUrl: "http://studio:3002",
141
+ serviceUrl: studioUrl,
98
142
  paths: ["/studio/"],
99
- stripPath: true,
143
+ stripPath: stripStudio,
100
144
  },
101
145
  {
102
146
  name: "app-root",
@@ -113,6 +157,8 @@ function runtimeRouteSpecUnified(): RuntimeRoute[] {
113
157
  * Kept for tests or explicit opt-out only — self-host uses unifiedGateway.
114
158
  */
115
159
  function runtimeRouteSpecSplit(opts: RuntimeRouteOptions): RuntimeRoute[] {
160
+ const studioUrl = studioServiceUrl(opts)
161
+ const stripStudio = studioStripPath(opts)
116
162
  const routes: RuntimeRoute[] = [
117
163
  {
118
164
  name: "rest-v1",
@@ -148,7 +194,7 @@ function runtimeRouteSpecSplit(opts: RuntimeRouteOptions): RuntimeRoute[] {
148
194
  serviceUrl: "http://realtime:4000",
149
195
  paths: ["/realtime/v1/"],
150
196
  stripPath: true,
151
- protocols: ["http", "https", "ws", "wss"],
197
+ protocols: ["http", "https"],
152
198
  },
153
199
  {
154
200
  name: "functions-v1",
@@ -157,12 +203,41 @@ function runtimeRouteSpecSplit(opts: RuntimeRouteOptions): RuntimeRoute[] {
157
203
  paths: ["/functions/v1/"],
158
204
  stripPath: false,
159
205
  },
206
+ {
207
+ name: "graphql-v1",
208
+ serviceName: "postgrest-graphql",
209
+ serviceUrl: "http://postgrest:3000/rpc/graphql",
210
+ paths: ["/graphql/v1"],
211
+ stripPath: true,
212
+ graphqlPostgrest: true,
213
+ },
214
+ {
215
+ name: "studio-auth",
216
+ serviceName: "auth-v1",
217
+ serviceUrl: "http://server:9999",
218
+ paths: ["/studio/auth/"],
219
+ stripPath: false,
220
+ },
221
+ {
222
+ name: "studio-proxy",
223
+ serviceName: "auth-v1",
224
+ serviceUrl: "http://server:9999",
225
+ paths: ["/studio/proxy/"],
226
+ stripPath: false,
227
+ },
228
+ {
229
+ name: "studio-exact",
230
+ serviceName: "studio-exact",
231
+ serviceUrl: studioUrl,
232
+ paths: ["~/studio$"],
233
+ stripPath: stripStudio,
234
+ },
160
235
  {
161
236
  name: "studio",
162
237
  serviceName: "studio",
163
- serviceUrl: "http://studio:3002",
238
+ serviceUrl: studioUrl,
164
239
  paths: ["/studio/"],
165
- stripPath: true,
240
+ stripPath: stripStudio,
166
241
  },
167
242
  {
168
243
  name: "studio-config-route",
@@ -210,7 +285,7 @@ function runtimeRouteSpecSplit(opts: RuntimeRouteOptions): RuntimeRoute[] {
210
285
  */
211
286
  export function runtimeRouteSpec(opts: RuntimeRouteOptions = {}): RuntimeRoute[] {
212
287
  if (opts.unifiedGateway) {
213
- return runtimeRouteSpecUnified()
288
+ return runtimeRouteSpecUnified(opts)
214
289
  }
215
290
  return runtimeRouteSpecSplit(opts)
216
291
  }