@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,212 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { execFileSync } from "node:child_process"
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
6
|
+
import { generateHooksModule } from "../src/hooks-generator.js"
|
|
7
|
+
import { extractSchemaAstFromTypes } from "../src/type-extractor.js"
|
|
8
|
+
|
|
9
|
+
const dirs: string[] = []
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
function astFor(schema: string): unknown {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-hooksgen-"))
|
|
17
|
+
dirs.push(dir)
|
|
18
|
+
const schemaPath = join(dir, "schema.ts")
|
|
19
|
+
writeFileSync(schemaPath, schema, "utf8")
|
|
20
|
+
return extractSchemaAstFromTypes(schemaPath, dir)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HOOKED_SCHEMA = `
|
|
24
|
+
import type { Model, Optional, RichText, Timestamp, UUID } from "@supatype/types"
|
|
25
|
+
|
|
26
|
+
export type Post = Model<{
|
|
27
|
+
id: UUID
|
|
28
|
+
title: string
|
|
29
|
+
body: RichText
|
|
30
|
+
views: Optional<number>
|
|
31
|
+
created_at: Timestamp
|
|
32
|
+
}, {
|
|
33
|
+
tableName: "posts"
|
|
34
|
+
hooks: { beforeChange: "moderate-post"; afterDelete: "purge-post" }
|
|
35
|
+
}>
|
|
36
|
+
|
|
37
|
+
export type Comment = Model<{ id: UUID; body: string }, { tableName: "comments" }>
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
describe("generateHooksModule", () => {
|
|
41
|
+
it("returns null when nothing declares a hook", () => {
|
|
42
|
+
const ast = astFor(`
|
|
43
|
+
import type { Model, UUID } from "@supatype/types"
|
|
44
|
+
|
|
45
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
46
|
+
`)
|
|
47
|
+
// A generated file nobody imports is a file somebody eventually edits.
|
|
48
|
+
expect(generateHooksModule(ast)).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("includes only the tables that declare a hook", () => {
|
|
52
|
+
const module = generateHooksModule(astFor(HOOKED_SCHEMA))
|
|
53
|
+
expect(module).not.toBeNull()
|
|
54
|
+
expect(module).toContain('"posts": {')
|
|
55
|
+
// `HookedTable` is what makes `BeforeChange<"comments">` a compile error, so an unhooked table
|
|
56
|
+
// must not appear at all.
|
|
57
|
+
expect(module).not.toContain('"comments": {')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("carries the model's real columns into Row, Insert and Update", () => {
|
|
61
|
+
const module = generateHooksModule(astFor(HOOKED_SCHEMA)) ?? ""
|
|
62
|
+
expect(module).toContain("title: string")
|
|
63
|
+
// `created_at` is server-generated, so it is optional on insert but present on a row.
|
|
64
|
+
expect(module).toMatch(/Insert: \{[\s\S]*created_at\?:/)
|
|
65
|
+
expect(module).toMatch(/Row: \{[\s\S]*created_at: string/)
|
|
66
|
+
// Every column is optional on a patch.
|
|
67
|
+
expect(module).toMatch(/Update: \{[\s\S]*title\?: string/)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("the generated module compiles", () => {
|
|
72
|
+
/**
|
|
73
|
+
* The generator emits TypeScript, so the only test that really proves it works is a compiler.
|
|
74
|
+
* A missing brace or a type that does not resolve would otherwise reach a user's editor — and this
|
|
75
|
+
* file is imported by their hook, so a broken emit breaks their function rather than our build.
|
|
76
|
+
*/
|
|
77
|
+
it("typechecks under strict mode, together with a handler that uses it", () => {
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-hookscompile-"))
|
|
79
|
+
dirs.push(dir)
|
|
80
|
+
|
|
81
|
+
const module = generateHooksModule(astFor(HOOKED_SCHEMA))
|
|
82
|
+
expect(module).not.toBeNull()
|
|
83
|
+
writeFileSync(join(dir, "hooks.ts"), module ?? "", "utf8")
|
|
84
|
+
|
|
85
|
+
// `supatype functions new` writes a `functions/deno.d.ts`, which is where a real project gets
|
|
86
|
+
// this. The generated module uses `Deno.env` to find the API, so the fixture needs it too.
|
|
87
|
+
writeFileSync(
|
|
88
|
+
join(dir, "deno.d.ts"),
|
|
89
|
+
`declare namespace Deno {
|
|
90
|
+
const env: { get(key: string): string | undefined }
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
"utf8",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// A handler written the way the docs will show it, including the narrowing that the
|
|
97
|
+
// discriminated union is there to provide.
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(dir, "handler.ts"),
|
|
100
|
+
`import { hook, type AfterDelete, type BeforeChange } from "./hooks.ts"
|
|
101
|
+
|
|
102
|
+
const moderate: BeforeChange<"posts"> = async (ctx) => {
|
|
103
|
+
if (ctx.operation === "insert") {
|
|
104
|
+
if (ctx.rows.some((r) => r.title.trim() === "")) return { reject: "A post needs a title" }
|
|
105
|
+
return { rows: ctx.rows.map((r) => ({ ...r, title: r.title.trim() })) }
|
|
106
|
+
}
|
|
107
|
+
const { rows, truncated } = await ctx.previous()
|
|
108
|
+
if (truncated) return { reject: { message: "Too many rows", status: 409 } }
|
|
109
|
+
if (rows.length === 0) return {}
|
|
110
|
+
return { patch: { ...ctx.patch, title: ctx.patch.title ?? rows[0]!.title } }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const purge: AfterDelete<"posts"> = async (ctx) => {
|
|
114
|
+
void ctx.filter
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default hook(moderate)
|
|
118
|
+
export const alsoPurge = hook(purge)
|
|
119
|
+
`,
|
|
120
|
+
"utf8",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(dir, "tsconfig.json"),
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
compilerOptions: {
|
|
127
|
+
strict: true,
|
|
128
|
+
exactOptionalPropertyTypes: true,
|
|
129
|
+
noEmit: true,
|
|
130
|
+
target: "ES2022",
|
|
131
|
+
module: "Preserve",
|
|
132
|
+
moduleResolution: "bundler",
|
|
133
|
+
allowImportingTsExtensions: true,
|
|
134
|
+
lib: ["ES2022", "DOM"],
|
|
135
|
+
types: [],
|
|
136
|
+
},
|
|
137
|
+
include: ["*.ts"],
|
|
138
|
+
}),
|
|
139
|
+
"utf8",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const tsc = join(__dirname, "..", "..", "..", "node_modules", "typescript", "bin", "tsc")
|
|
143
|
+
let output = ""
|
|
144
|
+
try {
|
|
145
|
+
execFileSync(process.execPath, [tsc, "-p", join(dir, "tsconfig.json")], {
|
|
146
|
+
encoding: "utf8",
|
|
147
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
148
|
+
})
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const failure = err as { stdout?: string; stderr?: string }
|
|
151
|
+
output = `${failure.stdout ?? ""}${failure.stderr ?? ""}`
|
|
152
|
+
}
|
|
153
|
+
expect(output).toBe("")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("rejects a handler for a table that declares no hook", () => {
|
|
157
|
+
// The point of `HookedTable`: naming an unhooked table must not silently produce a handler that
|
|
158
|
+
// never runs.
|
|
159
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-hooksneg-"))
|
|
160
|
+
dirs.push(dir)
|
|
161
|
+
writeFileSync(join(dir, "hooks.ts"), generateHooksModule(astFor(HOOKED_SCHEMA)) ?? "", "utf8")
|
|
162
|
+
// `supatype functions new` writes a `functions/deno.d.ts`, which is where a real project gets
|
|
163
|
+
// this. The generated module uses `Deno.env` to find the API, so the fixture needs it too.
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(dir, "deno.d.ts"),
|
|
166
|
+
`declare namespace Deno {
|
|
167
|
+
const env: { get(key: string): string | undefined }
|
|
168
|
+
}
|
|
169
|
+
`,
|
|
170
|
+
"utf8",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
writeFileSync(
|
|
174
|
+
join(dir, "handler.ts"),
|
|
175
|
+
`import type { BeforeChange } from "./hooks.ts"
|
|
176
|
+
const nope: BeforeChange<"comments"> = async () => ({})
|
|
177
|
+
export default nope
|
|
178
|
+
`,
|
|
179
|
+
"utf8",
|
|
180
|
+
)
|
|
181
|
+
writeFileSync(
|
|
182
|
+
join(dir, "tsconfig.json"),
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
compilerOptions: {
|
|
185
|
+
strict: true,
|
|
186
|
+
noEmit: true,
|
|
187
|
+
target: "ES2022",
|
|
188
|
+
module: "Preserve",
|
|
189
|
+
moduleResolution: "bundler",
|
|
190
|
+
allowImportingTsExtensions: true,
|
|
191
|
+
lib: ["ES2022", "DOM"],
|
|
192
|
+
types: [],
|
|
193
|
+
},
|
|
194
|
+
include: ["*.ts"],
|
|
195
|
+
}),
|
|
196
|
+
"utf8",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const tsc = join(__dirname, "..", "..", "..", "node_modules", "typescript", "bin", "tsc")
|
|
200
|
+
let output = ""
|
|
201
|
+
try {
|
|
202
|
+
execFileSync(process.execPath, [tsc, "-p", join(dir, "tsconfig.json")], {
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
205
|
+
})
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const failure = err as { stdout?: string; stderr?: string }
|
|
208
|
+
output = `${failure.stdout ?? ""}${failure.stderr ?? ""}`
|
|
209
|
+
}
|
|
210
|
+
expect(output).toContain("comments")
|
|
211
|
+
})
|
|
212
|
+
})
|
package/tests/init.test.ts
CHANGED
|
@@ -289,6 +289,9 @@ describe("scaffold()", () => {
|
|
|
289
289
|
expect(existsSync(join(tmpRoot, "functions/hello/index.ts"))).toBe(true)
|
|
290
290
|
expect(existsSync(join(tmpRoot, "functions/_shared/README.md"))).toBe(true)
|
|
291
291
|
expect(existsSync(join(tmpRoot, "functions/.env.local"))).toBe(true)
|
|
292
|
+
expect(existsSync(join(tmpRoot, "functions/deno.d.ts"))).toBe(true)
|
|
293
|
+
expect(existsSync(join(tmpRoot, "functions/tsconfig.json"))).toBe(true)
|
|
294
|
+
expect(readFileSync(join(tmpRoot, "functions/hello/index.ts"), "utf8")).toContain("Deno.env.get")
|
|
292
295
|
const pkg = readFileSync(join(tmpRoot, "package.json"), "utf8")
|
|
293
296
|
expect(pkg).toContain("supatype functions serve")
|
|
294
297
|
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import {
|
|
6
|
+
FALLBACK_AUTHENTICATOR_PASSWORD,
|
|
7
|
+
FALLBACK_JWT_SECRET,
|
|
8
|
+
FALLBACK_POSTGRES_PASSWORD,
|
|
9
|
+
devAuthenticatorPassword,
|
|
10
|
+
devJwtSecret,
|
|
11
|
+
devPostgresPassword,
|
|
12
|
+
secretFingerprint,
|
|
13
|
+
} from "../src/local-secrets.js"
|
|
14
|
+
import { upsertEnvFile } from "../src/env-file.js"
|
|
15
|
+
|
|
16
|
+
// The bug these exist to prevent: `supatype dev` used to pin POSTGRES_PASSWORD and JWT_SECRET
|
|
17
|
+
// to published constants in `.env` on every run — the same `.env` a self-host deployment reads.
|
|
18
|
+
// A generated secret survived until the developer's next `dev`, then reverted silently, and the
|
|
19
|
+
// deployment went out with a secret anyone can look up.
|
|
20
|
+
|
|
21
|
+
function project(env?: string): string {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-secrets-"))
|
|
23
|
+
if (env !== undefined) writeFileSync(join(dir, ".env"), env, "utf8")
|
|
24
|
+
return dir
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("resolving local secrets", () => {
|
|
28
|
+
it("prefers the project's own values", () => {
|
|
29
|
+
const dir = project("JWT_SECRET=mine\nPOSTGRES_PASSWORD=pg-mine\nAUTHENTICATOR_PASSWORD=auth-mine\n")
|
|
30
|
+
expect(devJwtSecret(dir)).toBe("mine")
|
|
31
|
+
expect(devPostgresPassword(dir)).toBe("pg-mine")
|
|
32
|
+
expect(devAuthenticatorPassword(dir)).toBe("auth-mine")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// A project with no `.env` still has to start, so the constants remain — as fallbacks only.
|
|
36
|
+
it("falls back when the project has none", () => {
|
|
37
|
+
const dir = project()
|
|
38
|
+
expect(devJwtSecret(dir)).toBe(FALLBACK_JWT_SECRET)
|
|
39
|
+
expect(devPostgresPassword(dir)).toBe(FALLBACK_POSTGRES_PASSWORD)
|
|
40
|
+
expect(devAuthenticatorPassword(dir)).toBe(FALLBACK_AUTHENTICATOR_PASSWORD)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("the dev env write does not clobber secrets", () => {
|
|
45
|
+
// This is the regression. `upsertEnvFile` replaces any key handed to it, so the guarantee has
|
|
46
|
+
// to be that the dev path never hands it these — asserted here at the level that matters:
|
|
47
|
+
// write what dev writes, then check the secrets are byte-identical.
|
|
48
|
+
it("leaves generated secrets untouched across repeated writes", () => {
|
|
49
|
+
const dir = project(
|
|
50
|
+
"JWT_SECRET=generated-secret-value\nPOSTGRES_PASSWORD=generated-pg-value\nAUTHENTICATOR_PASSWORD=generated-auth-value\n",
|
|
51
|
+
)
|
|
52
|
+
const before = readFileSync(join(dir, ".env"), "utf8")
|
|
53
|
+
|
|
54
|
+
// The keys `upsertDevComposeEnv` legitimately owns: derived values and local ports.
|
|
55
|
+
for (let run = 0; run < 2; run++) {
|
|
56
|
+
upsertEnvFile(dir, {
|
|
57
|
+
POSTGRES_USER: "supatype_admin",
|
|
58
|
+
POSTGRES_DB: "supatype",
|
|
59
|
+
AUTHENTICATOR_PASSWORD: devAuthenticatorPassword(dir),
|
|
60
|
+
ANON_KEY: `anon-${run}`,
|
|
61
|
+
SERVICE_ROLE_KEY: `service-${run}`,
|
|
62
|
+
SUPATYPE_KONG_PORT: "18473",
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const after = readFileSync(join(dir, ".env"), "utf8")
|
|
67
|
+
const value = (content: string, key: string): string | undefined =>
|
|
68
|
+
content.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]
|
|
69
|
+
|
|
70
|
+
for (const key of ["JWT_SECRET", "POSTGRES_PASSWORD", "AUTHENTICATOR_PASSWORD"]) {
|
|
71
|
+
expect(value(after, key), `${key} must survive a dev run`).toBe(value(before, key))
|
|
72
|
+
}
|
|
73
|
+
// Derived keys are still expected to move.
|
|
74
|
+
expect(value(after, "ANON_KEY")).toBe("anon-1")
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("secretFingerprint", () => {
|
|
79
|
+
// The ready panel used to echo the JWT secret in full. Harmless for a published constant,
|
|
80
|
+
// wrong once it is the project's own.
|
|
81
|
+
it("does not reveal the secret", () => {
|
|
82
|
+
const secret = "super-secret-value-that-must-not-be-printed"
|
|
83
|
+
const tag = secretFingerprint(secret)
|
|
84
|
+
expect(secret).not.toContain(tag)
|
|
85
|
+
expect(tag).toMatch(/^[0-9a-f]{8}$/)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("is stable and distinguishes secrets", () => {
|
|
89
|
+
expect(secretFingerprint("a")).toBe(secretFingerprint("a"))
|
|
90
|
+
expect(secretFingerprint("a")).not.toBe(secretFingerprint("b"))
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_HOOK_TIMEOUT_MS,
|
|
7
|
+
declaredHooks,
|
|
8
|
+
manifestHooks,
|
|
9
|
+
syncManifestHooks,
|
|
10
|
+
hooksReport,
|
|
11
|
+
validateModelHooks,
|
|
12
|
+
writeHooksModule,
|
|
13
|
+
} from "../src/model-hooks.js"
|
|
14
|
+
import { extractSchemaAstFromTypes } from "../src/type-extractor.js"
|
|
15
|
+
|
|
16
|
+
const dirs: string[] = []
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function project(schema: string): { dir: string; ast: unknown } {
|
|
23
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-hooks-"))
|
|
24
|
+
dirs.push(dir)
|
|
25
|
+
const schemaPath = join(dir, "schema.ts")
|
|
26
|
+
writeFileSync(schemaPath, schema, "utf8")
|
|
27
|
+
return { dir, ast: extractSchemaAstFromTypes(schemaPath, dir) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A function the worker would discover: a directory with an index.ts. */
|
|
31
|
+
function addFunction(dir: string, name: string): void {
|
|
32
|
+
mkdirSync(join(dir, "functions", name), { recursive: true })
|
|
33
|
+
writeFileSync(join(dir, "functions", name, "index.ts"), "export default () => new Response()", "utf8")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("model hooks — declaration", () => {
|
|
37
|
+
it("extracts both the shorthand and the options form", () => {
|
|
38
|
+
const { ast } = project(`
|
|
39
|
+
import type { Model, UUID } from "@supatype/types"
|
|
40
|
+
|
|
41
|
+
export type Post = Model<{ id: UUID; title: string }, {
|
|
42
|
+
tableName: "posts"
|
|
43
|
+
hooks: {
|
|
44
|
+
beforeChange: "moderate-post"
|
|
45
|
+
afterChange: { function: "index-post"; timeout: 5000; onUnavailable: "log" }
|
|
46
|
+
}
|
|
47
|
+
}>
|
|
48
|
+
`)
|
|
49
|
+
expect(declaredHooks(ast)).toEqual([
|
|
50
|
+
{ model: "Post", event: "afterChange", function: "index-post" },
|
|
51
|
+
{ model: "Post", event: "beforeChange", function: "moderate-post" },
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
const models = (ast as { models: { annotations: { platform: { hooks?: Record<string, unknown> } } }[] }).models
|
|
55
|
+
expect(models[0]?.annotations.platform.hooks).toEqual({
|
|
56
|
+
beforeChange: { function: "moderate-post" },
|
|
57
|
+
afterChange: { function: "index-post", timeout: 5000, onUnavailable: "log" },
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("carries no hooks annotation when a model declares none", () => {
|
|
62
|
+
const { ast } = project(`
|
|
63
|
+
import type { Model, UUID } from "@supatype/types"
|
|
64
|
+
|
|
65
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
66
|
+
`)
|
|
67
|
+
const models = (ast as { models: { annotations: { platform: Record<string, unknown> } }[] }).models
|
|
68
|
+
expect(models[0]?.annotations.platform["hooks"]).toBeUndefined()
|
|
69
|
+
expect(declaredHooks(ast)).toEqual([])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("ignores an event name that is not a lifecycle hook", () => {
|
|
73
|
+
// `beforeRead` was dropped from the design. Extracting it would put a hook in the manifest that
|
|
74
|
+
// the server has no point at which to call, which reads as "declared and silently ignored".
|
|
75
|
+
const { ast } = project(`
|
|
76
|
+
import type { Model, UUID } from "@supatype/types"
|
|
77
|
+
|
|
78
|
+
export type Post = Model<{ id: UUID }, {
|
|
79
|
+
tableName: "posts"
|
|
80
|
+
hooks: { beforeRead: "nope"; beforeChange: "yes" }
|
|
81
|
+
}>
|
|
82
|
+
`)
|
|
83
|
+
expect(declaredHooks(ast).map((h) => h.event)).toEqual(["beforeChange"])
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("model hooks — push-time validation", () => {
|
|
88
|
+
const schema = `
|
|
89
|
+
import type { Model, UUID } from "@supatype/types"
|
|
90
|
+
|
|
91
|
+
export type Post = Model<{ id: UUID }, {
|
|
92
|
+
tableName: "posts"
|
|
93
|
+
hooks: { beforeChange: "moderate-post" }
|
|
94
|
+
}>
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
it("passes when the named function exists", () => {
|
|
98
|
+
const { dir, ast } = project(schema)
|
|
99
|
+
addFunction(dir, "moderate-post")
|
|
100
|
+
expect(validateModelHooks(ast, join(dir, "functions"), dir)).toEqual([])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("reports the hook, the path searched, and what is available", () => {
|
|
104
|
+
// The failure this prevents: a typo'd name extracts cleanly, reaches the manifest, never fires,
|
|
105
|
+
// and the write it was meant to validate succeeds looking perfectly normal.
|
|
106
|
+
const { dir, ast } = project(schema)
|
|
107
|
+
addFunction(dir, "moderate-posts") // note the plural
|
|
108
|
+
const problems = validateModelHooks(ast, join(dir, "functions"), dir)
|
|
109
|
+
|
|
110
|
+
expect(problems.join("\n")).toContain("Post.beforeChange")
|
|
111
|
+
expect(problems.join("\n")).toContain('"moderate-post"')
|
|
112
|
+
expect(problems.join("\n")).toContain("moderate-posts")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("says how to create one when the hooks directory is empty", () => {
|
|
116
|
+
const { dir, ast } = project(schema)
|
|
117
|
+
mkdirSync(join(dir, "hooks"), { recursive: true })
|
|
118
|
+
expect(validateModelHooks(ast, join(dir, "hooks"), dir).join("\n")).toContain(
|
|
119
|
+
"supatype hooks new",
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("does not require a functions directory when no hooks are declared", () => {
|
|
124
|
+
const { dir, ast } = project(`
|
|
125
|
+
import type { Model, UUID } from "@supatype/types"
|
|
126
|
+
|
|
127
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
128
|
+
`)
|
|
129
|
+
expect(validateModelHooks(ast, join(dir, "functions"), dir)).toEqual([])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("ignores a file and an underscore directory when listing functions", () => {
|
|
133
|
+
// Matches how the worker discovers handlers: `_shared` is shared code, not a function.
|
|
134
|
+
const { dir, ast } = project(schema)
|
|
135
|
+
mkdirSync(join(dir, "functions", "_shared"), { recursive: true })
|
|
136
|
+
writeFileSync(join(dir, "functions", "_shared", "index.ts"), "export const x = 1", "utf8")
|
|
137
|
+
writeFileSync(join(dir, "functions", "readme.md"), "notes", "utf8")
|
|
138
|
+
|
|
139
|
+
const problems = validateModelHooks(ast, join(dir, "functions"), dir).join("\n")
|
|
140
|
+
expect(problems).toContain("No hooks found")
|
|
141
|
+
expect(problems).not.toContain("_shared")
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("model hooks — manifest map", () => {
|
|
146
|
+
const schema = `
|
|
147
|
+
import type { Model, UUID } from "@supatype/types"
|
|
148
|
+
|
|
149
|
+
export type Post = Model<{ id: UUID }, {
|
|
150
|
+
tableName: "posts"
|
|
151
|
+
hooks: { beforeChange: "moderate-post"; afterChange: { function: "index-post"; timeout: 9000 } }
|
|
152
|
+
}>
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
it("keys by table name and fills the safe default per phase", () => {
|
|
156
|
+
const { ast } = project(schema)
|
|
157
|
+
// Keyed by table, not model: the server matches a request path.
|
|
158
|
+
expect(manifestHooks(ast)).toEqual({
|
|
159
|
+
posts: {
|
|
160
|
+
beforeChange: {
|
|
161
|
+
function: "moderate-post",
|
|
162
|
+
timeout: DEFAULT_HOOK_TIMEOUT_MS,
|
|
163
|
+
// Unreachable before a write must not let the write through...
|
|
164
|
+
onUnavailable: "reject",
|
|
165
|
+
},
|
|
166
|
+
afterChange: {
|
|
167
|
+
function: "index-post",
|
|
168
|
+
timeout: 9000,
|
|
169
|
+
// ...but after one there is nothing left to fail.
|
|
170
|
+
onUnavailable: "log",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("merges into an existing manifest and leaves its other keys alone", () => {
|
|
177
|
+
const { dir, ast } = project(schema)
|
|
178
|
+
mkdirSync(join(dir, ".supatype"), { recursive: true })
|
|
179
|
+
writeFileSync(
|
|
180
|
+
join(dir, ".supatype", "manifest.json"),
|
|
181
|
+
JSON.stringify({ schema: "tenant_7", functions_enabled: true }),
|
|
182
|
+
"utf8",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
expect(syncManifestHooks(dir, ast)).toBe(true)
|
|
186
|
+
const manifest = JSON.parse(
|
|
187
|
+
readFileSync(join(dir, ".supatype", "manifest.json"), "utf8"),
|
|
188
|
+
) as Record<string, unknown>
|
|
189
|
+
expect(manifest["schema"]).toBe("tenant_7")
|
|
190
|
+
expect(manifest["functions_enabled"]).toBe(true)
|
|
191
|
+
expect(Object.keys(manifest["hooks"] as object)).toEqual(["posts"])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("never creates a manifest from scratch", () => {
|
|
195
|
+
// `functions_enabled` is a plain bool on the server's side, so a manifest carrying only hooks
|
|
196
|
+
// would read as functions *disabled* — the defect fixed one commit earlier, by another door.
|
|
197
|
+
const { dir, ast } = project(schema)
|
|
198
|
+
expect(syncManifestHooks(dir, ast)).toBe(false)
|
|
199
|
+
expect(existsSync(join(dir, ".supatype", "manifest.json"))).toBe(false)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("removes the hook map when the last hook goes away", () => {
|
|
203
|
+
const { dir, ast } = project(`
|
|
204
|
+
import type { Model, UUID } from "@supatype/types"
|
|
205
|
+
|
|
206
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
207
|
+
`)
|
|
208
|
+
mkdirSync(join(dir, ".supatype"), { recursive: true })
|
|
209
|
+
writeFileSync(
|
|
210
|
+
join(dir, ".supatype", "manifest.json"),
|
|
211
|
+
JSON.stringify({ functions_enabled: true, hooks: { posts: { beforeChange: { function: "gone" } } } }),
|
|
212
|
+
"utf8",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
expect(syncManifestHooks(dir, ast)).toBe(true)
|
|
216
|
+
const manifest = JSON.parse(
|
|
217
|
+
readFileSync(join(dir, ".supatype", "manifest.json"), "utf8"),
|
|
218
|
+
) as Record<string, unknown>
|
|
219
|
+
expect(manifest["hooks"]).toBeUndefined()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("model hooks — generated module on disk", () => {
|
|
224
|
+
it("removes a stale module when the last hook is deleted", () => {
|
|
225
|
+
// Otherwise a typed module sits there claiming tables are hooked, and a handler importing it
|
|
226
|
+
// keeps compiling while never running.
|
|
227
|
+
const { dir, ast } = project(`
|
|
228
|
+
import type { Model, UUID } from "@supatype/types"
|
|
229
|
+
|
|
230
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
231
|
+
`)
|
|
232
|
+
const functionsDir = join(dir, "functions")
|
|
233
|
+
mkdirSync(join(functionsDir, "_supatype"), { recursive: true })
|
|
234
|
+
writeFileSync(join(functionsDir, "_supatype", "hooks.ts"), "export type Stale = true", "utf8")
|
|
235
|
+
|
|
236
|
+
expect(writeHooksModule(dir, functionsDir, ast)).toBeNull()
|
|
237
|
+
expect(existsSync(join(functionsDir, "_supatype", "hooks.ts"))).toBe(false)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("writes the module beside the functions that import it", () => {
|
|
241
|
+
const { dir, ast } = project(`
|
|
242
|
+
import type { Model, UUID } from "@supatype/types"
|
|
243
|
+
|
|
244
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts"; hooks: { beforeChange: "x" } }>
|
|
245
|
+
`)
|
|
246
|
+
const functionsDir = join(dir, "functions")
|
|
247
|
+
const written = writeHooksModule(dir, functionsDir, ast)
|
|
248
|
+
expect(written).toContain(join("functions", "_supatype", "hooks.ts"))
|
|
249
|
+
expect(readFileSync(join(functionsDir, "_supatype", "hooks.ts"), "utf8")).toContain("HookedTable")
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe("model hooks — doctor report", () => {
|
|
254
|
+
const schema = `
|
|
255
|
+
import type { Model, UUID } from "@supatype/types"
|
|
256
|
+
|
|
257
|
+
export type Post = Model<{ id: UUID }, {
|
|
258
|
+
tableName: "posts"
|
|
259
|
+
hooks: { beforeChange: "moderate-post" }
|
|
260
|
+
}>
|
|
261
|
+
`
|
|
262
|
+
|
|
263
|
+
it("reports nothing for a schema with no hooks", () => {
|
|
264
|
+
const { dir, ast } = project(`
|
|
265
|
+
import type { Model, UUID } from "@supatype/types"
|
|
266
|
+
|
|
267
|
+
export type Post = Model<{ id: UUID }, { tableName: "posts" }>
|
|
268
|
+
`)
|
|
269
|
+
expect(hooksReport(dir, join(dir, "functions"), ast).declared).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("marks a hook whose function is absent", () => {
|
|
273
|
+
const { dir, ast } = project(schema)
|
|
274
|
+
const report = hooksReport(dir, join(dir, "functions"), ast)
|
|
275
|
+
expect(report.declared).toHaveLength(1)
|
|
276
|
+
expect(report.missing).toHaveLength(1)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("catches functions being switched off, which makes every hook inert silently", () => {
|
|
280
|
+
const { dir, ast } = project(schema)
|
|
281
|
+
addFunction(dir, "moderate-post")
|
|
282
|
+
mkdirSync(join(dir, ".supatype"), { recursive: true })
|
|
283
|
+
writeFileSync(
|
|
284
|
+
join(dir, ".supatype", "manifest.json"),
|
|
285
|
+
JSON.stringify({ functions_enabled: false, hooks: { posts: {} } }),
|
|
286
|
+
"utf8",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const report = hooksReport(dir, join(dir, "functions"), ast)
|
|
290
|
+
expect(report.missing).toEqual([])
|
|
291
|
+
expect(report.functionsDisabled).toBe(true)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it("catches a manifest with no hook map, so the server has nothing to call", () => {
|
|
295
|
+
const { dir, ast } = project(schema)
|
|
296
|
+
addFunction(dir, "moderate-post")
|
|
297
|
+
mkdirSync(join(dir, ".supatype"), { recursive: true })
|
|
298
|
+
writeFileSync(
|
|
299
|
+
join(dir, ".supatype", "manifest.json"),
|
|
300
|
+
JSON.stringify({ functions_enabled: true }),
|
|
301
|
+
"utf8",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const report = hooksReport(dir, join(dir, "functions"), ast)
|
|
305
|
+
expect(report.mapMissing).toBe(true)
|
|
306
|
+
expect(report.functionsDisabled).toBe(false)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("is clean when the function exists and the manifest agrees", () => {
|
|
310
|
+
const { dir, ast } = project(schema)
|
|
311
|
+
addFunction(dir, "moderate-post")
|
|
312
|
+
mkdirSync(join(dir, ".supatype"), { recursive: true })
|
|
313
|
+
writeFileSync(
|
|
314
|
+
join(dir, ".supatype", "manifest.json"),
|
|
315
|
+
JSON.stringify({ functions_enabled: true, hooks: { posts: { beforeChange: { function: "moderate-post" } } } }),
|
|
316
|
+
"utf8",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const report = hooksReport(dir, join(dir, "functions"), ast)
|
|
320
|
+
expect(report).toMatchObject({ missing: [], functionsDisabled: false, mapMissing: false })
|
|
321
|
+
})
|
|
322
|
+
})
|