@qkix/strapi-plugin-rewind 0.2.1 → 0.4.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.
@@ -14,8 +14,29 @@ export interface VersionDiff {
14
14
  changes: FieldChange[];
15
15
  identical: boolean;
16
16
  }
17
- export declare const ChangesDialog: ({ diff, loading, onClose, }: {
17
+ /**
18
+ * The comparison target lives here rather than in the panel.
19
+ *
20
+ * Picking two versions needs somewhere to put the second choice, and the panel
21
+ * is a narrow column whose rows already carry a badge, a title, a timestamp, a
22
+ * pin and a Restore button. Putting the choice beside the result also means the
23
+ * common case - compare with the one before - stays a single click, and the
24
+ * other question only costs a dropdown once you are already looking at a diff.
25
+ */
26
+ export declare const ChangesDialog: ({ diff, loading, onClose, subject, options, againstId, onChangeAgainst, hasUnloadedVersions, }: {
18
27
  diff: VersionDiff | null;
19
28
  loading: boolean;
20
29
  onClose: () => void;
30
+ /** How the version this was opened from reads, e.g. "Edited · 2 min ago". */
31
+ subject: string;
32
+ /** Every other loaded version of the same document, already described. */
33
+ options: Array<{
34
+ id: number;
35
+ name: string;
36
+ }>;
37
+ /** null means the version saved immediately before the subject. */
38
+ againstId: number | null;
39
+ onChangeAgainst: (againstId: number | null) => void;
40
+ /** Whether the panel is still holding older versions back behind its pager. */
41
+ hasUnloadedVersions: boolean;
21
42
  }) => React.JSX.Element;
@@ -4,6 +4,20 @@ export interface DiffSpan {
4
4
  op: DiffOp;
5
5
  value: string;
6
6
  }
7
+ /** One end of a relation, as a version recorded it. */
8
+ export interface RelationRef {
9
+ documentId?: string;
10
+ /** Media is addressed by numeric id rather than by documentId. */
11
+ id?: number;
12
+ targetUid: string;
13
+ /**
14
+ * What the target was called when the version was taken.
15
+ *
16
+ * Absent on versions written before Rewind recorded it, and on a target whose
17
+ * type has no field that reads as a name - so always render a fallback.
18
+ */
19
+ label?: string;
20
+ }
7
21
  export interface FieldChange {
8
22
  field: string;
9
23
  type: string;
@@ -13,16 +27,8 @@ export interface FieldChange {
13
27
  before?: unknown;
14
28
  after?: unknown;
15
29
  spans?: DiffSpan[];
16
- linked?: {
17
- documentId?: string;
18
- id?: number;
19
- targetUid: string;
20
- }[];
21
- unlinked?: {
22
- documentId?: string;
23
- id?: number;
24
- targetUid: string;
25
- }[];
30
+ linked?: RelationRef[];
31
+ unlinked?: RelationRef[];
26
32
  }
27
33
  export type DiffRenderer = React.ComponentType<{
28
34
  change: FieldChange;
@@ -1,2 +1,2 @@
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;
1
+ "use strict";const x=require("node:crypto"),ee=require("node:async_hooks"),W=e=>{const t=new Map;return async n=>{if(t.has(n))return t.get(n);let i;try{i=(await e.plugin("content-manager").service("content-types").findConfiguration(e.contentTypes[n]))?.settings?.mainField}catch{}return t.set(n,i),i}},K=["title","name","label"],G=e=>{const t=W(e);return async(n,i)=>{if(!i)return null;const r=[await t(n),...K].filter(Boolean);for(const s of r){const a=i[s];if(typeof a=="string"&&a.trim())return a.trim().slice(0,255)}return null}},te=["id","documentId","locale"],q=["createdAt","updatedAt","publishedAt","createdBy","updatedBy","strapi_stage","strapi_assignee","locale","localizations"],S=(e,t)=>e.getModel(t),ne=e=>e?.type==="relation"||e?.type==="media",$=e=>e?.type==="relation"&&String(e.relation??"").toLowerCase().startsWith("morph"),_="plugin::upload.file",oe=new Set(["string","uid","email","text","enumeration"]),se=async(e,t,n)=>{const i=new Map,o=async r=>{if(!r||i.has(r))return;let s;try{s=S(e,r).attributes}catch{return}const a=d=>!!d&&oe.has(s[d]?.type),l=[await n(r),...K].find(a);l&&i.set(r,l)};for(const r of Object.values(S(e,t).attributes))r?.type==="media"?await o(_):r?.type==="relation"&&!$(r)&&await o(r.target);return i},ie=e=>Object.fromEntries(Object.entries(e).map(([t,n])=>[t,n.map(({label:i,...o})=>o)])),j=(e,t)=>Object.entries(S(e,t).attributes).filter(([,n])=>!["relation","media","component","dynamiczone"].includes(n.type)).map(([n])=>n),F=(e,t,n=!0,i)=>{const o=S(e,t),r=n?"select":"fields",s=a=>{const l=i?.get(a);return l?[l]:[]};return Object.entries(o.attributes).reduce((a,[l,d])=>{switch(d.type){case"relation":{if($(d))break;a[l]={[r]:["documentId","locale","publishedAt",...s(d.target)]};break}case"media":{a[l]={[r]:["id",...s(_)]};break}case"component":{a[l]={populate:F(e,d.component,n),[r]:j(e,d.component)};break}case"dynamiczone":{a[l]={on:(d.components??[]).reduce((c,u)=>(c[u]={populate:F(e,u,n),[r]:j(e,u)},c),{})};break}}return a},{})},re=(e,t)=>{const n={},i=r=>{if(n[r])return;const s=S(e,r).attributes;n[r]=N(s,q);for(const a of Object.values(s))if(a.type==="component"&&i(a.component),a.type==="dynamiczone")for(const l of a.components??[])i(l)},o=S(e,t).attributes;for(const r of Object.values(o))if(r.type==="component"&&i(r.component),r.type==="dynamiczone")for(const s of r.components??[])i(s);return{attributes:N(o,q),components:n}},N=(e,t)=>Object.fromEntries(Object.entries(e??{}).filter(([n])=>!t.includes(n))),ae=(e,t,n,i)=>{const o=Array.isArray(e)?e:[e],r=s=>{const a=i?s[i]:void 0;return typeof a=="string"&&a.trim()?{label:a.trim().slice(0,255)}:{}};return o.filter(s=>!!s).map(s=>n?{id:s.id,targetUid:t,...r(s)}:{documentId:s.documentId,locale:s.locale??null,targetUid:t,...r(s)})},ce=(e,t,n,i)=>{const o=S(e,t).attributes,r={},s={},a=N(n,[...q,...te]);for(const[l,d]of Object.entries(a)){const c=o[l];if(c&&ne(c)){if($(c))continue;const u=c.type==="media"?_:c.target;s[l]=ae(d,u,c.type==="media",i?.get(u));continue}r[l]=c?V(e,c,d):d}return{data:r,relations:s,schemaSnapshot:re(e,t)}},V=(e,t,n)=>{if(n==null)return n;if(t.type==="component"){const i=o=>M(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 M(e,o.__component,o)}):n},M=(e,t,n)=>{if(!n||!t)return n;const i=S(e,t).attributes,o={};for(const[r,s]of Object.entries(n)){if(r==="id")continue;const a=i[r];o[r]=a?V(e,a,s):s}return o},D="plugin::rewind.version",le=new Set(["publish","unpublish","discardDraft","restore"]),de=e=>x.createHash("sha256").update(JSON.stringify(e)).digest("hex"),ue=({strapi:e})=>{const t=G(e),n=W(e),i=new Map,o=c=>{let u=i.get(c);return u||(u=se(e,c,n),i.set(c,u)),u},r=c=>!!e.contentTypes[c]?.options?.draftAndPublish,s=c=>!!(e.getModel(c)?.pluginOptions?.i18n?.localized??e.contentTypes[c]?.pluginOptions?.i18n?.localized),a=async(c,u,f)=>{const y=s(c)&&f.filter(Boolean).length>0?{locale:{$in:f.filter(Boolean)}}:{};return e.db.query(c).findMany({where:{documentId:u,...y,...r(c)?{publishedAt:null}:{}},populate:F(e,c,!0,await o(c))})},l=async(c,u)=>{try{const f=e.plugin("content-manager").service("document-metadata"),y=await f.getMetadata(c,u);return f.getStatus(u,y.availableStatus)}catch{return"draft"}},d=async(c,u,f)=>e.db.query(D).findOne({where:{contentType:c,relatedDocumentId:u,locale:f},orderBy:{createdAt:"desc"}});return{readRows:a,async captureAll(c,u){for(const f of c)await this.capture(f,u)},async capture(c,u){const{uid:f,relatedDocumentId:y,locale:g,origin:T}=c,p=c.before??await a(f,y,[g]);for(const h of p){const b=h.locale??null,{data:m,relations:v,schemaSnapshot:A}=ce(e,f,h,await o(f)),I=de({data:m,relations:ie(v)});!le.has(T)&&(await d(f,y,b))?.contentHash===I||await e.db.query(D).create({data:{contentType:f,relatedDocumentId:y,locale:b,status:await l(f,h),label:await t(f,m),data:m,relations:v,schemaSnapshot:A,contentHash:I,origin:T,userId:u,pinned:!1}})}}}},C=["restore","publish","unpublish","discardDraft","clone","create","update"],B=new WeakMap,fe=e=>{let t=B.get(e);return t||(t={intents:[],flushRegistered:!1},B.set(e,t)),t},pe=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 r=C.indexOf(n.origin)<C.indexOf(o.origin);t.set(i,{...r?n:o,before:o.before??n.before})}return[...t.values()]},X=new ee.AsyncLocalStorage,me=e=>X.run({active:!0},e),ye=()=>X.getStore()?.active===!0,ge=200,he=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=G(e);let r=0;for(;;){const s=await n.findMany({select:["id","contentType","data"],where:{label:null},limit:ge});if(s.length===0)break;for(const a of s){const l=await o(a.contentType,a.data);await n.update({where:{id:a.id},data:{label:l??""}}),r+=1}}return e.log.info(`[rewind] labelled ${r} older version(s)`),r},L={type:"core",key:"persisted_tables"},we=async(e,t)=>{const n=await e.store.get(L)??[];n.map(o=>typeof o=="string"?o:o.name).includes(t)||await e.store.set({...L,value:[...n,{name:t}]})},be=["create","update","clone","publish","unpublish","discardDraft"],Te=async({strapi:e})=>{const t=e.config.get("plugin::rewind");await we(e,"rewind_versions"),await De(e),he(e,D).catch(o=>e.log.warn(`[rewind] could not label older versions: ${o instanceof Error?o.message:String(o)}`)),Ae(e,t);const n=()=>e.plugin("rewind").service("snapshot"),i=o=>{if(!be.includes(o.action))return!1;const r=o.contentType?.uid??o.uid??"";return!(!r.startsWith("api::")||!t.contentTypes.includes(r)||ye()||!t.trackApiWrites&&!e.requestContext.get()?.request?.url?.startsWith("/content-manager"))};e.documents.use(async(o,r)=>{if(!i(o))return r();const s=o.contentType?.uid??o.uid,a=o.params?.locale??null,l=o.action==="discardDraft"&&o.params?.documentId?await n().readRows(s,o.params.documentId,[a]):null,d=await r(),c=o.action==="create"||o.action==="clone"?d?.documentId:o.params?.documentId;if(!c)return d;const u=ve(e);return await e.db.transaction(async({trx:f,onCommit:y})=>{const g=fe(f);g.intents.push({uid:s,relatedDocumentId:c,locale:a,origin:o.action,before:l}),!g.flushRegistered&&(g.flushRegistered=!0,y(()=>{const T=g.intents.splice(0,g.intents.length);n().captureAll(pe(T),u).catch(p=>e.log.error("[rewind] failed to record a version",p))}))}),d})},ve=e=>e.requestContext.get()?.state?.user?.id??null,De=async e=>{try{const{tableName:t}=e.db.metadata.get(D);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)}`)}},Ae=(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 * * *"}}})},Ie={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:r=30,maxAgeDays:s=365}=n;if(!(o<=r&&r<=s))throw new Error(`[rewind] config.retention needs keepAllDays <= dailyUntilDays <= maxAgeDays (received ${o}, ${r}, ${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(", ")}`)}},Oe="collectionType",Se="rewind_versions",ke={singularName:"version",pluralName:"versions",displayName:"Rewind Version",description:"One snapshot of one document, in one locale, at one point in time."},Ee={draftAndPublish:!1,comment:"Managed by strapi-plugin-rewind. Do not edit manually."},qe={"content-manager":{visible:!1},"content-type-builder":{visible:!1},i18n:{localized:!1}},Re={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"}},$e={kind:Oe,collectionName:Se,info:ke,options:Ee,pluginOptions:qe,attributes:Re},Fe={version:{schema:$e}},Ne=({strapi:e})=>{e.cron.remove("rewindPrune")},_e=["id","contentType","relatedDocumentId","locale","status","label","origin","userId","pinned","contentHash","createdAt"],je=({strapi:e})=>({async find(t){const{contentType:n,documentId:i,locale:o,page:r=1,pageSize:s=10}=t.query;if(!n||!i)return t.badRequest("contentType and documentId are required.");const a={contentType:n,relatedDocumentId:i,...o?{locale:o}:{}},l=Math.min(Number(s)||10,100),d=(Math.max(Number(r)||1,1)-1)*l,[c,u]=await Promise.all([e.db.query(D).findMany({select:_e,where:a,orderBy:{createdAt:"desc"},limit:l,offset:d}),e.db.query(D).count({where:a})]);t.body={data:await U(e,c),meta:{page:Number(r)||1,pageSize:l,total:u}}},async findOne(t){const n=await e.db.query(D).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;if(n!==void 0&&!Number.isInteger(Number(n)))return t.badRequest("against must be a version id.");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(r=>r.userId).filter(Boolean))];if(n.length===0)return t.map(r=>({...r,user:null}));const i=await e.db.query("admin::user").findMany({select:["id","firstname","lastname","email"],where:{id:n}}),o=new Map(i.map(r=>[r.id,r]));return t.map(r=>{const s=o.get(r.userId);return{...r,user:s?{id:s.id,name:[s.firstname,s.lastname].filter(Boolean).join(" ")||s.email}:null}})},Me={version:je},Ce={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"]}}]},Be={admin:Ce},z=e=>e.match(/\S+\s*/g)??[],P=2500,Le=(e,t)=>{const n=z(e),i=z(t);if(n.length>P||i.length>P)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 d=n.length-1;d>=0;d-=1)for(let c=i.length-1;c>=0;c-=1)o[d][c]=n[d]===i[c]?o[d+1][c+1]+1:Math.max(o[d+1][c],o[d][c+1]);const r=[],s=(d,c)=>{const u=r[r.length-1];u&&u.op===d?u.value+=c:r.push({op:d,value:c})};let a=0,l=0;for(;a<n.length&&l<i.length;)n[a]===i[l]?(s("equal",n[a]),a+=1,l+=1):o[a+1][l]>=o[a][l+1]?(s("removed",n[a]),a+=1):(s("added",i[l]),l+=1);for(;a<n.length;)s("removed",n[a++]);for(;l<i.length;)s("added",i[l++]);return r},Ue=new Set(["id","__component","type","format","level","language","url"]),R=(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=>R(n,t+1)).filter(Boolean).join(" "):typeof e=="object"?Object.entries(e).filter(([n])=>!Ue.has(n)).map(([,n])=>R(n,t+1)).filter(Boolean).join(" "):"",Y=e=>e.replace(/\s+/g," ").trim(),ze=6,Pe=(e,t=ze)=>{const n=[];return e.forEach((i,o)=>{if(i.op!=="equal"){n.push(i);return}const r=i.value.match(/\S+\s*/g)??[];if(r.length<=t*2+1){n.push(i);return}const s=o===0,a=o===e.length-1,l=s?[]:r.slice(0,t),d=a?[]:r.slice(-t);l.length&&n.push({op:"equal",value:l.join("")}),n.push({op:"equal",value:" … "}),d.length&&n.push({op:"equal",value:d.join("")})}),n},Ye=new Set(["string","text","integer","biginteger","float","decimal","boolean","date","datetime","time","enumeration","uid","email","password"]),He=(e,t)=>e==="text"||e==="richtext"||e==="blocks"||e==="json"||e==="customField"||e==="component"||e==="dynamiczone"||e==="string"&&typeof t=="string"&&t.length>80,E=e=>`${e.targetUid}:${e.documentId??e.id}`,We=({strapi:e})=>{const t=o=>o&&{id:o.id,label:o.label??null,createdAt:o.createdAt,origin:o.origin},n=async o=>{const r=await e.db.query(D).findOne({where:{id:o}});if(!r)throw new Error(`No version with id ${o}`);return r},i=async o=>{const[r]=await e.db.query(D).findMany({where:{contentType:o.contentType,relatedDocumentId:o.relatedDocumentId,locale:o.locale,id:{$lt:o.id}},orderBy:{id:"desc"},limit:1});return r??null};return{async between(o,r){const s=await n(o),a=r?await n(r):null;if(a&&(a.contentType!==s.contentType||a.relatedDocumentId!==s.relatedDocumentId||a.locale!==s.locale))throw new Error("Those two versions are not of the same document and locale.");let l,d;a?(l=a.id<s.id?a:s,d=a.id<s.id?s:a):(l=await i(s),d=s);const c=e.getModel(d.contentType)?.attributes,u=d.data??{},f=l?.data??{},y=d.relations??{},g=l?.relations??{},T=[];for(const p of new Set([...Object.keys(f),...Object.keys(u)])){const h=f[p],b=u[p];if(JSON.stringify(h)===JSON.stringify(b))continue;const m=c?.[p]?.type??"unknown",v=c?.[p]?.customField,A=h===void 0?"added":b===void 0?"removed":"changed";if(Ye.has(m)&&!He(m,h??b)){T.push({field:p,type:m,customField:v,kind:A,before:h,after:b});continue}const I=Y(R(h)),O=Y(R(b));if(I===O){T.push({field:p,type:m,customField:v,kind:A,before:void 0,after:void 0});continue}T.push({field:p,type:m,customField:v,kind:A,spans:Pe(Le(I,O))})}for(const p of new Set([...Object.keys(g),...Object.keys(y)])){const h=g[p]??[],b=y[p]??[],m=new Set(h.map(E)),v=new Set(b.map(E)),A=b.filter(O=>!m.has(E(O))),I=h.filter(O=>!v.has(E(O)));A.length===0&&I.length===0||T.push({field:p,type:c?.[p]?.type??"relation",kind:"changed",linked:A,unlinked:I})}return{from:l?t(l):null,to:t(d),changes:T,identical:T.length===0}}}},Ke=({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}})}}},Ge=({strapi:e})=>({async set(t,n){if(!Number.isInteger(t))throw new Error("A version id is required.");const i=await e.db.query(D).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(D).update({where:{id:t},data:{pinned:n}}),{id:t,pinned:n})}}),J={keepAllDays:7,dailyUntilDays:30,maxAgeDays:365,keepAnchors:!0},Ve=new Set(["publish","unpublish","discardDraft","restore"]),Z=24*60*60*1e3,Xe=(e,t)=>(t.getTime()-new Date(e).getTime())/Z,Je=e=>e.toISOString().slice(0,10),Ze=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())/Z+1)/7);return`${t.getUTCFullYear()}-W${o}`},Qe=(e,t,n=J)=>{const i=new Map,o=[],r=[...e].sort((s,a)=>new Date(a.createdAt).getTime()-new Date(s.createdAt).getTime());for(const s of r){if(s.pinned||n.keepAnchors&&Ve.has(s.origin))continue;const a=Xe(s.createdAt,t);if(a<=n.keepAllDays)continue;if(a>n.maxAgeDays){o.push(s.id);continue}const l=new Date(s.createdAt),d=a<=n.dailyUntilDays?`d:${Je(l)}`:`w:${Ze(l)}`;i.has(d)?o.push(s.id):i.set(d,s)}return o},H=500,xe=({strapi:e})=>{const t=()=>e.db.query(D),n=async i=>{const o=await t().findMany({select:["contentType","relatedDocumentId","locale"],where:{createdAt:{$lt:i}}}),r=new Map;for(const s of o){const a=`${s.contentType}|${s.relatedDocumentId}|${s.locale??""}`;r.has(a)||r.set(a,{contentType:s.contentType,relatedDocumentId:s.relatedDocumentId,locale:s.locale??null})}return[...r.values()]};return{async prune(i=new Date){const o=e.config.get("plugin::rewind"),r={...J,...o?.retention};if(o?.retention?.enabled===!1)return{scanned:0,deleted:0,documents:0};const s=new Date(i.getTime()-r.keepAllDays*24*60*60*1e3),a=await n(s);let l=0,d=0;for(const c of a){const u=await t().findMany({select:["id","createdAt","origin","pinned"],where:{contentType:c.contentType,relatedDocumentId:c.relatedDocumentId,locale:c.locale}});l+=u.length;const f=Qe(u,i,r);for(let y=0;y<f.length;y+=H){const g=f.slice(y,y+H);await t().deleteMany({where:{id:{$in:g}}}),d+=g.length}}return{scanned:l,deleted:d,documents:a.length}}}},et=e=>{const t=e.map(i=>i.label).filter(Boolean);if(t.length===0)return`${e.length} target(s)`;const n=e.length-t.length;return n?`${t.join(", ")} and ${n} more`:t.join(", ")},tt=({strapi:e})=>{const t=s=>e.getModel(s),n=async s=>{const a=await e.db.query(D).findOne({where:{id:s}});if(!a)throw new Error(`No version with id ${s}`);return a},i=s=>s?.pluginOptions?.i18n?.localized===!0,o=s=>e.contentTypes[s]?.pluginOptions?.i18n?.localized===!0,r=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 a=await n(s),l=a.contentType,d=t(l).attributes,c=a.schemaSnapshot?.attributes??{},u=a.data??{},f=a.relations??{},y=Object.keys(d).filter(w=>!q.includes(w)),g=Object.keys(c),T=g.filter(w=>!y.includes(w)),p=y.filter(w=>!g.includes(w)),h=g.filter(w=>y.includes(w)),b=h.filter(w=>$(d[w])),m=h.filter(w=>!b.includes(w)),v=o(l)?m.filter(w=>!i(d[w])):[],A=[],I=[];for(const[w,Q]of Object.entries(f))if(m.includes(w))for(const k of Q)await r(k)||(k.targetUid==="plugin::upload.file"?I.push(k):A.push(k));const O=v.length?await e.plugin("i18n")?.service("locales").find():[];return{fieldsToRestore:m.filter(w=>w in u),fieldsKeptAsIs:p,fieldsDropped:T,crossLocaleFields:v,unsupportedFields:b,brokenRelations:A,missingMedia:I,affectedLocales:O.map(w=>w.code)}},async apply(s,a){const l=await n(s),d=l.contentType,c=await this.preview(s),u=[];await e.plugin("rewind").service("snapshot").capture({uid:d,relatedDocumentId:l.relatedDocumentId,locale:l.locale,origin:"restore",before:null},a);const f={},y=l.data??{},g=l.relations??{};for(const p of c.fieldsToRestore)f[p]=y[p];const T=new Set([...c.brokenRelations,...c.missingMedia].map(p=>`${p.targetUid}:${p.documentId??p.id}`));for(const[p,h]of Object.entries(g)){if(c.unsupportedFields.includes(p)||c.fieldsDropped.includes(p))continue;const b=h.filter(m=>!T.has(`${m.targetUid}:${m.documentId??m.id}`));if(b.length!==h.length){const m=h.filter(v=>!b.includes(v));u.push(`${et(m)} of "${p}" no longer exist and were skipped.`)}f[p]=b.map(m=>m.targetUid==="plugin::upload.file"?m.id:m.documentId)}return c.fieldsDropped.length&&u.push(`Skipped ${c.fieldsDropped.length} field(s) no longer in the model: ${c.fieldsDropped.join(", ")}.`),c.crossLocaleFields.length&&u.push(`${c.crossLocaleFields.join(", ")} are not localised, so they changed in every locale.`),await me(async()=>{await e.documents(d).update({documentId:l.relatedDocumentId,locale:l.locale??void 0,status:"draft",data:f})}),{documentId:l.relatedDocumentId,warnings:u}}}},nt={snapshot:ue,restore:tt,diff:We,retention:xe,lock:Ke,pin:Ge},ot={bootstrap:Te,destroy:Ne,config:Ie,contentTypes:Fe,controllers:Me,routes:Be,services:nt};module.exports=ot;