@fabrika/app 0.8.2 → 0.9.0-cloudflare.1

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.
Files changed (194) hide show
  1. package/package.json +40 -24
  2. package/skill/SKILL.md +27 -3
  3. package/skill/references/app-setup.md +8 -8
  4. package/skill/references/dev.md +2 -1
  5. package/skill/references/endpoint.md +7 -3
  6. package/skill/references/errors.md +39 -35
  7. package/skill/references/files.md +4 -2
  8. package/skill/references/table-and-migration.md +19 -2
  9. package/skill/references/tests.md +11 -0
  10. package/skill/references/webhooks-links-connectors.md +1 -1
  11. package/src/build-recipe.ts +198 -0
  12. package/src/build.ts +55 -4
  13. package/src/bun.ts +23 -8
  14. package/src/cloudflare/application.ts +352 -0
  15. package/src/cloudflare/bindings.ts +138 -0
  16. package/src/cloudflare/names.ts +10 -0
  17. package/src/cloudflare/ops.ts +246 -0
  18. package/src/cloudflare/r2-files.ts +172 -0
  19. package/src/cloudflare.ts +99 -146
  20. package/src/contract/app-id.ts +5 -0
  21. package/src/contract/artifact-schema.ts +2 -0
  22. package/src/contract/build.ts +35 -1
  23. package/src/contract/compile.ts +1 -0
  24. package/src/contract/context.ts +9 -0
  25. package/src/contract/declaration.ts +2 -0
  26. package/src/contract/define.ts +28 -2
  27. package/src/contract/kit.ts +4 -1
  28. package/src/contract/platform.ts +3 -2
  29. package/src/contract/vocabulary.ts +26 -8
  30. package/src/dev/changes.ts +35 -0
  31. package/src/dev/contract-digest.ts +17 -0
  32. package/src/dev/index.ts +11 -0
  33. package/src/dev/launcher.ts +713 -0
  34. package/src/dev/output.ts +72 -0
  35. package/src/dev/wire.ts +136 -0
  36. package/src/files/archive.ts +2 -2
  37. package/src/files/job-names.ts +12 -0
  38. package/src/files/kinds.ts +19 -7
  39. package/src/files/scan.ts +3 -4
  40. package/src/files/streams.ts +3 -1
  41. package/src/implement/anonymous.ts +11 -1
  42. package/src/implement/boot.ts +61 -4
  43. package/src/implement/bounded.ts +21 -2
  44. package/src/implement/capability.ts +15 -3
  45. package/src/implement/dispatch.ts +249 -145
  46. package/src/implement/failures.ts +17 -1
  47. package/src/implement/faults.ts +1 -0
  48. package/src/implement/index.ts +1 -0
  49. package/src/implement/invocation.ts +8 -0
  50. package/src/implement/principal.ts +25 -2
  51. package/src/implement/terminals.ts +93 -0
  52. package/src/implement/workload.ts +24 -13
  53. package/src/jobs/report.ts +5 -1
  54. package/src/jobs/runner.ts +33 -17
  55. package/src/jobs/schedule.ts +7 -1
  56. package/src/jobs/scrub.ts +14 -1
  57. package/src/testing/endpoint-access.ts +180 -137
  58. package/src/testing/kit.ts +20 -2
  59. package/src/testing/proxy.ts +5 -5
  60. package/types/src/agent-skill.d.ts +29 -0
  61. package/types/src/build-recipe.d.ts +70 -0
  62. package/types/src/build.d.ts +29 -0
  63. package/types/src/bun.d.ts +66 -0
  64. package/types/src/client/caller.d.ts +19 -0
  65. package/types/src/client/contract.d.ts +131 -0
  66. package/types/src/client/error-reporting.d.ts +66 -0
  67. package/types/src/client/index.d.ts +4 -0
  68. package/types/src/client/rpc.d.ts +14 -0
  69. package/types/src/client/share.d.ts +2 -0
  70. package/types/src/client/transport.d.ts +42 -0
  71. package/types/src/cloudflare/application.d.ts +42 -0
  72. package/types/src/cloudflare/bindings.d.ts +27 -0
  73. package/types/src/cloudflare/names.d.ts +6 -0
  74. package/types/src/cloudflare/ops.d.ts +46 -0
  75. package/types/src/cloudflare/r2-files.d.ts +9 -0
  76. package/types/src/cloudflare.d.ts +42 -0
  77. package/types/src/contract/app-id.d.ts +2 -0
  78. package/types/src/contract/artifact-schema.d.ts +8 -0
  79. package/types/src/contract/attachment.d.ts +41 -0
  80. package/types/src/contract/build.d.ts +12 -0
  81. package/types/src/contract/compile.d.ts +11 -0
  82. package/types/src/contract/connect.d.ts +51 -0
  83. package/types/src/contract/context.d.ts +510 -0
  84. package/types/src/contract/declaration.d.ts +307 -0
  85. package/types/src/contract/defaults.d.ts +208 -0
  86. package/types/src/contract/define.d.ts +376 -0
  87. package/types/src/contract/file.d.ts +44 -0
  88. package/types/src/contract/group.d.ts +274 -0
  89. package/types/src/contract/handled.d.ts +18 -0
  90. package/types/src/contract/index.d.ts +19 -0
  91. package/types/src/contract/kit.d.ts +211 -0
  92. package/types/src/contract/platform.d.ts +9 -0
  93. package/types/src/contract/problems.d.ts +14 -0
  94. package/types/src/contract/redirect.d.ts +25 -0
  95. package/types/src/contract/schema-from-shape.d.ts +10 -0
  96. package/types/src/contract/schema-shape.d.ts +5 -0
  97. package/types/src/contract/snapshot-source.d.ts +15 -0
  98. package/types/src/contract/token.d.ts +58 -0
  99. package/types/src/contract/vocabulary.d.ts +32 -0
  100. package/types/src/contract/webhook-binding.d.ts +24 -0
  101. package/types/src/contract/webhook-scheme.d.ts +23 -0
  102. package/types/src/dev/changes.d.ts +7 -0
  103. package/types/src/dev/contract-digest.d.ts +1 -0
  104. package/types/src/dev/index.d.ts +2 -0
  105. package/types/src/dev/launcher.d.ts +32 -0
  106. package/types/src/dev/output.d.ts +28 -0
  107. package/types/src/dev/wire.d.ts +126 -0
  108. package/types/src/disk-assets.d.ts +3 -0
  109. package/types/src/errors.d.ts +46 -0
  110. package/types/src/fetch.d.ts +7 -0
  111. package/types/src/files/archive.d.ts +26 -0
  112. package/types/src/files/clamscan.d.ts +32 -0
  113. package/types/src/files/handle.d.ts +106 -0
  114. package/types/src/files/job-names.d.ts +9 -0
  115. package/types/src/files/kinds.d.ts +101 -0
  116. package/types/src/files/scan.d.ts +78 -0
  117. package/types/src/files/sniff.d.ts +7 -0
  118. package/types/src/files/streams.d.ts +17 -0
  119. package/types/src/files/treatment.d.ts +18 -0
  120. package/types/src/from-row.d.ts +20 -0
  121. package/types/src/honeypot.d.ts +2 -0
  122. package/types/src/implement/anonymous.d.ts +28 -0
  123. package/types/src/implement/attachment.d.ts +14 -0
  124. package/types/src/implement/boot.d.ts +114 -0
  125. package/types/src/implement/bounded.d.ts +32 -0
  126. package/types/src/implement/browser-config.d.ts +19 -0
  127. package/types/src/implement/capability.d.ts +142 -0
  128. package/types/src/implement/challenge.d.ts +75 -0
  129. package/types/src/implement/client-address.d.ts +11 -0
  130. package/types/src/implement/connect.d.ts +67 -0
  131. package/types/src/implement/crashes.d.ts +31 -0
  132. package/types/src/implement/dispatch.d.ts +220 -0
  133. package/types/src/implement/egress.d.ts +14 -0
  134. package/types/src/implement/environment.d.ts +14 -0
  135. package/types/src/implement/failures.d.ts +39 -0
  136. package/types/src/implement/faults.d.ts +45 -0
  137. package/types/src/implement/headers.d.ts +7 -0
  138. package/types/src/implement/implement.d.ts +58 -0
  139. package/types/src/implement/index.d.ts +25 -0
  140. package/types/src/implement/invocation.d.ts +125 -0
  141. package/types/src/implement/membership.d.ts +201 -0
  142. package/types/src/implement/multipart.d.ts +36 -0
  143. package/types/src/implement/params.d.ts +7 -0
  144. package/types/src/implement/principal.d.ts +57 -0
  145. package/types/src/implement/project.d.ts +11 -0
  146. package/types/src/implement/public.d.ts +61 -0
  147. package/types/src/implement/serve.d.ts +25 -0
  148. package/types/src/implement/terminals.d.ts +33 -0
  149. package/types/src/implement/tokens.d.ts +49 -0
  150. package/types/src/implement/types.d.ts +243 -0
  151. package/types/src/implement/urls.d.ts +15 -0
  152. package/types/src/implement/verifiers.d.ts +19 -0
  153. package/types/src/implement/webhook.d.ts +55 -0
  154. package/types/src/implement/workload.d.ts +56 -0
  155. package/types/src/implement/xlsx.d.ts +6 -0
  156. package/types/src/index.d.ts +12 -0
  157. package/types/src/jobs/exception.d.ts +40 -0
  158. package/types/src/jobs/mail.d.ts +67 -0
  159. package/types/src/jobs/report.d.ts +56 -0
  160. package/types/src/jobs/runner.d.ts +151 -0
  161. package/types/src/jobs/schedule.d.ts +17 -0
  162. package/types/src/jobs/scrub.d.ts +12 -0
  163. package/types/src/jobs/types.d.ts +98 -0
  164. package/types/src/react/caller.d.ts +30 -0
  165. package/types/src/react/errors.d.ts +40 -0
  166. package/types/src/react/hooks.d.ts +110 -0
  167. package/types/src/react/index.d.ts +5 -0
  168. package/types/src/react/provider.d.ts +58 -0
  169. package/types/src/react/store.d.ts +68 -0
  170. package/types/src/react/testing.d.ts +28 -0
  171. package/types/src/react/types.d.ts +33 -0
  172. package/types/src/rpc/builder.d.ts +52 -0
  173. package/types/src/rpc/contract.d.ts +23 -0
  174. package/types/src/rpc/dispatcher.d.ts +11 -0
  175. package/types/src/rpc/types.d.ts +62 -0
  176. package/types/src/service/define.d.ts +42 -0
  177. package/types/src/service/index.d.ts +3 -0
  178. package/types/src/service/serve.d.ts +28 -0
  179. package/types/src/standard-schema.d.ts +55 -0
  180. package/types/src/testing/caller.d.ts +95 -0
  181. package/types/src/testing/challenge.d.ts +11 -0
  182. package/types/src/testing/dev.d.ts +97 -0
  183. package/types/src/testing/endpoint-access.d.ts +107 -0
  184. package/types/src/testing/index.d.ts +12 -0
  185. package/types/src/testing/isolation.d.ts +3 -0
  186. package/types/src/testing/kit.d.ts +187 -0
  187. package/types/src/testing/membership.d.ts +132 -0
  188. package/types/src/testing/policy-isolation.d.ts +113 -0
  189. package/types/src/testing/preload.d.ts +1 -0
  190. package/types/src/testing/proxy.d.ts +40 -0
  191. package/types/src/testing/runner.d.ts +8 -0
  192. package/types/src/testing/sample-file.d.ts +5 -0
  193. package/types/src/testing/status.d.ts +4 -0
  194. package/types/src/undumpable.d.ts +5 -0
@@ -0,0 +1,198 @@
1
+ // The build, init and start of a contract application in a Bun container, as data (ADR-0043): the steps and their
2
+ // order are the platform's, never the repository's. A provider renders the recipe into its own build form, with its own
3
+ // paths, file writes and scheduler. PURE — no network, no filesystem.
4
+ //
5
+ // PLATFORM PROTOCOL, not framework API: the platform renders this with ITS copy for applications of older releases
6
+ // (ADR-0062), so the commands, script names and job names an application must accept change only additively. Never
7
+ // render the application's own copy.
8
+
9
+ import { APPLICATION_SERVER_MAX_HEADER_BYTES, FRAMEWORK_RELEASE_PATTERN } from '@fabrika/auth-core'
10
+ import { CONTRACT_APP_ID_PATTERN } from './contract/app-id.js'
11
+ import { FILE_SCAN_JOB, FILE_SCAN_SCHEDULE, FILE_SWEEP_JOB, FILE_SWEEP_SCHEDULE } from './files/job-names.js'
12
+
13
+ export { CONTRACT_APP_ID_PATTERN } from './contract/app-id.js'
14
+
15
+ export { FILE_SCAN_JOB, FILE_SWEEP_JOB } from './files/job-names.js'
16
+
17
+ /** The TypeScript release of the platform's type check; the application's own `typescript` is not run. */
18
+ export const PLATFORM_TYPESCRIPT_VERSION = '5.9.3'
19
+
20
+ /** The CLI a generated script runs, relative to the application root. */
21
+ const FABRIKA_BIN = './node_modules/.bin/fabrika'
22
+ const MIGRATE_SCRIPT = 'fabrika/migrate.generated.sh'
23
+ const VERIFY_SCRIPT = 'fabrika/verify.generated.sh'
24
+ const START_SCRIPT = 'fabrika/start.generated.sh'
25
+ const SERVE: readonly string[] = ['bun', `--max-http-header-size=${APPLICATION_SERVER_MAX_HEADER_BYTES}`, 'fabrika/entry.generated.ts']
26
+ const MIGRATE: readonly string[] = [FABRIKA_BIN, 'db', 'migrate']
27
+ const PLATFORM_TSCONFIG = 'fabrika/tsconfig.platform.json'
28
+
29
+ /**
30
+ * The compiler options of the platform's type check; the application's own `tsconfig.json` and its `paths` are not read.
31
+ * It covers the SPA under `web/` too: Bun bundles the SPA without checking its types.
32
+ */
33
+ const PLATFORM_TSCONFIG_CONTENTS = JSON.stringify({
34
+ compilerOptions: {
35
+ target: 'ES2022',
36
+ module: 'ESNext',
37
+ moduleResolution: 'bundler',
38
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
39
+ jsx: 'react-jsx',
40
+ types: ['bun'],
41
+ strict: true,
42
+ noUncheckedIndexedAccess: true,
43
+ noImplicitOverride: true,
44
+ forceConsistentCasingInFileNames: true,
45
+ skipLibCheck: true,
46
+ resolveJsonModule: true,
47
+ isolatedModules: true,
48
+ noEmit: true,
49
+ },
50
+ include: ['../fabrika.contract.ts', '../src', '../web', 'entry.generated.ts'],
51
+ })
52
+
53
+ const INSTALL = 'bun install --frozen-lockfile --ignore-scripts'
54
+
55
+ /**
56
+ * What `@fabrika/db/testing` prints after the label of a test file it skips without `FABRIKA_TEST_POSTGRES_URL`. Every
57
+ * framework release prints it, so the platform counts it for any application's release.
58
+ */
59
+ const DATABASE_TEST_SKIP_MARKER = ' skipped: set FABRIKA_TEST_POSTGRES_URL to run the Postgres-backed tests'
60
+
61
+ /** The prefix of the line the tests command prints after the test run. */
62
+ const TESTS_REPORT_PREFIX = 'fabrika-tests:'
63
+
64
+ /** One `bun -e "…"` running `script`, which may hold none of the characters a double-quoted shell word expands. */
65
+ const bunEvalCommand = (script: readonly string[]): string => {
66
+ const source = script.join(' ')
67
+ if (/["$`\\!]/.test(source)) throw new Error('a generated bun -e script must not contain a quote, an expansion or a backslash')
68
+ return `bun -e "${source}"`
69
+ }
70
+
71
+ // The build has no database, so every Postgres-backed test file skips and the run still passes. This command runs the
72
+ // tests as before and then says how many files did not run, so a green test step does not read as a tested database.
73
+ const TESTS_COMMAND = bunEvalCommand([
74
+ "const child = Bun.spawn([process.execPath, 'test', '--isolate', '--pass-with-no-tests'], { stdout: 'inherit', stderr: 'pipe' });",
75
+ 'const decoder = new TextDecoder();',
76
+ "let output = '';",
77
+ 'for await (const chunk of child.stderr) { process.stderr.write(chunk); output += decoder.decode(chunk, { stream: true }) }',
78
+ 'const code = await child.exited;',
79
+ `const marker = '${DATABASE_TEST_SKIP_MARKER}';`,
80
+ 'const skipped = output.split(String.fromCharCode(10)).filter((line) => line.includes(marker)).map((line) => line.slice(0, line.indexOf(marker)).trim());',
81
+ `const prefix = '${TESTS_REPORT_PREFIX}';`,
82
+ "console.warn(skipped.length === 0 ? prefix + ' no Postgres-backed test file skipped' : prefix + ' ' + skipped.length + ' Postgres-backed test files did not run, because the build has no database: ' + skipped.join(', '));",
83
+ 'process.exit(code);',
84
+ ])
85
+
86
+ /** A `c.cron` job name, which a crontab line carries unquoted. */
87
+ export const CONTRACT_JOB_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/
88
+ /** Five cron fields of digits and `*`, `/`, `,`, `-` only. */
89
+ export const CRON_SCHEDULE_PATTERN = /^[0-9*/,-]+(?: [0-9*/,-]+){4}$/
90
+
91
+ /** What one contract application's recipe depends on. `contractRecipe` refuses a value that a shell would read as more. */
92
+ export interface ContractRecipeInput {
93
+ readonly appId: string
94
+ /** The `@fabrika/check` release whose reader read the application (ADR-0062). */
95
+ readonly checkRelease: string
96
+ /** The absolute directory the build runs in: the checker reads the source there. */
97
+ readonly sourceRoot: string
98
+ /** The absolute directory the built application runs from: each generated script starts there. */
99
+ readonly runtimeRoot: string
100
+ /** With a database, every container migrates and checks its schema before the server starts. */
101
+ readonly database: boolean
102
+ /** Whether the build uploads its source maps, last and never failing the build. */
103
+ readonly sourceMaps: boolean
104
+ readonly jobs: readonly { readonly name: string; readonly schedule: string }[]
105
+ /** The contract declares a file collection: the sweep of removed files is scheduled. */
106
+ readonly files: boolean
107
+ /** A file collection scans its uploads: the scan pass is scheduled. */
108
+ readonly scan: boolean
109
+ }
110
+
111
+ /** One build step: a command run in the source root, or a generated file written there, line by line. */
112
+ export type ContractBuildStep = { readonly run: string } | { readonly write: string; readonly lines: readonly string[] }
113
+
114
+ /** One check of the build: a command run in the source root that fails the build. */
115
+ export interface ContractCheck {
116
+ readonly name: 'tests' | 'tsc' | 'fabrika-check'
117
+ readonly command: string
118
+ }
119
+
120
+ export interface ContractRecipe {
121
+ /** The steps after the authored SPA commands, in order. */
122
+ readonly build: readonly ContractBuildStep[]
123
+ /** The checks among `build`, in its order. */
124
+ readonly checks: readonly ContractCheck[]
125
+ /** The generated scripts every container runs before `start`, in order; any failure stops the activation. */
126
+ readonly init: readonly string[]
127
+ /** The generated script that starts the server, which only `exec`s it. */
128
+ readonly start: string
129
+ /** The command the start script `exec`s, run from the runtime root. */
130
+ readonly serve: readonly string[]
131
+ /** The command that migrates the application's database, run from the runtime root. */
132
+ readonly migrate: readonly string[]
133
+ /** The jobs to schedule, each on one container; a command runs from the runtime root. */
134
+ readonly jobs: readonly { readonly schedule: string; readonly command: string }[]
135
+ }
136
+
137
+ /**
138
+ * The recipe of one contract application. The application's tests run first: they are authored code, so everything
139
+ * after them is reinstalled, regenerated or checked again. They run `--isolate`, as the application skill runs them: a
140
+ * test file's DOM globals otherwise leak into the next file. `tsc` runs before the checker, as in the skill's loop: the
141
+ * usual quick fix of a type error is a cast or an `any`, which only the checker refuses, so the checker is the last word
142
+ * on the code. The build has no database, so the schema checks run as init scripts, after the migrations and before the
143
+ * readiness window opens.
144
+ */
145
+ export function contractRecipe(input: ContractRecipeInput): ContractRecipe {
146
+ const { appId, checkRelease, sourceRoot, runtimeRoot } = input
147
+ if (!CONTRACT_APP_ID_PATTERN.test(appId)) throw new Error('a contract application id must match [a-z0-9][a-z0-9-]*')
148
+ if (!FRAMEWORK_RELEASE_PATTERN.test(checkRelease)) throw new Error('the checker release must be a release version')
149
+ if (input.jobs.some((job) => !CONTRACT_JOB_NAME_PATTERN.test(job.name) || !CRON_SCHEDULE_PATTERN.test(job.schedule))) {
150
+ throw new Error('a job name or schedule is not one a generated crontab line may carry')
151
+ }
152
+ const script = (lines: readonly string[]): readonly string[] => ['#!/bin/sh', 'set -eu', `cd ${runtimeRoot}`, ...lines]
153
+ const init = input.database
154
+ ? [
155
+ { path: MIGRATE_SCRIPT, lines: script([`FABRIKA_APP_ID=${appId} exec ${MIGRATE.join(' ')}`]) },
156
+ // The committed schema types must match the migrated schema, and every generated isolation test must pass on a
157
+ // throwaway schema.
158
+ {
159
+ path: VERIFY_SCRIPT,
160
+ lines: script([`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`, `FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db test-isolation`]),
161
+ },
162
+ ]
163
+ : []
164
+ const tests: ContractCheck = { name: 'tests', command: TESTS_COMMAND }
165
+ const tsc: ContractCheck = {
166
+ name: 'tsc',
167
+ command: `cd /tmp && bun x --bun -p typescript@${PLATFORM_TYPESCRIPT_VERSION} tsc -p ${sourceRoot}/${PLATFORM_TSCONFIG}`,
168
+ }
169
+ const check: ContractCheck = {
170
+ name: 'fabrika-check',
171
+ command: `cd /tmp && bun x --bun -p @fabrika/check@${checkRelease} fabrika-check ${sourceRoot}`,
172
+ }
173
+ return {
174
+ build: [
175
+ { run: INSTALL },
176
+ { run: tests.command },
177
+ { run: `rm -rf node_modules && ${INSTALL}` },
178
+ { run: `bun -e "await import('@fabrika/app/build').then((build) => build.writeGeneratedEntry('.', 'bun'))"` },
179
+ { write: PLATFORM_TSCONFIG, lines: [PLATFORM_TSCONFIG_CONTENTS] },
180
+ { run: tsc.command },
181
+ { run: check.command },
182
+ ...init.map((generated) => ({ write: generated.path, lines: generated.lines })),
183
+ { write: START_SCRIPT, lines: script([`exec ${SERVE.join(' ')}`]) },
184
+ // After every check, so a map is uploaded only for a build that can deploy; the report line says how it went.
185
+ ...(input.sourceMaps ? [{ run: `cd /tmp && bun x --bun -p @fabrika/check@${checkRelease} fabrika-source-maps ${sourceRoot} || true` }] : []),
186
+ ],
187
+ checks: [tests, tsc, check],
188
+ init: init.map((generated) => generated.path),
189
+ start: START_SCRIPT,
190
+ serve: SERVE,
191
+ migrate: MIGRATE,
192
+ jobs: [
193
+ ...input.jobs.map((job) => ({ schedule: job.schedule, command: `${FABRIKA_BIN} job run ${job.name}` })),
194
+ ...(input.files ? [{ schedule: FILE_SWEEP_SCHEDULE, command: `${FABRIKA_BIN} job run ${FILE_SWEEP_JOB}` }] : []),
195
+ ...(input.scan ? [{ schedule: FILE_SCAN_SCHEDULE, command: `${FABRIKA_BIN} job run ${FILE_SCAN_JOB}` }] : []),
196
+ ],
197
+ }
198
+ }
package/src/build.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // the only module that reads `process.env` and the only caller of `boot`, through the runtime adapter, and it imports
3
3
  // the application only after the adapter took the framework's variables out of the process.
4
4
 
5
- import { mkdir, writeFile } from 'node:fs/promises'
5
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
6
6
  import { dirname, join } from 'node:path'
7
7
 
8
8
  /** An application root with this file is a contract application. The paths equal `@fabrika/check`'s layout. */
@@ -14,8 +14,19 @@ export const GENERATED_ENTRY_FILE = 'fabrika/entry.generated.ts'
14
14
  /** The module whose default export is `serve(contract, …)`, relative to the application root. */
15
15
  export const IMPLEMENTATION_FILE = 'src/app.ts'
16
16
 
17
+ /** The application's migrations as a module, relative to the application root: a Worker reads no file system. */
18
+ export const GENERATED_MIGRATIONS_FILE = 'fabrika/migrations.generated.ts'
19
+
20
+ /** Where an application keeps its migrations, relative to its root; `fabrika db new` writes there. */
21
+ export const MIGRATIONS_DIRECTORY = 'db/migrations'
22
+
17
23
  export type EntryRuntime = 'bun' | 'cloudflare'
18
24
 
25
+ export * from './build-recipe.js'
26
+
27
+ // In a module of their own, which the Worker imports without this one's file system.
28
+ export { CLOUDFLARE_APPLICATION_BINDING, CLOUDFLARE_APPLICATION_CLASS, CLOUDFLARE_FILES_BINDING } from './cloudflare/names.js'
29
+
19
30
  const HEADER = '// @generated by `fabrika app build` from src/app.ts. Do not edit: every build rewrites it.'
20
31
 
21
32
  /** The entrypoint source for one runtime. */
@@ -35,19 +46,59 @@ export function generatedEntry(runtime: EntryRuntime): string {
35
46
  return [
36
47
  HEADER,
37
48
  "import { createContractWorker, markProductionProcess } from '@fabrika/app/cloudflare'",
49
+ "import { migrations } from './migrations.generated'",
38
50
  '',
39
- '// The test kits refuse to load from here on; the application is imported only after the framework took its variables.',
51
+ "export { FabrikaApplication } from '@fabrika/app/cloudflare'",
52
+ '',
53
+ '// The test kits refuse to load from here on; the object imports the application only after the framework took its variables.',
40
54
  'markProductionProcess()',
41
- "export default createContractWorker(() => import('../src/app'))",
55
+ "export default createContractWorker(() => import('../src/app'), { migrations })",
42
56
  '',
43
57
  ].join('\n')
44
58
  }
45
59
  }
46
60
 
47
- /** Writes the entrypoint under `root` and returns its path. */
61
+ /** Writes the entrypoint under `root` and returns its path; the Cloudflare one also writes the migrations module it imports. */
48
62
  export async function writeGeneratedEntry(root: string, runtime: EntryRuntime): Promise<string> {
49
63
  const path = join(root, GENERATED_ENTRY_FILE)
50
64
  await mkdir(dirname(path), { recursive: true })
51
65
  await writeFile(path, generatedEntry(runtime))
66
+ if (runtime === 'cloudflare') {
67
+ await writeGeneratedMigrations(root)
68
+ }
69
+ return path
70
+ }
71
+
72
+ /** The application's migrations under `root`, every `.sql` file of `db/migrations` by name; none without the directory. */
73
+ export async function applicationMigrations(root: string): Promise<readonly { readonly name: string; readonly sql: string }[]> {
74
+ const directory = join(root, MIGRATIONS_DIRECTORY)
75
+ let names: string[]
76
+ try {
77
+ names = await readdir(directory)
78
+ } catch (error) {
79
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
80
+ return []
81
+ }
82
+ throw error
83
+ }
84
+ const files = names.filter((name) => name.endsWith('.sql')).sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
85
+ return await Promise.all(files.map(async (name) => ({ name, sql: await readFile(join(directory, name), 'utf8') })))
86
+ }
87
+
88
+ /** The migrations module source: the files' names and texts, verbatim, so their checksums stay the files'. */
89
+ export function generatedMigrations(migrations: readonly { readonly name: string; readonly sql: string }[]): string {
90
+ return [
91
+ `// @generated by \`fabrika app build\` from ${MIGRATIONS_DIRECTORY}. Do not edit: every build rewrites it.`,
92
+ '',
93
+ `export const migrations: readonly { readonly name: string; readonly sql: string }[] = ${JSON.stringify(migrations, null, '\t')}`,
94
+ '',
95
+ ].join('\n')
96
+ }
97
+
98
+ /** Writes the migrations module under `root` and returns its path. */
99
+ export async function writeGeneratedMigrations(root: string): Promise<string> {
100
+ const path = join(root, GENERATED_MIGRATIONS_FILE)
101
+ await mkdir(dirname(path), { recursive: true })
102
+ await writeFile(path, generatedMigrations(await applicationMigrations(root)))
52
103
  return path
53
104
  }
package/src/bun.ts CHANGED
@@ -5,9 +5,10 @@
5
5
  // contract application is served through `serveContract`, from the generated
6
6
  // entrypoint.
7
7
 
8
- import { HttpIamMembership } from '@fabrika/auth'
9
- import type { Database, PostgresOptions } from '@fabrika/db'
8
+ import { HttpIamMembership, HttpIamTerminals } from '@fabrika/auth'
9
+ import type { Database } from '@fabrika/db'
10
10
  import { isScopedDatabase } from '@fabrika/db/framework'
11
+ import type { PostgresOptions } from '@fabrika/db/postgres'
11
12
  import type { EmailProviderRegistry } from '@fabrika/email'
12
13
  import { directoryEmailProvider } from '@fabrika/email/directory'
13
14
  import { parseJwks } from '@fabrika/jwks'
@@ -117,7 +118,7 @@ export interface ContractServer {
117
118
 
118
119
  /**
119
120
  * What the adapter takes from `@fabrika/db` before the application loads, so no application module can stand in for
120
- * it: a replaced `Database.postgres` would receive the database URL.
121
+ * it: a replaced `openPostgres` would receive the database URL.
121
122
  */
122
123
  interface DataLayerOpeners {
123
124
  readonly postgres: (options: PostgresOptions) => Database
@@ -126,8 +127,8 @@ interface DataLayerOpeners {
126
127
 
127
128
  /** Loads `@fabrika/db` before the application: it captures its built-ins and freezes the Kysely compiler at load. */
128
129
  async function loadDataLayer(): Promise<DataLayerOpeners> {
129
- const { Database, applicationSchema } = await import('@fabrika/db')
130
- return { postgres: Database.postgres, applicationSchema }
130
+ const [{ applicationSchema }, { openPostgres }] = await Promise.all([import('@fabrika/db'), import('@fabrika/db/postgres')])
131
+ return { postgres: openPostgres, applicationSchema }
131
132
  }
132
133
 
133
134
  /** How long a stopping process waits for the work its requests left running; a kicked run aborts at once. */
@@ -277,12 +278,22 @@ export function openFileStorage(framework: Readonly<Record<string, string>>): Fi
277
278
  /**
278
279
  * IAM's membership transport, when the platform gives the process `FABRIKA_IAM_RPC_URL` (IAM's public origin) and
279
280
  * `FABRIKA_IAM_RPC_KEY` (this environment's membership transport key); without both, there is none, and `boot` refuses
280
- * a contract that needs one.
281
+ * a contract that needs one. The identifications at a terminal travel on the same transport.
281
282
  */
282
- function httpMembership(framework: Readonly<Record<string, string>>): HttpIamMembership | undefined {
283
+ function httpIamTransport(framework: Readonly<Record<string, string>>): { readonly origin: string; readonly key: string } | undefined {
283
284
  const origin = framework['FABRIKA_IAM_RPC_URL']?.trim() ?? ''
284
285
  const key = framework['FABRIKA_IAM_RPC_KEY']?.trim() ?? ''
285
- return origin === '' || key === '' ? undefined : new HttpIamMembership({ origin, key })
286
+ return origin === '' || key === '' ? undefined : { origin, key }
287
+ }
288
+
289
+ function httpMembership(framework: Readonly<Record<string, string>>): HttpIamMembership | undefined {
290
+ const transport = httpIamTransport(framework)
291
+ return transport === undefined ? undefined : new HttpIamMembership(transport)
292
+ }
293
+
294
+ function httpTerminals(framework: Readonly<Record<string, string>>): HttpIamTerminals | undefined {
295
+ const transport = httpIamTransport(framework)
296
+ return transport === undefined ? undefined : new HttpIamTerminals(transport)
286
297
  }
287
298
 
288
299
  /** IAM's public token exchange, at the issuer every deploy names; `boot` reports a missing issuer itself. */
@@ -381,6 +392,7 @@ async function startContractServer(
381
392
  const iam = issuerJwks(environment.framework['FABRIKA_IAM_ISSUER'])
382
393
  const database = isScopedDatabase(app.database) ? openApplicationDatabase(data, environment.framework) : undefined
383
394
  const membership = httpMembership(environment.framework)
395
+ const terminals = httpTerminals(environment.framework)
384
396
  // Zerops is where a Bun application runs, and a namespace there is one private network (ADR-0022 rule 5).
385
397
  const files = openFileStorage(environment.framework)
386
398
  const workloadTokens = workloadTokensOf(environment.framework)
@@ -391,6 +403,7 @@ async function startContractServer(
391
403
  database,
392
404
  files,
393
405
  membership,
406
+ terminals,
394
407
  workloadTokens,
395
408
  requireProxySecret: true,
396
409
  localEmailProviders: LOCAL_EMAIL_PROVIDERS,
@@ -454,6 +467,7 @@ export async function runContractJob(
454
467
  try {
455
468
  const workloadTokens = workloadTokensOf(environment.framework)
456
469
  const membership = httpMembership(environment.framework)
470
+ const terminals = httpTerminals(environment.framework)
457
471
  const files = openFileStorage(environment.framework)
458
472
  return await boot(app, {
459
473
  environment,
@@ -462,6 +476,7 @@ export async function runContractJob(
462
476
  database,
463
477
  files,
464
478
  membership,
479
+ terminals,
465
480
  workloadTokens,
466
481
  requireProxySecret: true,
467
482
  localEmailProviders: LOCAL_EMAIL_PROVIDERS,