@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.
Files changed (173) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +193 -157
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/api-config-cache.d.ts +34 -0
  5. package/dist/api-config-cache.d.ts.map +1 -0
  6. package/dist/api-config-cache.js +123 -0
  7. package/dist/api-config-cache.js.map +1 -0
  8. package/dist/cache-identity-scope.d.ts +38 -0
  9. package/dist/cache-identity-scope.d.ts.map +1 -0
  10. package/dist/cache-identity-scope.js +70 -0
  11. package/dist/cache-identity-scope.js.map +1 -0
  12. package/dist/cache-provider.d.ts +30 -0
  13. package/dist/cache-provider.d.ts.map +1 -0
  14. package/dist/cache-provider.js +60 -0
  15. package/dist/cache-provider.js.map +1 -0
  16. package/dist/cli-version-embedded.js +1 -1
  17. package/dist/commands/add.js +1 -1
  18. package/dist/commands/add.js.map +1 -1
  19. package/dist/commands/admin.d.ts.map +1 -1
  20. package/dist/commands/admin.js +21 -4
  21. package/dist/commands/admin.js.map +1 -1
  22. package/dist/commands/cache.d.ts +1 -1
  23. package/dist/commands/cache.js +3 -3
  24. package/dist/commands/cache.js.map +1 -1
  25. package/dist/commands/cloud.d.ts.map +1 -1
  26. package/dist/commands/cloud.js +2 -1
  27. package/dist/commands/cloud.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +2 -1
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +61 -8
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/diff.d.ts.map +1 -1
  35. package/dist/commands/diff.js +2 -1
  36. package/dist/commands/diff.js.map +1 -1
  37. package/dist/commands/functions.d.ts.map +1 -1
  38. package/dist/commands/functions.js +6 -2
  39. package/dist/commands/functions.js.map +1 -1
  40. package/dist/commands/init.d.ts.map +1 -1
  41. package/dist/commands/init.js +13 -9
  42. package/dist/commands/init.js.map +1 -1
  43. package/dist/commands/keys.d.ts +35 -0
  44. package/dist/commands/keys.d.ts.map +1 -1
  45. package/dist/commands/keys.js +90 -6
  46. package/dist/commands/keys.js.map +1 -1
  47. package/dist/commands/migrate.d.ts.map +1 -1
  48. package/dist/commands/migrate.js +2 -1
  49. package/dist/commands/migrate.js.map +1 -1
  50. package/dist/commands/push.d.ts.map +1 -1
  51. package/dist/commands/push.js +61 -1
  52. package/dist/commands/push.js.map +1 -1
  53. package/dist/compose-services.d.ts.map +1 -1
  54. package/dist/compose-services.js +2 -1
  55. package/dist/compose-services.js.map +1 -1
  56. package/dist/dev-compose.d.ts +13 -0
  57. package/dist/dev-compose.d.ts.map +1 -1
  58. package/dist/dev-compose.js +119 -16
  59. package/dist/dev-compose.js.map +1 -1
  60. package/dist/engine-floor.d.ts +11 -1
  61. package/dist/engine-floor.d.ts.map +1 -1
  62. package/dist/engine-floor.js +49 -16
  63. package/dist/engine-floor.js.map +1 -1
  64. package/dist/functions-deno-types.d.ts +9 -0
  65. package/dist/functions-deno-types.d.ts.map +1 -1
  66. package/dist/functions-deno-types.js +45 -1
  67. package/dist/functions-deno-types.js.map +1 -1
  68. package/dist/functions-router-gen.d.ts.map +1 -1
  69. package/dist/functions-router-gen.js +113 -81
  70. package/dist/functions-router-gen.js.map +1 -1
  71. package/dist/kong-config.d.ts +1 -1
  72. package/dist/model-cache.d.ts +44 -0
  73. package/dist/model-cache.d.ts.map +1 -0
  74. package/dist/model-cache.js +106 -0
  75. package/dist/model-cache.js.map +1 -0
  76. package/dist/model-hooks.d.ts +5 -0
  77. package/dist/model-hooks.d.ts.map +1 -1
  78. package/dist/model-hooks.js +29 -1
  79. package/dist/model-hooks.js.map +1 -1
  80. package/dist/model-versioning.d.ts +80 -0
  81. package/dist/model-versioning.d.ts.map +1 -0
  82. package/dist/model-versioning.js +140 -0
  83. package/dist/model-versioning.js.map +1 -0
  84. package/dist/postgres-ctl.d.ts +55 -0
  85. package/dist/postgres-ctl.d.ts.map +1 -1
  86. package/dist/postgres-ctl.js +84 -8
  87. package/dist/postgres-ctl.js.map +1 -1
  88. package/dist/preview-config-check.d.ts +21 -0
  89. package/dist/preview-config-check.d.ts.map +1 -0
  90. package/dist/preview-config-check.js +47 -0
  91. package/dist/preview-config-check.js.map +1 -0
  92. package/dist/project-config.d.ts +182 -5
  93. package/dist/project-config.d.ts.map +1 -1
  94. package/dist/project-config.js +125 -8
  95. package/dist/project-config.js.map +1 -1
  96. package/dist/resolve-target.d.ts +9 -0
  97. package/dist/resolve-target.d.ts.map +1 -1
  98. package/dist/resolve-target.js.map +1 -1
  99. package/dist/rest-cache-admin.d.ts.map +1 -1
  100. package/dist/rest-cache-admin.js +6 -2
  101. package/dist/rest-cache-admin.js.map +1 -1
  102. package/dist/schema-ast-v2.d.ts +32 -1
  103. package/dist/schema-ast-v2.d.ts.map +1 -1
  104. package/dist/schema-ast-v2.js +14 -1
  105. package/dist/schema-ast-v2.js.map +1 -1
  106. package/dist/self-host-compose.d.ts +20 -1
  107. package/dist/self-host-compose.d.ts.map +1 -1
  108. package/dist/self-host-compose.js +227 -29
  109. package/dist/self-host-compose.js.map +1 -1
  110. package/dist/studio-admin-roles.d.ts +8 -1
  111. package/dist/studio-admin-roles.d.ts.map +1 -1
  112. package/dist/studio-admin-roles.js +16 -2
  113. package/dist/studio-admin-roles.js.map +1 -1
  114. package/dist/type-extractor.d.ts +7 -0
  115. package/dist/type-extractor.d.ts.map +1 -1
  116. package/dist/type-extractor.js +234 -3
  117. package/dist/type-extractor.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/api-config-cache.ts +142 -0
  120. package/src/cache-identity-scope.ts +92 -0
  121. package/src/cache-provider.ts +61 -0
  122. package/src/cli-version-embedded.ts +1 -1
  123. package/src/commands/add.ts +1 -1
  124. package/src/commands/admin.ts +23 -6
  125. package/src/commands/cache.ts +3 -3
  126. package/src/commands/cloud.ts +2 -1
  127. package/src/commands/deploy.ts +2 -1
  128. package/src/commands/dev.ts +68 -7
  129. package/src/commands/diff.ts +2 -1
  130. package/src/commands/functions.ts +6 -2
  131. package/src/commands/init.ts +13 -9
  132. package/src/commands/keys.ts +112 -7
  133. package/src/commands/migrate.ts +2 -1
  134. package/src/commands/push.ts +68 -1
  135. package/src/compose-services.ts +2 -1
  136. package/src/dev-compose.ts +151 -15
  137. package/src/engine-floor.ts +73 -16
  138. package/src/functions-deno-types.ts +48 -1
  139. package/src/functions-router-gen.ts +113 -81
  140. package/src/kong-config.ts +1 -1
  141. package/src/model-cache.ts +122 -0
  142. package/src/model-hooks.ts +26 -2
  143. package/src/model-versioning.ts +167 -0
  144. package/src/postgres-ctl.ts +103 -9
  145. package/src/preview-config-check.ts +52 -0
  146. package/src/project-config.ts +273 -11
  147. package/src/resolve-target.ts +11 -1
  148. package/src/rest-cache-admin.ts +8 -2
  149. package/src/schema-ast-v2.ts +47 -0
  150. package/src/self-host-compose.ts +250 -29
  151. package/src/studio-admin-roles.ts +16 -2
  152. package/src/type-extractor.ts +281 -2
  153. package/tests/api-config-cache-seed.test.ts +180 -0
  154. package/tests/cache-identity-scope.test.ts +61 -0
  155. package/tests/cache-provider-config.test.ts +132 -0
  156. package/tests/engine-floor.test.ts +43 -0
  157. package/tests/external-database-compose.test.ts +74 -3
  158. package/tests/field-masking-tier.test.ts +3 -3
  159. package/tests/fixtures/identity-scope-corpus.json +42 -0
  160. package/tests/functions-concurrency.test.ts +71 -0
  161. package/tests/init.test.ts +36 -2
  162. package/tests/keys-write.test.ts +148 -0
  163. package/tests/model-cache-declaration.test.ts +229 -0
  164. package/tests/model-cache-manifest.test.ts +99 -0
  165. package/tests/model-hooks.test.ts +55 -0
  166. package/tests/model-versions.test.ts +229 -0
  167. package/tests/native-keyspace.test.ts +130 -0
  168. package/tests/preview-config-check.test.ts +104 -0
  169. package/tests/rest-cache-admin.test.ts +36 -0
  170. package/tests/runtime-contract.test.ts +176 -22
  171. package/tests/studio-admin-roles.test.ts +32 -0
  172. package/tests/type-extractor.test.ts +113 -0
  173. package/tsconfig.tsbuildinfo +1 -1
@@ -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,
@@ -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
  *
@@ -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`), and used
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>`.
@@ -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
- ANON_KEY: anonKey,
291
- SERVICE_ROLE_KEY: serviceRoleKey,
292
- PUBLIC_SUPATYPE_ANON_KEY: anonKey,
293
- VITE_SUPATYPE_ANON_KEY: anonKey,
294
- EXPO_PUBLIC_SUPATYPE_ANON_KEY: anonKey,
295
- PUBLIC_SUPATYPE_URL: apiUrl,
296
- EXPO_PUBLIC_SUPATYPE_URL: apiUrl,
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
- ...(!wantsLocalServer &&
337
- !("SUPATYPE_SERVER_IMAGE" in imagePins) && { remove: ["SUPATYPE_SERVER_IMAGE"] }),
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
+ }
@@ -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
- * Refuse a push whose schema needs helpers this engine does not create.
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 needed = boundsRequiringHelpers(ast)
99
- if (needed.length === 0) return
153
+ for (const requirement of ENGINE_REQUIREMENTS) {
154
+ if (compareVersions(pinnedEngineVersion, requirement.since) >= 0) continue
100
155
 
101
- const shown = needed.slice(0, 3).join(", ")
102
- const more = needed.length > 3 ? `, and ${needed.length - 3} more` : ""
103
- throw new Error(
104
- `This schema declares bounds that need schema-engine ${ENGINE_MIN_FOR_BOUNDS} or newer, ` +
105
- `and this project pins ${pinnedEngineVersion}.\n\n` +
106
- ` Declared on: ${shown}${more}\n\n` +
107
- `Bounds compile to CHECK constraints that call helpers in the _supatype schema, and only ` +
108
- `engine ${ENGINE_MIN_FOR_BOUNDS}+ creates them. Applying this would fail inside Postgres ` +
109
- `with "function _supatype.richtext_text(jsonb) does not exist".\n\n` +
110
- `Raise or remove the pin in supatype.config.ts:\n` +
111
- ` versions: { engine: "${ENGINE_MIN_FOR_BOUNDS}" } // or omit it to track latest`,
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(