@qkix/strapi-plugin-rewind 0.1.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/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/_chunks/en-BHfx2OWt.js +1 -0
- package/dist/_chunks/en-DCctmu-a.mjs +7 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +254 -0
- package/dist/admin/src/components/ChangesDialog.d.ts +21 -0
- package/dist/admin/src/components/HistoryPanel.d.ts +17 -0
- package/dist/admin/src/diffRegistry.d.ts +45 -0
- package/dist/admin/src/index.d.ts +18 -0
- package/dist/admin/src/pluginId.d.ts +6 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.mjs +930 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +58 -0
- package/dist/server/src/content-types/index.d.ts +95 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/controllers/version.d.ts +11 -0
- package/dist/server/src/destroy.d.ts +9 -0
- package/dist/server/src/index.d.ts +166 -0
- package/dist/server/src/routes/admin.d.ts +16 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/diff.d.ts +55 -0
- package/dist/server/src/services/index.d.ts +35 -0
- package/dist/server/src/services/lock.d.ts +20 -0
- package/dist/server/src/services/restore.d.ts +44 -0
- package/dist/server/src/services/retention.d.ts +18 -0
- package/dist/server/src/services/serializer.d.ts +65 -0
- package/dist/server/src/services/snapshot.d.ts +17 -0
- package/dist/server/src/utils/backfillLabels.d.ts +18 -0
- package/dist/server/src/utils/captureContext.d.ts +45 -0
- package/dist/server/src/utils/persistTable.d.ts +15 -0
- package/dist/server/src/utils/textDiff.d.ts +50 -0
- package/dist/server/src/utils/thinning.d.ts +35 -0
- package/dist/server/src/utils/versionLabel.d.ts +14 -0
- package/package.json +115 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import { createHash as x } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage as J } from "node:async_hooks";
|
|
3
|
+
const Z = ["id", "documentId", "locale"], S = [
|
|
4
|
+
"createdAt",
|
|
5
|
+
"updatedAt",
|
|
6
|
+
"publishedAt",
|
|
7
|
+
"createdBy",
|
|
8
|
+
"updatedBy",
|
|
9
|
+
"strapi_stage",
|
|
10
|
+
"strapi_assignee",
|
|
11
|
+
"locale",
|
|
12
|
+
"localizations"
|
|
13
|
+
], I = (e, t) => e.getModel(t), Q = (e) => e?.type === "relation" || e?.type === "media", $ = (e) => e?.type === "relation" && String(e.relation ?? "").toLowerCase().startsWith("morph"), N = (e, t) => Object.entries(I(e, t).attributes).filter(
|
|
14
|
+
([, n]) => !["relation", "media", "component", "dynamiczone"].includes(n.type)
|
|
15
|
+
).map(([n]) => n), q = (e, t, n = !0) => {
|
|
16
|
+
const r = I(e, t), o = n ? "select" : "fields";
|
|
17
|
+
return Object.entries(r.attributes).reduce(
|
|
18
|
+
(a, [s, i]) => {
|
|
19
|
+
switch (i.type) {
|
|
20
|
+
case "relation": {
|
|
21
|
+
if ($(i)) break;
|
|
22
|
+
a[s] = { [o]: ["documentId", "locale", "publishedAt"] };
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "media": {
|
|
26
|
+
a[s] = { [o]: ["id"] };
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "component": {
|
|
30
|
+
a[s] = {
|
|
31
|
+
populate: q(e, i.component, n),
|
|
32
|
+
[o]: N(e, i.component)
|
|
33
|
+
};
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "dynamiczone": {
|
|
37
|
+
a[s] = {
|
|
38
|
+
on: (i.components ?? []).reduce(
|
|
39
|
+
(c, l) => (c[l] = {
|
|
40
|
+
populate: q(e, l, n),
|
|
41
|
+
[o]: N(e, l)
|
|
42
|
+
}, c),
|
|
43
|
+
{}
|
|
44
|
+
)
|
|
45
|
+
};
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return a;
|
|
50
|
+
},
|
|
51
|
+
{}
|
|
52
|
+
);
|
|
53
|
+
}, ee = (e, t) => {
|
|
54
|
+
const n = {}, r = (a) => {
|
|
55
|
+
if (n[a]) return;
|
|
56
|
+
const s = I(e, a).attributes;
|
|
57
|
+
n[a] = R(s, S);
|
|
58
|
+
for (const i of Object.values(s))
|
|
59
|
+
if (i.type === "component" && r(i.component), i.type === "dynamiczone")
|
|
60
|
+
for (const c of i.components ?? []) r(c);
|
|
61
|
+
}, o = I(e, t).attributes;
|
|
62
|
+
for (const a of Object.values(o))
|
|
63
|
+
if (a.type === "component" && r(a.component), a.type === "dynamiczone")
|
|
64
|
+
for (const s of a.components ?? []) r(s);
|
|
65
|
+
return {
|
|
66
|
+
attributes: R(o, S),
|
|
67
|
+
components: n
|
|
68
|
+
};
|
|
69
|
+
}, R = (e, t) => Object.fromEntries(Object.entries(e ?? {}).filter(([n]) => !t.includes(n))), te = (e, t, n) => (Array.isArray(e) ? e : [e]).filter((o) => !!o).map(
|
|
70
|
+
(o) => n ? { id: o.id, targetUid: t } : {
|
|
71
|
+
documentId: o.documentId,
|
|
72
|
+
locale: o.locale ?? null,
|
|
73
|
+
targetUid: t
|
|
74
|
+
}
|
|
75
|
+
), ne = (e, t, n) => {
|
|
76
|
+
const r = I(e, t).attributes, o = {}, a = {}, s = R(n, [...S, ...Z]);
|
|
77
|
+
for (const [i, c] of Object.entries(s)) {
|
|
78
|
+
const l = r[i];
|
|
79
|
+
if (l && Q(l)) {
|
|
80
|
+
if ($(l)) continue;
|
|
81
|
+
const d = l.type === "media" ? "plugin::upload.file" : l.target;
|
|
82
|
+
a[i] = te(c, d, l.type === "media");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
o[i] = l ? Y(e, l, c) : c;
|
|
86
|
+
}
|
|
87
|
+
return { data: o, relations: a, schemaSnapshot: ee(e, t) };
|
|
88
|
+
}, Y = (e, t, n) => {
|
|
89
|
+
if (n == null) return n;
|
|
90
|
+
if (t.type === "component") {
|
|
91
|
+
const r = (o) => _(e, t.component, o);
|
|
92
|
+
return Array.isArray(n) ? n.map((o) => r(o)) : r(n);
|
|
93
|
+
}
|
|
94
|
+
return t.type === "dynamiczone" && Array.isArray(n) ? n.map((r) => {
|
|
95
|
+
const o = r;
|
|
96
|
+
return _(e, o.__component, o);
|
|
97
|
+
}) : n;
|
|
98
|
+
}, _ = (e, t, n) => {
|
|
99
|
+
if (!n || !t) return n;
|
|
100
|
+
const r = I(e, t).attributes, o = {};
|
|
101
|
+
for (const [a, s] of Object.entries(n)) {
|
|
102
|
+
if (a === "id") continue;
|
|
103
|
+
const i = r[a];
|
|
104
|
+
o[a] = i ? Y(e, i, s) : s;
|
|
105
|
+
}
|
|
106
|
+
return o;
|
|
107
|
+
}, H = (e) => {
|
|
108
|
+
const t = /* @__PURE__ */ new Map(), n = async (r) => {
|
|
109
|
+
if (t.has(r)) return t.get(r);
|
|
110
|
+
let o;
|
|
111
|
+
try {
|
|
112
|
+
o = (await e.plugin("content-manager").service("content-types").findConfiguration(e.contentTypes[r]))?.settings?.mainField;
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
return t.set(r, o), o;
|
|
116
|
+
};
|
|
117
|
+
return async (r, o) => {
|
|
118
|
+
if (!o) return null;
|
|
119
|
+
const s = [await n(r), "title", "name", "label"].filter(Boolean);
|
|
120
|
+
for (const i of s) {
|
|
121
|
+
const c = o[i];
|
|
122
|
+
if (typeof c == "string" && c.trim())
|
|
123
|
+
return c.trim().slice(0, 255);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
}, v = "plugin::rewind.version", oe = /* @__PURE__ */ new Set(["publish", "unpublish", "discardDraft", "restore"]), se = (e) => x("sha256").update(JSON.stringify(e)).digest("hex"), ie = ({ strapi: e }) => {
|
|
128
|
+
const t = H(e), n = (i) => !!e.contentTypes[i]?.options?.draftAndPublish, r = (i) => !!(e.getModel(i)?.pluginOptions?.i18n?.localized ?? e.contentTypes[i]?.pluginOptions?.i18n?.localized), o = async (i, c, l) => {
|
|
129
|
+
const d = r(i) && l.filter(Boolean).length > 0 ? { locale: { $in: l.filter(Boolean) } } : {};
|
|
130
|
+
return e.db.query(i).findMany({
|
|
131
|
+
where: {
|
|
132
|
+
documentId: c,
|
|
133
|
+
...d,
|
|
134
|
+
...n(i) ? { publishedAt: null } : {}
|
|
135
|
+
},
|
|
136
|
+
populate: q(e, i)
|
|
137
|
+
});
|
|
138
|
+
}, a = async (i, c) => {
|
|
139
|
+
try {
|
|
140
|
+
const l = e.plugin("content-manager").service("document-metadata"), d = await l.getMetadata(i, c);
|
|
141
|
+
return l.getStatus(c, d.availableStatus);
|
|
142
|
+
} catch {
|
|
143
|
+
return "draft";
|
|
144
|
+
}
|
|
145
|
+
}, s = async (i, c, l) => e.db.query(v).findOne({
|
|
146
|
+
where: { contentType: i, relatedDocumentId: c, locale: l },
|
|
147
|
+
orderBy: { createdAt: "desc" }
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
readRows: o,
|
|
151
|
+
/**
|
|
152
|
+
* Writes one version per affected locale, for each buffered intent.
|
|
153
|
+
*
|
|
154
|
+
* Called from the transaction's commit hook and never awaited by the write
|
|
155
|
+
* path — a failure here must not be able to reach the editor's save.
|
|
156
|
+
*/
|
|
157
|
+
async captureAll(i, c) {
|
|
158
|
+
for (const l of i)
|
|
159
|
+
await this.capture(l, c);
|
|
160
|
+
},
|
|
161
|
+
async capture(i, c) {
|
|
162
|
+
const { uid: l, relatedDocumentId: d, locale: p, origin: b } = i, m = i.before ?? await o(l, d, [p]);
|
|
163
|
+
for (const u of m) {
|
|
164
|
+
const h = u.locale ?? null, { data: f, relations: w, schemaSnapshot: T } = ne(e, l, u), g = se({ data: f, relations: w });
|
|
165
|
+
!oe.has(b) && (await s(l, d, h))?.contentHash === g || await e.db.query(v).create({
|
|
166
|
+
data: {
|
|
167
|
+
contentType: l,
|
|
168
|
+
relatedDocumentId: d,
|
|
169
|
+
locale: h,
|
|
170
|
+
status: await a(l, u),
|
|
171
|
+
label: await t(l, f),
|
|
172
|
+
data: f,
|
|
173
|
+
relations: w,
|
|
174
|
+
schemaSnapshot: T,
|
|
175
|
+
contentHash: g,
|
|
176
|
+
origin: b,
|
|
177
|
+
userId: c,
|
|
178
|
+
pinned: !1
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}, j = [
|
|
185
|
+
"restore",
|
|
186
|
+
"publish",
|
|
187
|
+
"unpublish",
|
|
188
|
+
"discardDraft",
|
|
189
|
+
"clone",
|
|
190
|
+
"create",
|
|
191
|
+
"update"
|
|
192
|
+
], C = /* @__PURE__ */ new WeakMap(), re = (e) => {
|
|
193
|
+
let t = C.get(e);
|
|
194
|
+
return t || (t = { intents: [], flushRegistered: !1 }, C.set(e, t)), t;
|
|
195
|
+
}, ae = (e) => {
|
|
196
|
+
const t = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const n of e) {
|
|
198
|
+
const r = `${n.uid}:${n.relatedDocumentId}:${n.locale ?? ""}`, o = t.get(r);
|
|
199
|
+
if (!o) {
|
|
200
|
+
t.set(r, n);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const a = j.indexOf(n.origin) < j.indexOf(o.origin);
|
|
204
|
+
t.set(r, {
|
|
205
|
+
// A winning `publish` still wants the rows an earlier `discardDraft`
|
|
206
|
+
// captured — losing them would lose the only copy of that state.
|
|
207
|
+
...a ? n : o,
|
|
208
|
+
before: o.before ?? n.before
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return [...t.values()];
|
|
212
|
+
}, W = new J(), ce = (e) => W.run({ active: !0 }, e), le = () => W.getStore()?.active === !0, de = 200, ue = async (e, t) => {
|
|
213
|
+
const n = e.db.query(t), r = await n.count({ where: { label: null } });
|
|
214
|
+
if (r === 0) return 0;
|
|
215
|
+
e.log.info(`[rewind] labelling ${r} older version(s)…`);
|
|
216
|
+
const o = H(e);
|
|
217
|
+
let a = 0;
|
|
218
|
+
for (; ; ) {
|
|
219
|
+
const s = await n.findMany({
|
|
220
|
+
select: ["id", "contentType", "data"],
|
|
221
|
+
where: { label: null },
|
|
222
|
+
limit: de
|
|
223
|
+
});
|
|
224
|
+
if (s.length === 0) break;
|
|
225
|
+
for (const i of s) {
|
|
226
|
+
const c = await o(i.contentType, i.data);
|
|
227
|
+
await n.update({
|
|
228
|
+
where: { id: i.id },
|
|
229
|
+
data: { label: c ?? "" }
|
|
230
|
+
}), a += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return e.log.info(`[rewind] labelled ${a} older version(s)`), a;
|
|
234
|
+
}, M = { type: "core", key: "persisted_tables" }, fe = async (e, t) => {
|
|
235
|
+
const n = await e.store.get(M) ?? [];
|
|
236
|
+
n.map((o) => typeof o == "string" ? o : o.name).includes(t) || await e.store.set({
|
|
237
|
+
...M,
|
|
238
|
+
value: [...n, { name: t }]
|
|
239
|
+
});
|
|
240
|
+
}, pe = [
|
|
241
|
+
"create",
|
|
242
|
+
"update",
|
|
243
|
+
"clone",
|
|
244
|
+
"publish",
|
|
245
|
+
"unpublish",
|
|
246
|
+
"discardDraft"
|
|
247
|
+
], me = async ({ strapi: e }) => {
|
|
248
|
+
const t = e.config.get("plugin::rewind");
|
|
249
|
+
await fe(e, "rewind_versions"), await ye(e), ue(e, v).catch(
|
|
250
|
+
(o) => e.log.warn(
|
|
251
|
+
`[rewind] could not label older versions: ${o instanceof Error ? o.message : String(o)}`
|
|
252
|
+
)
|
|
253
|
+
), he(e, t);
|
|
254
|
+
const n = () => e.plugin("rewind").service("snapshot"), r = (o) => {
|
|
255
|
+
if (!pe.includes(o.action)) return !1;
|
|
256
|
+
const a = o.contentType?.uid ?? o.uid ?? "";
|
|
257
|
+
return !(!a.startsWith("api::") || !t.contentTypes.includes(a) || le() || !t.trackApiWrites && !e.requestContext.get()?.request?.url?.startsWith("/content-manager"));
|
|
258
|
+
};
|
|
259
|
+
e.documents.use(async (o, a) => {
|
|
260
|
+
if (!r(o)) return a();
|
|
261
|
+
const s = o.contentType?.uid ?? o.uid, i = o.params?.locale ?? null, c = o.action === "discardDraft" && o.params?.documentId ? await n().readRows(s, o.params.documentId, [i]) : null, l = await a(), d = o.action === "create" || o.action === "clone" ? l?.documentId : o.params?.documentId;
|
|
262
|
+
if (!d) return l;
|
|
263
|
+
const p = ge(e);
|
|
264
|
+
return await e.db.transaction(async ({ trx: b, onCommit: m }) => {
|
|
265
|
+
const u = re(b);
|
|
266
|
+
u.intents.push({
|
|
267
|
+
uid: s,
|
|
268
|
+
relatedDocumentId: d,
|
|
269
|
+
locale: i,
|
|
270
|
+
origin: o.action,
|
|
271
|
+
before: c
|
|
272
|
+
}), !u.flushRegistered && (u.flushRegistered = !0, m(() => {
|
|
273
|
+
const h = u.intents.splice(0, u.intents.length);
|
|
274
|
+
n().captureAll(ae(h), p).catch(
|
|
275
|
+
(f) => e.log.error("[rewind] failed to record a version", f)
|
|
276
|
+
);
|
|
277
|
+
}));
|
|
278
|
+
}), l;
|
|
279
|
+
});
|
|
280
|
+
}, ge = (e) => e.requestContext.get()?.state?.user?.id ?? null, ye = async (e) => {
|
|
281
|
+
try {
|
|
282
|
+
const { tableName: t } = e.db.metadata.get(v);
|
|
283
|
+
await e.db.connection.raw(
|
|
284
|
+
`CREATE INDEX IF NOT EXISTS idx_rewind_lookup
|
|
285
|
+
ON ${t} (content_type, related_document_id, locale, created_at)`
|
|
286
|
+
);
|
|
287
|
+
} catch (t) {
|
|
288
|
+
e.log.warn(
|
|
289
|
+
`[rewind] could not create the lookup index, queries will be slower: ${t instanceof Error ? t.message : String(t)}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}, he = (e, t) => {
|
|
293
|
+
t.retention?.enabled !== !1 && e.cron.add({
|
|
294
|
+
rewindPrune: {
|
|
295
|
+
async task() {
|
|
296
|
+
const n = e.plugin("rewind").service("lock");
|
|
297
|
+
if (await n.acquire("prune", 30 * 60 * 1e3))
|
|
298
|
+
try {
|
|
299
|
+
const r = await e.plugin("rewind").service("retention").prune();
|
|
300
|
+
r.deleted > 0 && e.log.info(
|
|
301
|
+
`[rewind] thinned ${r.deleted} version(s) across ${r.documents} document(s)`
|
|
302
|
+
);
|
|
303
|
+
} catch (r) {
|
|
304
|
+
e.log.error("[rewind] thinning failed", r);
|
|
305
|
+
} finally {
|
|
306
|
+
await n.release("prune");
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
options: { rule: t.cron ?? "0 3 * * *" }
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}, we = {
|
|
313
|
+
default: {
|
|
314
|
+
/**
|
|
315
|
+
* Content types to version, by uid. Empty means nothing is tracked.
|
|
316
|
+
*
|
|
317
|
+
* The default is deliberate. A plugin that silently starts writing a row on
|
|
318
|
+
* every save the moment it is installed is a plugin that gets uninstalled
|
|
319
|
+
* after the first disk alert.
|
|
320
|
+
*
|
|
321
|
+
* 'rewind': {
|
|
322
|
+
* enabled: true,
|
|
323
|
+
* config: { contentTypes: ['api::article.article'] },
|
|
324
|
+
* }
|
|
325
|
+
*/
|
|
326
|
+
contentTypes: [],
|
|
327
|
+
/**
|
|
328
|
+
* Whether to version writes that do not come from the Content Manager.
|
|
329
|
+
*
|
|
330
|
+
* Strapi's own history ignores them, so `false` keeps the out-of-the-box
|
|
331
|
+
* behaviour familiar. With `true`, REST/GraphQL and programmatic writes are
|
|
332
|
+
* versioned too, with `createdById` left null.
|
|
333
|
+
*
|
|
334
|
+
* Either way this only ever sees the Document Service. Writes made through
|
|
335
|
+
* `strapi.db.query()` or the legacy entity service do not pass through the
|
|
336
|
+
* middleware and cannot be captured.
|
|
337
|
+
*/
|
|
338
|
+
trackApiWrites: !1,
|
|
339
|
+
/**
|
|
340
|
+
* How long history is kept.
|
|
341
|
+
*
|
|
342
|
+
* On by default, because a version table that only grows is a problem the
|
|
343
|
+
* plugin would be creating rather than solving. The defaults are gentle:
|
|
344
|
+
* a full week of every save, then a version a day for a month, then one a
|
|
345
|
+
* week — and anchors (publish, unpublish, discard, restore) and pinned
|
|
346
|
+
* versions are never removed at all, whatever their age.
|
|
347
|
+
*
|
|
348
|
+
* Set `enabled: false` to keep everything forever and manage it yourself.
|
|
349
|
+
*/
|
|
350
|
+
retention: {
|
|
351
|
+
enabled: !0,
|
|
352
|
+
keepAllDays: 7,
|
|
353
|
+
dailyUntilDays: 30,
|
|
354
|
+
maxAgeDays: 365,
|
|
355
|
+
keepAnchors: !0
|
|
356
|
+
},
|
|
357
|
+
/** When the thinning runs. Standard cron, in the server's timezone. */
|
|
358
|
+
cron: "0 3 * * *"
|
|
359
|
+
},
|
|
360
|
+
validator(e = {}) {
|
|
361
|
+
const { contentTypes: t, retention: n } = e;
|
|
362
|
+
if (n) {
|
|
363
|
+
const { keepAllDays: o = 7, dailyUntilDays: a = 30, maxAgeDays: s = 365 } = n;
|
|
364
|
+
if (!(o <= a && a <= s))
|
|
365
|
+
throw new Error(
|
|
366
|
+
`[rewind] config.retention needs keepAllDays <= dailyUntilDays <= maxAgeDays (received ${o}, ${a}, ${s})`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (t === void 0) return;
|
|
370
|
+
if (!Array.isArray(t) || t.some((o) => typeof o != "string"))
|
|
371
|
+
throw new Error(
|
|
372
|
+
'[rewind] config.contentTypes must be an array of content type uids, e.g. ["api::article.article"]'
|
|
373
|
+
);
|
|
374
|
+
const r = t.filter((o) => !o.startsWith("api::"));
|
|
375
|
+
if (r.length > 0)
|
|
376
|
+
throw new Error(
|
|
377
|
+
`[rewind] config.contentTypes only supports api:: content types, received: ${r.join(", ")}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}, be = "collectionType", Te = "rewind_versions", ve = {
|
|
381
|
+
singularName: "version",
|
|
382
|
+
pluralName: "versions",
|
|
383
|
+
displayName: "Rewind Version",
|
|
384
|
+
description: "One snapshot of one document, in one locale, at one point in time."
|
|
385
|
+
}, De = {
|
|
386
|
+
draftAndPublish: !1,
|
|
387
|
+
comment: "Managed by strapi-plugin-rewind. Do not edit manually."
|
|
388
|
+
}, Ae = {
|
|
389
|
+
"content-manager": {
|
|
390
|
+
visible: !1
|
|
391
|
+
},
|
|
392
|
+
"content-type-builder": {
|
|
393
|
+
visible: !1
|
|
394
|
+
},
|
|
395
|
+
i18n: {
|
|
396
|
+
localized: !1
|
|
397
|
+
}
|
|
398
|
+
}, Ie = {
|
|
399
|
+
contentType: {
|
|
400
|
+
type: "string",
|
|
401
|
+
required: !0
|
|
402
|
+
},
|
|
403
|
+
relatedDocumentId: {
|
|
404
|
+
type: "string",
|
|
405
|
+
required: !0
|
|
406
|
+
},
|
|
407
|
+
locale: {
|
|
408
|
+
type: "string"
|
|
409
|
+
},
|
|
410
|
+
status: {
|
|
411
|
+
type: "enumeration",
|
|
412
|
+
enum: [
|
|
413
|
+
"draft",
|
|
414
|
+
"published",
|
|
415
|
+
"modified"
|
|
416
|
+
],
|
|
417
|
+
required: !0
|
|
418
|
+
},
|
|
419
|
+
label: {
|
|
420
|
+
type: "string"
|
|
421
|
+
},
|
|
422
|
+
data: {
|
|
423
|
+
type: "json",
|
|
424
|
+
required: !0
|
|
425
|
+
},
|
|
426
|
+
schemaSnapshot: {
|
|
427
|
+
type: "json",
|
|
428
|
+
required: !0
|
|
429
|
+
},
|
|
430
|
+
relations: {
|
|
431
|
+
type: "json"
|
|
432
|
+
},
|
|
433
|
+
contentHash: {
|
|
434
|
+
type: "string",
|
|
435
|
+
required: !0
|
|
436
|
+
},
|
|
437
|
+
origin: {
|
|
438
|
+
type: "enumeration",
|
|
439
|
+
enum: [
|
|
440
|
+
"create",
|
|
441
|
+
"update",
|
|
442
|
+
"clone",
|
|
443
|
+
"publish",
|
|
444
|
+
"unpublish",
|
|
445
|
+
"discardDraft",
|
|
446
|
+
"restore"
|
|
447
|
+
],
|
|
448
|
+
required: !0
|
|
449
|
+
},
|
|
450
|
+
pinned: {
|
|
451
|
+
type: "boolean",
|
|
452
|
+
default: !1
|
|
453
|
+
},
|
|
454
|
+
userId: {
|
|
455
|
+
type: "integer"
|
|
456
|
+
}
|
|
457
|
+
}, ke = {
|
|
458
|
+
kind: be,
|
|
459
|
+
collectionName: Te,
|
|
460
|
+
info: ve,
|
|
461
|
+
options: De,
|
|
462
|
+
pluginOptions: Ae,
|
|
463
|
+
attributes: Ie
|
|
464
|
+
}, Oe = {
|
|
465
|
+
version: { schema: ke }
|
|
466
|
+
}, Se = ({ strapi: e }) => {
|
|
467
|
+
e.cron.remove("rewindPrune");
|
|
468
|
+
}, Ee = [
|
|
469
|
+
"id",
|
|
470
|
+
"contentType",
|
|
471
|
+
"relatedDocumentId",
|
|
472
|
+
"locale",
|
|
473
|
+
"status",
|
|
474
|
+
"label",
|
|
475
|
+
"origin",
|
|
476
|
+
"userId",
|
|
477
|
+
"pinned",
|
|
478
|
+
"contentHash",
|
|
479
|
+
"createdAt"
|
|
480
|
+
], qe = ({ strapi: e }) => ({
|
|
481
|
+
async find(t) {
|
|
482
|
+
const { contentType: n, documentId: r, locale: o, page: a = 1, pageSize: s = 10 } = t.query;
|
|
483
|
+
if (!n || !r)
|
|
484
|
+
return t.badRequest("contentType and documentId are required.");
|
|
485
|
+
const i = {
|
|
486
|
+
contentType: n,
|
|
487
|
+
relatedDocumentId: r,
|
|
488
|
+
...o ? { locale: o } : {}
|
|
489
|
+
}, c = Math.min(Number(s) || 10, 100), l = (Math.max(Number(a) || 1, 1) - 1) * c, [d, p] = await Promise.all([
|
|
490
|
+
e.db.query(v).findMany({
|
|
491
|
+
select: Ee,
|
|
492
|
+
where: i,
|
|
493
|
+
orderBy: { createdAt: "desc" },
|
|
494
|
+
limit: c,
|
|
495
|
+
offset: l
|
|
496
|
+
}),
|
|
497
|
+
e.db.query(v).count({ where: i })
|
|
498
|
+
]);
|
|
499
|
+
t.body = {
|
|
500
|
+
data: await U(e, d),
|
|
501
|
+
meta: { page: Number(a) || 1, pageSize: c, total: p }
|
|
502
|
+
};
|
|
503
|
+
},
|
|
504
|
+
async findOne(t) {
|
|
505
|
+
const n = await e.db.query(v).findOne({ where: { id: Number(t.params.id) } });
|
|
506
|
+
if (!n) return t.notFound("No such version.");
|
|
507
|
+
t.body = { data: (await U(e, [n]))[0] };
|
|
508
|
+
},
|
|
509
|
+
async diff(t) {
|
|
510
|
+
const { against: n } = t.query;
|
|
511
|
+
try {
|
|
512
|
+
t.body = {
|
|
513
|
+
data: await e.plugin("rewind").service("diff").between(Number(t.params.id), n ? Number(n) : void 0)
|
|
514
|
+
};
|
|
515
|
+
} catch (r) {
|
|
516
|
+
return t.badRequest(
|
|
517
|
+
r instanceof Error ? r.message : "Could not compare these versions."
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
async preview(t) {
|
|
522
|
+
try {
|
|
523
|
+
t.body = {
|
|
524
|
+
data: await e.plugin("rewind").service("restore").preview(Number(t.params.id))
|
|
525
|
+
};
|
|
526
|
+
} catch (n) {
|
|
527
|
+
return t.badRequest(n instanceof Error ? n.message : "Preview failed.");
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
async restore(t) {
|
|
531
|
+
try {
|
|
532
|
+
t.body = {
|
|
533
|
+
data: await e.plugin("rewind").service("restore").apply(Number(t.params.id), t.state?.user?.id ?? null)
|
|
534
|
+
};
|
|
535
|
+
} catch (n) {
|
|
536
|
+
return t.badRequest(n instanceof Error ? n.message : "Restore failed.");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}), U = async (e, t) => {
|
|
540
|
+
const n = [...new Set(t.map((a) => a.userId).filter(Boolean))];
|
|
541
|
+
if (n.length === 0) return t.map((a) => ({ ...a, user: null }));
|
|
542
|
+
const r = await e.db.query("admin::user").findMany({ select: ["id", "firstname", "lastname", "email"], where: { id: n } }), o = new Map(r.map((a) => [a.id, a]));
|
|
543
|
+
return t.map((a) => {
|
|
544
|
+
const s = o.get(a.userId);
|
|
545
|
+
return {
|
|
546
|
+
...a,
|
|
547
|
+
user: s ? {
|
|
548
|
+
id: s.id,
|
|
549
|
+
name: [s.firstname, s.lastname].filter(Boolean).join(" ") || s.email
|
|
550
|
+
} : null
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
}, Re = {
|
|
554
|
+
version: qe
|
|
555
|
+
}, $e = {
|
|
556
|
+
type: "admin",
|
|
557
|
+
routes: [
|
|
558
|
+
{
|
|
559
|
+
method: "GET",
|
|
560
|
+
path: "/versions",
|
|
561
|
+
handler: "version.find",
|
|
562
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
method: "GET",
|
|
566
|
+
path: "/versions/:id",
|
|
567
|
+
handler: "version.findOne",
|
|
568
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
method: "GET",
|
|
572
|
+
path: "/versions/:id/diff",
|
|
573
|
+
handler: "version.diff",
|
|
574
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
method: "GET",
|
|
578
|
+
path: "/versions/:id/preview",
|
|
579
|
+
handler: "version.preview",
|
|
580
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
method: "POST",
|
|
584
|
+
path: "/versions/:id/restore",
|
|
585
|
+
handler: "version.restore",
|
|
586
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
}, Fe = {
|
|
590
|
+
admin: $e
|
|
591
|
+
}, B = (e) => e.match(/\S+\s*/g) ?? [], L = 2500, Ne = (e, t) => {
|
|
592
|
+
const n = B(e), r = B(t);
|
|
593
|
+
if (n.length > L || r.length > L)
|
|
594
|
+
return [
|
|
595
|
+
{ op: "removed", value: e },
|
|
596
|
+
{ op: "added", value: t }
|
|
597
|
+
];
|
|
598
|
+
const o = Array.from(
|
|
599
|
+
{ length: n.length + 1 },
|
|
600
|
+
() => new Array(r.length + 1).fill(0)
|
|
601
|
+
);
|
|
602
|
+
for (let l = n.length - 1; l >= 0; l -= 1)
|
|
603
|
+
for (let d = r.length - 1; d >= 0; d -= 1)
|
|
604
|
+
o[l][d] = n[l] === r[d] ? o[l + 1][d + 1] + 1 : Math.max(o[l + 1][d], o[l][d + 1]);
|
|
605
|
+
const a = [], s = (l, d) => {
|
|
606
|
+
const p = a[a.length - 1];
|
|
607
|
+
p && p.op === l ? p.value += d : a.push({ op: l, value: d });
|
|
608
|
+
};
|
|
609
|
+
let i = 0, c = 0;
|
|
610
|
+
for (; i < n.length && c < r.length; )
|
|
611
|
+
n[i] === r[c] ? (s("equal", n[i]), i += 1, c += 1) : o[i + 1][c] >= o[i][c + 1] ? (s("removed", n[i]), i += 1) : (s("added", r[c]), c += 1);
|
|
612
|
+
for (; i < n.length; ) s("removed", n[i++]);
|
|
613
|
+
for (; c < r.length; ) s("added", r[c++]);
|
|
614
|
+
return a;
|
|
615
|
+
}, _e = /* @__PURE__ */ new Set([
|
|
616
|
+
"id",
|
|
617
|
+
"__component",
|
|
618
|
+
"type",
|
|
619
|
+
"format",
|
|
620
|
+
"level",
|
|
621
|
+
"language",
|
|
622
|
+
"url"
|
|
623
|
+
]), E = (e, t = 0) => t > 12 || e == null ? "" : typeof e == "string" ? e : typeof e == "boolean" ? "" : typeof e == "number" ? String(e) : Array.isArray(e) ? e.map((n) => E(n, t + 1)).filter(Boolean).join(" ") : typeof e == "object" ? Object.entries(e).filter(([n]) => !_e.has(n)).map(([, n]) => E(n, t + 1)).filter(Boolean).join(" ") : "", z = (e) => e.replace(/\s+/g, " ").trim(), je = 6, Ce = (e, t = je) => {
|
|
624
|
+
const n = [];
|
|
625
|
+
return e.forEach((r, o) => {
|
|
626
|
+
if (r.op !== "equal") {
|
|
627
|
+
n.push(r);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const a = r.value.match(/\S+\s*/g) ?? [];
|
|
631
|
+
if (a.length <= t * 2 + 1) {
|
|
632
|
+
n.push(r);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const s = o === 0, i = o === e.length - 1, c = s ? [] : a.slice(0, t), l = i ? [] : a.slice(-t);
|
|
636
|
+
c.length && n.push({ op: "equal", value: c.join("") }), n.push({ op: "equal", value: " … " }), l.length && n.push({ op: "equal", value: l.join("") });
|
|
637
|
+
}), n;
|
|
638
|
+
}, Me = /* @__PURE__ */ new Set([
|
|
639
|
+
"string",
|
|
640
|
+
"text",
|
|
641
|
+
"integer",
|
|
642
|
+
"biginteger",
|
|
643
|
+
"float",
|
|
644
|
+
"decimal",
|
|
645
|
+
"boolean",
|
|
646
|
+
"date",
|
|
647
|
+
"datetime",
|
|
648
|
+
"time",
|
|
649
|
+
"enumeration",
|
|
650
|
+
"uid",
|
|
651
|
+
"email",
|
|
652
|
+
"password"
|
|
653
|
+
]), Ue = (e, t) => e === "text" || e === "richtext" || e === "blocks" || e === "json" || e === "customField" || e === "component" || e === "dynamiczone" || e === "string" && typeof t == "string" && t.length > 80, O = (e) => `${e.targetUid}:${e.documentId ?? e.id}`, Be = ({ strapi: e }) => {
|
|
654
|
+
const t = (o) => o && {
|
|
655
|
+
id: o.id,
|
|
656
|
+
label: o.label ?? null,
|
|
657
|
+
createdAt: o.createdAt,
|
|
658
|
+
origin: o.origin
|
|
659
|
+
}, n = async (o) => {
|
|
660
|
+
const a = await e.db.query(v).findOne({ where: { id: o } });
|
|
661
|
+
if (!a) throw new Error(`No version with id ${o}`);
|
|
662
|
+
return a;
|
|
663
|
+
}, r = async (o) => {
|
|
664
|
+
const [a] = await e.db.query(v).findMany({
|
|
665
|
+
where: {
|
|
666
|
+
contentType: o.contentType,
|
|
667
|
+
relatedDocumentId: o.relatedDocumentId,
|
|
668
|
+
locale: o.locale,
|
|
669
|
+
id: { $lt: o.id }
|
|
670
|
+
},
|
|
671
|
+
orderBy: { id: "desc" },
|
|
672
|
+
limit: 1
|
|
673
|
+
});
|
|
674
|
+
return a ?? null;
|
|
675
|
+
};
|
|
676
|
+
return {
|
|
677
|
+
/**
|
|
678
|
+
* What changed between a version and the one before it.
|
|
679
|
+
*
|
|
680
|
+
* Comparing against the previous version — rather than against the document
|
|
681
|
+
* as it stands — is what makes a history readable: each entry answers "what
|
|
682
|
+
* did this edit do?". The other question, "what would restoring this
|
|
683
|
+
* change?", is what `restore.preview()` already answers.
|
|
684
|
+
*/
|
|
685
|
+
async between(o, a) {
|
|
686
|
+
const s = await n(o), i = a ? await n(a) : await r(s), c = e.getModel(s.contentType)?.attributes, l = s.data ?? {}, d = i?.data ?? {}, p = s.relations ?? {}, b = i?.relations ?? {}, m = [];
|
|
687
|
+
for (const u of /* @__PURE__ */ new Set([...Object.keys(d), ...Object.keys(l)])) {
|
|
688
|
+
const h = d[u], f = l[u];
|
|
689
|
+
if (JSON.stringify(h) === JSON.stringify(f)) continue;
|
|
690
|
+
const w = c?.[u]?.type ?? "unknown", T = c?.[u]?.customField, g = h === void 0 ? "added" : f === void 0 ? "removed" : "changed";
|
|
691
|
+
if (Me.has(w) && !Ue(w, h ?? f)) {
|
|
692
|
+
m.push({ field: u, type: w, customField: T, kind: g, before: h, after: f });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const D = z(E(h)), A = z(E(f));
|
|
696
|
+
if (D === A) {
|
|
697
|
+
m.push({ field: u, type: w, customField: T, kind: g, before: void 0, after: void 0 });
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
m.push({
|
|
701
|
+
field: u,
|
|
702
|
+
type: w,
|
|
703
|
+
customField: T,
|
|
704
|
+
kind: g,
|
|
705
|
+
// Collapsed here rather than in the panel: sending an entire article
|
|
706
|
+
// to highlight one word wastes the payload as well as the reader's
|
|
707
|
+
// attention.
|
|
708
|
+
spans: Ce(Ne(D, A))
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
for (const u of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(p)])) {
|
|
712
|
+
const h = b[u] ?? [], f = p[u] ?? [], w = new Set(h.map(O)), T = new Set(f.map(O)), g = f.filter((A) => !w.has(O(A))), D = h.filter((A) => !T.has(O(A)));
|
|
713
|
+
g.length === 0 && D.length === 0 || m.push({
|
|
714
|
+
field: u,
|
|
715
|
+
type: c?.[u]?.type ?? "relation",
|
|
716
|
+
kind: "changed",
|
|
717
|
+
linked: g,
|
|
718
|
+
unlinked: D
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
from: i ? t(i) : null,
|
|
723
|
+
to: t(s),
|
|
724
|
+
changes: m,
|
|
725
|
+
identical: m.length === 0
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}, Le = ({ strapi: e }) => {
|
|
730
|
+
const t = (n) => ({
|
|
731
|
+
type: "plugin",
|
|
732
|
+
name: "rewind",
|
|
733
|
+
key: `lock:${n}`
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
async acquire(n, r) {
|
|
737
|
+
const o = await e.store.get(t(n));
|
|
738
|
+
return o && o.expires > Date.now() ? !1 : (await e.store.set({
|
|
739
|
+
...t(n),
|
|
740
|
+
value: { expires: Date.now() + r }
|
|
741
|
+
}), !0);
|
|
742
|
+
},
|
|
743
|
+
async release(n) {
|
|
744
|
+
await e.store.set({ ...t(n), value: { expires: 0 } });
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}, K = {
|
|
748
|
+
keepAllDays: 7,
|
|
749
|
+
dailyUntilDays: 30,
|
|
750
|
+
maxAgeDays: 365,
|
|
751
|
+
keepAnchors: !0
|
|
752
|
+
}, ze = /* @__PURE__ */ new Set(["publish", "unpublish", "discardDraft", "restore"]), G = 24 * 60 * 60 * 1e3, Pe = (e, t) => (t.getTime() - new Date(e).getTime()) / G, Ye = (e) => e.toISOString().slice(0, 10), He = (e) => {
|
|
753
|
+
const t = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate())), n = t.getUTCDay() || 7;
|
|
754
|
+
t.setUTCDate(t.getUTCDate() + 4 - n);
|
|
755
|
+
const r = new Date(Date.UTC(t.getUTCFullYear(), 0, 1)), o = Math.ceil(((t.getTime() - r.getTime()) / G + 1) / 7);
|
|
756
|
+
return `${t.getUTCFullYear()}-W${o}`;
|
|
757
|
+
}, We = (e, t, n = K) => {
|
|
758
|
+
const r = /* @__PURE__ */ new Map(), o = [], a = [...e].sort(
|
|
759
|
+
(s, i) => new Date(i.createdAt).getTime() - new Date(s.createdAt).getTime()
|
|
760
|
+
);
|
|
761
|
+
for (const s of a) {
|
|
762
|
+
if (s.pinned || n.keepAnchors && ze.has(s.origin)) continue;
|
|
763
|
+
const i = Pe(s.createdAt, t);
|
|
764
|
+
if (i <= n.keepAllDays) continue;
|
|
765
|
+
if (i > n.maxAgeDays) {
|
|
766
|
+
o.push(s.id);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const c = new Date(s.createdAt), l = i <= n.dailyUntilDays ? `d:${Ye(c)}` : `w:${He(c)}`;
|
|
770
|
+
r.has(l) ? o.push(s.id) : r.set(l, s);
|
|
771
|
+
}
|
|
772
|
+
return o;
|
|
773
|
+
}, P = 500, Ke = ({ strapi: e }) => {
|
|
774
|
+
const t = () => e.db.query(v), n = async (r) => {
|
|
775
|
+
const o = await t().findMany({
|
|
776
|
+
select: ["contentType", "relatedDocumentId", "locale"],
|
|
777
|
+
where: { createdAt: { $lt: r } }
|
|
778
|
+
}), a = /* @__PURE__ */ new Map();
|
|
779
|
+
for (const s of o) {
|
|
780
|
+
const i = `${s.contentType}|${s.relatedDocumentId}|${s.locale ?? ""}`;
|
|
781
|
+
a.has(i) || a.set(i, {
|
|
782
|
+
contentType: s.contentType,
|
|
783
|
+
relatedDocumentId: s.relatedDocumentId,
|
|
784
|
+
locale: s.locale ?? null
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return [...a.values()];
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
/**
|
|
791
|
+
* Thins the history according to the configured policy.
|
|
792
|
+
*
|
|
793
|
+
* Returns what it did rather than logging and forgetting, so the cron can
|
|
794
|
+
* report it and a test can assert on it.
|
|
795
|
+
*/
|
|
796
|
+
async prune(r = /* @__PURE__ */ new Date()) {
|
|
797
|
+
const o = e.config.get("plugin::rewind"), a = { ...K, ...o?.retention };
|
|
798
|
+
if (o?.retention?.enabled === !1)
|
|
799
|
+
return { scanned: 0, deleted: 0, documents: 0 };
|
|
800
|
+
const s = new Date(r.getTime() - a.keepAllDays * 24 * 60 * 60 * 1e3), i = await n(s);
|
|
801
|
+
let c = 0, l = 0;
|
|
802
|
+
for (const d of i) {
|
|
803
|
+
const p = await t().findMany({
|
|
804
|
+
select: ["id", "createdAt", "origin", "pinned"],
|
|
805
|
+
where: {
|
|
806
|
+
contentType: d.contentType,
|
|
807
|
+
relatedDocumentId: d.relatedDocumentId,
|
|
808
|
+
locale: d.locale
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
c += p.length;
|
|
812
|
+
const b = We(p, r, a);
|
|
813
|
+
for (let m = 0; m < b.length; m += P) {
|
|
814
|
+
const u = b.slice(m, m + P);
|
|
815
|
+
await t().deleteMany({ where: { id: { $in: u } } }), l += u.length;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return { scanned: c, deleted: l, documents: i.length };
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}, Ge = ({ strapi: e }) => {
|
|
822
|
+
const t = (s) => e.getModel(s), n = async (s) => {
|
|
823
|
+
const i = await e.db.query(v).findOne({ where: { id: s } });
|
|
824
|
+
if (!i)
|
|
825
|
+
throw new Error(`No version with id ${s}`);
|
|
826
|
+
return i;
|
|
827
|
+
}, r = (s) => s?.pluginOptions?.i18n?.localized === !0, o = (s) => e.contentTypes[s]?.pluginOptions?.i18n?.localized === !0, a = async (s) => s.targetUid === "plugin::upload.file" ? !!await e.db.query("plugin::upload.file").findOne({ where: { id: s.id } }) : !!await e.db.query(s.targetUid).findOne({
|
|
828
|
+
where: {
|
|
829
|
+
documentId: s.documentId,
|
|
830
|
+
...s.locale ? { locale: s.locale } : {}
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
/**
|
|
835
|
+
* What restoring this version would actually do.
|
|
836
|
+
*
|
|
837
|
+
* A separate call, and the UI shows it before asking for confirmation.
|
|
838
|
+
* Restore without a preview of the consequences is a trap rather than a
|
|
839
|
+
* tool — `crossLocaleFields` in particular, where restoring the Polish
|
|
840
|
+
* version of a document quietly rewrites a field in every other language.
|
|
841
|
+
*/
|
|
842
|
+
async preview(s) {
|
|
843
|
+
const i = await n(s), c = i.contentType, l = t(c).attributes, d = i.schemaSnapshot?.attributes ?? {}, p = i.data ?? {}, b = i.relations ?? {}, m = Object.keys(l).filter((y) => !S.includes(y)), u = Object.keys(d), h = u.filter((y) => !m.includes(y)), f = m.filter((y) => !u.includes(y)), w = u.filter((y) => m.includes(y)), T = w.filter((y) => $(l[y])), g = w.filter((y) => !T.includes(y)), D = o(c) ? g.filter((y) => !r(l[y])) : [], A = [], F = [];
|
|
844
|
+
for (const [y, X] of Object.entries(b))
|
|
845
|
+
if (g.includes(y))
|
|
846
|
+
for (const k of X)
|
|
847
|
+
await a(k) || (k.targetUid === "plugin::upload.file" ? F.push(k) : A.push(k));
|
|
848
|
+
const V = D.length ? await e.plugin("i18n")?.service("locales").find() : [];
|
|
849
|
+
return {
|
|
850
|
+
fieldsToRestore: g.filter((y) => y in p),
|
|
851
|
+
fieldsKeptAsIs: f,
|
|
852
|
+
fieldsDropped: h,
|
|
853
|
+
crossLocaleFields: D,
|
|
854
|
+
unsupportedFields: T,
|
|
855
|
+
brokenRelations: A,
|
|
856
|
+
missingMedia: F,
|
|
857
|
+
affectedLocales: V.map((y) => y.code)
|
|
858
|
+
};
|
|
859
|
+
},
|
|
860
|
+
/**
|
|
861
|
+
* Writes the version back, to the draft only.
|
|
862
|
+
*
|
|
863
|
+
* The document lands in "modified" and publishing stays a deliberate act —
|
|
864
|
+
* restoring must never change what the public sees on its own.
|
|
865
|
+
*/
|
|
866
|
+
async apply(s, i) {
|
|
867
|
+
const c = await n(s), l = c.contentType, d = await this.preview(s), p = [];
|
|
868
|
+
await e.plugin("rewind").service("snapshot").capture(
|
|
869
|
+
{
|
|
870
|
+
uid: l,
|
|
871
|
+
relatedDocumentId: c.relatedDocumentId,
|
|
872
|
+
locale: c.locale,
|
|
873
|
+
origin: "restore",
|
|
874
|
+
before: null
|
|
875
|
+
},
|
|
876
|
+
i
|
|
877
|
+
);
|
|
878
|
+
const b = {}, m = c.data ?? {}, u = c.relations ?? {};
|
|
879
|
+
for (const f of d.fieldsToRestore)
|
|
880
|
+
b[f] = m[f];
|
|
881
|
+
const h = new Set(
|
|
882
|
+
[...d.brokenRelations, ...d.missingMedia].map(
|
|
883
|
+
(f) => `${f.targetUid}:${f.documentId ?? f.id}`
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
for (const [f, w] of Object.entries(u)) {
|
|
887
|
+
if (d.unsupportedFields.includes(f) || d.fieldsDropped.includes(f)) continue;
|
|
888
|
+
const T = w.filter(
|
|
889
|
+
(g) => !h.has(`${g.targetUid}:${g.documentId ?? g.id}`)
|
|
890
|
+
);
|
|
891
|
+
T.length !== w.length && p.push(
|
|
892
|
+
`${w.length - T.length} target(s) of "${f}" no longer exist and were skipped.`
|
|
893
|
+
), b[f] = T.map(
|
|
894
|
+
(g) => g.targetUid === "plugin::upload.file" ? g.id : g.documentId
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
return d.fieldsDropped.length && p.push(
|
|
898
|
+
`Skipped ${d.fieldsDropped.length} field(s) no longer in the model: ${d.fieldsDropped.join(", ")}.`
|
|
899
|
+
), d.crossLocaleFields.length && p.push(
|
|
900
|
+
`${d.crossLocaleFields.join(", ")} are not localised, so they changed in every locale.`
|
|
901
|
+
), await ce(async () => {
|
|
902
|
+
await e.documents(l).update({
|
|
903
|
+
documentId: c.relatedDocumentId,
|
|
904
|
+
locale: c.locale ?? void 0,
|
|
905
|
+
// Explicit, though update() forces draft anyway: passing 'published'
|
|
906
|
+
// here would publish, and that must never happen by accident.
|
|
907
|
+
status: "draft",
|
|
908
|
+
data: b
|
|
909
|
+
});
|
|
910
|
+
}), { documentId: c.relatedDocumentId, warnings: p };
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
}, Ve = {
|
|
914
|
+
snapshot: ie,
|
|
915
|
+
restore: Ge,
|
|
916
|
+
diff: Be,
|
|
917
|
+
retention: Ke,
|
|
918
|
+
lock: Le
|
|
919
|
+
}, Je = {
|
|
920
|
+
bootstrap: me,
|
|
921
|
+
destroy: Se,
|
|
922
|
+
config: we,
|
|
923
|
+
contentTypes: Oe,
|
|
924
|
+
controllers: Re,
|
|
925
|
+
routes: Fe,
|
|
926
|
+
services: Ve
|
|
927
|
+
};
|
|
928
|
+
export {
|
|
929
|
+
Je as default
|
|
930
|
+
};
|