@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +66 -61
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app/proxy-dev-app.d.ts +13 -0
- package/dist/app/proxy-dev-app.d.ts.map +1 -0
- package/dist/app/proxy-dev-app.js +53 -0
- package/dist/app/proxy-dev-app.js.map +1 -0
- package/dist/binary-cache.d.ts +5 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +13 -0
- package/dist/binary-cache.js.map +1 -1
- package/dist/commands/cloud.d.ts +11 -3
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +33 -25
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -17
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -3
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +66 -59
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +11 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/init.js +16 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +42 -12
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +16 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/dev-compose.d.ts +17 -0
- package/dist/dev-compose.d.ts.map +1 -0
- package/dist/dev-compose.js +374 -0
- package/dist/dev-compose.js.map +1 -0
- package/dist/diff-output.d.ts +4 -0
- package/dist/diff-output.d.ts.map +1 -0
- package/dist/diff-output.js +12 -0
- package/dist/diff-output.js.map +1 -0
- package/dist/docker-postgres.d.ts +21 -3
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +130 -18
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-client.d.ts +5 -3
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +2 -1
- package/dist/engine-client.js.map +1 -1
- package/dist/kong-config.d.ts +4 -0
- package/dist/kong-config.d.ts.map +1 -1
- package/dist/kong-config.js +12 -1
- package/dist/kong-config.js.map +1 -1
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +16 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/project-config.d.ts +21 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +15 -0
- package/dist/project-config.js.map +1 -1
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +75 -12
- package/dist/runtime-routes.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +127 -0
- package/dist/schema-ast-v2.d.ts.map +1 -0
- package/dist/schema-ast-v2.js +226 -0
- package/dist/schema-ast-v2.js.map +1 -0
- package/dist/seed.d.ts +8 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +32 -0
- package/dist/seed.js.map +1 -0
- package/dist/self-host-compose.d.ts +12 -4
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +146 -35
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +7 -0
- package/dist/studio-admin-roles.d.ts.map +1 -0
- package/dist/studio-admin-roles.js +14 -0
- package/dist/studio-admin-roles.js.map +1 -0
- package/dist/studio-dev-server.d.ts +22 -0
- package/dist/studio-dev-server.d.ts.map +1 -0
- package/dist/studio-dev-server.js +28 -0
- package/dist/studio-dev-server.js.map +1 -0
- package/dist/type-extractor.d.ts +3 -30
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +485 -148
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-resolver.d.ts +33 -0
- package/dist/type-resolver.d.ts.map +1 -0
- package/dist/type-resolver.js +338 -0
- package/dist/type-resolver.js.map +1 -0
- package/package.json +7 -3
- package/src/TYPE-RESOLUTION.md +294 -0
- package/src/app/proxy-dev-app.ts +67 -0
- package/src/binary-cache.ts +20 -0
- package/src/commands/cloud.ts +40 -30
- package/src/commands/deploy.ts +3 -18
- package/src/commands/dev.ts +72 -69
- package/src/commands/diff.ts +11 -1
- package/src/commands/init.ts +16 -3
- package/src/commands/push.ts +49 -13
- package/src/commands/update.ts +17 -0
- package/src/dev-compose.ts +455 -0
- package/src/diff-output.ts +12 -0
- package/src/docker-postgres.ts +184 -27
- package/src/engine-client.ts +9 -4
- package/src/kong-config.ts +16 -1
- package/src/process-manager.ts +18 -1
- package/src/project-config.ts +34 -1
- package/src/runtime-routes.ts +87 -12
- package/src/schema-ast-v2.ts +324 -0
- package/src/seed.ts +43 -0
- package/src/self-host-compose.ts +168 -36
- package/src/studio-admin-roles.ts +16 -0
- package/src/studio-dev-server.ts +53 -0
- package/src/type-extractor.ts +649 -186
- package/src/type-resolver.ts +457 -0
- package/tests/config.test.ts +34 -3
- package/tests/docker-postgres.test.ts +39 -0
- package/tests/normalize-admin-config.test.ts +48 -0
- package/tests/proxy-dev-app.test.ts +33 -0
- package/tests/runtime-contract.test.ts +119 -4
- package/tests/studio-admin-roles.test.ts +27 -0
- package/tests/type-extractor.test.ts +607 -23
- package/tests/type-resolver.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/docker-postgres.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
209
|
+
|
|
210
|
+
await sleep(500)
|
|
106
211
|
}
|
|
107
212
|
|
|
108
|
-
const logs =
|
|
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
|
-
(
|
|
118
|
-
(logs
|
|
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
|
-
//
|
|
125
|
-
return `postgres://${PG_USER}
|
|
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> {
|
package/src/engine-client.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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]
|
package/src/kong-config.ts
CHANGED
|
@@ -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"
|
package/src/process-manager.ts
CHANGED
|
@@ -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, {
|
|
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
|
|
package/src/project-config.ts
CHANGED
|
@@ -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:
|
|
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
|
package/src/runtime-routes.ts
CHANGED
|
@@ -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"
|
|
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: "
|
|
76
|
-
serviceUrl:
|
|
77
|
-
paths: ["/graphql/v1
|
|
78
|
-
stripPath:
|
|
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:
|
|
141
|
+
serviceUrl: studioUrl,
|
|
98
142
|
paths: ["/studio/"],
|
|
99
|
-
stripPath:
|
|
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"
|
|
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:
|
|
238
|
+
serviceUrl: studioUrl,
|
|
164
239
|
paths: ["/studio/"],
|
|
165
|
-
stripPath:
|
|
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
|
}
|