strapi-plugin-timeline 0.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/admin/App-BYK_GACN.mjs +559 -0
- package/dist/admin/App-CYrqQs-r.js +559 -0
- package/dist/admin/SchedulePage-Bl0OZTNc.mjs +166 -0
- package/dist/admin/SchedulePage-C1gAM9na.js +166 -0
- package/dist/admin/SettingsPage-BH7ytFs4.js +259 -0
- package/dist/admin/SettingsPage-C87KaVB_.mjs +259 -0
- package/dist/admin/en-B4KWt_jN.js +4 -0
- package/dist/admin/en-Byx4XI2L.mjs +4 -0
- package/dist/admin/index-BPC9ghPd.js +1008 -0
- package/dist/admin/index-DWXpMwlf.mjs +1005 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/index.d.ts +11 -0
- package/dist/server/index.js +1037 -0
- package/dist/server/index.mjs +1037 -0
- package/dist/server/src/index.d.ts +269 -0
- package/package.json +103 -0
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const IGNORED_UIDS = /* @__PURE__ */ new Set([
|
|
4
|
+
"plugin::timeline.timeline-entry",
|
|
5
|
+
"plugin::timeline.timeline-setting"
|
|
6
|
+
]);
|
|
7
|
+
const restoreInProgress = /* @__PURE__ */ new Set();
|
|
8
|
+
function markRestoreInProgress(key) {
|
|
9
|
+
restoreInProgress.add(key);
|
|
10
|
+
}
|
|
11
|
+
function clearRestoreInProgress(key) {
|
|
12
|
+
restoreInProgress.delete(key);
|
|
13
|
+
}
|
|
14
|
+
function isRestoreInProgress(key) {
|
|
15
|
+
return restoreInProgress.has(key);
|
|
16
|
+
}
|
|
17
|
+
function getRequestUser(strapi) {
|
|
18
|
+
try {
|
|
19
|
+
const ctx = strapi.requestContext?.get?.();
|
|
20
|
+
const user = ctx?.state?.user;
|
|
21
|
+
if (!user?.id) return null;
|
|
22
|
+
const name = [user.firstname, user.lastname].filter(Boolean).join(" ") || user.email || null;
|
|
23
|
+
return { id: user.id, name: name || `User #${user.id}` };
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function isTracked(strapi, uid, action) {
|
|
29
|
+
if (!uid.startsWith("api::")) return false;
|
|
30
|
+
if (IGNORED_UIDS.has(uid)) return false;
|
|
31
|
+
try {
|
|
32
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
33
|
+
const config2 = settings.contentTypeConfigs?.[uid];
|
|
34
|
+
if (!config2 || !config2.enabled) return false;
|
|
35
|
+
if (action && Array.isArray(config2.actions) && config2.actions.length > 0) {
|
|
36
|
+
return config2.actions.includes(action);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getRelationAttributeDetails(strapi, uid) {
|
|
44
|
+
const ct = strapi.contentTypes[uid];
|
|
45
|
+
if (!ct?.attributes) return {};
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const [name, attr] of Object.entries(ct.attributes)) {
|
|
48
|
+
const a = attr;
|
|
49
|
+
if (a.type === "relation" && a.target) {
|
|
50
|
+
result[name] = { target: a.target, relation: a.relation || "" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function extractAttributesSchema(strapi, attributes2, visited = /* @__PURE__ */ new Set()) {
|
|
56
|
+
const schema2 = {};
|
|
57
|
+
for (const [name, attr] of Object.entries(attributes2)) {
|
|
58
|
+
const a = attr;
|
|
59
|
+
const entry = { type: a.type };
|
|
60
|
+
if (a.type === "relation") {
|
|
61
|
+
entry.target = a.target || null;
|
|
62
|
+
entry.relation = a.relation || null;
|
|
63
|
+
}
|
|
64
|
+
if (a.type === "component") {
|
|
65
|
+
entry.component = a.component || null;
|
|
66
|
+
entry.repeatable = a.repeatable || false;
|
|
67
|
+
if (a.component && !visited.has(a.component)) {
|
|
68
|
+
const comp = strapi.components?.[a.component];
|
|
69
|
+
if (comp?.attributes) {
|
|
70
|
+
visited.add(a.component);
|
|
71
|
+
entry.reference = extractAttributesSchema(strapi, comp.attributes, visited);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (a.type === "dynamiczone") {
|
|
76
|
+
entry.components = a.components || [];
|
|
77
|
+
const references = {};
|
|
78
|
+
for (const compUid of a.components || []) {
|
|
79
|
+
if (visited.has(compUid)) continue;
|
|
80
|
+
const comp = strapi.components?.[compUid];
|
|
81
|
+
if (comp?.attributes) {
|
|
82
|
+
visited.add(compUid);
|
|
83
|
+
references[compUid] = extractAttributesSchema(strapi, comp.attributes, visited);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(references).length > 0) {
|
|
87
|
+
entry.references = references;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (a.type === "customField" && a.customField) {
|
|
91
|
+
entry.customField = a.customField;
|
|
92
|
+
try {
|
|
93
|
+
const cfReg = strapi.customFields?.get?.(a.customField);
|
|
94
|
+
if (cfReg?.type) {
|
|
95
|
+
entry.customFieldType = cfReg.type;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
schema2[name] = entry;
|
|
101
|
+
}
|
|
102
|
+
return schema2;
|
|
103
|
+
}
|
|
104
|
+
function extractSchema(strapi, uid) {
|
|
105
|
+
const ct = strapi.contentTypes[uid];
|
|
106
|
+
if (!ct?.attributes) return {};
|
|
107
|
+
return extractAttributesSchema(strapi, ct.attributes, /* @__PURE__ */ new Set([uid]));
|
|
108
|
+
}
|
|
109
|
+
async function resolveRelations(strapi, uid, entryId, rawContent) {
|
|
110
|
+
const relationDetails = getRelationAttributeDetails(strapi, uid);
|
|
111
|
+
const relationNames = Object.keys(relationDetails);
|
|
112
|
+
if (relationNames.length === 0) return rawContent;
|
|
113
|
+
try {
|
|
114
|
+
const populate = {};
|
|
115
|
+
for (const attr of relationNames) {
|
|
116
|
+
const { target } = relationDetails[attr];
|
|
117
|
+
const isUsersPermissions = target.startsWith("plugin::users-permissions.");
|
|
118
|
+
const isAdminUser = target === "admin::user";
|
|
119
|
+
if (isUsersPermissions) {
|
|
120
|
+
populate[attr] = { fields: ["id"] };
|
|
121
|
+
} else if (isAdminUser) {
|
|
122
|
+
populate[attr] = { fields: ["id", "firstname", "lastname", "email"] };
|
|
123
|
+
} else if (attr === "localizations") {
|
|
124
|
+
populate[attr] = { fields: ["documentId", "locale"] };
|
|
125
|
+
} else {
|
|
126
|
+
populate[attr] = { fields: ["documentId"] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const populated = await strapi.db.query(uid).findOne({
|
|
130
|
+
where: { id: entryId },
|
|
131
|
+
populate
|
|
132
|
+
});
|
|
133
|
+
if (!populated) return rawContent;
|
|
134
|
+
const content = { ...rawContent };
|
|
135
|
+
for (const attr of relationNames) {
|
|
136
|
+
const val = populated[attr];
|
|
137
|
+
const { target } = relationDetails[attr];
|
|
138
|
+
const isUsersPermissions = target.startsWith("plugin::users-permissions.");
|
|
139
|
+
const isAdminUser = target === "admin::user";
|
|
140
|
+
if (val == null) {
|
|
141
|
+
content[attr] = null;
|
|
142
|
+
} else if (isAdminUser) {
|
|
143
|
+
if (Array.isArray(val)) {
|
|
144
|
+
content[attr] = val.map((r) => ({
|
|
145
|
+
id: r.id,
|
|
146
|
+
firstname: r.firstname,
|
|
147
|
+
lastname: r.lastname,
|
|
148
|
+
email: r.email
|
|
149
|
+
}));
|
|
150
|
+
} else {
|
|
151
|
+
content[attr] = {
|
|
152
|
+
id: val.id,
|
|
153
|
+
firstname: val.firstname,
|
|
154
|
+
lastname: val.lastname,
|
|
155
|
+
email: val.email
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
} else if (attr === "localizations") {
|
|
159
|
+
if (Array.isArray(val)) {
|
|
160
|
+
content[attr] = val.map((r) => ({
|
|
161
|
+
documentId: r.documentId,
|
|
162
|
+
locale: r.locale
|
|
163
|
+
})).filter((r) => r.documentId);
|
|
164
|
+
} else if (val.documentId) {
|
|
165
|
+
content[attr] = [{ documentId: val.documentId, locale: val.locale }];
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
const idField = isUsersPermissions ? "id" : "documentId";
|
|
169
|
+
if (Array.isArray(val)) {
|
|
170
|
+
content[attr] = val.map((r) => r[idField]).filter(Boolean);
|
|
171
|
+
} else if (val[idField]) {
|
|
172
|
+
content[attr] = val[idField];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return content;
|
|
177
|
+
} catch {
|
|
178
|
+
return rawContent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const bootstrap = async ({ strapi }) => {
|
|
182
|
+
strapi.log.info("[Timeline] Registering global lifecycle hooks (collections checked dynamically).");
|
|
183
|
+
strapi.db.lifecycles.subscribe({
|
|
184
|
+
async afterCreate(event) {
|
|
185
|
+
try {
|
|
186
|
+
const { result, model } = event;
|
|
187
|
+
if (!result) return;
|
|
188
|
+
const ct = strapi.contentTypes[model.uid];
|
|
189
|
+
const hasDraftAndPublish = ct?.options?.draftAndPublish !== false;
|
|
190
|
+
const isPublish = hasDraftAndPublish && result.publishedAt != null;
|
|
191
|
+
const action = isPublish ? "publish" : "create";
|
|
192
|
+
if (!await isTracked(strapi, model.uid, action)) return;
|
|
193
|
+
const reqUser = getRequestUser(strapi);
|
|
194
|
+
const content = await resolveRelations(strapi, model.uid, result.id, { ...result });
|
|
195
|
+
const contentTypeSchema = extractSchema(strapi, model.uid);
|
|
196
|
+
await strapi.plugin("timeline").service("timeline").createSnapshot({
|
|
197
|
+
action,
|
|
198
|
+
contentType: model.uid,
|
|
199
|
+
entryDocumentId: result.documentId || String(result.id),
|
|
200
|
+
content,
|
|
201
|
+
contentTypeSchema,
|
|
202
|
+
userId: reqUser?.id || null,
|
|
203
|
+
userName: reqUser?.name || null,
|
|
204
|
+
locale: result.locale || null
|
|
205
|
+
});
|
|
206
|
+
await strapi.plugin("timeline").service("timeline").enforceEntriesLimit(model.uid);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
strapi.log.error(`[Timeline] afterCreate error: ${error}`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
async afterUpdate(event) {
|
|
212
|
+
try {
|
|
213
|
+
const { result, model } = event;
|
|
214
|
+
if (!result) return;
|
|
215
|
+
const docId = result.documentId || String(result.id);
|
|
216
|
+
const restoreKey = `${model.uid}:${docId}`;
|
|
217
|
+
if (isRestoreInProgress(restoreKey)) return;
|
|
218
|
+
const ct = strapi.contentTypes[model.uid];
|
|
219
|
+
const hasDraftAndPublish = ct?.options?.draftAndPublish !== false;
|
|
220
|
+
if (hasDraftAndPublish && result.publishedAt != null) return;
|
|
221
|
+
if (!await isTracked(strapi, model.uid, "update")) return;
|
|
222
|
+
const reqUser = getRequestUser(strapi);
|
|
223
|
+
const content = await resolveRelations(strapi, model.uid, result.id, { ...result });
|
|
224
|
+
const contentTypeSchema = extractSchema(strapi, model.uid);
|
|
225
|
+
await strapi.plugin("timeline").service("timeline").createSnapshot({
|
|
226
|
+
action: "update",
|
|
227
|
+
contentType: model.uid,
|
|
228
|
+
entryDocumentId: result.documentId || String(result.id),
|
|
229
|
+
content,
|
|
230
|
+
contentTypeSchema,
|
|
231
|
+
userId: reqUser?.id || null,
|
|
232
|
+
userName: reqUser?.name || null,
|
|
233
|
+
locale: result.locale || null
|
|
234
|
+
});
|
|
235
|
+
await strapi.plugin("timeline").service("timeline").enforceEntriesLimit(model.uid);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
strapi.log.error(`[Timeline] afterUpdate error: ${error}`);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async beforeDelete(event) {
|
|
241
|
+
try {
|
|
242
|
+
const { params, model } = event;
|
|
243
|
+
if (!await isTracked(strapi, model.uid, "delete")) return;
|
|
244
|
+
const where = params.where || {};
|
|
245
|
+
const entries = await strapi.db.query(model.uid).findMany({ where });
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const ct = strapi.contentTypes[model.uid];
|
|
248
|
+
const hasDraftAndPublish = ct?.options?.draftAndPublish !== false;
|
|
249
|
+
if (hasDraftAndPublish && entry.publishedAt != null) continue;
|
|
250
|
+
const reqUser = getRequestUser(strapi);
|
|
251
|
+
const content = await resolveRelations(strapi, model.uid, entry.id, { ...entry });
|
|
252
|
+
const contentTypeSchema = extractSchema(strapi, model.uid);
|
|
253
|
+
await strapi.plugin("timeline").service("timeline").createSnapshot({
|
|
254
|
+
action: "delete",
|
|
255
|
+
contentType: model.uid,
|
|
256
|
+
entryDocumentId: entry.documentId || String(entry.id),
|
|
257
|
+
content,
|
|
258
|
+
contentTypeSchema,
|
|
259
|
+
userId: reqUser?.id || null,
|
|
260
|
+
userName: reqUser?.name || null,
|
|
261
|
+
locale: entry.locale || null
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
strapi.log.error(`[Timeline] beforeDelete error: ${error}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
271
|
+
if (settings.cleanupEnabled && settings.cleanupCron) {
|
|
272
|
+
strapi.cron.add({
|
|
273
|
+
"timeline-cleanup": {
|
|
274
|
+
task: async ({ strapi: s }) => {
|
|
275
|
+
s.log.info("[Timeline] Running scheduled cleanup...");
|
|
276
|
+
try {
|
|
277
|
+
const results = await s.plugin("timeline").service("schedule").runCleanup();
|
|
278
|
+
s.log.info(`[Timeline] Cleanup complete: ${JSON.stringify(results)}`);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
s.log.error(`[Timeline] Cleanup failed: ${err}`);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
options: settings.cleanupCron
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
strapi.log.info(`[Timeline] Registered cleanup cron: ${settings.cleanupCron}`);
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
strapi.log.error(`[Timeline] Failed to register cleanup cron: ${error}`);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const destroy = ({ strapi }) => {
|
|
293
|
+
};
|
|
294
|
+
const register = ({ strapi }) => {
|
|
295
|
+
const actions = [
|
|
296
|
+
{
|
|
297
|
+
uid: "entries.read",
|
|
298
|
+
displayName: "Read timeline entries",
|
|
299
|
+
pluginName: "timeline",
|
|
300
|
+
section: "plugins"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
uid: "entries.delete",
|
|
304
|
+
displayName: "Delete all timeline entries",
|
|
305
|
+
pluginName: "timeline",
|
|
306
|
+
section: "plugins"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
uid: "settings.read",
|
|
310
|
+
displayName: "Read timeline settings",
|
|
311
|
+
pluginName: "timeline",
|
|
312
|
+
section: "plugins"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
uid: "settings.edit",
|
|
316
|
+
displayName: "Edit timeline settings",
|
|
317
|
+
pluginName: "timeline",
|
|
318
|
+
section: "plugins"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
uid: "schedule.read",
|
|
322
|
+
displayName: "Read timeline schedule",
|
|
323
|
+
pluginName: "timeline",
|
|
324
|
+
section: "plugins"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
uid: "schedule.run",
|
|
328
|
+
displayName: "Run timeline cleanup",
|
|
329
|
+
pluginName: "timeline",
|
|
330
|
+
section: "plugins"
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
strapi.admin?.services.permission.actionProvider.registerMany(actions);
|
|
334
|
+
};
|
|
335
|
+
const config = {
|
|
336
|
+
default: {},
|
|
337
|
+
validator() {
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const kind$1 = "singleType";
|
|
341
|
+
const collectionName$1 = "timeline_settings";
|
|
342
|
+
const info$1 = { "singularName": "timeline-setting", "pluralName": "timeline-settings", "displayName": "Timeline Settings" };
|
|
343
|
+
const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
344
|
+
const options$1 = { "draftAndPublish": false };
|
|
345
|
+
const attributes$1 = { "trackedContentTypes": { "type": "json", "default": [] }, "contentTypeConfigs": { "type": "json", "default": {} }, "cleanupEnabled": { "type": "boolean", "default": false }, "cleanupCron": { "type": "string", "default": "0 * * * *" }, "lastCleanupAt": { "type": "datetime" }, "lastCleanupResults": { "type": "json" } };
|
|
346
|
+
const schema$1 = {
|
|
347
|
+
kind: kind$1,
|
|
348
|
+
collectionName: collectionName$1,
|
|
349
|
+
info: info$1,
|
|
350
|
+
pluginOptions: pluginOptions$1,
|
|
351
|
+
options: options$1,
|
|
352
|
+
attributes: attributes$1
|
|
353
|
+
};
|
|
354
|
+
const timelineSetting = {
|
|
355
|
+
schema: schema$1
|
|
356
|
+
};
|
|
357
|
+
const kind = "collectionType";
|
|
358
|
+
const collectionName = "timeline_entries";
|
|
359
|
+
const info = { "singularName": "timeline-entry", "pluralName": "timeline-entries", "displayName": "Timeline Entry" };
|
|
360
|
+
const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
361
|
+
const options = { "draftAndPublish": false };
|
|
362
|
+
const attributes = { "action": { "type": "string", "required": true }, "contentType": { "type": "string", "required": true }, "entryDocumentId": { "type": "string", "required": true }, "content": { "type": "json" }, "userId": { "type": "integer" }, "userName": { "type": "string" }, "locale": { "type": "string" }, "contentTypeSchema": { "type": "json" } };
|
|
363
|
+
const schema = {
|
|
364
|
+
kind,
|
|
365
|
+
collectionName,
|
|
366
|
+
info,
|
|
367
|
+
pluginOptions,
|
|
368
|
+
options,
|
|
369
|
+
attributes
|
|
370
|
+
};
|
|
371
|
+
const timelineEntry = {
|
|
372
|
+
schema
|
|
373
|
+
};
|
|
374
|
+
const contentTypes = {
|
|
375
|
+
"timeline-setting": timelineSetting,
|
|
376
|
+
"timeline-entry": timelineEntry
|
|
377
|
+
};
|
|
378
|
+
const controller = ({ strapi }) => ({
|
|
379
|
+
index(ctx) {
|
|
380
|
+
ctx.body = strapi.plugin("timeline").service("service").getWelcomeMessage();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
const settingsController = ({ strapi }) => ({
|
|
384
|
+
async getSettings(ctx) {
|
|
385
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
386
|
+
ctx.body = { data: settings };
|
|
387
|
+
},
|
|
388
|
+
async setSettings(ctx) {
|
|
389
|
+
const { contentTypeConfigs } = ctx.request.body;
|
|
390
|
+
const settings = await strapi.plugin("timeline").service("settings").setSettings({
|
|
391
|
+
contentTypeConfigs: contentTypeConfigs || {}
|
|
392
|
+
});
|
|
393
|
+
ctx.body = { data: settings };
|
|
394
|
+
},
|
|
395
|
+
async getContentTypes(ctx) {
|
|
396
|
+
const contentTypes2 = await strapi.plugin("timeline").service("settings").getContentTypes();
|
|
397
|
+
ctx.body = { data: contentTypes2 };
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
const timelineController = ({ strapi }) => ({
|
|
401
|
+
async find(ctx) {
|
|
402
|
+
const {
|
|
403
|
+
contentType,
|
|
404
|
+
entryDocumentId,
|
|
405
|
+
userId,
|
|
406
|
+
action,
|
|
407
|
+
locale,
|
|
408
|
+
dateFrom,
|
|
409
|
+
dateTo,
|
|
410
|
+
sort,
|
|
411
|
+
sortOrder,
|
|
412
|
+
page = "1",
|
|
413
|
+
pageSize = "20"
|
|
414
|
+
} = ctx.query;
|
|
415
|
+
const result = await strapi.plugin("timeline").service("timeline").findEntries({
|
|
416
|
+
contentType: contentType || void 0,
|
|
417
|
+
entryDocumentId: entryDocumentId || void 0,
|
|
418
|
+
userId: userId || void 0,
|
|
419
|
+
action: action || void 0,
|
|
420
|
+
locale: locale || void 0,
|
|
421
|
+
dateFrom: dateFrom || void 0,
|
|
422
|
+
dateTo: dateTo || void 0,
|
|
423
|
+
sort: sort || void 0,
|
|
424
|
+
sortOrder: sortOrder || void 0,
|
|
425
|
+
page: parseInt(page, 10),
|
|
426
|
+
pageSize: parseInt(pageSize, 10)
|
|
427
|
+
});
|
|
428
|
+
ctx.body = { data: result.results, meta: { pagination: result.pagination } };
|
|
429
|
+
},
|
|
430
|
+
async findByDocument(ctx) {
|
|
431
|
+
const contentType = decodeURIComponent(ctx.params.contentType || "");
|
|
432
|
+
const entryDocumentId = decodeURIComponent(ctx.params.entryDocumentId || "");
|
|
433
|
+
if (!contentType || !entryDocumentId) {
|
|
434
|
+
return ctx.badRequest("contentType and entryDocumentId are required");
|
|
435
|
+
}
|
|
436
|
+
const entries = await strapi.plugin("timeline").service("timeline").findByDocumentId(contentType, entryDocumentId);
|
|
437
|
+
ctx.body = { data: entries };
|
|
438
|
+
},
|
|
439
|
+
async getUsers(ctx) {
|
|
440
|
+
const users = await strapi.plugin("timeline").service("timeline").getDistinctUsers();
|
|
441
|
+
ctx.body = { data: users };
|
|
442
|
+
},
|
|
443
|
+
async getLocales(ctx) {
|
|
444
|
+
const locales = await strapi.plugin("timeline").service("timeline").getDistinctLocales();
|
|
445
|
+
ctx.body = { data: locales };
|
|
446
|
+
},
|
|
447
|
+
async deleteAll(ctx) {
|
|
448
|
+
await strapi.plugin("timeline").service("timeline").deleteAll();
|
|
449
|
+
ctx.body = { data: { success: true } };
|
|
450
|
+
},
|
|
451
|
+
async clean(ctx) {
|
|
452
|
+
const { contentTypes: contentTypesToClean } = ctx.request.body || {};
|
|
453
|
+
if (!Array.isArray(contentTypesToClean) || contentTypesToClean.length === 0) {
|
|
454
|
+
return ctx.badRequest("contentTypes array is required");
|
|
455
|
+
}
|
|
456
|
+
const validUids = contentTypesToClean.filter(
|
|
457
|
+
(uid) => typeof uid === "string" && uid.startsWith("api::")
|
|
458
|
+
);
|
|
459
|
+
if (validUids.length === 0) {
|
|
460
|
+
return ctx.badRequest("No valid content type UIDs provided");
|
|
461
|
+
}
|
|
462
|
+
const user = ctx.state?.user;
|
|
463
|
+
const userId = user?.id || null;
|
|
464
|
+
const userName = user ? [user.firstname, user.lastname].filter(Boolean).join(" ") || user.email || `User #${user.id}` : null;
|
|
465
|
+
const deleted = await strapi.plugin("timeline").service("timeline").deleteByContentTypes(validUids);
|
|
466
|
+
for (const uid of validUids) {
|
|
467
|
+
await strapi.plugin("timeline").service("timeline").createSnapshot({
|
|
468
|
+
action: "clean",
|
|
469
|
+
contentType: uid,
|
|
470
|
+
entryDocumentId: "CLEAN",
|
|
471
|
+
content: { cleanedAt: (/* @__PURE__ */ new Date()).toISOString(), deletedCount: deleted },
|
|
472
|
+
userId,
|
|
473
|
+
userName
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
ctx.body = { data: { success: true, deleted } };
|
|
477
|
+
},
|
|
478
|
+
async snapshotBeforeRestore(ctx) {
|
|
479
|
+
const { contentType, entryDocumentId, locale, restoreData } = ctx.request.body || {};
|
|
480
|
+
if (!contentType || !entryDocumentId) {
|
|
481
|
+
return ctx.badRequest("contentType and entryDocumentId are required");
|
|
482
|
+
}
|
|
483
|
+
const user = ctx.state?.user;
|
|
484
|
+
const userId = user?.id || null;
|
|
485
|
+
const userName = user ? [user.firstname, user.lastname].filter(Boolean).join(" ") || user.email || `User #${user.id}` : null;
|
|
486
|
+
const restoreKey = `${contentType}:${entryDocumentId}`;
|
|
487
|
+
try {
|
|
488
|
+
const docService = strapi.documents(contentType);
|
|
489
|
+
const findParams = { documentId: entryDocumentId };
|
|
490
|
+
if (locale) findParams.locale = locale;
|
|
491
|
+
const currentDoc = await docService.findOne(findParams);
|
|
492
|
+
if (!currentDoc) {
|
|
493
|
+
ctx.body = { data: { success: false, reason: "Entry not found" } };
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (restoreData && typeof restoreData === "object") {
|
|
497
|
+
markRestoreInProgress(restoreKey);
|
|
498
|
+
try {
|
|
499
|
+
const updateParams = {
|
|
500
|
+
documentId: entryDocumentId,
|
|
501
|
+
data: restoreData
|
|
502
|
+
};
|
|
503
|
+
if (locale) updateParams.locale = locale;
|
|
504
|
+
await docService.update(updateParams);
|
|
505
|
+
} finally {
|
|
506
|
+
clearRestoreInProgress(restoreKey);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const updatedDoc = restoreData ? await docService.findOne(findParams) : null;
|
|
510
|
+
await strapi.plugin("timeline").service("timeline").createSnapshot({
|
|
511
|
+
action: "restore",
|
|
512
|
+
contentType,
|
|
513
|
+
entryDocumentId,
|
|
514
|
+
content: updatedDoc || currentDoc,
|
|
515
|
+
userId,
|
|
516
|
+
userName,
|
|
517
|
+
locale: locale || currentDoc.locale || null
|
|
518
|
+
});
|
|
519
|
+
await strapi.plugin("timeline").service("timeline").enforceEntriesLimit(contentType);
|
|
520
|
+
ctx.body = { data: { success: true } };
|
|
521
|
+
} catch (error) {
|
|
522
|
+
clearRestoreInProgress(restoreKey);
|
|
523
|
+
strapi.log.error(`[Timeline] Failed to restore: ${error}`);
|
|
524
|
+
ctx.internalServerError("Restore failed");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
const scheduleController = ({ strapi }) => ({
|
|
529
|
+
async getStatus(ctx) {
|
|
530
|
+
const status = await strapi.plugin("timeline").service("schedule").getStatus();
|
|
531
|
+
ctx.body = { data: status };
|
|
532
|
+
},
|
|
533
|
+
async runCleanup(ctx) {
|
|
534
|
+
const results = await strapi.plugin("timeline").service("schedule").runCleanup();
|
|
535
|
+
ctx.body = { data: { results, ranAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
536
|
+
},
|
|
537
|
+
async updateSchedule(ctx) {
|
|
538
|
+
const { cleanupEnabled, cleanupCron } = ctx.request.body;
|
|
539
|
+
await strapi.plugin("timeline").service("settings").updateScheduleConfig({
|
|
540
|
+
cleanupEnabled,
|
|
541
|
+
cleanupCron
|
|
542
|
+
});
|
|
543
|
+
ctx.body = {
|
|
544
|
+
data: {
|
|
545
|
+
success: true,
|
|
546
|
+
note: "Cron schedule changes take effect after server restart."
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const controllers = {
|
|
552
|
+
controller,
|
|
553
|
+
settings: settingsController,
|
|
554
|
+
timeline: timelineController,
|
|
555
|
+
schedule: scheduleController
|
|
556
|
+
};
|
|
557
|
+
const middlewares = {};
|
|
558
|
+
const policies = {};
|
|
559
|
+
const contentAPIRoutes = () => ({
|
|
560
|
+
type: "content-api",
|
|
561
|
+
routes: [
|
|
562
|
+
{
|
|
563
|
+
method: "GET",
|
|
564
|
+
path: "/",
|
|
565
|
+
// name of the controller file & the method.
|
|
566
|
+
handler: "controller.index",
|
|
567
|
+
config: {
|
|
568
|
+
policies: []
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
});
|
|
573
|
+
const adminAPIRoutes = () => ({
|
|
574
|
+
type: "admin",
|
|
575
|
+
routes: [
|
|
576
|
+
{
|
|
577
|
+
method: "GET",
|
|
578
|
+
path: "/settings",
|
|
579
|
+
handler: "settings.getSettings",
|
|
580
|
+
config: {
|
|
581
|
+
policies: [
|
|
582
|
+
"admin::isAuthenticatedAdmin",
|
|
583
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.settings.read"] } }
|
|
584
|
+
]
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
method: "PUT",
|
|
589
|
+
path: "/settings",
|
|
590
|
+
handler: "settings.setSettings",
|
|
591
|
+
config: {
|
|
592
|
+
policies: [
|
|
593
|
+
"admin::isAuthenticatedAdmin",
|
|
594
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.settings.edit"] } }
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
method: "GET",
|
|
600
|
+
path: "/content-types",
|
|
601
|
+
handler: "settings.getContentTypes",
|
|
602
|
+
config: {
|
|
603
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
method: "GET",
|
|
608
|
+
path: "/entries",
|
|
609
|
+
handler: "timeline.find",
|
|
610
|
+
config: {
|
|
611
|
+
policies: [
|
|
612
|
+
"admin::isAuthenticatedAdmin",
|
|
613
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.read"] } }
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
method: "GET",
|
|
619
|
+
path: "/entries/users",
|
|
620
|
+
handler: "timeline.getUsers",
|
|
621
|
+
config: {
|
|
622
|
+
policies: [
|
|
623
|
+
"admin::isAuthenticatedAdmin",
|
|
624
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.read"] } }
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
method: "GET",
|
|
630
|
+
path: "/entries/locales",
|
|
631
|
+
handler: "timeline.getLocales",
|
|
632
|
+
config: {
|
|
633
|
+
policies: [
|
|
634
|
+
"admin::isAuthenticatedAdmin",
|
|
635
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.read"] } }
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
method: "DELETE",
|
|
641
|
+
path: "/entries",
|
|
642
|
+
handler: "timeline.deleteAll",
|
|
643
|
+
config: {
|
|
644
|
+
policies: [
|
|
645
|
+
"admin::isAuthenticatedAdmin",
|
|
646
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.delete"] } }
|
|
647
|
+
]
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
method: "POST",
|
|
652
|
+
path: "/entries/clean",
|
|
653
|
+
handler: "timeline.clean",
|
|
654
|
+
config: {
|
|
655
|
+
policies: [
|
|
656
|
+
"admin::isAuthenticatedAdmin",
|
|
657
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.delete"] } }
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
method: "POST",
|
|
663
|
+
path: "/entries/snapshot-before-restore",
|
|
664
|
+
handler: "timeline.snapshotBeforeRestore",
|
|
665
|
+
config: {
|
|
666
|
+
policies: [
|
|
667
|
+
"admin::isAuthenticatedAdmin",
|
|
668
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.read"] } }
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
method: "GET",
|
|
674
|
+
path: "/entries/:contentType/:entryDocumentId",
|
|
675
|
+
handler: "timeline.findByDocument",
|
|
676
|
+
config: {
|
|
677
|
+
policies: [
|
|
678
|
+
"admin::isAuthenticatedAdmin",
|
|
679
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.entries.read"] } }
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
method: "GET",
|
|
685
|
+
path: "/schedule",
|
|
686
|
+
handler: "schedule.getStatus",
|
|
687
|
+
config: {
|
|
688
|
+
policies: [
|
|
689
|
+
"admin::isAuthenticatedAdmin",
|
|
690
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.schedule.read"] } }
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
method: "POST",
|
|
696
|
+
path: "/schedule/run",
|
|
697
|
+
handler: "schedule.runCleanup",
|
|
698
|
+
config: {
|
|
699
|
+
policies: [
|
|
700
|
+
"admin::isAuthenticatedAdmin",
|
|
701
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.schedule.run"] } }
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
method: "PUT",
|
|
707
|
+
path: "/schedule",
|
|
708
|
+
handler: "schedule.updateSchedule",
|
|
709
|
+
config: {
|
|
710
|
+
policies: [
|
|
711
|
+
"admin::isAuthenticatedAdmin",
|
|
712
|
+
{ name: "admin::hasPermissions", config: { actions: ["plugin::timeline.settings.edit"] } }
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
});
|
|
718
|
+
const routes = {
|
|
719
|
+
"content-api": contentAPIRoutes,
|
|
720
|
+
admin: adminAPIRoutes
|
|
721
|
+
};
|
|
722
|
+
const service = ({ strapi }) => ({
|
|
723
|
+
getWelcomeMessage() {
|
|
724
|
+
return "Welcome to Strapi 🚀";
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
const SETTINGS_UID = "plugin::timeline.timeline-setting";
|
|
728
|
+
const DEFAULT_ACTIONS = ["create", "update", "delete", "publish"];
|
|
729
|
+
const settingsService = ({ strapi }) => ({
|
|
730
|
+
async getSettings() {
|
|
731
|
+
const raw = await strapi.db.query(SETTINGS_UID).findOne({ where: {} });
|
|
732
|
+
if (!raw) {
|
|
733
|
+
return {
|
|
734
|
+
contentTypeConfigs: {},
|
|
735
|
+
cleanupEnabled: false,
|
|
736
|
+
cleanupCron: "0 * * * *",
|
|
737
|
+
lastCleanupAt: null,
|
|
738
|
+
lastCleanupResults: null
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
let contentTypeConfigs = raw.contentTypeConfigs || {};
|
|
742
|
+
const oldList = raw.trackedContentTypes || [];
|
|
743
|
+
if (Object.keys(contentTypeConfigs).length === 0 && Array.isArray(oldList) && oldList.length > 0) {
|
|
744
|
+
for (const uid of oldList) {
|
|
745
|
+
if (typeof uid === "string" && uid) {
|
|
746
|
+
contentTypeConfigs[uid] = {
|
|
747
|
+
enabled: true,
|
|
748
|
+
actions: [...DEFAULT_ACTIONS],
|
|
749
|
+
entriesLimit: 0,
|
|
750
|
+
durationLimit: 0,
|
|
751
|
+
durationUnit: "days"
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
await strapi.db.query(SETTINGS_UID).update({
|
|
756
|
+
where: { id: raw.id },
|
|
757
|
+
data: { contentTypeConfigs, trackedContentTypes: [] }
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
contentTypeConfigs,
|
|
762
|
+
cleanupEnabled: raw.cleanupEnabled ?? false,
|
|
763
|
+
cleanupCron: raw.cleanupCron || "0 * * * *",
|
|
764
|
+
lastCleanupAt: raw.lastCleanupAt || null,
|
|
765
|
+
lastCleanupResults: raw.lastCleanupResults || null
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
async setSettings(data) {
|
|
769
|
+
const existing = await strapi.db.query(SETTINGS_UID).findOne({ where: {} });
|
|
770
|
+
const payload = {
|
|
771
|
+
contentTypeConfigs: data.contentTypeConfigs,
|
|
772
|
+
trackedContentTypes: []
|
|
773
|
+
// clear old format
|
|
774
|
+
};
|
|
775
|
+
if (existing) {
|
|
776
|
+
return strapi.db.query(SETTINGS_UID).update({
|
|
777
|
+
where: { id: existing.id },
|
|
778
|
+
data: payload
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return strapi.db.query(SETTINGS_UID).create({ data: payload });
|
|
782
|
+
},
|
|
783
|
+
async updateScheduleConfig(data) {
|
|
784
|
+
const existing = await strapi.db.query(SETTINGS_UID).findOne({ where: {} });
|
|
785
|
+
const payload = {};
|
|
786
|
+
if (data.cleanupEnabled !== void 0) payload.cleanupEnabled = data.cleanupEnabled;
|
|
787
|
+
if (data.cleanupCron !== void 0) payload.cleanupCron = data.cleanupCron;
|
|
788
|
+
if (existing) {
|
|
789
|
+
return strapi.db.query(SETTINGS_UID).update({
|
|
790
|
+
where: { id: existing.id },
|
|
791
|
+
data: payload
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return strapi.db.query(SETTINGS_UID).create({ data: payload });
|
|
795
|
+
},
|
|
796
|
+
async updateCleanupResults(data) {
|
|
797
|
+
const existing = await strapi.db.query(SETTINGS_UID).findOne({ where: {} });
|
|
798
|
+
if (existing) {
|
|
799
|
+
return strapi.db.query(SETTINGS_UID).update({
|
|
800
|
+
where: { id: existing.id },
|
|
801
|
+
data
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
async getContentTypes() {
|
|
806
|
+
return Object.entries(strapi.contentTypes).filter(([uid]) => uid.startsWith("api::")).map(([uid, ct]) => ({
|
|
807
|
+
uid,
|
|
808
|
+
displayName: ct.info?.displayName || uid,
|
|
809
|
+
kind: ct.kind
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
const ENTRY_UID = "plugin::timeline.timeline-entry";
|
|
814
|
+
const timelineService = ({ strapi }) => ({
|
|
815
|
+
async createSnapshot(params) {
|
|
816
|
+
try {
|
|
817
|
+
const result = await strapi.db.query(ENTRY_UID).create({
|
|
818
|
+
data: {
|
|
819
|
+
action: params.action,
|
|
820
|
+
contentType: params.contentType,
|
|
821
|
+
entryDocumentId: params.entryDocumentId,
|
|
822
|
+
content: params.content,
|
|
823
|
+
contentTypeSchema: params.contentTypeSchema || null,
|
|
824
|
+
userId: params.userId ? Number(params.userId) : null,
|
|
825
|
+
userName: params.userName || null,
|
|
826
|
+
locale: params.locale || null
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
strapi.log.info(
|
|
830
|
+
`[Timeline] Snapshot: ${params.action} on ${params.contentType} (doc: ${params.entryDocumentId})`
|
|
831
|
+
);
|
|
832
|
+
return result;
|
|
833
|
+
} catch (error) {
|
|
834
|
+
strapi.log.error(`[Timeline] Failed to create snapshot: ${error}`);
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
async findEntries(params) {
|
|
839
|
+
const {
|
|
840
|
+
contentType,
|
|
841
|
+
entryDocumentId,
|
|
842
|
+
userId,
|
|
843
|
+
action,
|
|
844
|
+
locale,
|
|
845
|
+
dateFrom,
|
|
846
|
+
dateTo,
|
|
847
|
+
sort = "createdAt",
|
|
848
|
+
sortOrder = "desc",
|
|
849
|
+
page = 1,
|
|
850
|
+
pageSize = 20
|
|
851
|
+
} = params;
|
|
852
|
+
const where = {};
|
|
853
|
+
if (contentType) {
|
|
854
|
+
const ctArr = Array.isArray(contentType) ? contentType : [contentType];
|
|
855
|
+
if (ctArr.length === 1) where.contentType = ctArr[0];
|
|
856
|
+
else if (ctArr.length > 1) where.contentType = { $in: ctArr };
|
|
857
|
+
}
|
|
858
|
+
if (entryDocumentId) where.entryDocumentId = entryDocumentId;
|
|
859
|
+
if (userId) {
|
|
860
|
+
const uArr = Array.isArray(userId) ? userId.map(Number) : [Number(userId)];
|
|
861
|
+
if (uArr.length === 1) where.userId = uArr[0];
|
|
862
|
+
else if (uArr.length > 1) where.userId = { $in: uArr };
|
|
863
|
+
}
|
|
864
|
+
if (action) {
|
|
865
|
+
const aArr = Array.isArray(action) ? action : [action];
|
|
866
|
+
if (aArr.length === 1) where.action = aArr[0];
|
|
867
|
+
else if (aArr.length > 1) where.action = { $in: aArr };
|
|
868
|
+
}
|
|
869
|
+
if (locale) {
|
|
870
|
+
const lArr = Array.isArray(locale) ? locale : [locale];
|
|
871
|
+
if (lArr.length === 1) where.locale = lArr[0];
|
|
872
|
+
else if (lArr.length > 1) where.locale = { $in: lArr };
|
|
873
|
+
}
|
|
874
|
+
if (dateFrom || dateTo) {
|
|
875
|
+
where.createdAt = {};
|
|
876
|
+
if (dateFrom) where.createdAt.$gte = new Date(dateFrom);
|
|
877
|
+
if (dateTo) {
|
|
878
|
+
const end = new Date(dateTo);
|
|
879
|
+
end.setHours(23, 59, 59, 999);
|
|
880
|
+
where.createdAt.$lte = end;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const allowedSortFields = ["createdAt", "action", "contentType", "userName"];
|
|
884
|
+
const sortField = allowedSortFields.includes(sort) ? sort : "createdAt";
|
|
885
|
+
const [entries, count] = await Promise.all([
|
|
886
|
+
strapi.db.query(ENTRY_UID).findMany({
|
|
887
|
+
where,
|
|
888
|
+
orderBy: { [sortField]: sortOrder },
|
|
889
|
+
offset: (page - 1) * pageSize,
|
|
890
|
+
limit: pageSize
|
|
891
|
+
}),
|
|
892
|
+
strapi.db.query(ENTRY_UID).count({ where })
|
|
893
|
+
]);
|
|
894
|
+
return {
|
|
895
|
+
results: entries,
|
|
896
|
+
pagination: {
|
|
897
|
+
page,
|
|
898
|
+
pageSize,
|
|
899
|
+
total: count,
|
|
900
|
+
pageCount: Math.ceil(count / pageSize)
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
},
|
|
904
|
+
async getDistinctUsers() {
|
|
905
|
+
const knex = strapi.db.connection;
|
|
906
|
+
const rows = await knex("timeline_entries").select("user_id", "user_name").whereNotNull("user_id").groupBy("user_id", "user_name").orderBy("user_name", "asc");
|
|
907
|
+
return rows.map((r) => ({
|
|
908
|
+
userId: r.user_id,
|
|
909
|
+
userName: r.user_name || `User #${r.user_id}`
|
|
910
|
+
}));
|
|
911
|
+
},
|
|
912
|
+
async getDistinctLocales() {
|
|
913
|
+
const knex = strapi.db.connection;
|
|
914
|
+
const rows = await knex("timeline_entries").select("locale").whereNotNull("locale").where("locale", "!=", "").groupBy("locale").orderBy("locale", "asc");
|
|
915
|
+
return rows.map((r) => r.locale);
|
|
916
|
+
},
|
|
917
|
+
async findByDocumentId(contentType, entryDocumentId) {
|
|
918
|
+
return strapi.db.query(ENTRY_UID).findMany({
|
|
919
|
+
where: { contentType, entryDocumentId },
|
|
920
|
+
orderBy: { createdAt: "desc" }
|
|
921
|
+
});
|
|
922
|
+
},
|
|
923
|
+
async deleteAll() {
|
|
924
|
+
const knex = strapi.db.connection;
|
|
925
|
+
await knex("timeline_entries").delete();
|
|
926
|
+
},
|
|
927
|
+
async deleteByContentTypes(contentTypes2) {
|
|
928
|
+
if (!contentTypes2.length) return 0;
|
|
929
|
+
const knex = strapi.db.connection;
|
|
930
|
+
return knex("timeline_entries").whereIn("content_type", contentTypes2).delete();
|
|
931
|
+
},
|
|
932
|
+
async enforceEntriesLimit(contentType) {
|
|
933
|
+
try {
|
|
934
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
935
|
+
const config2 = settings.contentTypeConfigs?.[contentType];
|
|
936
|
+
if (!config2?.entriesLimit || config2.entriesLimit <= 0) return;
|
|
937
|
+
const count = await strapi.db.query(ENTRY_UID).count({ where: { contentType } });
|
|
938
|
+
if (count <= config2.entriesLimit) return;
|
|
939
|
+
const excess = count - config2.entriesLimit;
|
|
940
|
+
const oldEntries = await strapi.db.query(ENTRY_UID).findMany({
|
|
941
|
+
where: { contentType },
|
|
942
|
+
orderBy: { createdAt: "asc" },
|
|
943
|
+
limit: excess,
|
|
944
|
+
select: ["id"]
|
|
945
|
+
});
|
|
946
|
+
if (oldEntries.length > 0) {
|
|
947
|
+
const ids = oldEntries.map((e) => e.id);
|
|
948
|
+
await strapi.db.query(ENTRY_UID).deleteMany({ where: { id: { $in: ids } } });
|
|
949
|
+
strapi.log.info(`[Timeline] Enforced entries limit for ${contentType}: removed ${ids.length} old entries`);
|
|
950
|
+
}
|
|
951
|
+
} catch (error) {
|
|
952
|
+
strapi.log.error(`[Timeline] Failed to enforce entries limit: ${error}`);
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
async deleteByAge(contentType, cutoffDate) {
|
|
956
|
+
const knex = strapi.db.connection;
|
|
957
|
+
return knex("timeline_entries").where("content_type", contentType).where("created_at", "<", cutoffDate).delete();
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
const scheduleService = ({ strapi }) => ({
|
|
961
|
+
async getStatus() {
|
|
962
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
963
|
+
const contentTypeConfigs = settings.contentTypeConfigs || {};
|
|
964
|
+
const durationsConfigured = {};
|
|
965
|
+
for (const [uid, config2] of Object.entries(contentTypeConfigs)) {
|
|
966
|
+
if (config2.enabled && config2.durationLimit > 0) {
|
|
967
|
+
const ct = strapi.contentTypes[uid];
|
|
968
|
+
durationsConfigured[uid] = {
|
|
969
|
+
durationLimit: config2.durationLimit,
|
|
970
|
+
durationUnit: config2.durationUnit,
|
|
971
|
+
displayName: ct?.info?.displayName || uid
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
cleanupEnabled: settings.cleanupEnabled,
|
|
977
|
+
cleanupCron: settings.cleanupCron,
|
|
978
|
+
lastCleanupAt: settings.lastCleanupAt,
|
|
979
|
+
lastCleanupResults: settings.lastCleanupResults,
|
|
980
|
+
durationsConfigured
|
|
981
|
+
};
|
|
982
|
+
},
|
|
983
|
+
async runCleanup() {
|
|
984
|
+
const settings = await strapi.plugin("timeline").service("settings").getSettings();
|
|
985
|
+
const configs = settings.contentTypeConfigs || {};
|
|
986
|
+
const results = {};
|
|
987
|
+
for (const [uid, config2] of Object.entries(configs)) {
|
|
988
|
+
if (!config2.enabled || !config2.durationLimit || config2.durationLimit <= 0) continue;
|
|
989
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
990
|
+
switch (config2.durationUnit) {
|
|
991
|
+
case "hours":
|
|
992
|
+
cutoff.setHours(cutoff.getHours() - config2.durationLimit);
|
|
993
|
+
break;
|
|
994
|
+
case "days":
|
|
995
|
+
cutoff.setDate(cutoff.getDate() - config2.durationLimit);
|
|
996
|
+
break;
|
|
997
|
+
case "weeks":
|
|
998
|
+
cutoff.setDate(cutoff.getDate() - config2.durationLimit * 7);
|
|
999
|
+
break;
|
|
1000
|
+
case "months":
|
|
1001
|
+
cutoff.setMonth(cutoff.getMonth() - config2.durationLimit);
|
|
1002
|
+
break;
|
|
1003
|
+
default:
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const deleted = await strapi.plugin("timeline").service("timeline").deleteByAge(uid, cutoff);
|
|
1007
|
+
if (deleted > 0) {
|
|
1008
|
+
results[uid] = { deleted };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
await strapi.plugin("timeline").service("settings").updateCleanupResults({
|
|
1012
|
+
lastCleanupAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1013
|
+
lastCleanupResults: results
|
|
1014
|
+
});
|
|
1015
|
+
strapi.log.info(`[Timeline] Cleanup complete: ${JSON.stringify(results)}`);
|
|
1016
|
+
return results;
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
const services = {
|
|
1020
|
+
service,
|
|
1021
|
+
settings: settingsService,
|
|
1022
|
+
timeline: timelineService,
|
|
1023
|
+
schedule: scheduleService
|
|
1024
|
+
};
|
|
1025
|
+
const index = {
|
|
1026
|
+
register,
|
|
1027
|
+
bootstrap,
|
|
1028
|
+
destroy,
|
|
1029
|
+
config,
|
|
1030
|
+
controllers,
|
|
1031
|
+
routes,
|
|
1032
|
+
services,
|
|
1033
|
+
contentTypes,
|
|
1034
|
+
policies,
|
|
1035
|
+
middlewares
|
|
1036
|
+
};
|
|
1037
|
+
exports.default = index;
|