@supatype/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +144 -94
- package/.turbo/turbo-typecheck.log +1 -1
- package/deno.d.ts +52 -0
- package/dist/augmentation-generator.d.ts +22 -0
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +18 -4
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/binary-cache.d.ts +14 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +33 -13
- package/dist/binary-cache.js.map +1 -1
- package/dist/cache-clean.d.ts +15 -0
- package/dist/cache-clean.d.ts.map +1 -0
- package/dist/cache-clean.js +76 -0
- package/dist/cache-clean.js.map +1 -0
- package/dist/cache-pins.d.ts +51 -0
- package/dist/cache-pins.d.ts.map +1 -0
- package/dist/cache-pins.js +184 -0
- package/dist/cache-pins.js.map +1 -0
- package/dist/cli-install-method.d.ts +19 -0
- package/dist/cli-install-method.d.ts.map +1 -0
- package/dist/cli-install-method.js +60 -0
- package/dist/cli-install-method.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-credentials.d.ts +34 -0
- package/dist/cloud-credentials.d.ts.map +1 -0
- package/dist/cloud-credentials.js +139 -0
- package/dist/cloud-credentials.js.map +1 -0
- package/dist/commands/admin.d.ts +11 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +121 -19
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/cache.d.ts.map +1 -1
- package/dist/commands/cache.js +67 -71
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +32 -8
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +147 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +50 -10
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/functions.d.ts +7 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +40 -6
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/hooks.d.ts +12 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +118 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +41 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/internal.d.ts +6 -0
- package/dist/commands/internal.d.ts.map +1 -0
- package/dist/commands/internal.js +22 -0
- package/dist/commands/internal.js.map +1 -0
- package/dist/commands/link-helpers.d.ts.map +1 -1
- package/dist/commands/link-helpers.js +8 -2
- package/dist/commands/link-helpers.js.map +1 -1
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +110 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +110 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/self-update.d.ts +7 -0
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +18 -11
- package/dist/commands/self-update.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -3
- package/dist/config.js.map +1 -1
- package/dist/db-preflight.d.ts +78 -0
- package/dist/db-preflight.d.ts.map +1 -0
- package/dist/db-preflight.js +452 -0
- package/dist/db-preflight.js.map +1 -0
- package/dist/dev-compose.d.ts +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +127 -72
- package/dist/dev-compose.js.map +1 -1
- package/dist/env-file.d.ts +40 -2
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +132 -11
- package/dist/env-file.js.map +1 -1
- package/dist/field-masking-tier.d.ts +50 -0
- package/dist/field-masking-tier.d.ts.map +1 -0
- package/dist/field-masking-tier.js +91 -0
- package/dist/field-masking-tier.js.map +1 -0
- package/dist/functions-deno-types.d.ts +24 -0
- package/dist/functions-deno-types.d.ts.map +1 -0
- package/dist/functions-deno-types.js +155 -0
- package/dist/functions-deno-types.js.map +1 -0
- package/dist/hook-upload.d.ts +30 -0
- package/dist/hook-upload.d.ts.map +1 -0
- package/dist/hook-upload.js +86 -0
- package/dist/hook-upload.js.map +1 -0
- package/dist/hooks-generator.d.ts +8 -0
- package/dist/hooks-generator.d.ts.map +1 -0
- package/dist/hooks-generator.js +332 -0
- package/dist/hooks-generator.js.map +1 -0
- package/dist/link.d.ts +4 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +2 -0
- package/dist/link.js.map +1 -1
- package/dist/local-secrets.d.ts +71 -0
- package/dist/local-secrets.d.ts.map +1 -0
- package/dist/local-secrets.js +100 -0
- package/dist/local-secrets.js.map +1 -0
- package/dist/model-hooks.d.ts +73 -0
- package/dist/model-hooks.d.ts.map +1 -0
- package/dist/model-hooks.js +211 -0
- package/dist/model-hooks.js.map +1 -0
- package/dist/postgres-ctl.d.ts +11 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +25 -2
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +121 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +150 -1
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +4 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js +69 -73
- package/dist/resolve-target.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +2 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +4 -2
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/schema-push-lock.d.ts.map +1 -1
- package/dist/schema-push-lock.js +43 -16
- package/dist/schema-push-lock.js.map +1 -1
- package/dist/self-host-compose.d.ts +24 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +301 -69
- package/dist/self-host-compose.js.map +1 -1
- package/dist/service-role-check.d.ts +21 -0
- package/dist/service-role-check.d.ts.map +1 -0
- package/dist/service-role-check.js +85 -0
- package/dist/service-role-check.js.map +1 -0
- package/dist/target-client.d.ts +17 -0
- package/dist/target-client.d.ts.map +1 -1
- package/dist/target-client.js +69 -17
- package/dist/target-client.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +736 -32
- package/dist/type-extractor.js.map +1 -1
- package/dist/ui/runtime/command-chrome.js +1 -1
- package/dist/ui/runtime/command-chrome.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.js +3 -1
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
- package/package.json +4 -1
- package/scripts/generate-homebrew-formula.mjs +121 -0
- package/src/augmentation-generator.ts +18 -5
- package/src/binary-cache.ts +47 -15
- package/src/cache-clean.ts +81 -0
- package/src/cache-pins.ts +243 -0
- package/src/cli-install-method.ts +67 -0
- package/src/cli.ts +6 -0
- package/src/cloud-credentials.ts +183 -0
- package/src/commands/admin.ts +140 -25
- package/src/commands/cache.ts +90 -75
- package/src/commands/cloud.ts +32 -8
- package/src/commands/db.ts +162 -1
- package/src/commands/dev.ts +57 -8
- package/src/commands/doctor.ts +69 -3
- package/src/commands/functions.ts +42 -5
- package/src/commands/generate.ts +5 -1
- package/src/commands/hooks.ts +138 -0
- package/src/commands/init.ts +42 -8
- package/src/commands/internal.ts +23 -0
- package/src/commands/link-helpers.ts +9 -2
- package/src/commands/login.ts +127 -0
- package/src/commands/push.ts +124 -7
- package/src/commands/self-update.ts +85 -13
- package/src/config.ts +22 -3
- package/src/db-preflight.ts +575 -0
- package/src/dev-compose.ts +142 -73
- package/src/env-file.ts +157 -11
- package/src/field-masking-tier.ts +107 -0
- package/src/functions-deno-types.ts +188 -0
- package/src/hook-upload.ts +107 -0
- package/src/hooks-generator.ts +352 -0
- package/src/link.ts +7 -0
- package/src/local-secrets.ts +104 -0
- package/src/model-hooks.ts +260 -0
- package/src/postgres-ctl.ts +29 -2
- package/src/project-config.ts +236 -2
- package/src/resolve-target.ts +113 -68
- package/src/schema-ast-v2.ts +5 -2
- package/src/schema-push-lock.ts +56 -17
- package/src/self-host-compose.ts +344 -68
- package/src/service-role-check.ts +111 -0
- package/src/target-client.ts +105 -18
- package/src/type-extractor.ts +883 -28
- package/src/ui/runtime/command-chrome.ts +1 -1
- package/src/ui/runtime/stdin-after-ink.ts +3 -1
- package/tests/access-composition.test.ts +667 -0
- package/tests/admin-studio-membership.test.ts +121 -0
- package/tests/api-schemas.test.ts +83 -0
- package/tests/archive-name-alias.test.ts +42 -0
- package/tests/augmentation-generator.test.ts +28 -0
- package/tests/cache-clean.test.ts +206 -0
- package/tests/cli-help.test.ts +3 -0
- package/tests/cli-install-method.test.ts +66 -0
- package/tests/command-chrome-coverage.test.ts +31 -26
- package/tests/command-chrome-wrap.test.ts +1 -0
- package/tests/db-preflight.test.ts +72 -0
- package/tests/doctor-render.test.ts +62 -0
- package/tests/doctor-service-role.test.ts +55 -0
- package/tests/env-file.test.ts +121 -0
- package/tests/external-database-compose.test.ts +184 -0
- package/tests/external-database-config.test.ts +154 -0
- package/tests/field-masking-tier.test.ts +184 -0
- package/tests/functions-deno-types.test.ts +197 -0
- package/tests/homebrew-formula.test.ts +58 -0
- package/tests/hook-upload.test.ts +125 -0
- package/tests/hooks-generator.test.ts +212 -0
- package/tests/init.test.ts +3 -0
- package/tests/local-secrets.test.ts +92 -0
- package/tests/model-hooks.test.ts +322 -0
- package/tests/runtime-contract.test.ts +113 -1
- package/tests/service-role-check.test.ts +109 -0
- package/tests/service-role-routes.test.ts +51 -0
- package/tests/target-client-refresh.test.ts +94 -0
- package/tests/type-extractor.test.ts +187 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tests/engine-distribution.test.ts +0 -418
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `supatype hooks` — scaffold and inspect model hooks.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `supatype functions` because the two are different things wearing the same runtime.
|
|
5
|
+
* A function is a **public endpoint**: anyone holding the anon key can invoke it. A hook is
|
|
6
|
+
* **procedural** — only the API server calls it, around a write, and the gateway refuses the route
|
|
7
|
+
* from outside. Keeping them in separate directories is what makes that boundary structural instead
|
|
8
|
+
* of a list somebody has to maintain.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from "commander"
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
12
|
+
import { join, relative } from "node:path"
|
|
13
|
+
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
14
|
+
import { hooksPathFromProject, schemaPathFromProject } from "../project-config.js"
|
|
15
|
+
import { declaredHooks, hooksReport } from "../model-hooks.js"
|
|
16
|
+
import { error, info, plain } from "../ui/messages.js"
|
|
17
|
+
|
|
18
|
+
export function registerHooks(program: Command): void {
|
|
19
|
+
const hooksCmd = program
|
|
20
|
+
.command("hooks")
|
|
21
|
+
.description("Manage model hooks (procedural handlers the API runs around a write)")
|
|
22
|
+
|
|
23
|
+
hooksCmd
|
|
24
|
+
.command("new <name>")
|
|
25
|
+
.description("Scaffold a hook handler")
|
|
26
|
+
.action((name: string) => {
|
|
27
|
+
scaffoldHook(process.cwd(), name)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
hooksCmd
|
|
31
|
+
.command("list")
|
|
32
|
+
.description("Show the hooks the schema declares and whether each one can run")
|
|
33
|
+
.action(() => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
const config = loadConfig(cwd)
|
|
36
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
37
|
+
const report = hooksReport(cwd, hooksPathFromProject(config, cwd), ast)
|
|
38
|
+
|
|
39
|
+
if (report.declared.length === 0) {
|
|
40
|
+
info("No model declares a hook.")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
for (const hook of report.declared) {
|
|
44
|
+
const broken = report.missing.some((m) => m.model === hook.model && m.event === hook.event)
|
|
45
|
+
plain(` ${broken ? "✗" : "•"} ${hook.model}.${hook.event} → ${hook.function}`)
|
|
46
|
+
}
|
|
47
|
+
if (report.missing.length > 0) {
|
|
48
|
+
plain("\n ✗ names a handler that does not exist, so that hook never fires.")
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scaffoldHook(cwd: string, name: string): void {
|
|
54
|
+
const config = loadConfig(cwd)
|
|
55
|
+
const hooksDir = hooksPathFromProject(config, cwd)
|
|
56
|
+
const dir = join(hooksDir, name)
|
|
57
|
+
|
|
58
|
+
if (existsSync(dir)) {
|
|
59
|
+
error(`Hook "${name}" already exists at ${relative(cwd, dir)}`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
64
|
+
const declared = declaredHooks(ast).filter((hook) => hook.function === name)
|
|
65
|
+
|
|
66
|
+
mkdirSync(dir, { recursive: true })
|
|
67
|
+
writeFileSync(join(dir, "index.ts"), handlerTemplate(name, declared), "utf8")
|
|
68
|
+
|
|
69
|
+
info(`Created hook: ${relative(cwd, join(dir, "index.ts"))}`)
|
|
70
|
+
if (declared.length === 0) {
|
|
71
|
+
// Scaffolding a handler nothing calls is easy to do and impossible to notice, since a hook that
|
|
72
|
+
// is never referenced simply never runs.
|
|
73
|
+
plain("")
|
|
74
|
+
plain(" No model references it yet. Add it to a model's meta:")
|
|
75
|
+
plain(` hooks: { beforeChange: "${name}" }`)
|
|
76
|
+
plain(" then run `supatype push` to generate its types and register it.")
|
|
77
|
+
} else {
|
|
78
|
+
plain("")
|
|
79
|
+
for (const hook of declared) {
|
|
80
|
+
plain(` Wired to ${hook.model}.${hook.event}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The scaffold deliberately imports from `../_supatype/hooks.ts`, which `push` generates.
|
|
87
|
+
*
|
|
88
|
+
* Before the first push that file does not exist, so the handler will not compile — which is the
|
|
89
|
+
* honest state: the types come from the schema, and a handler typed against a model that has not been
|
|
90
|
+
* pushed is a handler typed against a guess.
|
|
91
|
+
*/
|
|
92
|
+
function handlerTemplate(name: string, declared: { model: string; event: string }[]): string {
|
|
93
|
+
const event = declared[0]?.event ?? "beforeChange"
|
|
94
|
+
const model = declared[0]?.model ?? "YourModel"
|
|
95
|
+
|
|
96
|
+
if (event.startsWith("after")) {
|
|
97
|
+
return `// ${name} — model hook (${event})
|
|
98
|
+
//
|
|
99
|
+
// Runs after the write has succeeded. It cannot change or undo it: a rejection here would be a
|
|
100
|
+
// rejection of something that already happened. Failures are logged, not surfaced to the caller.
|
|
101
|
+
|
|
102
|
+
import { hook, type AfterChange } from "../_supatype/hooks.ts"
|
|
103
|
+
|
|
104
|
+
const handler: AfterChange<"your_table"> = async (ctx) => {
|
|
105
|
+
console.log(\`\${ctx.table} \${ctx.operation} by \${ctx.user?.sub ?? "anon"}\`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default hook(handler)
|
|
109
|
+
`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `// ${name} — model hook (${event}) for ${model}
|
|
113
|
+
//
|
|
114
|
+
// Runs before the write reaches Postgres, so it can reject it or rewrite the body.
|
|
115
|
+
//
|
|
116
|
+
// Not a security boundary: hooks fire for writes through the API, and direct SQL, seeds and anything
|
|
117
|
+
// holding the service role bypass them. Invariants belong in the model's access rules or a CHECK.
|
|
118
|
+
|
|
119
|
+
import { hook, type BeforeChange } from "../_supatype/hooks.ts"
|
|
120
|
+
|
|
121
|
+
const handler: BeforeChange<"your_table"> = async (ctx) => {
|
|
122
|
+
if (ctx.operation === "insert") {
|
|
123
|
+
// \`ctx.rows\` is the submitted rows, typed from the model.
|
|
124
|
+
return { rows: ctx.rows }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// \`ctx.patch\` is the submitted partial; \`ctx.previous()\` reads the rows about to change.
|
|
128
|
+
// Those come back as stored — unmasked — so do not echo them into a rejection message.
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
// Reject with a message the caller sees, and a status of your choosing:
|
|
132
|
+
// return { reject: "A post needs a title" }
|
|
133
|
+
// return { reject: { message: "Already exists", status: 409, code: "duplicate" } }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default hook(handler)
|
|
137
|
+
`
|
|
138
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Command } from "commander"
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { resolve, join, basename } from "node:path"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
|
+
import { randomBytes } from "node:crypto"
|
|
5
6
|
import { p, runClackFlow } from "../ui/clack.js"
|
|
6
7
|
import { ensureNotCancelled } from "../ui/prompts.js"
|
|
7
8
|
import { generateAndWriteKeys } from "./keys.js"
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
ensureComponentBinaries,
|
|
14
15
|
reportComponentBinaryFailures,
|
|
15
16
|
} from "../ensure-component-binaries.js"
|
|
17
|
+
import { ensureFunctionsDenoTypes } from "../functions-deno-types.js"
|
|
16
18
|
import { mergeSupatypePackageJson } from "../init-package-json.js"
|
|
17
19
|
import { cliPackageVersion } from "../cli-package-version.js"
|
|
18
20
|
import {
|
|
@@ -792,6 +794,12 @@ function scaffoldHelloFunction(
|
|
|
792
794
|
if (!existsSync(join(dir, "functions/.env.local"))) {
|
|
793
795
|
write("functions/.env.local", functionsEnvLocalTemplate())
|
|
794
796
|
}
|
|
797
|
+
const denoTypes = ensureFunctionsDenoTypes(dir, join(dir, "functions"))
|
|
798
|
+
if (denoTypes.wroteDenoDts) file("created", "functions/deno.d.ts")
|
|
799
|
+
if (denoTypes.wroteTsconfig) file("created", "functions/tsconfig.json")
|
|
800
|
+
if (denoTypes.rootExclude === "updated") {
|
|
801
|
+
file("updated", 'tsconfig.json (exclude "functions")')
|
|
802
|
+
}
|
|
795
803
|
}
|
|
796
804
|
|
|
797
805
|
// ─── Templates ───────────────────────────────────────────────────────────────
|
|
@@ -1182,13 +1190,28 @@ export type SiteSettings = Model<{
|
|
|
1182
1190
|
|
|
1183
1191
|
function envTemplate(opts: ScaffoldOptions): string {
|
|
1184
1192
|
const sections: string[] = []
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1193
|
+
// Generated, not constant. These reach a server: the previous defaults meant a self-host
|
|
1194
|
+
// deployment ran with a password of `postgres` and a JWT secret published in this repo —
|
|
1195
|
+
// and anyone holding that secret can mint a token naming any Postgres role. Safe to generate
|
|
1196
|
+
// only because `supatype dev` no longer overwrites them on every run (see local-secrets.ts).
|
|
1197
|
+
const postgresPassword = randomBytes(24).toString("base64url")
|
|
1189
1198
|
|
|
1190
|
-
sections.push(
|
|
1191
|
-
|
|
1199
|
+
sections.push(`DATABASE_URL=postgresql://supatype_admin:${postgresPassword}@localhost:5432/${opts.projectName}
|
|
1200
|
+
POSTGRES_USER=supatype_admin
|
|
1201
|
+
POSTGRES_PASSWORD=${postgresPassword}
|
|
1202
|
+
POSTGRES_DB=${opts.projectName}
|
|
1203
|
+
|
|
1204
|
+
# PostgREST's own database credential. It connects as \`authenticator\`, a role that holds no
|
|
1205
|
+
# privileges and can only switch to anon/authenticated/service_role — so a request whose JWT
|
|
1206
|
+
# names any other role is refused by Postgres rather than honoured.
|
|
1207
|
+
#
|
|
1208
|
+
# Deliberately NOT the same as POSTGRES_PASSWORD: that one is yours, for direct SQL access, and
|
|
1209
|
+
# rotating it should never take the REST API down with it.
|
|
1210
|
+
AUTHENTICATOR_PASSWORD=${randomBytes(24).toString("base64url")}`)
|
|
1211
|
+
|
|
1212
|
+
sections.push(`# JWT — ANON_KEY and SERVICE_ROLE_KEY are derived from this secret.
|
|
1213
|
+
# Changing it invalidates every issued token; re-run \`supatype keys\` afterwards.
|
|
1214
|
+
JWT_SECRET=${randomBytes(48).toString("base64url")}
|
|
1192
1215
|
ANON_KEY=
|
|
1193
1216
|
SERVICE_ROLE_KEY=`)
|
|
1194
1217
|
|
|
@@ -1340,16 +1363,17 @@ function helloFunctionTemplate(): string {
|
|
|
1340
1363
|
|
|
1341
1364
|
export default async function handler(req: Request): Promise<Response> {
|
|
1342
1365
|
const { method } = req
|
|
1366
|
+
const url = Deno.env.get("SUPATYPE_URL")
|
|
1343
1367
|
|
|
1344
1368
|
if (method === "POST") {
|
|
1345
1369
|
const body = await req.json()
|
|
1346
|
-
return new Response(JSON.stringify({ message: "Hello from hello!", received: body }), {
|
|
1370
|
+
return new Response(JSON.stringify({ message: "Hello from hello!", received: body, url }), {
|
|
1347
1371
|
status: 200,
|
|
1348
1372
|
headers: { "Content-Type": "application/json" },
|
|
1349
1373
|
})
|
|
1350
1374
|
}
|
|
1351
1375
|
|
|
1352
|
-
return new Response(JSON.stringify({ message: "Hello from hello!" }), {
|
|
1376
|
+
return new Response(JSON.stringify({ message: "Hello from hello!", url }), {
|
|
1353
1377
|
status: 200,
|
|
1354
1378
|
headers: { "Content-Type": "application/json" },
|
|
1355
1379
|
})
|
|
@@ -1435,6 +1459,16 @@ function printNextSteps(args: {
|
|
|
1435
1459
|
info(`Supatype project ready${name ? ` in ${name}/` : ""}.`)
|
|
1436
1460
|
nextSteps(setupComplete ? "Next steps:" : "Finish setup:", steps)
|
|
1437
1461
|
|
|
1462
|
+
// Shown once, at creation. These are generated per project rather than shipped as
|
|
1463
|
+
// constants, so this is the moment to say they exist and where they live — nothing prints
|
|
1464
|
+
// them again, and the JWT secret is only echoed as a fingerprint from here on.
|
|
1465
|
+
nextSteps("Secrets — generated for this project, kept in .env:", [
|
|
1466
|
+
"POSTGRES_PASSWORD direct SQL access as the superuser",
|
|
1467
|
+
"AUTHENTICATOR_PASSWORD PostgREST's own login; rotating the one above does not affect it",
|
|
1468
|
+
"JWT_SECRET signs ANON_KEY and SERVICE_ROLE_KEY; changing it invalidates every token",
|
|
1469
|
+
"Keep .env out of version control, and use different values in production.",
|
|
1470
|
+
])
|
|
1471
|
+
|
|
1438
1472
|
if (result.app.mode === "none") {
|
|
1439
1473
|
nextSteps("Static frontend (self-host):", [
|
|
1440
1474
|
"supatype app add --static ./public",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hidden internal commands (Homebrew post_install, etc.).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Command } from "commander"
|
|
6
|
+
import { downloadAll } from "../binary-cache.js"
|
|
7
|
+
import { error, info } from "../ui/messages.js"
|
|
8
|
+
|
|
9
|
+
export function registerInternalCommands(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("_postinstall", { hidden: true })
|
|
12
|
+
.description("Prefetch CDN component binaries after install (Homebrew post_install)")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
info("Prefetching Supatype component binaries for this platform...")
|
|
16
|
+
await downloadAll(undefined, true)
|
|
17
|
+
info("Component prefetch complete.")
|
|
18
|
+
} catch (err) {
|
|
19
|
+
error(err instanceof Error ? err.message : String(err))
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
2
|
import { loadConfig } from "../config.js"
|
|
3
|
+
import { loadCloudCredentials } from "../cloud-credentials.js"
|
|
3
4
|
import {
|
|
4
5
|
createCloudLink,
|
|
5
6
|
createSelfHostLink,
|
|
@@ -23,7 +24,8 @@ function resolveLinkToken(opts: {
|
|
|
23
24
|
opts.serviceRoleKey ??
|
|
24
25
|
process.env["SUPATYPE_ACCESS_TOKEN"] ??
|
|
25
26
|
process.env["SUPATYPE_TOKEN"] ??
|
|
26
|
-
process.env["SERVICE_ROLE_KEY"]
|
|
27
|
+
process.env["SERVICE_ROLE_KEY"] ??
|
|
28
|
+
loadCloudCredentials()?.accessToken
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -211,11 +213,15 @@ export async function runLinkAction(opts: {
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
if (!token) {
|
|
214
|
-
error("Authentication required.
|
|
216
|
+
error("Authentication required. Run: supatype login (or set SUPATYPE_ACCESS_TOKEN / pass --token).")
|
|
215
217
|
process.exit(1)
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
const cloudApiUrl = opts.apiUrl.replace(/\/$/, "")
|
|
221
|
+
const creds = loadCloudCredentials()
|
|
222
|
+
const refreshToken =
|
|
223
|
+
(creds?.apiUrl.replace(/\/$/, "") === cloudApiUrl ? creds.refreshToken : undefined) ??
|
|
224
|
+
existing?.refreshToken
|
|
219
225
|
|
|
220
226
|
if (project) {
|
|
221
227
|
const one = await targetFetch<{ slug: string; orgId: string }>(cloudApiUrl, "/api/v1", {
|
|
@@ -250,6 +256,7 @@ export async function runLinkAction(opts: {
|
|
|
250
256
|
orgId: one.orgId,
|
|
251
257
|
environments,
|
|
252
258
|
existing,
|
|
259
|
+
...(refreshToken !== undefined ? { refreshToken } : {}),
|
|
253
260
|
})
|
|
254
261
|
saveProjectLink(cwd, link)
|
|
255
262
|
info(`Linked to cloud project: ${project}`)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud session login / logout for CLI control-plane auth.
|
|
3
|
+
* Self-host uses SERVICE_ROLE_KEY via `supatype link --url` — not this command.
|
|
4
|
+
*/
|
|
5
|
+
import type { Command } from "commander"
|
|
6
|
+
import {
|
|
7
|
+
clearCloudCredentials,
|
|
8
|
+
cloudPasswordLogin,
|
|
9
|
+
loadCloudCredentials,
|
|
10
|
+
persistCloudSession,
|
|
11
|
+
} from "../cloud-credentials.js"
|
|
12
|
+
import { loadProjectLink, saveProjectLink } from "../link.js"
|
|
13
|
+
import { isInteractive } from "../ui/interactive.js"
|
|
14
|
+
import { error, info, plain } from "../ui/messages.js"
|
|
15
|
+
import { ensureNotCancelled, p, printLogo, runClackFlow } from "../ui/prompts.js"
|
|
16
|
+
|
|
17
|
+
function defaultApiUrl(cwd: string, flag?: string): string {
|
|
18
|
+
if (flag?.trim()) return flag.trim().replace(/\/$/, "")
|
|
19
|
+
const link = loadProjectLink(cwd)
|
|
20
|
+
if (link?.kind === "cloud" && link.cloudApiUrl) {
|
|
21
|
+
return link.cloudApiUrl.replace(/\/$/, "")
|
|
22
|
+
}
|
|
23
|
+
return (process.env["SUPATYPE_API_URL"] ?? "https://api.supatype.com").replace(/\/$/, "")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerLogin(program: Command): void {
|
|
27
|
+
program
|
|
28
|
+
.command("login")
|
|
29
|
+
.description("Sign in to Supatype Cloud (stores refreshable session for push/deploy)")
|
|
30
|
+
.option("--api-url <url>", "Cloud control plane API URL")
|
|
31
|
+
.option("--email <email>", "Account email (non-interactive)")
|
|
32
|
+
.option("--password <password>", "Account password (prefer SUPATYPE_PASSWORD)")
|
|
33
|
+
.action(async (opts: { apiUrl?: string; email?: string; password?: string }) => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
const apiUrl = defaultApiUrl(cwd, opts.apiUrl)
|
|
36
|
+
|
|
37
|
+
let email = opts.email?.trim() ?? process.env["SUPATYPE_EMAIL"]?.trim()
|
|
38
|
+
let password =
|
|
39
|
+
opts.password ?? process.env["SUPATYPE_PASSWORD"] ?? process.env["SUPATYPE_LOGIN_PASSWORD"]
|
|
40
|
+
|
|
41
|
+
if ((!email || !password) && isInteractive()) {
|
|
42
|
+
await runClackFlow(async () => {
|
|
43
|
+
printLogo()
|
|
44
|
+
p.intro("Supatype Cloud login")
|
|
45
|
+
if (!email) {
|
|
46
|
+
email = ensureNotCancelled(
|
|
47
|
+
await p.text({
|
|
48
|
+
message: "Email",
|
|
49
|
+
placeholder: "you@example.com",
|
|
50
|
+
}),
|
|
51
|
+
).trim()
|
|
52
|
+
}
|
|
53
|
+
if (!password) {
|
|
54
|
+
password = ensureNotCancelled(await p.password({ message: "Password" }))
|
|
55
|
+
}
|
|
56
|
+
p.outro("Signing in...")
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!email || !password) {
|
|
61
|
+
error(
|
|
62
|
+
"Email and password required. Pass --email / --password, set SUPATYPE_EMAIL + SUPATYPE_PASSWORD, or run interactively.",
|
|
63
|
+
)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const session = await cloudPasswordLogin(apiUrl, email, password)
|
|
69
|
+
persistCloudSession(cwd, {
|
|
70
|
+
apiUrl,
|
|
71
|
+
accessToken: session.accessToken,
|
|
72
|
+
...(session.refreshToken !== undefined ? { refreshToken: session.refreshToken } : {}),
|
|
73
|
+
...(session.email !== undefined ? { email: session.email } : { email }),
|
|
74
|
+
})
|
|
75
|
+
const link = loadProjectLink(cwd)
|
|
76
|
+
info(`Logged in to ${apiUrl}${session.email ? ` as ${session.email}` : ""}`)
|
|
77
|
+
if (link?.kind === "cloud") {
|
|
78
|
+
info(`Updated cloud session for linked project ${link.projectRef}`)
|
|
79
|
+
} else if (!session.refreshToken) {
|
|
80
|
+
plain("Warning: no refresh_token returned — session may expire in ~1h. Update control plane.")
|
|
81
|
+
} else {
|
|
82
|
+
plain("Tip: link a project with: supatype link --project <slug>")
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
error((err as Error).message)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("logout")
|
|
92
|
+
.description("Clear stored Supatype Cloud credentials")
|
|
93
|
+
.action(() => {
|
|
94
|
+
const cwd = process.cwd()
|
|
95
|
+
clearCloudCredentials()
|
|
96
|
+
const link = loadProjectLink(cwd)
|
|
97
|
+
if (link?.kind === "cloud") {
|
|
98
|
+
delete link.token
|
|
99
|
+
delete link.refreshToken
|
|
100
|
+
saveProjectLink(cwd, link)
|
|
101
|
+
}
|
|
102
|
+
info("Logged out of Supatype Cloud.")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command("whoami")
|
|
107
|
+
.description("Show the current Supatype Cloud session")
|
|
108
|
+
.action(() => {
|
|
109
|
+
const creds = loadCloudCredentials()
|
|
110
|
+
const cwd = process.cwd()
|
|
111
|
+
const link = loadProjectLink(cwd)
|
|
112
|
+
if (!creds && !(link?.kind === "cloud" && link.token)) {
|
|
113
|
+
info("Not logged in. Run: supatype login")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if (creds) {
|
|
117
|
+
plain(`API: ${creds.apiUrl}`)
|
|
118
|
+
if (creds.email) plain(`Email: ${creds.email}`)
|
|
119
|
+
plain(`Refresh token: ${creds.refreshToken ? "yes" : "no"}`)
|
|
120
|
+
plain(`Updated: ${creds.updatedAt}`)
|
|
121
|
+
}
|
|
122
|
+
if (link?.kind === "cloud") {
|
|
123
|
+
plain(`Linked project: ${link.projectRef}`)
|
|
124
|
+
plain(`Link has refresh token: ${link.refreshToken ? "yes" : "no"}`)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
package/src/commands/push.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { Command } from "commander"
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
5
|
-
import {
|
|
5
|
+
import { syncManifestHooks, validateModelHooks, writeHooksModule } from "../model-hooks.js"
|
|
6
|
+
import { adapterEntry, readHookUpload } from "../hook-upload.js"
|
|
7
|
+
import { checkServiceRoleRoutes, serviceRoleProblemLines } from "../service-role-check.js"
|
|
8
|
+
import { fatalError } from "../ui/fatal.js"
|
|
9
|
+
import {
|
|
10
|
+
hooksPathFromProject,
|
|
11
|
+
resolveRuntimeProvider,
|
|
12
|
+
schemaPathFromProject,
|
|
13
|
+
serverBaseUrl,
|
|
14
|
+
} from "../project-config.js"
|
|
6
15
|
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
7
16
|
import { printDiffOperations, printDiffWarnings } from "../diff-output.js"
|
|
8
17
|
import { signJwt } from "../jwt.js"
|
|
@@ -20,6 +29,7 @@ import {
|
|
|
20
29
|
type DeployTarget,
|
|
21
30
|
} from "../resolve-target.js"
|
|
22
31
|
import { loadProjectLink } from "../link.js"
|
|
32
|
+
import { targetFetch } from "../target-client.js"
|
|
23
33
|
import {
|
|
24
34
|
buildSchemaSourcesPayload,
|
|
25
35
|
cacheSchemaSourcesLocally,
|
|
@@ -54,6 +64,8 @@ export function registerPush(program: Command): void {
|
|
|
54
64
|
const config = loadConfig(cwd)
|
|
55
65
|
const pgSchema = schemaPgSchema(cwd)
|
|
56
66
|
const ast = loadSchemaAst(schemaPathFromProject(config, cwd), cwd)
|
|
67
|
+
assertModelHooksResolve(cwd, config, ast)
|
|
68
|
+
assertServiceRoleGrantsResolve(cwd, config)
|
|
57
69
|
|
|
58
70
|
const linked = loadProjectLink(cwd)
|
|
59
71
|
const useDirect = opts.direct || opts.local || Boolean(opts.connection)
|
|
@@ -143,6 +155,10 @@ async function pushViaTarget(
|
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
|
|
158
|
+
if (target.mode === "cloud" || target.mode === "self-host") {
|
|
159
|
+
await deployHooksToTarget(cwd, config, target, ast)
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
if (target.mode === "direct" || target.mode === "local") {
|
|
147
163
|
await writeLocalAdminConfig(ast, config)
|
|
148
164
|
if (target.databaseUrl) {
|
|
@@ -150,17 +166,86 @@ async function pushViaTarget(
|
|
|
150
166
|
}
|
|
151
167
|
await generateTypesLocal(ast, config)
|
|
152
168
|
await provisionLocalStorage(ast, config)
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
|
|
170
|
+
// Local Studio only — a cloud/self-host push must not advertise the local
|
|
171
|
+
// gateway URL from config, which may not even be running.
|
|
172
|
+
const baseUrl = (serverBaseUrl(config) ?? "").replace(/\/$/, "")
|
|
173
|
+
if (baseUrl) {
|
|
174
|
+
plain(`\nStudio: ${baseUrl}/studio/`)
|
|
175
|
+
}
|
|
176
|
+
return
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
179
|
+
info(`Pushed to ${target.mode} (${target.environment}).`)
|
|
180
|
+
const envUrl = target.link?.environments?.[target.environment]?.apiUrl?.replace(/\/$/, "")
|
|
181
|
+
if (envUrl) {
|
|
182
|
+
plain(`\nProject API: ${envUrl}`)
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Upload the hooks this schema names to a managed stack.
|
|
188
|
+
*
|
|
189
|
+
* On `push` rather than `functions deploy`, because the hook *map* comes from the schema: the two have
|
|
190
|
+
* to move together, and a map naming a handler that was never uploaded turns every write to that table
|
|
191
|
+
* into a 503.
|
|
192
|
+
*
|
|
193
|
+
* The handler types are written locally as well. A project pushing to cloud still edits its hooks on
|
|
194
|
+
* this machine, and `hooks/_supatype/hooks.ts` is what makes them typed — it is generated, never
|
|
195
|
+
* committed, so a fresh clone that has only ever pushed to cloud would otherwise have no types at all.
|
|
196
|
+
*/
|
|
197
|
+
async function deployHooksToTarget(
|
|
198
|
+
cwd: string,
|
|
199
|
+
config: SupatypeProjectConfig,
|
|
200
|
+
target: DeployTarget,
|
|
201
|
+
ast: unknown,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const hooksDir = hooksPathFromProject(config, cwd)
|
|
204
|
+
const hooksPath = writeHooksModule(cwd, hooksDir, ast)
|
|
205
|
+
if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
|
|
206
|
+
|
|
207
|
+
let upload: ReturnType<typeof readHookUpload>
|
|
208
|
+
try {
|
|
209
|
+
upload = readHookUpload(cwd, hooksDir, ast)
|
|
210
|
+
} catch (err) {
|
|
211
|
+
// Refused rather than partially uploaded: the schema is already applied, so the honest thing is to
|
|
212
|
+
// say the hooks did not deploy and why, leaving the previous ones in place.
|
|
213
|
+
fatalError(err instanceof Error ? err.message : String(err))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (upload === null) return
|
|
217
|
+
|
|
218
|
+
const adapter = readGeneratedAdapter(hooksDir)
|
|
219
|
+
const handlers = adapter === null ? upload.handlers : [...upload.handlers, adapter]
|
|
220
|
+
|
|
221
|
+
await withSpinner(`Deploying ${upload.handlers.length} hook(s)`, () =>
|
|
222
|
+
targetFetch(target.apiBaseUrl, target.apiPrefix, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
path: `/projects/${target.projectRef}/hooks/deploy`,
|
|
225
|
+
body: { handlers, map: upload.map },
|
|
226
|
+
token: target.token!,
|
|
227
|
+
orgId: target.orgId,
|
|
228
|
+
environment: target.mode === "cloud" ? target.environment : undefined,
|
|
229
|
+
}),
|
|
230
|
+
).then(() => info(`Deployed ${upload.handlers.length} hook(s)`))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The generated adapter, flattened to the name handlers were rewritten to import. */
|
|
234
|
+
function readGeneratedAdapter(hooksDir: string): { name: string; source: string } | null {
|
|
235
|
+
const file = join(hooksDir, "_supatype", "hooks.ts")
|
|
236
|
+
if (!existsSync(file)) return null
|
|
237
|
+
return adapterEntry(readFileSync(file, "utf8"))
|
|
238
|
+
}
|
|
239
|
+
|
|
163
240
|
async function generateTypesLocal(ast: unknown, config: SupatypeProjectConfig): Promise<void> {
|
|
241
|
+
// Independent of `output`: a project with hooks needs its handler types whether or not it asked
|
|
242
|
+
// for client types, and the module is written next to the functions that import it.
|
|
243
|
+
const cwd = process.cwd()
|
|
244
|
+
const hooksPath = writeHooksModule(cwd, hooksPathFromProject(config, cwd), ast)
|
|
245
|
+
if (hooksPath !== null) info(`Hook handler types written to ${hooksPath}`)
|
|
246
|
+
// The server watches this file, so a changed hook takes effect without a restart.
|
|
247
|
+
if (syncManifestHooks(cwd, ast)) info("Hook map written to .supatype/manifest.json")
|
|
248
|
+
|
|
164
249
|
if (!config.output?.types && !config.output?.client) return
|
|
165
250
|
await withSpinner("Generating types", async () => {
|
|
166
251
|
await ensureEngine()
|
|
@@ -195,3 +280,35 @@ async function writeLocalAdminConfig(ast: unknown, config: SupatypeProjectConfig
|
|
|
195
280
|
restoreSystemRelationTargets(admin, ast)
|
|
196
281
|
writeFileSync(join(dir, "admin-config.json"), `${JSON.stringify(admin, null, 2)}\n`)
|
|
197
282
|
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Stop the push when a declared hook names a function that is not there.
|
|
286
|
+
*
|
|
287
|
+
* A hook is only enforcement if it runs. A typo'd name would extract cleanly, reach the manifest, and
|
|
288
|
+
* then never fire — so the write it was meant to validate would succeed and look fine. Cheaper to
|
|
289
|
+
* fail here, naming the directory searched.
|
|
290
|
+
*/
|
|
291
|
+
/**
|
|
292
|
+
* Refuse a push whose `functions.serviceRole` names a function that does not exist.
|
|
293
|
+
*
|
|
294
|
+
* The grant fails closed, which is the safe direction and the invisible one: the function reads no key
|
|
295
|
+
* at runtime, in a deploy that reported success. A warning is printed for an entry that is merely
|
|
296
|
+
* redundant, since a reader would reasonably assume the line is what does the granting.
|
|
297
|
+
*/
|
|
298
|
+
function assertServiceRoleGrantsResolve(cwd: string, config: SupatypeProjectConfig): void {
|
|
299
|
+
const problems = checkServiceRoleRoutes(config, cwd)
|
|
300
|
+
for (const warning of problems.warnings) plain(warning)
|
|
301
|
+
const lines = serviceRoleProblemLines(problems)
|
|
302
|
+
if (lines.length === 0) return
|
|
303
|
+
fatalError("functions.serviceRole names a function that does not exist.", lines, {
|
|
304
|
+
brand: { intro: "Push" },
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function assertModelHooksResolve(cwd: string, config: SupatypeProjectConfig, ast: unknown): void {
|
|
309
|
+
const problems = validateModelHooks(ast, hooksPathFromProject(config, cwd), cwd)
|
|
310
|
+
if (problems.length === 0) return
|
|
311
|
+
fatalError("A model declares a hook whose function does not exist.", problems, {
|
|
312
|
+
brand: { intro: "Push" },
|
|
313
|
+
})
|
|
314
|
+
}
|