@supatype/cli 0.1.0-alpha.6 → 0.1.0-alpha.7
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 +203 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app-config.d.ts +7 -0
- package/dist/app-config.d.ts.map +1 -0
- package/dist/app-config.js +113 -0
- package/dist/app-config.js.map +1 -0
- package/dist/augmentation-generator.d.ts +2 -0
- package/dist/augmentation-generator.d.ts.map +1 -0
- package/dist/augmentation-generator.js +111 -0
- package/dist/augmentation-generator.js.map +1 -0
- package/dist/binary-cache.d.ts +89 -0
- package/dist/binary-cache.d.ts.map +1 -0
- package/dist/binary-cache.js +656 -0
- package/dist/binary-cache.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +4 -3
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +56 -209
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/cache.d.ts +6 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +105 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/cloud.d.ts +12 -0
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +36 -46
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +47 -54
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.d.ts +2 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +92 -51
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +11 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +751 -384
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +20 -15
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/engine.d.ts +1 -3
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +13 -85
- package/dist/commands/engine.js.map +1 -1
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +92 -105
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +22 -12
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +124 -410
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate-from-v1.d.ts +5 -0
- package/dist/commands/migrate-from-v1.d.ts.map +1 -0
- package/dist/commands/migrate-from-v1.js +125 -0
- package/dist/commands/migrate-from-v1.js.map +1 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +27 -23
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/pg.d.ts +8 -0
- package/dist/commands/pg.d.ts.map +1 -0
- package/dist/commands/pg.js +102 -0
- package/dist/commands/pg.js.map +1 -0
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +5 -66
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +99 -39
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +44 -11
- package/dist/commands/seed.js.map +1 -1
- package/dist/commands/self-host.d.ts +7 -1
- package/dist/commands/self-host.d.ts.map +1 -1
- package/dist/commands/self-host.js +272 -758
- package/dist/commands/self-host.js.map +1 -1
- package/dist/commands/self-update.d.ts +9 -0
- package/dist/commands/self-update.d.ts.map +1 -0
- package/dist/commands/self-update.js +33 -0
- package/dist/commands/self-update.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +4 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/types.d.ts +3 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +62 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +77 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/components.d.ts +5 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +3 -0
- package/dist/components.js.map +1 -0
- package/dist/config.d.ts +10 -51
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +101 -33
- package/dist/config.js.map +1 -1
- package/dist/docker-postgres.d.ts +39 -0
- package/dist/docker-postgres.d.ts.map +1 -0
- package/dist/docker-postgres.js +96 -0
- package/dist/docker-postgres.js.map +1 -0
- package/dist/engine-client.d.ts +67 -0
- package/dist/engine-client.d.ts.map +1 -0
- package/dist/engine-client.js +156 -0
- package/dist/engine-client.js.map +1 -0
- package/dist/ensure-binary.d.ts +7 -0
- package/dist/ensure-binary.d.ts.map +1 -0
- package/dist/ensure-binary.js +17 -0
- package/dist/ensure-binary.js.map +1 -0
- package/dist/functions-router-gen.d.ts +14 -0
- package/dist/functions-router-gen.d.ts.map +1 -0
- package/dist/functions-router-gen.js +199 -0
- package/dist/functions-router-gen.js.map +1 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/kong-config.d.ts +21 -0
- package/dist/kong-config.d.ts.map +1 -0
- package/dist/kong-config.js +60 -0
- package/dist/kong-config.js.map +1 -0
- package/dist/local-gateway.d.ts +7 -0
- package/dist/local-gateway.d.ts.map +1 -0
- package/dist/local-gateway.js +9 -0
- package/dist/local-gateway.js.map +1 -0
- package/dist/local-storage.d.ts +8 -0
- package/dist/local-storage.d.ts.map +1 -0
- package/dist/local-storage.js +14 -0
- package/dist/local-storage.js.map +1 -0
- package/dist/pgbouncer-userlist.d.ts +5 -0
- package/dist/pgbouncer-userlist.d.ts.map +1 -0
- package/dist/pgbouncer-userlist.js +14 -0
- package/dist/pgbouncer-userlist.js.map +1 -0
- package/dist/postgres-ctl.d.ts +44 -0
- package/dist/postgres-ctl.d.ts.map +1 -0
- package/dist/postgres-ctl.js +137 -0
- package/dist/postgres-ctl.js.map +1 -0
- package/dist/process-manager.d.ts +41 -0
- package/dist/process-manager.d.ts.map +1 -0
- package/dist/process-manager.js +120 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/project-config.d.ts +215 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +145 -0
- package/dist/project-config.js.map +1 -0
- package/dist/pull-utils.d.ts +15 -0
- package/dist/pull-utils.d.ts.map +1 -1
- package/dist/pull-utils.js +12 -0
- package/dist/pull-utils.js.map +1 -1
- package/dist/release-pins.d.ts +7 -0
- package/dist/release-pins.d.ts.map +1 -0
- package/dist/release-pins.js +27 -0
- package/dist/release-pins.js.map +1 -0
- package/dist/release-public-key.d.ts +8 -0
- package/dist/release-public-key.d.ts.map +1 -0
- package/dist/release-public-key.js +13 -0
- package/dist/release-public-key.js.map +1 -0
- package/dist/runtime-routes.d.ts +25 -0
- package/dist/runtime-routes.d.ts.map +1 -0
- package/dist/runtime-routes.js +189 -0
- package/dist/runtime-routes.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +5 -6
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +36 -20
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/self-host-compose.d.ts +14 -0
- package/dist/self-host-compose.d.ts.map +1 -0
- package/dist/self-host-compose.js +236 -0
- package/dist/self-host-compose.js.map +1 -0
- package/dist/storage-provision.d.ts +24 -0
- package/dist/storage-provision.d.ts.map +1 -0
- package/dist/storage-provision.js +44 -0
- package/dist/storage-provision.js.map +1 -0
- package/dist/systemd.d.ts +26 -0
- package/dist/systemd.d.ts.map +1 -0
- package/dist/systemd.js +102 -0
- package/dist/systemd.js.map +1 -0
- package/dist/tsx-runner.d.ts.map +1 -1
- package/dist/tsx-runner.js +9 -2
- package/dist/tsx-runner.js.map +1 -1
- package/dist/type-extractor.d.ts +31 -0
- package/dist/type-extractor.d.ts.map +1 -0
- package/dist/type-extractor.js +876 -0
- package/dist/type-extractor.js.map +1 -0
- package/package.json +4 -3
- package/releases/deno/VERSION +1 -0
- package/scripts/mirror-deno-release.sh +76 -0
- package/src/app-config.ts +128 -0
- package/src/augmentation-generator.ts +126 -0
- package/src/binary-cache.ts +802 -0
- package/src/cli.ts +13 -8
- package/src/commands/admin.ts +4 -3
- package/src/commands/app.ts +67 -231
- package/src/commands/cache.ts +117 -0
- package/src/commands/cloud.ts +46 -57
- package/src/commands/db.ts +54 -63
- package/src/commands/deploy.ts +110 -61
- package/src/commands/dev.ts +930 -405
- package/src/commands/diff.ts +21 -29
- package/src/commands/engine.ts +13 -116
- package/src/commands/functions.ts +97 -115
- package/src/commands/generate.ts +23 -10
- package/src/commands/init.ts +136 -414
- package/src/commands/migrate-from-v1.ts +131 -0
- package/src/commands/migrate.ts +27 -23
- package/src/commands/pg.ts +133 -0
- package/src/commands/pull.ts +6 -85
- package/src/commands/push.ts +128 -59
- package/src/commands/seed.ts +54 -12
- package/src/commands/self-host.ts +312 -880
- package/src/commands/self-update.ts +45 -0
- package/src/commands/status.ts +4 -3
- package/src/commands/types.ts +76 -0
- package/src/commands/update.ts +92 -0
- package/src/components.ts +6 -0
- package/src/config.ts +127 -94
- package/src/docker-postgres.ts +138 -0
- package/src/engine-client.ts +231 -0
- package/src/ensure-binary.ts +28 -0
- package/src/functions-router-gen.ts +224 -0
- package/src/index.ts +4 -12
- package/src/kong-config.ts +78 -0
- package/src/local-gateway.ts +9 -0
- package/src/local-storage.ts +14 -0
- package/src/pgbouncer-userlist.ts +15 -0
- package/src/postgres-ctl.ts +171 -0
- package/src/process-manager.ts +151 -0
- package/src/project-config.ts +353 -0
- package/src/pull-utils.ts +24 -0
- package/src/release-pins.ts +31 -0
- package/src/release-public-key.ts +12 -0
- package/src/runtime-routes.ts +216 -0
- package/src/scripts/postinstall.ts +36 -25
- package/src/self-host-compose.ts +257 -0
- package/src/storage-provision.ts +58 -0
- package/src/systemd.ts +137 -0
- package/src/tsx-runner.ts +11 -1
- package/src/type-extractor.ts +1016 -0
- package/tests/app-command.test.ts +54 -0
- package/tests/augmentation-generator.test.ts +59 -0
- package/tests/binary-cache-cloud-overrides.test.ts +123 -0
- package/tests/cached-artifact-format.test.ts +84 -0
- package/tests/cli-help.test.ts +40 -14
- package/tests/config.test.ts +140 -37
- package/tests/engine-distribution.test.ts +3 -3
- package/tests/ensure-binary.test.ts +59 -0
- package/tests/init.test.ts +28 -86
- package/tests/migrate-from-v1.test.ts +29 -0
- package/tests/pg-spawn-env.test.ts +18 -0
- package/tests/postgres-archive-tag.test.ts +9 -0
- package/tests/pull-utils.test.ts +36 -1
- package/tests/release-pins.test.ts +28 -0
- package/tests/runtime-contract.test.ts +236 -0
- package/tests/seed-discover.test.ts +31 -0
- package/tests/tsconfig.json +9 -0
- package/tests/type-extractor.test.ts +401 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +12 -0
- package/dist/engine/cache.d.ts +0 -37
- package/dist/engine/cache.d.ts.map +0 -1
- package/dist/engine/cache.js +0 -121
- package/dist/engine/cache.js.map +0 -1
- package/dist/engine/download.d.ts +0 -19
- package/dist/engine/download.d.ts.map +0 -1
- package/dist/engine/download.js +0 -108
- package/dist/engine/download.js.map +0 -1
- package/dist/engine/platform.d.ts +0 -24
- package/dist/engine/platform.d.ts.map +0 -1
- package/dist/engine/platform.js +0 -50
- package/dist/engine/platform.js.map +0 -1
- package/dist/engine/resolve.d.ts +0 -37
- package/dist/engine/resolve.d.ts.map +0 -1
- package/dist/engine/resolve.js +0 -133
- package/dist/engine/resolve.js.map +0 -1
- package/dist/engine/update-notify.d.ts +0 -11
- package/dist/engine/update-notify.d.ts.map +0 -1
- package/dist/engine/update-notify.js +0 -43
- package/dist/engine/update-notify.js.map +0 -1
- package/dist/engine/verify.d.ts +0 -50
- package/dist/engine/verify.d.ts.map +0 -1
- package/dist/engine/verify.js +0 -161
- package/dist/engine/verify.js.map +0 -1
- package/dist/engine-version.d.ts +0 -35
- package/dist/engine-version.d.ts.map +0 -1
- package/dist/engine-version.js +0 -35
- package/dist/engine-version.js.map +0 -1
- package/dist/engine.d.ts +0 -34
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -76
- package/dist/engine.js.map +0 -1
- package/src/engine/cache.ts +0 -135
- package/src/engine/download.ts +0 -143
- package/src/engine/platform.ts +0 -66
- package/src/engine/resolve.ts +0 -197
- package/src/engine/update-notify.ts +0 -50
- package/src/engine/verify.ts +0 -206
- package/src/engine-version.ts +0 -39
- package/src/engine.ts +0 -99
package/src/commands/dev.ts
CHANGED
|
@@ -1,477 +1,1002 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supatype dev — start local Postgres, apply schema, run supatype-server.
|
|
3
|
+
*
|
|
4
|
+
* Supports two database providers (set in supatype.config.ts):
|
|
5
|
+
* provider = "native" — manages a native Postgres binary from the supatype cache (default when omitted)
|
|
6
|
+
* provider = "docker" — runs supatype/postgres via Docker (includes all extensions)
|
|
7
|
+
*
|
|
8
|
+
* Edge functions (when a functions/ dir exists): Deno is resolved from the CDN cache
|
|
9
|
+
* (auto-download on miss). Self-host/cloud Docker stacks use supatype-server in-container;
|
|
10
|
+
* Deno is not provisioned by the CLI on those paths.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import type { Command } from "commander"
|
|
2
|
-
import { spawnSync
|
|
14
|
+
import { spawnSync } from "node:child_process"
|
|
3
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
4
|
-
import {
|
|
16
|
+
import { homedir } from "node:os"
|
|
17
|
+
import { isAbsolute, join, relative, resolve } from "node:path"
|
|
5
18
|
import { loadConfig } from "../config.js"
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
import {
|
|
20
|
+
functionsPathCandidatesFromProject,
|
|
21
|
+
schemaPathFromProject,
|
|
22
|
+
type SupatypeProjectConfig,
|
|
23
|
+
} from "../project-config.js"
|
|
24
|
+
import { discoverTsFunctionsInDir, writeDevFunctionsRouter } from "../functions-router-gen.js"
|
|
25
|
+
import { signJwt } from "../jwt.js"
|
|
26
|
+
import {
|
|
27
|
+
normalisePlatformPath,
|
|
28
|
+
cachePath,
|
|
29
|
+
currentPlatform,
|
|
30
|
+
hasMeaningfulOverrides,
|
|
31
|
+
describeActiveOverrides,
|
|
32
|
+
postgresArchiveTag,
|
|
33
|
+
} from "../binary-cache.js"
|
|
34
|
+
import { ensureBinary } from "../ensure-binary.js"
|
|
35
|
+
import { ProcessManager } from "../process-manager.js"
|
|
36
|
+
import { localStorageEnv } from "../local-storage.js"
|
|
37
|
+
import {
|
|
38
|
+
initdb,
|
|
39
|
+
start as pgStart,
|
|
40
|
+
stop as pgStop,
|
|
41
|
+
waitReady as pgWaitReady,
|
|
42
|
+
isPortInUse,
|
|
43
|
+
pgSpawnEnv,
|
|
44
|
+
} from "../postgres-ctl.js"
|
|
45
|
+
import {
|
|
46
|
+
dockerPgStart,
|
|
47
|
+
dockerPgStop,
|
|
48
|
+
dockerPgWaitReady,
|
|
49
|
+
dockerDbUrl,
|
|
50
|
+
} from "../docker-postgres.js"
|
|
51
|
+
|
|
52
|
+
const DEFAULT_DOCKER_IMAGE = "supatype/postgres:17-latest"
|
|
53
|
+
|
|
54
|
+
/** Map `email.smtp` from supatype.config.ts into GOTRUE_SMTP_* for the embedded GoTrue process. */
|
|
55
|
+
function gotrueSMTPFromEmailConfig(email: SupatypeProjectConfig["email"] | undefined): Record<string, string> {
|
|
56
|
+
const s = email?.smtp
|
|
57
|
+
if (!s) return {}
|
|
58
|
+
const out: Record<string, string> = {}
|
|
59
|
+
const host = s.host?.trim()
|
|
60
|
+
if (host) out.GOTRUE_SMTP_HOST = host
|
|
61
|
+
if (s.port !== undefined) out.GOTRUE_SMTP_PORT = String(s.port)
|
|
62
|
+
const user = s.user?.trim()
|
|
63
|
+
if (user) out.GOTRUE_SMTP_USER = user
|
|
64
|
+
if (s.pass !== undefined && s.pass !== "") out.GOTRUE_SMTP_PASS = s.pass
|
|
65
|
+
const admin = s.admin_email?.trim()
|
|
66
|
+
if (admin) out.GOTRUE_SMTP_ADMIN_EMAIL = admin
|
|
67
|
+
const sender = s.sender_name?.trim()
|
|
68
|
+
if (sender) out.GOTRUE_SMTP_SENDER_NAME = sender
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
11
71
|
|
|
12
72
|
export function registerDev(program: Command): void {
|
|
13
73
|
program
|
|
14
74
|
.command("dev")
|
|
15
|
-
.description(
|
|
16
|
-
"Start local Postgres, PostgREST, and Kong via Docker Compose, then watch for schema changes",
|
|
17
|
-
)
|
|
75
|
+
.description("Start local Postgres, apply schema, and run supatype-server")
|
|
18
76
|
.option("--no-watch", "Start services but do not watch for schema changes")
|
|
19
|
-
.option("--
|
|
20
|
-
.action(async (opts: { watch: boolean;
|
|
77
|
+
.option("--port <port>", "Port for supatype-server (overrides config)", String)
|
|
78
|
+
.action(async (opts: { watch: boolean; port?: string }) => {
|
|
21
79
|
const cwd = process.cwd()
|
|
22
80
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!existsSync(composePath)) {
|
|
30
|
-
ensureInfraCompose(cwd)
|
|
81
|
+
// ── 1. Load project config ─────────────────────────────────────────────
|
|
82
|
+
const config = loadConfig(cwd)
|
|
83
|
+
if (hasMeaningfulOverrides(config)) {
|
|
84
|
+
console.warn("[supatype] Local binary overrides active:")
|
|
85
|
+
for (const line of describeActiveOverrides(config)) {
|
|
86
|
+
console.warn(line)
|
|
31
87
|
}
|
|
32
|
-
console.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
88
|
+
console.warn("")
|
|
89
|
+
}
|
|
90
|
+
const projectName = config.project.name
|
|
91
|
+
const serverPort = opts.port ?? String(config.server.port ?? 54321)
|
|
92
|
+
const postgrestPort = String(config.server.postgrestPort ?? 3001)
|
|
93
|
+
const provider = config.database.provider ?? "native"
|
|
94
|
+
|
|
95
|
+
// ── 2. Resolve engine + server binaries ──────────────────────────────
|
|
96
|
+
console.log(`[supatype] Resolving component binaries for "${projectName}"...`)
|
|
97
|
+
const [engineBin, serverBin] = await Promise.all([
|
|
98
|
+
ensureBinary("engine", config),
|
|
99
|
+
ensureBinary("server", config),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
// ── 3. Per-project state directories ─────────────────────────────────
|
|
103
|
+
const stateRoot = join(homedir(), ".supatype", "projects", projectName)
|
|
104
|
+
const pidDir = join(stateRoot, "pid")
|
|
105
|
+
const logsDir = join(stateRoot, "logs")
|
|
106
|
+
const tmpDir = join(stateRoot, "tmp")
|
|
107
|
+
|
|
108
|
+
for (const d of [pidDir, logsDir, tmpDir]) {
|
|
109
|
+
mkdirSync(d, { recursive: true })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── 4. Port collision check ───────────────────────────────────────────
|
|
113
|
+
const pgPort = 5432
|
|
114
|
+
if (await isPortInUse(pgPort)) {
|
|
115
|
+
console.error(
|
|
116
|
+
`[supatype] Port ${pgPort} is already in use. Another Postgres instance may be running.\n` +
|
|
117
|
+
` Check: lsof -i :${pgPort}`,
|
|
37
118
|
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
if (await isPortInUse(Number(serverPort))) {
|
|
122
|
+
console.error(
|
|
123
|
+
`[supatype] Port ${serverPort} is already in use. Another supatype-server may be running.\n` +
|
|
124
|
+
` Check: lsof -i :${serverPort}`,
|
|
125
|
+
)
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
if (await isPortInUse(Number(postgrestPort))) {
|
|
129
|
+
console.error(
|
|
130
|
+
`[supatype] Port ${postgrestPort} is already in use. Another service may be running.\n` +
|
|
131
|
+
` Check: lsof -i :${postgrestPort}`,
|
|
132
|
+
)
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── 5–7. Start Postgres ───────────────────────────────────────────────
|
|
137
|
+
let dbURL: string
|
|
138
|
+
let stopPostgres: () => void | Promise<void>
|
|
139
|
+
// pgBinDir is set on the native path and used to add DLL search path for
|
|
140
|
+
// PostgREST on Windows (PostgREST links against libpq + SSL from MinGW).
|
|
141
|
+
let pgBinDir: string | null = null
|
|
142
|
+
|
|
143
|
+
if (provider === "docker") {
|
|
144
|
+
console.log(
|
|
145
|
+
"[supatype] database.provider \"docker\" — Postgres runs in Docker; engine and supatype-server stay native.",
|
|
146
|
+
)
|
|
147
|
+
const image = config.database.image ?? DEFAULT_DOCKER_IMAGE
|
|
148
|
+
console.log(`[supatype] Starting Postgres via Docker (${image})...`)
|
|
149
|
+
dockerPgStart({ image, projectName, port: pgPort })
|
|
150
|
+
await dockerPgWaitReady(projectName, 90_000)
|
|
151
|
+
console.log("[supatype] Postgres is ready.")
|
|
152
|
+
dbURL = dockerDbUrl(projectName, pgPort)
|
|
153
|
+
stopPostgres = () => dockerPgStop(projectName)
|
|
42
154
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.log("
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
155
|
+
// native — resolve pg bin dir and manage with pg_ctl
|
|
156
|
+
pgBinDir = await resolvePgBinDir(config)
|
|
157
|
+
const dataDir = config.database.data_dir ?? join(stateRoot, "data")
|
|
158
|
+
mkdirSync(dataDir, { recursive: true })
|
|
159
|
+
const pgOpts = { pgBinDir, dataDir, port: pgPort, logPath: join(logsDir, "postgres.log") }
|
|
160
|
+
|
|
161
|
+
console.log("[supatype] Initialising Postgres data directory...")
|
|
162
|
+
initdb(pgOpts)
|
|
163
|
+
console.log("[supatype] Starting Postgres...")
|
|
164
|
+
pgStart(pgOpts)
|
|
165
|
+
await pgWaitReady(pgOpts, 15_000)
|
|
166
|
+
console.log("[supatype] Postgres is ready.")
|
|
167
|
+
dbURL = `postgres://postgres:postgres@127.0.0.1:${pgPort}/${projectName}?sslmode=disable`
|
|
168
|
+
stopPostgres = () => pgStop(pgOpts)
|
|
169
|
+
|
|
170
|
+
// Create project database if it doesn't exist.
|
|
171
|
+
const psqlBin = join(pgBinDir, process.platform === "win32" ? "psql.exe" : "psql")
|
|
172
|
+
const createdbBin = join(pgBinDir, process.platform === "win32" ? "createdb.exe" : "createdb")
|
|
173
|
+
const pgConnArgs = ["-h", "127.0.0.1", "-p", String(pgPort), "-U", "postgres"]
|
|
174
|
+
const pgEnv = pgSpawnEnv(pgBinDir)
|
|
175
|
+
const createDbResult = spawnSync(
|
|
176
|
+
createdbBin,
|
|
177
|
+
[...pgConnArgs, projectName],
|
|
178
|
+
{ stdio: "pipe", encoding: "utf8", env: pgEnv },
|
|
54
179
|
)
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
180
|
+
if (createDbResult.status !== 0) {
|
|
181
|
+
const stderr = createDbResult.stderr ?? ""
|
|
182
|
+
if (!stderr.includes("already exists")) {
|
|
183
|
+
throw new Error(`Failed to create database "${projectName}": ${stderr}`)
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`[supatype] Created database "${projectName}".`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create roles required by PostgREST and grant them to postgres so
|
|
190
|
+
// PostgREST can SET ROLE when processing requests.
|
|
191
|
+
// anon – unauthenticated requests (RLS enforced)
|
|
192
|
+
// authenticated – signed-in user requests (RLS enforced)
|
|
193
|
+
// service_role – developer/admin bypass (BYPASSRLS)
|
|
194
|
+
const rolesSql = `
|
|
195
|
+
CREATE SCHEMA IF NOT EXISTS auth;
|
|
196
|
+
DO $$ BEGIN
|
|
197
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon')
|
|
198
|
+
THEN CREATE ROLE anon NOLOGIN; END IF;
|
|
199
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated')
|
|
200
|
+
THEN CREATE ROLE authenticated NOLOGIN; END IF;
|
|
201
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'service_role')
|
|
202
|
+
THEN CREATE ROLE service_role NOLOGIN BYPASSRLS; END IF;
|
|
203
|
+
END $$;
|
|
204
|
+
GRANT anon, authenticated, service_role TO postgres;
|
|
205
|
+
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
|
|
206
|
+
-- Table-level privileges (RLS restricts rows; roles still need table access)
|
|
207
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;
|
|
208
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO authenticated;
|
|
209
|
+
GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
|
|
210
|
+
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO authenticated, service_role;
|
|
211
|
+
-- Default privileges so tables created by the engine push inherit these grants
|
|
212
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO anon;
|
|
213
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO authenticated;
|
|
214
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO service_role;
|
|
215
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated, service_role;
|
|
216
|
+
`
|
|
217
|
+
spawnSync(psqlBin, [...pgConnArgs, "-d", projectName, "-c", rolesSql],
|
|
218
|
+
{ stdio: "pipe", encoding: "utf8", env: pgEnv })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"
|
|
222
|
+
const authDbURL = dbURL.includes("?")
|
|
223
|
+
? `${dbURL}&search_path=auth`
|
|
224
|
+
: `${dbURL}?search_path=auth`
|
|
225
|
+
|
|
226
|
+
// ── 8. GoTrue migrations (auth.users before engine studio SQL) ─────────
|
|
227
|
+
console.log("[supatype] Running GoTrue migrations...")
|
|
228
|
+
runGotrueMigrations(serverBin, authDbURL, LOCAL_JWT_SECRET)
|
|
229
|
+
|
|
230
|
+
// ── 9. Engine: apply schema ───────────────────────────────────────────
|
|
231
|
+
const schemaPath = schemaPathFromProject(config, cwd)
|
|
232
|
+
const supatypeDir = join(cwd, ".supatype")
|
|
233
|
+
const manifestPath = join(supatypeDir, "manifest.json")
|
|
234
|
+
const adminConfigPath = join(supatypeDir, "admin-config.json")
|
|
235
|
+
mkdirSync(supatypeDir, { recursive: true })
|
|
236
|
+
|
|
237
|
+
const localStoragePath = config.storage?.provider !== "s3" ? join(stateRoot, "storage") : undefined
|
|
238
|
+
// Native Postgres builds don't include PostGIS — skip geo fields rather than failing.
|
|
239
|
+
const skipFieldKinds: ReadonlySet<string> = provider === "native" ? new Set(["geo", "vector"]) : new Set()
|
|
240
|
+
|
|
241
|
+
await runSchemaPush(cwd, engineBin, schemaPath, dbURL, manifestPath, adminConfigPath, localStoragePath, skipFieldKinds).catch(
|
|
242
|
+
(e: unknown) => console.error("[supatype] Initial schema push failed:", (e as Error).message),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
// ── 10. Spawn supatype-server ─────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
// Resolve edge functions config: only enable Deno if a functions dir exists.
|
|
248
|
+
const functionsDir = functionsPathCandidatesFromProject(config, cwd).find(dir => existsSync(dir))
|
|
249
|
+
const hasFunctionsDir = functionsDir !== undefined
|
|
250
|
+
/** Always set when a functions dir exists so Studio admin API can list functions; Deno runtime is separate. */
|
|
251
|
+
const denoFunctionsDir = hasFunctionsDir ? functionsDir : ""
|
|
252
|
+
const functionRoutes = hasFunctionsDir && functionsDir !== undefined
|
|
253
|
+
? discoverTsFunctionsInDir(functionsDir)
|
|
254
|
+
: []
|
|
255
|
+
const denoServeScriptAbs = hasFunctionsDir && functionsDir !== undefined
|
|
256
|
+
? (writeDevFunctionsRouter(cwd, functionsDir, functionRoutes) ?? "")
|
|
257
|
+
: ""
|
|
258
|
+
|
|
259
|
+
let denoBinPath: string | undefined
|
|
260
|
+
if (hasFunctionsDir) {
|
|
261
|
+
console.log(`[supatype] Edge functions enabled (${functionsDir})`)
|
|
262
|
+
try {
|
|
263
|
+
denoBinPath = await ensureBinary("deno", config)
|
|
264
|
+
console.log(`[supatype] Deno runtime: ${denoBinPath} (v${config.versions.deno})`)
|
|
265
|
+
if (functionRoutes.length > 0) {
|
|
266
|
+
console.log(
|
|
267
|
+
`[supatype] Edge functions router: ${relative(cwd, denoServeScriptAbs) || ".supatype/functions-router.ts"} ` +
|
|
268
|
+
`(${functionRoutes.length} function(s): ${functionRoutes.map(fn => fn.name).join(", ")})`,
|
|
269
|
+
)
|
|
270
|
+
} else {
|
|
271
|
+
console.log("[supatype] Edge functions router not generated (no handler files discovered yet)")
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.warn(
|
|
275
|
+
`[supatype] ⚠ Found ${functionsDir} but could not provision Deno v${config.versions.deno} — edge functions will not run.\n` +
|
|
276
|
+
` ${(err as Error).message}\n` +
|
|
277
|
+
" (Functions still appear in Studio; invocations need Deno.)",
|
|
278
|
+
)
|
|
58
279
|
}
|
|
59
280
|
}
|
|
60
281
|
|
|
61
|
-
|
|
62
|
-
|
|
282
|
+
// Matches GOTRUE_HOOK_SEND_EMAIL_SECRETS symmetric format (dev only). Override via .env.
|
|
283
|
+
const LOCAL_SEND_EMAIL_HOOK_SECRETS =
|
|
284
|
+
"v1,whsec_abcdefghijklmnopqrstuvwxyz01234567"
|
|
285
|
+
const now = Math.floor(Date.now() / 1000)
|
|
286
|
+
const jwtBase = { iss: "supatype", iat: now, exp: now + 315_360_000 }
|
|
287
|
+
const anonKey = signJwt({ ...jwtBase, role: "anon" }, LOCAL_JWT_SECRET)
|
|
288
|
+
const serviceRoleKey = signJwt({ ...jwtBase, role: "service_role" }, LOCAL_JWT_SECRET)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
const emailProvider = config.email?.provider ?? "console"
|
|
292
|
+
const gotrueMailerProvider =
|
|
293
|
+
emailProvider === "console"
|
|
294
|
+
? "console"
|
|
295
|
+
: emailProvider === "resend"
|
|
296
|
+
? "resend"
|
|
297
|
+
: emailProvider === "ses"
|
|
298
|
+
? "ses"
|
|
299
|
+
: "smtp"
|
|
300
|
+
|
|
301
|
+
const serverEnv: Record<string, string> = {
|
|
302
|
+
// supatype-server outer layer
|
|
303
|
+
SUPATYPE_MODE: config.server.mode ?? "dev",
|
|
304
|
+
SUPATYPE_MANIFEST_PATH: manifestPath,
|
|
305
|
+
SUPATYPE_ADMIN_CONFIG_PATH: adminConfigPath,
|
|
306
|
+
SUPATYPE_POSTGREST_URL: `http://127.0.0.1:${postgrestPort}`,
|
|
307
|
+
SUPATYPE_DENO_FUNCTIONS_DIR: denoFunctionsDir,
|
|
308
|
+
...(denoFunctionsDir !== "" ? { SUPATYPE_SHARED_ENV_FILE: resolve(denoFunctionsDir, ".env.local") } : {}),
|
|
309
|
+
...(denoBinPath !== undefined ? { SUPATYPE_DENO_PATH: denoBinPath } : {}),
|
|
310
|
+
...(denoServeScriptAbs !== ""
|
|
311
|
+
? { SUPATYPE_DENO_SERVE_SCRIPT: denoServeScriptAbs }
|
|
312
|
+
: {}),
|
|
313
|
+
SUPATYPE_URL: `http://localhost:${serverPort}`,
|
|
314
|
+
SUPATYPE_ANON_KEY: anonKey,
|
|
315
|
+
SUPATYPE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
316
|
+
PORT: serverPort,
|
|
317
|
+
SUPATYPE_APP_MODE: config.app.mode ?? "none",
|
|
318
|
+
...(config.app.mode === "static" && config.app.static_dir?.trim()
|
|
319
|
+
? { SUPATYPE_APP_STATIC_DIR: resolve(cwd, config.app.static_dir.trim()) }
|
|
320
|
+
: {}),
|
|
321
|
+
...(config.app.mode === "proxy" && config.app.upstream?.trim()
|
|
322
|
+
? { SUPATYPE_APP_UPSTREAM: config.app.upstream.trim() }
|
|
323
|
+
: {}),
|
|
324
|
+
...(config.app.vite_dev_url !== undefined && config.app.vite_dev_url.trim() !== ""
|
|
325
|
+
? { SUPATYPE_VITE_DEV_URL: config.app.vite_dev_url.trim() }
|
|
326
|
+
: {}),
|
|
327
|
+
// GoTrue required fields (sensible local-dev defaults)
|
|
328
|
+
DATABASE_URL: authDbURL,
|
|
329
|
+
SUPATYPE_SQL_DATABASE_URL: dbURL,
|
|
330
|
+
GOTRUE_DB_DRIVER: "postgres",
|
|
331
|
+
GOTRUE_JWT_SECRET: LOCAL_JWT_SECRET,
|
|
332
|
+
GOTRUE_JWT_EXP: "3600",
|
|
333
|
+
GOTRUE_JWT_AUD: "authenticated",
|
|
334
|
+
GOTRUE_JWT_ADMIN_ROLES: "supatype_admin,service_role",
|
|
335
|
+
API_EXTERNAL_URL: `http://localhost:${serverPort}/auth/v1`,
|
|
336
|
+
GOTRUE_API_HOST: "localhost",
|
|
337
|
+
GOTRUE_SITE_URL: `http://localhost:${serverPort}`,
|
|
338
|
+
GOTRUE_MAILER_MAILER_PROVIDER: gotrueMailerProvider,
|
|
339
|
+
GOTRUE_MAILER_AUTOCONFIRM: "true",
|
|
340
|
+
GOTRUE_LOG_LEVEL: "info",
|
|
341
|
+
GOTRUE_DISABLE_SIGNUP: "false",
|
|
342
|
+
...(config.email?.resend_api_key !== undefined && config.email.resend_api_key !== ""
|
|
343
|
+
? { RESEND_API_KEY: config.email.resend_api_key }
|
|
344
|
+
: {}),
|
|
345
|
+
...(gotrueMailerProvider === "resend" &&
|
|
346
|
+
config.email?.resend_from !== undefined &&
|
|
347
|
+
config.email.resend_from.trim() !== ""
|
|
348
|
+
? { RESEND_FROM: config.email.resend_from.trim() }
|
|
349
|
+
: {}),
|
|
350
|
+
...(gotrueMailerProvider === "ses" &&
|
|
351
|
+
config.email?.ses_from !== undefined &&
|
|
352
|
+
config.email.ses_from.trim() !== ""
|
|
353
|
+
? { SES_FROM: config.email.ses_from.trim() }
|
|
354
|
+
: {}),
|
|
355
|
+
...(gotrueMailerProvider === "smtp" ? gotrueSMTPFromEmailConfig(config.email) : {}),
|
|
356
|
+
...(config.email?.send_email_hook === true
|
|
357
|
+
? {
|
|
358
|
+
GOTRUE_HOOK_SEND_EMAIL_ENABLED: "true",
|
|
359
|
+
GOTRUE_HOOK_SEND_EMAIL_URI:
|
|
360
|
+
config.email?.send_email_hook_uri !== undefined &&
|
|
361
|
+
config.email.send_email_hook_uri.trim() !== ""
|
|
362
|
+
? config.email.send_email_hook_uri.trim()
|
|
363
|
+
: `http://127.0.0.1:${serverPort}/internal/v0hooks/send-email`,
|
|
364
|
+
GOTRUE_HOOK_SEND_EMAIL_SECRETS:
|
|
365
|
+
config.email?.send_email_hook_secrets !== undefined &&
|
|
366
|
+
config.email.send_email_hook_secrets.trim() !== ""
|
|
367
|
+
? config.email.send_email_hook_secrets.trim()
|
|
368
|
+
: LOCAL_SEND_EMAIL_HOOK_SECRETS,
|
|
369
|
+
}
|
|
370
|
+
: {}),
|
|
371
|
+
...(config.storage?.provider !== "s3" ? localStorageEnv(stateRoot) : {}),
|
|
372
|
+
...loadDotEnv(cwd),
|
|
373
|
+
}
|
|
63
374
|
|
|
64
|
-
const
|
|
375
|
+
const serverProc = new ProcessManager(serverBin, [], {
|
|
376
|
+
label: "server",
|
|
377
|
+
pidDir,
|
|
378
|
+
colour: "\x1b[32m",
|
|
379
|
+
env: serverEnv,
|
|
380
|
+
})
|
|
381
|
+
serverProc.start()
|
|
382
|
+
|
|
383
|
+
// ── 9b. PostgREST ────────────────────────────────────────────────────
|
|
384
|
+
let postgrestProc: ProcessManager | null = null
|
|
385
|
+
const postgrestBin = await resolvePostgrestBin(config.overrides?.postgrest)
|
|
386
|
+
if (postgrestBin) {
|
|
387
|
+
// Windows PostgREST builds are dynamically linked and require libpq/OpenSSL
|
|
388
|
+
// DLLs from a Postgres bin directory, even when the database runs in Docker.
|
|
389
|
+
let postgrestRuntimeBinDir = pgBinDir
|
|
390
|
+
if (process.platform === "win32" && postgrestRuntimeBinDir === null) {
|
|
391
|
+
try {
|
|
392
|
+
postgrestRuntimeBinDir = await resolvePgBinDir(config)
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.warn(
|
|
395
|
+
`[supatype] ⚠ Could not resolve Postgres runtime DLL directory for PostgREST: ${(error as Error).message}\n` +
|
|
396
|
+
" PostgREST may fail to start on Windows until Postgres binaries are available locally.",
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const postgrestEnv: Record<string, string> = {
|
|
402
|
+
PGRST_DB_URI: dbURL,
|
|
403
|
+
PGRST_DB_SCHEMA: "public, supatype",
|
|
404
|
+
PGRST_DB_ANON_ROLE: "anon",
|
|
405
|
+
PGRST_SERVER_PORT: postgrestPort,
|
|
406
|
+
PGRST_SERVER_HOST: "127.0.0.1",
|
|
407
|
+
PGRST_JWT_SECRET: serverEnv["GOTRUE_JWT_SECRET"] ?? "",
|
|
408
|
+
PGRST_LOG_LEVEL: "warn",
|
|
409
|
+
// On Windows, PostgREST (MinGW/GHC binary) needs libpq.dll and
|
|
410
|
+
// OpenSSL DLLs. Prepend a Postgres bin dir which bundles these
|
|
411
|
+
// runtime dependencies.
|
|
412
|
+
...(process.platform === "win32" && postgrestRuntimeBinDir !== null
|
|
413
|
+
? { PATH: `${postgrestRuntimeBinDir};${process.env["PATH"] ?? ""}` }
|
|
414
|
+
: {}),
|
|
415
|
+
}
|
|
65
416
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...
|
|
417
|
+
const preflight = spawnSync(
|
|
418
|
+
postgrestBin,
|
|
419
|
+
["--help"],
|
|
420
|
+
{ env: { ...process.env, ...postgrestEnv }, stdio: "pipe", encoding: "utf8" },
|
|
70
421
|
)
|
|
422
|
+
if (preflight.status !== 0) {
|
|
423
|
+
const detail = (preflight.stderr || preflight.stdout || "").trim()
|
|
424
|
+
console.warn(
|
|
425
|
+
`[supatype] ⚠ PostgREST failed preflight (exit ${preflight.status}). ` +
|
|
426
|
+
"Skipping /rest/v1 startup to avoid crash loop.",
|
|
427
|
+
)
|
|
428
|
+
if (detail) {
|
|
429
|
+
console.warn(`[supatype] PostgREST preflight output:\n${detail}`)
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
postgrestProc = new ProcessManager(postgrestBin, [], {
|
|
433
|
+
label: "postgrest",
|
|
434
|
+
pidDir,
|
|
435
|
+
colour: "\x1b[36m",
|
|
436
|
+
env: postgrestEnv,
|
|
437
|
+
})
|
|
438
|
+
postgrestProc.start()
|
|
439
|
+
}
|
|
71
440
|
}
|
|
72
441
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
442
|
+
// ── 9d. Studio (optional) ─────────────────────────────────────────────
|
|
443
|
+
const studioPort = 3002
|
|
444
|
+
let studioProc: ProcessManager | null = null
|
|
445
|
+
|
|
446
|
+
const studioOverride = config.overrides?.studio
|
|
447
|
+
if (studioOverride) {
|
|
448
|
+
const studioDir = resolve(cwd, studioOverride)
|
|
449
|
+
// Run vite's JS entry directly via node — avoids .cmd/.sh wrapper spawn issues on Windows.
|
|
450
|
+
const viteJs = join(studioDir, "node_modules", "vite", "bin", "vite.js")
|
|
451
|
+
if (existsSync(viteJs)) {
|
|
452
|
+
studioProc = new ProcessManager(
|
|
453
|
+
process.execPath,
|
|
454
|
+
[viteJs, "--port", String(studioPort), "--strictPort"],
|
|
455
|
+
{
|
|
456
|
+
label: "studio",
|
|
457
|
+
pidDir,
|
|
458
|
+
cwd: studioDir,
|
|
459
|
+
colour: "\x1b[35m",
|
|
460
|
+
env: {
|
|
461
|
+
// Point the studio at the Vite dev server (same origin as the
|
|
462
|
+
// browser) so all API requests are same-origin — CORS never fires.
|
|
463
|
+
// Vite's dev proxy (configured via SUPATYPE_PROXY_TARGET) then
|
|
464
|
+
// forwards those requests server-side to the actual backend.
|
|
465
|
+
VITE_SUPATYPE_URL: `http://localhost:${studioPort}`,
|
|
466
|
+
SUPATYPE_PROXY_TARGET: `http://localhost:${serverPort}`,
|
|
467
|
+
// Studio is a developer tool — use service_role key to bypass
|
|
468
|
+
// RLS so all tables and rows are visible regardless of policies.
|
|
469
|
+
VITE_SUPATYPE_ANON_KEY: serviceRoleKey,
|
|
470
|
+
VITE_SUPATYPE_SERVICE_ROLE_KEY: serviceRoleKey,
|
|
471
|
+
VITE_BASE_PATH: "/",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
studioProc.start()
|
|
476
|
+
} else {
|
|
477
|
+
console.warn(`[supatype] ⚠ Studio override set but vite not found at ${viteJs}. Run: pnpm install`)
|
|
478
|
+
}
|
|
83
479
|
}
|
|
84
|
-
console.log()
|
|
85
480
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
481
|
+
// ── Print status ──────────────────────────────────────────────────────
|
|
482
|
+
console.log(`
|
|
483
|
+
[supatype] Services running:
|
|
484
|
+
Postgres ${dbURL}
|
|
485
|
+
supatype-server http://localhost:${serverPort}
|
|
486
|
+
REST API http://localhost:${serverPort}/rest/v1/
|
|
487
|
+
Auth http://localhost:${serverPort}/auth/v1/
|
|
488
|
+
Storage http://localhost:${serverPort}/storage/v1/
|
|
489
|
+
Realtime ws://localhost:${serverPort}/realtime/v1/${studioProc ? `\n Studio http://localhost:${studioPort}` : ""}
|
|
490
|
+
|
|
491
|
+
API keys (local dev only):
|
|
492
|
+
anon key ${anonKey}
|
|
493
|
+
service_role ${serviceRoleKey}
|
|
494
|
+
|
|
495
|
+
JWT secret: ${LOCAL_JWT_SECRET}
|
|
496
|
+
|
|
497
|
+
Press Ctrl+C to stop.
|
|
498
|
+
`)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
// ── Shutdown handler ──────────────────────────────────────────────────
|
|
502
|
+
const cleanup = async () => {
|
|
503
|
+
console.log("\n[supatype] Shutting down...")
|
|
504
|
+
await Promise.all([
|
|
505
|
+
serverProc.stop(),
|
|
506
|
+
postgrestProc?.stop(),
|
|
507
|
+
studioProc?.stop(),
|
|
508
|
+
])
|
|
509
|
+
await stopPostgres()
|
|
91
510
|
process.exit(0)
|
|
92
511
|
}
|
|
93
|
-
process.
|
|
94
|
-
process.
|
|
512
|
+
process.once("SIGINT", cleanup)
|
|
513
|
+
process.once("SIGTERM", cleanup)
|
|
95
514
|
|
|
515
|
+
// ── 10. Schema watch ──────────────────────────────────────────────────
|
|
96
516
|
if (opts.watch) {
|
|
97
|
-
|
|
517
|
+
const schemaDir = resolve(cwd, schemaPath, "..")
|
|
518
|
+
console.log(`[supatype] Watching ${schemaDir} for changes...`)
|
|
519
|
+
|
|
520
|
+
const { watch } = await import("node:fs")
|
|
521
|
+
// Debounce: Windows fs.watch fires multiple events per save.
|
|
522
|
+
// Wait 300 ms after the last event before pushing.
|
|
523
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
524
|
+
watch(schemaDir, { recursive: true }, (_eventType, filename) => {
|
|
525
|
+
if (!filename?.endsWith(".ts")) return
|
|
526
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
527
|
+
debounceTimer = setTimeout(() => {
|
|
528
|
+
debounceTimer = null
|
|
529
|
+
console.log(`\n[supatype] Change detected in ${filename}, checking schema...`)
|
|
530
|
+
runSchemaPush(cwd, engineBin, schemaPath, dbURL, manifestPath, adminConfigPath, localStoragePath, skipFieldKinds).catch((e: unknown) =>
|
|
531
|
+
console.error("[supatype] Schema push failed:", (e as Error).message),
|
|
532
|
+
)
|
|
533
|
+
}, 300)
|
|
534
|
+
})
|
|
98
535
|
}
|
|
536
|
+
|
|
537
|
+
// Block until killed.
|
|
538
|
+
await new Promise<never>(() => undefined)
|
|
99
539
|
})
|
|
100
540
|
}
|
|
101
541
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
test: ["CMD", "mc", "ready", "local"]
|
|
184
|
-
interval: 5s
|
|
185
|
-
timeout: 5s
|
|
186
|
-
retries: 10
|
|
187
|
-
|
|
188
|
-
kong:
|
|
189
|
-
image: kong:3.6
|
|
190
|
-
environment:
|
|
191
|
-
KONG_DATABASE: "off"
|
|
192
|
-
KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml
|
|
193
|
-
KONG_PROXY_ACCESS_LOG: /dev/stdout
|
|
194
|
-
KONG_ADMIN_ACCESS_LOG: /dev/stdout
|
|
195
|
-
KONG_PROXY_ERROR_LOG: /dev/stderr
|
|
196
|
-
KONG_ADMIN_ERROR_LOG: /dev/stderr
|
|
197
|
-
volumes:
|
|
198
|
-
- ./.supatype/kong.yml:/etc/kong/kong.yml:ro
|
|
199
|
-
ports:
|
|
200
|
-
- "8000:8000"
|
|
201
|
-
depends_on:
|
|
202
|
-
- postgrest
|
|
203
|
-
- gotrue
|
|
204
|
-
|
|
205
|
-
volumes:
|
|
206
|
-
db-data:
|
|
207
|
-
minio-data:
|
|
208
|
-
`
|
|
209
|
-
writeFileSync(resolve(cwd, "docker-compose.yml"), content, "utf8")
|
|
210
|
-
console.log(" created docker-compose.yml (infra only)\n")
|
|
211
|
-
|
|
212
|
-
// Also ensure pgbouncer config exists
|
|
213
|
-
const supatypeDir = resolve(cwd, ".supatype")
|
|
214
|
-
mkdirSync(supatypeDir, { recursive: true })
|
|
215
|
-
|
|
216
|
-
if (!existsSync(resolve(supatypeDir, "pgbouncer.ini"))) {
|
|
217
|
-
writeFileSync(resolve(supatypeDir, "pgbouncer.ini"), `[databases]
|
|
218
|
-
* = host=db port=5432
|
|
219
|
-
|
|
220
|
-
[pgbouncer]
|
|
221
|
-
listen_addr = 0.0.0.0
|
|
222
|
-
listen_port = 6432
|
|
223
|
-
auth_type = trust
|
|
224
|
-
auth_file = /etc/pgbouncer/userlist.txt
|
|
225
|
-
pool_mode = transaction
|
|
226
|
-
default_pool_size = 20
|
|
227
|
-
max_db_connections = 60
|
|
228
|
-
max_client_conn = 100
|
|
229
|
-
server_reset_query = DEALLOCATE ALL
|
|
230
|
-
ignore_startup_parameters = extra_float_digits
|
|
231
|
-
`, "utf8")
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Schema push (engine subprocess)
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
// Last successfully-pushed AST JSON — used to skip no-op re-fires.
|
|
547
|
+
let _lastPushedAst: string | null = null
|
|
548
|
+
// AST that failed on its last attempt — always retried even if content is unchanged.
|
|
549
|
+
let _lastFailedAst: string | null = null
|
|
550
|
+
|
|
551
|
+
async function runSchemaPush(
|
|
552
|
+
cwd: string,
|
|
553
|
+
engineBin: string,
|
|
554
|
+
schemaPath: string,
|
|
555
|
+
dbURL: string,
|
|
556
|
+
manifestPath: string,
|
|
557
|
+
adminConfigPath?: string,
|
|
558
|
+
storagePath?: string,
|
|
559
|
+
skipFieldKinds?: ReadonlySet<string>,
|
|
560
|
+
): Promise<void> {
|
|
561
|
+
// Build AST JSON from schema file.
|
|
562
|
+
const { loadSchemaAst } = await import("../config.js")
|
|
563
|
+
let ast = loadSchemaAst(schemaPath, cwd)
|
|
564
|
+
|
|
565
|
+
// Strip fields whose kind requires an unavailable Postgres extension.
|
|
566
|
+
if (skipFieldKinds && skipFieldKinds.size > 0) {
|
|
567
|
+
const { filtered, adapted } = adaptUnsupportedKinds(ast, skipFieldKinds)
|
|
568
|
+
ast = filtered
|
|
569
|
+
if (adapted.length > 0) {
|
|
570
|
+
console.warn(
|
|
571
|
+
`[supatype] ⚠ ${adapted.length} field(s) replaced with JSONB — required extensions not available:\n` +
|
|
572
|
+
adapted.map((s: string) => ` ${s}`).join("\n"),
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const astJson = JSON.stringify(ast)
|
|
578
|
+
|
|
579
|
+
// Skip only when the last push of this exact AST succeeded.
|
|
580
|
+
// If it previously failed we always retry so the user can trigger a re-run
|
|
581
|
+
// by simply saving the file again without needing to make a content change.
|
|
582
|
+
if (astJson === _lastPushedAst && astJson !== _lastFailedAst) {
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const astPath = join(cwd, ".supatype", "schema.ast.json")
|
|
587
|
+
writeFileSync(astPath, astJson)
|
|
588
|
+
|
|
589
|
+
// Push schema.
|
|
590
|
+
console.log("[supatype] Applying schema...")
|
|
591
|
+
const pushResult = spawnSync(
|
|
592
|
+
engineBin,
|
|
593
|
+
["push", "-i", astPath, "--database-url", dbURL, "--force"],
|
|
594
|
+
{ cwd, stdio: "inherit", encoding: "utf8" },
|
|
595
|
+
)
|
|
596
|
+
if (pushResult.status !== 0) {
|
|
597
|
+
_lastFailedAst = astJson
|
|
598
|
+
throw new Error(`Engine schema push failed (exit ${pushResult.status})`)
|
|
599
|
+
}
|
|
600
|
+
_lastPushedAst = astJson
|
|
601
|
+
_lastFailedAst = null
|
|
602
|
+
|
|
603
|
+
// Provision storage buckets declared in the schema.
|
|
604
|
+
if (storagePath) {
|
|
605
|
+
const parseResult = spawnSync(engineBin, ["parse", "-i", astPath], { cwd, stdio: "pipe", encoding: "utf8" })
|
|
606
|
+
if (parseResult.status === 0 && parseResult.stdout) {
|
|
607
|
+
try {
|
|
608
|
+
const resolvedAst = JSON.parse(parseResult.stdout) as {
|
|
609
|
+
storageBuckets?: Array<{
|
|
610
|
+
id: string
|
|
611
|
+
public: boolean
|
|
612
|
+
allowedMimeTypes?: string[]
|
|
613
|
+
fileSizeLimit?: number
|
|
614
|
+
accessMode?: string
|
|
615
|
+
s3BucketPolicy?: string | null
|
|
616
|
+
}>
|
|
617
|
+
}
|
|
618
|
+
if (resolvedAst.storageBuckets && resolvedAst.storageBuckets.length > 0) {
|
|
619
|
+
provisionStorageBuckets(resolvedAst.storageBuckets, storagePath)
|
|
620
|
+
}
|
|
621
|
+
} catch { /* ignore parse errors */ }
|
|
622
|
+
}
|
|
232
623
|
}
|
|
233
624
|
|
|
234
|
-
|
|
235
|
-
|
|
625
|
+
// Generate manifest.
|
|
626
|
+
const genResult = spawnSync(
|
|
627
|
+
engineBin,
|
|
628
|
+
["generate", "-i", astPath, "-o", manifestPath],
|
|
629
|
+
{ cwd, stdio: "pipe", encoding: "utf8" },
|
|
630
|
+
)
|
|
631
|
+
if (genResult.status !== 0) {
|
|
632
|
+
console.warn("[supatype] Manifest generation failed — server routing may be stale.")
|
|
236
633
|
}
|
|
237
634
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- /rest/v1/
|
|
249
|
-
- name: auth-v1
|
|
250
|
-
url: http://gotrue:9999
|
|
251
|
-
routes:
|
|
252
|
-
- name: auth-v1-all
|
|
253
|
-
strip_path: true
|
|
254
|
-
paths:
|
|
255
|
-
- /auth/v1/
|
|
256
|
-
- name: storage-v1
|
|
257
|
-
url: http://host.docker.internal:5000
|
|
258
|
-
routes:
|
|
259
|
-
- name: storage-v1-all
|
|
260
|
-
strip_path: true
|
|
261
|
-
paths:
|
|
262
|
-
- /storage/v1/
|
|
263
|
-
- name: realtime-v1
|
|
264
|
-
url: http://host.docker.internal:4000
|
|
265
|
-
routes:
|
|
266
|
-
- name: realtime-v1-all
|
|
267
|
-
strip_path: true
|
|
268
|
-
paths:
|
|
269
|
-
- /realtime/v1/
|
|
270
|
-
protocols:
|
|
271
|
-
- http
|
|
272
|
-
- https
|
|
273
|
-
- ws
|
|
274
|
-
- wss
|
|
275
|
-
- name: functions-v1
|
|
276
|
-
url: http://host.docker.internal:54321
|
|
277
|
-
routes:
|
|
278
|
-
- name: functions-v1-all
|
|
279
|
-
strip_path: false
|
|
280
|
-
paths:
|
|
281
|
-
- /functions/v1/
|
|
282
|
-
- name: studio
|
|
283
|
-
url: http://host.docker.internal:3002
|
|
284
|
-
routes:
|
|
285
|
-
- name: studio-all
|
|
286
|
-
strip_path: true
|
|
287
|
-
paths:
|
|
288
|
-
- /studio/
|
|
289
|
-
`, "utf8")
|
|
635
|
+
// Generate admin config (for Studio). Engine writes to stdout.
|
|
636
|
+
if (adminConfigPath) {
|
|
637
|
+
const adminResult = spawnSync(
|
|
638
|
+
engineBin,
|
|
639
|
+
["admin", "-i", astPath],
|
|
640
|
+
{ cwd, stdio: "pipe", encoding: "utf8" },
|
|
641
|
+
)
|
|
642
|
+
if (adminResult.status === 0 && adminResult.stdout) {
|
|
643
|
+
writeFileSync(adminConfigPath, adminResult.stdout)
|
|
644
|
+
}
|
|
290
645
|
}
|
|
646
|
+
|
|
647
|
+
console.log("[supatype] Schema applied.")
|
|
291
648
|
}
|
|
292
649
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// Storage bucket provisioning (local dev only)
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
|
|
654
|
+
function provisionStorageBuckets(
|
|
655
|
+
declared: Array<{
|
|
656
|
+
id: string
|
|
657
|
+
public: boolean
|
|
658
|
+
allowedMimeTypes?: string[]
|
|
659
|
+
fileSizeLimit?: number
|
|
660
|
+
accessMode?: string
|
|
661
|
+
s3BucketPolicy?: string | null
|
|
662
|
+
}>,
|
|
663
|
+
storagePath: string,
|
|
664
|
+
): void {
|
|
665
|
+
const bucketsDir = join(storagePath, ".supatype")
|
|
666
|
+
const bucketsFile = join(bucketsDir, "buckets.json")
|
|
667
|
+
mkdirSync(bucketsDir, { recursive: true })
|
|
668
|
+
|
|
669
|
+
let existing: Array<Record<string, unknown>> = []
|
|
670
|
+
try {
|
|
671
|
+
existing = JSON.parse(readFileSync(bucketsFile, "utf8")) as Array<Record<string, unknown>>
|
|
672
|
+
} catch { /* file doesn't exist yet */ }
|
|
673
|
+
|
|
674
|
+
const existingIds = new Set(existing.map((b) => b["id"] as string))
|
|
675
|
+
let added = 0
|
|
676
|
+
|
|
677
|
+
for (const bucket of declared) {
|
|
678
|
+
if (existingIds.has(bucket.id)) continue
|
|
679
|
+
const now = new Date().toISOString()
|
|
680
|
+
existing.push({
|
|
681
|
+
id: bucket.id,
|
|
682
|
+
name: bucket.id,
|
|
683
|
+
public: bucket.public,
|
|
684
|
+
file_size_limit: bucket.fileSizeLimit ?? null,
|
|
685
|
+
allowed_mime_types: bucket.allowedMimeTypes ?? null,
|
|
686
|
+
access_mode:
|
|
687
|
+
bucket.accessMode ?? (bucket.public ? "public" : "private"),
|
|
688
|
+
s3_bucket_policy: bucket.s3BucketPolicy ?? null,
|
|
689
|
+
created_at: now,
|
|
690
|
+
updated_at: now,
|
|
691
|
+
})
|
|
692
|
+
mkdirSync(join(storagePath, bucket.id), { recursive: true })
|
|
693
|
+
added++
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (added > 0) {
|
|
697
|
+
writeFileSync(bucketsFile, JSON.stringify(existing, null, 2))
|
|
698
|
+
console.log(`[supatype] Storage: provisioned ${added} bucket(s).`)
|
|
699
|
+
}
|
|
324
700
|
}
|
|
325
701
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
// Resolve Postgres bin dir
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
|
|
706
|
+
async function resolvePgBinDir(config: Awaited<ReturnType<typeof loadConfig>>): Promise<string> {
|
|
707
|
+
const override = config.overrides?.postgres_dir
|
|
708
|
+
if (override) {
|
|
709
|
+
// Normalize Git Bash (/c/Users/...) paths to Win32 form (C:\Users\...) on Windows.
|
|
710
|
+
const normalised = normalisePlatformPath(override)
|
|
711
|
+
const resolved = resolve(process.cwd(), normalised)
|
|
712
|
+
const binDir = join(resolved, "bin")
|
|
713
|
+
if (!existsSync(binDir)) {
|
|
714
|
+
throw new Error(`[overrides] postgres_dir does not contain a bin/ directory: ${resolved}`)
|
|
715
|
+
}
|
|
716
|
+
console.warn(`\u26a0 Using local Postgres build: ${resolved}`)
|
|
717
|
+
return binDir
|
|
338
718
|
}
|
|
339
|
-
|
|
719
|
+
|
|
720
|
+
// Locate cached Postgres archive.
|
|
721
|
+
const { cachePath } = await import("../binary-cache.js")
|
|
722
|
+
const version = config.versions.postgres
|
|
723
|
+
const { currentPlatform } = await import("../binary-cache.js")
|
|
724
|
+
const platform = currentPlatform()
|
|
725
|
+
|
|
726
|
+
const pgCacheDir = cachePath("postgres", version)
|
|
727
|
+
const extractedDir = join(pgCacheDir, `pg-${version}`)
|
|
728
|
+
|
|
729
|
+
const pgCtlName = platform.os === "windows" ? "pg_ctl.exe" : "pg_ctl"
|
|
730
|
+
if (!existsSync(join(extractedDir, "bin", pgCtlName))) {
|
|
731
|
+
// Try to extract the cached archive.
|
|
732
|
+
await extractPostgresArchive(pgCacheDir, version, platform, extractedDir)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return join(extractedDir, "bin")
|
|
340
736
|
}
|
|
341
737
|
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
738
|
+
async function extractPostgresArchive(
|
|
739
|
+
pgCacheDir: string,
|
|
740
|
+
version: string,
|
|
741
|
+
platform: { os: string; arch: string },
|
|
742
|
+
extractDir: string,
|
|
743
|
+
): Promise<void> {
|
|
744
|
+
const ext = platform.os === "windows" ? ".zip" : ".tar.gz"
|
|
745
|
+
const archiveName = `supatype-pg-${postgresArchiveTag(version)}-${platform.os}-${platform.arch}${ext}`
|
|
746
|
+
const archivePath = join(pgCacheDir, archiveName)
|
|
747
|
+
|
|
748
|
+
if (!existsSync(archivePath)) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`Postgres ${version} archive not found. Run: supatype update`,
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
mkdirSync(extractDir, { recursive: true })
|
|
755
|
+
|
|
756
|
+
// On Windows, Git Bash tar is typically first in PATH and chokes on drive-letter
|
|
757
|
+
// paths (C:\...). Use PowerShell's Expand-Archive instead, which handles Windows
|
|
758
|
+
// paths natively. On Linux/macOS, use tar as normal.
|
|
759
|
+
const result = platform.os === "windows"
|
|
760
|
+
? spawnSync(
|
|
761
|
+
"powershell.exe",
|
|
762
|
+
["-NoProfile", "-Command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`],
|
|
763
|
+
{ stdio: "inherit" },
|
|
764
|
+
)
|
|
765
|
+
: spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], { stdio: "inherit" })
|
|
766
|
+
|
|
767
|
+
if (result.status !== 0) {
|
|
768
|
+
throw new Error(`Failed to extract Postgres archive: ${archivePath}`)
|
|
359
769
|
}
|
|
360
|
-
// .env file values override defaults
|
|
361
|
-
const dotEnv = loadDotEnv(cwd)
|
|
362
|
-
return { ...defaults, ...dotEnv }
|
|
363
770
|
}
|
|
364
771
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
// PostgREST resolver — downloads from GitHub releases if not cached
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
368
775
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
{ name: "realtime", filter: "@supatype/realtime", color: "\x1b[35m" },
|
|
372
|
-
{ name: "studio", filter: "@supatype/studio", color: "\x1b[36m" },
|
|
373
|
-
]
|
|
776
|
+
const POSTGREST_DEFAULT_VERSION = "12.2.3"
|
|
777
|
+
const POSTGREST_GITHUB = "https://github.com/PostgREST/postgrest/releases/download"
|
|
374
778
|
|
|
375
|
-
|
|
376
|
-
|
|
779
|
+
async function resolvePostgrestBin(overridePath?: string): Promise<string | null> {
|
|
780
|
+
// Honour local override (same pattern as engine/server).
|
|
781
|
+
if (overridePath) {
|
|
782
|
+
let p = resolve(process.cwd(), normalisePlatformPath(overridePath))
|
|
783
|
+
if (process.platform === "win32" && !p.endsWith(".exe") && !existsSync(p)) {
|
|
784
|
+
const withExe = p + ".exe"
|
|
785
|
+
if (existsSync(withExe)) p = withExe
|
|
786
|
+
}
|
|
787
|
+
if (existsSync(p)) return p
|
|
788
|
+
console.warn(`[supatype] ⚠ PostgREST override not found at ${p}`)
|
|
789
|
+
return null
|
|
790
|
+
}
|
|
377
791
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
792
|
+
const version = POSTGREST_DEFAULT_VERSION
|
|
793
|
+
const platform = currentPlatform()
|
|
794
|
+
const arch = platform.arch === "arm64" ? "aarch64" : "x86_64"
|
|
795
|
+
const binName = platform.os === "windows" ? "postgrest.exe" : "postgrest"
|
|
796
|
+
const cacheDir = cachePath("postgres", version).replace(/postgres/, "postgrest")
|
|
797
|
+
const binPath = join(cacheDir, binName)
|
|
798
|
+
const archiveName = platform.os === "windows"
|
|
799
|
+
? `postgrest-v${version}-windows-x64.zip`
|
|
800
|
+
: platform.os === "darwin"
|
|
801
|
+
? `postgrest-v${version}-macos-${arch}.tar.xz`
|
|
802
|
+
: `postgrest-v${version}-linux-static-${arch}.tar.xz`
|
|
803
|
+
const archivePath = join(cacheDir, archiveName)
|
|
804
|
+
|
|
805
|
+
if (existsSync(binPath)) {
|
|
806
|
+
// Backfill DLLs for older cached Windows installs where only postgrest.exe
|
|
807
|
+
// was copied from the release archive.
|
|
808
|
+
if (platform.os === "windows" && !hasLikelyWindowsRuntimeDlls(cacheDir) && existsSync(archivePath)) {
|
|
809
|
+
const repaired = repairWindowsPostgrestRuntime(cacheDir, archivePath, binPath)
|
|
810
|
+
if (!repaired) {
|
|
811
|
+
console.warn("[supatype] ⚠ PostgREST runtime DLL repair failed; REST API may be unavailable.")
|
|
812
|
+
}
|
|
381
813
|
}
|
|
814
|
+
return binPath
|
|
815
|
+
}
|
|
382
816
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const child = spawn("pnpm", ["dev"], {
|
|
387
|
-
cwd: pkgDir,
|
|
388
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
389
|
-
shell: true,
|
|
390
|
-
env: {
|
|
391
|
-
...process.env,
|
|
392
|
-
...devEnv,
|
|
393
|
-
PORT: svc.name === "storage" ? "5000" : svc.name === "realtime" ? "4000" : "3002",
|
|
394
|
-
},
|
|
395
|
-
})
|
|
817
|
+
// Download from GitHub releases.
|
|
818
|
+
const url = `${POSTGREST_GITHUB}/v${version}/${archiveName}`
|
|
396
819
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
console.log(`${prefix} ${line}`)
|
|
400
|
-
}
|
|
401
|
-
})
|
|
402
|
-
child.stderr?.on("data", (data: Buffer) => {
|
|
403
|
-
for (const line of data.toString().trimEnd().split("\n")) {
|
|
404
|
-
console.error(`${prefix} ${line}`)
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
child.on("exit", (code) => {
|
|
408
|
-
if (code !== 0 && code !== null) {
|
|
409
|
-
console.error(`${prefix} exited with code ${code}`)
|
|
410
|
-
}
|
|
411
|
-
})
|
|
820
|
+
console.log(`[supatype] Downloading PostgREST v${version}...`)
|
|
821
|
+
mkdirSync(cacheDir, { recursive: true })
|
|
412
822
|
|
|
413
|
-
|
|
414
|
-
|
|
823
|
+
let resp: Response
|
|
824
|
+
try {
|
|
825
|
+
resp = await fetch(url)
|
|
826
|
+
} catch (e) {
|
|
827
|
+
console.warn(
|
|
828
|
+
`[supatype] ⚠ Could not download PostgREST (${(e as Error).message}).\n` +
|
|
829
|
+
` REST API (/rest/v1/) will be unavailable until the download succeeds.\n` +
|
|
830
|
+
` Re-run 'supatype dev' once network access to github.com:443 is restored.`,
|
|
831
|
+
)
|
|
832
|
+
return null
|
|
833
|
+
}
|
|
834
|
+
if (!resp.ok) {
|
|
835
|
+
console.warn(`[supatype] ⚠ Could not download PostgREST: HTTP ${resp.status}. REST API will be unavailable.`)
|
|
836
|
+
return null
|
|
415
837
|
}
|
|
416
838
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
839
|
+
const buf = Buffer.from(await resp.arrayBuffer())
|
|
840
|
+
writeFileSync(archivePath, buf)
|
|
841
|
+
|
|
842
|
+
// Extract. The Windows zip may nest postgrest.exe inside a subdirectory, so
|
|
843
|
+
// after Expand-Archive we copy postgrest.exe and sibling DLLs to cacheDir.
|
|
844
|
+
if (platform.os === "windows") {
|
|
845
|
+
const r = spawnSync(
|
|
846
|
+
"powershell.exe",
|
|
847
|
+
[
|
|
848
|
+
"-NoProfile", "-Command",
|
|
849
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${cacheDir}' -Force; ` +
|
|
850
|
+
`$exe = Get-ChildItem -Path '${cacheDir}' -Recurse -Filter 'postgrest.exe' | Select-Object -First 1; ` +
|
|
851
|
+
`if ($exe) { ` +
|
|
852
|
+
` Copy-Item -Path $exe.FullName -Destination '${binPath}' -Force; ` +
|
|
853
|
+
` Get-ChildItem -Path $exe.Directory.FullName -Filter '*.dll' | ` +
|
|
854
|
+
` ForEach-Object { Copy-Item -Path $_.FullName -Destination '${cacheDir}' -Force }; ` +
|
|
855
|
+
`}`,
|
|
856
|
+
],
|
|
857
|
+
{ stdio: "pipe", encoding: "utf8" },
|
|
858
|
+
)
|
|
859
|
+
if (r.status !== 0) {
|
|
860
|
+
console.warn(`[supatype] ⚠ PostgREST extraction failed: ${r.stderr?.trim() ?? "unknown error"}. REST API will be unavailable.`)
|
|
861
|
+
return null
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
const r = spawnSync("tar", ["-xJf", archivePath, "-C", cacheDir], { stdio: "pipe" })
|
|
865
|
+
if (r.status !== 0) {
|
|
866
|
+
console.warn("[supatype] ⚠ PostgREST extraction failed. REST API will be unavailable.")
|
|
867
|
+
return null
|
|
428
868
|
}
|
|
429
|
-
await sleep(HEALTH_POLL_MS)
|
|
430
869
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
870
|
+
|
|
871
|
+
if (!existsSync(binPath)) {
|
|
872
|
+
console.warn("[supatype] ⚠ PostgREST binary not found after extraction. REST API will be unavailable.")
|
|
873
|
+
return null
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (platform.os !== "windows") {
|
|
877
|
+
const { chmod } = await import("node:fs/promises")
|
|
878
|
+
await chmod(binPath, 0o755)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
console.log(`[supatype] PostgREST v${version} ready.`)
|
|
882
|
+
return binPath
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function hasLikelyWindowsRuntimeDlls(dir: string): boolean {
|
|
886
|
+
if (process.platform !== "win32") return true
|
|
887
|
+
return (
|
|
888
|
+
existsSync(join(dir, "libpq.dll")) ||
|
|
889
|
+
existsSync(join(dir, "libpq-5.dll")) ||
|
|
890
|
+
existsSync(join(dir, "libssl-3-x64.dll"))
|
|
434
891
|
)
|
|
435
892
|
}
|
|
436
893
|
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
894
|
+
function repairWindowsPostgrestRuntime(cacheDir: string, archivePath: string, binPath: string): boolean {
|
|
895
|
+
const r = spawnSync(
|
|
896
|
+
"powershell.exe",
|
|
897
|
+
[
|
|
898
|
+
"-NoProfile",
|
|
899
|
+
"-Command",
|
|
900
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${cacheDir}' -Force; ` +
|
|
901
|
+
`$exe = Get-ChildItem -Path '${cacheDir}' -Recurse -Filter 'postgrest.exe' | Select-Object -First 1; ` +
|
|
902
|
+
`if ($exe) { ` +
|
|
903
|
+
` Copy-Item -Path $exe.FullName -Destination '${binPath}' -Force; ` +
|
|
904
|
+
` Get-ChildItem -Path $exe.Directory.FullName -Filter '*.dll' | ` +
|
|
905
|
+
` ForEach-Object { Copy-Item -Path $_.FullName -Destination '${cacheDir}' -Force }; ` +
|
|
906
|
+
`}`,
|
|
907
|
+
],
|
|
908
|
+
{ stdio: "pipe", encoding: "utf8" },
|
|
909
|
+
)
|
|
910
|
+
return r.status === 0
|
|
911
|
+
}
|
|
440
912
|
|
|
441
|
-
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
// Local-dev JWT generator (no external dep — pure crypto)
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
442
916
|
|
|
443
|
-
// Initial push on start
|
|
444
|
-
await runPush(cwd)
|
|
445
917
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
919
|
+
// AST adaptation — replace extension-dependent fields with JSONB fallbacks
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
|
|
922
|
+
interface AstField { kind: string; required?: boolean; [k: string]: unknown }
|
|
923
|
+
interface AstModel { name: string; fields?: Record<string, AstField> }
|
|
924
|
+
interface AstSchema { models?: AstModel[] }
|
|
925
|
+
|
|
926
|
+
// Field kinds that require Postgres extensions not available in all builds.
|
|
927
|
+
// Maps kind → { extension name, JSONB fallback AST }
|
|
928
|
+
const EXTENSION_FIELDS: Record<string, { ext: string; fallback: AstField }> = {
|
|
929
|
+
geo: { ext: "PostGIS", fallback: { kind: "json", pgType: "JSONB" } },
|
|
930
|
+
vector: { ext: "pgvector", fallback: { kind: "json", pgType: "JSONB" } },
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function adaptUnsupportedKinds(
|
|
934
|
+
ast: unknown,
|
|
935
|
+
skipKinds: ReadonlySet<string>,
|
|
936
|
+
): { filtered: unknown; adapted: string[] } {
|
|
937
|
+
const adapted: string[] = []
|
|
938
|
+
if (!ast || typeof ast !== "object") return { filtered: ast, adapted }
|
|
939
|
+
const schema = ast as AstSchema
|
|
940
|
+
if (!Array.isArray(schema.models)) return { filtered: ast, adapted }
|
|
941
|
+
|
|
942
|
+
const models = schema.models.map((model) => {
|
|
943
|
+
const fields: Record<string, AstField> = {}
|
|
944
|
+
for (const [name, field] of Object.entries(model.fields ?? {})) {
|
|
945
|
+
const info = skipKinds.has(field.kind) ? EXTENSION_FIELDS[field.kind] : undefined
|
|
946
|
+
if (info) {
|
|
947
|
+
fields[name] = { ...info.fallback, required: field.required ?? false }
|
|
948
|
+
adapted.push(`${model.name}.${name} (${info.ext} → JSONB)`)
|
|
949
|
+
} else {
|
|
950
|
+
fields[name] = field
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return { ...model, fields }
|
|
453
954
|
})
|
|
454
955
|
|
|
455
|
-
|
|
456
|
-
await new Promise<never>(() => undefined)
|
|
956
|
+
return { filtered: { ...schema, models }, adapted }
|
|
457
957
|
}
|
|
458
958
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// .env loader
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
/** Apply GoTrue DDL (auth.users, etc.) before engine push references auth schema. */
|
|
964
|
+
function runGotrueMigrations(
|
|
965
|
+
serverBin: string,
|
|
966
|
+
authDbURL: string,
|
|
967
|
+
jwtSecret: string,
|
|
968
|
+
): void {
|
|
969
|
+
const result = spawnSync(serverBin, ["migrate"], {
|
|
970
|
+
stdio: "pipe",
|
|
971
|
+
encoding: "utf8",
|
|
972
|
+
env: {
|
|
973
|
+
...process.env,
|
|
974
|
+
DATABASE_URL: authDbURL,
|
|
975
|
+
GOTRUE_DB_DRIVER: "postgres",
|
|
976
|
+
GOTRUE_JWT_SECRET: jwtSecret,
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
if (result.status !== 0) {
|
|
980
|
+
const detail = (result.stderr ?? result.stdout ?? "").trim()
|
|
981
|
+
throw new Error(
|
|
982
|
+
`GoTrue migrations failed (exit ${result.status ?? "unknown"})` +
|
|
983
|
+
(detail ? `:\n${detail}` : ""),
|
|
984
|
+
)
|
|
471
985
|
}
|
|
472
|
-
console.log(result.stdout || "Schema up to date.")
|
|
473
986
|
}
|
|
474
987
|
|
|
475
|
-
function
|
|
476
|
-
|
|
988
|
+
function loadDotEnv(cwd: string): Record<string, string> {
|
|
989
|
+
const candidates = [resolve(cwd, ".env"), resolve(cwd, ".env.local")]
|
|
990
|
+
const vars: Record<string, string> = {}
|
|
991
|
+
for (const envPath of candidates) {
|
|
992
|
+
if (!existsSync(envPath)) continue
|
|
993
|
+
for (const line of readFileSync(envPath, "utf8").split("\n")) {
|
|
994
|
+
const trimmed = line.trim()
|
|
995
|
+
if (!trimmed || trimmed.startsWith("#")) continue
|
|
996
|
+
const eq = trimmed.indexOf("=")
|
|
997
|
+
if (eq === -1) continue
|
|
998
|
+
vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return vars
|
|
477
1002
|
}
|