@forumone/create-throughline 0.3.1 → 0.4.0
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/CHANGELOG.md +40 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +4 -0
- package/dist/generator.js.map +1 -1
- package/dist/post-install.d.ts.map +1 -1
- package/dist/post-install.js +10 -3
- package/dist/post-install.js.map +1 -1
- package/dist/templates/base/.env.example +9 -13
- package/dist/templates/base/README.md +55 -2
- package/dist/templates/base/apps/web/package.json.template +11 -2
- package/dist/templates/base/apps/web/scripts/check-migrations.ts +147 -0
- package/dist/templates/base/apps/web/scripts/lib/target.ts +95 -0
- package/dist/templates/base/apps/web/scripts/write-migration-snapshot.ts +119 -0
- package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +8 -1
- package/dist/templates/base/apps/web/src/lib/database.ts +158 -0
- package/dist/templates/base/apps/web/src/payload.config.ts.template +73 -16
- package/dist/templates/base/github/scripts/check-generated.sh +44 -0
- package/dist/templates/base/github/workflows/ci.yml +131 -0
- package/dist/templates/base/package.json.template +6 -1
- package/dist/templates/base/scripts/check-audit.ts +117 -0
- package/dist/templates/base/scripts/check-single-instance.ts +113 -0
- package/dist/templates/base/scripts/check-tested-packages.ts +165 -0
- package/dist/templates/base/turbo.json +16 -1
- package/dist/templates/with-reference-ds/design-system/scripts/build-manifest.ts +59 -29
- package/dist/templates/with-reference-ds/design-system/scripts/validate.ts +26 -1
- package/dist/versions.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @forumone/create-throughline
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8b7d698: A scaffolded project ships a two-tier CI workflow:
|
|
8
|
+
|
|
9
|
+
- `fast` runs on every pull request with no database. It installs from the lockfile, then runs typecheck, lint, the workspace and migration gates, manifest validation, a generated-files check and tests, with a non-blocking audit.
|
|
10
|
+
- `verify` runs on pushes to main. It migrates a fresh Postgres and runs a production build.
|
|
11
|
+
|
|
12
|
+
`check-generated.sh` regenerates `payload-types.ts` and the admin import map and fails on any difference, including an uncommitted file. `apps/web` gains `generate:types` and `generate:importmap`.
|
|
13
|
+
|
|
14
|
+
- 8b7d698: A scaffolded project manages its schema through migrations from the first table on:
|
|
15
|
+
|
|
16
|
+
- **Config.** `payload.config.ts` sets `push: false` and an explicit `migrationDir`. It connects through a resolver that knows the Vercel–Neon integration's variables, keeps a migration off Neon's pooled endpoint, and accepts an optional migration-only credential.
|
|
17
|
+
- **Scripts.** `apps/web` gains `migrate`, `migrate:create`, `migrate:status`, `migrate:snapshot` and `check:migrations`. The check asserts that every migration has its schema snapshot, that no snapshot is orphaned, and that no drop follows a `CASCADE` that has already carried it out.
|
|
18
|
+
- **Write guard.** `scripts/lib/target.ts` gives scripts that write to a database a `requireTarget` guard.
|
|
19
|
+
|
|
20
|
+
- 8b7d698: A scaffolded project ships three workspace gates as root scripts:
|
|
21
|
+
|
|
22
|
+
- `check:instances`: `payload`, `@payloadcms/ui`, `react` and `react-dom` each resolve to exactly one copy
|
|
23
|
+
- `check:tested`: every package that ships code has at least one test
|
|
24
|
+
- `check:audit`: production advisories are checked against an allowlist that carries a reason for each entry and fails when an entry goes stale
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- e9077b3: The reference design system's manifest build and validation, which every scaffolded project vendors, now:
|
|
29
|
+
|
|
30
|
+
- refuse a component directory with no contract, instead of silently leaving it out of the manifest
|
|
31
|
+
- read the directories that hold components from a `CONTRACT_LAYERS` list
|
|
32
|
+
- name the manifest from the project's own `package.json`, instead of claiming to be `@forumone/throughline-reference-ds`
|
|
33
|
+
- treat `antiExamples.empty` and `intent.brevity` as errors
|
|
34
|
+
|
|
35
|
+
- 6800052: The scaffold compiles against the current Throughline releases, and serves its tools over MCP:
|
|
36
|
+
|
|
37
|
+
- `payload.config.ts` no longer imports `createApiKeysCollection`, which core removed.
|
|
38
|
+
- One `createMcpToolCollector()` is passed to every tool-bearing plugin, and `@payloadcms/plugin-mcp` is registered after them, with its key collection narrowed to admins.
|
|
39
|
+
- The Inngest route types the integration registry, so `serve()` can type its function list.
|
|
40
|
+
- `@payloadcms/plugin-form-builder` is a caret range like the rest of `@payloadcms/*`, so it no longer installs a second `@payloadcms/ui`.
|
|
41
|
+
- `.env.example` and the printed next steps drop the per-server MCP keys that no longer exist.
|
|
42
|
+
|
|
3
43
|
## 0.3.1
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/dist/generator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAgC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAa,MAAM,kBAAkB,CAAA;AAK5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAK3C,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC7F;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,iBAAe,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmClF;AAiBD,iBAAS,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3C;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAgC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAa,MAAM,kBAAkB,CAAA;AAK5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAK3C,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC7F;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED;;;;;;;;;GASG;AACH,iBAAe,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmClF;AAiBD,iBAAS,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3C;AAcD,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AA6BD,eAAO,MAAM,SAAS;;;;;;;CAAsE,CAAA"}
|
package/dist/generator.js
CHANGED
|
@@ -117,9 +117,13 @@ function isBinary(filename) {
|
|
|
117
117
|
/**
|
|
118
118
|
* npm strips files literally named `.gitignore` from a published tarball, so
|
|
119
119
|
* we author them as `gitignore` in templates and restore the dot on output.
|
|
120
|
+
* `.github` goes the same way: a dot-directory in a tarball is at the mercy of
|
|
121
|
+
* whichever ignore rules the publishing npm applies, and the scaffold's CI is
|
|
122
|
+
* not something to lose silently.
|
|
120
123
|
*/
|
|
121
124
|
const DOTFILE_RENAMES = {
|
|
122
125
|
gitignore: '.gitignore',
|
|
126
|
+
github: '.github',
|
|
123
127
|
};
|
|
124
128
|
function applyDotfileRename(name) {
|
|
125
129
|
return DOTFILE_RENAMES[name] ?? name;
|
package/dist/generator.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-install.d.ts","sourceRoot":"","sources":["../src/post-install.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"post-install.d.ts","sourceRoot":"","sources":["../src/post-install.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwFrD"}
|
package/dist/post-install.js
CHANGED
|
@@ -28,11 +28,18 @@ export function printNextSteps(answers) {
|
|
|
28
28
|
lines.push(` ${pc.dim('openssl rand -base64 48 # PAYLOAD_SECRET')}`);
|
|
29
29
|
lines.push(` ${pc.dim('openssl rand -base64 48 # APPROVAL_TOKEN_SECRET')}`);
|
|
30
30
|
lines.push(` ${pc.dim('openssl rand -base64 48 # FORMS_IP_HASH_SECRET')}`);
|
|
31
|
+
// `push: false`: the schema exists only once a migration has created it.
|
|
32
|
+
lines.push(formatStep(step++, 'Create and apply the first migration:'));
|
|
33
|
+
lines.push(` ${pc.dim('pnpm --dir apps/web migrate:create initial')}`);
|
|
34
|
+
lines.push(` ${pc.dim('pnpm --dir apps/web migrate')}`);
|
|
35
|
+
// Committed and checked by CI, so the first push is green only with them.
|
|
36
|
+
lines.push(formatStep(step++, 'Generate the Payload types and import map, and commit them:'));
|
|
37
|
+
lines.push(` ${pc.dim('pnpm --dir apps/web generate:types')}`);
|
|
38
|
+
lines.push(` ${pc.dim('pnpm --dir apps/web generate:importmap')}`);
|
|
31
39
|
lines.push(formatStep(step++, `${pc.bold('pnpm dev')} to run locally`));
|
|
32
40
|
lines.push(formatStep(step++, 'Open http://localhost:3000/admin and create your first admin user'));
|
|
33
|
-
lines.push(formatStep(step++, 'In the admin,
|
|
34
|
-
lines.push(formatStep(step
|
|
35
|
-
lines.push(formatStep(step, 'Wire Claude (or your MCP client) to your MCP endpoints — see the generated README.md'));
|
|
41
|
+
lines.push(formatStep(step++, 'In the admin, create one key under MCP → Payload MCP API Keys'));
|
|
42
|
+
lines.push(formatStep(step, 'Give that key to Claude (or your MCP client) for /api/mcp — see the generated README.md'));
|
|
36
43
|
// eslint-disable-next-line no-console
|
|
37
44
|
console.log();
|
|
38
45
|
// eslint-disable-next-line no-console
|
package/dist/post-install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-install.js","sourceRoot":"","sources":["../src/post-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,MAAM,YAAY,CAAA;AAG3B;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,IAAI,GAAG,CAAC,CAAA;IAEZ,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC3C,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAA;IAE7D,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,IAAI,CACR,UAAU,CACR,IAAI,EAAE,EACN,QAAQ,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,qBAAqB,CACjF,CACF,CAAA;IAED,IAAI,OAAO,CAAC,gBAAgB,KAAK,MAAM,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,qEAAqE,CAAC,EAAE,CAAC,CAAA;IACvG,CAAC;SAAM,IAAI,OAAO,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;QACnD,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,gFAAgF,CAAC,EAAE,CAAC,CAAA;IAClH,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,+EAA+E,CAAC,EAAE,CAAC,CAAA;IACjH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAA;IAC5D,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,2CAA2C,CAAC,EAAE,CAAC,CAAA;IAC3E,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,kDAAkD,CAAC,EAAE,CAAC,CAAA;IAClF,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,iDAAiD,CAAC,EAAE,CAAC,CAAA;IAEjF,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"post-install.js","sourceRoot":"","sources":["../src/post-install.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# --- Required ---
|
|
2
2
|
DATABASE_URI=postgres://user:pass@host/dbname
|
|
3
|
+
# Optional: a schema-owner credential used only by `pnpm migrate`, so the app
|
|
4
|
+
# itself can run as a role that reads and writes rows and no more. See
|
|
5
|
+
# apps/web/src/lib/database.ts.
|
|
6
|
+
# MIGRATION_DATABASE_URL=
|
|
3
7
|
PAYLOAD_SECRET=generate-with-openssl-rand-base64-48
|
|
4
8
|
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
|
5
9
|
|
|
@@ -23,19 +27,11 @@ APPROVAL_TOKEN_SECRET=
|
|
|
23
27
|
# Generate: openssl rand -base64 48
|
|
24
28
|
FORMS_IP_HASH_SECRET=
|
|
25
29
|
|
|
26
|
-
# --- MCP
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
APPROVALS_SERVER_API_KEY=
|
|
32
|
-
AUDIT_SERVER_API_KEY=
|
|
33
|
-
FORMS_SERVER_API_KEY=
|
|
34
|
-
INTEGRATIONS_SERVER_API_KEY=
|
|
35
|
-
|
|
36
|
-
# System API key used by scheduled workflows that talk back to the
|
|
37
|
-
# Publishing MCP server (executes scheduled publishes).
|
|
38
|
-
PUBLISHING_SYSTEM_API_KEY=
|
|
30
|
+
# --- MCP ---
|
|
31
|
+
# No keys here. Every Throughline tool is served on one /api/mcp by
|
|
32
|
+
# @payloadcms/plugin-mcp; create a key in the admin under MCP -> Payload MCP
|
|
33
|
+
# API Keys and give it to your MCP client, not to this file. Scheduled
|
|
34
|
+
# publishes run in process and need no key either.
|
|
39
35
|
|
|
40
36
|
# --- Optional: storage ---
|
|
41
37
|
# Vercel Blob token if deploying to Vercel; remove this and the storage
|
|
@@ -32,10 +32,20 @@ openssl rand -base64 48
|
|
|
32
32
|
# - RESEND_API_KEY — from Resend dashboard
|
|
33
33
|
# - INNGEST_EVENT_KEY/SIGNING — from Inngest (optional in local dev)
|
|
34
34
|
|
|
35
|
-
# 5.
|
|
35
|
+
# 5. Create and apply the first migration. The config sets `push: false`, so
|
|
36
|
+
# migrations are the only way the schema changes, from the first table on.
|
|
37
|
+
pnpm --dir apps/web migrate:create initial
|
|
38
|
+
pnpm --dir apps/web migrate
|
|
39
|
+
|
|
40
|
+
# 6. Generate the Payload types and admin import map, and commit both along
|
|
41
|
+
# with the migration. CI fails until they match the config.
|
|
42
|
+
pnpm --dir apps/web generate:types
|
|
43
|
+
pnpm --dir apps/web generate:importmap
|
|
44
|
+
|
|
45
|
+
# 7. Start the dev server
|
|
36
46
|
pnpm dev
|
|
37
47
|
|
|
38
|
-
#
|
|
48
|
+
# 8. Visit http://localhost:3000/admin and create your first user.
|
|
39
49
|
```
|
|
40
50
|
|
|
41
51
|
## After admin signup
|
|
@@ -66,6 +76,49 @@ pnpm dev
|
|
|
66
76
|
- `pnpm lint` — ESLint
|
|
67
77
|
- `pnpm test` — run all package tests
|
|
68
78
|
|
|
79
|
+
## Migrations
|
|
80
|
+
|
|
81
|
+
The schema changes only through `apps/web/src/migrations` — `push` is off, in
|
|
82
|
+
development too. From `apps/web`:
|
|
83
|
+
|
|
84
|
+
- `pnpm migrate:create <name>` — generate a migration from the config. It diffs against the `.json` snapshot beside the newest migration, not against the database, and needs no database.
|
|
85
|
+
- `pnpm migrate` — apply pending migrations. Connects through `MIGRATION_DATABASE_URL` when it is set.
|
|
86
|
+
- `pnpm migrate:status` — what has been applied.
|
|
87
|
+
- `pnpm migrate:snapshot` — after **hand-writing** a migration, write its snapshot, or the generator falls a step behind and re-proposes changes that already shipped.
|
|
88
|
+
- `pnpm check:migrations` — every migration has a snapshot, no snapshot is orphaned, and no drop follows a `CASCADE` that already carried it out. Also runs from the root.
|
|
89
|
+
|
|
90
|
+
Scripts that write to a database can call `requireTarget` from `apps/web/scripts/lib/target.ts`, which prints the database, host and variable it is about to write through, and demands `--confirm` before overwriting anything.
|
|
91
|
+
|
|
92
|
+
## Generated files
|
|
93
|
+
|
|
94
|
+
`apps/web/src/payload-types.ts` and `apps/web/src/app/(payload)/admin/importMap.js`
|
|
95
|
+
are generated from `payload.config.ts` and **committed**. Regenerate both after
|
|
96
|
+
any change to the config, a collection or a plugin version, and commit the
|
|
97
|
+
result — CI regenerates them and fails on a difference:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm --dir apps/web generate:types
|
|
101
|
+
pnpm --dir apps/web generate:importmap
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A stale import map is a 500 on any admin screen that renders a component the
|
|
105
|
+
map has never heard of.
|
|
106
|
+
|
|
107
|
+
## CI
|
|
108
|
+
|
|
109
|
+
`.github/workflows/ci.yml` runs two tiers. `fast` runs on every pull request
|
|
110
|
+
and needs no database: install from the lockfile, typecheck, lint, the gates
|
|
111
|
+
below, manifest validation, the generated-files check and tests. `verify` runs
|
|
112
|
+
on pushes to main: it migrates a fresh Postgres and runs a production build.
|
|
113
|
+
|
|
114
|
+
## Workspace gates
|
|
115
|
+
|
|
116
|
+
Each is explained at the top of its script in `scripts/`:
|
|
117
|
+
|
|
118
|
+
- `pnpm check:instances` — `payload`, `@payloadcms/ui`, `react` and `react-dom` each resolve to exactly one copy. Two byte-identical copies are two React contexts, and a runtime failure nothing else can see.
|
|
119
|
+
- `pnpm check:tested` — every workspace package that ships code has at least one test. Not a coverage number. `apps/web` starts on its exception list; delete the entry with your first test.
|
|
120
|
+
- `pnpm check:audit` — high and critical production advisories against an allowlist that carries a reason for each entry, and that fails when an entry stops applying.
|
|
121
|
+
|
|
69
122
|
## Project layout
|
|
70
123
|
|
|
71
124
|
```
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"clean": "rm -rf .next .turbo",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
|
-
"payload": "payload"
|
|
12
|
+
"payload": "payload",
|
|
13
|
+
"generate:types": "payload generate:types",
|
|
14
|
+
"generate:importmap": "payload generate:importmap",
|
|
15
|
+
"migrate": "PAYLOAD_MIGRATING=1 payload migrate",
|
|
16
|
+
"migrate:create": "payload migrate:create",
|
|
17
|
+
"migrate:status": "payload migrate:status",
|
|
18
|
+
"migrate:snapshot": "tsx scripts/write-migration-snapshot.ts",
|
|
19
|
+
"check:migrations": "tsx scripts/check-migrations.ts"
|
|
13
20
|
},
|
|
14
21
|
"dependencies": {
|
|
15
22
|
"@forumone/throughline-approvals": "^{{version_approvals}}",
|
|
@@ -25,7 +32,8 @@
|
|
|
25
32
|
"{{designSystemName}}": "workspace:*",
|
|
26
33
|
{{/if}}"@payloadcms/db-postgres": "^3.83.0",
|
|
27
34
|
"@payloadcms/next": "^3.83.0",
|
|
28
|
-
"@payloadcms/plugin-form-builder": "3.83.0",
|
|
35
|
+
"@payloadcms/plugin-form-builder": "^3.83.0",
|
|
36
|
+
"@payloadcms/plugin-mcp": "^3.83.0",
|
|
29
37
|
"@payloadcms/richtext-lexical": "^3.83.0",
|
|
30
38
|
"graphql": "^16.8.1",
|
|
31
39
|
"inngest": "^4.2.0",
|
|
@@ -38,6 +46,7 @@
|
|
|
38
46
|
"@types/node": "^20.17.0",
|
|
39
47
|
"@types/react": "^19.2.0",
|
|
40
48
|
"@types/react-dom": "^19.2.0",
|
|
49
|
+
"tsx": "^4.21.0",
|
|
41
50
|
"typescript": "^5.6.0"
|
|
42
51
|
}
|
|
43
52
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Three checks on `src/migrations`, each about a failure nobody sees until the
|
|
7
|
+
moment it has to work.
|
|
8
|
+
|
|
9
|
+
pnpm check:migrations
|
|
10
|
+
|
|
11
|
+
With `push: false` a migration is the only way this schema ever changes, so the
|
|
12
|
+
migration chain has to stay usable.
|
|
13
|
+
|
|
14
|
+
## 1. A migration with no schema snapshot
|
|
15
|
+
|
|
16
|
+
`migrate:create` does not diff the config against the database. It diffs it
|
|
17
|
+
against the `.json` snapshot beside the **most recent** migration, and writes one
|
|
18
|
+
only when it generated the SQL itself. So a migration written by hand leaves the
|
|
19
|
+
generator a step behind, and every run after it re-proposes a change that
|
|
20
|
+
already shipped — often as drizzle's interactive rename prompt, which cannot be
|
|
21
|
+
answered non-interactively, so the command exits having written nothing.
|
|
22
|
+
|
|
23
|
+
Every migration is checked, not only the newest: a gap in the middle is inert
|
|
24
|
+
today and was a blocked generator on the day it merged, which is the day this
|
|
25
|
+
needs to fail. Write one with `pnpm migrate:snapshot`.
|
|
26
|
+
|
|
27
|
+
## 2. An orphaned snapshot
|
|
28
|
+
|
|
29
|
+
A `.json` with no `.ts` beside it. Delete a migration and leave its snapshot,
|
|
30
|
+
and that snapshot sorts last and becomes what the generator diffs against — a
|
|
31
|
+
schema no database will ever be in.
|
|
32
|
+
|
|
33
|
+
## 3. A drop that a CASCADE has already carried out
|
|
34
|
+
|
|
35
|
+
Payload's generator can emit, in this order:
|
|
36
|
+
|
|
37
|
+
DROP TABLE "posts" CASCADE;
|
|
38
|
+
ALTER TABLE "search_rels" DROP CONSTRAINT "search_rels_posts_fk";
|
|
39
|
+
|
|
40
|
+
The CASCADE already removed that constraint, so the second statement errors,
|
|
41
|
+
and because a migration runs in a transaction it takes the whole migration — or
|
|
42
|
+
the whole rollback — with it. What is flagged is a bare `DROP CONSTRAINT` or
|
|
43
|
+
`DROP INDEX` that follows a cascading `DROP` in the same function. A bare drop
|
|
44
|
+
with no CASCADE above it is dropping something that is still there, and
|
|
45
|
+
demanding `IF EXISTS` of it would be the kind of blanket rule people learn to
|
|
46
|
+
suppress.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
50
|
+
const MIGRATIONS = path.resolve(here, '../src/migrations')
|
|
51
|
+
|
|
52
|
+
const BARE_DROP = /\bDROP\s+(CONSTRAINT|INDEX)\s+"/gi
|
|
53
|
+
|
|
54
|
+
// A `DROP` that cascaded, kept inside one statement by `[^;]*`. Not the word
|
|
55
|
+
// on its own, which also matches the `ON DELETE cascade` of every foreign key.
|
|
56
|
+
const CASCADE = /\bDROP\b[^;]*\bCASCADE\b/i
|
|
57
|
+
|
|
58
|
+
const stem = (name: string) => name.replace(/\.(json|ts)$/, '')
|
|
59
|
+
|
|
60
|
+
const entries = await readdir(MIGRATIONS).catch((error: NodeJS.ErrnoException) => {
|
|
61
|
+
if (error.code === 'ENOENT') return [] as string[]
|
|
62
|
+
throw error
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const files = entries.filter((name) => name.endsWith('.ts') && name !== 'index.ts').sort()
|
|
66
|
+
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
console.log('No migrations yet. Create the first with:\n\n' + ' pnpm migrate:create initial\n')
|
|
69
|
+
process.exit(0)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Basenames on both sides, because that is what `createMigration` sorts: it
|
|
73
|
+
// reads the directory, keeps the `.json`, sorts, and takes the last.
|
|
74
|
+
const snapshots = new Set(entries.filter((name) => name.endsWith('.json')).map(stem))
|
|
75
|
+
|
|
76
|
+
const missing = files.filter((file) => !snapshots.has(stem(file)))
|
|
77
|
+
|
|
78
|
+
if (missing.length > 0) {
|
|
79
|
+
console.error(
|
|
80
|
+
`${missing.length} migration(s) with no schema snapshot:\n\n` +
|
|
81
|
+
missing.map((file) => ` ${file}\n ${stem(file)}.json <- missing`).join('\n\n') +
|
|
82
|
+
'\n\nmigrate:create diffs the config against the snapshot beside the most\n' +
|
|
83
|
+
'recent migration. Without one it diffs against a schema that is a step\n' +
|
|
84
|
+
'behind, and re-proposes a change that has already shipped. Write it with:\n\n' +
|
|
85
|
+
' pnpm migrate:snapshot\n\n' +
|
|
86
|
+
'Run it only once the migration is complete: the snapshot claims the\n' +
|
|
87
|
+
'database matches the config.\n',
|
|
88
|
+
)
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const migrationNames = new Set(files.map(stem))
|
|
93
|
+
const orphans = [...snapshots].filter((name) => !migrationNames.has(name)).sort()
|
|
94
|
+
|
|
95
|
+
if (orphans.length > 0) {
|
|
96
|
+
console.error(
|
|
97
|
+
`${orphans.length} schema snapshot(s) with no migration beside them:\n\n` +
|
|
98
|
+
orphans.map((name) => ` ${name}.json`).join('\n') +
|
|
99
|
+
'\n\nA snapshot sorting last is what migrate:create diffs against, so one\n' +
|
|
100
|
+
'left behind by a deleted migration describes a schema no database will\n' +
|
|
101
|
+
'reach. Delete it, or restore the migration it belongs to.\n',
|
|
102
|
+
)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const problems: Array<{ file: string; statement: string }> = []
|
|
107
|
+
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const source = await readFile(path.join(MIGRATIONS, file), 'utf8')
|
|
110
|
+
|
|
111
|
+
for (const name of ['up', 'down'] as const) {
|
|
112
|
+
const start = source.indexOf(`export async function ${name}`)
|
|
113
|
+
if (start === -1) continue
|
|
114
|
+
const other = source.indexOf('export async function', start + 1)
|
|
115
|
+
const body = source.slice(start, other === -1 ? undefined : other)
|
|
116
|
+
|
|
117
|
+
for (const match of body.matchAll(BARE_DROP)) {
|
|
118
|
+
const at = match.index ?? 0
|
|
119
|
+
if (!CASCADE.test(body.slice(0, at))) continue
|
|
120
|
+
|
|
121
|
+
const absolute = start + at
|
|
122
|
+
const lineStart = source.lastIndexOf('\n', absolute) + 1
|
|
123
|
+
const lineEnd = source.indexOf('\n', absolute)
|
|
124
|
+
problems.push({
|
|
125
|
+
file,
|
|
126
|
+
statement: source.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim(),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (problems.length > 0) {
|
|
133
|
+
console.error(
|
|
134
|
+
`${problems.length} drop(s) after a CASCADE without IF EXISTS:\n\n` +
|
|
135
|
+
problems.map((p) => ` ${p.file}\n ${p.statement}`).join('\n\n') +
|
|
136
|
+
'\n\nA preceding DROP ... CASCADE has already removed these, so the\n' +
|
|
137
|
+
'migration will abort. Add IF EXISTS:\n\n' +
|
|
138
|
+
' DROP CONSTRAINT IF EXISTS "..."\n' +
|
|
139
|
+
' DROP INDEX IF EXISTS "..."\n',
|
|
140
|
+
)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(
|
|
145
|
+
`Checked ${files.length} migration(s): every drop is guarded, every migration has\n` +
|
|
146
|
+
`its schema snapshot, and the generator will diff against ${stem(files.at(-1)!)}.`,
|
|
147
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { resolveDatabaseConnection } from '../../src/lib/database'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Which database a script is about to write to, and an explicit confirmation of
|
|
5
|
+
it. For seed, import and repair scripts:
|
|
6
|
+
|
|
7
|
+
const where = requireTarget(process.argv, overwriting, 'Replaces every page')
|
|
8
|
+
|
|
9
|
+
A script reads its connection string from the environment like everything else,
|
|
10
|
+
so a shell that happens to have production's connection string loaded points it
|
|
11
|
+
at production and nothing asks. This prints the target before anything is
|
|
12
|
+
written, and demands `--confirm <database>@<host>` when the run would overwrite.
|
|
13
|
+
|
|
14
|
+
It is not authentication — anyone who can run the script can type the token. It
|
|
15
|
+
is the difference between a mistake and a decision, and the print alone is worth
|
|
16
|
+
it: a run aimed at the wrong host is visible before it writes rather than after.
|
|
17
|
+
|
|
18
|
+
Sized to the risk: creating what is missing is recoverable by deleting it;
|
|
19
|
+
replacing what an editor has worked on is not.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface Target {
|
|
23
|
+
host: string
|
|
24
|
+
database: string
|
|
25
|
+
/** The environment variable the connection string came from, or `(none)`. */
|
|
26
|
+
source: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The database the config will actually connect to — through the same resolver
|
|
31
|
+
* `payload.config.ts` uses, not by reading `DATABASE_URI` directly. A guard that
|
|
32
|
+
* reports a different database than the code connects to is not a weaker guard;
|
|
33
|
+
* it is a false one.
|
|
34
|
+
*/
|
|
35
|
+
export function target(): Target {
|
|
36
|
+
let connection = ''
|
|
37
|
+
let source = '(none)'
|
|
38
|
+
try {
|
|
39
|
+
const resolved = resolveDatabaseConnection()
|
|
40
|
+
connection = resolved.value
|
|
41
|
+
source = resolved.name
|
|
42
|
+
} catch {
|
|
43
|
+
// Nothing set at all: a target of "none", which every destructive run refuses.
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(connection)
|
|
48
|
+
return { host: parsed.hostname, database: parsed.pathname.replace(/^\//, ''), source }
|
|
49
|
+
} catch {
|
|
50
|
+
return { host: '(unparseable)', database: '', source }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
The token that has to be typed back. Not the database name on its own: Neon
|
|
56
|
+
names every branch's database `neondb`, so `--confirm neondb` would match
|
|
57
|
+
production and a preview alike. The host distinguishes them, so the host is in
|
|
58
|
+
the token — long enough that it cannot be typed from memory while thinking about
|
|
59
|
+
a different environment.
|
|
60
|
+
*/
|
|
61
|
+
export const confirmToken = (where: Target): string => `${where.database}@${where.host}`
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Print the target, and exit unless a destructive run has named it back.
|
|
65
|
+
*
|
|
66
|
+
* @param argv The script's arguments, for `--confirm <database>@<host>`.
|
|
67
|
+
* @param destructive Whether this run would overwrite existing documents.
|
|
68
|
+
* @param consequence One line naming what would be overwritten, shown in the refusal.
|
|
69
|
+
*/
|
|
70
|
+
export function requireTarget(argv: string[], destructive: boolean, consequence: string): Target {
|
|
71
|
+
const where = target()
|
|
72
|
+
console.log(
|
|
73
|
+
`\n target ${where.database || '(none)'} on ${where.host}\n from ${where.source}`,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (!destructive) return where
|
|
77
|
+
|
|
78
|
+
const index = argv.indexOf('--confirm')
|
|
79
|
+
const confirmed = index === -1 ? undefined : argv[index + 1]
|
|
80
|
+
const expected = confirmToken(where)
|
|
81
|
+
|
|
82
|
+
if (where.database === '' || confirmed !== expected) {
|
|
83
|
+
console.error(
|
|
84
|
+
`\n Refusing to write.\n\n` +
|
|
85
|
+
` ${consequence}\n` +
|
|
86
|
+
` in the database "${where.database}" on ${where.host},\n` +
|
|
87
|
+
` read from ${where.source}.\n\n` +
|
|
88
|
+
` Re-run with --confirm ${where.database ? expected : '<database>@<host>'}\n` +
|
|
89
|
+
` if that is what you meant.\n`,
|
|
90
|
+
)
|
|
91
|
+
process.exit(2)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return where
|
|
95
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { parseArgs } from 'node:util'
|
|
5
|
+
import { getPayload } from 'payload'
|
|
6
|
+
import config from '@payload-config'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Writes the `.json` schema snapshot beside a hand-written migration.
|
|
10
|
+
|
|
11
|
+
pnpm migrate:snapshot # the newest migration
|
|
12
|
+
pnpm migrate:snapshot 20260812_154500_add_jobs
|
|
13
|
+
pnpm migrate:snapshot --force # replace an existing snapshot
|
|
14
|
+
|
|
15
|
+
`migrate:create` diffs the config against the snapshot beside the **most
|
|
16
|
+
recent** migration, and writes a fresh one only when it generates the SQL
|
|
17
|
+
itself. A migration written by hand therefore leaves the generator one step
|
|
18
|
+
behind, and every run after it re-proposes a change that already shipped.
|
|
19
|
+
|
|
20
|
+
This closes the gap the way `createMigration` does, minus the diff: it asks the
|
|
21
|
+
adapter for the schema the config describes and serialises it. The adapter
|
|
22
|
+
builds that schema from the config alone, so this needs **no database**.
|
|
23
|
+
|
|
24
|
+
## When it would be a lie
|
|
25
|
+
|
|
26
|
+
The snapshot says: *the database, once this migration has run, matches the
|
|
27
|
+
config.* Run it only after the hand-written migration is complete. A snapshot
|
|
28
|
+
claiming a config change the migration only half implements makes the next
|
|
29
|
+
generated migration quietly omit the missing half.
|
|
30
|
+
|
|
31
|
+
Overwriting needs `--force`, because the common mistake is running this instead
|
|
32
|
+
of writing the migration: the snapshot is rewritten to match a config that moved
|
|
33
|
+
on, and the change becomes invisible to the generator — the schema drifts with
|
|
34
|
+
no migration anywhere that would apply it.
|
|
35
|
+
|
|
36
|
+
The config is loaded, so the environment it validates must be present. Values
|
|
37
|
+
that only need to parse — secrets, keys — can be placeholders on this run.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
41
|
+
const MIGRATIONS = path.resolve(here, '../src/migrations')
|
|
42
|
+
|
|
43
|
+
// `pnpm run x -- --flag` forwards the `--` itself, which `parseArgs` would read
|
|
44
|
+
// as "positionals from here".
|
|
45
|
+
const argv = process.argv.slice(2).filter((arg) => arg !== '--')
|
|
46
|
+
|
|
47
|
+
const { positionals, values } = parseArgs({
|
|
48
|
+
args: argv,
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
options: { force: { type: 'boolean', default: false } },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const migrations = (await readdir(MIGRATIONS).catch(() => [] as string[]))
|
|
54
|
+
.filter((name) => name.endsWith('.ts') && name !== 'index.ts')
|
|
55
|
+
.map((name) => name.slice(0, -'.ts'.length))
|
|
56
|
+
.sort()
|
|
57
|
+
|
|
58
|
+
if (migrations.length === 0) {
|
|
59
|
+
console.error(`No migrations in ${MIGRATIONS}.`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const target = positionals[0]?.replace(/\.(json|ts)$/, '') ?? migrations.at(-1)!
|
|
64
|
+
|
|
65
|
+
if (!migrations.includes(target)) {
|
|
66
|
+
console.error(
|
|
67
|
+
`No migration named ${target}.\n\nMigrations in src/migrations:\n` +
|
|
68
|
+
migrations.map((name) => ` ${name}`).join('\n') +
|
|
69
|
+
'\n',
|
|
70
|
+
)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const destination = path.join(MIGRATIONS, `${target}.json`)
|
|
75
|
+
const existing = await readFile(destination, 'utf8').then(
|
|
76
|
+
() => true,
|
|
77
|
+
() => false,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (existing && !values.force) {
|
|
81
|
+
console.error(
|
|
82
|
+
`${target}.json already exists, and describes the schema as of that migration.\n` +
|
|
83
|
+
'Rewriting it to match a config that has since changed hides the change from\n' +
|
|
84
|
+
'`migrate:create`. If replacing it is genuinely what you want, pass --force.\n',
|
|
85
|
+
)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (target !== migrations.at(-1)) {
|
|
90
|
+
console.warn(
|
|
91
|
+
`Warning: ${target} is not the newest migration. Only the newest snapshot is\n` +
|
|
92
|
+
'read by migrate:create, so this describes a state nothing will diff against.\n',
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// No database, and nothing that runs on startup (seeds, syncs, cron
|
|
97
|
+
// registration) from a script whose whole job is to serialise a schema.
|
|
98
|
+
const payload = await getPayload({ config, disableDBConnect: true, disableOnInit: true })
|
|
99
|
+
|
|
100
|
+
const db = payload.db as typeof payload.db & {
|
|
101
|
+
requireDrizzleKit: () => { generateDrizzleJson: (schema: unknown) => Promise<unknown> }
|
|
102
|
+
schema: unknown
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { generateDrizzleJson } = db.requireDrizzleKit()
|
|
106
|
+
const snapshot = await generateDrizzleJson(db.schema)
|
|
107
|
+
|
|
108
|
+
// Byte for byte what `createMigration` writes, with no trailing newline, so a
|
|
109
|
+
// hand-written migration's snapshot is indistinguishable from a generated one.
|
|
110
|
+
await writeFile(destination, JSON.stringify(snapshot, null, 2))
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
`Wrote src/migrations/${target}.json\n\n` +
|
|
114
|
+
'Commit it beside the migration. Confirm the generator is unblocked with:\n\n' +
|
|
115
|
+
' pnpm migrate:create check --skip-empty\n\n' +
|
|
116
|
+
'which exits without writing anything when there is no diff.\n',
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
process.exit(0)
|