les-revisions 1.0.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 +3 -0
- package/dist/_chunks/App-BkgPjE-W.js +726 -0
- package/dist/_chunks/App-CYbCF5EG.mjs +726 -0
- package/dist/_chunks/en-Cx-m8SNd.mjs +63 -0
- package/dist/_chunks/en-Edhi5HYx.js +63 -0
- package/dist/_chunks/fr-DXjiOCnX.js +63 -0
- package/dist/_chunks/fr-Dbv27Oj8.mjs +63 -0
- package/dist/_chunks/index-BYJMXObQ.mjs +70 -0
- package/dist/_chunks/index-C7ce1PfW.js +69 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/index.d.ts +10 -0
- package/dist/admin/src/pages/App.d.ts +2 -0
- package/dist/admin/src/pages/ComparePage.d.ts +2 -0
- package/dist/admin/src/pages/EntriesPage.d.ts +2 -0
- package/dist/admin/src/pages/HomePage.d.ts +2 -0
- package/dist/admin/src/pages/RevisionHistoryPage.d.ts +2 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/utils/api.d.ts +14 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/server/index.js +900 -0
- package/dist/server/index.mjs +901 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +12 -0
- package/dist/server/src/content-types/index.d.ts +76 -0
- package/dist/server/src/content-types/revision/index.d.ts +74 -0
- package/dist/server/src/controllers/index.d.ts +15 -0
- package/dist/server/src/controllers/revision.d.ts +41 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +189 -0
- package/dist/server/src/middlewares/index.d.ts +2 -0
- package/dist/server/src/policies/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin/index.d.ts +12 -0
- package/dist/server/src/routes/content-api/index.d.ts +5 -0
- package/dist/server/src/routes/index.d.ts +18 -0
- package/dist/server/src/services/index.d.ts +59 -0
- package/dist/server/src/services/revision.d.ts +84 -0
- package/package.json +69 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const REVISION_UID$1 = "plugin::les-revisions.revision";
|
|
3
|
+
const bootstrap = ({ strapi }) => {
|
|
4
|
+
strapi.documents.use(async (context, next) => {
|
|
5
|
+
const { action, uid } = context;
|
|
6
|
+
if (uid === REVISION_UID$1) return next();
|
|
7
|
+
const revisionService2 = strapi.plugin("les-revisions").service("revision");
|
|
8
|
+
if (!revisionService2.isEnabled(uid)) return next();
|
|
9
|
+
const requestCtx = strapi.requestContext?.get?.();
|
|
10
|
+
if (requestCtx?.state?._lesRevisionsSkip) return next();
|
|
11
|
+
const userId = requestCtx?.state?.user?.id ?? null;
|
|
12
|
+
if (action === "create") {
|
|
13
|
+
const result = await next();
|
|
14
|
+
try {
|
|
15
|
+
if (result?.documentId) {
|
|
16
|
+
const populated = await strapi.documents(uid).findOne({
|
|
17
|
+
documentId: result.documentId,
|
|
18
|
+
populate: revisionService2.getPopulate(uid)
|
|
19
|
+
});
|
|
20
|
+
await revisionService2.createRevision({
|
|
21
|
+
contentType: uid,
|
|
22
|
+
documentId: result.documentId,
|
|
23
|
+
snapshot: populated ?? result,
|
|
24
|
+
changeType: "create",
|
|
25
|
+
userId
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
strapi.log.error("[les-revisions] Failed to create revision on CREATE:", error);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
if (action === "update") {
|
|
34
|
+
let previousState = null;
|
|
35
|
+
const docId = context.params?.documentId;
|
|
36
|
+
if (docId) {
|
|
37
|
+
try {
|
|
38
|
+
previousState = await strapi.documents(uid).findOne({
|
|
39
|
+
documentId: docId,
|
|
40
|
+
populate: revisionService2.getPopulate(uid)
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
strapi.log.warn("[les-revisions] Could not capture pre-update state:", error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const result = await next();
|
|
47
|
+
if (previousState?.documentId) {
|
|
48
|
+
try {
|
|
49
|
+
await revisionService2.createRevision({
|
|
50
|
+
contentType: uid,
|
|
51
|
+
documentId: previousState.documentId,
|
|
52
|
+
snapshot: previousState,
|
|
53
|
+
changeType: "update",
|
|
54
|
+
userId
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
strapi.log.error("[les-revisions] Failed to create revision on UPDATE:", error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (action === "delete") {
|
|
63
|
+
let previousState = null;
|
|
64
|
+
const docId = context.params?.documentId;
|
|
65
|
+
if (docId) {
|
|
66
|
+
try {
|
|
67
|
+
previousState = await strapi.documents(uid).findOne({
|
|
68
|
+
documentId: docId,
|
|
69
|
+
populate: revisionService2.getPopulate(uid)
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
strapi.log.warn("[les-revisions] Could not capture pre-delete state:", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const result = await next();
|
|
76
|
+
if (previousState?.documentId) {
|
|
77
|
+
try {
|
|
78
|
+
await revisionService2.createRevision({
|
|
79
|
+
contentType: uid,
|
|
80
|
+
documentId: previousState.documentId,
|
|
81
|
+
snapshot: previousState,
|
|
82
|
+
changeType: "delete",
|
|
83
|
+
userId
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
strapi.log.error("[les-revisions] Failed to create revision on DELETE:", error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return next();
|
|
92
|
+
});
|
|
93
|
+
strapi.log.info("[les-revisions] Versioning plugin bootstrapped");
|
|
94
|
+
};
|
|
95
|
+
const destroy = ({ strapi }) => {
|
|
96
|
+
};
|
|
97
|
+
const register = ({ strapi }) => {
|
|
98
|
+
strapi.log.info("[les-revisions] Plugin registered");
|
|
99
|
+
};
|
|
100
|
+
const config = {
|
|
101
|
+
default: {
|
|
102
|
+
maxVersions: 50,
|
|
103
|
+
enabledContentTypes: "*",
|
|
104
|
+
excludeContentTypes: [],
|
|
105
|
+
softDelete: true,
|
|
106
|
+
excludeFields: ["createdBy", "updatedBy"],
|
|
107
|
+
populateDepth: 3
|
|
108
|
+
},
|
|
109
|
+
validator(config2) {
|
|
110
|
+
if (config2.maxVersions !== void 0) {
|
|
111
|
+
if (typeof config2.maxVersions !== "number" || config2.maxVersions < 1) {
|
|
112
|
+
throw new Error("les-revisions: maxVersions must be a positive number");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (config2.enabledContentTypes !== void 0 && config2.enabledContentTypes !== "*") {
|
|
116
|
+
if (!Array.isArray(config2.enabledContentTypes)) {
|
|
117
|
+
throw new Error('les-revisions: enabledContentTypes must be an array or "*"');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (config2.excludeContentTypes !== void 0) {
|
|
121
|
+
if (!Array.isArray(config2.excludeContentTypes)) {
|
|
122
|
+
throw new Error("les-revisions: excludeContentTypes must be an array");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (config2.softDelete !== void 0 && typeof config2.softDelete !== "boolean") {
|
|
126
|
+
throw new Error("les-revisions: softDelete must be a boolean");
|
|
127
|
+
}
|
|
128
|
+
if (config2.populateDepth !== void 0) {
|
|
129
|
+
if (typeof config2.populateDepth !== "number" || config2.populateDepth < 0) {
|
|
130
|
+
throw new Error("les-revisions: populateDepth must be a non-negative number");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const kind = "collectionType";
|
|
136
|
+
const collectionName = "les_revisions";
|
|
137
|
+
const info = {
|
|
138
|
+
singularName: "revision",
|
|
139
|
+
pluralName: "revisions",
|
|
140
|
+
displayName: "Revision",
|
|
141
|
+
description: "Stores versioned snapshots of content entries"
|
|
142
|
+
};
|
|
143
|
+
const options = {
|
|
144
|
+
draftAndPublish: false
|
|
145
|
+
};
|
|
146
|
+
const pluginOptions = {
|
|
147
|
+
"content-manager": {
|
|
148
|
+
visible: false
|
|
149
|
+
},
|
|
150
|
+
"content-type-builder": {
|
|
151
|
+
visible: false
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const attributes = {
|
|
155
|
+
contentType: {
|
|
156
|
+
type: "string",
|
|
157
|
+
required: true,
|
|
158
|
+
configurable: false
|
|
159
|
+
},
|
|
160
|
+
entryDocumentId: {
|
|
161
|
+
type: "string",
|
|
162
|
+
required: true,
|
|
163
|
+
configurable: false
|
|
164
|
+
},
|
|
165
|
+
version: {
|
|
166
|
+
type: "integer",
|
|
167
|
+
required: true,
|
|
168
|
+
configurable: false,
|
|
169
|
+
"default": 1
|
|
170
|
+
},
|
|
171
|
+
snapshot: {
|
|
172
|
+
type: "json",
|
|
173
|
+
required: true,
|
|
174
|
+
configurable: false
|
|
175
|
+
},
|
|
176
|
+
changeType: {
|
|
177
|
+
type: "enumeration",
|
|
178
|
+
"enum": [
|
|
179
|
+
"create",
|
|
180
|
+
"update",
|
|
181
|
+
"delete"
|
|
182
|
+
],
|
|
183
|
+
required: true,
|
|
184
|
+
configurable: false
|
|
185
|
+
},
|
|
186
|
+
changedByName: {
|
|
187
|
+
type: "string",
|
|
188
|
+
configurable: false
|
|
189
|
+
},
|
|
190
|
+
changedByEmail: {
|
|
191
|
+
type: "string",
|
|
192
|
+
configurable: false
|
|
193
|
+
},
|
|
194
|
+
changedById: {
|
|
195
|
+
type: "integer",
|
|
196
|
+
configurable: false
|
|
197
|
+
},
|
|
198
|
+
isDeleted: {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
"default": false,
|
|
201
|
+
configurable: false
|
|
202
|
+
},
|
|
203
|
+
metadata: {
|
|
204
|
+
type: "json",
|
|
205
|
+
configurable: false
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const schema = {
|
|
209
|
+
kind,
|
|
210
|
+
collectionName,
|
|
211
|
+
info,
|
|
212
|
+
options,
|
|
213
|
+
pluginOptions,
|
|
214
|
+
attributes
|
|
215
|
+
};
|
|
216
|
+
const revision = {
|
|
217
|
+
schema
|
|
218
|
+
};
|
|
219
|
+
const contentTypes = {
|
|
220
|
+
revision
|
|
221
|
+
};
|
|
222
|
+
const revisionController = ({ strapi }) => ({
|
|
223
|
+
/**
|
|
224
|
+
* GET /revisions
|
|
225
|
+
* Query: contentType, documentId, page?, pageSize?, includeDeleted?
|
|
226
|
+
*/
|
|
227
|
+
async find(ctx) {
|
|
228
|
+
const { contentType, documentId, page, pageSize, includeDeleted } = ctx.query;
|
|
229
|
+
if (!contentType || !documentId) {
|
|
230
|
+
return ctx.badRequest("contentType and documentId are required");
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const result = await strapi.plugin("les-revisions").service("revision").findRevisions(contentType, documentId, {
|
|
234
|
+
page: page ? parseInt(page, 10) : 1,
|
|
235
|
+
pageSize: pageSize ? parseInt(pageSize, 10) : 25,
|
|
236
|
+
includeDeleted: includeDeleted === "true"
|
|
237
|
+
});
|
|
238
|
+
ctx.body = result;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return ctx.badRequest(error.message);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
/**
|
|
244
|
+
* GET /revisions/:documentId
|
|
245
|
+
*/
|
|
246
|
+
async findOne(ctx) {
|
|
247
|
+
const { documentId } = ctx.params;
|
|
248
|
+
try {
|
|
249
|
+
const revision2 = await strapi.plugin("les-revisions").service("revision").findOne(documentId);
|
|
250
|
+
if (!revision2) {
|
|
251
|
+
return ctx.notFound("Revision not found");
|
|
252
|
+
}
|
|
253
|
+
ctx.body = revision2;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return ctx.badRequest(error.message);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* POST /revisions/:documentId/restore
|
|
260
|
+
*/
|
|
261
|
+
async restore(ctx) {
|
|
262
|
+
const { documentId } = ctx.params;
|
|
263
|
+
try {
|
|
264
|
+
const restored = await strapi.plugin("les-revisions").service("revision").restore(documentId);
|
|
265
|
+
ctx.body = {
|
|
266
|
+
data: restored,
|
|
267
|
+
message: "Entry restored successfully"
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return ctx.badRequest(error.message);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
/**
|
|
274
|
+
* DELETE /revisions/:documentId
|
|
275
|
+
*/
|
|
276
|
+
async delete(ctx) {
|
|
277
|
+
const { documentId } = ctx.params;
|
|
278
|
+
try {
|
|
279
|
+
await strapi.plugin("les-revisions").service("revision").deleteRevision(documentId);
|
|
280
|
+
ctx.body = { message: "Revision deleted successfully" };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return ctx.badRequest(error.message);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* GET /revisions/compare
|
|
287
|
+
* Query: rev1, rev2
|
|
288
|
+
*/
|
|
289
|
+
async compare(ctx) {
|
|
290
|
+
const { rev1, rev2 } = ctx.query;
|
|
291
|
+
if (!rev1 || !rev2) {
|
|
292
|
+
return ctx.badRequest("rev1 and rev2 query parameters are required");
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const result = await strapi.plugin("les-revisions").service("revision").compareRevisions(rev1, rev2);
|
|
296
|
+
ctx.body = result;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return ctx.badRequest(error.message);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
/**
|
|
302
|
+
* GET /content-types
|
|
303
|
+
*/
|
|
304
|
+
async getContentTypes(ctx) {
|
|
305
|
+
try {
|
|
306
|
+
const result = await strapi.plugin("les-revisions").service("revision").getContentTypesWithRevisions();
|
|
307
|
+
ctx.body = result;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return ctx.badRequest(error.message);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* GET /entries
|
|
314
|
+
* Query: contentType, page?, pageSize?
|
|
315
|
+
*/
|
|
316
|
+
async getEntries(ctx) {
|
|
317
|
+
const { contentType, page, pageSize } = ctx.query;
|
|
318
|
+
if (!contentType) {
|
|
319
|
+
return ctx.badRequest("contentType is required");
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const result = await strapi.plugin("les-revisions").service("revision").getEntriesWithRevisions(contentType, {
|
|
323
|
+
page: page ? parseInt(page, 10) : 1,
|
|
324
|
+
pageSize: pageSize ? parseInt(pageSize, 10) : 25
|
|
325
|
+
});
|
|
326
|
+
ctx.body = result;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return ctx.badRequest(error.message);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
/**
|
|
332
|
+
* GET /config
|
|
333
|
+
*/
|
|
334
|
+
async getConfig(ctx) {
|
|
335
|
+
const config2 = strapi.plugin("les-revisions").service("revision").getConfig();
|
|
336
|
+
ctx.body = config2;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
const controllers = {
|
|
340
|
+
revision: revisionController
|
|
341
|
+
};
|
|
342
|
+
const middlewares = {};
|
|
343
|
+
const policies = {};
|
|
344
|
+
const contentAPIRoutes = () => ({
|
|
345
|
+
type: "content-api",
|
|
346
|
+
routes: []
|
|
347
|
+
});
|
|
348
|
+
const adminAPIRoutes = () => ({
|
|
349
|
+
type: "admin",
|
|
350
|
+
routes: [
|
|
351
|
+
{
|
|
352
|
+
method: "GET",
|
|
353
|
+
path: "/content-types",
|
|
354
|
+
handler: "revision.getContentTypes",
|
|
355
|
+
config: { policies: [] }
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
method: "GET",
|
|
359
|
+
path: "/entries",
|
|
360
|
+
handler: "revision.getEntries",
|
|
361
|
+
config: { policies: [] }
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
method: "GET",
|
|
365
|
+
path: "/revisions/compare",
|
|
366
|
+
handler: "revision.compare",
|
|
367
|
+
config: { policies: [] }
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
method: "GET",
|
|
371
|
+
path: "/revisions",
|
|
372
|
+
handler: "revision.find",
|
|
373
|
+
config: { policies: [] }
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
method: "GET",
|
|
377
|
+
path: "/revisions/:documentId",
|
|
378
|
+
handler: "revision.findOne",
|
|
379
|
+
config: { policies: [] }
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
method: "POST",
|
|
383
|
+
path: "/revisions/:documentId/restore",
|
|
384
|
+
handler: "revision.restore",
|
|
385
|
+
config: { policies: [] }
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
method: "DELETE",
|
|
389
|
+
path: "/revisions/:documentId",
|
|
390
|
+
handler: "revision.delete",
|
|
391
|
+
config: { policies: [] }
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
method: "GET",
|
|
395
|
+
path: "/config",
|
|
396
|
+
handler: "revision.getConfig",
|
|
397
|
+
config: { policies: [] }
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
const routes = {
|
|
402
|
+
"content-api": contentAPIRoutes,
|
|
403
|
+
admin: adminAPIRoutes
|
|
404
|
+
};
|
|
405
|
+
const REVISION_UID = "plugin::les-revisions.revision";
|
|
406
|
+
function buildDeepPopulate(uid, strapi, depth = 3, visited = /* @__PURE__ */ new Set()) {
|
|
407
|
+
if (depth <= 0 || visited.has(uid)) return true;
|
|
408
|
+
visited.add(uid);
|
|
409
|
+
const model = strapi.contentTypes[uid] || strapi.components?.[uid];
|
|
410
|
+
if (!model) return true;
|
|
411
|
+
const populate = {};
|
|
412
|
+
for (const [key, attribute] of Object.entries(model.attributes)) {
|
|
413
|
+
switch (attribute.type) {
|
|
414
|
+
case "relation": {
|
|
415
|
+
populate[key] = { populate: "*" };
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "component": {
|
|
419
|
+
const sub = buildDeepPopulate(attribute.component, strapi, depth - 1, new Set(visited));
|
|
420
|
+
populate[key] = typeof sub === "object" ? { populate: sub } : { populate: "*" };
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "dynamiczone": {
|
|
424
|
+
const on = {};
|
|
425
|
+
for (const componentUID of attribute.components || []) {
|
|
426
|
+
const sub = buildDeepPopulate(componentUID, strapi, depth - 1, new Set(visited));
|
|
427
|
+
on[componentUID] = typeof sub === "object" ? { populate: sub } : { populate: "*" };
|
|
428
|
+
}
|
|
429
|
+
populate[key] = { on };
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "media": {
|
|
433
|
+
populate[key] = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return Object.keys(populate).length > 0 ? populate : true;
|
|
439
|
+
}
|
|
440
|
+
function sanitizeSnapshot(data, excludeFields = []) {
|
|
441
|
+
if (!data || typeof data !== "object") return data;
|
|
442
|
+
const skip = /* @__PURE__ */ new Set(["createdBy", "updatedBy", ...excludeFields]);
|
|
443
|
+
const result = {};
|
|
444
|
+
for (const [key, value] of Object.entries(data)) {
|
|
445
|
+
if (skip.has(key)) continue;
|
|
446
|
+
result[key] = value;
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
function prepareRestoreData(snapshot, uid, strapi) {
|
|
451
|
+
const model = strapi.contentTypes[uid];
|
|
452
|
+
if (!model) return snapshot;
|
|
453
|
+
const internal = /* @__PURE__ */ new Set([
|
|
454
|
+
"id",
|
|
455
|
+
"documentId",
|
|
456
|
+
"createdAt",
|
|
457
|
+
"updatedAt",
|
|
458
|
+
"publishedAt",
|
|
459
|
+
"createdBy",
|
|
460
|
+
"updatedBy",
|
|
461
|
+
"locale",
|
|
462
|
+
"localizations"
|
|
463
|
+
]);
|
|
464
|
+
const data = {};
|
|
465
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
466
|
+
if (internal.has(key)) continue;
|
|
467
|
+
const attr = model.attributes[key];
|
|
468
|
+
if (!attr) continue;
|
|
469
|
+
switch (attr.type) {
|
|
470
|
+
case "relation":
|
|
471
|
+
data[key] = transformRelation(value);
|
|
472
|
+
break;
|
|
473
|
+
case "component":
|
|
474
|
+
data[key] = transformComponent(value, attr, strapi);
|
|
475
|
+
break;
|
|
476
|
+
case "dynamiczone":
|
|
477
|
+
data[key] = transformDynamicZone(value, strapi);
|
|
478
|
+
break;
|
|
479
|
+
case "media":
|
|
480
|
+
data[key] = transformMedia(value);
|
|
481
|
+
break;
|
|
482
|
+
default:
|
|
483
|
+
data[key] = value;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return data;
|
|
487
|
+
}
|
|
488
|
+
function transformRelation(value) {
|
|
489
|
+
if (value === null || value === void 0) return { set: [] };
|
|
490
|
+
if (Array.isArray(value)) {
|
|
491
|
+
return { set: value.filter((v) => v?.documentId).map((v) => ({ documentId: v.documentId })) };
|
|
492
|
+
}
|
|
493
|
+
if (typeof value === "object" && value.documentId) {
|
|
494
|
+
return { set: [{ documentId: value.documentId }] };
|
|
495
|
+
}
|
|
496
|
+
return { set: [] };
|
|
497
|
+
}
|
|
498
|
+
function transformComponent(value, attr, strapi) {
|
|
499
|
+
if (value === null || value === void 0) return null;
|
|
500
|
+
if (attr.repeatable && Array.isArray(value)) {
|
|
501
|
+
return value.map((v) => cleanComponentData(v, attr.component, strapi));
|
|
502
|
+
}
|
|
503
|
+
return cleanComponentData(value, attr.component, strapi);
|
|
504
|
+
}
|
|
505
|
+
function transformDynamicZone(value, strapi) {
|
|
506
|
+
if (!Array.isArray(value)) return value;
|
|
507
|
+
return value.map((v) => ({
|
|
508
|
+
__component: v.__component,
|
|
509
|
+
...cleanComponentData(v, v.__component, strapi)
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
function transformMedia(value) {
|
|
513
|
+
if (value === null || value === void 0) return null;
|
|
514
|
+
if (Array.isArray(value)) return value.map((v) => v.id).filter(Boolean);
|
|
515
|
+
if (typeof value === "object") return value.id ?? null;
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
function cleanComponentData(data, componentUID, strapi) {
|
|
519
|
+
if (!data || typeof data !== "object") return data;
|
|
520
|
+
const component = strapi.components?.[componentUID];
|
|
521
|
+
if (!component) {
|
|
522
|
+
const { id: _id, __component: _c, ...rest } = data;
|
|
523
|
+
return rest;
|
|
524
|
+
}
|
|
525
|
+
const cleaned = {};
|
|
526
|
+
for (const [key, value] of Object.entries(data)) {
|
|
527
|
+
if (key === "id" || key === "__component") continue;
|
|
528
|
+
const attr = component.attributes[key];
|
|
529
|
+
if (!attr) continue;
|
|
530
|
+
switch (attr.type) {
|
|
531
|
+
case "relation":
|
|
532
|
+
cleaned[key] = transformRelation(value);
|
|
533
|
+
break;
|
|
534
|
+
case "component":
|
|
535
|
+
cleaned[key] = transformComponent(value, attr, strapi);
|
|
536
|
+
break;
|
|
537
|
+
case "dynamiczone":
|
|
538
|
+
cleaned[key] = transformDynamicZone(value, strapi);
|
|
539
|
+
break;
|
|
540
|
+
case "media":
|
|
541
|
+
cleaned[key] = transformMedia(value);
|
|
542
|
+
break;
|
|
543
|
+
default:
|
|
544
|
+
cleaned[key] = value;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return cleaned;
|
|
548
|
+
}
|
|
549
|
+
function computeDiff(oldObj, newObj, path = "") {
|
|
550
|
+
const diffs = [];
|
|
551
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldObj ?? {}), ...Object.keys(newObj ?? {})]);
|
|
552
|
+
for (const key of allKeys) {
|
|
553
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
554
|
+
const oldVal = oldObj?.[key];
|
|
555
|
+
const newVal = newObj?.[key];
|
|
556
|
+
if (oldVal === void 0 && newVal !== void 0) {
|
|
557
|
+
diffs.push({ path: currentPath, type: "added", newValue: newVal });
|
|
558
|
+
} else if (oldVal !== void 0 && newVal === void 0) {
|
|
559
|
+
diffs.push({ path: currentPath, type: "removed", oldValue: oldVal });
|
|
560
|
+
} else if (typeof oldVal === "object" && oldVal !== null && typeof newVal === "object" && newVal !== null && !Array.isArray(oldVal) && !Array.isArray(newVal)) {
|
|
561
|
+
diffs.push(...computeDiff(oldVal, newVal, currentPath));
|
|
562
|
+
} else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
563
|
+
diffs.push({ path: currentPath, type: "changed", oldValue: oldVal, newValue: newVal });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return diffs;
|
|
567
|
+
}
|
|
568
|
+
function getEntryTitle(entry) {
|
|
569
|
+
if (!entry) return "Unknown";
|
|
570
|
+
return entry.title ?? entry.name ?? entry.label ?? entry.slug ?? entry.username ?? entry.email ?? entry.documentId ?? "Untitled";
|
|
571
|
+
}
|
|
572
|
+
const revisionService = ({ strapi }) => ({
|
|
573
|
+
/* ── Configuration ────────────────────────────────────────────────── */
|
|
574
|
+
getConfig() {
|
|
575
|
+
return strapi.config.get("plugin::les-revisions");
|
|
576
|
+
},
|
|
577
|
+
isEnabled(contentTypeUID) {
|
|
578
|
+
if (contentTypeUID === REVISION_UID) return false;
|
|
579
|
+
if (contentTypeUID.startsWith("admin::") || contentTypeUID.startsWith("plugin::") || contentTypeUID.startsWith("strapi::")) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const config2 = this.getConfig();
|
|
583
|
+
if (config2.excludeContentTypes.includes(contentTypeUID)) return false;
|
|
584
|
+
if (config2.enabledContentTypes === "*") return true;
|
|
585
|
+
return config2.enabledContentTypes.includes(contentTypeUID);
|
|
586
|
+
},
|
|
587
|
+
getPopulate(contentTypeUID) {
|
|
588
|
+
const config2 = this.getConfig();
|
|
589
|
+
return buildDeepPopulate(contentTypeUID, strapi, config2.populateDepth);
|
|
590
|
+
},
|
|
591
|
+
/* ── CRUD ──────────────────────────────────────────────────────────── */
|
|
592
|
+
async createRevision({ contentType, documentId, snapshot, changeType, userId }) {
|
|
593
|
+
const config2 = this.getConfig();
|
|
594
|
+
const existing = await strapi.documents(REVISION_UID).findMany({
|
|
595
|
+
filters: {
|
|
596
|
+
contentType: { $eq: contentType },
|
|
597
|
+
entryDocumentId: { $eq: documentId },
|
|
598
|
+
isDeleted: { $eq: false }
|
|
599
|
+
},
|
|
600
|
+
sort: { version: "desc" },
|
|
601
|
+
limit: 1
|
|
602
|
+
});
|
|
603
|
+
const nextVersion = existing.length > 0 ? (existing[0].version ?? 0) + 1 : 1;
|
|
604
|
+
let changedByName = null;
|
|
605
|
+
let changedByEmail = null;
|
|
606
|
+
let changedById = null;
|
|
607
|
+
if (userId) {
|
|
608
|
+
try {
|
|
609
|
+
const user = await strapi.query("admin::user").findOne({ where: { id: userId } });
|
|
610
|
+
if (user) {
|
|
611
|
+
changedByName = `${user.firstname ?? ""} ${user.lastname ?? ""}`.trim() || null;
|
|
612
|
+
changedByEmail = user.email ?? null;
|
|
613
|
+
changedById = user.id;
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const sanitized = sanitizeSnapshot(snapshot, config2.excludeFields);
|
|
619
|
+
const revision2 = await strapi.documents(REVISION_UID).create({
|
|
620
|
+
data: {
|
|
621
|
+
contentType,
|
|
622
|
+
entryDocumentId: documentId,
|
|
623
|
+
version: nextVersion,
|
|
624
|
+
snapshot: sanitized,
|
|
625
|
+
changeType,
|
|
626
|
+
changedByName,
|
|
627
|
+
changedByEmail,
|
|
628
|
+
changedById,
|
|
629
|
+
isDeleted: false,
|
|
630
|
+
metadata: {
|
|
631
|
+
locale: snapshot?.locale ?? null,
|
|
632
|
+
publishedAt: snapshot?.publishedAt ?? null
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
await this.pruneRevisions(contentType, documentId);
|
|
637
|
+
return revision2;
|
|
638
|
+
},
|
|
639
|
+
async findRevisions(contentType, documentId, params = {}) {
|
|
640
|
+
const { page = 1, pageSize = 25, includeDeleted = false } = params;
|
|
641
|
+
const filters = {
|
|
642
|
+
contentType: { $eq: contentType },
|
|
643
|
+
entryDocumentId: { $eq: documentId }
|
|
644
|
+
};
|
|
645
|
+
if (!includeDeleted) {
|
|
646
|
+
filters.isDeleted = { $eq: false };
|
|
647
|
+
}
|
|
648
|
+
const start = (page - 1) * pageSize;
|
|
649
|
+
const [revisions, total] = await Promise.all([
|
|
650
|
+
strapi.documents(REVISION_UID).findMany({
|
|
651
|
+
filters,
|
|
652
|
+
sort: { version: "desc" },
|
|
653
|
+
start,
|
|
654
|
+
limit: pageSize
|
|
655
|
+
}),
|
|
656
|
+
strapi.db.query(REVISION_UID).count({ where: this._filtersToWhere(filters) })
|
|
657
|
+
]);
|
|
658
|
+
return {
|
|
659
|
+
results: revisions,
|
|
660
|
+
pagination: {
|
|
661
|
+
page,
|
|
662
|
+
pageSize,
|
|
663
|
+
pageCount: Math.ceil(total / pageSize),
|
|
664
|
+
total
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
},
|
|
668
|
+
async findOne(revisionDocumentId) {
|
|
669
|
+
return strapi.documents(REVISION_UID).findOne({
|
|
670
|
+
documentId: revisionDocumentId
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
/* ── Restore ───────────────────────────────────────────────────────── */
|
|
674
|
+
async restore(revisionDocumentId) {
|
|
675
|
+
const revision2 = await strapi.documents(REVISION_UID).findOne({
|
|
676
|
+
documentId: revisionDocumentId
|
|
677
|
+
});
|
|
678
|
+
if (!revision2) throw new Error("Revision not found");
|
|
679
|
+
const { contentType, entryDocumentId, snapshot } = revision2;
|
|
680
|
+
if (!strapi.contentTypes[contentType]) {
|
|
681
|
+
throw new Error(`Content-type ${contentType} no longer exists`);
|
|
682
|
+
}
|
|
683
|
+
const currentEntry = await strapi.documents(contentType).findOne({
|
|
684
|
+
documentId: entryDocumentId,
|
|
685
|
+
populate: this.getPopulate(contentType)
|
|
686
|
+
});
|
|
687
|
+
if (!currentEntry) {
|
|
688
|
+
throw new Error(`Entry ${entryDocumentId} no longer exists in ${contentType}`);
|
|
689
|
+
}
|
|
690
|
+
const ctx = strapi.requestContext?.get?.();
|
|
691
|
+
const userId = ctx?.state?.user?.id ?? null;
|
|
692
|
+
await this.createRevision({
|
|
693
|
+
contentType,
|
|
694
|
+
documentId: entryDocumentId,
|
|
695
|
+
snapshot: currentEntry,
|
|
696
|
+
changeType: "update",
|
|
697
|
+
userId
|
|
698
|
+
});
|
|
699
|
+
const restoreData = prepareRestoreData(snapshot, contentType, strapi);
|
|
700
|
+
if (ctx) {
|
|
701
|
+
ctx.state = ctx.state || {};
|
|
702
|
+
ctx.state._lesRevisionsSkip = true;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const restored = await strapi.documents(contentType).update({
|
|
706
|
+
documentId: entryDocumentId,
|
|
707
|
+
data: restoreData,
|
|
708
|
+
populate: this.getPopulate(contentType)
|
|
709
|
+
});
|
|
710
|
+
return restored;
|
|
711
|
+
} finally {
|
|
712
|
+
if (ctx?.state) {
|
|
713
|
+
ctx.state._lesRevisionsSkip = false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
/* ── Delete ────────────────────────────────────────────────────────── */
|
|
718
|
+
async deleteRevision(revisionDocumentId) {
|
|
719
|
+
const config2 = this.getConfig();
|
|
720
|
+
if (config2.softDelete) {
|
|
721
|
+
return strapi.documents(REVISION_UID).update({
|
|
722
|
+
documentId: revisionDocumentId,
|
|
723
|
+
data: { isDeleted: true }
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return strapi.documents(REVISION_UID).delete({
|
|
727
|
+
documentId: revisionDocumentId
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
/* ── Compare ───────────────────────────────────────────────────────── */
|
|
731
|
+
async compareRevisions(docId1, docId2) {
|
|
732
|
+
const [rev1, rev2] = await Promise.all([
|
|
733
|
+
strapi.documents(REVISION_UID).findOne({ documentId: docId1 }),
|
|
734
|
+
strapi.documents(REVISION_UID).findOne({ documentId: docId2 })
|
|
735
|
+
]);
|
|
736
|
+
if (!rev1 || !rev2) throw new Error("One or both revisions not found");
|
|
737
|
+
const r1 = rev1;
|
|
738
|
+
const r2 = rev2;
|
|
739
|
+
if (r1.contentType !== r2.contentType || r1.entryDocumentId !== r2.entryDocumentId) {
|
|
740
|
+
throw new Error("Cannot compare revisions from different entries");
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
revision1: rev1,
|
|
744
|
+
revision2: rev2,
|
|
745
|
+
diff: computeDiff(r1.snapshot ?? {}, r2.snapshot ?? {})
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
/* ── Pruning ───────────────────────────────────────────────────────── */
|
|
749
|
+
async pruneRevisions(contentType, documentId) {
|
|
750
|
+
const config2 = this.getConfig();
|
|
751
|
+
const { maxVersions, softDelete } = config2;
|
|
752
|
+
const all = await strapi.documents(REVISION_UID).findMany({
|
|
753
|
+
filters: {
|
|
754
|
+
contentType: { $eq: contentType },
|
|
755
|
+
entryDocumentId: { $eq: documentId },
|
|
756
|
+
isDeleted: { $eq: false }
|
|
757
|
+
},
|
|
758
|
+
sort: { version: "desc" }
|
|
759
|
+
});
|
|
760
|
+
if (all.length <= maxVersions) return;
|
|
761
|
+
const toRemove = all.slice(maxVersions);
|
|
762
|
+
for (const rev of toRemove) {
|
|
763
|
+
const docId = rev.documentId;
|
|
764
|
+
if (softDelete) {
|
|
765
|
+
await strapi.documents(REVISION_UID).update({
|
|
766
|
+
documentId: docId,
|
|
767
|
+
data: { isDeleted: true }
|
|
768
|
+
});
|
|
769
|
+
} else {
|
|
770
|
+
await strapi.documents(REVISION_UID).delete({ documentId: docId });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
/* ── Listing helpers (admin UI) ────────────────────────────────────── */
|
|
775
|
+
async getContentTypesWithRevisions() {
|
|
776
|
+
const contentTypes2 = Object.entries(strapi.contentTypes).filter(([uid]) => this.isEnabled(uid)).map(([uid, ct]) => ({ uid, info: ct.info }));
|
|
777
|
+
const result = [];
|
|
778
|
+
for (const ct of contentTypes2) {
|
|
779
|
+
const count = await strapi.db.query(REVISION_UID).count({
|
|
780
|
+
where: { contentType: ct.uid, isDeleted: false }
|
|
781
|
+
});
|
|
782
|
+
result.push({ ...ct, revisionCount: count });
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
},
|
|
786
|
+
async getEntriesWithRevisions(contentType, params = {}) {
|
|
787
|
+
const { page = 1, pageSize = 25 } = params;
|
|
788
|
+
const revisions = await strapi.documents(REVISION_UID).findMany({
|
|
789
|
+
filters: {
|
|
790
|
+
contentType: { $eq: contentType },
|
|
791
|
+
isDeleted: { $eq: false }
|
|
792
|
+
},
|
|
793
|
+
sort: { createdAt: "desc" }
|
|
794
|
+
});
|
|
795
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const rev of revisions) {
|
|
797
|
+
const r = rev;
|
|
798
|
+
const entry = entryMap.get(r.entryDocumentId);
|
|
799
|
+
if (!entry) {
|
|
800
|
+
entryMap.set(r.entryDocumentId, {
|
|
801
|
+
documentId: r.entryDocumentId,
|
|
802
|
+
count: 1,
|
|
803
|
+
lastModified: r.createdAt,
|
|
804
|
+
lastVersion: r.version
|
|
805
|
+
});
|
|
806
|
+
} else {
|
|
807
|
+
entry.count++;
|
|
808
|
+
if (r.version > entry.lastVersion) {
|
|
809
|
+
entry.lastModified = r.createdAt;
|
|
810
|
+
entry.lastVersion = r.version;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const entries = Array.from(entryMap.values()).sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
|
|
815
|
+
const total = entries.length;
|
|
816
|
+
const start = (page - 1) * pageSize;
|
|
817
|
+
const paged = entries.slice(start, start + pageSize);
|
|
818
|
+
const enriched = [];
|
|
819
|
+
for (const entry of paged) {
|
|
820
|
+
try {
|
|
821
|
+
const doc = await strapi.documents(contentType).findOne({
|
|
822
|
+
documentId: entry.documentId
|
|
823
|
+
});
|
|
824
|
+
enriched.push({
|
|
825
|
+
...entry,
|
|
826
|
+
entryTitle: getEntryTitle(doc),
|
|
827
|
+
exists: !!doc
|
|
828
|
+
});
|
|
829
|
+
} catch {
|
|
830
|
+
enriched.push({
|
|
831
|
+
...entry,
|
|
832
|
+
entryTitle: `[Supprimé] ${entry.documentId}`,
|
|
833
|
+
exists: false
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
results: enriched,
|
|
839
|
+
pagination: {
|
|
840
|
+
page,
|
|
841
|
+
pageSize,
|
|
842
|
+
pageCount: Math.ceil(total / pageSize),
|
|
843
|
+
total
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
},
|
|
847
|
+
/**
|
|
848
|
+
* Soft-delete all revisions for an entry (called on entry hard-delete).
|
|
849
|
+
*/
|
|
850
|
+
async deleteAllRevisionsForEntry(contentType, documentId) {
|
|
851
|
+
const config2 = this.getConfig();
|
|
852
|
+
const revisions = await strapi.documents(REVISION_UID).findMany({
|
|
853
|
+
filters: {
|
|
854
|
+
contentType: { $eq: contentType },
|
|
855
|
+
entryDocumentId: { $eq: documentId }
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
for (const rev of revisions) {
|
|
859
|
+
const docId = rev.documentId;
|
|
860
|
+
if (config2.softDelete) {
|
|
861
|
+
await strapi.documents(REVISION_UID).update({
|
|
862
|
+
documentId: docId,
|
|
863
|
+
data: { isDeleted: true }
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
await strapi.documents(REVISION_UID).delete({ documentId: docId });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
/* ── Internal utilities ────────────────────────────────────────────── */
|
|
871
|
+
/** Convert Document Service filters to query-engine where clause */
|
|
872
|
+
_filtersToWhere(filters) {
|
|
873
|
+
const where = {};
|
|
874
|
+
for (const [key, condition] of Object.entries(filters)) {
|
|
875
|
+
if (typeof condition === "object" && condition !== null) {
|
|
876
|
+
if ("$eq" in condition) where[key] = condition.$eq;
|
|
877
|
+
else where[key] = condition;
|
|
878
|
+
} else {
|
|
879
|
+
where[key] = condition;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return where;
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
const services = {
|
|
886
|
+
revision: revisionService
|
|
887
|
+
};
|
|
888
|
+
const index = {
|
|
889
|
+
register,
|
|
890
|
+
bootstrap,
|
|
891
|
+
destroy,
|
|
892
|
+
config,
|
|
893
|
+
controllers,
|
|
894
|
+
routes,
|
|
895
|
+
services,
|
|
896
|
+
contentTypes,
|
|
897
|
+
policies,
|
|
898
|
+
middlewares
|
|
899
|
+
};
|
|
900
|
+
module.exports = index;
|