@qkix/strapi-plugin-rewind 0.1.4 → 0.2.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/README.md +11 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +145 -121
- package/dist/server/index.js +2 -2
- package/dist/server/index.mjs +197 -164
- package/dist/server/src/controllers/index.d.ts +1 -0
- package/dist/server/src/controllers/version.d.ts +6 -0
- package/dist/server/src/index.d.ts +9 -0
- package/dist/server/src/services/index.d.ts +8 -0
- package/dist/server/src/services/pin.d.ts +19 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,6 +172,17 @@ type, since every custom field stores itself as `json`.
|
|
|
172
172
|
> generic text diff. A ready-made renderer for Better Blocks documents is
|
|
173
173
|
> wanted, and not yet written.
|
|
174
174
|
|
|
175
|
+
## Pinning a version
|
|
176
|
+
|
|
177
|
+
Every entry has a pin. A pinned version is never thinned, whatever the retention
|
|
178
|
+
settings say and however old it gets - the state the site launched in, the draft
|
|
179
|
+
before a rewrite, the version somebody will ask about in a year.
|
|
180
|
+
|
|
181
|
+
Pinning is the one thing in this panel that writes to the history rather than to
|
|
182
|
+
the document, and it is the whole of it: a version is pinned or it is not.
|
|
183
|
+
Nothing else about the version changes, and unpinning hands it straight back to
|
|
184
|
+
the next prune.
|
|
185
|
+
|
|
175
186
|
## Configuration
|
|
176
187
|
|
|
177
188
|
| Option | Default | Meaning |
|
|
@@ -202,8 +213,6 @@ Worth knowing before you install it, not after:
|
|
|
202
213
|
- **Only the Document Service is visible.** Writes made through
|
|
203
214
|
`strapi.db.query()` or the legacy entity service bypass the middleware
|
|
204
215
|
entirely and cannot be captured at any setting.
|
|
205
|
-
- **Nothing pins a version from the panel yet.** The column is there and prune
|
|
206
|
-
honours it, but there is no button; pin by setting `pinned` on the row.
|
|
207
216
|
- **Deleted relation targets lose their names.** A version stores which document
|
|
208
217
|
a relation pointed at, not what it was called, so a link to something since
|
|
209
218
|
deleted shows as missing rather than as "the article it used to be".
|
package/dist/admin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("react/jsx-runtime"),
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("react/jsx-runtime"),W=require("react"),z=require("@strapi/admin/strapi-admin"),t=require("@strapi/design-system"),G=require("@strapi/icons");function U(n){if(n&&n.__esModule)return n;const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const s in n)if(s!=="default"){const a=Object.getOwnPropertyDescriptor(n,s);Object.defineProperty(r,s,a.get?a:{enumerable:!0,get:()=>n[s]})}}return r.default=n,Object.freeze(r)}const d=U(W),K=(n,r,s)=>{const a=n[r];return a?typeof a=="function"?a():Promise.resolve(a):new Promise((C,y)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(y.bind(null,new Error("Unknown variable dynamic import: "+r+(r.split("/").length!==s?". Note that variables only represent file names one level deep.":""))))})},P=new Map,V=(n,r)=>{P.set(n,r)},R=n=>P.get(n),J=({spans:n})=>e.jsx(t.Typography,{variant:"omega",tag:"p",children:n.map((r,s)=>r.op==="equal"?e.jsx(t.Typography,{variant:"omega",textColor:"neutral600",children:r.value},s):e.jsx(t.Typography,{variant:"omega",textColor:r.op==="added"?"success600":"danger600",style:r.op==="removed"?{textDecoration:"line-through"}:{fontWeight:600},children:r.value},s))}),F=n=>n==null||n===""?"(empty)":typeof n=="object"?JSON.stringify(n):String(n),Z=({change:n})=>{const r=(n.customField?R(n.customField):void 0)??R(n.type);return r?e.jsx(r,{change:n}):n.spans?e.jsx(J,{spans:n.spans}):n.linked||n.unlinked?e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:1,children:[n.linked?.length?e.jsxs(t.Typography,{variant:"pi",textColor:"success600",children:["+ ",n.linked.length," linked"]}):null,n.unlinked?.length?e.jsxs(t.Typography,{variant:"pi",textColor:"danger600",children:["− ",n.unlinked.length," unlinked"]}):null]}):n.before!==void 0||n.after!==void 0?e.jsxs(t.Flex,{gap:2,alignItems:"center",wrap:"wrap",children:[e.jsx(t.Typography,{variant:"omega",textColor:"danger600",style:{textDecoration:"line-through"},children:F(n.before)}),e.jsx(t.Typography,{variant:"omega",textColor:"neutral500",children:"→"}),e.jsx(t.Typography,{variant:"omega",textColor:"success600",children:F(n.after)})]}):e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"Formatting or structure changed; the text is the same."})},Q=({diff:n,loading:r,onClose:s})=>e.jsx(t.Modal.Root,{open:!0,onOpenChange:s,children:e.jsxs(t.Modal.Content,{children:[e.jsx(t.Modal.Header,{children:e.jsx(t.Modal.Title,{children:"What changed"})}),e.jsx(t.Modal.Body,{children:r?e.jsx(t.Flex,{justifyContent:"center",padding:4,children:e.jsx(t.Loader,{small:!0,children:"Comparing"})}):n?e.jsxs(t.Flex,{direction:"column",alignItems:"stretch",gap:4,children:[e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:n.from?"Compared with the version saved just before it.":"This is the earliest version of the document, so everything in it is new."}),n.identical?e.jsx(t.Typography,{textColor:"neutral600",children:"Nothing changed between these two versions."}):n.changes.map(a=>e.jsxs(t.Box,{children:[e.jsx(t.Typography,{variant:"sigma",textColor:"neutral600",children:a.field}),e.jsx(t.Box,{paddingTop:1,children:e.jsx(Z,{change:a})})]},a.field))]}):e.jsx(t.Typography,{textColor:"danger600",children:"Could not compare these versions."})}),e.jsx(t.Modal.Footer,{children:e.jsx(t.Button,{variant:"tertiary",onClick:s,children:"Close"})})]})}),c="rewind",X={create:"Created",update:"Edited",clone:"Cloned",publish:"Published",unpublish:"Unpublished",discardDraft:"Draft discarded",restore:"Replaced by restore"},I=new Set(["publish","unpublish","discardDraft","restore"]),Y=10,ee=n=>{const r=new Date(n),s=Math.round((Date.now()-r.getTime())/6e4);return s<1?"just now":s<60?`${s} min ago`:s<60*24?`${Math.round(s/60)} h ago`:r.toLocaleString()},te=({model:n,documentId:r,updatedAt:s})=>{const{get:a,post:C,put:y}=z.useFetchClient(),[p,j]=d.useState([]),[B,L]=d.useState(0),[M,O]=d.useState(1),[w,T]=d.useState(!0),[x,h]=d.useState(null),[o,f]=d.useState(null),[b,S]=d.useState(!1),[_,k]=d.useState(!1),[$,m]=d.useState(null),[N,D]=d.useState(!1),v=d.useCallback(async i=>{T(!0),h(null);try{const{data:l}=await a(`/${c}/versions`,{params:{contentType:n,documentId:r,page:i,pageSize:Y}});j(g=>i===1?l.data:[...g,...l.data]),L(l.meta.total),O(i)}catch{h("Could not load the history for this document.")}finally{T(!1)}},[a,n,r]);d.useEffect(()=>{v(1)},[v,s]);const A=async i=>{k(!0),D(!0),m(null);try{const{data:l}=await a(`/${c}/versions/${i.id}/diff`);m(l.data)}catch{m(null)}finally{D(!1)}},q=async i=>{const l=!i.pinned;j(g=>g.map(u=>u.id===i.id?{...u,pinned:l}:u));try{await y(`/${c}/versions/${i.id}/pin`,{pinned:l})}catch{j(g=>g.map(u=>u.id===i.id?{...u,pinned:!l}:u)),h(l?"Could not pin that version.":"Could not unpin that version.")}},E=async i=>{try{const{data:l}=await a(`/${c}/versions/${i.id}/preview`);f({version:i,preview:l.data})}catch{h("Could not work out what restoring this version would change.")}},H=async()=>{if(o){S(!0);try{await C(`/${c}/versions/${o.version.id}/restore`,{}),window.location.reload()}catch{h("The restore failed. Nothing was changed."),S(!1),f(null)}}};return w&&p.length===0?e.jsx(t.Flex,{justifyContent:"center",padding:4,children:e.jsx(t.Loader,{small:!0,children:"Loading history"})}):x&&p.length===0?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:x}):p.length===0?e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"No versions yet. The next time this document is saved, a version will appear here."}):e.jsxs(t.Flex,{direction:"column",alignItems:"stretch",gap:2,width:"100%",children:[p.map(i=>e.jsxs(t.Box,{paddingBottom:2,children:[e.jsxs(t.Flex,{justifyContent:"space-between",alignItems:"center",gap:2,children:[e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:1,grow:1,minWidth:0,children:[e.jsxs(t.Flex,{gap:2,alignItems:"center",children:[e.jsx(t.Badge,{backgroundColor:I.has(i.origin)?"primary100":"neutral150",textColor:I.has(i.origin)?"primary600":"neutral600",children:X[i.origin]??i.origin}),i.locale?e.jsx(t.Typography,{variant:"pi",textColor:"neutral500",children:i.locale}):null]}),i.label?e.jsx(t.Box,{width:"100%",children:e.jsx(t.Typography,{variant:"omega",textColor:"neutral800",ellipsis:!0,children:i.label})}):null,e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:[ee(i.createdAt),i.user?` · ${i.user.name}`:"",i.pinned?e.jsx(t.Typography,{variant:"pi",textColor:"primary600",fontWeight:"bold",children:" · Pinned"}):null]}),e.jsx(t.TextButton,{onClick:()=>A(i),children:"What changed"})]}),e.jsxs(t.Flex,{shrink:0,gap:1,alignItems:"center",children:[e.jsx(t.IconButton,{size:"S",variant:"ghost",label:i.pinned?"Unpin this version, so tidying up can remove it":"Pin this version, so tidying up never removes it",onClick:()=>q(i),children:e.jsx(G.Pin,{fill:i.pinned?"primary600":"neutral500"})}),e.jsx(t.Button,{size:"S",variant:"tertiary",onClick:()=>E(i),children:"Restore"})]})]}),e.jsx(t.Box,{paddingTop:2,children:e.jsx(t.Divider,{})})]},i.id)),p.length<B?e.jsx(t.Button,{size:"S",variant:"tertiary",loading:w,onClick:()=>v(M+1),children:"Show older versions"}):null,x?e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:x}):null,_?e.jsx(Q,{diff:$,loading:N,onClose:()=>k(!1)}):null,e.jsx(t.Dialog.Root,{open:o!==null,onOpenChange:()=>f(null),children:e.jsxs(t.Dialog.Content,{children:[e.jsx(t.Dialog.Header,{children:"Restore this version?"}),e.jsx(t.Dialog.Body,{children:e.jsxs(t.Flex,{direction:"column",alignItems:"flex-start",gap:2,children:[e.jsx(t.Typography,{children:"The current draft is saved as a version first, so this can be undone. The published version is not touched."}),o?.preview.crossLocaleFields.length?e.jsxs(t.Typography,{textColor:"danger600",children:[o.preview.crossLocaleFields.join(", ")," ",o.preview.crossLocaleFields.length===1?"is":"are"," not translated per locale, so restoring"," ",o.preview.crossLocaleFields.length===1?"it":"them"," changes"," ",o.preview.affectedLocales.length," locales at once:"," ",o.preview.affectedLocales.join(", "),"."]}):null,o?.preview.fieldsKeptAsIs.length?e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:["Added since this version and left untouched:"," ",o.preview.fieldsKeptAsIs.join(", "),"."]}):null,o?.preview.fieldsDropped.length?e.jsxs(t.Typography,{variant:"pi",textColor:"neutral600",children:["No longer in the model and skipped: ",o.preview.fieldsDropped.join(", "),"."]}):null,o?.preview.brokenRelations.length||o?.preview.missingMedia.length?e.jsxs(t.Typography,{variant:"pi",textColor:"warning600",children:[(o.preview.brokenRelations.length??0)+(o.preview.missingMedia.length??0)," ","linked item(s) no longer exist and will be left out."]}):null]})}),e.jsxs(t.Dialog.Footer,{children:[e.jsx(t.Dialog.Cancel,{children:e.jsx(t.Button,{variant:"tertiary",disabled:b,children:"Cancel"})}),e.jsx(t.Button,{onClick:H,loading:b,children:"Restore"})]})]})})]})},ne=({model:n,documentId:r,document:s})=>r?{title:"History",content:e.jsx(te,{model:n,documentId:r,updatedAt:s?.updatedAt})}:null,re={register(n){n.registerPlugin({id:c,name:c,isReady:!0})},bootstrap(n){n.getPlugin("content-manager").apis.addEditViewSidePanel([ne])},async registerTrads({locales:n}){return Promise.all(n.map(async r=>{try{const{default:s}=await K(Object.assign({"./translations/en.json":()=>Promise.resolve().then(()=>require("../_chunks/en-BHfx2OWt.js"))}),`./translations/${r}.json`,3);return{data:s,locale:r}}catch{return{data:{},locale:r}}}))}};exports.PLUGIN_ID=c;exports.default=re;exports.registerDiffRenderer=V;
|
package/dist/admin/index.mjs
CHANGED
|
@@ -1,71 +1,72 @@
|
|
|
1
1
|
import { jsx as t, jsxs as o } from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
3
|
-
import { useFetchClient as
|
|
4
|
-
import { Modal as g, Flex as
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import * as c from "react";
|
|
3
|
+
import { useFetchClient as Z } from "@strapi/admin/strapi-admin";
|
|
4
|
+
import { Modal as g, Flex as u, Loader as O, Typography as i, Box as w, Button as y, Badge as Q, TextButton as X, IconButton as Y, Divider as ee, Dialog as m } from "@strapi/design-system";
|
|
5
|
+
import { Pin as te } from "@strapi/icons";
|
|
6
|
+
const ne = (e, r, a) => {
|
|
7
|
+
const s = e[r];
|
|
8
|
+
return s ? typeof s == "function" ? s() : Promise.resolve(s) : new Promise((T, b) => {
|
|
8
9
|
(typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
|
|
9
|
-
|
|
10
|
+
b.bind(
|
|
10
11
|
null,
|
|
11
12
|
new Error(
|
|
12
|
-
"Unknown variable dynamic import: " +
|
|
13
|
+
"Unknown variable dynamic import: " + r + (r.split("/").length !== a ? ". Note that variables only represent file names one level deep." : "")
|
|
13
14
|
)
|
|
14
15
|
)
|
|
15
16
|
);
|
|
16
17
|
});
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
|
|
18
|
+
}, E = /* @__PURE__ */ new Map(), me = (e, r) => {
|
|
19
|
+
E.set(e, r);
|
|
20
|
+
}, N = (e) => E.get(e), re = ({ spans: e }) => /* @__PURE__ */ t(i, { variant: "omega", tag: "p", children: e.map((r, a) => r.op === "equal" ? /* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral600", children: r.value }, a) : /* @__PURE__ */ t(
|
|
21
|
+
i,
|
|
21
22
|
{
|
|
22
23
|
variant: "omega",
|
|
23
|
-
textColor:
|
|
24
|
-
style:
|
|
25
|
-
children:
|
|
24
|
+
textColor: r.op === "added" ? "success600" : "danger600",
|
|
25
|
+
style: r.op === "removed" ? { textDecoration: "line-through" } : { fontWeight: 600 },
|
|
26
|
+
children: r.value
|
|
26
27
|
},
|
|
27
28
|
a
|
|
28
|
-
)) }),
|
|
29
|
-
const
|
|
30
|
-
return
|
|
31
|
-
e.linked?.length ? /* @__PURE__ */ o(
|
|
29
|
+
)) }), A = (e) => e == null || e === "" ? "(empty)" : typeof e == "object" ? JSON.stringify(e) : String(e), ie = ({ change: e }) => {
|
|
30
|
+
const r = (e.customField ? N(e.customField) : void 0) ?? N(e.type);
|
|
31
|
+
return r ? /* @__PURE__ */ t(r, { change: e }) : e.spans ? /* @__PURE__ */ t(re, { spans: e.spans }) : e.linked || e.unlinked ? /* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
32
|
+
e.linked?.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "success600", children: [
|
|
32
33
|
"+ ",
|
|
33
34
|
e.linked.length,
|
|
34
35
|
" linked"
|
|
35
36
|
] }) : null,
|
|
36
|
-
e.unlinked?.length ? /* @__PURE__ */ o(
|
|
37
|
+
e.unlinked?.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "danger600", children: [
|
|
37
38
|
"− ",
|
|
38
39
|
e.unlinked.length,
|
|
39
40
|
" unlinked"
|
|
40
41
|
] }) : null
|
|
41
|
-
] }) : e.before !== void 0 || e.after !== void 0 ? /* @__PURE__ */ o(
|
|
42
|
+
] }) : e.before !== void 0 || e.after !== void 0 ? /* @__PURE__ */ o(u, { gap: 2, alignItems: "center", wrap: "wrap", children: [
|
|
42
43
|
/* @__PURE__ */ t(
|
|
43
|
-
|
|
44
|
+
i,
|
|
44
45
|
{
|
|
45
46
|
variant: "omega",
|
|
46
47
|
textColor: "danger600",
|
|
47
48
|
style: { textDecoration: "line-through" },
|
|
48
|
-
children:
|
|
49
|
+
children: A(e.before)
|
|
49
50
|
}
|
|
50
51
|
),
|
|
51
|
-
/* @__PURE__ */ t(
|
|
52
|
-
/* @__PURE__ */ t(
|
|
53
|
-
] }) : /* @__PURE__ */ t(
|
|
54
|
-
},
|
|
52
|
+
/* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral500", children: "→" }),
|
|
53
|
+
/* @__PURE__ */ t(i, { variant: "omega", textColor: "success600", children: A(e.after) })
|
|
54
|
+
] }) : /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: "Formatting or structure changed; the text is the same." });
|
|
55
|
+
}, oe = ({
|
|
55
56
|
diff: e,
|
|
56
|
-
loading:
|
|
57
|
+
loading: r,
|
|
57
58
|
onClose: a
|
|
58
59
|
}) => /* @__PURE__ */ t(g.Root, { open: !0, onOpenChange: a, children: /* @__PURE__ */ o(g.Content, { children: [
|
|
59
60
|
/* @__PURE__ */ t(g.Header, { children: /* @__PURE__ */ t(g.Title, { children: "What changed" }) }),
|
|
60
|
-
/* @__PURE__ */ t(g.Body, { children:
|
|
61
|
-
/* @__PURE__ */ t(
|
|
62
|
-
e.identical ? /* @__PURE__ */ t(
|
|
63
|
-
/* @__PURE__ */ t(
|
|
64
|
-
/* @__PURE__ */ t(
|
|
61
|
+
/* @__PURE__ */ t(g.Body, { children: r ? /* @__PURE__ */ t(u, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t(O, { small: !0, children: "Comparing" }) }) : e ? /* @__PURE__ */ o(u, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
62
|
+
/* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: e.from ? "Compared with the version saved just before it." : "This is the earliest version of the document, so everything in it is new." }),
|
|
63
|
+
e.identical ? /* @__PURE__ */ t(i, { textColor: "neutral600", children: "Nothing changed between these two versions." }) : e.changes.map((s) => /* @__PURE__ */ o(w, { children: [
|
|
64
|
+
/* @__PURE__ */ t(i, { variant: "sigma", textColor: "neutral600", children: s.field }),
|
|
65
|
+
/* @__PURE__ */ t(w, { paddingTop: 1, children: /* @__PURE__ */ t(ie, { change: s }) })
|
|
65
66
|
] }, s.field))
|
|
66
|
-
] }) : /* @__PURE__ */ t(
|
|
67
|
-
/* @__PURE__ */ t(g.Footer, { children: /* @__PURE__ */ t(
|
|
68
|
-
] }) }),
|
|
67
|
+
] }) : /* @__PURE__ */ t(i, { textColor: "danger600", children: "Could not compare these versions." }) }),
|
|
68
|
+
/* @__PURE__ */ t(g.Footer, { children: /* @__PURE__ */ t(y, { variant: "tertiary", onClick: a, children: "Close" }) })
|
|
69
|
+
] }) }), p = "rewind", ae = {
|
|
69
70
|
create: "Created",
|
|
70
71
|
update: "Edited",
|
|
71
72
|
clone: "Cloned",
|
|
@@ -73,12 +74,12 @@ const Q = (e, n, a) => {
|
|
|
73
74
|
unpublish: "Unpublished",
|
|
74
75
|
discardDraft: "Draft discarded",
|
|
75
76
|
restore: "Replaced by restore"
|
|
76
|
-
},
|
|
77
|
-
const
|
|
78
|
-
return a < 1 ? "just now" : a < 60 ? `${a} min ago` : a < 60 * 24 ? `${Math.round(a / 60)} h ago` :
|
|
79
|
-
},
|
|
77
|
+
}, B = /* @__PURE__ */ new Set(["publish", "unpublish", "discardDraft", "restore"]), le = 10, se = (e) => {
|
|
78
|
+
const r = new Date(e), a = Math.round((Date.now() - r.getTime()) / 6e4);
|
|
79
|
+
return a < 1 ? "just now" : a < 60 ? `${a} min ago` : a < 60 * 24 ? `${Math.round(a / 60)} h ago` : r.toLocaleString();
|
|
80
|
+
}, de = ({
|
|
80
81
|
model: e,
|
|
81
|
-
documentId:
|
|
82
|
+
documentId: r,
|
|
82
83
|
/**
|
|
83
84
|
* Changes whenever the Content Manager re-reads the document, which is what
|
|
84
85
|
* it does after a save. Used purely as a refresh signal: without it the panel
|
|
@@ -87,102 +88,125 @@ const Q = (e, n, a) => {
|
|
|
87
88
|
*/
|
|
88
89
|
updatedAt: a
|
|
89
90
|
}) => {
|
|
90
|
-
const { get: s, post:
|
|
91
|
-
async (
|
|
92
|
-
|
|
91
|
+
const { get: s, post: T, put: b } = Z(), [f, k] = c.useState([]), [M, _] = c.useState(0), [H, W] = c.useState(1), [D, j] = c.useState(!0), [x, C] = c.useState(null), [l, S] = c.useState(null), [L, P] = c.useState(!1), [z, $] = c.useState(!1), [G, R] = c.useState(null), [U, F] = c.useState(!1), I = c.useCallback(
|
|
92
|
+
async (n) => {
|
|
93
|
+
j(!0), C(null);
|
|
93
94
|
try {
|
|
94
|
-
const { data:
|
|
95
|
+
const { data: d } = await s(`/${p}/versions`, {
|
|
95
96
|
params: {
|
|
96
97
|
contentType: e,
|
|
97
|
-
documentId:
|
|
98
|
-
page:
|
|
99
|
-
pageSize:
|
|
98
|
+
documentId: r,
|
|
99
|
+
page: n,
|
|
100
|
+
pageSize: le
|
|
100
101
|
}
|
|
101
102
|
});
|
|
102
|
-
|
|
103
|
+
k((v) => n === 1 ? d.data : [...v, ...d.data]), _(d.meta.total), W(n);
|
|
103
104
|
} catch {
|
|
104
|
-
|
|
105
|
+
C("Could not load the history for this document.");
|
|
105
106
|
} finally {
|
|
106
|
-
|
|
107
|
+
j(!1);
|
|
107
108
|
}
|
|
108
109
|
},
|
|
109
|
-
[s, e,
|
|
110
|
+
[s, e, r]
|
|
110
111
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}, [
|
|
114
|
-
const
|
|
115
|
-
|
|
112
|
+
c.useEffect(() => {
|
|
113
|
+
I(1);
|
|
114
|
+
}, [I, a]);
|
|
115
|
+
const q = async (n) => {
|
|
116
|
+
$(!0), F(!0), R(null);
|
|
116
117
|
try {
|
|
117
|
-
const { data:
|
|
118
|
-
`/${
|
|
118
|
+
const { data: d } = await s(
|
|
119
|
+
`/${p}/versions/${n.id}/diff`
|
|
119
120
|
);
|
|
120
|
-
|
|
121
|
+
R(d.data);
|
|
121
122
|
} catch {
|
|
122
|
-
|
|
123
|
+
R(null);
|
|
123
124
|
} finally {
|
|
124
|
-
|
|
125
|
+
F(!1);
|
|
125
126
|
}
|
|
126
|
-
},
|
|
127
|
+
}, K = async (n) => {
|
|
128
|
+
const d = !n.pinned;
|
|
129
|
+
k((v) => v.map((h) => h.id === n.id ? { ...h, pinned: d } : h));
|
|
127
130
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
await b(`/${p}/versions/${n.id}/pin`, { pinned: d });
|
|
132
|
+
} catch {
|
|
133
|
+
k(
|
|
134
|
+
(v) => v.map((h) => h.id === n.id ? { ...h, pinned: !d } : h)
|
|
135
|
+
), C(d ? "Could not pin that version." : "Could not unpin that version.");
|
|
136
|
+
}
|
|
137
|
+
}, V = async (n) => {
|
|
138
|
+
try {
|
|
139
|
+
const { data: d } = await s(
|
|
140
|
+
`/${p}/versions/${n.id}/preview`
|
|
130
141
|
);
|
|
131
|
-
|
|
142
|
+
S({ version: n, preview: d.data });
|
|
132
143
|
} catch {
|
|
133
|
-
|
|
144
|
+
C("Could not work out what restoring this version would change.");
|
|
134
145
|
}
|
|
135
|
-
},
|
|
146
|
+
}, J = async () => {
|
|
136
147
|
if (l) {
|
|
137
|
-
|
|
148
|
+
P(!0);
|
|
138
149
|
try {
|
|
139
|
-
await
|
|
150
|
+
await T(`/${p}/versions/${l.version.id}/restore`, {}), window.location.reload();
|
|
140
151
|
} catch {
|
|
141
|
-
|
|
152
|
+
C("The restore failed. Nothing was changed."), P(!1), S(null);
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
};
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
/* @__PURE__ */ o(
|
|
148
|
-
/* @__PURE__ */ o(
|
|
149
|
-
/* @__PURE__ */ o(
|
|
156
|
+
return D && f.length === 0 ? /* @__PURE__ */ t(u, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ t(O, { small: !0, children: "Loading history" }) }) : x && f.length === 0 ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "danger600", children: x }) : f.length === 0 ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral600", children: "No versions yet. The next time this document is saved, a version will appear here." }) : /* @__PURE__ */ o(u, { direction: "column", alignItems: "stretch", gap: 2, width: "100%", children: [
|
|
157
|
+
f.map((n) => /* @__PURE__ */ o(w, { paddingBottom: 2, children: [
|
|
158
|
+
/* @__PURE__ */ o(u, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
|
|
159
|
+
/* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 1, grow: 1, minWidth: 0, children: [
|
|
160
|
+
/* @__PURE__ */ o(u, { gap: 2, alignItems: "center", children: [
|
|
150
161
|
/* @__PURE__ */ t(
|
|
151
|
-
|
|
162
|
+
Q,
|
|
152
163
|
{
|
|
153
|
-
backgroundColor:
|
|
154
|
-
textColor:
|
|
155
|
-
children:
|
|
164
|
+
backgroundColor: B.has(n.origin) ? "primary100" : "neutral150",
|
|
165
|
+
textColor: B.has(n.origin) ? "primary600" : "neutral600",
|
|
166
|
+
children: ae[n.origin] ?? n.origin
|
|
156
167
|
}
|
|
157
168
|
),
|
|
158
|
-
|
|
169
|
+
n.locale ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "neutral500", children: n.locale }) : null
|
|
159
170
|
] }),
|
|
160
|
-
|
|
161
|
-
/* @__PURE__ */ o(
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
n.label ? /* @__PURE__ */ t(w, { width: "100%", children: /* @__PURE__ */ t(i, { variant: "omega", textColor: "neutral800", ellipsis: !0, children: n.label }) }) : null,
|
|
172
|
+
/* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
173
|
+
se(n.createdAt),
|
|
174
|
+
n.user ? ` · ${n.user.name}` : "",
|
|
175
|
+
n.pinned ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "primary600", fontWeight: "bold", children: " · Pinned" }) : null
|
|
164
176
|
] }),
|
|
165
|
-
/* @__PURE__ */ t(
|
|
177
|
+
/* @__PURE__ */ t(X, { onClick: () => q(n), children: "What changed" })
|
|
166
178
|
] }),
|
|
167
|
-
/* @__PURE__ */
|
|
179
|
+
/* @__PURE__ */ o(u, { shrink: 0, gap: 1, alignItems: "center", children: [
|
|
180
|
+
/* @__PURE__ */ t(
|
|
181
|
+
Y,
|
|
182
|
+
{
|
|
183
|
+
size: "S",
|
|
184
|
+
variant: "ghost",
|
|
185
|
+
label: n.pinned ? "Unpin this version, so tidying up can remove it" : "Pin this version, so tidying up never removes it",
|
|
186
|
+
onClick: () => K(n),
|
|
187
|
+
children: /* @__PURE__ */ t(te, { fill: n.pinned ? "primary600" : "neutral500" })
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
/* @__PURE__ */ t(y, { size: "S", variant: "tertiary", onClick: () => V(n), children: "Restore" })
|
|
191
|
+
] })
|
|
168
192
|
] }),
|
|
169
|
-
/* @__PURE__ */ t(
|
|
170
|
-
] },
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
/* @__PURE__ */ t(w, { paddingTop: 2, children: /* @__PURE__ */ t(ee, {}) })
|
|
194
|
+
] }, n.id)),
|
|
195
|
+
f.length < M ? /* @__PURE__ */ t(y, { size: "S", variant: "tertiary", loading: D, onClick: () => I(H + 1), children: "Show older versions" }) : null,
|
|
196
|
+
x ? /* @__PURE__ */ t(i, { variant: "pi", textColor: "danger600", children: x }) : null,
|
|
197
|
+
z ? /* @__PURE__ */ t(
|
|
198
|
+
oe,
|
|
175
199
|
{
|
|
176
|
-
diff:
|
|
177
|
-
loading:
|
|
178
|
-
onClose: () =>
|
|
200
|
+
diff: G,
|
|
201
|
+
loading: U,
|
|
202
|
+
onClose: () => $(!1)
|
|
179
203
|
}
|
|
180
204
|
) : null,
|
|
181
|
-
/* @__PURE__ */ t(
|
|
182
|
-
/* @__PURE__ */ t(
|
|
183
|
-
/* @__PURE__ */ t(
|
|
184
|
-
/* @__PURE__ */ t(
|
|
185
|
-
l?.preview.crossLocaleFields.length ? /* @__PURE__ */ o(
|
|
205
|
+
/* @__PURE__ */ t(m.Root, { open: l !== null, onOpenChange: () => S(null), children: /* @__PURE__ */ o(m.Content, { children: [
|
|
206
|
+
/* @__PURE__ */ t(m.Header, { children: "Restore this version?" }),
|
|
207
|
+
/* @__PURE__ */ t(m.Body, { children: /* @__PURE__ */ o(u, { direction: "column", alignItems: "flex-start", gap: 2, children: [
|
|
208
|
+
/* @__PURE__ */ t(i, { children: "The current draft is saved as a version first, so this can be undone. The published version is not touched." }),
|
|
209
|
+
l?.preview.crossLocaleFields.length ? /* @__PURE__ */ o(i, { textColor: "danger600", children: [
|
|
186
210
|
l.preview.crossLocaleFields.join(", "),
|
|
187
211
|
" ",
|
|
188
212
|
l.preview.crossLocaleFields.length === 1 ? "is" : "are",
|
|
@@ -197,58 +221,58 @@ const Q = (e, n, a) => {
|
|
|
197
221
|
l.preview.affectedLocales.join(", "),
|
|
198
222
|
"."
|
|
199
223
|
] }) : null,
|
|
200
|
-
l?.preview.fieldsKeptAsIs.length ? /* @__PURE__ */ o(
|
|
224
|
+
l?.preview.fieldsKeptAsIs.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
201
225
|
"Added since this version and left untouched:",
|
|
202
226
|
" ",
|
|
203
227
|
l.preview.fieldsKeptAsIs.join(", "),
|
|
204
228
|
"."
|
|
205
229
|
] }) : null,
|
|
206
|
-
l?.preview.fieldsDropped.length ? /* @__PURE__ */ o(
|
|
230
|
+
l?.preview.fieldsDropped.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "neutral600", children: [
|
|
207
231
|
"No longer in the model and skipped: ",
|
|
208
232
|
l.preview.fieldsDropped.join(", "),
|
|
209
233
|
"."
|
|
210
234
|
] }) : null,
|
|
211
|
-
l?.preview.brokenRelations.length || l?.preview.missingMedia.length ? /* @__PURE__ */ o(
|
|
235
|
+
l?.preview.brokenRelations.length || l?.preview.missingMedia.length ? /* @__PURE__ */ o(i, { variant: "pi", textColor: "warning600", children: [
|
|
212
236
|
(l.preview.brokenRelations.length ?? 0) + (l.preview.missingMedia.length ?? 0),
|
|
213
237
|
" ",
|
|
214
238
|
"linked item(s) no longer exist and will be left out."
|
|
215
239
|
] }) : null
|
|
216
240
|
] }) }),
|
|
217
|
-
/* @__PURE__ */ o(
|
|
218
|
-
/* @__PURE__ */ t(
|
|
219
|
-
/* @__PURE__ */ t(
|
|
241
|
+
/* @__PURE__ */ o(m.Footer, { children: [
|
|
242
|
+
/* @__PURE__ */ t(m.Cancel, { children: /* @__PURE__ */ t(y, { variant: "tertiary", disabled: L, children: "Cancel" }) }),
|
|
243
|
+
/* @__PURE__ */ t(y, { onClick: J, loading: L, children: "Restore" })
|
|
220
244
|
] })
|
|
221
245
|
] }) })
|
|
222
246
|
] });
|
|
223
|
-
},
|
|
247
|
+
}, ce = ({
|
|
224
248
|
model: e,
|
|
225
|
-
documentId:
|
|
249
|
+
documentId: r,
|
|
226
250
|
document: a
|
|
227
|
-
}) =>
|
|
251
|
+
}) => r ? {
|
|
228
252
|
title: "History",
|
|
229
|
-
content: /* @__PURE__ */ t(
|
|
230
|
-
} : null,
|
|
253
|
+
content: /* @__PURE__ */ t(de, { model: e, documentId: r, updatedAt: a?.updatedAt })
|
|
254
|
+
} : null, fe = {
|
|
231
255
|
register(e) {
|
|
232
|
-
e.registerPlugin({ id:
|
|
256
|
+
e.registerPlugin({ id: p, name: p, isReady: !0 });
|
|
233
257
|
},
|
|
234
258
|
bootstrap(e) {
|
|
235
|
-
e.getPlugin("content-manager").apis.addEditViewSidePanel([
|
|
259
|
+
e.getPlugin("content-manager").apis.addEditViewSidePanel([ce]);
|
|
236
260
|
},
|
|
237
261
|
async registerTrads({ locales: e }) {
|
|
238
262
|
return Promise.all(
|
|
239
|
-
e.map(async (
|
|
263
|
+
e.map(async (r) => {
|
|
240
264
|
try {
|
|
241
|
-
const { default: a } = await
|
|
242
|
-
return { data: a, locale:
|
|
265
|
+
const { default: a } = await ne(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-DCctmu-a.mjs") }), `./translations/${r}.json`, 3);
|
|
266
|
+
return { data: a, locale: r };
|
|
243
267
|
} catch {
|
|
244
|
-
return { data: {}, locale:
|
|
268
|
+
return { data: {}, locale: r };
|
|
245
269
|
}
|
|
246
270
|
})
|
|
247
271
|
);
|
|
248
272
|
}
|
|
249
273
|
};
|
|
250
274
|
export {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
275
|
+
p as PLUGIN_ID,
|
|
276
|
+
fe as default,
|
|
277
|
+
me as registerDiffRenderer
|
|
254
278
|
};
|
package/dist/server/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const
|
|
2
|
-
ON ${t} (content_type, related_document_id, locale, created_at)`)}catch(t){e.log.warn(`[rewind] could not create the lookup index, queries will be slower: ${t instanceof Error?t.message:String(t)}`)}},he=(e,t)=>{t.retention?.enabled!==!1&&e.cron.add({rewindPrune:{async task(){const n=e.plugin("rewind").service("lock");if(await n.acquire("prune",30*60*1e3))try{const r=await e.plugin("rewind").service("retention").prune();r.deleted>0&&e.log.info(`[rewind] thinned ${r.deleted} version(s) across ${r.documents} document(s)`)}catch(r){e.log.error("[rewind] thinning failed",r)}finally{await n.release("prune")}},options:{rule:t.cron??"0 3 * * *"}}})},we={default:{contentTypes:[],trackApiWrites:!1,retention:{enabled:!0,keepAllDays:7,dailyUntilDays:30,maxAgeDays:365,keepAnchors:!0},cron:"0 3 * * *"},validator(e={}){const{contentTypes:t,retention:n}=e;if(n){const{keepAllDays:o=7,dailyUntilDays:a=30,maxAgeDays:s=365}=n;if(!(o<=a&&a<=s))throw new Error(`[rewind] config.retention needs keepAllDays <= dailyUntilDays <= maxAgeDays (received ${o}, ${a}, ${s})`)}if(t===void 0)return;if(!Array.isArray(t)||t.some(o=>typeof o!="string"))throw new Error('[rewind] config.contentTypes must be an array of content type uids, e.g. ["api::article.article"]');const r=t.filter(o=>!o.startsWith("api::"));if(r.length>0)throw new Error(`[rewind] config.contentTypes only supports api:: content types, received: ${r.join(", ")}`)}},be="collectionType",Te="rewind_versions",ve={singularName:"version",pluralName:"versions",displayName:"Rewind Version",description:"One snapshot of one document, in one locale, at one point in time."},De={draftAndPublish:!1,comment:"Managed by strapi-plugin-rewind. Do not edit manually."},Ae={"content-manager":{visible:!1},"content-type-builder":{visible:!1},i18n:{localized:!1}},Ie={contentType:{type:"string",required:!0},relatedDocumentId:{type:"string",required:!0},locale:{type:"string"},status:{type:"enumeration",enum:["draft","published","modified"],required:!0},label:{type:"string"},data:{type:"json",required:!0},schemaSnapshot:{type:"json",required:!0},relations:{type:"json"},contentHash:{type:"string",required:!0},origin:{type:"enumeration",enum:["create","update","clone","publish","unpublish","discardDraft","restore"],required:!0},pinned:{type:"boolean",default:!1},userId:{type:"integer"}},ke={kind:be,collectionName:Te,info:ve,options:De,pluginOptions:Ae,attributes:Ie},Oe={version:{schema:ke}},Se=({strapi:e})=>{e.cron.remove("rewindPrune")},qe=["id","contentType","relatedDocumentId","locale","status","label","origin","userId","pinned","contentHash","createdAt"],Ee=({strapi:e})=>({async find(t){const{contentType:n,documentId:r,locale:o,page:a=1,pageSize:s=10}=t.query;if(!n||!r)return t.badRequest("contentType and documentId are required.");const i={contentType:n,relatedDocumentId:r,...o?{locale:o}:{}},c=Math.min(Number(s)||10,100),l=(Math.max(Number(a)||1,1)-1)*c,[d,p]=await Promise.all([e.db.query(v).findMany({select:qe,where:i,orderBy:{createdAt:"desc"},limit:c,offset:l}),e.db.query(v).count({where:i})]);t.body={data:await U(e,d),meta:{page:Number(a)||1,pageSize:c,total:p}}},async findOne(t){const n=await e.db.query(v).findOne({where:{id:Number(t.params.id)}});if(!n)return t.notFound("No such version.");t.body={data:(await U(e,[n]))[0]}},async diff(t){const{against:n}=t.query;try{t.body={data:await e.plugin("rewind").service("diff").between(Number(t.params.id),n?Number(n):void 0)}}catch(r){return t.badRequest(r instanceof Error?r.message:"Could not compare these versions.")}},async preview(t){try{t.body={data:await e.plugin("rewind").service("restore").preview(Number(t.params.id))}}catch(n){return t.badRequest(n instanceof Error?n.message:"Preview failed.")}},async restore(t){try{t.body={data:await e.plugin("rewind").service("restore").apply(Number(t.params.id),t.state?.user?.id??null)}}catch(n){return t.badRequest(n instanceof Error?n.message:"Restore failed.")}}}),U=async(e,t)=>{const n=[...new Set(t.map(a=>a.userId).filter(Boolean))];if(n.length===0)return t.map(a=>({...a,user:null}));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]));return t.map(a=>{const s=o.get(a.userId);return{...a,user:s?{id:s.id,name:[s.firstname,s.lastname].filter(Boolean).join(" ")||s.email}:null}})},Re={version:Ee},$e={type:"admin",routes:[{method:"GET",path:"/versions",handler:"version.find",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id",handler:"version.findOne",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id/diff",handler:"version.diff",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id/preview",handler:"version.preview",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"POST",path:"/versions/:id/restore",handler:"version.restore",config:{policies:["admin::isAuthenticatedAdmin"]}}]},_e={admin:$e},B=e=>e.match(/\S+\s*/g)??[],L=2500,Fe=(e,t)=>{const n=B(e),r=B(t);if(n.length>L||r.length>L)return[{op:"removed",value:e},{op:"added",value:t}];const o=Array.from({length:n.length+1},()=>new Array(r.length+1).fill(0));for(let l=n.length-1;l>=0;l-=1)for(let d=r.length-1;d>=0;d-=1)o[l][d]=n[l]===r[d]?o[l+1][d+1]+1:Math.max(o[l+1][d],o[l][d+1]);const a=[],s=(l,d)=>{const p=a[a.length-1];p&&p.op===l?p.value+=d:a.push({op:l,value:d})};let i=0,c=0;for(;i<n.length&&c<r.length;)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);for(;i<n.length;)s("removed",n[i++]);for(;c<r.length;)s("added",r[c++]);return a},Ne=new Set(["id","__component","type","format","level","language","url"]),q=(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=>q(n,t+1)).filter(Boolean).join(" "):typeof e=="object"?Object.entries(e).filter(([n])=>!Ne.has(n)).map(([,n])=>q(n,t+1)).filter(Boolean).join(" "):"",z=e=>e.replace(/\s+/g," ").trim(),je=6,Ce=(e,t=je)=>{const n=[];return e.forEach((r,o)=>{if(r.op!=="equal"){n.push(r);return}const a=r.value.match(/\S+\s*/g)??[];if(a.length<=t*2+1){n.push(r);return}const s=o===0,i=o===e.length-1,c=s?[]:a.slice(0,t),l=i?[]:a.slice(-t);c.length&&n.push({op:"equal",value:c.join("")}),n.push({op:"equal",value:" … "}),l.length&&n.push({op:"equal",value:l.join("")})}),n},Me=new Set(["string","text","integer","biginteger","float","decimal","boolean","date","datetime","time","enumeration","uid","email","password"]),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})=>{const t=o=>o&&{id:o.id,label:o.label??null,createdAt:o.createdAt,origin:o.origin},n=async o=>{const a=await e.db.query(v).findOne({where:{id:o}});if(!a)throw new Error(`No version with id ${o}`);return a},r=async o=>{const[a]=await e.db.query(v).findMany({where:{contentType:o.contentType,relatedDocumentId:o.relatedDocumentId,locale:o.locale,id:{$lt:o.id}},orderBy:{id:"desc"},limit:1});return a??null};return{async between(o,a){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=[];for(const u of new Set([...Object.keys(d),...Object.keys(l)])){const h=d[u],f=l[u];if(JSON.stringify(h)===JSON.stringify(f))continue;const w=c?.[u]?.type??"unknown",T=c?.[u]?.customField,g=h===void 0?"added":f===void 0?"removed":"changed";if(Me.has(w)&&!Ue(w,h??f)){m.push({field:u,type:w,customField:T,kind:g,before:h,after:f});continue}const D=z(q(h)),A=z(q(f));if(D===A){m.push({field:u,type:w,customField:T,kind:g,before:void 0,after:void 0});continue}m.push({field:u,type:w,customField:T,kind:g,spans:Ce(Fe(D,A))})}for(const u of new Set([...Object.keys(b),...Object.keys(p)])){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)));g.length===0&&D.length===0||m.push({field:u,type:c?.[u]?.type??"relation",kind:"changed",linked:g,unlinked:D})}return{from:i?t(i):null,to:t(s),changes:m,identical:m.length===0}}}},Le=({strapi:e})=>{const t=n=>({type:"plugin",name:"rewind",key:`lock:${n}`});return{async acquire(n,r){const o=await e.store.get(t(n));return o&&o.expires>Date.now()?!1:(await e.store.set({...t(n),value:{expires:Date.now()+r}}),!0)},async release(n){await e.store.set({...t(n),value:{expires:0}})}}},K={keepAllDays:7,dailyUntilDays:30,maxAgeDays:365,keepAnchors:!0},ze=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=>{const t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);const r=new Date(Date.UTC(t.getUTCFullYear(),0,1)),o=Math.ceil(((t.getTime()-r.getTime())/G+1)/7);return`${t.getUTCFullYear()}-W${o}`},We=(e,t,n=K)=>{const r=new Map,o=[],a=[...e].sort((s,i)=>new Date(i.createdAt).getTime()-new Date(s.createdAt).getTime());for(const s of a){if(s.pinned||n.keepAnchors&&ze.has(s.origin))continue;const i=Pe(s.createdAt,t);if(i<=n.keepAllDays)continue;if(i>n.maxAgeDays){o.push(s.id);continue}const c=new Date(s.createdAt),l=i<=n.dailyUntilDays?`d:${Ye(c)}`:`w:${He(c)}`;r.has(l)?o.push(s.id):r.set(l,s)}return o},P=500,Ke=({strapi:e})=>{const t=()=>e.db.query(v),n=async r=>{const o=await t().findMany({select:["contentType","relatedDocumentId","locale"],where:{createdAt:{$lt:r}}}),a=new Map;for(const s of o){const i=`${s.contentType}|${s.relatedDocumentId}|${s.locale??""}`;a.has(i)||a.set(i,{contentType:s.contentType,relatedDocumentId:s.relatedDocumentId,locale:s.locale??null})}return[...a.values()]};return{async prune(r=new Date){const o=e.config.get("plugin::rewind"),a={...K,...o?.retention};if(o?.retention?.enabled===!1)return{scanned:0,deleted:0,documents:0};const s=new Date(r.getTime()-a.keepAllDays*24*60*60*1e3),i=await n(s);let c=0,l=0;for(const d of i){const p=await t().findMany({select:["id","createdAt","origin","pinned"],where:{contentType:d.contentType,relatedDocumentId:d.relatedDocumentId,locale:d.locale}});c+=p.length;const b=We(p,r,a);for(let m=0;m<b.length;m+=P){const u=b.slice(m,m+P);await t().deleteMany({where:{id:{$in:u}}}),l+=u.length}}return{scanned:c,deleted:l,documents:i.length}}}},Ge=({strapi:e})=>{const t=s=>e.getModel(s),n=async s=>{const i=await e.db.query(v).findOne({where:{id:s}});if(!i)throw new Error(`No version with id ${s}`);return i},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({where:{documentId:s.documentId,...s.locale?{locale:s.locale}:{}}});return{async preview(s){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=[],_=[];for(const[y,X]of Object.entries(b))if(g.includes(y))for(const k of X)await a(k)||(k.targetUid==="plugin::upload.file"?_.push(k):A.push(k));const V=D.length?await e.plugin("i18n")?.service("locales").find():[];return{fieldsToRestore:g.filter(y=>y in p),fieldsKeptAsIs:f,fieldsDropped:h,crossLocaleFields:D,unsupportedFields:T,brokenRelations:A,missingMedia:_,affectedLocales:V.map(y=>y.code)}},async apply(s,i){const c=await n(s),l=c.contentType,d=await this.preview(s),p=[];await e.plugin("rewind").service("snapshot").capture({uid:l,relatedDocumentId:c.relatedDocumentId,locale:c.locale,origin:"restore",before:null},i);const b={},m=c.data??{},u=c.relations??{};for(const f of d.fieldsToRestore)b[f]=m[f];const h=new Set([...d.brokenRelations,...d.missingMedia].map(f=>`${f.targetUid}:${f.documentId??f.id}`));for(const[f,w]of Object.entries(u)){if(d.unsupportedFields.includes(f)||d.fieldsDropped.includes(f))continue;const T=w.filter(g=>!h.has(`${g.targetUid}:${g.documentId??g.id}`));T.length!==w.length&&p.push(`${w.length-T.length} target(s) of "${f}" no longer exist and were skipped.`),b[f]=T.map(g=>g.targetUid==="plugin::upload.file"?g.id:g.documentId)}return d.fieldsDropped.length&&p.push(`Skipped ${d.fieldsDropped.length} field(s) no longer in the model: ${d.fieldsDropped.join(", ")}.`),d.crossLocaleFields.length&&p.push(`${d.crossLocaleFields.join(", ")} are not localised, so they changed in every locale.`),await ce(async()=>{await e.documents(l).update({documentId:c.relatedDocumentId,locale:c.locale??void 0,status:"draft",data:b})}),{documentId:c.relatedDocumentId,warnings:p}}}},Ve={snapshot:ie,restore:Ge,diff:Be,retention:Ke,lock:Le},Xe={bootstrap:me,destroy:Se,config:we,contentTypes:Oe,controllers:Re,routes:_e,services:Ve};module.exports=Xe;
|
|
1
|
+
"use strict";const J=require("node:crypto"),Z=require("node:async_hooks"),Q=["id","documentId","locale"],S=["createdAt","updatedAt","publishedAt","createdBy","updatedBy","strapi_stage","strapi_assignee","locale","localizations"],I=(e,t)=>e.getModel(t),x=e=>e?.type==="relation"||e?.type==="media",$=e=>e?.type==="relation"&&String(e.relation??"").toLowerCase().startsWith("morph"),_=(e,t)=>Object.entries(I(e,t).attributes).filter(([,n])=>!["relation","media","component","dynamiczone"].includes(n.type)).map(([n])=>n),E=(e,t,n=!0)=>{const i=I(e,t),o=n?"select":"fields";return Object.entries(i.attributes).reduce((a,[s,r])=>{switch(r.type){case"relation":{if($(r))break;a[s]={[o]:["documentId","locale","publishedAt"]};break}case"media":{a[s]={[o]:["id"]};break}case"component":{a[s]={populate:E(e,r.component,n),[o]:_(e,r.component)};break}case"dynamiczone":{a[s]={on:(r.components??[]).reduce((c,l)=>(c[l]={populate:E(e,l,n),[o]:_(e,l)},c),{})};break}}return a},{})},ee=(e,t)=>{const n={},i=a=>{if(n[a])return;const s=I(e,a).attributes;n[a]=R(s,S);for(const r of Object.values(s))if(r.type==="component"&&i(r.component),r.type==="dynamiczone")for(const c of r.components??[])i(c)},o=I(e,t).attributes;for(const a of Object.values(o))if(a.type==="component"&&i(a.component),a.type==="dynamiczone")for(const s of a.components??[])i(s);return{attributes:R(o,S),components:n}},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(o=>n?{id:o.id,targetUid:t}:{documentId:o.documentId,locale:o.locale??null,targetUid:t}),ne=(e,t,n)=>{const i=I(e,t).attributes,o={},a={},s=R(n,[...S,...Q]);for(const[r,c]of Object.entries(s)){const l=i[r];if(l&&x(l)){if($(l))continue;const d=l.type==="media"?"plugin::upload.file":l.target;a[r]=te(c,d,l.type==="media");continue}o[r]=l?Y(e,l,c):c}return{data:o,relations:a,schemaSnapshot:ee(e,t)}},Y=(e,t,n)=>{if(n==null)return n;if(t.type==="component"){const i=o=>F(e,t.component,o);return Array.isArray(n)?n.map(o=>i(o)):i(n)}return t.type==="dynamiczone"&&Array.isArray(n)?n.map(i=>{const o=i;return F(e,o.__component,o)}):n},F=(e,t,n)=>{if(!n||!t)return n;const i=I(e,t).attributes,o={};for(const[a,s]of Object.entries(n)){if(a==="id")continue;const r=i[a];o[a]=r?Y(e,r,s):s}return o},H=e=>{const t=new Map,n=async i=>{if(t.has(i))return t.get(i);let o;try{o=(await e.plugin("content-manager").service("content-types").findConfiguration(e.contentTypes[i]))?.settings?.mainField}catch{}return t.set(i,o),o};return async(i,o)=>{if(!o)return null;const s=[await n(i),"title","name","label"].filter(Boolean);for(const r of s){const c=o[r];if(typeof c=="string"&&c.trim())return c.trim().slice(0,255)}return null}},v="plugin::rewind.version",oe=new Set(["publish","unpublish","discardDraft","restore"]),se=e=>J.createHash("sha256").update(JSON.stringify(e)).digest("hex"),ie=({strapi:e})=>{const t=H(e),n=r=>!!e.contentTypes[r]?.options?.draftAndPublish,i=r=>!!(e.getModel(r)?.pluginOptions?.i18n?.localized??e.contentTypes[r]?.pluginOptions?.i18n?.localized),o=async(r,c,l)=>{const d=i(r)&&l.filter(Boolean).length>0?{locale:{$in:l.filter(Boolean)}}:{};return e.db.query(r).findMany({where:{documentId:c,...d,...n(r)?{publishedAt:null}:{}},populate:E(e,r)})},a=async(r,c)=>{try{const l=e.plugin("content-manager").service("document-metadata"),d=await l.getMetadata(r,c);return l.getStatus(c,d.availableStatus)}catch{return"draft"}},s=async(r,c,l)=>e.db.query(v).findOne({where:{contentType:r,relatedDocumentId:c,locale:l},orderBy:{createdAt:"desc"}});return{readRows:o,async captureAll(r,c){for(const l of r)await this.capture(l,c)},async capture(r,c){const{uid:l,relatedDocumentId:d,locale:p,origin:b}=r,m=r.before??await o(l,d,[p]);for(const u of m){const h=u.locale??null,{data:f,relations:w,schemaSnapshot:T}=ne(e,l,u),g=se({data:f,relations:w});!oe.has(b)&&(await s(l,d,h))?.contentHash===g||await e.db.query(v).create({data:{contentType:l,relatedDocumentId:d,locale:h,status:await a(l,u),label:await t(l,f),data:f,relations:w,schemaSnapshot:T,contentHash:g,origin:b,userId:c,pinned:!1}})}}}},j=["restore","publish","unpublish","discardDraft","clone","create","update"],C=new WeakMap,re=e=>{let t=C.get(e);return t||(t={intents:[],flushRegistered:!1},C.set(e,t)),t},ae=e=>{const t=new Map;for(const n of e){const i=`${n.uid}:${n.relatedDocumentId}:${n.locale??""}`,o=t.get(i);if(!o){t.set(i,n);continue}const a=j.indexOf(n.origin)<j.indexOf(o.origin);t.set(i,{...a?n:o,before:o.before??n.before})}return[...t.values()]},W=new Z.AsyncLocalStorage,ce=e=>W.run({active:!0},e),le=()=>W.getStore()?.active===!0,de=200,ue=async(e,t)=>{const n=e.db.query(t),i=await n.count({where:{label:null}});if(i===0)return 0;e.log.info(`[rewind] labelling ${i} older version(s)…`);const o=H(e);let a=0;for(;;){const s=await n.findMany({select:["id","contentType","data"],where:{label:null},limit:de});if(s.length===0)break;for(const r of s){const c=await o(r.contentType,r.data);await n.update({where:{id:r.id},data:{label:c??""}}),a+=1}}return e.log.info(`[rewind] labelled ${a} older version(s)`),a},M={type:"core",key:"persisted_tables"},fe=async(e,t)=>{const n=await e.store.get(M)??[];n.map(o=>typeof o=="string"?o:o.name).includes(t)||await e.store.set({...M,value:[...n,{name:t}]})},pe=["create","update","clone","publish","unpublish","discardDraft"],me=async({strapi:e})=>{const t=e.config.get("plugin::rewind");await fe(e,"rewind_versions"),await ye(e),ue(e,v).catch(o=>e.log.warn(`[rewind] could not label older versions: ${o instanceof Error?o.message:String(o)}`)),he(e,t);const n=()=>e.plugin("rewind").service("snapshot"),i=o=>{if(!pe.includes(o.action))return!1;const a=o.contentType?.uid??o.uid??"";return!(!a.startsWith("api::")||!t.contentTypes.includes(a)||le()||!t.trackApiWrites&&!e.requestContext.get()?.request?.url?.startsWith("/content-manager"))};e.documents.use(async(o,a)=>{if(!i(o))return a();const s=o.contentType?.uid??o.uid,r=o.params?.locale??null,c=o.action==="discardDraft"&&o.params?.documentId?await n().readRows(s,o.params.documentId,[r]):null,l=await a(),d=o.action==="create"||o.action==="clone"?l?.documentId:o.params?.documentId;if(!d)return l;const p=ge(e);return await e.db.transaction(async({trx:b,onCommit:m})=>{const u=re(b);u.intents.push({uid:s,relatedDocumentId:d,locale:r,origin:o.action,before:c}),!u.flushRegistered&&(u.flushRegistered=!0,m(()=>{const h=u.intents.splice(0,u.intents.length);n().captureAll(ae(h),p).catch(f=>e.log.error("[rewind] failed to record a version",f))}))}),l})},ge=e=>e.requestContext.get()?.state?.user?.id??null,ye=async e=>{try{const{tableName:t}=e.db.metadata.get(v);await e.db.connection.raw(`CREATE INDEX IF NOT EXISTS idx_rewind_lookup
|
|
2
|
+
ON ${t} (content_type, related_document_id, locale, created_at)`)}catch(t){e.log.warn(`[rewind] could not create the lookup index, queries will be slower: ${t instanceof Error?t.message:String(t)}`)}},he=(e,t)=>{t.retention?.enabled!==!1&&e.cron.add({rewindPrune:{async task(){const n=e.plugin("rewind").service("lock");if(await n.acquire("prune",30*60*1e3))try{const i=await e.plugin("rewind").service("retention").prune();i.deleted>0&&e.log.info(`[rewind] thinned ${i.deleted} version(s) across ${i.documents} document(s)`)}catch(i){e.log.error("[rewind] thinning failed",i)}finally{await n.release("prune")}},options:{rule:t.cron??"0 3 * * *"}}})},we={default:{contentTypes:[],trackApiWrites:!1,retention:{enabled:!0,keepAllDays:7,dailyUntilDays:30,maxAgeDays:365,keepAnchors:!0},cron:"0 3 * * *"},validator(e={}){const{contentTypes:t,retention:n}=e;if(n){const{keepAllDays:o=7,dailyUntilDays:a=30,maxAgeDays:s=365}=n;if(!(o<=a&&a<=s))throw new Error(`[rewind] config.retention needs keepAllDays <= dailyUntilDays <= maxAgeDays (received ${o}, ${a}, ${s})`)}if(t===void 0)return;if(!Array.isArray(t)||t.some(o=>typeof o!="string"))throw new Error('[rewind] config.contentTypes must be an array of content type uids, e.g. ["api::article.article"]');const i=t.filter(o=>!o.startsWith("api::"));if(i.length>0)throw new Error(`[rewind] config.contentTypes only supports api:: content types, received: ${i.join(", ")}`)}},be="collectionType",Te="rewind_versions",ve={singularName:"version",pluralName:"versions",displayName:"Rewind Version",description:"One snapshot of one document, in one locale, at one point in time."},De={draftAndPublish:!1,comment:"Managed by strapi-plugin-rewind. Do not edit manually."},Ae={"content-manager":{visible:!1},"content-type-builder":{visible:!1},i18n:{localized:!1}},Ie={contentType:{type:"string",required:!0},relatedDocumentId:{type:"string",required:!0},locale:{type:"string"},status:{type:"enumeration",enum:["draft","published","modified"],required:!0},label:{type:"string"},data:{type:"json",required:!0},schemaSnapshot:{type:"json",required:!0},relations:{type:"json"},contentHash:{type:"string",required:!0},origin:{type:"enumeration",enum:["create","update","clone","publish","unpublish","discardDraft","restore"],required:!0},pinned:{type:"boolean",default:!1},userId:{type:"integer"}},ke={kind:be,collectionName:Te,info:ve,options:De,pluginOptions:Ae,attributes:Ie},Oe={version:{schema:ke}},Se=({strapi:e})=>{e.cron.remove("rewindPrune")},qe=["id","contentType","relatedDocumentId","locale","status","label","origin","userId","pinned","contentHash","createdAt"],Ee=({strapi:e})=>({async find(t){const{contentType:n,documentId:i,locale:o,page:a=1,pageSize:s=10}=t.query;if(!n||!i)return t.badRequest("contentType and documentId are required.");const r={contentType:n,relatedDocumentId:i,...o?{locale:o}:{}},c=Math.min(Number(s)||10,100),l=(Math.max(Number(a)||1,1)-1)*c,[d,p]=await Promise.all([e.db.query(v).findMany({select:qe,where:r,orderBy:{createdAt:"desc"},limit:c,offset:l}),e.db.query(v).count({where:r})]);t.body={data:await U(e,d),meta:{page:Number(a)||1,pageSize:c,total:p}}},async findOne(t){const n=await e.db.query(v).findOne({where:{id:Number(t.params.id)}});if(!n)return t.notFound("No such version.");t.body={data:(await U(e,[n]))[0]}},async diff(t){const{against:n}=t.query;try{t.body={data:await e.plugin("rewind").service("diff").between(Number(t.params.id),n?Number(n):void 0)}}catch(i){return t.badRequest(i instanceof Error?i.message:"Could not compare these versions.")}},async preview(t){try{t.body={data:await e.plugin("rewind").service("restore").preview(Number(t.params.id))}}catch(n){return t.badRequest(n instanceof Error?n.message:"Preview failed.")}},async pin(t){const{pinned:n}=t.request.body??{};if(typeof n!="boolean")return t.badRequest("pinned must be true or false.");try{t.body={data:await e.plugin("rewind").service("pin").set(Number(t.params.id),n)}}catch(i){return t.notFound(i instanceof Error?i.message:"No such version.")}},async restore(t){try{t.body={data:await e.plugin("rewind").service("restore").apply(Number(t.params.id),t.state?.user?.id??null)}}catch(n){return t.badRequest(n instanceof Error?n.message:"Restore failed.")}}}),U=async(e,t)=>{const n=[...new Set(t.map(a=>a.userId).filter(Boolean))];if(n.length===0)return t.map(a=>({...a,user:null}));const i=await e.db.query("admin::user").findMany({select:["id","firstname","lastname","email"],where:{id:n}}),o=new Map(i.map(a=>[a.id,a]));return t.map(a=>{const s=o.get(a.userId);return{...a,user:s?{id:s.id,name:[s.firstname,s.lastname].filter(Boolean).join(" ")||s.email}:null}})},Re={version:Ee},$e={type:"admin",routes:[{method:"GET",path:"/versions",handler:"version.find",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id",handler:"version.findOne",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id/diff",handler:"version.diff",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"GET",path:"/versions/:id/preview",handler:"version.preview",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"PUT",path:"/versions/:id/pin",handler:"version.pin",config:{policies:["admin::isAuthenticatedAdmin"]}},{method:"POST",path:"/versions/:id/restore",handler:"version.restore",config:{policies:["admin::isAuthenticatedAdmin"]}}]},Ne={admin:$e},B=e=>e.match(/\S+\s*/g)??[],L=2500,_e=(e,t)=>{const n=B(e),i=B(t);if(n.length>L||i.length>L)return[{op:"removed",value:e},{op:"added",value:t}];const o=Array.from({length:n.length+1},()=>new Array(i.length+1).fill(0));for(let l=n.length-1;l>=0;l-=1)for(let d=i.length-1;d>=0;d-=1)o[l][d]=n[l]===i[d]?o[l+1][d+1]+1:Math.max(o[l+1][d],o[l][d+1]);const a=[],s=(l,d)=>{const p=a[a.length-1];p&&p.op===l?p.value+=d:a.push({op:l,value:d})};let r=0,c=0;for(;r<n.length&&c<i.length;)n[r]===i[c]?(s("equal",n[r]),r+=1,c+=1):o[r+1][c]>=o[r][c+1]?(s("removed",n[r]),r+=1):(s("added",i[c]),c+=1);for(;r<n.length;)s("removed",n[r++]);for(;c<i.length;)s("added",i[c++]);return a},Fe=new Set(["id","__component","type","format","level","language","url"]),q=(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=>q(n,t+1)).filter(Boolean).join(" "):typeof e=="object"?Object.entries(e).filter(([n])=>!Fe.has(n)).map(([,n])=>q(n,t+1)).filter(Boolean).join(" "):"",z=e=>e.replace(/\s+/g," ").trim(),je=6,Ce=(e,t=je)=>{const n=[];return e.forEach((i,o)=>{if(i.op!=="equal"){n.push(i);return}const a=i.value.match(/\S+\s*/g)??[];if(a.length<=t*2+1){n.push(i);return}const s=o===0,r=o===e.length-1,c=s?[]:a.slice(0,t),l=r?[]:a.slice(-t);c.length&&n.push({op:"equal",value:c.join("")}),n.push({op:"equal",value:" … "}),l.length&&n.push({op:"equal",value:l.join("")})}),n},Me=new Set(["string","text","integer","biginteger","float","decimal","boolean","date","datetime","time","enumeration","uid","email","password"]),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})=>{const t=o=>o&&{id:o.id,label:o.label??null,createdAt:o.createdAt,origin:o.origin},n=async o=>{const a=await e.db.query(v).findOne({where:{id:o}});if(!a)throw new Error(`No version with id ${o}`);return a},i=async o=>{const[a]=await e.db.query(v).findMany({where:{contentType:o.contentType,relatedDocumentId:o.relatedDocumentId,locale:o.locale,id:{$lt:o.id}},orderBy:{id:"desc"},limit:1});return a??null};return{async between(o,a){const s=await n(o),r=a?await n(a):await i(s),c=e.getModel(s.contentType)?.attributes,l=s.data??{},d=r?.data??{},p=s.relations??{},b=r?.relations??{},m=[];for(const u of new Set([...Object.keys(d),...Object.keys(l)])){const h=d[u],f=l[u];if(JSON.stringify(h)===JSON.stringify(f))continue;const w=c?.[u]?.type??"unknown",T=c?.[u]?.customField,g=h===void 0?"added":f===void 0?"removed":"changed";if(Me.has(w)&&!Ue(w,h??f)){m.push({field:u,type:w,customField:T,kind:g,before:h,after:f});continue}const D=z(q(h)),A=z(q(f));if(D===A){m.push({field:u,type:w,customField:T,kind:g,before:void 0,after:void 0});continue}m.push({field:u,type:w,customField:T,kind:g,spans:Ce(_e(D,A))})}for(const u of new Set([...Object.keys(b),...Object.keys(p)])){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)));g.length===0&&D.length===0||m.push({field:u,type:c?.[u]?.type??"relation",kind:"changed",linked:g,unlinked:D})}return{from:r?t(r):null,to:t(s),changes:m,identical:m.length===0}}}},Le=({strapi:e})=>{const t=n=>({type:"plugin",name:"rewind",key:`lock:${n}`});return{async acquire(n,i){const o=await e.store.get(t(n));return o&&o.expires>Date.now()?!1:(await e.store.set({...t(n),value:{expires:Date.now()+i}}),!0)},async release(n){await e.store.set({...t(n),value:{expires:0}})}}},ze=({strapi:e})=>({async set(t,n){if(!Number.isInteger(t))throw new Error("A version id is required.");const i=await e.db.query(v).findOne({where:{id:t},select:["id","pinned"]});if(!i)throw new Error(`No version with id ${t}`);return!!i.pinned===n?{id:t,pinned:n}:(await e.db.query(v).update({where:{id:t},data:{pinned:n}}),{id:t,pinned:n})}}),K={keepAllDays:7,dailyUntilDays:30,maxAgeDays:365,keepAnchors:!0},Pe=new Set(["publish","unpublish","discardDraft","restore"]),G=24*60*60*1e3,Ye=(e,t)=>(t.getTime()-new Date(e).getTime())/G,He=e=>e.toISOString().slice(0,10),We=e=>{const t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);const i=new Date(Date.UTC(t.getUTCFullYear(),0,1)),o=Math.ceil(((t.getTime()-i.getTime())/G+1)/7);return`${t.getUTCFullYear()}-W${o}`},Ke=(e,t,n=K)=>{const i=new Map,o=[],a=[...e].sort((s,r)=>new Date(r.createdAt).getTime()-new Date(s.createdAt).getTime());for(const s of a){if(s.pinned||n.keepAnchors&&Pe.has(s.origin))continue;const r=Ye(s.createdAt,t);if(r<=n.keepAllDays)continue;if(r>n.maxAgeDays){o.push(s.id);continue}const c=new Date(s.createdAt),l=r<=n.dailyUntilDays?`d:${He(c)}`:`w:${We(c)}`;i.has(l)?o.push(s.id):i.set(l,s)}return o},P=500,Ge=({strapi:e})=>{const t=()=>e.db.query(v),n=async i=>{const o=await t().findMany({select:["contentType","relatedDocumentId","locale"],where:{createdAt:{$lt:i}}}),a=new Map;for(const s of o){const r=`${s.contentType}|${s.relatedDocumentId}|${s.locale??""}`;a.has(r)||a.set(r,{contentType:s.contentType,relatedDocumentId:s.relatedDocumentId,locale:s.locale??null})}return[...a.values()]};return{async prune(i=new Date){const o=e.config.get("plugin::rewind"),a={...K,...o?.retention};if(o?.retention?.enabled===!1)return{scanned:0,deleted:0,documents:0};const s=new Date(i.getTime()-a.keepAllDays*24*60*60*1e3),r=await n(s);let c=0,l=0;for(const d of r){const p=await t().findMany({select:["id","createdAt","origin","pinned"],where:{contentType:d.contentType,relatedDocumentId:d.relatedDocumentId,locale:d.locale}});c+=p.length;const b=Ke(p,i,a);for(let m=0;m<b.length;m+=P){const u=b.slice(m,m+P);await t().deleteMany({where:{id:{$in:u}}}),l+=u.length}}return{scanned:c,deleted:l,documents:r.length}}}},Ve=({strapi:e})=>{const t=s=>e.getModel(s),n=async s=>{const r=await e.db.query(v).findOne({where:{id:s}});if(!r)throw new Error(`No version with id ${s}`);return r},i=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({where:{documentId:s.documentId,...s.locale?{locale:s.locale}:{}}});return{async preview(s){const r=await n(s),c=r.contentType,l=t(c).attributes,d=r.schemaSnapshot?.attributes??{},p=r.data??{},b=r.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=>!i(l[y])):[],A=[],N=[];for(const[y,X]of Object.entries(b))if(g.includes(y))for(const k of X)await a(k)||(k.targetUid==="plugin::upload.file"?N.push(k):A.push(k));const V=D.length?await e.plugin("i18n")?.service("locales").find():[];return{fieldsToRestore:g.filter(y=>y in p),fieldsKeptAsIs:f,fieldsDropped:h,crossLocaleFields:D,unsupportedFields:T,brokenRelations:A,missingMedia:N,affectedLocales:V.map(y=>y.code)}},async apply(s,r){const c=await n(s),l=c.contentType,d=await this.preview(s),p=[];await e.plugin("rewind").service("snapshot").capture({uid:l,relatedDocumentId:c.relatedDocumentId,locale:c.locale,origin:"restore",before:null},r);const b={},m=c.data??{},u=c.relations??{};for(const f of d.fieldsToRestore)b[f]=m[f];const h=new Set([...d.brokenRelations,...d.missingMedia].map(f=>`${f.targetUid}:${f.documentId??f.id}`));for(const[f,w]of Object.entries(u)){if(d.unsupportedFields.includes(f)||d.fieldsDropped.includes(f))continue;const T=w.filter(g=>!h.has(`${g.targetUid}:${g.documentId??g.id}`));T.length!==w.length&&p.push(`${w.length-T.length} target(s) of "${f}" no longer exist and were skipped.`),b[f]=T.map(g=>g.targetUid==="plugin::upload.file"?g.id:g.documentId)}return d.fieldsDropped.length&&p.push(`Skipped ${d.fieldsDropped.length} field(s) no longer in the model: ${d.fieldsDropped.join(", ")}.`),d.crossLocaleFields.length&&p.push(`${d.crossLocaleFields.join(", ")} are not localised, so they changed in every locale.`),await ce(async()=>{await e.documents(l).update({documentId:c.relatedDocumentId,locale:c.locale??void 0,status:"draft",data:b})}),{documentId:c.relatedDocumentId,warnings:p}}}},Xe={snapshot:ie,restore:Ve,diff:Be,retention:Ge,lock:Le,pin:ze},Je={bootstrap:me,destroy:Se,config:we,contentTypes:Oe,controllers:Re,routes:Ne,services:Xe};module.exports=Je;
|