@supatype/cli 0.2.1 → 0.3.0
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 +180 -143
- 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/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/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 +52 -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.map +1 -1
- package/dist/dev-compose.js +18 -7
- 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 +17 -0
- package/dist/model-cache.d.ts.map +1 -0
- package/dist/model-cache.js +45 -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 +181 -27
- 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/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/migrate.ts +2 -1
- package/src/commands/push.ts +57 -1
- package/src/compose-services.ts +2 -1
- package/src/dev-compose.ts +18 -7
- 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 +74 -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 +204 -27
- 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/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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an access rule's outcome varies by *who* is asking.
|
|
3
|
+
*
|
|
4
|
+
* This decides whether a model may declare `cache: { public: true }`. A public cache entry is
|
|
5
|
+
* global — one response served to every caller — so on a table whose read rule varies by caller it
|
|
6
|
+
* serves one user's rows to another.
|
|
7
|
+
*
|
|
8
|
+
* **Row-dependence is not the same question, and getting that wrong in either direction is a bug.**
|
|
9
|
+
* `Lte<"published_at", Now>` varies by row and by time but not by caller, so a shared entry is
|
|
10
|
+
* exactly right for it: that is the case public caching exists to serve. Refusing it would make the
|
|
11
|
+
* feature useless. Allowing `Owner<"user_id">` would make it a data leak.
|
|
12
|
+
*
|
|
13
|
+
* ## This is the second implementation of one rule
|
|
14
|
+
*
|
|
15
|
+
* supatype-server already answers this at runtime, in
|
|
16
|
+
* `internal/studiobootstrap.IsIdentityDependent`, to decide whether a response may be cached under
|
|
17
|
+
* a shared scope. This one answers it at *push* time, where the error can name the model and the
|
|
18
|
+
* rule instead of a request quietly not being cached.
|
|
19
|
+
*
|
|
20
|
+
* Two implementations of one rule drift, and the drift is silent: the push says "shareable" and the
|
|
21
|
+
* runtime says "not", or worse the other way round. `tests/fixtures/identity-scope-corpus.json` is
|
|
22
|
+
* the defence — a shared corpus of rules and expected answers that both sides run against. Change
|
|
23
|
+
* this file and you change that corpus; the Go side reads the same cases.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** A rule node, in the shape the extractor emits and the server parses. */
|
|
27
|
+
export interface AccessRuleNode {
|
|
28
|
+
type?: string
|
|
29
|
+
left?: AccessOperand
|
|
30
|
+
right?: AccessOperand
|
|
31
|
+
operand?: AccessOperand
|
|
32
|
+
rules?: AccessRuleNode[]
|
|
33
|
+
rule?: AccessRuleNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AccessOperand {
|
|
37
|
+
kind?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The operands whose value differs per caller. Columns, literals and the temporal operands
|
|
42
|
+
* (`Now` and friends) are the same for everyone, so they are not here.
|
|
43
|
+
*/
|
|
44
|
+
const IDENTITY_OPERANDS = new Set(["authUid", "authRole", "claim"])
|
|
45
|
+
|
|
46
|
+
function operandIsIdentityDependent(o: AccessOperand | undefined): boolean {
|
|
47
|
+
if (!o) return false
|
|
48
|
+
return IDENTITY_OPERANDS.has(o.kind ?? "")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isIdentityDependent(rule: AccessRuleNode | undefined | null): boolean {
|
|
52
|
+
// No rule at all is not a caller-varying rule. An absent `access.read` means the table's
|
|
53
|
+
// readability is decided elsewhere, and treating that as identity-dependent would refuse public
|
|
54
|
+
// caching on every model that never wrote one.
|
|
55
|
+
if (!rule) return false
|
|
56
|
+
|
|
57
|
+
switch (rule.type) {
|
|
58
|
+
case "public":
|
|
59
|
+
case "private":
|
|
60
|
+
return false
|
|
61
|
+
|
|
62
|
+
// "Any authenticated user" still distinguishes signed-in from anonymous, so a shared entry
|
|
63
|
+
// could serve a member's view to a stranger.
|
|
64
|
+
case "authenticated":
|
|
65
|
+
case "role":
|
|
66
|
+
case "owner":
|
|
67
|
+
return true
|
|
68
|
+
|
|
69
|
+
// Membership is per caller by construction.
|
|
70
|
+
case "in":
|
|
71
|
+
case "exists":
|
|
72
|
+
return true
|
|
73
|
+
|
|
74
|
+
case "compare":
|
|
75
|
+
return operandIsIdentityDependent(rule.left) || operandIsIdentityDependent(rule.right)
|
|
76
|
+
|
|
77
|
+
case "nullCheck":
|
|
78
|
+
return operandIsIdentityDependent(rule.operand)
|
|
79
|
+
|
|
80
|
+
case "any":
|
|
81
|
+
case "all":
|
|
82
|
+
return (rule.rules ?? []).some(isIdentityDependent)
|
|
83
|
+
|
|
84
|
+
case "not":
|
|
85
|
+
return rule.rule ? isIdentityDependent(rule.rule) : false
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
// An unrecognised rule is assumed to depend on identity. Guessing the other way would share
|
|
89
|
+
// a cache entry across callers on the strength of not recognising something.
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which RESP server this stack runs: one decision, in one place.
|
|
3
|
+
*
|
|
4
|
+
* The response cache and Kong's ACME certificates share a RESP server. Since P1 that can be
|
|
5
|
+
* pg_keyspace inside the Postgres container — one stateful service instead of two, no second
|
|
6
|
+
* image to keep patched, and a cache whose memory is accounted for in the database's own
|
|
7
|
+
* reservation. That is now the default rather than the option.
|
|
8
|
+
*
|
|
9
|
+
* **Valkey stays selectable**, and stays the answer for `database.external`: pg_keyspace is loaded
|
|
10
|
+
* through `shared_preload_libraries`, which is a property of a Postgres this stack starts. Against
|
|
11
|
+
* a database Supatype does not manage there is nothing to configure, so the fallback is not a
|
|
12
|
+
* preference — it is the only thing that can run.
|
|
13
|
+
*
|
|
14
|
+
* A resolver rather than a default written into the config type, because the answer depends on
|
|
15
|
+
* more than one field: the same absent `cache.provider` means pg_keyspace beside a managed
|
|
16
|
+
* database and Valkey beside an external one. A default baked into the parser would have to be
|
|
17
|
+
* either wrong for one of them or applied twice, and applied twice is how the compose file and the
|
|
18
|
+
* native path end up disagreeing about which server is running — a cache that silently never hits
|
|
19
|
+
* and, with TLS on, a Kong that cannot store its certificate.
|
|
20
|
+
*/
|
|
21
|
+
import { pinnedVersion } from "./binary-cache.js"
|
|
22
|
+
import type { SupatypeProjectConfig } from "./project-config.js"
|
|
23
|
+
|
|
24
|
+
export type CacheProvider = "valkey" | "pg_keyspace"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The provider this project runs, explicit or resolved.
|
|
28
|
+
*
|
|
29
|
+
* An explicit `cache.provider` is honoured exactly — including `"valkey"`, which is how a project
|
|
30
|
+
* keeps the sidecar it has. `validateProjectConfig` has already refused the one explicit
|
|
31
|
+
* combination that cannot run (pg_keyspace against `database.external`), so this never overrides
|
|
32
|
+
* what someone wrote.
|
|
33
|
+
*
|
|
34
|
+
* The two things that resolve an absent one to Valkey are both cases where this cannot know that a
|
|
35
|
+
* keyspace would work:
|
|
36
|
+
*
|
|
37
|
+
* - **`database.external`**: pg_keyspace is loaded through `shared_preload_libraries`, and there
|
|
38
|
+
* is no container here to set that on.
|
|
39
|
+
* - **A pinned `versions.postgres`**: the keyspace is served by the image, and the entrypoint
|
|
40
|
+
* honours `SUPATYPE_KEYSPACE_ENABLED` only from the release that introduced it. An older image
|
|
41
|
+
* ignores the variable and starts with no RESP listener, which is not an error anywhere — the
|
|
42
|
+
* stack comes up and the cache never hits. A pin says this project's image is fixed, and a
|
|
43
|
+
* default must not assume something about a fixed image it cannot check. Unpinned tracks
|
|
44
|
+
* latest, which carries the toggle by definition.
|
|
45
|
+
*
|
|
46
|
+
* A project on a pin that *does* carry the toggle says `cache: { provider: "pg_keyspace" }` and
|
|
47
|
+
* gets it — which is also the only instruction anyone needs, and is better than a version floor
|
|
48
|
+
* here that would have to be right about a release number in another repository.
|
|
49
|
+
*/
|
|
50
|
+
export function cacheProvider(config: SupatypeProjectConfig): CacheProvider {
|
|
51
|
+
const declared = config.cache?.provider
|
|
52
|
+
if (declared === "valkey" || declared === "pg_keyspace") return declared
|
|
53
|
+
if (config.database?.external) return "valkey"
|
|
54
|
+
if (pinnedVersion("postgres", config) !== undefined) return "valkey"
|
|
55
|
+
return "pg_keyspace"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether the RESP keyspace is served by the Postgres this stack starts. */
|
|
59
|
+
export function keyspaceInPostgres(config: SupatypeProjectConfig): boolean {
|
|
60
|
+
return cacheProvider(config) === "pg_keyspace"
|
|
61
|
+
}
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
// Annotated `string`, not left to inference: once a release stamps a value here, the
|
|
10
10
|
// inferred literal type makes the `!== ""` test in cliPackageVersion() a compile error
|
|
11
11
|
// (TS2367, no overlap), so the build would only fail during a release.
|
|
12
|
-
export const EMBEDDED_CLI_VERSION: string = "0.
|
|
12
|
+
export const EMBEDDED_CLI_VERSION: string = "0.3.0"
|
package/src/commands/add.ts
CHANGED
|
@@ -101,5 +101,5 @@ function printDomainNextSteps(cwd: string, domain: string): void {
|
|
|
101
101
|
`Platform URL: https://${domain}`,
|
|
102
102
|
])
|
|
103
103
|
plain(" App, REST, Auth, Storage, Realtime, Functions, and Studio, one HTTPS domain.")
|
|
104
|
-
plain(" Certificates
|
|
104
|
+
plain(" Certificates outlive restarts — in the keyspace's durable store, or the valkey-data volume.\n")
|
|
105
105
|
}
|
package/src/commands/cache.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* supatype cache: list and clean cached component binaries + REST
|
|
2
|
+
* supatype cache: list and clean cached component binaries + the REST response cache.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Command } from "commander"
|
|
@@ -135,7 +135,7 @@ export function registerCache(program: Command): void {
|
|
|
135
135
|
|
|
136
136
|
const rest = cache
|
|
137
137
|
.command("rest")
|
|
138
|
-
.description("Manage REST API response cache
|
|
138
|
+
.description("Manage the server-side REST API response cache")
|
|
139
139
|
|
|
140
140
|
rest
|
|
141
141
|
.command("list")
|
|
@@ -170,7 +170,7 @@ export function registerCache(program: Command): void {
|
|
|
170
170
|
|
|
171
171
|
rest
|
|
172
172
|
.command("get <key>")
|
|
173
|
-
.description("Show one cache entry (full
|
|
173
|
+
.description("Show one cache entry (full key)")
|
|
174
174
|
.option("--json", "Output JSON")
|
|
175
175
|
.action(async (key: string, opts: { json?: boolean }) => {
|
|
176
176
|
const cwd = process.cwd()
|
package/src/commands/cloud.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withPublishing } from "../model-versioning.js"
|
|
1
2
|
import type { Command } from "commander"
|
|
2
3
|
import { readFileSync, writeFileSync, existsSync } from "node:fs"
|
|
3
4
|
import { resolve } from "node:path"
|
|
@@ -91,7 +92,7 @@ export async function pushSchemaToLinkedProject(
|
|
|
91
92
|
process.exit(1)
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
95
|
+
const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
|
|
95
96
|
info(`Pushing schema to ${target.mode} project ${target.projectRef} (${target.environment})...`)
|
|
96
97
|
|
|
97
98
|
const result = await targetSchemaPush(target, ast, {
|
package/src/commands/deploy.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* supatype deploy logs <version>, show build logs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { withPublishing } from "../model-versioning.js"
|
|
14
15
|
import type { Command } from "commander"
|
|
15
16
|
import { existsSync, readdirSync, statSync, createReadStream, mkdirSync, cpSync } from "node:fs"
|
|
16
17
|
import { join, relative } from "node:path"
|
|
@@ -74,7 +75,7 @@ export function registerDeploy(program: Command): void {
|
|
|
74
75
|
|
|
75
76
|
// Step 1: Schema push (unless --app-only or --skip-build, or already done via cloud.json)
|
|
76
77
|
if (!opts.appOnly && !opts.skipBuild && !schemaDone) {
|
|
77
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
78
|
+
const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
|
|
78
79
|
|
|
79
80
|
if (opts.local) {
|
|
80
81
|
step("Schema Push (local)")
|
package/src/commands/dev.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Deno is not provisioned by the CLI on those paths.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { keyspaceInPostgres } from "../cache-provider.js"
|
|
13
14
|
import type { Command } from "commander"
|
|
14
15
|
import { spawnSync } from "node:child_process"
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from "../project-config.js"
|
|
27
28
|
import { discoverTsFunctionsInDir, writeDevFunctionsRouter } from "../functions-router-gen.js"
|
|
28
29
|
import { fieldMaskingTierFromProject } from "../field-masking-tier.js"
|
|
30
|
+
import { projectHasVersionedModels } from "../model-versioning.js"
|
|
29
31
|
import { signJwt } from "../jwt.js"
|
|
30
32
|
import {
|
|
31
33
|
devAuthenticatorPassword,
|
|
@@ -55,9 +57,13 @@ import { registerDevShutdown } from "../dev-shutdown.js"
|
|
|
55
57
|
import { patchRouteManifest } from "../route-manifest.js"
|
|
56
58
|
import { resolveRealtimeLaunch } from "../realtime-launch.js"
|
|
57
59
|
import { writeAppViteEnv } from "../app-vite-env.js"
|
|
58
|
-
import { ensureValkeySidecar, stopValkeySidecar } from "../valkey-sidecar.js"
|
|
60
|
+
import { ensureValkeySidecar, probeTcp, stopValkeySidecar } from "../valkey-sidecar.js"
|
|
59
61
|
import {
|
|
62
|
+
firstFreePort,
|
|
60
63
|
initdb,
|
|
64
|
+
KEYSPACE_PORT_BASE,
|
|
65
|
+
KEYSPACE_PORT_SPAN,
|
|
66
|
+
nativeKeyspaceLibraryPresent,
|
|
61
67
|
nativeMaskLibraryPresent,
|
|
62
68
|
start as pgStart,
|
|
63
69
|
stop as pgStop,
|
|
@@ -193,13 +199,46 @@ export function registerDev(program: Command): void {
|
|
|
193
199
|
// pgBinDir is set on the native path and used to add DLL search path for
|
|
194
200
|
// PostgREST on Windows (PostgREST links against libpq + SSL from MinGW).
|
|
195
201
|
let pgBinDir: string | null = null
|
|
202
|
+
// The port the native Postgres serves RESP on, or null when this archive
|
|
203
|
+
// has no pg_keyspace and the Valkey sidecar is what the cache talks to.
|
|
204
|
+
let keyspacePort: number | null = null
|
|
196
205
|
|
|
197
206
|
{
|
|
198
207
|
// native: resolve pg bin dir and manage with pg_ctl
|
|
199
208
|
pgBinDir = await resolvePgBinDir(config)
|
|
200
209
|
const dataDir = config.database.data_dir ?? join(stateRoot, "data")
|
|
201
210
|
mkdirSync(dataDir, { recursive: true })
|
|
202
|
-
|
|
211
|
+
// The RESP keyspace, when this archive carries it: one process instead
|
|
212
|
+
// of a Postgres and a Valkey container. The port is chosen rather than
|
|
213
|
+
// fixed because 6379 is the first thing a developer's own Redis or a
|
|
214
|
+
// leftover sidecar takes, and a keyspace that cannot bind its port
|
|
215
|
+
// fails inside the postmaster log where nobody is looking.
|
|
216
|
+
//
|
|
217
|
+
// `cache.provider: "valkey"` is honoured here too, and this is the only place it could
|
|
218
|
+
// be: the native path asks the archive what it carries rather than the config what it
|
|
219
|
+
// wants, so a project that deliberately kept the sidecar would otherwise find Postgres
|
|
220
|
+
// serving RESP anyway and the sidecar never started.
|
|
221
|
+
const wantsPgKeyspace = keyspaceInPostgres(config)
|
|
222
|
+
keyspacePort =
|
|
223
|
+
wantsPgKeyspace && nativeKeyspaceLibraryPresent(pgBinDir)
|
|
224
|
+
? await firstFreePort(KEYSPACE_PORT_BASE, KEYSPACE_PORT_SPAN)
|
|
225
|
+
: null
|
|
226
|
+
if (wantsPgKeyspace && nativeKeyspaceLibraryPresent(pgBinDir) && keyspacePort === null) {
|
|
227
|
+
console.warn(
|
|
228
|
+
`[supatype] ⚠ No free port in ${KEYSPACE_PORT_BASE}-${KEYSPACE_PORT_BASE + KEYSPACE_PORT_SPAN - 1} ` +
|
|
229
|
+
"for the Postgres keyspace — starting without it.",
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const pgOpts = {
|
|
234
|
+
pgBinDir,
|
|
235
|
+
dataDir,
|
|
236
|
+
port: pgPort,
|
|
237
|
+
logPath: join(logsDir, "postgres.log"),
|
|
238
|
+
...(keyspacePort !== null
|
|
239
|
+
? { keyspacePort, keyspaceDatabase: projectName }
|
|
240
|
+
: {}),
|
|
241
|
+
}
|
|
203
242
|
|
|
204
243
|
console.log("[supatype] Initialising Postgres data directory...")
|
|
205
244
|
initdb(pgOpts)
|
|
@@ -399,7 +438,27 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
|
|
|
399
438
|
? "ses"
|
|
400
439
|
: "smtp"
|
|
401
440
|
|
|
402
|
-
|
|
441
|
+
// Postgres serves RESP itself when the archive had pg_keyspace and the
|
|
442
|
+
// port came up. The probe is not ceremony: shared memory limits, a
|
|
443
|
+
// preload ordering mistake or a port taken between the check and the
|
|
444
|
+
// start all leave a Postgres that is running and a keyspace that is not,
|
|
445
|
+
// and the difference is a cache that silently never hits.
|
|
446
|
+
const nativeKeyspaceAddr =
|
|
447
|
+
keyspacePort !== null && (await probeTcp("127.0.0.1", keyspacePort))
|
|
448
|
+
? `127.0.0.1:${keyspacePort}`
|
|
449
|
+
: null
|
|
450
|
+
if (keyspacePort !== null && nativeKeyspaceAddr === null) {
|
|
451
|
+
console.warn(
|
|
452
|
+
`[supatype] ⚠ Postgres has pg_keyspace but is not serving RESP on :${keyspacePort} — ` +
|
|
453
|
+
"see logs/postgres.log. Falling back to the Valkey sidecar.",
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
if (nativeKeyspaceAddr) {
|
|
457
|
+
console.log(`[supatype] Keyspace served by Postgres (${nativeKeyspaceAddr}).`)
|
|
458
|
+
}
|
|
459
|
+
const valkeySidecar = nativeKeyspaceAddr
|
|
460
|
+
? { addr: nativeKeyspaceAddr, containerName: null, started: false }
|
|
461
|
+
: ensureValkeySidecar(projectName)
|
|
403
462
|
|
|
404
463
|
const serverEnv: Record<string, string> = {
|
|
405
464
|
// supatype-server outer layer
|
|
@@ -513,12 +572,14 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticate
|
|
|
513
572
|
// a non-default pg_schema pushed correctly and then answered PGRST106 on every request.
|
|
514
573
|
// Native dev has no masking extension either, so a schema with field rules is served from
|
|
515
574
|
// `api` here too: the case an "is the database external" rule would have got wrong.
|
|
516
|
-
PGRST_DB_SCHEMA: apiSchemaList(
|
|
517
|
-
config,
|
|
575
|
+
PGRST_DB_SCHEMA: apiSchemaList(config, {
|
|
518
576
|
// The installed archive decides: one downloaded before the masking library was bundled
|
|
519
577
|
// has no extension, so its field rules are enforced by views instead.
|
|
520
|
-
fieldMaskingTierFromProject(cwd, config, nativeMaskLibraryPresent(pgBinDir)),
|
|
521
|
-
|
|
578
|
+
tier: fieldMaskingTierFromProject(cwd, config, nativeMaskLibraryPresent(pgBinDir)),
|
|
579
|
+
// A versioned model adds the generated `draft` schema, or `.draft()` would answer
|
|
580
|
+
// PGRST106 against a stack that had migrated the views into place.
|
|
581
|
+
drafts: projectHasVersionedModels(cwd, config),
|
|
582
|
+
}),
|
|
522
583
|
// Parity with self-host, which has always set this. Unqualified names in column defaults
|
|
523
584
|
// (`uuid_generate_v4()`) resolve here, and with a non-public pg_schema the request schema
|
|
524
585
|
// alone does not reach them.
|
package/src/commands/diff.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withPublishing } from "../model-versioning.js"
|
|
1
2
|
import type { Command } from "commander"
|
|
2
3
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
3
4
|
import { resolveRuntimeProvider, schemaPathFromProject } from "../project-config.js"
|
|
@@ -19,7 +20,7 @@ export function registerDiff(program: Command): void {
|
|
|
19
20
|
const pgSchema = schemaPgSchema(cwd)
|
|
20
21
|
|
|
21
22
|
const ast = await withSpinner("Loading schema", async () =>
|
|
22
|
-
loadSchemaAst(schemaPathFromProject(config, cwd), cwd),
|
|
23
|
+
withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config),
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
const linked = loadProjectLink(cwd)
|
|
@@ -141,9 +141,13 @@ function scaffoldFunction(cwd: string, name: string): void {
|
|
|
141
141
|
const indexContent = `// ${name}, Supatype Edge Function
|
|
142
142
|
// Docs: https://supatype.com/docs/edge-functions
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
import type { FunctionContext } from "../_shared/context.ts"
|
|
145
|
+
|
|
146
|
+
export default async function handler(req: Request, ctx: FunctionContext): Promise<Response> {
|
|
145
147
|
const { method } = req
|
|
146
|
-
|
|
148
|
+
// From the context, not the environment. Per-invocation values are passed in, which is what lets
|
|
149
|
+
// the worker run your functions concurrently instead of one at a time.
|
|
150
|
+
const { url } = ctx
|
|
147
151
|
|
|
148
152
|
// Example: read request body for POST requests
|
|
149
153
|
if (method === "POST") {
|
package/src/commands/init.ts
CHANGED
|
@@ -70,7 +70,7 @@ const STORAGE_PROVIDER_OPTIONS: {
|
|
|
70
70
|
label: string
|
|
71
71
|
hint: string
|
|
72
72
|
}[] = [
|
|
73
|
-
{ value: "local", label: "Local", hint: "storage you host yourself (
|
|
73
|
+
{ value: "local", label: "Local", hint: "storage you host yourself (SeaweedFS)" },
|
|
74
74
|
{ value: "s3", label: "S3", hint: "external bucket (AWS S3 or compatible)" },
|
|
75
75
|
]
|
|
76
76
|
|
|
@@ -1164,7 +1164,7 @@ function storageConfigLines(
|
|
|
1164
1164
|
if (storageProduction === "s3" && storageLocal !== "s3") {
|
|
1165
1165
|
lines.push(` // Production storage: external S3 bucket, set production S3_* in .env`)
|
|
1166
1166
|
} else if (storageProduction === "local" && storageLocal === "s3") {
|
|
1167
|
-
lines.push(` // Production storage:
|
|
1167
|
+
lines.push(` // Production storage: SeaweedFS on your server (included in self-host compose)`)
|
|
1168
1168
|
}
|
|
1169
1169
|
return lines
|
|
1170
1170
|
}
|
|
@@ -1305,7 +1305,7 @@ S3_SECRET_KEY=`
|
|
|
1305
1305
|
}
|
|
1306
1306
|
return `${localStorageEnvSection("local")}
|
|
1307
1307
|
|
|
1308
|
-
# Production storage (
|
|
1308
|
+
# Production storage (SeaweedFS on your server)
|
|
1309
1309
|
# Included in the self-host compose stack, no extra configuration needed.`
|
|
1310
1310
|
}
|
|
1311
1311
|
|
|
@@ -1323,8 +1323,8 @@ S3_BUCKET=
|
|
|
1323
1323
|
S3_ACCESS_KEY=
|
|
1324
1324
|
S3_SECRET_KEY=`
|
|
1325
1325
|
}
|
|
1326
|
-
return `# Storage (local development,
|
|
1327
|
-
S3_ENDPOINT=http://localhost:
|
|
1326
|
+
return `# Storage (local development, SeaweedFS)
|
|
1327
|
+
S3_ENDPOINT=http://localhost:8333
|
|
1328
1328
|
S3_ACCESS_KEY=supatype
|
|
1329
1329
|
S3_SECRET_KEY=supatype-secret`
|
|
1330
1330
|
}
|
|
@@ -1338,7 +1338,7 @@ S3_BUCKET=
|
|
|
1338
1338
|
S3_ACCESS_KEY=
|
|
1339
1339
|
S3_SECRET_KEY=`
|
|
1340
1340
|
}
|
|
1341
|
-
return `# Storage (production,
|
|
1341
|
+
return `# Storage (production, SeaweedFS on your server)
|
|
1342
1342
|
# Included in the self-host compose stack, no extra configuration needed.`
|
|
1343
1343
|
}
|
|
1344
1344
|
|
|
@@ -1372,9 +1372,13 @@ function helloFunctionTemplate(): string {
|
|
|
1372
1372
|
return `// hello: Supatype Edge Function
|
|
1373
1373
|
// Docs: https://supatype.com/docs/edge-functions
|
|
1374
1374
|
|
|
1375
|
-
|
|
1375
|
+
import type { FunctionContext } from "../_shared/context.ts"
|
|
1376
|
+
|
|
1377
|
+
export default async function handler(req: Request, ctx: FunctionContext): Promise<Response> {
|
|
1376
1378
|
const { method } = req
|
|
1377
|
-
|
|
1379
|
+
// From the context, not the environment. Per-invocation values are passed in, which is what lets
|
|
1380
|
+
// the worker run your functions concurrently instead of one at a time.
|
|
1381
|
+
const { url } = ctx
|
|
1378
1382
|
|
|
1379
1383
|
if (method === "POST") {
|
|
1380
1384
|
const body = await req.json()
|
|
@@ -1507,7 +1511,7 @@ function printNextSteps(args: {
|
|
|
1507
1511
|
selfHostSteps.push("supatype self-host compose up -d # Kong provisions HTTPS automatically")
|
|
1508
1512
|
selfHostSteps.push(`Your Supatype platform goes live at https://${domain}`)
|
|
1509
1513
|
selfHostSteps.push(
|
|
1510
|
-
"Your app, REST, Auth, Storage, Realtime, Functions, and Studio, all behind one HTTPS domain (
|
|
1514
|
+
"Your app, REST, Auth, Storage, Realtime, Functions, and Studio, all behind one HTTPS domain (certificates outlive restarts)",
|
|
1511
1515
|
)
|
|
1512
1516
|
} else {
|
|
1513
1517
|
selfHostSteps.push(
|
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"
|
|
@@ -19,6 +20,10 @@ import {
|
|
|
19
20
|
} from "../project-config.js"
|
|
20
21
|
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
21
22
|
import { assertEngineSupportsSchema } from "../engine-floor.js"
|
|
23
|
+
import {
|
|
24
|
+
modelsWithUnresolvablePreview,
|
|
25
|
+
unresolvablePreviewMessage,
|
|
26
|
+
} from "../preview-config-check.js"
|
|
22
27
|
import { pinnedVersion } from "../binary-cache.js"
|
|
23
28
|
import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
|
|
24
29
|
import { signJwt } from "../jwt.js"
|
|
@@ -27,6 +32,7 @@ import type { ExtractedSchemaAstV2 } from "../schema-ast-v2.js"
|
|
|
27
32
|
import { ensureFirstAdminUser } from "./admin.js"
|
|
28
33
|
import { withAdminRoles } from "../studio-admin-roles.js"
|
|
29
34
|
import { restoreSystemRelationTargets } from "../restore-system-relation-targets.js"
|
|
35
|
+
import { freeTierCacheNote, seedApiConfigCache } from "../api-config-cache.js"
|
|
30
36
|
import type { SupatypeProjectConfig } from "../project-config.js"
|
|
31
37
|
import {
|
|
32
38
|
resolveTarget,
|
|
@@ -71,10 +77,11 @@ export function registerPush(program: Command): void {
|
|
|
71
77
|
const cwd = process.cwd()
|
|
72
78
|
const config = loadConfig(cwd)
|
|
73
79
|
const pgSchema = schemaPgSchema(cwd)
|
|
74
|
-
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
80
|
+
const ast = withPublishing(loadSchemaAst(schemaPathFromProject(config, cwd), cwd), config)
|
|
75
81
|
assertModelHooksResolve(cwd, config, ast)
|
|
76
82
|
assertServiceRoleGrantsResolve(cwd, config)
|
|
77
83
|
assertEngineSupportsSchema(ast, pinnedVersion("engine", config))
|
|
84
|
+
assertPreviewAddressesResolve(config)
|
|
78
85
|
|
|
79
86
|
const linked = loadProjectLink(cwd)
|
|
80
87
|
const useDirect = opts.direct || opts.local || Boolean(opts.connection)
|
|
@@ -175,6 +182,7 @@ async function pushViaTarget(
|
|
|
175
182
|
}
|
|
176
183
|
await generateTypesLocal(ast, config)
|
|
177
184
|
await provisionLocalStorage(ast, config)
|
|
185
|
+
reportCacheSeeding(cwd, ast)
|
|
178
186
|
|
|
179
187
|
// Local Studio only: a cloud/self-host push must not advertise the local
|
|
180
188
|
// gateway URL from config, which may not even be running.
|
|
@@ -186,6 +194,12 @@ async function pushViaTarget(
|
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
info(`Pushed to ${target.mode} (${target.environment}).`)
|
|
197
|
+
// Cloud only. A self-host control plane has tiers too and they mean nothing there, so the same
|
|
198
|
+
// sentence would be telling a self-hosted project to upgrade something it already has.
|
|
199
|
+
if (target.mode === "cloud") {
|
|
200
|
+
const note = freeTierCacheNote((pushResult as { cache?: { tables?: string[]; honoured?: boolean } }).cache)
|
|
201
|
+
if (note) info(note)
|
|
202
|
+
}
|
|
189
203
|
const envUrl = target.link?.environments?.[target.environment]?.apiUrl?.replace(/\/$/, "")
|
|
190
204
|
if (envUrl) {
|
|
191
205
|
plain(`\nProject API: ${envUrl}`)
|
|
@@ -239,6 +253,33 @@ async function deployHooksToTarget(
|
|
|
239
253
|
).then(() => info(`Deployed ${upload.handlers.length} hook(s)`))
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Switch on what the schema newly declares, and say what is still in the way.
|
|
258
|
+
*
|
|
259
|
+
* Two things have to be true for a local `.cache({ server: true })` to be served: the schema
|
|
260
|
+
* declares the table, and the runtime allowlist has it on. A push writes the first, so without
|
|
261
|
+
* this the second is a trip to Studio that nothing in the output asks for — the symptom being a
|
|
262
|
+
* BYPASS header and a declaration that appears to do nothing.
|
|
263
|
+
*
|
|
264
|
+
* Never an error, and never a rewrite of an entry that already exists. See `seedApiConfigCache`.
|
|
265
|
+
*/
|
|
266
|
+
function reportCacheSeeding(cwd: string, ast: unknown): void {
|
|
267
|
+
const result = seedApiConfigCache(cwd, ast)
|
|
268
|
+
if (result === null) return
|
|
269
|
+
|
|
270
|
+
if (result.seeded.length > 0) {
|
|
271
|
+
info(`Server cache enabled for ${result.seeded.join(", ")} in .supatype/api-config.json`)
|
|
272
|
+
}
|
|
273
|
+
if (result.ttlIsOff) {
|
|
274
|
+
// A note rather than a fix. Zero is an off switch someone may have chosen, and a push that
|
|
275
|
+
// turned caching on project-wide would be overriding a decision rather than filling in a blank.
|
|
276
|
+
info(
|
|
277
|
+
`${result.declared.length} table(s) declare a cache, but cache_max_ttl is 0 — nothing is ` +
|
|
278
|
+
`cached until it is set, under API → REST → Settings or in .supatype/api-config.json.`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
242
283
|
/** The generated adapter, flattened to the name handlers were rewritten to import. */
|
|
243
284
|
function readGeneratedAdapter(hooksDir: string): { name: string; source: string } | null {
|
|
244
285
|
const file = join(hooksDir, "_supatype", "hooks.ts")
|
|
@@ -295,6 +336,21 @@ async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig
|
|
|
295
336
|
writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
|
|
296
337
|
}
|
|
297
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Stop the push when a preview address could never open.
|
|
341
|
+
*
|
|
342
|
+
* Refused here rather than left to fail later, because later means in front of whoever was sent the
|
|
343
|
+
* link, not in front of whoever configured it.
|
|
344
|
+
*/
|
|
345
|
+
function assertPreviewAddressesResolve(config: SupatypeProjectConfig): void {
|
|
346
|
+
const models = modelsWithUnresolvablePreview(config)
|
|
347
|
+
if (models.length === 0) return
|
|
348
|
+
fatalError(unresolvablePreviewMessage(models), [
|
|
349
|
+
'Give an absolute URL: urlPattern: "https://example.com/preview/{slug}"',
|
|
350
|
+
'Or set app.mode to "static" or "proxy", if this deployment should serve the app.',
|
|
351
|
+
])
|
|
352
|
+
}
|
|
353
|
+
|
|
298
354
|
/**
|
|
299
355
|
* Stop the push when a declared hook names a function that is not there.
|
|
300
356
|
*
|
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>`.
|