@aduptive/strapi-blockscene 2.0.0-alpha.6 → 2.0.0-alpha.7
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 +293 -38
- package/dist/admin.cjs +3303 -1302
- package/dist/admin.mjs +3326 -1302
- package/dist/server.cjs +620 -37
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -146,24 +146,37 @@ var require_settings = __commonJS({
|
|
|
146
146
|
duplicate: true,
|
|
147
147
|
clipboard: true,
|
|
148
148
|
// Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
|
|
149
|
-
friendlyLabels: true
|
|
149
|
+
friendlyLabels: true,
|
|
150
|
+
// Page preview pane (Strapi 5): which toolbar controls show, in order, and the width menu's entries.
|
|
151
|
+
previewToolbar: ["modes", "history", "devices", "status", "actions"],
|
|
152
|
+
previewDevices: ["fit", "mobile", "tablet", "desktop"]
|
|
150
153
|
},
|
|
151
154
|
// Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
|
|
152
155
|
// previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
|
|
153
156
|
contentTypes: {},
|
|
154
157
|
// Version history (Strapi 5, see README "Version history"): off until turned on. contentTypes: 'all' (every api:: type)
|
|
155
|
-
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays
|
|
156
|
-
|
|
158
|
+
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays; deleted
|
|
159
|
+
// documents stay in the trash trashDays (fixed per document when it is deleted).
|
|
160
|
+
history: { enabled: false, contentTypes: "all", retentionDays: 90, maxSnapshots: 100, eventDays: 365, trashDays: 90 }
|
|
157
161
|
};
|
|
158
162
|
var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
|
|
159
163
|
var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
|
|
160
164
|
var OPENS = ["modal", "drawer"];
|
|
161
165
|
var LABEL = /^[^<>]{1,40}$/;
|
|
166
|
+
var TOOLBAR = ["modes", "history", "devices", "status", "actions"];
|
|
167
|
+
var DEVICE_NAMES = ["fit", "mobile", "tablet", "desktop"];
|
|
168
|
+
var previewToolbar = (value) => Array.isArray(value) && value.every((id) => TOOLBAR.includes(id)) && new Set(value).size === value.length;
|
|
169
|
+
var DEVICE_LABEL = /^[^<>]{1,24}$/;
|
|
170
|
+
var customDevice = (item) => item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).every((key) => ["label", "width"].includes(key)) && typeof item.label === "string" && DEVICE_LABEL.test(item.label.trim()) && Number.isInteger(item.width) && item.width >= 240 && item.width <= 3840;
|
|
171
|
+
var previewDevices = (value) => Array.isArray(value) && value.length >= 1 && value.length <= 8 && value.every((item) => DEVICE_NAMES.includes(item) || customDevice(item)) && new Set(value.map((item) => typeof item === "string" ? item : item.width)).size === value.length;
|
|
172
|
+
var cleanDevices = (value) => value.map((item) => typeof item === "string" ? item : { label: item.label.trim(), width: item.width });
|
|
162
173
|
var sidebarItem = (item, attributes) => item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).every((key) => ["label", "icon", "open", "fields"].includes(key)) && typeof item.label === "string" && LABEL.test(item.label.trim()) && (item.icon === void 0 || item.icon === "" || ICONS.includes(item.icon)) && OPENS.includes(item.open) && Array.isArray(item.fields) && item.fields.length > 0 && item.fields.length <= 30 && item.fields.every((field) => typeof field === "string" && (!attributes || attributes.includes(field)));
|
|
163
174
|
var TYPE_KEYS = {
|
|
164
175
|
enabled: (value) => typeof value === "boolean",
|
|
165
176
|
previewMode: (value) => PREVIEW_MODES.includes(value),
|
|
166
177
|
sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
|
|
178
|
+
previewToolbar,
|
|
179
|
+
previewDevices,
|
|
167
180
|
sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
|
|
168
181
|
};
|
|
169
182
|
var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
|
|
@@ -180,7 +193,8 @@ var require_settings = __commonJS({
|
|
|
180
193
|
contentTypes: (value, uids) => value === "all" || Array.isArray(value) && value.length <= 500 && value.every((uid) => typeof uid === "string" && API_UID.test(uid) && (!uids || uids.includes(uid))),
|
|
181
194
|
retentionDays: days,
|
|
182
195
|
maxSnapshots: (value) => Number.isInteger(value) && value >= 1 && value <= 1e3,
|
|
183
|
-
eventDays: days
|
|
196
|
+
eventDays: days,
|
|
197
|
+
trashDays: days
|
|
184
198
|
};
|
|
185
199
|
var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
|
|
186
200
|
var TAG = /^[^<>\n]{1,24}$/;
|
|
@@ -281,9 +295,9 @@ var require_settings = __commonJS({
|
|
|
281
295
|
}
|
|
282
296
|
for (const [key, value] of Object.entries(input.editor || {})) {
|
|
283
297
|
if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
|
|
284
|
-
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : typeof value === "boolean";
|
|
298
|
+
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : key === "previewToolbar" ? previewToolbar(value) : key === "previewDevices" ? previewDevices(value) : typeof value === "boolean";
|
|
285
299
|
if (!valid) fail2(`Invalid value for editor option "${key}"`);
|
|
286
|
-
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : value;
|
|
300
|
+
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : key === "previewDevices" ? cleanDevices(value) : value;
|
|
287
301
|
}
|
|
288
302
|
const components = input.components || {};
|
|
289
303
|
if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
|
|
@@ -317,7 +331,7 @@ var require_settings = __commonJS({
|
|
|
317
331
|
if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
|
|
318
332
|
if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
|
|
319
333
|
}
|
|
320
|
-
if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
|
|
334
|
+
if (Object.keys(entry).length) out.contentTypes[uid] = { ...structuredClone(entry), ...entry.previewDevices && { previewDevices: cleanDevices(entry.previewDevices) } };
|
|
321
335
|
}
|
|
322
336
|
const history = input.history || {};
|
|
323
337
|
if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
|
|
@@ -342,6 +356,8 @@ var require_settings = __commonJS({
|
|
|
342
356
|
if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
|
|
343
357
|
if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
|
|
344
358
|
if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
|
|
359
|
+
if (key === "previewToolbar" && !previewToolbar(value)) continue;
|
|
360
|
+
if (key === "previewDevices" && !previewDevices(value)) continue;
|
|
345
361
|
if (key === "lazyFields") {
|
|
346
362
|
if (!Array.isArray(value)) continue;
|
|
347
363
|
out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
|
|
@@ -374,22 +390,25 @@ var require_settings = __commonJS({
|
|
|
374
390
|
return out;
|
|
375
391
|
}
|
|
376
392
|
var ENTRY_KEYS = ["components", "contentTypes"];
|
|
393
|
+
var UNSET_WHEN_EMPTY = ["previewUrl", "blockPreviewUrl"];
|
|
394
|
+
var withoutEmpty = (editor) => Object.fromEntries(Object.entries(editor).filter(([key, value]) => !(UNSET_WHEN_EMPTY.includes(key) && value === "")));
|
|
377
395
|
function layer2(base, saved) {
|
|
378
396
|
if (!base) return saved;
|
|
379
397
|
const doc = saved && typeof saved === "object" ? saved : {};
|
|
380
|
-
|
|
398
|
+
const own = (key) => doc[key] && typeof doc[key] === "object" ? key === "editor" ? withoutEmpty(doc[key]) : doc[key] : {};
|
|
399
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...own(key) }]));
|
|
381
400
|
}
|
|
382
401
|
function overrides2(next, base) {
|
|
383
402
|
if (!base) return next;
|
|
384
403
|
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => {
|
|
385
|
-
const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]));
|
|
404
|
+
const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]) && !(key === "editor" && UNSET_WHEN_EMPTY.includes(name) && value === ""));
|
|
386
405
|
if (ENTRY_KEYS.includes(key)) {
|
|
387
406
|
for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
|
|
388
407
|
}
|
|
389
408
|
return [key, Object.fromEntries(entries)];
|
|
390
409
|
}));
|
|
391
410
|
}
|
|
392
|
-
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120 };
|
|
411
|
+
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120, help: 500 };
|
|
393
412
|
var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
394
413
|
var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
395
414
|
var fieldText = (value, max) => typeof value === "string" ? value.trim() !== "" && value.length <= max : plain(value) && Object.keys(value).length > 0 && Object.keys(value).length <= 30 && Object.entries(value).every(([locale, text]) => LOCALE.test(locale) && fieldText(text, max));
|
|
@@ -613,6 +632,453 @@ var require_diff = __commonJS({
|
|
|
613
632
|
}
|
|
614
633
|
});
|
|
615
634
|
|
|
635
|
+
// server/trash.js
|
|
636
|
+
var require_trash = __commonJS({
|
|
637
|
+
"server/trash.js"(exports2, module2) {
|
|
638
|
+
"use strict";
|
|
639
|
+
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
640
|
+
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
641
|
+
var { kept, canonical, stable } = require_diff();
|
|
642
|
+
var history = () => require_history();
|
|
643
|
+
var TRASH_UID = `plugin::${PLUGIN2}.trash`;
|
|
644
|
+
var STATUSES = ["trashed", "restored"];
|
|
645
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
646
|
+
var BATCH = 500;
|
|
647
|
+
var restoring = new AsyncLocalStorage();
|
|
648
|
+
var trashContentType2 = { schema: {
|
|
649
|
+
kind: "collectionType",
|
|
650
|
+
collectionName: "blockscene_trash",
|
|
651
|
+
info: { singularName: "trash", pluralName: "trashes", displayName: "Blockscene trash" },
|
|
652
|
+
options: { draftAndPublish: false },
|
|
653
|
+
pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
|
|
654
|
+
attributes: {
|
|
655
|
+
contentType: { type: "string", required: true },
|
|
656
|
+
relatedDocumentId: { type: "string", required: true },
|
|
657
|
+
// The document's main field (Content Manager setting) at delete time, for the list and the search.
|
|
658
|
+
title: { type: "string" },
|
|
659
|
+
// The locales the delete removed ([null] for a type without i18n).
|
|
660
|
+
locales: { type: "json" },
|
|
661
|
+
// [{ locale, status: 'draft' | 'published' | null (no Draft & Publish), snapshot }]: canonical snapshots (server/diff.js).
|
|
662
|
+
entries: { type: "json" },
|
|
663
|
+
// Links other documents' and blocks' unidirectional relations had to the deleted rows (see `incomingRows`).
|
|
664
|
+
incoming: { type: "json" },
|
|
665
|
+
actor: { type: "string", required: true },
|
|
666
|
+
actorName: { type: "string" },
|
|
667
|
+
deletedAt: { type: "datetime", required: true },
|
|
668
|
+
// deletedAt + trash retention at delete time; the nightly purge deletes the row after it.
|
|
669
|
+
expiresAt: { type: "datetime", required: true },
|
|
670
|
+
status: { type: "enumeration", enum: STATUSES, required: true, default: "trashed" },
|
|
671
|
+
restoredAs: { type: "string" },
|
|
672
|
+
restoredAt: { type: "datetime" },
|
|
673
|
+
size: { type: "integer" }
|
|
674
|
+
},
|
|
675
|
+
indexes: [{ name: "blockscene_trash_list", columns: ["status", "content_type", "deleted_at"] }, { name: "blockscene_trash_expiry", columns: ["expires_at"] }]
|
|
676
|
+
} };
|
|
677
|
+
var expiresAt = (deletedAt, days) => new Date(new Date(deletedAt).getTime() + days * DAY);
|
|
678
|
+
var isObject = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
679
|
+
var blank = (value) => value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length;
|
|
680
|
+
var toMany = (attr) => /ToMany$|^manyWay$/.test(String(attr?.relation || ""));
|
|
681
|
+
var toOne = (attr) => ["oneToOne", "manyToOne"].includes(attr?.relation);
|
|
682
|
+
var uniqueFields = (schema) => Object.entries(schema?.attributes || {}).filter(([name, attr]) => kept(name, attr) && (attr.unique === true || attr.type === "uid")).map(([name]) => name);
|
|
683
|
+
function chooseEntries(entries) {
|
|
684
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
685
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
686
|
+
const locale = entry?.locale ?? null, current = chosen.get(locale);
|
|
687
|
+
if (!current || current.status === "published" && entry.status !== "published") chosen.set(locale, entry);
|
|
688
|
+
}
|
|
689
|
+
return chosen;
|
|
690
|
+
}
|
|
691
|
+
function restorePayload(snapshot, schema, schemaOf, { media = /* @__PURE__ */ new Set(), relation = () => false } = {}) {
|
|
692
|
+
const report = { removed: [], media: 0, relations: 0, blocks: 0 };
|
|
693
|
+
const ref = (item) => ({ documentId: item.documentId, ...item.locale && { locale: item.locale } });
|
|
694
|
+
const build = (data, schema2, top, depth) => {
|
|
695
|
+
const out = {};
|
|
696
|
+
if (!isObject(data) || depth > 20) return out;
|
|
697
|
+
for (const [name, value] of Object.entries(data)) {
|
|
698
|
+
if (name === "__component") continue;
|
|
699
|
+
const attr = schema2?.attributes?.[name];
|
|
700
|
+
const drop = () => {
|
|
701
|
+
if (top) report.removed.push(name);
|
|
702
|
+
};
|
|
703
|
+
if (!kept(name, attr)) {
|
|
704
|
+
drop();
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const list = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
708
|
+
if (attr.type === "relation") {
|
|
709
|
+
const refs = list.filter((item) => isObject(item) && typeof item.documentId === "string");
|
|
710
|
+
const found = refs.filter((item) => relation(attr.target, item));
|
|
711
|
+
report.relations += refs.length - found.length;
|
|
712
|
+
out[name] = toMany(attr) ? found.map(ref) : found[0] ? ref(found[0]) : null;
|
|
713
|
+
} else if (attr.type === "media") {
|
|
714
|
+
const ids = list.filter(Number.isInteger), found = ids.filter((id) => media.has(id));
|
|
715
|
+
report.media += ids.length - found.length;
|
|
716
|
+
out[name] = attr.multiple ? found : found[0] ?? null;
|
|
717
|
+
} else if (attr.type === "component") {
|
|
718
|
+
const child = schemaOf(attr.component);
|
|
719
|
+
if (!child) {
|
|
720
|
+
drop();
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
out[name] = attr.repeatable ? list.filter(isObject).map((item) => build(item, child, false, depth + 1)) : isObject(value) ? build(value, child, false, depth + 1) : null;
|
|
724
|
+
} else if (attr.type === "dynamiczone") {
|
|
725
|
+
const rows = list.filter((row) => isObject(row) && typeof row.__component === "string");
|
|
726
|
+
const allowed = rows.filter((row) => (attr.components || []).includes(row.__component) && schemaOf(row.__component));
|
|
727
|
+
report.blocks += rows.length - allowed.length;
|
|
728
|
+
out[name] = allowed.map((row) => ({ __component: row.__component, ...build(row, schemaOf(row.__component), false, depth + 1) }));
|
|
729
|
+
} else if (attr.type === "enumeration" && !(attr.enum || []).includes(value)) drop();
|
|
730
|
+
else out[name] = value;
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
};
|
|
734
|
+
return { data: build(snapshot, schema, true, 0), report };
|
|
735
|
+
}
|
|
736
|
+
function planRestore(entries, { schema, schemaOf, locales = null, defaultLocale = null, existing = null, singleTaken = false, taken = () => false, media, relation }) {
|
|
737
|
+
const blocking = [], warnings = [], steps = [];
|
|
738
|
+
const published = (Array.isArray(entries) ? entries : []).filter((entry) => entry?.status === "published").map((entry) => entry.locale ?? null);
|
|
739
|
+
if (published.length) warnings.push({ code: "published", locales: published });
|
|
740
|
+
if (singleTaken && !existing) blocking.push({ code: "singleExists" });
|
|
741
|
+
for (const [locale, entry] of chooseEntries(entries)) {
|
|
742
|
+
if (locales && !locales.includes(locale)) {
|
|
743
|
+
warnings.push({ code: "localeRemoved", locale });
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (existing?.includes(locale)) {
|
|
747
|
+
blocking.push({ code: "localeExists", locale });
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const { data, report } = restorePayload(entry.snapshot, schema, schemaOf, { media, relation });
|
|
751
|
+
for (const field of uniqueFields(schema)) if (!blank(data[field]) && taken(field, locale, data[field])) blocking.push({ code: "unique", locale, field, value: String(data[field]).slice(0, 100) });
|
|
752
|
+
const required = Object.entries(schema?.attributes || {}).filter(([name, attr]) => attr?.required && kept(name, attr) && blank(data[name])).map(([name]) => name);
|
|
753
|
+
if (required.length) warnings.push({ code: "required", locale, fields: required });
|
|
754
|
+
if (report.removed.length) warnings.push({ code: "fieldsRemoved", locale, fields: report.removed });
|
|
755
|
+
for (const code of ["media", "relations", "blocks"]) if (report[code]) warnings.push({ code, locale, count: report[code] });
|
|
756
|
+
steps.push({ locale, data });
|
|
757
|
+
}
|
|
758
|
+
if (!steps.length && !blocking.length) blocking.push({ code: "nothing" });
|
|
759
|
+
steps.sort((a, b) => (b.locale === defaultLocale) - (a.locale === defaultLocale));
|
|
760
|
+
return { blocking, warnings, steps };
|
|
761
|
+
}
|
|
762
|
+
var incomingSources = (uid, models) => models.flatMap((model) => Object.entries(model.attributes || {}).filter(([, attr]) => attr?.type === "relation" && attr.target === uid && !attr.inversedBy && !attr.mappedBy && attr.joinTable).map(([field, attr]) => ({ owner: model.uid, kind: model.kind, field, joinTable: attr.joinTable, single: toOne(attr) })));
|
|
763
|
+
function incomingRows(source, links, deleted, owners = /* @__PURE__ */ new Map()) {
|
|
764
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = source.joinTable;
|
|
765
|
+
return links.filter((link) => deleted.has(link[inverseJoinColumn.name]) && !(source.kind === "contentType" && source.owner === source.target && deleted.has(link[joinColumn.name]))).map((link) => {
|
|
766
|
+
const target = deleted.get(link[inverseJoinColumn.name]), owner = owners.get(link[joinColumn.name]);
|
|
767
|
+
return {
|
|
768
|
+
owner: source.owner,
|
|
769
|
+
kind: source.kind,
|
|
770
|
+
field: source.field,
|
|
771
|
+
ownerId: link[joinColumn.name],
|
|
772
|
+
...source.kind === "contentType" && { ownerDocumentId: owner?.documentId ?? null, ownerLocale: owner?.locale ?? null },
|
|
773
|
+
locale: target.locale ?? null,
|
|
774
|
+
published: Boolean(target.published),
|
|
775
|
+
order: orderColumnName ? link[orderColumnName] ?? null : null
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function previewOf(snapshot, schema, schemaOf) {
|
|
780
|
+
const fields = [], blocks = [];
|
|
781
|
+
const text = (value) => {
|
|
782
|
+
const s = typeof value === "string" ? value : stable(value);
|
|
783
|
+
return s.length > 200 ? `${s.slice(0, 199)}\u2026` : s;
|
|
784
|
+
};
|
|
785
|
+
for (const [name, value] of Object.entries(isObject(snapshot) ? snapshot : {})) {
|
|
786
|
+
const attr = schema?.attributes?.[name];
|
|
787
|
+
if (attr?.type === "dynamiczone") {
|
|
788
|
+
for (const row of Array.isArray(value) ? value : []) blocks.push({ zone: name, uid: row?.__component, name: schemaOf(row?.__component)?.info?.displayName || row?.__component });
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const count = Array.isArray(value) ? value.length : 1;
|
|
792
|
+
fields.push({ name, type: attr?.type || "removed", ...["relation", "media", "component"].includes(attr?.type) ? { count } : { value: text(value) } });
|
|
793
|
+
}
|
|
794
|
+
return { fields, blocks };
|
|
795
|
+
}
|
|
796
|
+
function trashService2({ strapi }) {
|
|
797
|
+
const events = () => strapi.plugin(PLUGIN2).service("history");
|
|
798
|
+
const rows = () => strapi.db.query(TRASH_UID);
|
|
799
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
800
|
+
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
801
|
+
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
802
|
+
const models = () => [...Object.values(strapi.contentTypes || {}).map((model) => ({ uid: model.uid, kind: "contentType" })), ...Object.values(strapi.components || {}).map((model) => ({ uid: model.uid, kind: "component" }))].map((model) => ({ ...model, attributes: strapi.db.metadata.get(model.uid)?.attributes })).filter((model) => model.attributes);
|
|
803
|
+
const table = (trx, name) => strapi.db.getConnection(name).transacting(trx);
|
|
804
|
+
const lookups = async (uid, snapshots) => {
|
|
805
|
+
const ids = /* @__PURE__ */ new Set(), refs = [];
|
|
806
|
+
for (const snapshot of snapshots) {
|
|
807
|
+
const found = history().refsOf(snapshot, schemaOf(uid), schemaOf);
|
|
808
|
+
found.media.forEach((id) => ids.add(id));
|
|
809
|
+
refs.push(...found.relations);
|
|
810
|
+
}
|
|
811
|
+
const files = ids.size ? await strapi.db.query("plugin::upload.file").findMany({ select: ["id"], where: { id: { $in: [...ids] } } }) : [];
|
|
812
|
+
const present = /* @__PURE__ */ new Set();
|
|
813
|
+
for (const target of [...new Set(refs.map((ref) => ref.target))]) {
|
|
814
|
+
if (!strapi.contentTypes?.[target]) continue;
|
|
815
|
+
const found = await strapi.db.query(target).findMany({ select: ["documentId", "locale"], where: {
|
|
816
|
+
documentId: { $in: [...new Set(refs.filter((ref) => ref.target === target).map((ref) => ref.documentId))] },
|
|
817
|
+
...draftAndPublish(target) && { publishedAt: { $null: true } }
|
|
818
|
+
} });
|
|
819
|
+
for (const row of found) {
|
|
820
|
+
present.add(`${target}|${row.documentId}|${row.locale || ""}`);
|
|
821
|
+
present.add(`${target}|${row.documentId}|*`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return { media: new Set(files.map((file) => file.id)), relation: (target, ref) => present.has(`${target}|${ref.documentId}|${localized(target) && ref.locale ? ref.locale : "*"}`) };
|
|
825
|
+
};
|
|
826
|
+
const resolveIncoming = async (uid, incoming, locales, trx) => {
|
|
827
|
+
const out = { links: [], gone: 0, published: 0, replaced: 0 };
|
|
828
|
+
for (const link of Array.isArray(incoming) ? incoming : []) {
|
|
829
|
+
if (!locales.includes(link.locale)) continue;
|
|
830
|
+
if (link.published) {
|
|
831
|
+
out.published++;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const attr = strapi.db.metadata.get(link.owner)?.attributes?.[link.field];
|
|
835
|
+
if (!attr?.joinTable || attr.target !== uid) {
|
|
836
|
+
out.gone++;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
let ownerId = null;
|
|
840
|
+
if (link.kind === "component") ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: { id: link.ownerId } }))?.id ?? null;
|
|
841
|
+
else if (strapi.contentTypes?.[link.owner] && link.ownerDocumentId) ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: {
|
|
842
|
+
documentId: link.ownerDocumentId,
|
|
843
|
+
...localized(link.owner) && { locale: link.ownerLocale },
|
|
844
|
+
...draftAndPublish(link.owner) && { publishedAt: { $null: true } }
|
|
845
|
+
} }))?.id ?? null;
|
|
846
|
+
if (!ownerId) {
|
|
847
|
+
out.gone++;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const { joinTable } = attr;
|
|
851
|
+
if (toOne(attr) && await table(trx, joinTable.name).where(joinTable.joinColumn.name, ownerId).first()) {
|
|
852
|
+
out.replaced++;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
out.links.push({ ...link, ownerRowId: ownerId, joinTable });
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
};
|
|
859
|
+
const self = {
|
|
860
|
+
TRASH_UID,
|
|
861
|
+
// Before the delete, in its transaction: the links other documents and blocks have to the rows about to be deleted.
|
|
862
|
+
async incoming(uid, deleted) {
|
|
863
|
+
if (!deleted.size) return [];
|
|
864
|
+
return strapi.db.transaction(async ({ trx }) => {
|
|
865
|
+
const out = [];
|
|
866
|
+
for (const source of incomingSources(uid, models())) {
|
|
867
|
+
const links = await table(trx, source.joinTable.name).select("*").whereIn(source.joinTable.inverseJoinColumn.name, [...deleted.keys()]);
|
|
868
|
+
if (!links.length) continue;
|
|
869
|
+
const owners = source.kind === "contentType" && strapi.contentTypes?.[source.owner] ? new Map((await strapi.db.query(source.owner).findMany({
|
|
870
|
+
select: ["id", "documentId", "locale"],
|
|
871
|
+
where: { id: { $in: [...new Set(links.map((link) => link[source.joinTable.joinColumn.name]))] } }
|
|
872
|
+
})).map((row) => [row.id, row])) : /* @__PURE__ */ new Map();
|
|
873
|
+
out.push(...incomingRows({ ...source, target: uid }, links, deleted, owners));
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
});
|
|
877
|
+
},
|
|
878
|
+
// After the delete, in its transaction: the trash row. drafts/published: the stored rows read before the delete.
|
|
879
|
+
async record({ uid, documentId, locales, drafts, published, incoming, actor, days }) {
|
|
880
|
+
const dp = draftAndPublish(uid);
|
|
881
|
+
const entries = [...drafts.map((row) => [row, dp ? "draft" : null]), ...published.map((row) => [row, "published"])].map(([row, status]) => ({ locale: row.locale ?? null, status, snapshot: canonical(row, schemaOf(uid), schemaOf) }));
|
|
882
|
+
const main = await events().mainField(uid);
|
|
883
|
+
const first = chooseEntries(entries);
|
|
884
|
+
const source = first.get(await events().scope(uid).then((list) => list?.[0] ?? null).catch(() => null)) || [...first.values()][0];
|
|
885
|
+
const title = main && source?.snapshot?.[main] != null ? String(source.snapshot[main]).slice(0, 255) : null;
|
|
886
|
+
const deletedAt = /* @__PURE__ */ new Date();
|
|
887
|
+
return rows().create({ data: {
|
|
888
|
+
contentType: uid,
|
|
889
|
+
relatedDocumentId: documentId,
|
|
890
|
+
title,
|
|
891
|
+
locales: locales || [...new Set(entries.map((entry) => entry.locale))],
|
|
892
|
+
entries,
|
|
893
|
+
incoming,
|
|
894
|
+
...actor,
|
|
895
|
+
deletedAt,
|
|
896
|
+
expiresAt: expiresAt(deletedAt, days),
|
|
897
|
+
status: "trashed",
|
|
898
|
+
size: Buffer.byteLength(stable({ entries, incoming }))
|
|
899
|
+
} });
|
|
900
|
+
},
|
|
901
|
+
// One page of trash rows, without their snapshots. `types`: the content types the user may read.
|
|
902
|
+
async list({ contentType, status = "trashed", page = 1, pageSize = 20, types }) {
|
|
903
|
+
const where = { status, contentType: contentType ? contentType : { $in: types } };
|
|
904
|
+
if (contentType && !types.includes(contentType)) return { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 } };
|
|
905
|
+
const [results, total] = await Promise.all([
|
|
906
|
+
rows().findMany({
|
|
907
|
+
select: ["id", "contentType", "relatedDocumentId", "title", "locales", "actor", "actorName", "deletedAt", "expiresAt", "status", "restoredAs", "restoredAt"],
|
|
908
|
+
where,
|
|
909
|
+
orderBy: [{ deletedAt: "desc" }, { id: "desc" }],
|
|
910
|
+
offset: (page - 1) * pageSize,
|
|
911
|
+
limit: pageSize
|
|
912
|
+
}),
|
|
913
|
+
rows().count({ where })
|
|
914
|
+
]);
|
|
915
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) } };
|
|
916
|
+
},
|
|
917
|
+
async find(id) {
|
|
918
|
+
return rows().findOne({ where: { id } });
|
|
919
|
+
},
|
|
920
|
+
// One row for the preview: fields and blocks of each entry, and how many links it held.
|
|
921
|
+
preview(row) {
|
|
922
|
+
const schema = schemaOf(row.contentType);
|
|
923
|
+
const { entries, incoming, ...meta } = row;
|
|
924
|
+
return {
|
|
925
|
+
...meta,
|
|
926
|
+
incoming: (incoming || []).length,
|
|
927
|
+
entries: (entries || []).map((entry) => ({ locale: entry.locale ?? null, status: entry.status ?? null, ...previewOf(entry.snapshot, schema, schemaOf) }))
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
// The pre-check: every lookup a restore needs, then planRestore. Read-only; the restore runs it again in its transaction.
|
|
931
|
+
async plan(row) {
|
|
932
|
+
const uid = row.contentType, schema = schemaOf(uid);
|
|
933
|
+
if (!schema || !strapi.contentTypes?.[uid]) return { blocking: [{ code: "typeGone" }], warnings: [], steps: [], incoming: { links: [], gone: 0, published: 0, replaced: 0 } };
|
|
934
|
+
const i18n = localized(uid);
|
|
935
|
+
const locales = i18n ? (await strapi.plugin("i18n").service("locales").find()).map((locale) => locale.code) : null;
|
|
936
|
+
const defaultLocale = i18n ? await strapi.plugin("i18n").service("locales").getDefaultLocale() : null;
|
|
937
|
+
const current = await strapi.db.query(uid).findMany({ select: ["locale"], where: { documentId: row.relatedDocumentId } });
|
|
938
|
+
const existing = current.length ? [...new Set(current.map((entry) => i18n ? entry.locale : null))] : null;
|
|
939
|
+
const singleTaken = schema.kind === "singleType" && !existing && Boolean(await strapi.db.query(uid).findOne({ select: ["id"] }));
|
|
940
|
+
const chosen = [...chooseEntries(row.entries).values()];
|
|
941
|
+
const { media, relation } = await lookups(uid, chosen.map((entry) => entry.snapshot));
|
|
942
|
+
const taken = /* @__PURE__ */ new Set();
|
|
943
|
+
for (const entry of chosen) for (const field of uniqueFields(schema)) {
|
|
944
|
+
const value = entry.snapshot?.[field];
|
|
945
|
+
if (blank(value) || typeof value === "object") continue;
|
|
946
|
+
const other = await strapi.db.query(uid).findOne({ select: ["id"], where: { [field]: value, ...i18n && { locale: entry.locale }, ...existing && { documentId: { $ne: row.relatedDocumentId } } } });
|
|
947
|
+
if (other) taken.add(stable([field, entry.locale ?? null, value]));
|
|
948
|
+
}
|
|
949
|
+
const plan = planRestore(row.entries, { schema, schemaOf, locales, defaultLocale, existing, singleTaken, media, relation, taken: (field, locale, value) => taken.has(stable([field, locale ?? null, value])) });
|
|
950
|
+
const incoming = await strapi.db.transaction(({ trx }) => resolveIncoming(uid, row.incoming, plan.steps.map((step) => step.locale), trx));
|
|
951
|
+
return { ...plan, documentId: existing ? row.relatedDocumentId : null, incoming };
|
|
952
|
+
},
|
|
953
|
+
// What the pre-check endpoint returns: the plan without the data it would write.
|
|
954
|
+
async check(row) {
|
|
955
|
+
if (row.status !== "trashed") return { status: row.status, restoredAs: row.restoredAs, blocking: [{ code: "restored" }], warnings: [] };
|
|
956
|
+
const { steps, incoming: { links, ...incoming }, ...plan } = await self.plan(row);
|
|
957
|
+
return { ...plan, locales: steps.map((step) => step.locale), incoming: { ...incoming, reconnect: links.length } };
|
|
958
|
+
},
|
|
959
|
+
// Restore as a draft, in one transaction: claim the row (a second restore of the same row, concurrent or later, gets
|
|
960
|
+
// `already` with the first one's document), check again, recreate (default locale first, the others under the same
|
|
961
|
+
// documentId), put the links back, record `restore` events. A blocking problem throws `conflict` and nothing is written.
|
|
962
|
+
async restore(id, { actor, userId }) {
|
|
963
|
+
return restoring.run(true, () => strapi.db.transaction(async ({ trx }) => {
|
|
964
|
+
const claimed = await rows().updateMany({ where: { id, status: "trashed" }, data: { status: "restored", restoredAt: /* @__PURE__ */ new Date() } });
|
|
965
|
+
const row = await rows().findOne({ where: { id } });
|
|
966
|
+
if (!row) return null;
|
|
967
|
+
if (!claimed?.count) return { already: true, documentId: row.restoredAs || null };
|
|
968
|
+
const plan = await self.plan(row);
|
|
969
|
+
if (plan.blocking.length) {
|
|
970
|
+
const error = new Error("conflict");
|
|
971
|
+
error.conflict = { blocking: plan.blocking, warnings: plan.warnings };
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
const uid = row.contentType, documents = strapi.documents(uid);
|
|
975
|
+
const creator = userId ? { createdBy: userId, updatedBy: userId } : {};
|
|
976
|
+
let documentId = plan.documentId;
|
|
977
|
+
for (const { locale, data } of plan.steps) {
|
|
978
|
+
const params = { ...locale && { locale }, data: { ...data, ...creator } };
|
|
979
|
+
if (documentId) await documents.update({ documentId, ...params });
|
|
980
|
+
else documentId = (await documents.create(params)).documentId;
|
|
981
|
+
}
|
|
982
|
+
const restored = await events().rows(uid, documentId, localized(uid) ? plan.steps.map((step) => step.locale) : null);
|
|
983
|
+
const rowOf = new Map(restored.map((entry) => [entry.locale ?? null, entry]));
|
|
984
|
+
for (const link of plan.incoming.links) {
|
|
985
|
+
const target = rowOf.get(link.locale);
|
|
986
|
+
if (!target) continue;
|
|
987
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = link.joinTable;
|
|
988
|
+
await table(trx, link.joinTable.name).insert({ [joinColumn.name]: link.ownerRowId, [inverseJoinColumn.name]: target.id, ...orderColumnName && link.order != null && { [orderColumnName]: link.order } });
|
|
989
|
+
}
|
|
990
|
+
for (const entry of restored) await events().record({ uid, documentId, locale: entry.locale ?? null, action: "restore", entry, actor, extra: { restoredFrom: row.relatedDocumentId, trash: row.id } });
|
|
991
|
+
await rows().update({ where: { id }, data: { restoredAs: documentId } });
|
|
992
|
+
const { links, ...incoming } = plan.incoming;
|
|
993
|
+
return { documentId, locales: plan.steps.map((step) => step.locale), warnings: plan.warnings, incoming: { ...incoming, reconnected: links.length } };
|
|
994
|
+
}));
|
|
995
|
+
},
|
|
996
|
+
// Delete forever (one row), recorded as a `purge` event. Never touches media files.
|
|
997
|
+
async remove(row, actor) {
|
|
998
|
+
return strapi.db.transaction(async () => {
|
|
999
|
+
const { count } = await rows().deleteMany({ where: { id: row.id } });
|
|
1000
|
+
if (count) await events().record({ uid: row.contentType, documentId: row.relatedDocumentId, action: "purge", actor, extra: { trash: row.id, title: row.title } });
|
|
1001
|
+
return count;
|
|
1002
|
+
});
|
|
1003
|
+
},
|
|
1004
|
+
// Nightly, in batches, idempotent: rows past expiresAt are deleted (restored ones too). Never touches media files.
|
|
1005
|
+
async purge(now = Date.now()) {
|
|
1006
|
+
let count = 0;
|
|
1007
|
+
for (; ; ) {
|
|
1008
|
+
const ids = (await rows().findMany({ select: ["id"], where: { expiresAt: { $lt: new Date(now) } }, orderBy: { id: "asc" }, limit: BATCH })).map((row) => row.id);
|
|
1009
|
+
if (ids.length) await rows().deleteMany({ where: { id: { $in: ids } } });
|
|
1010
|
+
count += ids.length;
|
|
1011
|
+
if (ids.length < BATCH) return count;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
return self;
|
|
1016
|
+
}
|
|
1017
|
+
var ID = /^[1-9]\d{0,15}$/;
|
|
1018
|
+
var API_UID = /^api::[\w-]+\.[\w-]+$/;
|
|
1019
|
+
var int = (value, fallback, max) => {
|
|
1020
|
+
if (value === void 0 || value === "") return fallback;
|
|
1021
|
+
const n = Number(value);
|
|
1022
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1023
|
+
};
|
|
1024
|
+
var trashController2 = ({ strapi }) => {
|
|
1025
|
+
const service = () => strapi.plugin(PLUGIN2).service("trash");
|
|
1026
|
+
const { can, readableTypes, actorOf } = history();
|
|
1027
|
+
const load = async (ctx, action = "read") => {
|
|
1028
|
+
if (!ID.test(String(ctx.params?.id || ""))) {
|
|
1029
|
+
ctx.badRequest("Invalid trash entry");
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
const row = await service().find(Number(ctx.params.id));
|
|
1033
|
+
if (!row || !can(strapi, ctx, row.contentType, "read") || action !== "read" && !can(strapi, ctx, row.contentType, action)) {
|
|
1034
|
+
ctx.notFound();
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
return row;
|
|
1038
|
+
};
|
|
1039
|
+
return {
|
|
1040
|
+
async list(ctx) {
|
|
1041
|
+
const { contentType = "", status = "trashed" } = ctx.query || {};
|
|
1042
|
+
const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 20, 100);
|
|
1043
|
+
if (typeof contentType !== "string" || contentType && !API_UID.test(contentType) || !STATUSES.includes(status) || Number.isNaN(page) || Number.isNaN(pageSize)) return ctx.badRequest("Invalid filters");
|
|
1044
|
+
const types = readableTypes(strapi, ctx);
|
|
1045
|
+
ctx.body = {
|
|
1046
|
+
...await service().list({ contentType, status, page, pageSize, types }),
|
|
1047
|
+
types: types.map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid, kind: strapi.contentTypes[uid].kind, localized: Boolean(strapi.contentTypes[uid].pluginOptions?.i18n?.localized) }))
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
async find(ctx) {
|
|
1051
|
+
const row = await load(ctx);
|
|
1052
|
+
if (row) ctx.body = service().preview(row);
|
|
1053
|
+
},
|
|
1054
|
+
async check(ctx) {
|
|
1055
|
+
const row = await load(ctx, "create");
|
|
1056
|
+
if (row) ctx.body = await service().check(row);
|
|
1057
|
+
},
|
|
1058
|
+
async restore(ctx) {
|
|
1059
|
+
const row = await load(ctx, "create");
|
|
1060
|
+
if (!row) return;
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await service().restore(row.id, { actor: actorOf(strapi), userId: ctx.state?.user?.id });
|
|
1063
|
+
if (!result) return ctx.notFound();
|
|
1064
|
+
ctx.body = result;
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
if (!error?.conflict) throw error;
|
|
1067
|
+
ctx.status = 409;
|
|
1068
|
+
ctx.body = { data: null, error: { status: 409, name: "ConflictError", message: "The document cannot be restored as it is: nothing was written.", details: error.conflict } };
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
async remove(ctx) {
|
|
1072
|
+
const row = await load(ctx, "delete");
|
|
1073
|
+
if (!row) return;
|
|
1074
|
+
ctx.body = { deleted: await service().remove(row, actorOf(strapi)) };
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
};
|
|
1078
|
+
module2.exports = { TRASH_UID, STATUSES, restoring, trashContentType: trashContentType2, expiresAt, uniqueFields, chooseEntries, restorePayload, planRestore, incomingSources, incomingRows, previewOf, trashService: trashService2, trashController: trashController2 };
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
616
1082
|
// server/history.js
|
|
617
1083
|
var require_history = __commonJS({
|
|
618
1084
|
"server/history.js"(exports2, module2) {
|
|
@@ -620,13 +1086,16 @@ var require_history = __commonJS({
|
|
|
620
1086
|
var { createHash } = require("node:crypto");
|
|
621
1087
|
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
622
1088
|
var { snapshotPopulate, canonical, stable, summarize } = require_diff();
|
|
1089
|
+
var { TRASH_UID, restoring } = require_trash();
|
|
623
1090
|
var EVENT_UID = `plugin::${PLUGIN2}.event`;
|
|
624
|
-
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore"];
|
|
1091
|
+
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore", "purge"];
|
|
625
1092
|
var CAPTURED = { create: "create", clone: "create", update: "update", publish: "publish", unpublish: "unpublish", discardDraft: "discard", delete: "delete" };
|
|
626
1093
|
var CRON2 = "blocksceneHistoryPurge";
|
|
627
1094
|
var BATCH = 500;
|
|
628
1095
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
629
1096
|
var LIST_LIMIT = 100;
|
|
1097
|
+
var ACTORS_LIMIT = 500;
|
|
1098
|
+
var SEARCH_LIMIT = 200;
|
|
630
1099
|
var eventContentType2 = { schema: {
|
|
631
1100
|
kind: "collectionType",
|
|
632
1101
|
collectionName: "blockscene_events",
|
|
@@ -642,7 +1111,8 @@ var require_history = __commonJS({
|
|
|
642
1111
|
actor: { type: "string", required: true },
|
|
643
1112
|
actorName: { type: "string" },
|
|
644
1113
|
at: { type: "datetime", required: true },
|
|
645
|
-
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed
|
|
1114
|
+
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed;
|
|
1115
|
+
// restore: + { restoredFrom, trash }; purge: { trash, title }.
|
|
646
1116
|
summary: { type: "json" },
|
|
647
1117
|
// Null when identical to the previous snapshot of the same locale (same hash) or purged.
|
|
648
1118
|
snapshot: { type: "json" },
|
|
@@ -705,6 +1175,11 @@ var require_history = __commonJS({
|
|
|
705
1175
|
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
706
1176
|
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
707
1177
|
const events = () => strapi.db.query(EVENT_UID);
|
|
1178
|
+
const actors = async () => {
|
|
1179
|
+
const meta = strapi.db.metadata.get(EVENT_UID);
|
|
1180
|
+
const column = (name) => meta.attributes[name].columnName || name;
|
|
1181
|
+
return strapi.db.connection(meta.tableName).select({ actor: column("actor") }).max({ actorName: column("actorName") }).groupBy(column("actor")).orderBy(column("actor")).limit(ACTORS_LIMIT);
|
|
1182
|
+
};
|
|
708
1183
|
const scope = async (uid, locale) => {
|
|
709
1184
|
if (!localized(uid)) return [null];
|
|
710
1185
|
if (locale === "*") return null;
|
|
@@ -727,7 +1202,7 @@ var require_history = __commonJS({
|
|
|
727
1202
|
},
|
|
728
1203
|
covers: covers2,
|
|
729
1204
|
// One event. The snapshot is stored unless identical to the latest stored one of the same document and locale.
|
|
730
|
-
async record({ uid, documentId, locale = null, action, entry, actor, missing = false }) {
|
|
1205
|
+
async record({ uid, documentId, locale = null, action, entry, actor, missing = false, extra = null }) {
|
|
731
1206
|
let summary = missing ? { missing: true } : null, snapshot = null, hash = null, size = 0;
|
|
732
1207
|
if (entry) {
|
|
733
1208
|
const content = canonical(entry, schemaOf(uid), schemaOf);
|
|
@@ -740,6 +1215,7 @@ var require_history = __commonJS({
|
|
|
740
1215
|
size = Buffer.byteLength(text);
|
|
741
1216
|
}
|
|
742
1217
|
}
|
|
1218
|
+
if (extra) summary = { ...summary, ...extra };
|
|
743
1219
|
return events().create({ data: { contentType: uid, relatedDocumentId: documentId, locale, action, ...actor, at: /* @__PURE__ */ new Date(), summary, snapshot, hash, size } });
|
|
744
1220
|
},
|
|
745
1221
|
// After a save or publish: one event per affected entry (drafts, or the published versions after a publish).
|
|
@@ -749,6 +1225,16 @@ var require_history = __commonJS({
|
|
|
749
1225
|
},
|
|
750
1226
|
scope,
|
|
751
1227
|
rows,
|
|
1228
|
+
// The Content Manager's main field of a content type when it is a plain value (what list views show), else null.
|
|
1229
|
+
async mainField(uid) {
|
|
1230
|
+
let name = null;
|
|
1231
|
+
try {
|
|
1232
|
+
name = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[uid]))?.settings?.mainField;
|
|
1233
|
+
} catch {
|
|
1234
|
+
name = null;
|
|
1235
|
+
}
|
|
1236
|
+
return name && name !== "id" && schemaOf(uid)?.attributes?.[name] && !["relation", "media", "component", "dynamiczone", "password", "json", "blocks"].includes(schemaOf(uid).attributes[name].type) ? name : null;
|
|
1237
|
+
},
|
|
752
1238
|
// Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
|
|
753
1239
|
async list({ uid, documentId, locale }) {
|
|
754
1240
|
const where = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
|
|
@@ -772,13 +1258,7 @@ var require_history = __commonJS({
|
|
|
772
1258
|
const relations = {};
|
|
773
1259
|
for (const target of [...new Set(refs.relations.map((ref) => ref.target))]) {
|
|
774
1260
|
if (!strapi.contentTypes?.[target]) continue;
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
mainField = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[target]))?.settings?.mainField;
|
|
778
|
-
} catch {
|
|
779
|
-
mainField = null;
|
|
780
|
-
}
|
|
781
|
-
const scalar = mainField && !["relation", "media", "component", "dynamiczone", "password"].includes(schemaOf(target)?.attributes?.[mainField]?.type) && mainField !== "id";
|
|
1261
|
+
const mainField = await self.mainField(target), scalar = Boolean(mainField);
|
|
782
1262
|
const found = await strapi.db.query(target).findMany({
|
|
783
1263
|
select: ["id", "documentId", "locale", ...scalar ? [mainField] : []],
|
|
784
1264
|
where: { documentId: { $in: [...new Set(refs.relations.filter((ref) => ref.target === target).map((ref) => ref.documentId))] }, ...draftAndPublish(target) && { publishedAt: { $null: true } } }
|
|
@@ -787,6 +1267,56 @@ var require_history = __commonJS({
|
|
|
787
1267
|
}
|
|
788
1268
|
return { event: meta, snapshot, media: Object.fromEntries(files.map((file) => [file.id, file])), relations };
|
|
789
1269
|
},
|
|
1270
|
+
// Content activity: one page of events (no snapshots), newest first, with each document's title and whether it still
|
|
1271
|
+
// exists (the list links to it). `types`: the content types the user may read. `q`: documents whose main field
|
|
1272
|
+
// contains it now, or whose trash entry's title does (at most SEARCH_LIMIT per content type).
|
|
1273
|
+
async activity({ actor, contentType, action, from, to, q, page, pageSize, types }) {
|
|
1274
|
+
const empty = { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 }, actors: [] };
|
|
1275
|
+
if (contentType && !types.includes(contentType)) return empty;
|
|
1276
|
+
const where = {
|
|
1277
|
+
contentType: contentType || { $in: types },
|
|
1278
|
+
...actor && { actor },
|
|
1279
|
+
...action && { action },
|
|
1280
|
+
...(from || to) && { at: { ...from && { $gte: from }, ...to && { $lte: to } } }
|
|
1281
|
+
};
|
|
1282
|
+
if (q) {
|
|
1283
|
+
const matches = [];
|
|
1284
|
+
for (const uid of contentType ? [contentType] : types) {
|
|
1285
|
+
const main = await self.mainField(uid);
|
|
1286
|
+
const found = new Set(main ? (await strapi.db.query(uid).findMany({ select: ["documentId"], where: { [main]: { $containsi: q } }, limit: SEARCH_LIMIT })).map((row) => row.documentId) : []);
|
|
1287
|
+
for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId"], where: { contentType: uid, title: { $containsi: q } }, limit: SEARCH_LIMIT })) found.add(row.relatedDocumentId);
|
|
1288
|
+
if (found.size) matches.push({ contentType: uid, relatedDocumentId: { $in: [...found] } });
|
|
1289
|
+
}
|
|
1290
|
+
if (!matches.length) return { ...empty, actors: await actors() };
|
|
1291
|
+
where.$or = matches;
|
|
1292
|
+
}
|
|
1293
|
+
const [list, total] = await Promise.all([
|
|
1294
|
+
events().findMany({ select: ["id", "contentType", "relatedDocumentId", "locale", "action", "actor", "actorName", "at", "summary"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
|
|
1295
|
+
events().count({ where })
|
|
1296
|
+
]);
|
|
1297
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1298
|
+
for (const uid of [...new Set(list.map((event) => event.contentType))]) {
|
|
1299
|
+
const ids = [...new Set(list.filter((event) => event.contentType === uid).map((event) => event.relatedDocumentId))];
|
|
1300
|
+
const main = strapi.contentTypes?.[uid] ? await self.mainField(uid) : null;
|
|
1301
|
+
const found = strapi.contentTypes?.[uid] ? await strapi.db.query(uid).findMany({ select: ["documentId", "locale", ...main ? [main] : []], where: { documentId: { $in: ids } } }) : [];
|
|
1302
|
+
for (const row of found) {
|
|
1303
|
+
const key = `${uid}|${row.documentId}`;
|
|
1304
|
+
docs.set(key, [...docs.get(key) || [], { locale: row.locale ?? null, title: main && row[main] != null ? String(row[main]) : null }]);
|
|
1305
|
+
}
|
|
1306
|
+
const gone = ids.filter((id) => !docs.has(`${uid}|${id}`));
|
|
1307
|
+
if (gone.length) {
|
|
1308
|
+
for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId", "title"], where: { contentType: uid, relatedDocumentId: { $in: gone } }, orderBy: { id: "desc" } }))
|
|
1309
|
+
if (!docs.has(`trash|${uid}|${row.relatedDocumentId}`)) docs.set(`trash|${uid}|${row.relatedDocumentId}`, row.title);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const results = list.map((event) => {
|
|
1313
|
+
const rows2 = docs.get(`${event.contentType}|${event.relatedDocumentId}`) || [];
|
|
1314
|
+
const own = rows2.find((row) => row.locale === (event.locale ?? null));
|
|
1315
|
+
if (event.action === "purge") return { ...event, exists: false, title: event.summary?.title ?? null };
|
|
1316
|
+
return { ...event, exists: Boolean(own), title: (own || rows2[0])?.title ?? docs.get(`trash|${event.contentType}|${event.relatedDocumentId}`) ?? null };
|
|
1317
|
+
});
|
|
1318
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }, actors: await actors() };
|
|
1319
|
+
},
|
|
790
1320
|
// Nightly, in batches, idempotent (two instances running it is harmless): events past eventDays are deleted;
|
|
791
1321
|
// snapshots past retentionDays, or beyond maxSnapshots per document (newest kept), are emptied. Never touches media.
|
|
792
1322
|
async purge(now = Date.now()) {
|
|
@@ -823,16 +1353,21 @@ var require_history = __commonJS({
|
|
|
823
1353
|
const warn = (message, error) => strapi.log.error(`[${PLUGIN2}] ${message}: ${error?.message || error}`);
|
|
824
1354
|
strapi.documents.use(async (ctx, next) => {
|
|
825
1355
|
const action = CAPTURED[ctx.action];
|
|
826
|
-
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true) return next();
|
|
1356
|
+
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true || restoring.getStore()) return next();
|
|
827
1357
|
const history = service();
|
|
828
1358
|
const params = ctx.params || {};
|
|
829
1359
|
if (action === "delete") {
|
|
830
|
-
|
|
1360
|
+
const config = params.documentId ? await history.config() : null;
|
|
1361
|
+
if (!config || !history.covers(config, ctx.uid)) return next();
|
|
831
1362
|
return strapi.db.transaction(async () => {
|
|
832
|
-
const actor2 = actorOf(strapi);
|
|
833
|
-
|
|
1363
|
+
const actor2 = actorOf(strapi), trash = strapi.plugin(PLUGIN2).service("trash");
|
|
1364
|
+
const published = strapi.getModel(ctx.uid)?.options?.draftAndPublish === true;
|
|
1365
|
+
let locales2, entries, live, incoming;
|
|
834
1366
|
try {
|
|
835
|
-
|
|
1367
|
+
locales2 = await history.scope(ctx.uid, params.locale);
|
|
1368
|
+
entries = await history.rows(ctx.uid, params.documentId, locales2);
|
|
1369
|
+
live = published ? await history.rows(ctx.uid, params.documentId, locales2, true) : [];
|
|
1370
|
+
incoming = await trash.incoming(ctx.uid, new Map([...entries.map((row) => [row.id, { locale: row.locale, published: false }]), ...live.map((row) => [row.id, { locale: row.locale, published: true }])]));
|
|
836
1371
|
} catch (error) {
|
|
837
1372
|
warn("reading the document before a delete failed", error);
|
|
838
1373
|
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
@@ -840,6 +1375,7 @@ var require_history = __commonJS({
|
|
|
840
1375
|
const result2 = await next();
|
|
841
1376
|
try {
|
|
842
1377
|
for (const entry of entries) await history.record({ uid: ctx.uid, documentId: params.documentId, locale: entry.locale ?? null, action, entry, actor: actor2 });
|
|
1378
|
+
if (entries.length || live.length) await trash.record({ uid: ctx.uid, documentId: params.documentId, locales: locales2, drafts: entries, published: live, incoming, actor: actor2, days: config.trashDays });
|
|
843
1379
|
} catch (error) {
|
|
844
1380
|
warn("recording a delete failed, the delete was rolled back", error);
|
|
845
1381
|
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
@@ -885,17 +1421,36 @@ var require_history = __commonJS({
|
|
|
885
1421
|
} catch (error) {
|
|
886
1422
|
strapi.log.error(`[${PLUGIN2}] history purge failed: ${error?.message || error}`);
|
|
887
1423
|
}
|
|
1424
|
+
try {
|
|
1425
|
+
const count = await strapi.plugin(PLUGIN2).service("trash").purge();
|
|
1426
|
+
if (count) strapi.log.info(`[${PLUGIN2}] trash purge: ${count} expired entries deleted`);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
strapi.log.error(`[${PLUGIN2}] trash purge failed: ${error?.message || error}`);
|
|
1429
|
+
}
|
|
888
1430
|
} } });
|
|
889
1431
|
}
|
|
890
1432
|
var DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
891
1433
|
var LOCALE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
892
|
-
var
|
|
1434
|
+
var can = (strapi, ctx, uid, action = "read") => {
|
|
893
1435
|
try {
|
|
894
|
-
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot
|
|
1436
|
+
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot[action]();
|
|
895
1437
|
} catch {
|
|
896
1438
|
return false;
|
|
897
1439
|
}
|
|
898
1440
|
};
|
|
1441
|
+
var canRead = (strapi, ctx, uid) => can(strapi, ctx, uid);
|
|
1442
|
+
var readableTypes = (strapi, ctx) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::") && canRead(strapi, ctx, uid));
|
|
1443
|
+
var ACTOR = /^(system|(admin|token|user):\d{1,15})$/;
|
|
1444
|
+
var int = (value, fallback, max) => {
|
|
1445
|
+
if (value === void 0 || value === "") return fallback;
|
|
1446
|
+
const n = Number(value);
|
|
1447
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1448
|
+
};
|
|
1449
|
+
var date = (value) => {
|
|
1450
|
+
if (!value) return null;
|
|
1451
|
+
const time = Date.parse(value);
|
|
1452
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(time) ? new Date(time) : NaN;
|
|
1453
|
+
};
|
|
899
1454
|
var historyController2 = ({ strapi }) => ({
|
|
900
1455
|
async list(ctx) {
|
|
901
1456
|
const { uid, documentId } = ctx.params || {};
|
|
@@ -911,9 +1466,21 @@ var require_history = __commonJS({
|
|
|
911
1466
|
const found = await strapi.plugin(PLUGIN2).service("history").find(id);
|
|
912
1467
|
if (!found || !canRead(strapi, ctx, found.event.contentType)) return ctx.notFound();
|
|
913
1468
|
ctx.body = found;
|
|
1469
|
+
},
|
|
1470
|
+
async activity(ctx) {
|
|
1471
|
+
const { actor = "", contentType = "", action = "", q = "" } = ctx.query || {};
|
|
1472
|
+
const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 25, 100), from = date(ctx.query?.from), to = date(ctx.query?.to);
|
|
1473
|
+
if ([actor, contentType, action, q].some((value) => typeof value !== "string") || actor && !ACTOR.test(actor) || contentType && !/^api::[\w-]+\.[\w-]+$/.test(contentType) || action && !ACTIONS.includes(action) || q.length > 100 || [page, pageSize, from, to].some((value) => Number.isNaN(value))) return ctx.badRequest("Invalid filters");
|
|
1474
|
+
const history = strapi.plugin(PLUGIN2).service("history");
|
|
1475
|
+
const types = readableTypes(strapi, ctx);
|
|
1476
|
+
ctx.body = {
|
|
1477
|
+
...await history.activity({ actor, contentType, action, from, to, q: q.trim(), page, pageSize, types }),
|
|
1478
|
+
enabled: Boolean((await history.config()).enabled) && strapi.plugin(PLUGIN2).config("disabled") !== true,
|
|
1479
|
+
types: types.map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid, kind: strapi.contentTypes[uid].kind, localized: Boolean(strapi.contentTypes[uid].pluginOptions?.i18n?.localized) }))
|
|
1480
|
+
};
|
|
914
1481
|
}
|
|
915
1482
|
});
|
|
916
|
-
module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
|
|
1483
|
+
module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, can, readableTypes, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
|
|
917
1484
|
}
|
|
918
1485
|
});
|
|
919
1486
|
|
|
@@ -922,6 +1489,7 @@ var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, valida
|
|
|
922
1489
|
var { safeGroups, validateGroups } = require_groups();
|
|
923
1490
|
var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
|
|
924
1491
|
var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
|
|
1492
|
+
var { trashContentType, trashService, trashController } = require_trash();
|
|
925
1493
|
var STRAPI5 = true;
|
|
926
1494
|
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
927
1495
|
var LEGACY_PLUGIN = "block-picker";
|
|
@@ -1061,7 +1629,13 @@ module.exports = {
|
|
|
1061
1629
|
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
1062
1630
|
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
1063
1631
|
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN },
|
|
1064
|
-
...STRAPI5 ? [
|
|
1632
|
+
...STRAPI5 ? [
|
|
1633
|
+
["history.read", "Read version history"],
|
|
1634
|
+
["activity.read", "Read content activity"],
|
|
1635
|
+
["trash.read", "Read the trash"],
|
|
1636
|
+
["trash.restore", "Restore from the trash"],
|
|
1637
|
+
["trash.purge", "Delete from the trash forever"]
|
|
1638
|
+
].map(([uid, displayName]) => ({ section: "plugins", displayName, uid, pluginName: PLUGIN })) : []
|
|
1065
1639
|
]);
|
|
1066
1640
|
strapi.plugin(PLUGIN).service("settings").projectDefaults();
|
|
1067
1641
|
strapi.plugin(PLUGIN).service("settings").fields();
|
|
@@ -1075,8 +1649,8 @@ module.exports = {
|
|
|
1075
1649
|
destroy({ strapi }) {
|
|
1076
1650
|
if (STRAPI5) strapi.cron?.remove?.(CRON);
|
|
1077
1651
|
},
|
|
1078
|
-
// Version history events (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1079
|
-
contentTypes: STRAPI5 ? { event: eventContentType } : {},
|
|
1652
|
+
// Version history events and the trash (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1653
|
+
contentTypes: STRAPI5 ? { event: eventContentType, trash: trashContentType } : {},
|
|
1080
1654
|
services: { settings: ({ strapi }) => {
|
|
1081
1655
|
let project;
|
|
1082
1656
|
const projectDefaults = () => {
|
|
@@ -1124,7 +1698,7 @@ module.exports = {
|
|
|
1124
1698
|
return get();
|
|
1125
1699
|
}
|
|
1126
1700
|
};
|
|
1127
|
-
}, ...STRAPI5 && { history: historyService } },
|
|
1701
|
+
}, ...STRAPI5 && { history: historyService, trash: trashService } },
|
|
1128
1702
|
controllers: {
|
|
1129
1703
|
catalog: ({ strapi }) => ({
|
|
1130
1704
|
async find(ctx) {
|
|
@@ -1218,7 +1792,7 @@ module.exports = {
|
|
|
1218
1792
|
ctx.body = value;
|
|
1219
1793
|
}
|
|
1220
1794
|
}),
|
|
1221
|
-
...STRAPI5 && { history: historyController }
|
|
1795
|
+
...STRAPI5 && { history: historyController, trash: trashController }
|
|
1222
1796
|
},
|
|
1223
1797
|
routes: { admin: { type: "admin", routes: [
|
|
1224
1798
|
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
@@ -1232,11 +1806,20 @@ module.exports = {
|
|
|
1232
1806
|
config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
|
|
1233
1807
|
}] }
|
|
1234
1808
|
})),
|
|
1235
|
-
...STRAPI5 ? [
|
|
1236
|
-
|
|
1809
|
+
...STRAPI5 ? [
|
|
1810
|
+
["GET", "/history/:uid/:documentId", "history.list", "history.read"],
|
|
1811
|
+
["GET", "/history-events/:id", "history.find", "history.read"],
|
|
1812
|
+
["GET", "/activity", "history.activity", "activity.read"],
|
|
1813
|
+
["GET", "/trash", "trash.list", "trash.read"],
|
|
1814
|
+
["GET", "/trash/:id", "trash.find", "trash.read"],
|
|
1815
|
+
["GET", "/trash/:id/check", "trash.check", "trash.restore"],
|
|
1816
|
+
["POST", "/trash/:id/restore", "trash.restore", "trash.restore"],
|
|
1817
|
+
["DELETE", "/trash/:id", "trash.remove", "trash.purge"]
|
|
1818
|
+
].map(([method, path, handler, action]) => ({
|
|
1819
|
+
method,
|
|
1237
1820
|
path,
|
|
1238
|
-
handler
|
|
1239
|
-
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}
|
|
1821
|
+
handler,
|
|
1822
|
+
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.${action}`] } }] }
|
|
1240
1823
|
})) : []
|
|
1241
1824
|
] } }
|
|
1242
1825
|
};
|