@supatype/cli 0.1.5 → 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 +143 -93
- 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 +43 -7
- 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 +131 -22
- 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/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +2 -0
- package/dist/commands/keys.js.map +1 -1
- 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 +129 -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 +46 -9
- 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 +153 -28
- 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/keys.ts +2 -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 +144 -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 +72 -4
- 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,125 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
|
|
6
|
+
import { ADAPTER_KEY, readHookUpload } from "../src/hook-upload.js"
|
|
7
|
+
|
|
8
|
+
function project(): { cwd: string; hooks: string } {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), "supatype-hook-upload-"))
|
|
10
|
+
const hooks = join(cwd, "hooks")
|
|
11
|
+
mkdirSync(hooks, { recursive: true })
|
|
12
|
+
return { cwd, hooks }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeHook(hooks: string, name: string, source: string, extra?: Record<string, string>): void {
|
|
16
|
+
const dir = join(hooks, name)
|
|
17
|
+
mkdirSync(dir, { recursive: true })
|
|
18
|
+
writeFileSync(join(dir, "index.ts"), source, "utf8")
|
|
19
|
+
for (const [file, contents] of Object.entries(extra ?? {})) {
|
|
20
|
+
writeFileSync(join(dir, file), contents, "utf8")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An AST shaped as the extractor produces, carrying one hooked model. */
|
|
25
|
+
function astWith(hooks: Record<string, string>): unknown {
|
|
26
|
+
return {
|
|
27
|
+
models: [
|
|
28
|
+
{
|
|
29
|
+
name: "Post",
|
|
30
|
+
fields: [],
|
|
31
|
+
annotations: {
|
|
32
|
+
db: { tableName: "posts" },
|
|
33
|
+
platform: {
|
|
34
|
+
hooks: Object.fromEntries(
|
|
35
|
+
Object.entries(hooks).map(([event, fn]) => [event, { function: fn }]),
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const HANDLER = `import { hook, type BeforeChange } from "../_supatype/hooks.ts"
|
|
45
|
+
|
|
46
|
+
const moderate: BeforeChange<"posts"> = async () => ({})
|
|
47
|
+
export default hook(moderate)
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
describe("readHookUpload", () => {
|
|
51
|
+
it("is null when the schema names no hooks", () => {
|
|
52
|
+
const { cwd, hooks } = project()
|
|
53
|
+
expect(readHookUpload(cwd, hooks, astWith({}))).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("rewrites the generated adapter import to the flattened name", () => {
|
|
57
|
+
// The nested path cannot survive flattening, and this is the specifier our own scaffold writes.
|
|
58
|
+
const { cwd, hooks } = project()
|
|
59
|
+
writeHook(hooks, "moderate-post", HANDLER)
|
|
60
|
+
|
|
61
|
+
const upload = readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" }))
|
|
62
|
+
expect(upload?.handlers).toHaveLength(1)
|
|
63
|
+
expect(upload?.handlers[0]?.source).toContain(`./${ADAPTER_KEY}`)
|
|
64
|
+
expect(upload?.handlers[0]?.source).not.toContain("../_supatype/hooks.ts")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("uploads only the handlers the schema names", () => {
|
|
68
|
+
// An unnamed handler would sit on the worker holding the service-role key with nothing able to
|
|
69
|
+
// call it. Uploading it buys nothing and widens what runs with that credential.
|
|
70
|
+
const { cwd, hooks } = project()
|
|
71
|
+
writeHook(hooks, "moderate-post", HANDLER)
|
|
72
|
+
writeHook(hooks, "abandoned", HANDLER)
|
|
73
|
+
|
|
74
|
+
const upload = readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" }))
|
|
75
|
+
expect(upload?.handlers.map((h) => h.name)).toEqual(["moderate-post"])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("carries the hook map alongside the sources", () => {
|
|
79
|
+
// Both halves in one payload: a map naming a handler whose source has not arrived is the failure
|
|
80
|
+
// this whole feature is meant to avoid.
|
|
81
|
+
const { cwd, hooks } = project()
|
|
82
|
+
writeHook(hooks, "moderate-post", HANDLER)
|
|
83
|
+
|
|
84
|
+
const upload = readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" }))
|
|
85
|
+
expect(upload?.map["posts"]?.["beforeChange"]?.function).toBe("moderate-post")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("refuses a hook with a second file rather than dropping it", () => {
|
|
89
|
+
// Silent truncation would deploy successfully and fail at import — which reads to the caller as
|
|
90
|
+
// that table's writes being broken for no visible reason.
|
|
91
|
+
const { cwd, hooks } = project()
|
|
92
|
+
writeHook(hooks, "moderate-post", HANDLER, { "helpers.ts": "export const x = 1\n" })
|
|
93
|
+
|
|
94
|
+
expect(() => readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" })))
|
|
95
|
+
.toThrow(/more than one file \(helpers\.ts\)/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("refuses a relative import it cannot resolve after flattening", () => {
|
|
99
|
+
const { cwd, hooks } = project()
|
|
100
|
+
writeHook(hooks, "moderate-post", `import { x } from "./helpers.ts"\nexport default () => x\n`)
|
|
101
|
+
|
|
102
|
+
expect(() => readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" })))
|
|
103
|
+
.toThrow(/imports \.\/helpers\.ts/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("leaves bare specifiers and URLs alone", () => {
|
|
107
|
+
// Only relative paths depend on the layout. A hook is expected to import from npm: or https:.
|
|
108
|
+
const { cwd, hooks } = project()
|
|
109
|
+
writeHook(
|
|
110
|
+
hooks,
|
|
111
|
+
"moderate-post",
|
|
112
|
+
`import { z } from "npm:zod"\nimport { j } from "https://esm.sh/joi"\n` + HANDLER,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const upload = readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" }))
|
|
116
|
+
expect(upload?.handlers).toHaveLength(1)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("names the missing handler when a declared hook has no source", () => {
|
|
120
|
+
const { cwd, hooks } = project()
|
|
121
|
+
|
|
122
|
+
expect(() => readHookUpload(cwd, hooks, astWith({ beforeChange: "moderate-post" })))
|
|
123
|
+
.toThrow(/moderate-post/)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -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
|
+
})
|