@forumone/throughline-publishing 0.3.2 → 0.3.4
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 +38 -0
- package/README.md +16 -5
- package/dist/admin/PublishButton.d.ts +0 -2
- package/dist/admin/PublishButton.d.ts.map +1 -1
- package/dist/admin/PublishButton.js +10 -12
- package/dist/admin/PublishButton.js.map +1 -1
- package/dist/hooks/block-status-writes.d.ts +25 -12
- package/dist/hooks/block-status-writes.d.ts.map +1 -1
- package/dist/hooks/block-status-writes.js +90 -31
- package/dist/hooks/block-status-writes.js.map +1 -1
- package/dist/pipeline/steps/execute.d.ts +17 -3
- package/dist/pipeline/steps/execute.d.ts.map +1 -1
- package/dist/pipeline/steps/execute.js +18 -4
- package/dist/pipeline/steps/execute.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +4 -8
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @forumone/throughline-publishing
|
|
2
2
|
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- add60df: Stop overwriting `publishedAt` on every publish.
|
|
8
|
+
|
|
9
|
+
`executeStep` wrote the current time into the collection's `publishedAtField` on
|
|
10
|
+
every run, so re-publishing an edit re-dated the document. The field means "when
|
|
11
|
+
this went live" — a listing sorts on it and a template prints it — so the visible
|
|
12
|
+
effect was that editing a two-year-old article sent it to the top of its index
|
|
13
|
+
under today's date, and an editor who typed the original date into the sidebar
|
|
14
|
+
watched publishing replace it.
|
|
15
|
+
|
|
16
|
+
The guard was already there and unused: the step computes `wasFirstPublish` for
|
|
17
|
+
the `content/page.published` payload, where it is reported as `isFirstPublish`.
|
|
18
|
+
It now also decides the write, so `publishedAt` is stamped on a first publish and
|
|
19
|
+
left alone afterwards. A document that should genuinely be re-dated is re-dated
|
|
20
|
+
by editing the field, which now survives.
|
|
21
|
+
|
|
22
|
+
`previousPublishedAt` and `isFirstPublish` in the event are unchanged, so a
|
|
23
|
+
subscriber that wants the publish time of _this_ publish still has it.
|
|
24
|
+
|
|
25
|
+
Patch rather than minor: the fix restores what the field was documented to mean.
|
|
26
|
+
Anything relying on it as a last-published timestamp was relying on the bug — and
|
|
27
|
+
`updatedAt` is the field for that.
|
|
28
|
+
|
|
29
|
+
## 0.3.3
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- de1d480: Fix two defects reported against 0.3.2.
|
|
34
|
+
|
|
35
|
+
**A direct unpublish was allowed once a draft version existed.** `beforeChange` compared `data._status` against `originalDoc._status` and treated a match as a harmless no-op — but `originalDoc` is the _latest version_, not the live document. After any draft save of a published page, `originalDoc._status` is `'draft'` while the page is still live, so an unpublish matched the no-op branch and went through with no pipeline, no audit event and no revalidation. The hook now asks whether the write changes what the public sees, reading the live row when `originalDoc` cannot answer. Promoting a pending draft with a direct `_status: 'published'` write is blocked too — the live status never changes there, so no status comparison could have caught it. Ordinary edits of published documents are unaffected.
|
|
36
|
+
|
|
37
|
+
**The publish button left the form showing pre-edit content.** After publishing, the button reset the form from `useDocumentInfo().data` — the document as it was when the edit view mounted — which replaced the editor's just-saved values with the pre-edit ones and read as though the publish had discarded them. The draft save already merges the server's response into form state, so the reset is removed rather than replaced.
|
|
38
|
+
|
|
39
|
+
Also adds an integration suite that exercises the hooks inside a real Payload instance against a real database, covering the full matrix of draft saves, edits, publishes and unpublishes. Every defect in this hook so far came from unit tests encoding assumptions Payload does not hold.
|
|
40
|
+
|
|
3
41
|
## 0.3.2
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -103,17 +103,28 @@ The plugin installs two hooks on every configured collection, and they work as a
|
|
|
103
103
|
- `beforeOperation` records whether the update in flight is a draft write.
|
|
104
104
|
- `beforeChange` rejects writes that change the **live** document's `_status`.
|
|
105
105
|
|
|
106
|
+
The question they answer is not "did `_status` change" but **would this write change what the public sees**:
|
|
107
|
+
|
|
106
108
|
| Write | Result |
|
|
107
109
|
|---|---|
|
|
108
|
-
| `draft: true`
|
|
109
|
-
|
|
|
110
|
-
|
|
|
110
|
+
| Any `draft: true` save | **allowed** — writes a version, the live document is untouched |
|
|
111
|
+
| A field edit of a published document with nothing pending | **allowed** — status unchanged, no draft to promote |
|
|
112
|
+
| A `_status: 'draft'` write to a document that was never published, or is already down | **allowed** — nothing is live |
|
|
113
|
+
| `_status: 'draft'` on a live document | **blocked** — unpublish through the pipeline |
|
|
114
|
+
| `_status: 'published'` on a draft document | **blocked** — publish through the pipeline |
|
|
115
|
+
| A non-draft write while a draft is pending | **blocked** — this is the publish, or an accidental unpublish |
|
|
111
116
|
| `draft: true` with `_status: 'published'` | **blocked** — Payload treats this as a publish, not a draft save |
|
|
112
117
|
| Any write carrying `bypassPublishingServer` | **allowed** |
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
Three things about Payload's update pipeline make this less obvious than it looks, and each has caused a defect here:
|
|
120
|
+
|
|
121
|
+
1. **`data` is the stored document merged with the caller's changes**, so `_status` is present on nearly every update. An ordinary field edit of a published page arrives carrying `_status: 'published'`. The presence of `_status` means nothing by itself.
|
|
122
|
+
2. **Payload injects `data._status = 'draft'` into every `draft: true` update** before `beforeChange` runs, whether or not the caller supplied it — so a draft save of a published document is shaped exactly like an unpublish. The real `draft` flag is read in `beforeOperation`, which sees it identically on the Local API, REST and GraphQL. (`req.query.draft` is only populated on the REST path, so keying on the request would cover the admin and silently miss scripts.)
|
|
123
|
+
3. **`originalDoc` is the latest version, not the live document.** Once a draft is pending on a published page, `originalDoc._status` is `'draft'` while the page is still live. Comparing the two statuses therefore reads a genuine unpublish as a harmless no-op.
|
|
124
|
+
|
|
125
|
+
A consequence of (1) and (3) worth knowing: once a draft is pending, a plain `payload.update(...)` with no `draft: true` would carry the pending draft's `'draft'` status and take the page down. That is blocked. Pass `draft: true` to edit the draft, or publish through the pipeline.
|
|
115
126
|
|
|
116
|
-
If you install `createBlockStatusWritesHook` yourself without the recorder, it fails closed: every status change is blocked.
|
|
127
|
+
If you install `createBlockStatusWritesHook` yourself without the recorder, it fails closed: every status change is blocked. The behaviour above is verified against a real Payload instance in `block-status-writes.integration.test.ts`.
|
|
117
128
|
|
|
118
129
|
## Bypassing the pipeline
|
|
119
130
|
|
|
@@ -2,8 +2,6 @@ import React from 'react';
|
|
|
2
2
|
export interface ThroughlinePublishButtonProps {
|
|
3
3
|
/** Route prefix the plugin is mounted under. Injected via `clientProps`. */
|
|
4
4
|
routePrefix?: string;
|
|
5
|
-
/** Publish-timestamp field for this collection. Injected via `clientProps`. */
|
|
6
|
-
publishedAtField?: string;
|
|
7
5
|
}
|
|
8
6
|
/**
|
|
9
7
|
* Replaces Payload's Publish button on collections the publishing plugin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PublishButton.d.ts","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAYpD,MAAM,WAAW,6BAA6B;IAC5C,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"PublishButton.d.ts","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAYpD,MAAM,WAAW,6BAA6B;IAC5C,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,6BAAkC,GAAG,KAAK,CAAC,SAAS,CAyIxF"}
|
|
@@ -20,9 +20,9 @@ import { callPublishingEndpoint, describeBlock } from './publishing-client.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export function PublishButton(props = {}) {
|
|
22
22
|
const routePrefix = props.routePrefix ?? '/publishing';
|
|
23
|
-
const { collectionSlug,
|
|
23
|
+
const { collectionSlug, hasPublishedDoc, hasPublishPermission, id, setHasPublishedDoc, setMostRecentVersionIsAutosaved, setUnpublishedVersionCount, unpublishedVersionCount, uploadStatus, } = useDocumentInfo();
|
|
24
24
|
const { config } = useConfig();
|
|
25
|
-
const {
|
|
25
|
+
const { submit } = useForm();
|
|
26
26
|
const modified = useFormModified();
|
|
27
27
|
const { t } = useTranslation();
|
|
28
28
|
const [publishing, setPublishing] = useState(false);
|
|
@@ -74,13 +74,14 @@ export function PublishButton(props = {}) {
|
|
|
74
74
|
});
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Deliberately not resetting the form here. The draft save above
|
|
78
|
+
// already merged the server's response into form state, so the fields
|
|
79
|
+
// on screen are what was just written. Resetting from
|
|
80
|
+
// `useDocumentInfo().data` — the document as it was when the view
|
|
81
|
+
// mounted — would replace the editor's saved edits with the pre-edit
|
|
82
|
+
// values and read as though the publish had silently discarded them.
|
|
83
|
+
//
|
|
84
|
+
// The status indicators below are what actually needs updating.
|
|
84
85
|
setHasPublishedDoc(true);
|
|
85
86
|
setUnpublishedVersionCount(0);
|
|
86
87
|
setMostRecentVersionIsAutosaved(false);
|
|
@@ -104,12 +105,9 @@ export function PublishButton(props = {}) {
|
|
|
104
105
|
}, [
|
|
105
106
|
api,
|
|
106
107
|
collectionSlug,
|
|
107
|
-
data,
|
|
108
108
|
id,
|
|
109
109
|
modified,
|
|
110
|
-
props.publishedAtField,
|
|
111
110
|
publishing,
|
|
112
|
-
reset,
|
|
113
111
|
routePrefix,
|
|
114
112
|
serverURL,
|
|
115
113
|
setHasPublishedDoc,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PublishButton.js","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACpD,OAAO,EACL,UAAU,EACV,KAAK,EACL,SAAS,EACT,eAAe,EACf,OAAO,EACP,eAAe,EACf,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"PublishButton.js","sourceRoot":"","sources":["../../src/admin/PublishButton.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
2
|
/**
|
|
3
3
|
* The trust boundary. This `beforeChange` hook rejects every update that
|
|
4
|
-
* changes
|
|
5
|
-
*
|
|
4
|
+
* changes whether a document is live, unless the request explicitly carries
|
|
5
|
+
* the `bypassPublishingServer` context flag (set by the pipeline's
|
|
6
6
|
* `executeStep`).
|
|
7
7
|
*
|
|
8
8
|
* Without this hook, the Payload MCP could let Claude flip a document to
|
|
9
9
|
* `published` directly, sidestepping every check the publishing server
|
|
10
10
|
* exists to enforce. With it, the only sanctioned path is the pipeline.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* versions table and leaves the published document alone, so it changes no
|
|
14
|
-
* live status — but Payload injects `data._status = 'draft'` into every such
|
|
15
|
-
* update before this hook runs, which made saving a draft of a published
|
|
16
|
-
* document indistinguishable from unpublishing it. `createRecordDraftWritesHook`
|
|
17
|
-
* captures the operation's real `draft` flag so the two can be told apart;
|
|
18
|
-
* both hooks must be installed together.
|
|
12
|
+
* Three details of Payload's update pipeline shape this:
|
|
19
13
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* 1. `data` is the existing document merged with the incoming changes, so
|
|
15
|
+
* `_status` is present on essentially every update — an ordinary field
|
|
16
|
+
* edit on a published document arrives carrying `_status: 'published'`.
|
|
17
|
+
* The presence of `_status` therefore means nothing on its own.
|
|
18
|
+
*
|
|
19
|
+
* 2. Payload injects `data._status = 'draft'` into every `draft: true`
|
|
20
|
+
* update before this hook runs, so a draft save of a published document
|
|
21
|
+
* arrives looking like an unpublish. `createRecordDraftWritesHook`
|
|
22
|
+
* captures the operation's real `draft` flag to tell them apart; both
|
|
23
|
+
* hooks must be installed together.
|
|
24
|
+
*
|
|
25
|
+
* 3. `originalDoc` is the *latest version*, not the live document. Once any
|
|
26
|
+
* draft version exists on a published document, `originalDoc._status` is
|
|
27
|
+
* `'draft'` while the document is still live — so comparing against it
|
|
28
|
+
* reads a genuine unpublish as a harmless no-op.
|
|
29
|
+
*
|
|
30
|
+
* So the question asked here is narrow and concrete: **would this write
|
|
31
|
+
* change what the public sees?** A draft write never does. A non-draft
|
|
32
|
+
* write does when it takes a live document down, or when it puts content
|
|
33
|
+
* live — including promoting a pending draft, which leaves `_status` on
|
|
34
|
+
* `published` throughout and so cannot be caught by comparing statuses
|
|
35
|
+
* alone.
|
|
23
36
|
*/
|
|
24
37
|
export declare function createBlockStatusWritesHook(): CollectionBeforeChangeHook;
|
|
25
38
|
//# sourceMappingURL=block-status-writes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-status-writes.d.ts","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,
|
|
1
|
+
{"version":3,"file":"block-status-writes.d.ts","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAkB,MAAM,SAAS,CAAA;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,2BAA2B,IAAI,0BAA0B,CAsCxE"}
|
|
@@ -2,57 +2,116 @@ import { APIError } from 'payload';
|
|
|
2
2
|
import { isDraftWrite } from './draft-writes.js';
|
|
3
3
|
/**
|
|
4
4
|
* The trust boundary. This `beforeChange` hook rejects every update that
|
|
5
|
-
* changes
|
|
6
|
-
*
|
|
5
|
+
* changes whether a document is live, unless the request explicitly carries
|
|
6
|
+
* the `bypassPublishingServer` context flag (set by the pipeline's
|
|
7
7
|
* `executeStep`).
|
|
8
8
|
*
|
|
9
9
|
* Without this hook, the Payload MCP could let Claude flip a document to
|
|
10
10
|
* `published` directly, sidestepping every check the publishing server
|
|
11
11
|
* exists to enforce. With it, the only sanctioned path is the pipeline.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* versions table and leaves the published document alone, so it changes no
|
|
15
|
-
* live status — but Payload injects `data._status = 'draft'` into every such
|
|
16
|
-
* update before this hook runs, which made saving a draft of a published
|
|
17
|
-
* document indistinguishable from unpublishing it. `createRecordDraftWritesHook`
|
|
18
|
-
* captures the operation's real `draft` flag so the two can be told apart;
|
|
19
|
-
* both hooks must be installed together.
|
|
13
|
+
* Three details of Payload's update pipeline shape this:
|
|
20
14
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
15
|
+
* 1. `data` is the existing document merged with the incoming changes, so
|
|
16
|
+
* `_status` is present on essentially every update — an ordinary field
|
|
17
|
+
* edit on a published document arrives carrying `_status: 'published'`.
|
|
18
|
+
* The presence of `_status` therefore means nothing on its own.
|
|
19
|
+
*
|
|
20
|
+
* 2. Payload injects `data._status = 'draft'` into every `draft: true`
|
|
21
|
+
* update before this hook runs, so a draft save of a published document
|
|
22
|
+
* arrives looking like an unpublish. `createRecordDraftWritesHook`
|
|
23
|
+
* captures the operation's real `draft` flag to tell them apart; both
|
|
24
|
+
* hooks must be installed together.
|
|
25
|
+
*
|
|
26
|
+
* 3. `originalDoc` is the *latest version*, not the live document. Once any
|
|
27
|
+
* draft version exists on a published document, `originalDoc._status` is
|
|
28
|
+
* `'draft'` while the document is still live — so comparing against it
|
|
29
|
+
* reads a genuine unpublish as a harmless no-op.
|
|
30
|
+
*
|
|
31
|
+
* So the question asked here is narrow and concrete: **would this write
|
|
32
|
+
* change what the public sees?** A draft write never does. A non-draft
|
|
33
|
+
* write does when it takes a live document down, or when it puts content
|
|
34
|
+
* live — including promoting a pending draft, which leaves `_status` on
|
|
35
|
+
* `published` throughout and so cannot be caught by comparing statuses
|
|
36
|
+
* alone.
|
|
24
37
|
*/
|
|
25
38
|
export function createBlockStatusWritesHook() {
|
|
26
|
-
return ({ collection, data, originalDoc, operation, context, req }) => {
|
|
39
|
+
return async ({ collection, data, originalDoc, operation, context, req }) => {
|
|
27
40
|
if (operation !== 'update')
|
|
28
41
|
return data;
|
|
29
|
-
if (!
|
|
30
|
-
return data;
|
|
31
|
-
// No-op writes (status set to its current value) are harmless.
|
|
32
|
-
if (originalDoc &&
|
|
33
|
-
typeof originalDoc === 'object' &&
|
|
34
|
-
data['_status'] ===
|
|
35
|
-
originalDoc['_status']) {
|
|
42
|
+
if (!carriesStatus(data))
|
|
36
43
|
return data;
|
|
37
|
-
}
|
|
38
44
|
if (isBypassed(context) || isBypassed(req?.context))
|
|
39
45
|
return data;
|
|
40
|
-
|
|
46
|
+
const nextStatus = data['_status'];
|
|
47
|
+
const previousStatus = originalDoc?.['_status'];
|
|
48
|
+
const id = originalDoc?.id;
|
|
49
|
+
// A draft save writes a version and leaves the live document alone.
|
|
41
50
|
//
|
|
42
|
-
// Mirror Payload's own `isSavingDraft`, which
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
// would be a way to publish around the pipeline.
|
|
47
|
-
if (data['_status'] !== 'published' &&
|
|
48
|
-
isDraftWrite(req, collection?.slug, originalDoc?.id)) {
|
|
51
|
+
// Mirror Payload's own `isSavingDraft`, which excludes `published`: a
|
|
52
|
+
// request asking for `draft: true` while setting `_status: 'published'`
|
|
53
|
+
// is a real publish, so `draft` cannot become a way around the pipeline.
|
|
54
|
+
if (nextStatus !== 'published' && isDraftWrite(req, collection?.slug, id)) {
|
|
49
55
|
return data;
|
|
50
56
|
}
|
|
57
|
+
if (nextStatus === 'published') {
|
|
58
|
+
// This puts content live. Allowed only when the document is already
|
|
59
|
+
// live *and* nothing is pending: a latest version of `published`
|
|
60
|
+
// means there is no newer draft for this write to promote. When a
|
|
61
|
+
// draft is pending, this is the publish and it belongs in the
|
|
62
|
+
// pipeline — even though the live status does not change.
|
|
63
|
+
if (previousStatus === 'published')
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// This takes the live document down. Harmless only if nothing is
|
|
68
|
+
// live: a document that was never published, or is already down.
|
|
69
|
+
const liveStatus = await resolveLiveStatus(req, collection?.slug, id, previousStatus);
|
|
70
|
+
if (liveStatus !== undefined && liveStatus !== 'published')
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
51
73
|
throw new APIError('Direct writes to `_status` are not allowed. Use the publishing server (publish / unpublish / rollback) so the policy pipeline runs.', 400);
|
|
52
74
|
};
|
|
53
75
|
}
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The live document's current `_status`, or `undefined` when it cannot be
|
|
78
|
+
* established — in which case the caller blocks the write.
|
|
79
|
+
*
|
|
80
|
+
* `originalDoc` is the latest version. A latest version of `published`
|
|
81
|
+
* means the live document is published, so that answer is taken directly.
|
|
82
|
+
* A latest version of `draft` is ambiguous — the document may still be live
|
|
83
|
+
* with a newer draft sitting on top of it — and only then is the published
|
|
84
|
+
* row read. Draft writes return before reaching this, so the admin's
|
|
85
|
+
* publish flow never pays for the query.
|
|
86
|
+
*/
|
|
87
|
+
async function resolveLiveStatus(req, collectionSlug, id, previousStatus) {
|
|
88
|
+
if (previousStatus === 'published')
|
|
89
|
+
return 'published';
|
|
90
|
+
if (!req?.payload || !collectionSlug || id === undefined || id === null)
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
const live = await req.payload.findByID({
|
|
94
|
+
collection: collectionSlug,
|
|
95
|
+
id: id,
|
|
96
|
+
depth: 0,
|
|
97
|
+
overrideAccess: true,
|
|
98
|
+
// Share the request so this reads inside the open transaction.
|
|
99
|
+
req,
|
|
100
|
+
});
|
|
101
|
+
const status = live?.['_status'];
|
|
102
|
+
return typeof status === 'string' ? status : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether `_status` is present at all. Payload merges the stored document
|
|
110
|
+
* into `data`, so this is true for nearly every update — it only filters
|
|
111
|
+
* out collections without drafts.
|
|
112
|
+
*/
|
|
113
|
+
function carriesStatus(data) {
|
|
114
|
+
return typeof data === 'object' && data !== null && '_status' in data;
|
|
56
115
|
}
|
|
57
116
|
function isBypassed(context) {
|
|
58
117
|
if (!context || typeof context !== 'object')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-status-writes.js","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD
|
|
1
|
+
{"version":3,"file":"block-status-writes.js","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { PipelineStep } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Performs the actual publish: writes `_status: 'published'
|
|
4
|
-
*
|
|
5
|
-
* subscribers can react to (revalidation, integrations, etc.).
|
|
3
|
+
* Performs the actual publish: writes `_status: 'published'`, stamps the
|
|
4
|
+
* publishedAt field on a first publish, then fires `content/page.published`
|
|
5
|
+
* with metadata subscribers can react to (revalidation, integrations, etc.).
|
|
6
|
+
*
|
|
7
|
+
* `publishedAt` is set only when the document does not already have one. It
|
|
8
|
+
* means "when this went live", which is what a listing sorts on and what a
|
|
9
|
+
* template prints on the page — so re-publishing an edit must not move it. It
|
|
10
|
+
* did: every publish overwrote the field with the current time, which sent an
|
|
11
|
+
* edited article to the top of its index and printed today's date on a piece
|
|
12
|
+
* written months ago. An editor who typed the original date into the sidebar
|
|
13
|
+
* watched it be replaced by the act of publishing.
|
|
14
|
+
*
|
|
15
|
+
* The guard is `wasFirstPublish`, which this step already computed for the
|
|
16
|
+
* event payload and did not apply to the write.
|
|
17
|
+
*
|
|
18
|
+
* A document that should genuinely be re-dated is re-dated by editing the
|
|
19
|
+
* field, which now survives.
|
|
6
20
|
*
|
|
7
21
|
* Sets `context.bypassPublishingServer = true` on the Payload update so the
|
|
8
22
|
* status-write blocking hook recognizes this as a sanctioned write. Any
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/pipeline/steps/execute.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/pipeline/steps/execute.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,WAAW,EAAE,YAoCzB,CAAA"}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { sendEventSafely } from '../../events.js';
|
|
2
2
|
/**
|
|
3
|
-
* Performs the actual publish: writes `_status: 'published'
|
|
4
|
-
*
|
|
5
|
-
* subscribers can react to (revalidation, integrations, etc.).
|
|
3
|
+
* Performs the actual publish: writes `_status: 'published'`, stamps the
|
|
4
|
+
* publishedAt field on a first publish, then fires `content/page.published`
|
|
5
|
+
* with metadata subscribers can react to (revalidation, integrations, etc.).
|
|
6
|
+
*
|
|
7
|
+
* `publishedAt` is set only when the document does not already have one. It
|
|
8
|
+
* means "when this went live", which is what a listing sorts on and what a
|
|
9
|
+
* template prints on the page — so re-publishing an edit must not move it. It
|
|
10
|
+
* did: every publish overwrote the field with the current time, which sent an
|
|
11
|
+
* edited article to the top of its index and printed today's date on a piece
|
|
12
|
+
* written months ago. An editor who typed the original date into the sidebar
|
|
13
|
+
* watched it be replaced by the act of publishing.
|
|
14
|
+
*
|
|
15
|
+
* The guard is `wasFirstPublish`, which this step already computed for the
|
|
16
|
+
* event payload and did not apply to the write.
|
|
17
|
+
*
|
|
18
|
+
* A document that should genuinely be re-dated is re-dated by editing the
|
|
19
|
+
* field, which now survives.
|
|
6
20
|
*
|
|
7
21
|
* Sets `context.bypassPublishingServer = true` on the Payload update so the
|
|
8
22
|
* status-write blocking hook recognizes this as a sanctioned write. Any
|
|
@@ -28,7 +42,7 @@ export const executeStep = async (ctx) => {
|
|
|
28
42
|
id: ctx.documentId,
|
|
29
43
|
data: {
|
|
30
44
|
_status: 'published',
|
|
31
|
-
[ctx.collection.publishedAtField]: now,
|
|
45
|
+
...(wasFirstPublish ? { [ctx.collection.publishedAtField]: now } : {}),
|
|
32
46
|
},
|
|
33
47
|
...(ctx.actor.enforceAccessAs
|
|
34
48
|
? { user: ctx.actor.enforceAccessAs, overrideAccess: false }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../../src/pipeline/steps/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../../src/pipeline/steps/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,MAAM,WAAW,GAAiB,KAAK,EAAE,GAAG,EAAE,EAAE;IACrD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,MAAM,mBAAmB,GACvB,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,gBAAgB,CAAC,KAAK,QAAQ;QAC/D,CAAC,CAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,gBAAgB,CAAY;QAC3D,CAAC,CAAC,IAAI,CAAA;IACV,MAAM,eAAe,GAAG,mBAAmB,KAAK,IAAI,CAAA;IAEpD,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;QACvB,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,IAAI;QAC/B,EAAE,EAAE,GAAG,CAAC,UAAU;QAClB,IAAI,EAAE;YACJ,OAAO,EAAE,WAAW;YACpB,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE;QACD,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe;YAC3B,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,eAAe,EAAE,cAAc,EAAE,KAAK,EAAE;YAC5D,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE;KAC1C,CAAC,CAAA;IAEF,uEAAuE;IACvE,yCAAyC;IACzC,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE;QACjD,IAAI,EAAE,wBAAwB;QAC9B,IAAI,EAAE;YACJ,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,IAAI;YAC/B,EAAE,EAAE,GAAG,CAAC,UAAU;YAClB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC;YACtE,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,QAAQ;YAC3C,mBAAmB;YACnB,cAAc,EAAE,eAAe;SAChC;KACF,CAAC,CAAA;IAEF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACpE,CAAC,CAAA"}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AAQvE,OAAO,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AAQvE,OAAO,EAAE,KAAK,uBAAuB,EAAmB,MAAM,cAAc,CAAA;AAqB5E,eAAO,MAAM,gBAAgB,EAAE,UAAU,CAAC,uBAAuB,CA8G9D,CAAA"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPluginRegistry } from '@forumone/throughline-plugin-contract';
|
|
2
2
|
import { createMcpHandler, createNamedLogger, defaultLogger, getAuditWriter, } from '@forumone/throughline-core';
|
|
3
|
-
import {
|
|
3
|
+
import { validateOptions } from './options.js';
|
|
4
4
|
import { createBlockStatusWritesHook } from './hooks/block-status-writes.js';
|
|
5
5
|
import { createRecordDraftWritesHook } from './hooks/draft-writes.js';
|
|
6
6
|
import { createAdminEndpoints } from './endpoints/admin.js';
|
|
@@ -41,7 +41,7 @@ export const publishingPlugin = (rawOptions) => (incomingConfig) => {
|
|
|
41
41
|
],
|
|
42
42
|
},
|
|
43
43
|
...(adminComponents
|
|
44
|
-
? { admin: withAdminControls(collection, routePrefix
|
|
44
|
+
? { admin: withAdminControls(collection, routePrefix) }
|
|
45
45
|
: {}),
|
|
46
46
|
};
|
|
47
47
|
});
|
|
@@ -115,8 +115,7 @@ export const publishingPlugin = (rawOptions) => (incomingConfig) => {
|
|
|
115
115
|
* Hosts must run `payload generate:importmap` after adding the plugin (the
|
|
116
116
|
* dev server does it automatically) so Payload can resolve these paths.
|
|
117
117
|
*/
|
|
118
|
-
function withAdminControls(collection, routePrefix
|
|
119
|
-
const resolved = resolveCollection(options, collection.slug);
|
|
118
|
+
function withAdminControls(collection, routePrefix) {
|
|
120
119
|
const edit = collection.admin?.components?.edit ?? {};
|
|
121
120
|
return {
|
|
122
121
|
...(collection.admin ?? {}),
|
|
@@ -129,10 +128,7 @@ function withAdminControls(collection, routePrefix, options) {
|
|
|
129
128
|
PublishButton: {
|
|
130
129
|
path: CLIENT_ENTRY,
|
|
131
130
|
exportName: 'PublishButton',
|
|
132
|
-
clientProps: {
|
|
133
|
-
routePrefix,
|
|
134
|
-
publishedAtField: resolved.publishedAtField,
|
|
135
|
-
},
|
|
131
|
+
clientProps: { routePrefix },
|
|
136
132
|
},
|
|
137
133
|
}
|
|
138
134
|
: {}),
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AACzE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,GACf,MAAM,4BAA4B,CAAA;AACnC,OAAO,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumone/throughline-publishing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@forumone/throughline-plugin-contract": "0.2.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
+
"@payloadcms/db-sqlite": "^3.83.0",
|
|
66
67
|
"@payloadcms/ui": "^3.83.0",
|
|
67
68
|
"@types/node": "^20.17.0",
|
|
68
69
|
"@types/react": "^19.2.0",
|