@bygghammar/cms-platform 0.91.1 → 0.92.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.
- package/CHANGELOG.md +125 -0
- package/README.md +20 -9
- package/docs/NEW-SITE.md +29 -14
- package/docs/PAGES.md +79 -21
- package/docs/PUBLICATION-REVIEW.md +298 -0
- package/migrations/20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql +3298 -0
- package/package.json +8 -2
- package/scripts/cms-review.mjs +832 -0
- package/scripts/review-fingerprint.sql +43 -0
- package/scripts/review-scan.sql +56 -0
- package/src/backup/datasets.ts +203 -2
- package/src/backup/restore-core.ts +382 -11
- package/src/platform/contract.ts +82 -60
- package/src/platform/digest.ts +203 -0
- package/src/platform/migration-manifest.ts +310 -0
- package/src/platform/publication-review.ts +413 -0
- package/src/server/backup-core.ts +291 -18
- package/src/server/page-seo-core.ts +56 -10
- package/src/server/pages-core.ts +90 -53
- package/src/server/redirects-core.ts +51 -9
- package/src/server/schema-drift.ts +21 -65
- package/src/site/route-index.ts +641 -0
- package/src/site/routes.ts +2 -8
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,131 @@ matches `package.json` exactly. See
|
|
|
8
8
|
Versions 0.1.0 through 0.78.0 predate this repository and were reconstructed
|
|
9
9
|
from the published npm tarballs.
|
|
10
10
|
|
|
11
|
+
## 0.92.1
|
|
12
|
+
|
|
13
|
+
- Fix the page-builder save and publish RPC call by removing the unsupported
|
|
14
|
+
`_source_page_id` argument. The database supplies the source page internally,
|
|
15
|
+
preserving self-reveal protection. No migration is added or changed, and
|
|
16
|
+
platform contract v3 is unchanged.
|
|
17
|
+
- Clarify publication RPC errors so an argument mismatch is not presented as
|
|
18
|
+
proof that migrations must be replayed. Include only a recognized diagnostic
|
|
19
|
+
code and request a schema comparison before applying changes.
|
|
20
|
+
|
|
21
|
+
## 0.92.0
|
|
22
|
+
|
|
23
|
+
**Breaking: platform contract v3.** A site pinned to this package must call
|
|
24
|
+
`assertPlatformContract(3)`, and its database must have applied
|
|
25
|
+
`20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql`. Upgrading a live
|
|
26
|
+
site is an ordered procedure, not a deploy — read the next four bullets before
|
|
27
|
+
starting one.
|
|
28
|
+
|
|
29
|
+
- **The publication reader changed, and on some sites that takes a page down.**
|
|
30
|
+
`0.90.0-m7.12` dropped closed publication rows and then took the newest of
|
|
31
|
+
what was left; v3 takes the newest row that has started and only then asks
|
|
32
|
+
whether it is still open. The new order is what makes a scheduled window mean
|
|
33
|
+
what it says, but where a page's newest row has expired and an older one is
|
|
34
|
+
still open, the old reader served the open row and the new one serves nothing.
|
|
35
|
+
The page goes dark on deploy with no error anywhere.
|
|
36
|
+
- **`cms-review` is the procedure for that**, and ships as a `bin` of this
|
|
37
|
+
package. `cms-review scan` reads the site and classifies every live page;
|
|
38
|
+
pages the upgrade can keep alive on its own are repaired without being asked
|
|
39
|
+
about, and the three classifications that carry two opposite intentions —
|
|
40
|
+
contradictory rows, the two readers serving different versions, and a page
|
|
41
|
+
whose only window has not opened yet — stop the upgrade until somebody
|
|
42
|
+
answers. `cms-review approve` seals the answers to one database, one release,
|
|
43
|
+
one package version, one migration file and one hour; the migration recomputes
|
|
44
|
+
all of it and refuses an approval that no longer matches. Full procedure in
|
|
45
|
+
`docs/PUBLICATION-REVIEW.md`.
|
|
46
|
+
- **`cms-review record` carries an approval to a machine with no psql prompt.**
|
|
47
|
+
A self-hosted VPS installs the approval as a file and reads it back with a
|
|
48
|
+
shell function in the deployment template; that file used to be written by
|
|
49
|
+
hand against a format nobody generated, and four things about it disagreed
|
|
50
|
+
with what `cms-review` emitted — hyphenated nonces, millisecond timestamps, a
|
|
51
|
+
missing `decision_count`, and two email addresses the record had no slot for,
|
|
52
|
+
so the migration recorded a reviewed change as unattended. Both roads are now
|
|
53
|
+
rendered from one builder, so they cannot drift again. The record is nineteen
|
|
54
|
+
ordered `key=value` lines, printable ASCII, checked field by field before it
|
|
55
|
+
is printed. `decisions` is canonical JSON, which is what makes the shell's
|
|
56
|
+
`sha256sum` of that text, the migration's digest of the same value as `jsonb`
|
|
57
|
+
and `decisions_digest` agree; a zero-decision approval is expressible as
|
|
58
|
+
`decision_count=0` with `decisions=[]`. `ops/approval-record-roundtrip.mjs`
|
|
59
|
+
proves the whole path end to end against the deployment template's real
|
|
60
|
+
reader and a real PostgreSQL; it is `bun run test:roundtrip`, and step 3 of
|
|
61
|
+
the release process asks for it whenever the record's format moves.
|
|
62
|
+
- **Two identities, and they are not interchangeable.** `database_identity`
|
|
63
|
+
names the cluster, which is what the deployment tooling checks; the new
|
|
64
|
+
`review_database_identity` names the database the scan actually read, which
|
|
65
|
+
is what the migration checks and what it now requires. A cluster holds more
|
|
66
|
+
than one database and only one of them was reviewed. The run records the
|
|
67
|
+
sharper one, in `cms_publication_review_runs.review_database_identity` — a
|
|
68
|
+
column named for the setting it holds, rather than for the other one.
|
|
69
|
+
- **Release order, and it is not optional.** Apply every missing canonical
|
|
70
|
+
migration; scan, review and apply the publication decisions; then deploy this
|
|
71
|
+
package with the site pinned to contract v3. The migration itself publishes,
|
|
72
|
+
unpublishes and edits nothing — applying it changes what the site can be
|
|
73
|
+
*asked*, not what it serves — which is what makes the first step safe to take
|
|
74
|
+
ahead of the deploy.
|
|
75
|
+
- **A site with nothing to decide upgrades with no ceremony.** The migration
|
|
76
|
+
counts the pages needing an answer first; where there are none it is an
|
|
77
|
+
ordinary schema change. Where there is even one and no approval was supplied,
|
|
78
|
+
it stops and names what it did not get rather than choosing.
|
|
79
|
+
- **Five facts that used to be one.** "The deployment succeeded" stood for the
|
|
80
|
+
migration having run, the schema therefore offering a capability, somebody
|
|
81
|
+
having understood what the new reader does to the existing rows, those
|
|
82
|
+
decisions having been applied, and the routes having been activated. Each now
|
|
83
|
+
has its own record and the later ones refuse to be written while an earlier
|
|
84
|
+
one is missing. `cms_schema_capabilities` is written by the migration that
|
|
85
|
+
makes it true, so it cannot drift from the schema it describes.
|
|
86
|
+
- **Duplicating a page is one transaction.** It was a sequence of unrelated
|
|
87
|
+
inserts that could leave a page with no version, or two copies behind one
|
|
88
|
+
retried click. `cms_duplicate_page` takes the page lock, checks that the
|
|
89
|
+
source still holds the version and the content the caller read, and records
|
|
90
|
+
the copy against a caller-supplied idempotency key inside the same
|
|
91
|
+
transaction — so a retry returns the first copy instead of making a second,
|
|
92
|
+
and a rollback leaves the key unspent.
|
|
93
|
+
- **A route has one owner.** Two pages could hold the same URL, because nothing
|
|
94
|
+
in the database was responsible for the pair. `(origin, path)` is now a
|
|
95
|
+
primary key, and a release's claims are written against it. A clash comes
|
|
96
|
+
back as a value listing every colliding route and changes nothing, rather
|
|
97
|
+
than raising on the first one — a deploy can then report every clash at once
|
|
98
|
+
instead of sending an operator to find the rest by hand.
|
|
99
|
+
- **One lock order everywhere**: pages, then publications, then review, with the
|
|
100
|
+
route advisory lock taken after the page lock. Two deploys racing for the same
|
|
101
|
+
route take turns instead of interleaving.
|
|
102
|
+
- **One list of canonical migrations.** Two modules used to carry the same sixty
|
|
103
|
+
names, and the test comparing them could only report that they had already
|
|
104
|
+
drifted apart — it could not say which was right. The catalog is now
|
|
105
|
+
generated from the `migrations/` directory with a digest per file, and
|
|
106
|
+
`bun run manifest:check` fails the build — and the release — when the file
|
|
107
|
+
and the catalog disagree. `cms-review` cross-checks it before sealing
|
|
108
|
+
anything, because an approval binds itself to what the package ships.
|
|
109
|
+
- **Replace mode clears once, before anything is written.** It used to delete
|
|
110
|
+
inside the per-table loop, so a later table's `DELETE` could remove rows an
|
|
111
|
+
earlier one had just written, and a table the archive said nothing about was
|
|
112
|
+
skipped before the delete ran — leaving it populated in the one mode whose
|
|
113
|
+
whole job is to replace it. The selected groups are now emptied in a single
|
|
114
|
+
pass in reverse dependency order, with deferred pointers released first, and
|
|
115
|
+
a clear that fails aborts that table's children instead of warning.
|
|
116
|
+
- **Breaking for callers of `backup/restore-core`**: deciding what to clear and
|
|
117
|
+
rendering it are now two functions. `buildClearPlan(groups)` returns the
|
|
118
|
+
steps, and it is the one that drops insert-only tables; `buildClear(step)`
|
|
119
|
+
renders one and always answers a statement, where it used to be
|
|
120
|
+
`Statement | null` and returned null for an insert-only table. TypeScript
|
|
121
|
+
rejects the old call, but a suite that only runs tests will see a `DELETE`
|
|
122
|
+
for a table that used to be spared — so read `buildClearPlan` first if you
|
|
123
|
+
clear tables yourself.
|
|
124
|
+
- **Archive schema 3.** Backups written by this version declare
|
|
125
|
+
`SCHEMA_VERSION` 3 and carry the three publication-review tables. The archive
|
|
126
|
+
*format* is unchanged at 2, so a 0.91.x build still opens the container — and
|
|
127
|
+
then refuses it, because it has no branch that says what schema 3 means. That
|
|
128
|
+
refusal is the point: an archive is read by a build that knows what every
|
|
129
|
+
version in it meant, or it is not read.
|
|
130
|
+
- **Restoring a 0.91.x backup into a v3 site is supported.** Schema 2 has no
|
|
131
|
+
review history, no page status and no publication signature, but everything
|
|
132
|
+
it does say still means what it said, and what it omits is recoverable by
|
|
133
|
+
scanning the restored content. The restore records a reconciliation run
|
|
134
|
+
rather than inventing a review that never happened.
|
|
135
|
+
|
|
11
136
|
## 0.91.1
|
|
12
137
|
|
|
13
138
|
- Documentation correction, and nothing else. The npm-facing `README.md` said
|
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ exactly as it did before locks existed.
|
|
|
43
43
|
| `docs/MEDIA-FOLDERS.md` | Automatic month folders, custom upload folders and their sanitising, library browsing |
|
|
44
44
|
| `docs/GOOGLE-SHEETS.md` | Service-account form delivery, the connection test, and the server variables it needs |
|
|
45
45
|
| `docs/TYPOGRAPHY.md` | Independent Google Fonts for the menu and the footer |
|
|
46
|
+
| `docs/PUBLICATION-REVIEW.md` | The `cms-review` procedure for upgrading a live site to contract v3 |
|
|
46
47
|
|
|
47
48
|
## Server variables
|
|
48
49
|
|
|
@@ -59,21 +60,31 @@ related is ever read in the browser, and the font picker needs no API key. See
|
|
|
59
60
|
releases are always compatible.
|
|
60
61
|
- `PLATFORM_CONTRACT_VERSION` (`platform/contract`) is bumped when a shared
|
|
61
62
|
screen starts requiring schema or adapter capabilities an older site lacks.
|
|
62
|
-
Sites pin it in a test. It is **
|
|
63
|
-
restoring go through database functions
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
Sites pin it in a test. It is **v3**: page saves, publishing, trashing and
|
|
64
|
+
restoring go through database functions (v2), and a release records the five
|
|
65
|
+
facts it used to assert with one sentence — migration applied, schema
|
|
66
|
+
capability, publication scan, decisions applied, routes activated — as five
|
|
67
|
+
separate records the later of which refuse to be written without the earlier
|
|
68
|
+
(v3). Upgrading straight from the last pre-v2 published package,
|
|
69
|
+
`0.90.0-m7.12`, means applying all four new canonical migrations, in
|
|
70
|
+
chronological order:
|
|
66
71
|
|
|
67
72
|
1. `20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql`
|
|
68
73
|
2. `20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql`
|
|
69
74
|
3. `20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql`
|
|
75
|
+
4. `20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql`
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
Files 2 and 3 are the pair that establishes contract-v2 publication
|
|
72
78
|
behaviour, but the first is canonical schema too, so applying only the pair
|
|
73
|
-
still leaves `missingMigrations` and `compareSchema` reporting drift.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
still leaves `missingMigrations` and `compareSchema` reporting drift. File 4
|
|
80
|
+
adds contract v3: the release ledger, the review model, the route claims, one
|
|
81
|
+
lock order every page mutation follows, and duplication as a single
|
|
82
|
+
transaction. It replaces five publication function bodies at the signatures
|
|
83
|
+
they already have, without changing what any of them decides, so nothing a
|
|
84
|
+
visitor sees today changes.
|
|
85
|
+
Apply them before the v3 package is deployed, then verify separately:
|
|
86
|
+
`missingMigrations(applied)` reports nothing missing, **and**
|
|
87
|
+
`assertPlatformContract(3)` passes. The second only compares version numbers
|
|
77
88
|
in the site's own code and never reads the database, so it is no substitute
|
|
78
89
|
for the first. `docs/NEW-SITE.md` gives the full order.
|
|
79
90
|
- `migrations/` is schema only. Site content, SEO copy and seeds never belong
|
package/docs/NEW-SITE.md
CHANGED
|
@@ -34,34 +34,42 @@ Add one test that pins the contract version the site was built against and one
|
|
|
34
34
|
that asserts `missingMigrations(appliedFilenames)` is empty. See
|
|
35
35
|
`tests/platform-contract.test.ts` in any existing site for the exact shape.
|
|
36
36
|
|
|
37
|
-
The current contract is **v2
|
|
38
|
-
draft save included
|
|
39
|
-
fallback
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
The current contract is **v3**. It is v2 for the reason v2 existed — saving a
|
|
38
|
+
page version, an ordinary draft save included, is a single database function
|
|
39
|
+
call with no JavaScript fallback — and v3 because a release now records the
|
|
40
|
+
five facts it used to assert with one sentence. "The deployment succeeded" used
|
|
41
|
+
to mean, all at once: the migration ran, the schema therefore offers a
|
|
42
|
+
capability, somebody looked at the existing publication rows and understood
|
|
43
|
+
what the new reader does differently to them, those decisions were applied, and
|
|
44
|
+
the routes were activated. Each fails on its own, so each has its own record,
|
|
45
|
+
and the later ones refuse to be written while an earlier one is missing.
|
|
46
|
+
|
|
47
|
+
Four canonical migrations are new since the last published `0.90.0-m7.12`
|
|
48
|
+
package, so a site upgrading from that baseline is missing all four:
|
|
42
49
|
|
|
43
50
|
| File | Adds |
|
|
44
51
|
| --- | --- |
|
|
45
52
|
| `20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql` | restates the `draft` default, records the `pages.status` column semantics at that migration step, and adds two partial `pages` indexes: one on `key` for undeleted status-published rows, one on `status`/`updated_at` for undeleted rows |
|
|
46
53
|
| `20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql` | `page_publications` — the row that decides what is public — the backfill for pages that were already live, and the first transactional page functions |
|
|
47
54
|
| `20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql` | the rest of the transactional page lifecycle: reveals, trash, restore and cancellation, plus two repair `UPDATE`s |
|
|
55
|
+
| `20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql` | contract v3: the installation identity, the schema-capability record, the canonical digest functions, the publication review model, the route claims, the readiness and activation records, one lock order every page mutation follows, and `cms_duplicate_page` |
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
Files two and three are the contract-v2 publication pair, and the fourth is
|
|
58
|
+
contract v3. All four are in the canonical schema manifest, so
|
|
59
|
+
`missingMigrations` and `compareSchema` report every one that has not been
|
|
60
|
+
applied — applying only the pair still leaves the schema out of sync. Go out in
|
|
61
|
+
this order, and not in another:
|
|
54
62
|
|
|
55
63
|
1. Apply every missing canonical migration. Upgrading from `0.90.0-m7.12`, that
|
|
56
|
-
is all
|
|
57
|
-
2. Deploy the package and the site pinned to
|
|
64
|
+
is all four files above, in chronological order.
|
|
65
|
+
2. Deploy the package and the site pinned to v3.
|
|
58
66
|
3. Verify both: `missingMigrations(applied)` is empty, and
|
|
59
|
-
`assertPlatformContract(
|
|
67
|
+
`assertPlatformContract(3)` passes. The second compares two version numbers
|
|
60
68
|
in the site's own code and never reads the database, so it is no substitute
|
|
61
69
|
for the first.
|
|
62
70
|
4. Let editors back in.
|
|
63
71
|
|
|
64
|
-
The migrations are designed to be applied before the
|
|
72
|
+
The migrations are designed to be applied before the v3 package is deployed,
|
|
65
73
|
and the still-running m7.12 package remains compatible during that ordered
|
|
66
74
|
rollout. They are not pure DDL and not function-only: the set carries an
|
|
67
75
|
`ALTER … SET DEFAULT`, indexes and a column comment, a backfill `INSERT`, two
|
|
@@ -117,12 +125,19 @@ Wrap the shared server cores in the site's own `createServerFn` files:
|
|
|
117
125
|
| `server/forms-core` | form definitions, versions, settings, delivery log |
|
|
118
126
|
| `server/redirects-core` | path normalisation and redirect writes |
|
|
119
127
|
| `server/page-seo-core` | slug conflicts and slug-change 301s |
|
|
128
|
+
| `site/route-index` | the one spelling of a page's address, shared by all of the above |
|
|
120
129
|
| `server/media-usage-core` | usage scan and remote import |
|
|
121
130
|
| `server/media-cleanup-core` | duplicate/rename planning and apply |
|
|
122
131
|
|
|
123
132
|
The site supplies clients, storage and page-key → path maps; the package owns
|
|
124
133
|
the rules. Keep privileged clients inside handlers.
|
|
125
134
|
|
|
135
|
+
`resolvePathCore` and `detectSlugConflictsCore` both need `defaultLocale`, and
|
|
136
|
+
`resolvePathCore` also needs the `locale` the site derived from the request's
|
|
137
|
+
domain. Pass the site's real values: a wrong default locale moves every
|
|
138
|
+
non-default page's prefix, and passing the default locale for every request
|
|
139
|
+
turns a locale domain back into a site that serves one language.
|
|
140
|
+
|
|
126
141
|
## 7. Backoffice routes
|
|
127
142
|
|
|
128
143
|
Each backoffice route is a thin adapter around the package view
|
package/docs/PAGES.md
CHANGED
|
@@ -30,6 +30,41 @@ the same question of the same rows.
|
|
|
30
30
|
|
|
31
31
|
Knowing the URL gets a visitor a 404.
|
|
32
32
|
|
|
33
|
+
## One address, spelled one way
|
|
34
|
+
|
|
35
|
+
A page's address is the locale prefix plus its slug, and every part of the
|
|
36
|
+
platform that needs to know one asks the same two functions for it:
|
|
37
|
+
`servedPath` builds it and `normalizeRoutePath` spells it. The site's own link
|
|
38
|
+
building, the conflict check an editor sees while typing a slug, the resolver
|
|
39
|
+
that turns an incoming request into a page and the route claims written at
|
|
40
|
+
deploy all go through that pair, in `site/route-index`.
|
|
41
|
+
|
|
42
|
+
They used to have three opinions between them, and each disagreement was a bug
|
|
43
|
+
you could see from the outside:
|
|
44
|
+
|
|
45
|
+
- The resolver put every locale's slugs into one map, first one in wins. Two
|
|
46
|
+
pages sharing a slug in different locales were one page, and the loser was
|
|
47
|
+
unreachable at every address it was supposed to answer at.
|
|
48
|
+
- The conflict check compared bare slug strings, one locale at a time. `/Om-Oss`
|
|
49
|
+
and `/om-oss` were two pages to the editor and one page to the browser; a
|
|
50
|
+
Swedish page slugged `en` and the English front page both answered at `/en`
|
|
51
|
+
and nothing ever said so.
|
|
52
|
+
- Neither knew that a page with no slug in a locale is still served there under
|
|
53
|
+
its default-locale slug, so an address the site really serves could be claimed
|
|
54
|
+
twice.
|
|
55
|
+
|
|
56
|
+
`resolvePathCore` now takes the locale the host derived from the domain, and a
|
|
57
|
+
locale prefix in the path overrides it — the address the visitor typed is the
|
|
58
|
+
address they asked for. Its answer carries the locale it resolved under, so the
|
|
59
|
+
caller renders the language the URL named rather than the one the host guessed.
|
|
60
|
+
Within that locale a slug answers both prefixed and bare, because a locale
|
|
61
|
+
served from its own domain has no prefix to strip; across locales nothing is
|
|
62
|
+
shared.
|
|
63
|
+
|
|
64
|
+
Stored redirect rows are the exception: they are literal strings an editor
|
|
65
|
+
typed, and they are matched as they were stored. Re-spelling one through the
|
|
66
|
+
route identity would look it up at an address the table does not hold.
|
|
67
|
+
|
|
33
68
|
## One rule decides what is public
|
|
34
69
|
|
|
35
70
|
`src/cms/page-publication.ts` holds it, in a sentence:
|
|
@@ -164,42 +199,65 @@ carry a 404 five seconds past the moment a page goes live.
|
|
|
164
199
|
|
|
165
200
|
### The migrations, and the order they go out in
|
|
166
201
|
|
|
167
|
-
|
|
168
|
-
upgrading from that baseline is missing all
|
|
202
|
+
Four files are new since the last published `0.90.0-m7.12` package, so a site
|
|
203
|
+
upgrading from that baseline is missing all four:
|
|
169
204
|
|
|
170
205
|
| File | Adds | Needed for |
|
|
171
206
|
| --- | --- | --- |
|
|
172
207
|
| `20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql` | restates the `draft` default, records the `pages.status` column semantics at that migration step, and adds two partial `pages` indexes: one on `key` for undeleted status-published rows, one on `status`/`updated_at` for undeleted rows | canonical schema parity |
|
|
173
208
|
| `20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql` | `page_publications`, the backfill, `cms_save_page_version`, `cms_set_page_status`, `cms_schedule_page_reveal` | contract-v2 publication behaviour |
|
|
174
209
|
| `20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql` | reveals and audit metadata inside `cms_save_page_version`, `cms_soft_delete_page`, `cms_restore_page`, `cms_cancel_page_publication`, `cms_schedule_page_version`, and two repair `UPDATE`s | contract-v2 publication behaviour |
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
halfway.
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
| `20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql` | `app.cms_lock_pages` and the rewritten bodies of `cms_save_page_version`, `cms_set_page_status`, `cms_schedule_page_reveal`, `cms_schedule_page_version` and `cms_cancel_page_publication` — the same signatures, taking the same locks in the same order — plus `cms_duplicate_page` and the ledger it retries against | contract-v3 publication behaviour |
|
|
211
|
+
|
|
212
|
+
The last three establish contract-v3 publication behaviour: together they are
|
|
213
|
+
what `PLATFORM_CONTRACT_VERSION = 3` means. A site declares the contract it was
|
|
214
|
+
built against and `assertPlatformContract` refuses the mismatch at startup,
|
|
215
|
+
rather than letting an editor discover it when a publish fails halfway.
|
|
216
|
+
|
|
217
|
+
The fourth file changes no signature and no decision: every one of the five
|
|
218
|
+
bodies it replaces answers exactly what it answered before. What changes is the
|
|
219
|
+
order in which each one takes its locks. Every page a statement will touch is
|
|
220
|
+
locked first, in `uuid` order, by a single `app.cms_lock_pages` call; only then
|
|
221
|
+
are that page's publication rows locked, by `id`. Two things were wrong before.
|
|
222
|
+
A save that also scheduled a reveal locked its own page and then, still holding
|
|
223
|
+
it, reached for the reveal's target — so two editors saving two pages that point
|
|
224
|
+
at each other could each end up holding what the other was waiting for. And
|
|
225
|
+
`cms_cancel_page_publication` locked the publication before the page, the
|
|
226
|
+
reverse of every sibling. One order, with the page tier taken in one statement,
|
|
227
|
+
is what removes both.
|
|
228
|
+
|
|
229
|
+
`cms_duplicate_page` is that same rule applied to a copy. Source and destination
|
|
230
|
+
are locked together, the source's blocks are re-identified and written to a new
|
|
231
|
+
draft, and the caller's idempotency key is recorded — one transaction that
|
|
232
|
+
either produces the whole copy or leaves nothing behind. The caller sends a
|
|
233
|
+
digest of the content it read, so a source that moved in between is refused
|
|
234
|
+
rather than quietly copied from something the editor never saw, and a retry
|
|
235
|
+
carrying the same key answers with the copy that already exists instead of
|
|
236
|
+
making a second one.
|
|
237
|
+
|
|
238
|
+
All four are listed in `SCHEMA_MIGRATIONS`. `compareSchema` names whichever of
|
|
183
239
|
them is missing and does not report in sync until every canonical migration has
|
|
184
240
|
been applied, so applying only the publication pair leaves the drift check
|
|
185
|
-
pointing at the first file.
|
|
241
|
+
pointing at the first file and the fourth.
|
|
186
242
|
|
|
187
243
|
Release order, and it is not interchangeable:
|
|
188
244
|
|
|
189
245
|
1. **Apply every missing canonical migration.** Upgrading from `0.90.0-m7.12`,
|
|
190
|
-
that is all
|
|
246
|
+
that is all four files above, in chronological order. They add a column
|
|
191
247
|
default, indexes and a comment, a table, functions and replaced function
|
|
192
|
-
bodies, and the running m7.12 package keeps working against them
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
248
|
+
bodies, and the running m7.12 package keeps working against them — including
|
|
249
|
+
the fourth, whose replaced bodies keep both the signatures and the answers
|
|
250
|
+
that package already expects. They are not, however, pure DDL: the second
|
|
251
|
+
backfills an open publication for every published page that has none, and the
|
|
252
|
+
third runs two repair `UPDATE`s over rows the old code left inconsistent —
|
|
253
|
+
retiring publications still open on pages already in the trash, and setting
|
|
254
|
+
those pages' status back to `draft`. Both are described below. Nothing is
|
|
255
|
+
deleted and no page that is public before the upgrade stops being public
|
|
256
|
+
because of it.
|
|
257
|
+
2. **Deploy the contract-v3 package.** It calls the new signatures, which by then
|
|
200
258
|
exist.
|
|
201
259
|
3. **Verify both.** `compareSchema` reports in sync — no canonical migration
|
|
202
|
-
missing — and `assertPlatformContract(
|
|
260
|
+
missing — and `assertPlatformContract(3)` returns. The second compares two
|
|
203
261
|
version numbers in the site's own code and never reads the database, so on
|
|
204
262
|
its own it says nothing about whether the migrations ran.
|
|
205
263
|
4. **Let editors back in.**
|