@supatype/cli 0.2.1 → 0.3.1
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 +193 -157
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/api-config-cache.d.ts +34 -0
- package/dist/api-config-cache.d.ts.map +1 -0
- package/dist/api-config-cache.js +123 -0
- package/dist/api-config-cache.js.map +1 -0
- package/dist/cache-identity-scope.d.ts +38 -0
- package/dist/cache-identity-scope.d.ts.map +1 -0
- package/dist/cache-identity-scope.js +70 -0
- package/dist/cache-identity-scope.js.map +1 -0
- package/dist/cache-provider.d.ts +30 -0
- package/dist/cache-provider.d.ts.map +1 -0
- package/dist/cache-provider.js +60 -0
- package/dist/cache-provider.js.map +1 -0
- package/dist/cli-version-embedded.js +1 -1
- package/dist/commands/add.js +1 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +21 -4
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/cache.d.ts +1 -1
- package/dist/commands/cache.js +3 -3
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +61 -8
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +2 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +6 -2
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +35 -0
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +90 -6
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +2 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +61 -1
- package/dist/commands/push.js.map +1 -1
- package/dist/compose-services.d.ts.map +1 -1
- package/dist/compose-services.js +2 -1
- package/dist/compose-services.js.map +1 -1
- package/dist/dev-compose.d.ts +13 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +119 -16
- package/dist/dev-compose.js.map +1 -1
- package/dist/engine-floor.d.ts +11 -1
- package/dist/engine-floor.d.ts.map +1 -1
- package/dist/engine-floor.js +49 -16
- package/dist/engine-floor.js.map +1 -1
- package/dist/functions-deno-types.d.ts +9 -0
- package/dist/functions-deno-types.d.ts.map +1 -1
- package/dist/functions-deno-types.js +45 -1
- package/dist/functions-deno-types.js.map +1 -1
- package/dist/functions-router-gen.d.ts.map +1 -1
- package/dist/functions-router-gen.js +113 -81
- package/dist/functions-router-gen.js.map +1 -1
- package/dist/kong-config.d.ts +1 -1
- package/dist/model-cache.d.ts +44 -0
- package/dist/model-cache.d.ts.map +1 -0
- package/dist/model-cache.js +106 -0
- package/dist/model-cache.js.map +1 -0
- package/dist/model-hooks.d.ts +5 -0
- package/dist/model-hooks.d.ts.map +1 -1
- package/dist/model-hooks.js +29 -1
- package/dist/model-hooks.js.map +1 -1
- package/dist/model-versioning.d.ts +80 -0
- package/dist/model-versioning.d.ts.map +1 -0
- package/dist/model-versioning.js +140 -0
- package/dist/model-versioning.js.map +1 -0
- package/dist/postgres-ctl.d.ts +55 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +84 -8
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/preview-config-check.d.ts +21 -0
- package/dist/preview-config-check.d.ts.map +1 -0
- package/dist/preview-config-check.js +47 -0
- package/dist/preview-config-check.js.map +1 -0
- package/dist/project-config.d.ts +182 -5
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +125 -8
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +9 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js.map +1 -1
- package/dist/rest-cache-admin.d.ts.map +1 -1
- package/dist/rest-cache-admin.js +6 -2
- package/dist/rest-cache-admin.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +32 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +14 -1
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/self-host-compose.d.ts +20 -1
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +227 -29
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +8 -1
- package/dist/studio-admin-roles.d.ts.map +1 -1
- package/dist/studio-admin-roles.js +16 -2
- package/dist/studio-admin-roles.js.map +1 -1
- package/dist/type-extractor.d.ts +7 -0
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +234 -3
- package/dist/type-extractor.js.map +1 -1
- package/package.json +2 -2
- package/src/api-config-cache.ts +142 -0
- package/src/cache-identity-scope.ts +92 -0
- package/src/cache-provider.ts +61 -0
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/add.ts +1 -1
- package/src/commands/admin.ts +23 -6
- package/src/commands/cache.ts +3 -3
- package/src/commands/cloud.ts +2 -1
- package/src/commands/deploy.ts +2 -1
- package/src/commands/dev.ts +68 -7
- package/src/commands/diff.ts +2 -1
- package/src/commands/functions.ts +6 -2
- package/src/commands/init.ts +13 -9
- package/src/commands/keys.ts +112 -7
- package/src/commands/migrate.ts +2 -1
- package/src/commands/push.ts +68 -1
- package/src/compose-services.ts +2 -1
- package/src/dev-compose.ts +151 -15
- package/src/engine-floor.ts +73 -16
- package/src/functions-deno-types.ts +48 -1
- package/src/functions-router-gen.ts +113 -81
- package/src/kong-config.ts +1 -1
- package/src/model-cache.ts +122 -0
- package/src/model-hooks.ts +26 -2
- package/src/model-versioning.ts +167 -0
- package/src/postgres-ctl.ts +103 -9
- package/src/preview-config-check.ts +52 -0
- package/src/project-config.ts +273 -11
- package/src/resolve-target.ts +11 -1
- package/src/rest-cache-admin.ts +8 -2
- package/src/schema-ast-v2.ts +47 -0
- package/src/self-host-compose.ts +250 -29
- package/src/studio-admin-roles.ts +16 -2
- package/src/type-extractor.ts +281 -2
- package/tests/api-config-cache-seed.test.ts +180 -0
- package/tests/cache-identity-scope.test.ts +61 -0
- package/tests/cache-provider-config.test.ts +132 -0
- package/tests/engine-floor.test.ts +43 -0
- package/tests/external-database-compose.test.ts +74 -3
- package/tests/field-masking-tier.test.ts +3 -3
- package/tests/fixtures/identity-scope-corpus.json +42 -0
- package/tests/functions-concurrency.test.ts +71 -0
- package/tests/init.test.ts +36 -2
- package/tests/keys-write.test.ts +148 -0
- package/tests/model-cache-declaration.test.ts +229 -0
- package/tests/model-cache-manifest.test.ts +99 -0
- package/tests/model-hooks.test.ts +55 -0
- package/tests/model-versions.test.ts +229 -0
- package/tests/native-keyspace.test.ts +130 -0
- package/tests/preview-config-check.test.ts +104 -0
- package/tests/rest-cache-admin.test.ts +36 -0
- package/tests/runtime-contract.test.ts +176 -22
- package/tests/studio-admin-roles.test.ts +32 -0
- package/tests/type-extractor.test.ts +113 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/migrate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withPublishing } from "../model-versioning.js"
|
|
1
2
|
import type { Command } from "commander"
|
|
2
3
|
import { join } from "node:path"
|
|
3
4
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
@@ -149,7 +150,7 @@ export function registerMigrate(program: Command): void {
|
|
|
149
150
|
const cwd = process.cwd()
|
|
150
151
|
|
|
151
152
|
await ensureEngine()
|
|
152
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
153
|
+
const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
|
|
153
154
|
const result = await engineRequest<{ message?: string }>("/push", {
|
|
154
155
|
ast,
|
|
155
156
|
database_url: connection,
|
package/src/commands/push.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withPublishing } from "../model-versioning.js"
|
|
1
2
|
import type { Command } from "commander"
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
4
|
import { join } from "node:path"
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
validateModelValidators,
|
|
9
10
|
writeHooksModule,
|
|
10
11
|
} from "../model-hooks.js"
|
|
12
|
+
import { syncRowCacheEnv } from "../model-cache.js"
|
|
11
13
|
import { adapterEntry, readHookUpload } from "../hook-upload.js"
|
|
12
14
|
import { checkServiceRoleRoutes, serviceRoleProblemLines } from "../service-role-check.js"
|
|
13
15
|
import { fatalError } from "../ui/fatal.js"
|
|
@@ -19,6 +21,10 @@ import {
|
|
|
19
21
|
} from "../project-config.js"
|
|
20
22
|
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
21
23
|
import { assertEngineSupportsSchema } from "../engine-floor.js"
|
|
24
|
+
import {
|
|
25
|
+
modelsWithUnresolvablePreview,
|
|
26
|
+
unresolvablePreviewMessage,
|
|
27
|
+
} from "../preview-config-check.js"
|
|
22
28
|
import { pinnedVersion } from "../binary-cache.js"
|
|
23
29
|
import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
|
|
24
30
|
import { signJwt } from "../jwt.js"
|
|
@@ -27,6 +33,7 @@ import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
|
|
|
27
33
|
import { ensureFirstAdminUser } from "./admin.js"
|
|
28
34
|
import { withAdminRoles } from "../studio-admin-roles.js"
|
|
29
35
|
import { restoreSystemRelationTargets } from "../restore-system-relation-targets.js"
|
|
36
|
+
import { freeTierCacheNote, seedApiConfigCache } from "../api-config-cache.js"
|
|
30
37
|
import type { SupatypeProjectConfig } from "../project-config.js"
|
|
31
38
|
import {
|
|
32
39
|
resolveTarget,
|
|
@@ -71,10 +78,11 @@ export function registerPush(program: Command): void {
|
|
|
71
78
|
const cwd = process.cwd()
|
|
72
79
|
const config = loadConfig(cwd)
|
|
73
80
|
const pgSchema = schemaPgSchema(cwd)
|
|
74
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
81
|
+
const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
|
|
75
82
|
assertModelHooksResolve(cwd, config, ast)
|
|
76
83
|
assertServiceRoleGrantsResolve(cwd, config)
|
|
77
84
|
assertEngineSupportsSchema(ast, pinnedVersion("engine", config))
|
|
85
|
+
assertPreviewAddressesResolve(config)
|
|
78
86
|
|
|
79
87
|
const linked = loadProjectLink(cwd)
|
|
80
88
|
const useDirect = opts.direct || opts.local || Boolean(opts.connection)
|
|
@@ -175,6 +183,7 @@ async function pushViaTarget(
|
|
|
175
183
|
}
|
|
176
184
|
await generateTypesLocal(ast, config)
|
|
177
185
|
await provisionLocalStorage(ast, config)
|
|
186
|
+
reportCacheSeeding(cwd, ast)
|
|
178
187
|
|
|
179
188
|
// Local Studio only: a cloud/self-host push must not advertise the local
|
|
180
189
|
// gateway URL from config, which may not even be running.
|
|
@@ -186,6 +195,12 @@ async function pushViaTarget(
|
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
info(`Pushed to ${target.mode} (${target.environment}).`)
|
|
198
|
+
// Cloud only. A self-host control plane has tiers too and they mean nothing there, so the same
|
|
199
|
+
// sentence would be telling a self-hosted project to upgrade something it already has.
|
|
200
|
+
if (target.mode === "cloud") {
|
|
201
|
+
const note = freeTierCacheNote((pushResult as { cache?: { tables?: string[]; honoured?: boolean } }).cache)
|
|
202
|
+
if (note) info(note)
|
|
203
|
+
}
|
|
189
204
|
const envUrl = target.link?.environments?.[target.environment]?.apiUrl?.replace(/\/$/, "")
|
|
190
205
|
if (envUrl) {
|
|
191
206
|
plain(`\nProject API: ${envUrl}`)
|
|
@@ -239,6 +254,33 @@ async function deployHooksToTarget(
|
|
|
239
254
|
).then(() => info(`Deployed ${upload.handlers.length} hook(s)`))
|
|
240
255
|
}
|
|
241
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Switch on what the schema newly declares, and say what is still in the way.
|
|
259
|
+
*
|
|
260
|
+
* Two things have to be true for a local `.cache({ server: true })` to be served: the schema
|
|
261
|
+
* declares the table, and the runtime allowlist has it on. A push writes the first, so without
|
|
262
|
+
* this the second is a trip to Studio that nothing in the output asks for — the symptom being a
|
|
263
|
+
* BYPASS header and a declaration that appears to do nothing.
|
|
264
|
+
*
|
|
265
|
+
* Never an error, and never a rewrite of an entry that already exists. See `seedApiConfigCache`.
|
|
266
|
+
*/
|
|
267
|
+
function reportCacheSeeding(cwd: string, ast: unknown): void {
|
|
268
|
+
const result = seedApiConfigCache(cwd, ast)
|
|
269
|
+
if (result === null) return
|
|
270
|
+
|
|
271
|
+
if (result.seeded.length > 0) {
|
|
272
|
+
info(`Server cache enabled for ${result.seeded.join(", ")} in .supatype/api-config.json`)
|
|
273
|
+
}
|
|
274
|
+
if (result.ttlIsOff) {
|
|
275
|
+
// A note rather than a fix. Zero is an off switch someone may have chosen, and a push that
|
|
276
|
+
// turned caching on project-wide would be overriding a decision rather than filling in a blank.
|
|
277
|
+
info(
|
|
278
|
+
`${result.declared.length} table(s) declare a cache, but cache_max_ttl is 0 — nothing is ` +
|
|
279
|
+
`cached until it is set, under API → REST → Settings or in .supatype/api-config.json.`,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
242
284
|
/** The generated adapter, flattened to the name handlers were rewritten to import. */
|
|
243
285
|
function readGeneratedAdapter(hooksDir: string): { name: string; source: string } | null {
|
|
244
286
|
const file = join(hooksDir, "_supatype", "hooks.ts")
|
|
@@ -254,6 +296,16 @@ async function generateTypesLocal(ast: unknown, config: SupatypeProjectConfig):
|
|
|
254
296
|
if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
|
|
255
297
|
// The server watches this file, so a changed hook takes effect without a restart.
|
|
256
298
|
if (syncManifestHooks(cwd, ast)) info("Hook map written to .supatype/manifest.json")
|
|
299
|
+
// The row cache is configured at postmaster start, so this is the one cache setting a push
|
|
300
|
+
// cannot make take effect on its own.
|
|
301
|
+
const rowCache = syncRowCacheEnv(cwd, ast)
|
|
302
|
+
if (rowCache !== null) {
|
|
303
|
+
info(
|
|
304
|
+
`Row cache switched ${rowCache} in .env. Recreate the database container for it to take ` +
|
|
305
|
+
"effect: the image writes pg_keyspace.conf at start, so a running Postgres keeps the " +
|
|
306
|
+
"setting it booted with.",
|
|
307
|
+
)
|
|
308
|
+
}
|
|
257
309
|
|
|
258
310
|
if (!config.output?.types && !config.output?.client) return
|
|
259
311
|
// The CLI writes these, it does not ask the engine to. Passing types_path and client_path and
|
|
@@ -295,6 +347,21 @@ async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig
|
|
|
295
347
|
writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
|
|
296
348
|
}
|
|
297
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Stop the push when a preview address could never open.
|
|
352
|
+
*
|
|
353
|
+
* Refused here rather than left to fail later, because later means in front of whoever was sent the
|
|
354
|
+
* link, not in front of whoever configured it.
|
|
355
|
+
*/
|
|
356
|
+
function assertPreviewAddressesResolve(config: SupatypeProjectConfig): void {
|
|
357
|
+
const models = modelsWithUnresolvablePreview(config)
|
|
358
|
+
if (models.length === 0) return
|
|
359
|
+
fatalError(unresolvablePreviewMessage(models), [
|
|
360
|
+
'Give an absolute URL: urlPattern: "https://example.com/preview/{slug}"',
|
|
361
|
+
'Or set app.mode to "static" or "proxy", if this deployment should serve the app.',
|
|
362
|
+
])
|
|
363
|
+
}
|
|
364
|
+
|
|
298
365
|
/**
|
|
299
366
|
* Stop the push when a declared hook names a function that is not there.
|
|
300
367
|
*
|
package/src/compose-services.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `status` and `logs` each used to carry a hardcoded list of container names.
|
|
5
5
|
* Both lists were wrong: they named containers the compose file does not create
|
|
6
6
|
* (`supatype-postgres`, `supatype-kong`), omitted every service it does create
|
|
7
|
-
* (`server`, `storage`, `functions-worker`, `schema-engine`, `valkey`
|
|
7
|
+
* (`server`, `storage`, `functions-worker`, `schema-engine`, and `valkey` when the
|
|
8
|
+
* stack runs one), and used
|
|
8
9
|
* a naming scheme the compose file never applies, since it sets no
|
|
9
10
|
* `container_name` and Docker therefore names containers
|
|
10
11
|
* `<project>-<service>-<n>`.
|
package/src/dev-compose.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* `supatype dev` when `provider: docker`, full self-host Compose stack (Kong gateway).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { withPublishing } from "./model-versioning.js"
|
|
5
6
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
6
7
|
import { homedir } from "node:os"
|
|
7
8
|
import { dirname, join, resolve } from "node:path"
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
type SupatypeProjectConfig,
|
|
21
22
|
} from "./project-config.js"
|
|
22
23
|
import { syncManifestHooks, writeHooksModule } from "./model-hooks.js"
|
|
24
|
+
import { syncRowCacheEnv } from "./model-cache.js"
|
|
25
|
+
import { anonKeyEnvUpdates } from "./commands/keys.js"
|
|
23
26
|
import { signJwt } from "./jwt.js"
|
|
24
27
|
import { ensureDevDbPort, ensureKongPort } from "./dev-ports.js"
|
|
25
28
|
import { handleComposeProjectRename } from "./compose-rename.js"
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
runDockerCompose,
|
|
35
38
|
schemaEngineImageForPush,
|
|
36
39
|
writeSelfHostCompose,
|
|
40
|
+
selfHostComposePaths,
|
|
37
41
|
type SelfHostComposePaths,
|
|
38
42
|
} from "./self-host-compose.js"
|
|
39
43
|
import type { DockerBrandOptions } from "./docker-runtime.js"
|
|
@@ -287,13 +291,13 @@ export function upsertDevComposeEnv(
|
|
|
287
291
|
...seedMissingLocalSecrets(cwd),
|
|
288
292
|
// Project configuration, seeded not overwritten, see seedMissingDatabaseIdentity.
|
|
289
293
|
...seedMissingDatabaseIdentity(cwd),
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
// Shared with `supatype keys --write`, so a front end's prefix cannot be written by one and
|
|
295
|
+
// forgotten by the other. See `anonKeyEnvUpdates`.
|
|
296
|
+
//
|
|
297
|
+
// This list used to live here as well, and the two had already drifted: `dev` wrote
|
|
298
|
+
// PUBLIC_ and EXPO_PUBLIC_ URLs and no VITE_SUPATYPE_URL, so a Vite app whose `.env` had only
|
|
299
|
+
// ever been touched by `supatype dev` built against an empty string.
|
|
300
|
+
...anonKeyEnvUpdates(anonKey, serviceRoleKey, apiUrl),
|
|
297
301
|
SUPATYPE_KONG_PORT: String(kongPort),
|
|
298
302
|
API_EXTERNAL_URL: apiUrl,
|
|
299
303
|
SITE_URL: apiUrl,
|
|
@@ -328,13 +332,23 @@ export function upsertDevComposeEnv(
|
|
|
328
332
|
(key) => !(wantsLocalServer && key === "SUPATYPE_SERVER_IMAGE"),
|
|
329
333
|
)
|
|
330
334
|
const removeImageKeys = managedImageKeys.filter((key) => !(key in imagePins))
|
|
335
|
+
// The server refuses to start when *any* retired name is in scope, and it reads the project's
|
|
336
|
+
// config files into its environment before checking, so a `.env` this CLI wrote under the old
|
|
337
|
+
// spelling is fatal to a stack that has otherwise been upgraded. Only the key this file used to
|
|
338
|
+
// write is removed: an operator's own GOTRUE_ variable is theirs, and the server's error names it
|
|
339
|
+
// and its replacement precisely enough to fix by hand.
|
|
340
|
+
const retiredKeys = ["GOTRUE_MAILER_AUTOCONFIRM"]
|
|
331
341
|
upsertEnvFile(cwd, updates, {
|
|
332
342
|
removeManaged: removeImageKeys,
|
|
333
343
|
managed: managedImageKeys,
|
|
334
344
|
// A local image left in `.env` after the project stopped asking for one would keep pointing
|
|
335
345
|
// compose at a stale build, and it carries no marker for `removeManaged` to act on.
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
remove: [
|
|
347
|
+
...retiredKeys,
|
|
348
|
+
...(!wantsLocalServer && !("SUPATYPE_SERVER_IMAGE" in imagePins)
|
|
349
|
+
? ["SUPATYPE_SERVER_IMAGE"]
|
|
350
|
+
: []),
|
|
351
|
+
],
|
|
338
352
|
})
|
|
339
353
|
}
|
|
340
354
|
|
|
@@ -583,12 +597,42 @@ async function refreshSchemaArtifacts(
|
|
|
583
597
|
if (syncManifestHooks(cwd, ast)) {
|
|
584
598
|
console.log("[supatype] Hook and validator maps written to .supatype/manifest.json")
|
|
585
599
|
}
|
|
600
|
+
// Written before the stack comes up, so a first run with a row cache declared starts with it on
|
|
601
|
+
// rather than needing a second `supatype dev`.
|
|
602
|
+
const rowCache = syncRowCacheEnv(cwd, ast)
|
|
603
|
+
if (rowCache !== null) {
|
|
604
|
+
console.log(`[supatype] Row cache switched ${rowCache} in .env (pg_keyspace Mode B).`)
|
|
605
|
+
}
|
|
586
606
|
|
|
587
607
|
try {
|
|
588
608
|
await ensureEngine()
|
|
589
609
|
} catch (err) {
|
|
610
|
+
// The host engine is a CDN download and this machine could not complete it. The schema is
|
|
611
|
+
// already applied, so fall back to the engine in compose rather than giving up: without
|
|
612
|
+
// admin-config.json Studio reports "No schema has been pushed yet" on a stack whose schema is
|
|
613
|
+
// entirely live, which reads as a failed push rather than a missing binary.
|
|
614
|
+
const paths = selfHostComposePaths(cwd)
|
|
615
|
+
const composeProject = composeProjectName(config.project.name)
|
|
616
|
+
const wrote = await generateViaComposeEngine(
|
|
617
|
+
paths,
|
|
618
|
+
cwd,
|
|
619
|
+
composeProject,
|
|
620
|
+
config,
|
|
621
|
+
ast,
|
|
622
|
+
adminConfigPath,
|
|
623
|
+
).catch(() => false)
|
|
624
|
+
if (wrote) {
|
|
625
|
+
console.warn(
|
|
626
|
+
`[supatype] Host engine unavailable (${(err as Error).message}); used the in-compose ` +
|
|
627
|
+
"engine instead.",
|
|
628
|
+
)
|
|
629
|
+
return
|
|
630
|
+
}
|
|
590
631
|
console.warn(
|
|
591
|
-
`[supatype] Host engine unavailable, admin/types not refreshed: ${(err as Error).message}
|
|
632
|
+
`[supatype] Host engine unavailable, admin/types not refreshed: ${(err as Error).message}
|
|
633
|
+
` +
|
|
634
|
+
"[supatype] Studio will report no schema until this succeeds, even though the schema is " +
|
|
635
|
+
"applied. Retry with the stack up, or run `supatype push`.",
|
|
592
636
|
)
|
|
593
637
|
return
|
|
594
638
|
}
|
|
@@ -635,7 +679,7 @@ async function runComposeSchemaPush(
|
|
|
635
679
|
schemaPath: string,
|
|
636
680
|
composeProject: string,
|
|
637
681
|
): Promise<void> {
|
|
638
|
-
const ast = loadSchemaAst(schemaPath, cwd)
|
|
682
|
+
const ast = withPublishing(loadSchemaAst(schemaPath, cwd), config)
|
|
639
683
|
const astJson = JSON.stringify(ast)
|
|
640
684
|
|
|
641
685
|
const supatypeDir = join(cwd, ".supatype")
|
|
@@ -868,7 +912,7 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
868
912
|
const brand = { intro: "Schema diff" }
|
|
869
913
|
await ensureDockerDbPublishedForHostEngine(cwd, config, brand)
|
|
870
914
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
871
|
-
const ast = loadSchemaAst(schemaPath, cwd)
|
|
915
|
+
const ast = withPublishing(loadSchemaAst(schemaPath, cwd), config)
|
|
872
916
|
await ensureEngine()
|
|
873
917
|
return engineRequest<DiffResult>("/diff", {
|
|
874
918
|
ast,
|
|
@@ -890,7 +934,7 @@ export async function diffSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
890
934
|
await startComposeDatabase(config, paths, cwd, project, diffBrand)
|
|
891
935
|
|
|
892
936
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
893
|
-
const ast = loadSchemaAst(schemaPath, cwd)
|
|
937
|
+
const ast = withPublishing(loadSchemaAst(schemaPath, cwd), config)
|
|
894
938
|
|
|
895
939
|
const supatypeDir = join(cwd, ".supatype")
|
|
896
940
|
mkdirSync(supatypeDir, { recursive: true })
|
|
@@ -945,7 +989,7 @@ export async function pushSchemaDocker(cwd: string, config: SupatypeProjectConfi
|
|
|
945
989
|
await startComposeDatabase(config, paths, cwd, project, pushBrand)
|
|
946
990
|
|
|
947
991
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
948
|
-
const ast = loadSchemaAst(schemaPath, cwd)
|
|
992
|
+
const ast = withPublishing(loadSchemaAst(schemaPath, cwd), config)
|
|
949
993
|
await runComposeSchemaPush(cwd, config, paths, schemaPath, project)
|
|
950
994
|
|
|
951
995
|
const upGateway = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, {
|
|
@@ -1157,7 +1201,7 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
1157
1201
|
startedAt: new Date().toISOString(),
|
|
1158
1202
|
})
|
|
1159
1203
|
|
|
1160
|
-
const ast = loadSchemaAst(schemaPath, cwd)
|
|
1204
|
+
const ast = withPublishing(loadSchemaAst(schemaPath, cwd), config)
|
|
1161
1205
|
await provisionDockerStorageBuckets(ast, kongPort, serviceRoleKey)
|
|
1162
1206
|
|
|
1163
1207
|
const pidDir = join(homedir(), ".supatype", "projects", config.project.name, "pid")
|
|
@@ -1322,3 +1366,95 @@ function grantAuthSchemaAccess(
|
|
|
1322
1366
|
console.warn("[supatype] Could not grant service_role access to auth.users, Studio relation preview may fail.")
|
|
1323
1367
|
}
|
|
1324
1368
|
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Run one read-only generator in the in-compose schema-engine and return its stdout.
|
|
1372
|
+
*
|
|
1373
|
+
* The same image and the same bind mount the push just used, so if the schema could be applied
|
|
1374
|
+
* this can run. It exists because the host engine is a separate binary fetched from a CDN, and a
|
|
1375
|
+
* machine that cannot fetch it is not a machine that cannot generate: the schema is already
|
|
1376
|
+
* applied, the AST is already on disk, and the container that did it is one `docker compose run`
|
|
1377
|
+
* away.
|
|
1378
|
+
*/
|
|
1379
|
+
async function runComposeEngineGenerator(
|
|
1380
|
+
paths: SelfHostComposePaths,
|
|
1381
|
+
cwd: string,
|
|
1382
|
+
composeProject: string,
|
|
1383
|
+
config: SupatypeProjectConfig,
|
|
1384
|
+
args: readonly string[],
|
|
1385
|
+
): Promise<string | null> {
|
|
1386
|
+
const envFile = resolve(cwd, ".env")
|
|
1387
|
+
const composeArgs = ["compose", "--progress", "quiet"]
|
|
1388
|
+
if (composeProject) composeArgs.push("-p", composeProject)
|
|
1389
|
+
composeArgs.push("--project-directory", cwd)
|
|
1390
|
+
composeArgs.push("-f", paths.composePath)
|
|
1391
|
+
if (existsSync(envFile)) composeArgs.push("--env-file", envFile)
|
|
1392
|
+
composeArgs.push(
|
|
1393
|
+
"--profile",
|
|
1394
|
+
"tools",
|
|
1395
|
+
"run",
|
|
1396
|
+
"--rm",
|
|
1397
|
+
"-T",
|
|
1398
|
+
"schema-engine",
|
|
1399
|
+
...args,
|
|
1400
|
+
"-i",
|
|
1401
|
+
"/project/.supatype/schema.ast.json",
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
const env: NodeJS.ProcessEnv = { ...process.env, COMPOSE_PROGRESS: "quiet" }
|
|
1405
|
+
const engineImage = await schemaEngineImageForPush(config)
|
|
1406
|
+
if (engineImage) env.SUPATYPE_ENGINE_IMAGE = engineImage
|
|
1407
|
+
|
|
1408
|
+
const result = spawnSync("docker", composeArgs, {
|
|
1409
|
+
cwd,
|
|
1410
|
+
encoding: "utf8",
|
|
1411
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1412
|
+
env,
|
|
1413
|
+
})
|
|
1414
|
+
if ((result.status ?? 1) !== 0) return null
|
|
1415
|
+
return String(result.stdout ?? "")
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Write `admin-config.json` and the generated types using the in-compose engine.
|
|
1420
|
+
*
|
|
1421
|
+
* The fallback for a host engine that could not be fetched. Without it, a `supatype dev` that
|
|
1422
|
+
* applied the schema perfectly well still left no `admin-config.json`, and Studio reads that file
|
|
1423
|
+
* to decide whether a schema exists: the stack came up, every table was live, the API served them,
|
|
1424
|
+
* and Studio said **"No schema has been pushed yet."** The warning that preceded it named the
|
|
1425
|
+
* download, not the consequence, so nothing connected the two.
|
|
1426
|
+
*
|
|
1427
|
+
* Returns true when the admin config was written, which is the file Studio actually needs.
|
|
1428
|
+
*/
|
|
1429
|
+
export async function generateViaComposeEngine(
|
|
1430
|
+
paths: SelfHostComposePaths,
|
|
1431
|
+
cwd: string,
|
|
1432
|
+
composeProject: string,
|
|
1433
|
+
config: SupatypeProjectConfig,
|
|
1434
|
+
ast: unknown,
|
|
1435
|
+
adminConfigPath: string,
|
|
1436
|
+
): Promise<boolean> {
|
|
1437
|
+
const typesPath = config.output?.types
|
|
1438
|
+
if (typeof typesPath === "string" && typesPath.trim().length > 0) {
|
|
1439
|
+
const out = await runComposeEngineGenerator(paths, cwd, composeProject, config, ["generate"])
|
|
1440
|
+
const marker = out?.indexOf("// Generated by supatype-engine") ?? -1
|
|
1441
|
+
if (out && out.includes("export type")) {
|
|
1442
|
+
const ts = (marker >= 0 ? out.slice(marker) : out).trimStart()
|
|
1443
|
+
const hostPath = join(cwd, typesPath)
|
|
1444
|
+
mkdirSync(dirname(hostPath), { recursive: true })
|
|
1445
|
+
writeFileSync(hostPath, ts)
|
|
1446
|
+
console.log(`[supatype] Types written to ${typesPath} (in-compose engine).`)
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const adminOut = await runComposeEngineGenerator(paths, cwd, composeProject, config, ["admin"])
|
|
1451
|
+
if (!adminOut) return false
|
|
1452
|
+
const parsed = parseEngineJsonOutput<unknown>(adminOut)
|
|
1453
|
+
if (parsed === null) return false
|
|
1454
|
+
|
|
1455
|
+
const admin = withAdminRoles(parsed, config)
|
|
1456
|
+
restoreSystemRelationTargets(admin, ast)
|
|
1457
|
+
writeFileSync(adminConfigPath, `${JSON.stringify(admin, null, 2)}\n`)
|
|
1458
|
+
console.log("[supatype] Admin config written to .supatype/admin-config.json (in-compose engine).")
|
|
1459
|
+
return true
|
|
1460
|
+
}
|
package/src/engine-floor.ts
CHANGED
|
@@ -27,6 +27,15 @@ import type { ExtractedSchemaAstV2, ModelAstV2 } from "./schema-ast-v2.js"
|
|
|
27
27
|
/** First engine release that creates the `_supatype` constraint helpers (schema-engine v0.2.0). */
|
|
28
28
|
export const ENGINE_MIN_FOR_BOUNDS = "0.2.0"
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* First engine release that emits the drafts and publishing layer (schema-engine v0.3.0).
|
|
32
|
+
*
|
|
33
|
+
* The failure this guards is silent rather than loud, which is why it is worth a check of its own:
|
|
34
|
+
* an older engine does not reject `versions`, it ignores the key. The push succeeds, the model has
|
|
35
|
+
* no snapshot table, and Studio saves drafts into nothing.
|
|
36
|
+
*/
|
|
37
|
+
export const ENGINE_MIN_FOR_VERSIONS = "0.3.0"
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Compare two dotted versions numerically. Returns <0, 0 or >0.
|
|
32
41
|
*
|
|
@@ -80,8 +89,55 @@ export function boundsRequiringHelpers(ast: ExtractedSchemaAstV2): string[] {
|
|
|
80
89
|
return [...fieldsWithBounds(ast.models), ...modelsWithConstraints(ast.models)]
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/** Models declaring `versions`, which needs a whole layer only a new enough engine emits. */
|
|
93
|
+
export function modelsRequiringVersions(ast: ExtractedSchemaAstV2): string[] {
|
|
94
|
+
return ast.models
|
|
95
|
+
.filter((model) => model.options["versions"] !== undefined)
|
|
96
|
+
.map((model) => model.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
/**
|
|
84
|
-
*
|
|
100
|
+
* One thing a schema can declare that an older engine cannot apply.
|
|
101
|
+
*
|
|
102
|
+
* A table rather than a second copy of the check, because there will be a third: every feature that
|
|
103
|
+
* puts something in the database which only the engine creates lands here, and the shape of the
|
|
104
|
+
* refusal should not be rewritten each time.
|
|
105
|
+
*/
|
|
106
|
+
interface EngineRequirement {
|
|
107
|
+
/** First engine release that can apply it. */
|
|
108
|
+
since: string
|
|
109
|
+
/** What in this schema needs it, named so the message can point at it. */
|
|
110
|
+
declaredBy: (ast: ExtractedSchemaAstV2) => string[]
|
|
111
|
+
/** What the author wrote. */
|
|
112
|
+
feature: string
|
|
113
|
+
/** What happens if it is applied anyway, in the words the failure would use. */
|
|
114
|
+
consequence: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ENGINE_REQUIREMENTS: EngineRequirement[] = [
|
|
118
|
+
{
|
|
119
|
+
since: ENGINE_MIN_FOR_BOUNDS,
|
|
120
|
+
declaredBy: boundsRequiringHelpers,
|
|
121
|
+
feature: "bounds",
|
|
122
|
+
consequence:
|
|
123
|
+
`Bounds compile to CHECK constraints that call helpers in the _supatype schema, and only ` +
|
|
124
|
+
`engine ${ENGINE_MIN_FOR_BOUNDS}+ creates them. Applying this would fail inside Postgres ` +
|
|
125
|
+
`with "function _supatype.richtext_text(jsonb) does not exist".`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
since: ENGINE_MIN_FOR_VERSIONS,
|
|
129
|
+
declaredBy: modelsRequiringVersions,
|
|
130
|
+
feature: "`versions`",
|
|
131
|
+
consequence:
|
|
132
|
+
`Only engine ${ENGINE_MIN_FOR_VERSIONS}+ emits the snapshot tables, the draft views and the ` +
|
|
133
|
+
`publish functions. An older engine ignores the declaration and applies the rest, so the ` +
|
|
134
|
+
`push reports success and you get a model with no history, no drafts and an editor that ` +
|
|
135
|
+
`saves into nothing. That silence is the reason this refuses.`,
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Refuse a push whose schema needs something this engine cannot apply.
|
|
85
141
|
*
|
|
86
142
|
* Takes the pin from `versions.engine`, or undefined when unpinned. See the note above on why
|
|
87
143
|
* that is the right source rather than the binary's own `--version`.
|
|
@@ -93,21 +149,22 @@ export function assertEngineSupportsSchema(
|
|
|
93
149
|
// Unpinned resolves to latest, and `local` points at a build whose version the config does not
|
|
94
150
|
// know. Neither can be compared, and neither is the case that breaks.
|
|
95
151
|
if (pinnedEngineVersion === undefined || pinnedEngineVersion === "local") return
|
|
96
|
-
if (compareVersions(pinnedEngineVersion, ENGINE_MIN_FOR_BOUNDS) >= 0) return
|
|
97
152
|
|
|
98
|
-
const
|
|
99
|
-
|
|
153
|
+
for (const requirement of ENGINE_REQUIREMENTS) {
|
|
154
|
+
if (compareVersions(pinnedEngineVersion, requirement.since) >= 0) continue
|
|
100
155
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
const needed = requirement.declaredBy(ast)
|
|
157
|
+
if (needed.length === 0) continue
|
|
158
|
+
|
|
159
|
+
const shown = needed.slice(0, 3).join(", ")
|
|
160
|
+
const more = needed.length > 3 ? `, and ${needed.length - 3} more` : ""
|
|
161
|
+
throw new Error(
|
|
162
|
+
`This schema declares ${requirement.feature}, which needs schema-engine ` +
|
|
163
|
+
`${requirement.since} or newer, and this project pins ${pinnedEngineVersion}.\n\n` +
|
|
164
|
+
` Declared on: ${shown}${more}\n\n` +
|
|
165
|
+
`${requirement.consequence}\n\n` +
|
|
166
|
+
`Raise or remove the pin in supatype.config.ts:\n` +
|
|
167
|
+
` versions: { engine: "${requirement.since}" } // or omit it to track latest`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
113
170
|
}
|
|
@@ -76,6 +76,44 @@ declare namespace Deno {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** `functions/tsconfig.json`: owns function files so the app config can exclude them. */
|
|
79
|
+
/**
|
|
80
|
+
* The context type, as a module a function can import.
|
|
81
|
+
*
|
|
82
|
+
* Not an ambient declaration. `functions/deno.d.ts` is read by tsserver and by the tsconfig in this
|
|
83
|
+
* directory, but `deno check` builds its own type graph and does not look at either, so a function
|
|
84
|
+
* naming an ambient type fails the check the runtime itself would run. An import works in both.
|
|
85
|
+
*/
|
|
86
|
+
export function functionsContextModuleSource(): string {
|
|
87
|
+
return `/**
|
|
88
|
+
* What a function is told about the call it is serving.
|
|
89
|
+
*
|
|
90
|
+
* Handed to the handler as its second argument. Per-invocation values used to be injected into the
|
|
91
|
+
* process environment and read with Deno.env.get, which is global: two concurrent calls saw each
|
|
92
|
+
* other's configuration, including a service-role key granted to one route and not another. The
|
|
93
|
+
* worker therefore held a lock across every handler and ran one invocation at a time.
|
|
94
|
+
*
|
|
95
|
+
* Passing them in removes the global, so functions run concurrently.
|
|
96
|
+
*
|
|
97
|
+
* Regenerated by the CLI. Edits are overwritten.
|
|
98
|
+
*/
|
|
99
|
+
export interface FunctionContext {
|
|
100
|
+
/** Unique to this invocation. Every log line it writes carries the same value. */
|
|
101
|
+
readonly executionId: string
|
|
102
|
+
/** The function or hook route being served. */
|
|
103
|
+
readonly functionName: string
|
|
104
|
+
/** The project's API base URL, reachable from inside the function. */
|
|
105
|
+
readonly url: string
|
|
106
|
+
readonly anonKey: string
|
|
107
|
+
/** Present only when the project granted this route the service role. */
|
|
108
|
+
readonly serviceRoleKey?: string
|
|
109
|
+
readonly dbUrl?: string
|
|
110
|
+
readonly region: string
|
|
111
|
+
/** Values from .env and .env.<function>.local. */
|
|
112
|
+
readonly env: Readonly<Record<string, string>>
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
export function functionsTsConfigSource(): string {
|
|
80
118
|
return `{
|
|
81
119
|
"compilerOptions": {
|
|
@@ -97,6 +135,7 @@ export function functionsTsConfigSource(): string {
|
|
|
97
135
|
|
|
98
136
|
export type FunctionsDenoTypesResult = {
|
|
99
137
|
wroteDenoDts: boolean
|
|
138
|
+
wroteContextModule: boolean
|
|
100
139
|
wroteTsconfig: boolean
|
|
101
140
|
rootExclude: "updated" | "already" | "missing" | "manual"
|
|
102
141
|
hints: string[]
|
|
@@ -121,6 +160,14 @@ export function ensureFunctionsDenoTypes(
|
|
|
121
160
|
wroteDenoDts = true
|
|
122
161
|
}
|
|
123
162
|
|
|
163
|
+
// Always rewritten, unlike the other two: this one is a contract the runtime enforces, so a stale
|
|
164
|
+
// copy is a type error in the user's editor rather than a missing convenience.
|
|
165
|
+
const contextDir = join(functionsDir, "_shared")
|
|
166
|
+
const contextPath = join(contextDir, "context.ts")
|
|
167
|
+
mkdirSync(contextDir, { recursive: true })
|
|
168
|
+
writeFileSync(contextPath, functionsContextModuleSource(), "utf8")
|
|
169
|
+
const wroteContextModule = true
|
|
170
|
+
|
|
124
171
|
let wroteTsconfig = false
|
|
125
172
|
if (!existsSync(tsconfigPath)) {
|
|
126
173
|
writeFileSync(tsconfigPath, functionsTsConfigSource(), "utf8")
|
|
@@ -141,7 +188,7 @@ export function ensureFunctionsDenoTypes(
|
|
|
141
188
|
)
|
|
142
189
|
}
|
|
143
190
|
|
|
144
|
-
return { wroteDenoDts, wroteTsconfig, rootExclude, hints }
|
|
191
|
+
return { wroteDenoDts, wroteContextModule, wroteTsconfig, rootExclude, hints }
|
|
145
192
|
}
|
|
146
193
|
|
|
147
194
|
function ensureRootTsconfigExcludesFunctions(
|