@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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,2BAA2B,IAAI,0BAA0B,CAsCxE"}
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":"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"}
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",
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.0",
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": "^9.15.0",
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-eslint-config": "0.0.0",
77
- "@forumone/throughline-tsconfig": "0.0.0"
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",