@supatype/cli 0.3.1 → 0.3.3
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 +168 -158
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +77 -0
- package/dist/api-config-cache.d.ts +20 -0
- package/dist/api-config-cache.d.ts.map +1 -1
- package/dist/api-config-cache.js +27 -0
- package/dist/api-config-cache.js.map +1 -1
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +126 -26
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/client-generator.d.ts +3 -0
- package/dist/client-generator.d.ts.map +1 -0
- package/dist/client-generator.js +108 -0
- package/dist/client-generator.js.map +1 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +2 -2
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +3 -3
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +36 -29
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +14 -13
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/seed.d.ts +27 -1
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +206 -33
- package/dist/commands/seed.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +19 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/db-port.d.ts +36 -0
- package/dist/db-port.d.ts.map +1 -0
- package/dist/db-port.js +90 -0
- package/dist/db-port.js.map +1 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +108 -13
- package/dist/dev-compose.js.map +1 -1
- package/dist/engine-client.d.ts +1 -0
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +47 -2
- package/dist/engine-client.js.map +1 -1
- package/dist/engine-floor.d.ts +27 -4
- package/dist/engine-floor.d.ts.map +1 -1
- package/dist/engine-floor.js +90 -9
- package/dist/engine-floor.js.map +1 -1
- package/dist/functions-context-refresh.d.ts +4 -0
- package/dist/functions-context-refresh.d.ts.map +1 -0
- package/dist/functions-context-refresh.js +26 -0
- package/dist/functions-context-refresh.js.map +1 -0
- package/dist/functions-deno-types.d.ts +9 -0
- package/dist/functions-deno-types.d.ts.map +1 -1
- package/dist/functions-deno-types.js +21 -1
- package/dist/functions-deno-types.js.map +1 -1
- package/dist/gitignore.d.ts +3 -1
- package/dist/gitignore.d.ts.map +1 -1
- package/dist/gitignore.js +35 -6
- package/dist/gitignore.js.map +1 -1
- package/dist/hooks-generator.d.ts.map +1 -1
- package/dist/hooks-generator.js +18 -3
- package/dist/hooks-generator.js.map +1 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +17 -8
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/seed-connection.d.ts +60 -0
- package/dist/seed-connection.d.ts.map +1 -0
- package/dist/seed-connection.js +90 -0
- package/dist/seed-connection.js.map +1 -0
- package/dist/seed-ir.d.ts +66 -0
- package/dist/seed-ir.d.ts.map +1 -0
- package/dist/seed-ir.js +265 -0
- package/dist/seed-ir.js.map +1 -0
- package/dist/seed-runner.d.ts +44 -0
- package/dist/seed-runner.d.ts.map +1 -0
- package/dist/seed-runner.js +83 -0
- package/dist/seed-runner.js.map +1 -0
- package/dist/seed.d.ts +345 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +138 -0
- package/dist/seed.js.map +1 -1
- package/dist/self-host-compose.d.ts +10 -2
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +73 -6
- package/dist/self-host-compose.js.map +1 -1
- package/dist/strict.d.ts +29 -0
- package/dist/strict.d.ts.map +1 -0
- package/dist/strict.js +37 -0
- package/dist/strict.js.map +1 -0
- package/dist/studio-dev-server.d.ts +12 -2
- package/dist/studio-dev-server.d.ts.map +1 -1
- package/dist/studio-dev-server.js +12 -2
- package/dist/studio-dev-server.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +81 -2
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-generation.d.ts +43 -0
- package/dist/type-generation.d.ts.map +1 -1
- package/dist/type-generation.js +93 -2
- package/dist/type-generation.js.map +1 -1
- package/package.json +2 -2
- package/src/api-config-cache.ts +30 -0
- package/src/augmentation-generator.ts +142 -26
- package/src/cli-version-embedded.ts +1 -1
- package/src/client-generator.ts +122 -0
- package/src/commands/functions.ts +5 -2
- package/src/commands/generate.ts +7 -3
- package/src/commands/init.ts +41 -31
- package/src/commands/push.ts +14 -15
- package/src/commands/seed.ts +294 -44
- package/src/commands/update.ts +20 -1
- package/src/db-port.ts +99 -0
- package/src/dev-compose.ts +116 -16
- package/src/engine-client.ts +52 -3
- package/src/engine-floor.ts +100 -9
- package/src/functions-context-refresh.ts +25 -0
- package/src/functions-deno-types.ts +24 -1
- package/src/gitignore.ts +38 -10
- package/src/hooks-generator.ts +21 -6
- package/src/schema-ast-v2.ts +17 -8
- package/src/seed-connection.ts +146 -0
- package/src/seed-ir.ts +311 -0
- package/src/seed-runner.ts +109 -0
- package/src/seed.ts +408 -0
- package/src/self-host-compose.ts +74 -6
- package/src/strict.ts +40 -0
- package/src/studio-dev-server.ts +12 -2
- package/src/type-extractor.ts +109 -2
- package/src/type-generation.ts +111 -2
- package/tests/access-composition.test.ts +123 -0
- package/tests/ast-derived-outputs.test.ts +92 -0
- package/tests/augmentation-generator.test.ts +273 -7
- package/tests/client-generator.test.ts +103 -0
- package/tests/db-port.test.ts +128 -0
- package/tests/engine-floor.test.ts +93 -0
- package/tests/external-database-compose.test.ts +8 -1
- package/tests/fixtures/seed_golden_ir.json +186 -0
- package/tests/functions-context-refresh.test.ts +86 -0
- package/tests/gitignore-secrets.test.ts +90 -0
- package/tests/runtime-contract.test.ts +146 -8
- package/tests/seed-connection.test.ts +158 -0
- package/tests/seed-ir.test.ts +436 -0
- package/tests/strict.test.ts +78 -0
- package/tests/type-extractor.test.ts +34 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
{
|
|
2
|
+
"irVersion": 1,
|
|
3
|
+
"schemaFingerprint": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
4
|
+
"operations": [
|
|
5
|
+
{
|
|
6
|
+
"op": "upsert",
|
|
7
|
+
"model": "Profile",
|
|
8
|
+
"id": "alex",
|
|
9
|
+
"where": {
|
|
10
|
+
"id": "a0000000-0000-4000-8000-000000000001"
|
|
11
|
+
},
|
|
12
|
+
"data": {
|
|
13
|
+
"display_name": "Alex Rivera",
|
|
14
|
+
"full_name": "Alex Rivera"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"op": "upsert",
|
|
19
|
+
"model": "Organiser",
|
|
20
|
+
"id": "northern",
|
|
21
|
+
"where": {
|
|
22
|
+
"slug": "northern-lights-events"
|
|
23
|
+
},
|
|
24
|
+
"data": {
|
|
25
|
+
"name": "Northern Lights Events",
|
|
26
|
+
"contact_email": "hello@northernlights.test",
|
|
27
|
+
"owner": {
|
|
28
|
+
"connect": {
|
|
29
|
+
"id": {
|
|
30
|
+
"$ref": "alex.id"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"events": {
|
|
35
|
+
"create": [
|
|
36
|
+
{
|
|
37
|
+
"title": "Southwest Dev Conference",
|
|
38
|
+
"status": "published",
|
|
39
|
+
"starts_at": {
|
|
40
|
+
"kind": "after",
|
|
41
|
+
"base": {
|
|
42
|
+
"kind": "startOf",
|
|
43
|
+
"unit": "day"
|
|
44
|
+
},
|
|
45
|
+
"interval": {
|
|
46
|
+
"days": 30,
|
|
47
|
+
"hours": 9
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"ends_at": {
|
|
51
|
+
"kind": "after",
|
|
52
|
+
"base": {
|
|
53
|
+
"kind": "startOf",
|
|
54
|
+
"unit": "day"
|
|
55
|
+
},
|
|
56
|
+
"interval": {
|
|
57
|
+
"days": 30,
|
|
58
|
+
"hours": 18
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"created_at": {
|
|
62
|
+
"kind": "now"
|
|
63
|
+
},
|
|
64
|
+
"ticket_types": {
|
|
65
|
+
"createMany": [
|
|
66
|
+
{
|
|
67
|
+
"name": "Early Bird",
|
|
68
|
+
"price": {
|
|
69
|
+
"$numeric": "75.0000"
|
|
70
|
+
},
|
|
71
|
+
"quantity": 100
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "Standard",
|
|
75
|
+
"price": {
|
|
76
|
+
"$numeric": "125.0000"
|
|
77
|
+
},
|
|
78
|
+
"quantity": 250
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"op": "createMany",
|
|
89
|
+
"model": "Venue",
|
|
90
|
+
"rows": [
|
|
91
|
+
{
|
|
92
|
+
"name": "The Exchange",
|
|
93
|
+
"city": "Bristol",
|
|
94
|
+
"capacity": 450,
|
|
95
|
+
"country": {
|
|
96
|
+
"$default": true
|
|
97
|
+
},
|
|
98
|
+
"organiser": {
|
|
99
|
+
"connect": {
|
|
100
|
+
"slug": "northern-lights-events"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "Archway Studios",
|
|
106
|
+
"city": "London",
|
|
107
|
+
"capacity": 180,
|
|
108
|
+
"country": {
|
|
109
|
+
"$default": true
|
|
110
|
+
},
|
|
111
|
+
"organiser": {
|
|
112
|
+
"connect": {
|
|
113
|
+
"slug": "northern-lights-events"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"op": "group",
|
|
121
|
+
"if": {
|
|
122
|
+
"kind": "notExists",
|
|
123
|
+
"model": "SiteSettings"
|
|
124
|
+
},
|
|
125
|
+
"operations": [
|
|
126
|
+
{
|
|
127
|
+
"op": "create",
|
|
128
|
+
"model": "SiteSettings",
|
|
129
|
+
"data": {
|
|
130
|
+
"site_name": "Launch Test Tickets",
|
|
131
|
+
"default_currency": "GBP"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"op": "group",
|
|
138
|
+
"if": {
|
|
139
|
+
"kind": "exists",
|
|
140
|
+
"model": "Event",
|
|
141
|
+
"where": {
|
|
142
|
+
"type": "compare",
|
|
143
|
+
"op": "eq",
|
|
144
|
+
"left": {
|
|
145
|
+
"kind": "column",
|
|
146
|
+
"name": "status"
|
|
147
|
+
},
|
|
148
|
+
"right": {
|
|
149
|
+
"kind": "literal",
|
|
150
|
+
"value": "published"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"operations": [
|
|
155
|
+
{
|
|
156
|
+
"op": "create",
|
|
157
|
+
"model": "PromoCode",
|
|
158
|
+
"data": {
|
|
159
|
+
"code": "EARLY20",
|
|
160
|
+
"discount_kind": "percentage",
|
|
161
|
+
"discount_value": 20,
|
|
162
|
+
"valid_until": {
|
|
163
|
+
"kind": "after",
|
|
164
|
+
"base": {
|
|
165
|
+
"kind": "now"
|
|
166
|
+
},
|
|
167
|
+
"interval": {
|
|
168
|
+
"days": 25
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"event": {
|
|
172
|
+
"connect": {
|
|
173
|
+
"slug": "southwest-dev-conference"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"op": "sql",
|
|
182
|
+
"text": "ANALYZE event",
|
|
183
|
+
"params": []
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `functions/_shared/context.ts` reaches a project that already exists.
|
|
3
|
+
*
|
|
4
|
+
* The module was written by `init` and `supatype functions new`, and by nothing else. So it arrived
|
|
5
|
+
* with new projects and new functions, and never arrived at a project that already had functions
|
|
6
|
+
* when the two-argument handler contract landed: those keep `(req)` handlers with no
|
|
7
|
+
* `FunctionContext` to import, while the generated file itself asserts
|
|
8
|
+
*
|
|
9
|
+
* Regenerated by the CLI. Edits are overwritten.
|
|
10
|
+
*
|
|
11
|
+
* which was true only if you scaffolded again. `tests/integration` in this repo had four functions
|
|
12
|
+
* and no context module at all, which is how it was found.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it, beforeEach, afterEach } from "vitest"
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { join } from "node:path"
|
|
17
|
+
import { tmpdir } from "node:os"
|
|
18
|
+
import { refreshFunctionsContext } from "../src/functions-context-refresh.js"
|
|
19
|
+
import type { SupatypeProjectConfig } from "../src/project-config.js"
|
|
20
|
+
|
|
21
|
+
const config: SupatypeProjectConfig = {
|
|
22
|
+
project: { name: "ctx" },
|
|
23
|
+
database: { provider: "docker" },
|
|
24
|
+
} as SupatypeProjectConfig
|
|
25
|
+
|
|
26
|
+
let dir: string
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
dir = mkdtempSync(join(tmpdir(), "supatype-ctx-"))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** A project with functions but no `_shared`, which is what a pre-migration project looks like. */
|
|
37
|
+
function projectWithFunctions(): string {
|
|
38
|
+
const functions = join(dir, "functions")
|
|
39
|
+
mkdirSync(functions, { recursive: true })
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(functions, "ping.ts"),
|
|
42
|
+
"export default function ping(_req: Request) { return new Response('ok') }\n",
|
|
43
|
+
"utf8",
|
|
44
|
+
)
|
|
45
|
+
return functions
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("a project that already has functions", () => {
|
|
49
|
+
it("gets the context module written", () => {
|
|
50
|
+
const functions = projectWithFunctions()
|
|
51
|
+
expect(existsSync(join(functions, "_shared", "context.ts"))).toBe(false)
|
|
52
|
+
|
|
53
|
+
expect(refreshFunctionsContext(dir, config)).toBe(true)
|
|
54
|
+
expect(existsSync(join(functions, "_shared", "context.ts"))).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("writes a module that actually declares the contract", () => {
|
|
58
|
+
// Not merely that a file appeared. The point is the type handlers import.
|
|
59
|
+
projectWithFunctions()
|
|
60
|
+
refreshFunctionsContext(dir, config)
|
|
61
|
+
const source = readFileSync(join(dir, "functions", "_shared", "context.ts"), "utf8")
|
|
62
|
+
expect(source).toContain("FunctionContext")
|
|
63
|
+
expect(source).toContain("executionId")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("rewrites a stale copy rather than leaving it", () => {
|
|
67
|
+
// The file says edits are overwritten, so they have to be.
|
|
68
|
+
const functions = projectWithFunctions()
|
|
69
|
+
mkdirSync(join(functions, "_shared"), { recursive: true })
|
|
70
|
+
writeFileSync(join(functions, "_shared", "context.ts"), "// stale\n", "utf8")
|
|
71
|
+
|
|
72
|
+
refreshFunctionsContext(dir, config)
|
|
73
|
+
const source = readFileSync(join(functions, "_shared", "context.ts"), "utf8")
|
|
74
|
+
expect(source).not.toBe("// stale\n")
|
|
75
|
+
expect(source).toContain("FunctionContext")
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("a project with no functions", () => {
|
|
80
|
+
it("is left alone", () => {
|
|
81
|
+
// Writing `_shared/` into a project with no functions would create the thing it describes out
|
|
82
|
+
// of nothing, and leave a directory nobody asked for in the diff.
|
|
83
|
+
expect(refreshFunctionsContext(dir, config)).toBe(false)
|
|
84
|
+
expect(existsSync(join(dir, "functions"))).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `.gitignore` we write has to cover the secrets we write.
|
|
3
|
+
*
|
|
4
|
+
* `supatype functions new` writes `functions/.env.local` with "These are NOT committed to git" at
|
|
5
|
+
* the top of it, and nothing made that true: `.env` matches a file named exactly `.env`, and both
|
|
6
|
+
* `.env.local` and `.env.<function>.local` were tracked.
|
|
7
|
+
*
|
|
8
|
+
* Asserted through `git check-ignore` rather than by reading the patterns, because the question is
|
|
9
|
+
* what git does with them and a pattern that looks right is how this got shipped.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, beforeEach, afterEach } from "vitest"
|
|
13
|
+
import { execFileSync } from "node:child_process"
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
15
|
+
import { join } from "node:path"
|
|
16
|
+
import { tmpdir } from "node:os"
|
|
17
|
+
import { SUPATYPE_GITIGNORE_BLOCK, SUPATYPE_GITIGNORE_PATHS } from "../src/gitignore.js"
|
|
18
|
+
|
|
19
|
+
let dir: string
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
dir = mkdtempSync(join(tmpdir(), "supatype-ignore-"))
|
|
23
|
+
execFileSync("git", ["init", "-q", "."], { cwd: dir })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** What git itself says about a path, which is the only answer that counts. */
|
|
31
|
+
function ignored(relPath: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync("git", ["check-ignore", "-q", relPath], { cwd: dir })
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeIgnore(contents: string): void {
|
|
41
|
+
writeFileSync(join(dir, ".gitignore"), contents, "utf8")
|
|
42
|
+
mkdirSync(join(dir, "functions"), { recursive: true })
|
|
43
|
+
mkdirSync(join(dir, "src"), { recursive: true })
|
|
44
|
+
for (const f of [
|
|
45
|
+
".env",
|
|
46
|
+
".env.local",
|
|
47
|
+
"functions/.env.local",
|
|
48
|
+
"functions/.env.checkout.local",
|
|
49
|
+
"src/index.ts",
|
|
50
|
+
"src/env.local.ts",
|
|
51
|
+
]) {
|
|
52
|
+
writeFileSync(join(dir, f), "x", "utf8")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("the block written into an existing .gitignore", () => {
|
|
57
|
+
beforeEach(() => writeIgnore(SUPATYPE_GITIGNORE_BLOCK))
|
|
58
|
+
|
|
59
|
+
it("ignores the plain env file", () => {
|
|
60
|
+
expect(ignored(".env")).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("ignores the function env files, at the root and nested", () => {
|
|
64
|
+
// The ones `supatype functions new` writes and the worker reads.
|
|
65
|
+
expect(ignored(".env.local")).toBe(true)
|
|
66
|
+
expect(ignored("functions/.env.local")).toBe(true)
|
|
67
|
+
expect(ignored("functions/.env.checkout.local")).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("does not swallow source files", () => {
|
|
71
|
+
// `.env.*.local` is a glob, and a careless widening of it catches real code.
|
|
72
|
+
expect(ignored("src/index.ts")).toBe(false)
|
|
73
|
+
expect(ignored("src/env.local.ts")).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("the two emitters", () => {
|
|
78
|
+
it("agree, because they read one list", () => {
|
|
79
|
+
// They were separate copies and had already drifted. The copy that drifts is the one found
|
|
80
|
+
// when a key reaches a public repository.
|
|
81
|
+
for (const path of SUPATYPE_GITIGNORE_PATHS) {
|
|
82
|
+
expect(SUPATYPE_GITIGNORE_BLOCK).toContain(path)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("carries the env patterns `.env` alone does not cover", () => {
|
|
87
|
+
expect(SUPATYPE_GITIGNORE_PATHS).toContain(".env.local")
|
|
88
|
+
expect(SUPATYPE_GITIGNORE_PATHS).toContain(".env.*.local")
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -9,6 +9,22 @@ import { updateAppConfigInProject, updateServerConfigInProject } from "../src/ap
|
|
|
9
9
|
import type { SupatypeProjectConfig } from "../src/project-config.js"
|
|
10
10
|
import { DENO_RELEASE_PIN } from "../src/release-pins.js"
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* One service's block from a rendered compose file.
|
|
14
|
+
*
|
|
15
|
+
* Compose is asserted as text here rather than parsed, because the CLI ships no YAML parser and a
|
|
16
|
+
* test is not worth a dependency. Slicing to a single service is what keeps that honest: service
|
|
17
|
+
* names and `condition: service_healthy` both recur, so a whole-document substring match would
|
|
18
|
+
* pass on another service's dependency.
|
|
19
|
+
*/
|
|
20
|
+
function serviceBlock(compose: string, name: string): string {
|
|
21
|
+
const lines = compose.split("\n")
|
|
22
|
+
const start = lines.indexOf(` ${name}:`)
|
|
23
|
+
if (start === -1) return ""
|
|
24
|
+
const after = lines.slice(start + 1).findIndex((l) => /^ {2}[a-z][a-z0-9-]*:$/.test(l))
|
|
25
|
+
return lines.slice(start, after === -1 ? lines.length : start + 1 + after).join("\n")
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
const baseConfig: SupatypeProjectConfig = {
|
|
13
29
|
project: { name: "acme" },
|
|
14
30
|
database: { provider: "docker" },
|
|
@@ -260,22 +276,47 @@ describe("runtime contract", () => {
|
|
|
260
276
|
expect(compose).not.toContain("- ./kong.yml:/etc/kong/kong.yml:ro")
|
|
261
277
|
})
|
|
262
278
|
|
|
263
|
-
it("devLocal compose omits
|
|
279
|
+
it("devLocal compose omits the production ports", () => {
|
|
264
280
|
const compose = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
265
281
|
expect(compose).not.toContain('"5432:5432"')
|
|
266
282
|
expect(compose).not.toContain('"9999:9999"')
|
|
267
283
|
expect(compose).toContain("${SUPATYPE_KONG_PORT:-18473}:8000")
|
|
268
|
-
expect(compose).not.toContain("SUPATYPE_DEV_DB_PORT")
|
|
269
284
|
})
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
/**
|
|
287
|
+
* The database is reachable from this machine, always.
|
|
288
|
+
*
|
|
289
|
+
* It used to be published in dev only when something was known to need it, which meant a
|
|
290
|
+
* fresh project had a database nothing on the host could reach: `supatype seed` resolves a
|
|
291
|
+
* connection string and the engine connects over TCP, so the first seed got ECONNREFUSED
|
|
292
|
+
* and the remedy was an environment variable nobody had reason to know about.
|
|
293
|
+
*/
|
|
294
|
+
it("devLocal compose publishes the database on loopback", () => {
|
|
295
|
+
const compose = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
296
|
+
expect(compose).toContain("127.0.0.1:${SUPATYPE_DEV_DB_PORT:-54329}:5432")
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
/** Loopback, so it is reachable from here and from nowhere else. */
|
|
300
|
+
it("devLocal never publishes the database beyond this machine", () => {
|
|
301
|
+
const compose = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
302
|
+
const dbPorts = compose
|
|
303
|
+
.split("\n")
|
|
304
|
+
.filter((line) => line.includes(":5432"))
|
|
305
|
+
.filter((line) => line.trim().startsWith("-"))
|
|
306
|
+
expect(dbPorts.length).toBeGreaterThan(0)
|
|
307
|
+
for (const line of dbPorts) expect(line).toContain("127.0.0.1:")
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it("an engine override no longer decides whether the database is published", () => {
|
|
311
|
+
const withOverride = renderSelfHostCompose(
|
|
273
312
|
{ ...baseConfig, overrides: { engine: "/tmp/supatype-engine" } },
|
|
274
313
|
process.cwd(),
|
|
275
314
|
{ devLocal: true },
|
|
276
315
|
)
|
|
277
|
-
|
|
278
|
-
expect(
|
|
316
|
+
const without = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
317
|
+
expect(withOverride).toContain("127.0.0.1:${SUPATYPE_DEV_DB_PORT:-54329}:5432")
|
|
318
|
+
expect(without).toContain("127.0.0.1:${SUPATYPE_DEV_DB_PORT:-54329}:5432")
|
|
319
|
+
expect(withOverride).not.toContain('"5432:5432"')
|
|
279
320
|
})
|
|
280
321
|
|
|
281
322
|
it("devLocal proxy upstream rewrites localhost to host.docker.internal", () => {
|
|
@@ -375,9 +416,52 @@ describe("runtime contract", () => {
|
|
|
375
416
|
}
|
|
376
417
|
})
|
|
377
418
|
|
|
378
|
-
it("
|
|
419
|
+
it("maps host.docker.internal, so a host proxy works on Linux too", () => {
|
|
420
|
+
// Docker Desktop resolves this name; Linux does not unless it is mapped. A project proxying
|
|
421
|
+
// the site or Studio to something on the host therefore worked on macOS and Windows and
|
|
422
|
+
// failed on Linux, with the gateway reaching nothing and no error naming the cause.
|
|
379
423
|
const compose = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
380
|
-
|
|
424
|
+
for (const service of ["server", "kong"]) {
|
|
425
|
+
expect(serviceBlock(compose, service), service).toContain(
|
|
426
|
+
'- "host.docker.internal:host-gateway"',
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it("publishes the database when a port is asked for", () => {
|
|
432
|
+
// In dev the port is published only when something on the host needs it, which is normally a
|
|
433
|
+
// host engine build. Naming a port is the same request: without this the number was honoured
|
|
434
|
+
// and the port never existed, so anything connecting over TCP got ECONNREFUSED.
|
|
435
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-dbport-"))
|
|
436
|
+
try {
|
|
437
|
+
writeFileSync(join(dir, ".env"), "SUPATYPE_DEV_DB_PORT=54329\n")
|
|
438
|
+
const compose = renderSelfHostCompose(baseConfig, dir, { devLocal: true })
|
|
439
|
+
// The number stays a compose variable so Docker resolves it; what changes is that the
|
|
440
|
+
// ports block exists at all.
|
|
441
|
+
expect(serviceBlock(compose, "db")).toContain("ports:")
|
|
442
|
+
expect(serviceBlock(compose, "db")).toContain("SUPATYPE_DEV_DB_PORT")
|
|
443
|
+
} finally {
|
|
444
|
+
rmSync(dir, { recursive: true, force: true })
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it("leaves the database unpublished when nothing asked for it", () => {
|
|
449
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-dbport-"))
|
|
450
|
+
try {
|
|
451
|
+
writeFileSync(join(dir, ".env"), "ANON_KEY=x\n")
|
|
452
|
+
expect(serviceBlock(renderSelfHostCompose(baseConfig, dir, { devLocal: true }), "db"))
|
|
453
|
+
.not.toContain("5432:5432")
|
|
454
|
+
} finally {
|
|
455
|
+
rmSync(dir, { recursive: true, force: true })
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it("devLocal compose enables STUDIO_OPEN_DEV on supatype-server, and lets it be turned off", () => {
|
|
460
|
+
const compose = renderSelfHostCompose(baseConfig, process.cwd(), { devLocal: true })
|
|
461
|
+
// Defaulted rather than hardcoded. `supatype dev` still opens Studio without a sign-in, and
|
|
462
|
+
// the browser suite can set STUDIO_OPEN_DEV=0 to drive the gate a deployment actually has.
|
|
463
|
+
// Held as a literal "1" there was no way to test Studio with its authorization on at all.
|
|
464
|
+
expect(compose).toContain('STUDIO_OPEN_DEV: "${STUDIO_OPEN_DEV:-1}"')
|
|
381
465
|
})
|
|
382
466
|
|
|
383
467
|
it("production self-host compose does not enable STUDIO_OPEN_DEV", () => {
|
|
@@ -597,6 +681,60 @@ export default defineConfig({
|
|
|
597
681
|
}
|
|
598
682
|
})
|
|
599
683
|
|
|
684
|
+
it("offers functions a database URL, off unless the operator sets one", () => {
|
|
685
|
+
// `ctx.dbUrl` is read from SUPATYPE_DB_URL by the worker, and the generated compose never set
|
|
686
|
+
// it, so the field was permanently undefined on self-host: a function reaching for it got no
|
|
687
|
+
// value and nothing to explain why.
|
|
688
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-compose-"))
|
|
689
|
+
try {
|
|
690
|
+
const compose = readFileSync(
|
|
691
|
+
writeSelfHostCompose(dir, { ...baseConfig, app: { mode: "none" } }).composePath,
|
|
692
|
+
"utf8",
|
|
693
|
+
)
|
|
694
|
+
const worker = serviceBlock(compose, "functions-worker")
|
|
695
|
+
|
|
696
|
+
// Passed through, so the capability is reachable at all.
|
|
697
|
+
expect(worker).toContain("SUPATYPE_DB_URL:")
|
|
698
|
+
// Empty by default. The worker treats an empty value as absent, so `ctx.dbUrl` stays
|
|
699
|
+
// undefined until somebody opts in.
|
|
700
|
+
expect(worker).toContain("SUPATYPE_DB_URL: ${SUPATYPE_FUNCTIONS_DB_URL:-}")
|
|
701
|
+
// And its own variable, not the project's owner DSN. Interpolating DATABASE_URL here would
|
|
702
|
+
// give every function a connection that bypasses access rules, masking and hooks.
|
|
703
|
+
expect(worker).not.toContain("SUPATYPE_DB_URL: ${DATABASE_URL")
|
|
704
|
+
} finally {
|
|
705
|
+
rmSync(dir, { recursive: true, force: true })
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it("starts storage only once the object store is answering", () => {
|
|
710
|
+
// seaweedfs had no healthcheck and nothing depended on it, so compose started it alongside
|
|
711
|
+
// storage. Storage then accepted a bucket creation before seaweedfs was listening, every
|
|
712
|
+
// bucket in the schema failed with `connect ECONNREFUSED <ip>:8333`, and the metadata row was
|
|
713
|
+
// written anyway so a later push looked fine. Measured on a live stack: seaweedfs started
|
|
714
|
+
// twelve minutes after storage with RestartCount 0.
|
|
715
|
+
//
|
|
716
|
+
// It lands on the first push after a stack comes up, which is the first push anyone runs.
|
|
717
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-compose-"))
|
|
718
|
+
try {
|
|
719
|
+
const out = writeSelfHostCompose(dir, { ...baseConfig, app: { mode: "none" } })
|
|
720
|
+
const compose = readFileSync(out.composePath, "utf8")
|
|
721
|
+
|
|
722
|
+
// Sliced to one service, because `seaweedfs` and `service_healthy` both appear elsewhere in
|
|
723
|
+
// this file and a whole-document substring match would pass on either.
|
|
724
|
+
const storage = serviceBlock(compose, "storage")
|
|
725
|
+
expect(storage).toContain("depends_on:")
|
|
726
|
+
expect(storage).toMatch(/seaweedfs:\s*\n\s*condition: service_healthy/)
|
|
727
|
+
// Still waits for the database. The fix must not swap one dependency for another.
|
|
728
|
+
expect(storage).toMatch(/db:\s*\n\s*condition: service_healthy/)
|
|
729
|
+
|
|
730
|
+
// `service_healthy` is only meaningful if the service defines health. Without it compose
|
|
731
|
+
// refuses to start the stack at all, which is a worse failure than the race.
|
|
732
|
+
expect(serviceBlock(compose, "seaweedfs")).toContain("healthcheck:")
|
|
733
|
+
} finally {
|
|
734
|
+
rmSync(dir, { recursive: true, force: true })
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
|
|
600
738
|
it("renders SeaweedFS with its identities and its own volume", () => {
|
|
601
739
|
const dir = mkdtempSync(join(tmpdir(), "supatype-seaweed-"))
|
|
602
740
|
try {
|