@forumone/throughline-publishing 0.9.3 → 0.9.5
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
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @forumone/throughline-publishing
|
|
2
2
|
|
|
3
|
+
## 0.9.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1f96c31: "Revert to published" works on a governed collection.
|
|
8
|
+
|
|
9
|
+
Payload's revert reads the live document and writes it back with no draft
|
|
10
|
+
flag, which the trust boundary read as promoting the pending draft and
|
|
11
|
+
refused with "Direct writes to `_status` are not allowed". A non-draft
|
|
12
|
+
`published` write over a pending draft is now allowed when its content is
|
|
13
|
+
exactly what is already live — it discards the draft and changes nothing a
|
|
14
|
+
reader sees. Any difference in content still has to go through the pipeline.
|
|
15
|
+
|
|
16
|
+
## 0.9.4
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [d02772f]
|
|
21
|
+
- @forumone/throughline-core@0.9.1
|
|
22
|
+
|
|
3
23
|
## 0.9.3
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -33,6 +33,12 @@ import type { CollectionBeforeChangeHook } from 'payload';
|
|
|
33
33
|
* live — including promoting a pending draft, which leaves `_status` on
|
|
34
34
|
* `published` throughout and so cannot be caught by comparing statuses
|
|
35
35
|
* alone.
|
|
36
|
+
*
|
|
37
|
+
* One non-draft `published` write over a pending draft is harmless: one
|
|
38
|
+
* whose content is exactly what is already live. That is Payload's own
|
|
39
|
+
* "Revert to published", which reads the live document and writes it back
|
|
40
|
+
* unchanged to discard the draft — the public sees nothing new, so there is
|
|
41
|
+
* nothing for the pipeline to check.
|
|
36
42
|
*/
|
|
37
43
|
export declare function createBlockStatusWritesHook(): CollectionBeforeChangeHook;
|
|
38
44
|
//# 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,EAAkB,MAAM,SAAS,CAAA;AAGzE
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,2BAA2B,IAAI,0BAA0B,CAuCxE"}
|
|
@@ -34,6 +34,12 @@ import { isDraftWrite } from './draft-writes.js';
|
|
|
34
34
|
* live — including promoting a pending draft, which leaves `_status` on
|
|
35
35
|
* `published` throughout and so cannot be caught by comparing statuses
|
|
36
36
|
* alone.
|
|
37
|
+
*
|
|
38
|
+
* One non-draft `published` write over a pending draft is harmless: one
|
|
39
|
+
* whose content is exactly what is already live. That is Payload's own
|
|
40
|
+
* "Revert to published", which reads the live document and writes it back
|
|
41
|
+
* unchanged to discard the draft — the public sees nothing new, so there is
|
|
42
|
+
* nothing for the pipeline to check.
|
|
37
43
|
*/
|
|
38
44
|
export function createBlockStatusWritesHook() {
|
|
39
45
|
return async ({ collection, data, originalDoc, operation, context, req }) => {
|
|
@@ -62,6 +68,8 @@ export function createBlockStatusWritesHook() {
|
|
|
62
68
|
// pipeline — even though the live status does not change.
|
|
63
69
|
if (previousStatus === 'published')
|
|
64
70
|
return data;
|
|
71
|
+
if (await restatesLiveContent(req, collection?.slug, id, data))
|
|
72
|
+
return data;
|
|
65
73
|
}
|
|
66
74
|
else {
|
|
67
75
|
// This takes the live document down. Harmless only if nothing is
|
|
@@ -105,6 +113,77 @@ async function resolveLiveStatus(req, collectionSlug, id, previousStatus) {
|
|
|
105
113
|
return undefined;
|
|
106
114
|
}
|
|
107
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Fields that differ between two writes of the same content: the row's
|
|
118
|
+
* identity, its timestamps, and the status this hook is already judging.
|
|
119
|
+
*/
|
|
120
|
+
const NOT_CONTENT = new Set(['id', 'createdAt', 'updatedAt', '_status']);
|
|
121
|
+
/**
|
|
122
|
+
* Whether `data` is the live document's content, unchanged — so writing it
|
|
123
|
+
* would leave the public site exactly as it is.
|
|
124
|
+
*
|
|
125
|
+
* Compared as JSON so a `Date` and its ISO string agree, ignoring key order,
|
|
126
|
+
* and with a missing key equal to `null`, which is how Payload stores an
|
|
127
|
+
* empty field. Anything
|
|
128
|
+
* it cannot read counts as a change, and the caller blocks.
|
|
129
|
+
*/
|
|
130
|
+
async function restatesLiveContent(req, collectionSlug, id, data) {
|
|
131
|
+
if (!req?.payload || !collectionSlug || id === undefined || id === null)
|
|
132
|
+
return false;
|
|
133
|
+
let live;
|
|
134
|
+
try {
|
|
135
|
+
live = await req.payload.findByID({
|
|
136
|
+
collection: collectionSlug,
|
|
137
|
+
id: id,
|
|
138
|
+
depth: 0,
|
|
139
|
+
draft: false,
|
|
140
|
+
overrideAccess: true,
|
|
141
|
+
req,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (!live || typeof live !== 'object')
|
|
148
|
+
return false;
|
|
149
|
+
if (live['_status'] !== 'published')
|
|
150
|
+
return false;
|
|
151
|
+
const next = asJSON(data);
|
|
152
|
+
const current = asJSON(live);
|
|
153
|
+
const keys = new Set([...Object.keys(next), ...Object.keys(current)]);
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
if (NOT_CONTENT.has(key))
|
|
156
|
+
continue;
|
|
157
|
+
if (!sameContent(next[key], current[key]))
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function asJSON(value) {
|
|
163
|
+
return JSON.parse(JSON.stringify(value));
|
|
164
|
+
}
|
|
165
|
+
/** Deep equality over JSON values, ignoring object key order. */
|
|
166
|
+
function sameContent(a, b) {
|
|
167
|
+
a = a ?? null;
|
|
168
|
+
b = b ?? null;
|
|
169
|
+
if (a === b)
|
|
170
|
+
return true;
|
|
171
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
172
|
+
return false;
|
|
173
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
174
|
+
return false;
|
|
175
|
+
if (Array.isArray(a)) {
|
|
176
|
+
const other = b;
|
|
177
|
+
return a.length === other.length && a.every((item, i) => sameContent(item, other[i]));
|
|
178
|
+
}
|
|
179
|
+
const left = a;
|
|
180
|
+
const right = b;
|
|
181
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
182
|
+
for (const key of keys)
|
|
183
|
+
if (!sameContent(left[key], right[key]))
|
|
184
|
+
return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
108
187
|
/**
|
|
109
188
|
* Whether `_status` is present at all. Payload merges the stored document
|
|
110
189
|
* into `data`, so this is true for nearly every update — it only filters
|
|
@@ -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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumone/throughline-publishing",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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.9.
|
|
62
|
+
"@forumone/throughline-core": "0.9.1",
|
|
63
63
|
"@forumone/throughline-plugin-contract": "0.4.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"@payloadcms/ui": "^3.83.0",
|
|
68
68
|
"@types/node": "^24.13.2",
|
|
69
69
|
"@types/react": "^19.2.0",
|
|
70
|
-
"eslint": "^
|
|
70
|
+
"eslint": "^10.11.0",
|
|
71
71
|
"inngest": "^4.2.0",
|
|
72
72
|
"payload": "^3.83.0",
|
|
73
73
|
"react": "^19.2.0",
|
|
74
74
|
"typescript": "^5.6.0",
|
|
75
75
|
"vitest": "^4.1.10",
|
|
76
|
-
"@forumone/throughline-
|
|
77
|
-
"@forumone/throughline-
|
|
76
|
+
"@forumone/throughline-tsconfig": "0.0.0",
|
|
77
|
+
"@forumone/throughline-eslint-config": "0.0.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"build": "tsc -b",
|