@forumone/throughline-publishing 0.3.4 → 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 +63 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/approval.d.ts.map +1 -1
- package/dist/pipeline/steps/approval.js +13 -1
- package/dist/pipeline/steps/approval.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# @forumone/throughline-publishing
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d20f909: Bind an approval to the document's content rather than to its `updatedAt`.
|
|
8
|
+
|
|
9
|
+
`request_approval` stored `String(document['updatedAt'] ?? …)` as `targetVersion`,
|
|
10
|
+
and publishing's approval step recomputed the same expression at publish time. So
|
|
11
|
+
an approval was tied to a timestamp that moves on **every** save. An editor fixing
|
|
12
|
+
a typo between an approver opening the request and clicking approve invalidated the
|
|
13
|
+
approval — and the approver spent that time reading a version that no longer
|
|
14
|
+
existed.
|
|
15
|
+
|
|
16
|
+
Requiring re-approval after an edit is a defensible rule. Inheriting it from
|
|
17
|
+
whichever timestamp field happened to be nearby is not, and it is why **autosave
|
|
18
|
+
could not be turned on** anywhere the approvals plugin is installed: autosave moves
|
|
19
|
+
`updatedAt` every couple of seconds of typing, so a pending approval would be
|
|
20
|
+
invalidated continuously.
|
|
21
|
+
|
|
22
|
+
Both sides now call `documentContentHash(document)`, new in
|
|
23
|
+
`@forumone/throughline-core`. It hashes the document with the metadata that moves
|
|
24
|
+
without the content moving stripped at every level — `id`, `createdAt`,
|
|
25
|
+
`updatedAt`, `_status`, `__v`, `_id`, `globalType` — over keys in sorted order,
|
|
26
|
+
since blocks come back out of JSONB in no promised order. Array order is preserved,
|
|
27
|
+
because that is the order of the blocks on the page. `{ exclude }` adds
|
|
28
|
+
app-specific bookkeeping fields to the strip list.
|
|
29
|
+
|
|
30
|
+
The rule is now the one that was wanted all along: a save that changed nothing
|
|
31
|
+
keeps a granted approval, a save that changed something invalidates it, and an edit
|
|
32
|
+
that is reverted brings the approval back. That last one is why this is a content
|
|
33
|
+
hash rather than a version id — a version id moves whether or not the content did.
|
|
34
|
+
|
|
35
|
+
The two sides only agree because they load the document identically, with
|
|
36
|
+
`payload.findByID({ collection, id, draft: true })` at the config's default depth.
|
|
37
|
+
A populated relationship and a bare relationship id are different values and no
|
|
38
|
+
normalising makes them one, so a caller hashing a document fetched at some other
|
|
39
|
+
depth gets a hash that matches nothing. That is stated on the function.
|
|
40
|
+
|
|
41
|
+
**Approvals pending at upgrade must be re-requested.** Their `targetVersion` holds
|
|
42
|
+
an ISO timestamp; the publish step now computes a hash, so nothing matches and
|
|
43
|
+
those documents report `approval-required` until a fresh request is granted. No
|
|
44
|
+
migration is offered, because the old value cannot be converted — the content it
|
|
45
|
+
was granted against is not recoverable from a timestamp. Grant a moment for
|
|
46
|
+
in-flight requests to clear before upgrading, or expect approvers to be asked once
|
|
47
|
+
more.
|
|
48
|
+
|
|
49
|
+
Also exports `isDraftWrite` from `@forumone/throughline-publishing`. It is the
|
|
50
|
+
predicate the plugin's own trust boundary uses to tell a "Save draft" apart from
|
|
51
|
+
an unpublish, and it is unavailable to host code that needs the same answer: an
|
|
52
|
+
`afterChange` hook cannot work it out, because Payload sets `data._status =
|
|
53
|
+
'draft'` on any `draft: true` update before the hooks run and `previousDoc` is the
|
|
54
|
+
latest _version_ rather than the live document. With autosave on, a host hook that
|
|
55
|
+
drops a cache or sends a notification fires every few seconds of typing unless it
|
|
56
|
+
asks this first.
|
|
57
|
+
|
|
58
|
+
Minor rather than patch on all three: `documentContentHash` and `isDraftWrite` are
|
|
59
|
+
new public API, and the stored meaning of `targetVersion` changes.
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [d20f909]
|
|
64
|
+
- @forumone/throughline-core@0.3.0
|
|
65
|
+
|
|
3
66
|
## 0.3.4
|
|
4
67
|
|
|
5
68
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,21 @@ export type { PipelineContext, PipelineResult, PipelineStep, PipelineStepResult,
|
|
|
8
8
|
*/
|
|
9
9
|
export { getPublishStatus, getPublishingService, publishDocument, unpublishDocument, } from './service.js';
|
|
10
10
|
export type { DocumentActionArgs, PublishOutcome, PublishRequest, PublishStatusOutcome, PublishingActor, PublishingService, UnpublishOutcome, } from './service.js';
|
|
11
|
+
/**
|
|
12
|
+
* Whether the update in flight on this document is a draft write — a "Save
|
|
13
|
+
* draft", or a tick of autosave — rather than one that changes what the
|
|
14
|
+
* public sees.
|
|
15
|
+
*
|
|
16
|
+
* `afterChange` cannot work this out for itself. Payload sets
|
|
17
|
+
* `data._status = 'draft'` on any `draft: true` update before the hooks
|
|
18
|
+
* run, and `previousDoc` is the latest *version* rather than the live
|
|
19
|
+
* document, so a draft save of a published page and an unpublish of it look
|
|
20
|
+
* the same from inside the hook. The publishing plugin already records the
|
|
21
|
+
* operation's real `draft` argument in `beforeOperation` for its own trust
|
|
22
|
+
* boundary; this exposes the same answer to host hooks.
|
|
23
|
+
*
|
|
24
|
+
* With autosave on, a host `afterChange` that drops a cache or sends a
|
|
25
|
+
* notification fires every few seconds of typing unless it asks this first.
|
|
26
|
+
*/
|
|
27
|
+
export { isDraftWrite } from './hooks/draft-writes.js';
|
|
11
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AAE5B;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AAE5B;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,21 @@ export { publishingPlugin } from './plugin.js';
|
|
|
5
5
|
* given user, with no API key and correct audit attribution.
|
|
6
6
|
*/
|
|
7
7
|
export { getPublishStatus, getPublishingService, publishDocument, unpublishDocument, } from './service.js';
|
|
8
|
+
/**
|
|
9
|
+
* Whether the update in flight on this document is a draft write — a "Save
|
|
10
|
+
* draft", or a tick of autosave — rather than one that changes what the
|
|
11
|
+
* public sees.
|
|
12
|
+
*
|
|
13
|
+
* `afterChange` cannot work this out for itself. Payload sets
|
|
14
|
+
* `data._status = 'draft'` on any `draft: true` update before the hooks
|
|
15
|
+
* run, and `previousDoc` is the latest *version* rather than the live
|
|
16
|
+
* document, so a draft save of a published page and an unpublish of it look
|
|
17
|
+
* the same from inside the hook. The publishing plugin already records the
|
|
18
|
+
* operation's real `draft` argument in `beforeOperation` for its own trust
|
|
19
|
+
* boundary; this exposes the same answer to host hooks.
|
|
20
|
+
*
|
|
21
|
+
* With autosave on, a host `afterChange` that drops a cache or sends a
|
|
22
|
+
* notification fires every few seconds of typing unless it asks this first.
|
|
23
|
+
*/
|
|
24
|
+
export { isDraftWrite } from './hooks/draft-writes.js';
|
|
8
25
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAmB9C;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAmB9C;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAYrB;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.d.ts","sourceRoot":"","sources":["../../../src/pipeline/steps/approval.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"approval.d.ts","sourceRoot":"","sources":["../../../src/pipeline/steps/approval.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,eAErC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,YA+C1B,CAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { documentContentHash } from '@forumone/throughline-core';
|
|
1
2
|
/**
|
|
2
3
|
* Symbol the approvals plugin attaches its resolver under. Publishing's
|
|
3
4
|
* approval step looks here when no resolver is supplied via options, which
|
|
@@ -29,7 +30,18 @@ export const approvalStep = async (ctx) => {
|
|
|
29
30
|
suggestion: 'Register approvalsPlugin in your Payload config (it attaches the resolver automatically) or pass an explicit `approvalResolver` to publishingPlugin.',
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
/*
|
|
34
|
+
The approval binds to the document's content, not to its `updatedAt`. The
|
|
35
|
+
approvals plugin's `request_approval` computes this same hash, with this
|
|
36
|
+
same function, over a document loaded by the same `findByID` call — so an
|
|
37
|
+
approval granted on what an approver read still resolves after a save that
|
|
38
|
+
changed nothing, and does not resolve after one that changed something.
|
|
39
|
+
|
|
40
|
+
Binding to `updatedAt` made that a property of a timestamp rather than a
|
|
41
|
+
rule: any save at all invalidated a pending approval, which is also why
|
|
42
|
+
autosave and approvals could not both be on. See #341.
|
|
43
|
+
*/
|
|
44
|
+
const versionId = await documentContentHash(ctx.document);
|
|
33
45
|
const approval = await resolver.getActiveApproval(ctx.collection.slug, ctx.documentId, versionId);
|
|
34
46
|
if (!approval) {
|
|
35
47
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/pipeline/steps/approval.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/pipeline/steps/approval.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAIhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC,GAAG,CACjD,0CAA0C,CAC3C,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB,KAAK,EAAE,GAAG,EAAE,EAAE;IACtD,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAEzC,CAAA;IACb,IAAI,CAAC,MAAM,EAAE,CAAC,kBAAkB,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAExD,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,IAAI,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACrF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,2BAA2B;YACjC,MAAM,EAAE,mEAAmE;YAC3E,UAAU,EACR,sJAAsJ;SACzJ,CAAA;IACH,CAAC;IAED;;;;;;;;;;MAUE;IACF,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACzD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAC/C,GAAG,CAAC,UAAU,CAAC,IAAI,EACnB,GAAG,CAAC,UAAU,EACd,SAAS,CACV,CAAA;IAED,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,mBAAmB;YACzB,MAAM,EACJ,wFAAwF;YAC1F,UAAU,EACR,mFAAmF;SACtF,CAAA;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AACvB,CAAC,CAAA;AAED,SAAS,uBAAuB,CAAC,OAAe;IAC9C,MAAM,KAAK,GAAI,OAAmC,CAAC,yBAAyB,CAAC,CAAA;IAC7E,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAE,KAA0B,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9F,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumone/throughline-publishing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Policy-gated publishing server for Throughline. Wraps Payload's update operation with composition, accessibility, required-field, embargo, and approval gating.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"zod": "^3.23.0",
|
|
62
|
-
"@forumone/throughline-core": "0.
|
|
62
|
+
"@forumone/throughline-core": "0.3.0",
|
|
63
63
|
"@forumone/throughline-plugin-contract": "0.2.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|