@aduptive/strapi-blockscene 1.0.0-alpha.1 → 1.0.0-alpha.11
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 +1017 -54
- package/dist/admin.cjs +8675 -550
- package/dist/admin.mjs +8699 -523
- package/dist/server.cjs +1865 -48
- package/package.json +3 -4
package/dist/server.cjs
CHANGED
|
@@ -21,6 +21,10 @@ var require_groups = __commonJS({
|
|
|
21
21
|
return Object.keys(out).length ? out : null;
|
|
22
22
|
}
|
|
23
23
|
var uidOf = (row) => typeof row?.__component === "string" ? row.__component : "";
|
|
24
|
+
var invalidComponents2 = (rows, allowed) => {
|
|
25
|
+
const uids = new Set(Array.isArray(allowed) ? allowed : []);
|
|
26
|
+
return (Array.isArray(rows) ? rows : []).flatMap((row, index) => uidOf(row) && !uids.has(uidOf(row)) ? [{ index, uid: uidOf(row) }] : []);
|
|
27
|
+
};
|
|
24
28
|
var isOpener = (row, g) => Boolean(g) && Object.prototype.hasOwnProperty.call(g, uidOf(row));
|
|
25
29
|
var isClose = (row, g) => Boolean(g) && Object.values(g).includes(uidOf(row));
|
|
26
30
|
var blockKey = (row) => row && row.id != null ? `${row.__component}#${row.id}` : row && row.__temp_key__ != null ? String(row.__temp_key__) : null;
|
|
@@ -104,9 +108,54 @@ var require_groups = __commonJS({
|
|
|
104
108
|
const start = rows.findIndex((row) => blockKey(row) === String(key));
|
|
105
109
|
if (start < 0 || !isOpener(rows[start], g)) return null;
|
|
106
110
|
const [, end] = groupRange(rows, start, g);
|
|
111
|
+
if (end <= start || uidOf(rows[end]) !== g[uidOf(rows[start])]) return null;
|
|
107
112
|
return [...rows.slice(0, start), ...rows.slice(end + 1)];
|
|
108
113
|
}
|
|
109
|
-
|
|
114
|
+
function unwrapGroup(rows, key, g) {
|
|
115
|
+
if (!g) return null;
|
|
116
|
+
const start = rows.findIndex((row) => blockKey(row) === String(key));
|
|
117
|
+
if (start < 0 || !isOpener(rows[start], g)) return null;
|
|
118
|
+
const [, end] = groupRange(rows, start, g);
|
|
119
|
+
if (end <= start || uidOf(rows[end]) !== g[uidOf(rows[start])]) return null;
|
|
120
|
+
return [...rows.slice(0, start), ...rows.slice(start + 1, end), ...rows.slice(end + 1)];
|
|
121
|
+
}
|
|
122
|
+
var MAX_COLUMNS = 12;
|
|
123
|
+
var parseColumns = (value) => {
|
|
124
|
+
const n = typeof value === "number" ? value : typeof value === "string" && /^\s*\d{1,3}\s*$/.test(value) ? Number(value) : NaN;
|
|
125
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
126
|
+
};
|
|
127
|
+
var columnsAttribute = (attr) => Boolean(attr) && typeof attr === "object" && !attr.private && (["integer", "biginteger", "float", "decimal", "string"].includes(attr.type) || attr.type === "enumeration" && Array.isArray(attr.enum) && attr.enum.length > 0 && attr.enum.every((value) => parseColumns(value) !== null));
|
|
128
|
+
var layoutFields2 = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => columnsAttribute(attr)).map(([name]) => name);
|
|
129
|
+
function validLayout(value, attributes) {
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
131
|
+
if (!Object.keys(value).every((key) => ["columnsField", "mobileColumnsField", "maxColumns"].includes(key))) return false;
|
|
132
|
+
const field = (name) => typeof name === "string" && /^[A-Za-z_][\w-]{0,63}$/.test(name) && (!attributes || columnsAttribute(attributes[name]));
|
|
133
|
+
return field(value.columnsField) && (value.mobileColumnsField === void 0 || field(value.mobileColumnsField)) && (value.maxColumns === void 0 || Number.isInteger(value.maxColumns) && value.maxColumns >= 1 && value.maxColumns <= MAX_COLUMNS);
|
|
134
|
+
}
|
|
135
|
+
function layoutColumns(row, layout, schema) {
|
|
136
|
+
const max = layout.maxColumns || MAX_COLUMNS;
|
|
137
|
+
const read = (name) => Math.min(max, parseColumns(row?.[name]) ?? parseColumns(schema?.attributes?.[name]?.default) ?? 1);
|
|
138
|
+
return { desktop: read(layout.columnsField), mobile: layout.mobileColumnsField ? read(layout.mobileColumnsField) : null };
|
|
139
|
+
}
|
|
140
|
+
module2.exports = {
|
|
141
|
+
safeGroups: safeGroups2,
|
|
142
|
+
validateGroups: validateGroups2,
|
|
143
|
+
invalidComponents: invalidComponents2,
|
|
144
|
+
groupRange,
|
|
145
|
+
groupRows,
|
|
146
|
+
topLevelRanges,
|
|
147
|
+
moveGroup,
|
|
148
|
+
removeGroup,
|
|
149
|
+
unwrapGroup,
|
|
150
|
+
blockKey,
|
|
151
|
+
isOpener,
|
|
152
|
+
isClose,
|
|
153
|
+
MAX_COLUMNS,
|
|
154
|
+
parseColumns,
|
|
155
|
+
layoutFields: layoutFields2,
|
|
156
|
+
validLayout,
|
|
157
|
+
layoutColumns
|
|
158
|
+
};
|
|
110
159
|
}
|
|
111
160
|
});
|
|
112
161
|
|
|
@@ -118,6 +167,7 @@ var require_settings = __commonJS({
|
|
|
118
167
|
var TEMPLATES2 = ["generic", "banner", "cards", "imageText", "faq", "none"];
|
|
119
168
|
var INITIAL_STATES = ["closed", "open", "remember"];
|
|
120
169
|
var PREVIEW_MODES = ["form", "split", "preview"];
|
|
170
|
+
var HIDDEN_MODES = ["strip", "flag", "off"];
|
|
121
171
|
var DEFAULTS2 = {
|
|
122
172
|
palette: { background: "#F6F6F9", surface: "#DCDCE4", text: "#32324D", accent: "#4945FF" },
|
|
123
173
|
components: {},
|
|
@@ -125,13 +175,125 @@ var require_settings = __commonJS({
|
|
|
125
175
|
// blockPreviewInForm: compact read-only preview above a block's fields in form
|
|
126
176
|
// mode. Only installations that ship the accordion integration (server config
|
|
127
177
|
// `blockPreview: true`) show and honour it; the page preview is the editor.
|
|
128
|
-
editor: {
|
|
178
|
+
editor: {
|
|
179
|
+
enabled: true,
|
|
180
|
+
showOpenAll: true,
|
|
181
|
+
showCloseAll: true,
|
|
182
|
+
showRowThumbnails: true,
|
|
183
|
+
initialState: "closed",
|
|
184
|
+
previewMode: "form",
|
|
185
|
+
previewUrl: "",
|
|
186
|
+
blockPreviewUrl: "",
|
|
187
|
+
blockPreviewInForm: false,
|
|
188
|
+
// Strapi 5: rich-text custom fields render a read-only preview until clicked (see admin/LazyInput.tsx).
|
|
189
|
+
lazyEditors: true,
|
|
190
|
+
lazyFields: ["plugin::ckeditor5.CKEditor"],
|
|
191
|
+
// Row actions in each block header (see README "Row actions"). hiddenBlocks: what the content API does with rows
|
|
192
|
+
// hidden on the site: 'strip' removes them, 'flag' sends them with the attribute, 'off' hides the eye icon.
|
|
193
|
+
confirmDelete: true,
|
|
194
|
+
hiddenBlocks: "strip",
|
|
195
|
+
duplicate: true,
|
|
196
|
+
clipboard: true,
|
|
197
|
+
// Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
|
|
198
|
+
friendlyLabels: true,
|
|
199
|
+
// Page preview pane (Strapi 5): which toolbar controls show, in order, and the width menu's entries.
|
|
200
|
+
previewToolbar: ["modes", "history", "versions", "devices", "status", "actions"],
|
|
201
|
+
previewDevices: ["fit", "mobile", "tablet", "desktop"]
|
|
202
|
+
},
|
|
203
|
+
// Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
|
|
204
|
+
// previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
|
|
205
|
+
contentTypes: {},
|
|
206
|
+
// Version history (Strapi 5, see README "Version history"): off until turned on. contentTypes: 'all' (every api:: type)
|
|
207
|
+
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays; deleted
|
|
208
|
+
// documents stay in the trash trashDays (fixed per document when it is deleted).
|
|
209
|
+
history: { enabled: false, contentTypes: "all", retentionDays: 90, maxSnapshots: 100, eventDays: 365, trashDays: 90 }
|
|
129
210
|
};
|
|
211
|
+
var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
|
|
212
|
+
var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
|
|
213
|
+
var OPENS = ["modal", "drawer"];
|
|
214
|
+
var LABEL = /^[^<>]{1,40}$/;
|
|
215
|
+
var TOOLBAR = ["modes", "history", "versions", "devices", "status", "actions"];
|
|
216
|
+
var DEVICE_NAMES = ["fit", "mobile", "tablet", "desktop"];
|
|
217
|
+
var previewToolbar = (value) => Array.isArray(value) && value.every((id) => TOOLBAR.includes(id)) && new Set(value).size === value.length;
|
|
218
|
+
var DEVICE_LABEL = /^[^<>]{1,24}$/;
|
|
219
|
+
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;
|
|
220
|
+
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;
|
|
221
|
+
var cleanDevices = (value) => value.map((item) => typeof item === "string" ? item : { label: item.label.trim(), width: item.width });
|
|
222
|
+
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)));
|
|
223
|
+
var TYPE_KEYS = {
|
|
224
|
+
enabled: (value) => typeof value === "boolean",
|
|
225
|
+
previewMode: (value) => PREVIEW_MODES.includes(value),
|
|
226
|
+
sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
|
|
227
|
+
previewToolbar,
|
|
228
|
+
previewDevices,
|
|
229
|
+
sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
|
|
230
|
+
};
|
|
231
|
+
var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
|
|
130
232
|
var PREVIEW_URL = /^https?:\/\/[^\s"'<>]{1,500}$/;
|
|
233
|
+
var blockPreviewUrl = (value) => typeof value === "string" && (value === "" || PREVIEW_URL.test(value) && !/[{}]/.test(value.replace(/\{(uid|name|category|variant|locale)\}/g, "")) && URL.canParse?.(value.replace(/[{}]/g, "")) !== false);
|
|
131
234
|
var COLOR = /^#[0-9A-Fa-f]{6}$/;
|
|
235
|
+
var FIELD_UID = /^(plugin|global)::[\w.-]+$/;
|
|
236
|
+
var lazyFields = (value) => Array.isArray(value) && value.length <= 20 && value.every((uid) => typeof uid === "string" && FIELD_UID.test(uid));
|
|
132
237
|
var UID = /^[a-z0-9-]+\.[a-z0-9-]+$/;
|
|
238
|
+
var API_UID = /^api::[\w-]+\.[\w-]+$/;
|
|
239
|
+
var days = (value) => Number.isInteger(value) && value >= 1 && value <= 3650;
|
|
240
|
+
var HISTORY_KEYS = {
|
|
241
|
+
enabled: (value) => typeof value === "boolean",
|
|
242
|
+
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))),
|
|
243
|
+
retentionDays: days,
|
|
244
|
+
maxSnapshots: (value) => Number.isInteger(value) && value >= 1 && value <= 1e3,
|
|
245
|
+
eventDays: days,
|
|
246
|
+
trashDays: days
|
|
247
|
+
};
|
|
248
|
+
var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
|
|
249
|
+
var TAG = /^[^<>\n]{1,24}$/;
|
|
250
|
+
var cleanTags = (value) => Array.isArray(value) ? [...new Set(value.filter((tag) => typeof tag === "string" && TAG.test(tag.trim())).map((tag) => tag.trim()))].slice(0, 10) : [];
|
|
251
|
+
var validTags = (value) => Array.isArray(value) && value.length <= 10 && value.every((tag) => typeof tag === "string" && TAG.test(tag.trim()));
|
|
252
|
+
var directComponentUids = (contentTypes, perType = {}) => [...new Set(Object.entries(contentTypes || {}).filter(([uid]) => perType?.[uid]?.enabled !== false).flatMap(([, schema]) => Object.values(schema?.attributes || {}).filter((attr) => attr?.type === "dynamiczone").flatMap((attr) => attr.components || [])))];
|
|
253
|
+
var COMPONENT_CATEGORY = /^[a-z0-9][a-z0-9_-]{0,49}$/;
|
|
254
|
+
var safeComponentCategories2 = (value) => value == null ? null : Array.isArray(value) && value.length > 0 && value.length <= 50 && value.every((category) => typeof category === "string" && COMPONENT_CATEGORY.test(category)) ? [...new Set(value)] : null;
|
|
255
|
+
var settingsComponentUids2 = (contentTypes, components, perType = {}, categories = null) => {
|
|
256
|
+
const allowed = safeComponentCategories2(categories);
|
|
257
|
+
return directComponentUids(contentTypes, perType).filter((uid) => !allowed || allowed.includes(components?.[uid]?.category || uid.split(".")[0]));
|
|
258
|
+
};
|
|
259
|
+
var GUESSES = [
|
|
260
|
+
["hero", /hero|banner|cover/],
|
|
261
|
+
["text", /text|rich|quote|title/],
|
|
262
|
+
["media", /image|media|video|gallery|carousel/],
|
|
263
|
+
["listing", /list|query|archive|related|posts|projects/],
|
|
264
|
+
["cards", /card/],
|
|
265
|
+
["cta", /cta|button|link/],
|
|
266
|
+
["form", /form|contact/],
|
|
267
|
+
["layout", /wrapper|column|grid|divider|divisor|spacer|section/]
|
|
268
|
+
];
|
|
269
|
+
function guessTypology(uid, displayName = "") {
|
|
270
|
+
const name = `${uid.split(".").pop()} ${displayName}`.toLowerCase();
|
|
271
|
+
return GUESSES.find(([, pattern]) => pattern.test(name))?.[0] || "text";
|
|
272
|
+
}
|
|
273
|
+
function facetsOf(uid, schema, schemas = {}) {
|
|
274
|
+
const facets = /* @__PURE__ */ new Set();
|
|
275
|
+
const read = (attributes, nested) => {
|
|
276
|
+
for (const attr of Object.values(attributes || {})) {
|
|
277
|
+
if (attr?.type === "media") {
|
|
278
|
+
const allowed = attr.allowedTypes;
|
|
279
|
+
if (!allowed || allowed.includes("images")) facets.add("image");
|
|
280
|
+
if (!allowed || allowed.includes("videos")) facets.add("video");
|
|
281
|
+
if (attr.multiple) facets.add("gallery");
|
|
282
|
+
} else if (["richtext", "blocks"].includes(attr?.type) || attr?.type === "customField" && /ckeditor/i.test(String(attr.customField))) facets.add("richtext");
|
|
283
|
+
else if (attr?.type === "relation") facets.add("dynamic");
|
|
284
|
+
else if (attr?.type === "component") {
|
|
285
|
+
if (attr.repeatable) facets.add("list");
|
|
286
|
+
if (!nested) read(schemas[attr.component]?.attributes, true);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
read(schema?.attributes, false);
|
|
291
|
+
if (/query|archive|related/i.test(uid)) facets.add("dynamic");
|
|
292
|
+
if (/form|contact/i.test(`${uid} ${schema?.info?.displayName || ""}`)) facets.add("form");
|
|
293
|
+
return [...facets];
|
|
294
|
+
}
|
|
133
295
|
var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
|
|
134
|
-
var { safeGroups: safeGroups2 } = require_groups();
|
|
296
|
+
var { safeGroups: safeGroups2, validLayout } = require_groups();
|
|
135
297
|
var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
|
|
136
298
|
var fail2 = (message, details) => {
|
|
137
299
|
let ValidationError;
|
|
@@ -156,6 +318,17 @@ var require_settings = __commonJS({
|
|
|
156
318
|
["label", "description", "category", "keywords"].filter((key) => typeof entry[key] === "string").map((key) => [key, entry[key]])
|
|
157
319
|
);
|
|
158
320
|
entries[uid].image = safeUrl2(entry.image);
|
|
321
|
+
if (typeof entry.showRowThumbnail === "boolean") entries[uid].showRowThumbnail = entry.showRowThumbnail;
|
|
322
|
+
if (TYPOLOGIES2.includes(entry.typology)) entries[uid].typology = entry.typology;
|
|
323
|
+
const tags = cleanTags(entry.tags);
|
|
324
|
+
if (tags.length || entries[uid].category) entries[uid].tags = tags.length ? tags : cleanTags([entries[uid].category]);
|
|
325
|
+
}
|
|
326
|
+
for (const [uid, schema] of Object.entries(config.schemas || {})) {
|
|
327
|
+
const entry = entries[uid] ||= {};
|
|
328
|
+
for (const [key, value] of Object.entries(config.blocks?.[uid] || {})) if (entry[key] === void 0) entry[key] = value;
|
|
329
|
+
entry.facets = facetsOf(uid, schema, config.schemas);
|
|
330
|
+
entry.typology ||= guessTypology(uid, schema?.info?.displayName);
|
|
331
|
+
entry.tags ||= [];
|
|
159
332
|
}
|
|
160
333
|
return {
|
|
161
334
|
components: entries,
|
|
@@ -167,7 +340,8 @@ var require_settings = __commonJS({
|
|
|
167
340
|
groups: safeGroups2(config.groups, config.componentUids)
|
|
168
341
|
};
|
|
169
342
|
}
|
|
170
|
-
function validateSettings2(input, componentUids2) {
|
|
343
|
+
function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
|
|
344
|
+
const types = typeMap(contentTypeUids2);
|
|
171
345
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
|
|
172
346
|
if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
|
|
173
347
|
const out = structuredClone(DEFAULTS2);
|
|
@@ -179,9 +353,9 @@ var require_settings = __commonJS({
|
|
|
179
353
|
}
|
|
180
354
|
for (const [key, value] of Object.entries(input.editor || {})) {
|
|
181
355
|
if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
|
|
182
|
-
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : typeof value === "boolean";
|
|
356
|
+
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";
|
|
183
357
|
if (!valid) fail2(`Invalid value for editor option "${key}"`);
|
|
184
|
-
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : value;
|
|
358
|
+
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : key === "previewDevices" ? cleanDevices(value) : value;
|
|
185
359
|
}
|
|
186
360
|
const components = input.components || {};
|
|
187
361
|
if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
|
|
@@ -193,16 +367,48 @@ var require_settings = __commonJS({
|
|
|
193
367
|
if (key === "template") {
|
|
194
368
|
if (!TEMPLATES2.includes(entry.template)) fail2(`Unknown template for "${uid}"`);
|
|
195
369
|
clean.template = entry.template;
|
|
370
|
+
} else if (key === "typology") {
|
|
371
|
+
if (!TYPOLOGIES2.includes(entry.typology)) fail2(`Unknown typology for "${uid}"`);
|
|
372
|
+
clean.typology = entry.typology;
|
|
373
|
+
} else if (key === "tags") {
|
|
374
|
+
if (!validTags(entry.tags)) fail2(`Invalid tags for "${uid}": up to 10, each 1 to 24 characters`);
|
|
375
|
+
clean.tags = cleanTags(entry.tags);
|
|
196
376
|
} else if (key === "mediaId") {
|
|
197
377
|
if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
|
|
198
378
|
clean.mediaId = entry.mediaId;
|
|
379
|
+
} else if (key === "showRowThumbnail") {
|
|
380
|
+
if (typeof entry.showRowThumbnail !== "boolean") fail2(`Invalid row thumbnail setting for "${uid}"`);
|
|
381
|
+
clean.showRowThumbnail = entry.showRowThumbnail;
|
|
382
|
+
} else if (key === "layout") {
|
|
383
|
+
if (!validLayout(entry.layout, schemas && (schemas[uid]?.attributes || {}))) fail2(`Invalid layout for "${uid}": columnsField (and mobileColumnsField) must name a number, string or numeric enumeration attribute of the component; maxColumns 1 to 12`);
|
|
384
|
+
clean.layout = structuredClone(entry.layout);
|
|
199
385
|
} else fail2(`Unknown component setting "${key}"`);
|
|
200
386
|
}
|
|
201
387
|
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
202
388
|
}
|
|
389
|
+
const perType = input.contentTypes || {};
|
|
390
|
+
if (typeof perType !== "object" || Array.isArray(perType)) fail2("contentTypes must be an object");
|
|
391
|
+
for (const [uid, entry] of Object.entries(perType)) {
|
|
392
|
+
if (!(uid in types)) fail2(`Unknown content type "${uid}"`);
|
|
393
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) fail2(`Invalid entry for "${uid}"`);
|
|
394
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
395
|
+
if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
|
|
396
|
+
if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
|
|
397
|
+
}
|
|
398
|
+
if (Object.keys(entry).length) out.contentTypes[uid] = { ...structuredClone(entry), ...entry.previewDevices && { previewDevices: cleanDevices(entry.previewDevices) } };
|
|
399
|
+
}
|
|
400
|
+
const history = input.history || {};
|
|
401
|
+
if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
|
|
402
|
+
for (const [key, value] of Object.entries(history)) {
|
|
403
|
+
if (!(key in HISTORY_KEYS)) fail2(`Unknown history option "${key}"`);
|
|
404
|
+
if (!HISTORY_KEYS[key](value, historyTypes)) fail2(`Invalid value for history option "${key}"`);
|
|
405
|
+
out.history[key] = Array.isArray(value) ? [...new Set(value)] : value;
|
|
406
|
+
}
|
|
407
|
+
if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
|
|
203
408
|
return out;
|
|
204
409
|
}
|
|
205
|
-
function mergeSaved2(saved, componentUids2) {
|
|
410
|
+
function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
|
|
411
|
+
const types = typeMap(contentTypeUids2);
|
|
206
412
|
const out = structuredClone(DEFAULTS2);
|
|
207
413
|
if (!saved || typeof saved !== "object") return out;
|
|
208
414
|
for (const key of Object.keys(DEFAULTS2.palette)) if (COLOR.test(String(saved.palette?.[key]))) out.palette[key] = saved.palette[key];
|
|
@@ -211,19 +417,1415 @@ var require_settings = __commonJS({
|
|
|
211
417
|
if (value === void 0 || typeof value !== typeof DEFAULTS2.editor[key]) continue;
|
|
212
418
|
if (key === "initialState" && !INITIAL_STATES.includes(value)) continue;
|
|
213
419
|
if (key === "previewMode" && !PREVIEW_MODES.includes(value)) continue;
|
|
420
|
+
if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
|
|
214
421
|
if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
|
|
422
|
+
if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
|
|
423
|
+
if (key === "previewToolbar" && !previewToolbar(value)) continue;
|
|
424
|
+
if (key === "previewDevices" && !previewDevices(value)) continue;
|
|
425
|
+
if (key === "lazyFields") {
|
|
426
|
+
if (!Array.isArray(value)) continue;
|
|
427
|
+
out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
215
430
|
out.editor[key] = value;
|
|
216
431
|
}
|
|
217
|
-
for (const [uid, entry] of Object.entries(saved.components || {}))
|
|
432
|
+
for (const [uid, entry] of Object.entries(saved.components || {})) {
|
|
433
|
+
if (!componentUids2.includes(uid) || !entry || typeof entry !== "object") continue;
|
|
434
|
+
const clean = { ...entry };
|
|
435
|
+
if ("typology" in clean && !TYPOLOGIES2.includes(clean.typology)) delete clean.typology;
|
|
436
|
+
if ("tags" in clean) {
|
|
437
|
+
clean.tags = cleanTags(clean.tags);
|
|
438
|
+
if (!clean.tags.length) delete clean.tags;
|
|
439
|
+
}
|
|
440
|
+
if ("showRowThumbnail" in clean && typeof clean.showRowThumbnail !== "boolean") delete clean.showRowThumbnail;
|
|
441
|
+
if ("layout" in clean && !validLayout(clean.layout, schemas && (schemas[uid]?.attributes || {}))) delete clean.layout;
|
|
442
|
+
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
443
|
+
}
|
|
444
|
+
for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
|
|
445
|
+
if (!(uid in types) || !entry || typeof entry !== "object") continue;
|
|
446
|
+
const fix = (key, value) => key === "sidebar" && Array.isArray(value) ? value.filter((item) => sidebarItem(item, types[uid])) : value;
|
|
447
|
+
const clean = Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, fix(key, value)]).filter(([key, value]) => TYPE_KEYS[key]?.(value, types[uid])));
|
|
448
|
+
if (Object.keys(clean).length) out.contentTypes[uid] = clean;
|
|
449
|
+
}
|
|
450
|
+
for (const key of Object.keys(HISTORY_KEYS)) {
|
|
451
|
+
let value = saved.history?.[key];
|
|
452
|
+
if (key === "contentTypes" && Array.isArray(value)) value = value.filter((uid) => HISTORY_KEYS.contentTypes([uid], historyTypes));
|
|
453
|
+
if (value !== void 0 && HISTORY_KEYS[key](value, historyTypes)) out.history[key] = value;
|
|
454
|
+
}
|
|
455
|
+
out.history.eventDays = Math.max(out.history.eventDays, out.history.retentionDays);
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
var ENTRY_KEYS = ["components", "contentTypes"];
|
|
459
|
+
var UNSET_WHEN_EMPTY = ["previewUrl", "blockPreviewUrl"];
|
|
460
|
+
var withoutEmpty = (editor) => Object.fromEntries(Object.entries(editor).filter(([key, value]) => !(UNSET_WHEN_EMPTY.includes(key) && value === "")));
|
|
461
|
+
function layer2(base, saved) {
|
|
462
|
+
if (!base) return saved;
|
|
463
|
+
const doc = saved && typeof saved === "object" ? saved : {};
|
|
464
|
+
const own = (key) => doc[key] && typeof doc[key] === "object" ? key === "editor" ? withoutEmpty(doc[key]) : doc[key] : {};
|
|
465
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...own(key) }]));
|
|
466
|
+
}
|
|
467
|
+
function overrides2(next, base) {
|
|
468
|
+
if (!base) return next;
|
|
469
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => {
|
|
470
|
+
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 === ""));
|
|
471
|
+
if (ENTRY_KEYS.includes(key)) {
|
|
472
|
+
for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
|
|
473
|
+
}
|
|
474
|
+
return [key, Object.fromEntries(entries)];
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120, help: 500 };
|
|
478
|
+
var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
479
|
+
var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
480
|
+
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, text2]) => LOCALE.test(locale) && fieldText(text2, max));
|
|
481
|
+
function validateFields2(input, schemas = {}, stale = []) {
|
|
482
|
+
if (input === null || input === void 0) return {};
|
|
483
|
+
if (!plain(input)) fail2("fields must be an object");
|
|
484
|
+
if (JSON.stringify(input).length > 256 * 1024) fail2("fields config too large");
|
|
485
|
+
const out = {};
|
|
486
|
+
for (const [uid, entries] of Object.entries(input)) {
|
|
487
|
+
if (!plain(entries)) fail2(`Invalid entry for "${uid}"`);
|
|
488
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
489
|
+
if (!plain(entry)) fail2(`Invalid entry for "${uid}.${name}"`);
|
|
490
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
491
|
+
if (!(key in FIELD_TEXT)) fail2(`Unknown field setting "${key}" of "${uid}.${name}"`);
|
|
492
|
+
if (!fieldText(value, FIELD_TEXT[key])) fail2(`Invalid ${key} of "${uid}.${name}": a string of 1 to ${FIELD_TEXT[key]} characters, or { "<locale>": string }`);
|
|
493
|
+
}
|
|
494
|
+
if (!schemas[uid]?.includes(name)) {
|
|
495
|
+
stale.push(`${uid}.${name}`);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
;
|
|
499
|
+
(out[uid] ||= {})[name] = structuredClone(entry);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
var str = (max) => (value) => typeof value === "string" && value.trim() !== "" && value.length <= max;
|
|
505
|
+
var BLOCK_META = { label: str(80), typology: (value) => TYPOLOGIES2.includes(value), tags: validTags, keywords: str(500), image: (value) => safeUrl2(value) !== void 0 };
|
|
506
|
+
var localize = (holder, key, source, locale, text2) => {
|
|
507
|
+
holder[key] = { ...plain(holder[key]) ? holder[key] : { en: holder[key] ?? source }, [locale]: text2 };
|
|
508
|
+
};
|
|
509
|
+
function schemaMetadata2(schemas = {}, translations = null, notes = []) {
|
|
510
|
+
const blocks = {}, fields = {};
|
|
511
|
+
const own = (holder, where) => {
|
|
512
|
+
const value = holder?.pluginOptions?.blockscene;
|
|
513
|
+
if (value === void 0 || plain(value)) return value || {};
|
|
514
|
+
notes.push(`${where}: pluginOptions.blockscene must be an object`);
|
|
515
|
+
return {};
|
|
516
|
+
};
|
|
517
|
+
for (const [uid, schema] of Object.entries(schemas)) {
|
|
518
|
+
if (!uid.includes("::")) {
|
|
519
|
+
const block = {};
|
|
520
|
+
for (const [key, value] of Object.entries(own(schema, uid))) {
|
|
521
|
+
if (!Object.hasOwn(BLOCK_META, key)) notes.push(`${uid}: unknown key "${key}"`);
|
|
522
|
+
else if (!BLOCK_META[key](value)) notes.push(`${uid}: invalid ${key}`);
|
|
523
|
+
else block[key] = key === "tags" ? cleanTags(value) : value;
|
|
524
|
+
}
|
|
525
|
+
if (typeof schema?.info?.description === "string" && schema.info.description.trim()) block.description = schema.info.description;
|
|
526
|
+
if (Object.keys(block).length) blocks[uid] = block;
|
|
527
|
+
}
|
|
528
|
+
for (const [name, attr] of Object.entries(schema?.attributes || {})) {
|
|
529
|
+
const texts = {};
|
|
530
|
+
for (const [key, value] of Object.entries(own(attr, `${uid}.${name}`))) {
|
|
531
|
+
if (!Object.hasOwn(FIELD_TEXT, key)) notes.push(`${uid}.${name}: unknown key "${key}"`);
|
|
532
|
+
else if (!str(FIELD_TEXT[key])(value)) notes.push(`${uid}.${name}: ${key} must be a string of 1 to ${FIELD_TEXT[key]} characters`);
|
|
533
|
+
else texts[key] = value;
|
|
534
|
+
}
|
|
535
|
+
if (Object.keys(texts).length) (fields[uid] ||= {})[name] = texts;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (translations != null && !plain(translations)) {
|
|
539
|
+
notes.push('translations must be { "<locale>": { "<key>": text } }');
|
|
540
|
+
translations = null;
|
|
541
|
+
}
|
|
542
|
+
const stale = [];
|
|
543
|
+
for (const [locale, entries] of Object.entries(translations || {})) {
|
|
544
|
+
if (!LOCALE.test(locale) || !plain(entries) || JSON.stringify(entries).length > 256 * 1024) {
|
|
545
|
+
notes.push(`translations "${locale}" ignored: expected a locale code with a flat { "<key>": text } map under 256 KB`);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
for (const [key, text2] of Object.entries(entries)) {
|
|
549
|
+
const uid = Object.keys(schemas).find((uid2) => key === uid2 || key.startsWith(`${uid2}.`));
|
|
550
|
+
const [name, part = "label", extra] = uid ? key.slice(uid.length + 1).split(".") : [];
|
|
551
|
+
const block = uid && !uid.includes("::");
|
|
552
|
+
const target = block && key === uid ? [blocks[uid] ||= {}, "label", schemas[uid]?.info?.displayName || uid, 80] : block && key === `${uid}.description` ? [blocks[uid] ||= {}, "description", null, 300] : uid && Object.hasOwn(schemas[uid]?.attributes || {}, name) && Object.hasOwn(FIELD_TEXT, part) && extra === void 0 ? [null, part, null, FIELD_TEXT[part]] : null;
|
|
553
|
+
if (!target) {
|
|
554
|
+
stale.push(`${locale} "${key}"`);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const [holder, field, source, max] = target;
|
|
558
|
+
if (!str(max)(text2)) {
|
|
559
|
+
notes.push(`translations ${locale} "${key}": a string of 1 to ${max} characters`);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
localize(holder || ((fields[uid] ||= {})[name] ||= {}), field, source, locale, text2);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (stale.length) notes.push(`translation keys naming no block or field were skipped: ${stale.slice(0, 20).join(", ")}${stale.length > 20 ? ` and ${stale.length - 20} more` : ""}`);
|
|
566
|
+
return { blocks, fields };
|
|
567
|
+
}
|
|
568
|
+
var VARIANT = { id: /^[a-z0-9][a-z0-9_-]{0,39}$/, perComponent: 12, bytes: 16 * 1024, total: 256 * 1024, items: 100 };
|
|
569
|
+
var text = (value) => typeof value === "string";
|
|
570
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
571
|
+
var SCALARS = {
|
|
572
|
+
string: text,
|
|
573
|
+
text,
|
|
574
|
+
richtext: text,
|
|
575
|
+
email: text,
|
|
576
|
+
uid: text,
|
|
577
|
+
date: text,
|
|
578
|
+
datetime: text,
|
|
579
|
+
time: text,
|
|
580
|
+
timestamp: text,
|
|
581
|
+
integer: Number.isInteger,
|
|
582
|
+
biginteger: (value) => Number.isInteger(value) || text(value) && /^-?\d{1,19}$/.test(value),
|
|
583
|
+
float: finite,
|
|
584
|
+
decimal: finite,
|
|
585
|
+
boolean: (value) => typeof value === "boolean",
|
|
586
|
+
json: () => true,
|
|
587
|
+
customField: () => true,
|
|
588
|
+
blocks: Array.isArray,
|
|
589
|
+
enumeration: (value, attr) => (attr.enum || []).includes(value)
|
|
590
|
+
};
|
|
591
|
+
function variantValues(values, schema, schemas, path, skipped) {
|
|
592
|
+
if (!plain(values)) throw new Error(`${path} must be an object`);
|
|
593
|
+
const out = {};
|
|
594
|
+
for (const [name, value] of Object.entries(values)) {
|
|
595
|
+
if (name === "id" || name === "__component") continue;
|
|
596
|
+
const attr = schema.attributes?.[name], at = `${path}.${name}`;
|
|
597
|
+
if (!attr) throw new Error(`${at} is not an attribute of the component`);
|
|
598
|
+
if (attr.type === "media" || attr.type === "relation") {
|
|
599
|
+
skipped.push(at);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (attr.type === "component") {
|
|
603
|
+
const nested = schemas[attr.component], max = Math.min(attr.max ?? Infinity, VARIANT.items);
|
|
604
|
+
if (!nested) throw new Error(`${at}: unknown component "${attr.component}"`);
|
|
605
|
+
if (!attr.repeatable) out[name] = value === null ? null : variantValues(value, nested, schemas, at, skipped);
|
|
606
|
+
else if (!Array.isArray(value) || value.length > max) throw new Error(`${at} must be a list of at most ${max} items`);
|
|
607
|
+
else out[name] = value.map((item, index) => variantValues(item, nested, schemas, `${at}[${index}]`, skipped));
|
|
608
|
+
} else if (value === null || SCALARS[attr.type]?.(value, attr)) out[name] = structuredClone(value);
|
|
609
|
+
else throw new Error(`${at}: ${SCALARS[attr.type] ? `not a valid ${attr.type}` : `${attr.type} values are not supported`}`);
|
|
610
|
+
}
|
|
218
611
|
return out;
|
|
219
612
|
}
|
|
220
|
-
|
|
613
|
+
function validateVariants2(config, schemas = {}, notes = []) {
|
|
614
|
+
const out = {};
|
|
615
|
+
let total = 0;
|
|
616
|
+
for (const [uid, entry] of Object.entries(plain(config) ? config : {})) {
|
|
617
|
+
if (!plain(entry) || entry.variants === void 0) continue;
|
|
618
|
+
if (!schemas[uid]) {
|
|
619
|
+
notes.push(`${uid}: unknown component`);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (!Array.isArray(entry.variants) || entry.variants.length > VARIANT.perComponent) {
|
|
623
|
+
notes.push(`${uid}: variants must be a list of at most ${VARIANT.perComponent}`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const ids = /* @__PURE__ */ new Set();
|
|
627
|
+
entry.variants.forEach((variant, index) => {
|
|
628
|
+
const name = `${uid} variant "${variant?.id ?? index}"`;
|
|
629
|
+
try {
|
|
630
|
+
if (!plain(variant) || Object.keys(variant).some((key) => !["id", "label", "values"].includes(key))) throw new Error("expected { id, label, values }");
|
|
631
|
+
if (!text(variant.id) || !VARIANT.id.test(variant.id) || ids.has(variant.id)) throw new Error('id must be 1 to 40 lowercase letters, digits, "-" or "_", unique per component');
|
|
632
|
+
if (!fieldText(variant.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
|
|
633
|
+
const skipped = [];
|
|
634
|
+
const values = variantValues(variant.values ?? {}, schemas[uid], schemas, "values", skipped);
|
|
635
|
+
const size = JSON.stringify(values).length;
|
|
636
|
+
if (size > VARIANT.bytes) throw new Error(`values over ${VARIANT.bytes / 1024} KB`);
|
|
637
|
+
if (total + size > VARIANT.total) throw new Error(`all variants together over ${VARIANT.total / 1024} KB`);
|
|
638
|
+
if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
|
|
639
|
+
ids.add(variant.id);
|
|
640
|
+
total += size;
|
|
641
|
+
(out[uid] ||= []).push({ id: variant.id, label: structuredClone(variant.label), values });
|
|
642
|
+
} catch (error) {
|
|
643
|
+
notes.push(`${name} ignored: ${error.message}`);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
function requiredProblems(schema, values, schemas, path, media, problems) {
|
|
650
|
+
for (const [field, attr] of Object.entries(schema?.attributes || {})) {
|
|
651
|
+
const at = `${path}.${field}`;
|
|
652
|
+
const supplied = Object.hasOwn(values, field);
|
|
653
|
+
const value = supplied ? values[field] : attr.default;
|
|
654
|
+
if (attr.type === "media" || attr.type === "relation") {
|
|
655
|
+
if (attr.required && !supplied) media.push(at);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (attr.type === "component") {
|
|
659
|
+
const nested = schemas[attr.component];
|
|
660
|
+
if (!nested) {
|
|
661
|
+
problems.push(`${at}: unknown component "${attr.component}"`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (attr.repeatable) {
|
|
665
|
+
const minimum = Math.max(attr.min ?? 0, attr.required ? 1 : 0);
|
|
666
|
+
const items = supplied ? value : Array.from({ length: minimum }, () => ({}));
|
|
667
|
+
if (!Array.isArray(items) || items.length < minimum) problems.push(`${at} requires at least ${minimum} item${minimum === 1 ? "" : "s"}`);
|
|
668
|
+
else items.forEach((item, index) => requiredProblems(nested, item, schemas, `${at}[${index}]`, media, problems));
|
|
669
|
+
} else if (supplied && value === null) {
|
|
670
|
+
if (attr.required) problems.push(`${at} is required`);
|
|
671
|
+
} else if (supplied || attr.required) requiredProblems(nested, supplied ? value : {}, schemas, at, media, problems);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (attr.required && (value === void 0 || value === null || value === "")) problems.push(`${at} is required`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function validateKits2(input, contentTypes = {}, schemas = {}, groups = null, notes = []) {
|
|
678
|
+
if (input === null || input === void 0) return {};
|
|
679
|
+
if (!plain(input) || JSON.stringify(input).length > 256 * 1024) {
|
|
680
|
+
notes.push("expected an object up to 256 KB");
|
|
681
|
+
return {};
|
|
682
|
+
}
|
|
683
|
+
const out = {};
|
|
684
|
+
for (const [uid, kits] of Object.entries(input)) {
|
|
685
|
+
const type = contentTypes[uid];
|
|
686
|
+
if (!type || !uid.startsWith("api::")) {
|
|
687
|
+
notes.push(`${uid}: unknown content type`);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (!Array.isArray(kits) || kits.length > 12) {
|
|
691
|
+
notes.push(`${uid}: kits must be a list of at most 12`);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const ids = /* @__PURE__ */ new Set();
|
|
695
|
+
for (const kit of kits) {
|
|
696
|
+
const name = `${uid} kit "${kit?.id ?? "?"}"`;
|
|
697
|
+
try {
|
|
698
|
+
if (!plain(kit) || Object.keys(kit).some((key) => !["id", "label", "locales", "zones"].includes(key))) throw new Error("expected { id, label, locales?, zones }");
|
|
699
|
+
if (!text(kit.id) || !VARIANT.id.test(kit.id) || ids.has(kit.id)) throw new Error('id must be 1 to 40 lowercase letters, digits, "-" or "_", unique per content type');
|
|
700
|
+
if (!fieldText(kit.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
|
|
701
|
+
if (kit.locales !== void 0 && (!Array.isArray(kit.locales) || !kit.locales.length || kit.locales.length > 20 || kit.locales.some((locale) => !text(locale) || !LOCALE.test(locale)))) throw new Error("locales must be 1 to 20 locale codes");
|
|
702
|
+
if (!plain(kit.zones) || !Object.keys(kit.zones).length) throw new Error("zones must be a non-empty object");
|
|
703
|
+
const zones = {};
|
|
704
|
+
for (const [zoneName, rows] of Object.entries(kit.zones)) {
|
|
705
|
+
const zone = type.attributes?.[zoneName];
|
|
706
|
+
if (zone?.type !== "dynamiczone") throw new Error(`unknown Dynamic Zone "${zoneName}"`);
|
|
707
|
+
if (!Array.isArray(rows) || !rows.length || rows.length > 50) throw new Error(`${zoneName} must contain 1 to 50 rows`);
|
|
708
|
+
const clean = [];
|
|
709
|
+
for (let index = 0; index < rows.length; index++) {
|
|
710
|
+
const row = rows[index];
|
|
711
|
+
if (!plain(row) || !text(row.__component)) throw new Error(`${zoneName}[${index}] must have __component`);
|
|
712
|
+
const component = row.__component;
|
|
713
|
+
if (!zone.components?.includes(component) || !schemas[component]) throw new Error(`${zoneName}[${index}]: component "${component}" is not allowed`);
|
|
714
|
+
const skipped = [];
|
|
715
|
+
const values = variantValues(row, schemas[component], schemas, `${zoneName}[${index}]`, skipped);
|
|
716
|
+
if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
|
|
717
|
+
const media = [], required = [];
|
|
718
|
+
requiredProblems(schemas[component], values, schemas, `${zoneName}[${index}]`, media, required);
|
|
719
|
+
if (required.length) throw new Error(required.slice(0, 5).join("; "));
|
|
720
|
+
if (media.length) notes.push(`${name}: required media or relations must be completed before publishing (${media.slice(0, 5).join(", ")}${media.length > 5 ? ", ..." : ""})`);
|
|
721
|
+
clean.push({ __component: component, values });
|
|
722
|
+
const close = groups?.[component];
|
|
723
|
+
if (close) {
|
|
724
|
+
if (!zone.components?.includes(close) || !schemas[close]) throw new Error(`${zoneName}[${index}]: close component "${close}" is not allowed`);
|
|
725
|
+
clean.push({ __component: close, values: {} });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (clean.length > (zone.max ?? Infinity)) throw new Error(`${zoneName} exceeds its maximum of ${zone.max}`);
|
|
729
|
+
zones[zoneName] = clean;
|
|
730
|
+
}
|
|
731
|
+
for (const [zoneName, zone] of Object.entries(type.attributes || {})) {
|
|
732
|
+
if (zone?.type !== "dynamiczone") continue;
|
|
733
|
+
const minimum = Math.max(zone.min ?? 0, zone.required ? 1 : 0);
|
|
734
|
+
if ((zones[zoneName]?.length || 0) < minimum) throw new Error(`${zoneName} requires at least ${minimum} row${minimum === 1 ? "" : "s"}`);
|
|
735
|
+
}
|
|
736
|
+
ids.add(kit.id);
|
|
737
|
+
(out[uid] ||= []).push({ id: kit.id, label: structuredClone(kit.label), ...kit.locales && { locales: [...new Set(kit.locales)] }, zones });
|
|
738
|
+
} catch (error) {
|
|
739
|
+
notes.push(`${name} ignored: ${error.message}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
var PREFS = { starred: 200, recent: 20 };
|
|
746
|
+
function validatePrefs2(input, componentUids2) {
|
|
747
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
|
|
748
|
+
const out = { starred: [], recent: [] };
|
|
749
|
+
for (const [key, value] of Object.entries(input)) {
|
|
750
|
+
if (!(key in PREFS)) fail2(`Unknown preference "${key}"`);
|
|
751
|
+
if (!Array.isArray(value) || value.length > PREFS[key] || !value.every((uid) => typeof uid === "string" && componentUids2.includes(uid))) fail2(`Invalid value for "${key}"`);
|
|
752
|
+
out[key] = [...new Set(value)];
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
var mergePrefs2 = (saved, componentUids2) => Object.fromEntries(Object.entries(PREFS).map(([key, max]) => [key, Array.isArray(saved?.[key]) ? [...new Set(saved[key].filter((uid) => componentUids2.includes(uid)))].slice(0, max) : []]));
|
|
757
|
+
module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, directComponentUids, settingsComponentUids: settingsComponentUids2, safeComponentCategories: safeComponentCategories2, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, validateVariants: validateVariants2, validateKits: validateKits2, schemaMetadata: schemaMetadata2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// server/hidden.js
|
|
762
|
+
var require_hidden = __commonJS({
|
|
763
|
+
"server/hidden.js"(exports2, module2) {
|
|
764
|
+
"use strict";
|
|
765
|
+
var NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/;
|
|
766
|
+
var DEFAULT_NAME = "bsHidden";
|
|
767
|
+
var hiddenName2 = (value) => value === void 0 ? DEFAULT_NAME : value === false ? false : typeof value === "string" && NAME.test(value) ? value : null;
|
|
768
|
+
var zoneComponents = (contentTypes) => [...new Set(Object.values(contentTypes || {}).flatMap((schema) => Object.values(schema?.attributes || {}).filter((attr) => attr?.type === "dynamiczone").flatMap((attr) => attr.components || [])))];
|
|
769
|
+
function injectHidden2(components, contentTypes, name) {
|
|
770
|
+
const added = [], skipped = [];
|
|
771
|
+
for (const uid of zoneComponents(contentTypes)) {
|
|
772
|
+
const schema = components?.[uid];
|
|
773
|
+
if (!schema) continue;
|
|
774
|
+
schema.attributes ||= {};
|
|
775
|
+
const current = schema.attributes[name];
|
|
776
|
+
if (current && current.type !== "boolean") {
|
|
777
|
+
skipped.push(uid);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!current) schema.attributes[name] = { type: "boolean", default: false, configurable: false };
|
|
781
|
+
added.push(uid);
|
|
782
|
+
}
|
|
783
|
+
return { added, skipped };
|
|
784
|
+
}
|
|
785
|
+
function hideFromBuilder2(services, uids, name) {
|
|
786
|
+
const has = new Set(uids);
|
|
787
|
+
const drop = (attributes) => Array.isArray(attributes) ? attributes.filter((attr) => attr?.name !== name) : attributes && typeof attributes === "object" ? Object.fromEntries(Object.entries(attributes).filter(([key]) => key !== name)) : attributes;
|
|
788
|
+
const extend = (uid, wrap) => {
|
|
789
|
+
if (services?.get?.(uid)) services.extend(uid, wrap);
|
|
790
|
+
};
|
|
791
|
+
extend("plugin::content-type-builder.components", (service) => ({ ...service, formatComponent(component, ...rest) {
|
|
792
|
+
const out = service.formatComponent(component, ...rest);
|
|
793
|
+
return has.has(component?.uid) && out?.schema ? { ...out, schema: { ...out.schema, attributes: drop(out.schema.attributes) } } : out;
|
|
794
|
+
} }));
|
|
795
|
+
extend("plugin::content-type-builder.schema", (service) => ({ ...service, async getSchema(...args) {
|
|
796
|
+
const out = await service.getSchema(...args);
|
|
797
|
+
for (const uid of has) if (out?.components?.[uid]) out.components[uid] = { ...out.components[uid], attributes: drop(out.components[uid].attributes) };
|
|
798
|
+
return out;
|
|
799
|
+
} }));
|
|
800
|
+
}
|
|
801
|
+
function stripHidden(data, schema, schemaOf, name, depth = 0) {
|
|
802
|
+
if (!data || typeof data !== "object" || !schema || depth > 20) return data;
|
|
803
|
+
if (Array.isArray(data)) {
|
|
804
|
+
data.forEach((item) => stripHidden(item, schema, schemaOf, name, depth));
|
|
805
|
+
return data;
|
|
806
|
+
}
|
|
807
|
+
const walk = (value, target) => Array.isArray(value) ? value.forEach((item) => stripHidden(item, target, schemaOf, name, depth + 1)) : stripHidden(value, target, schemaOf, name, depth + 1);
|
|
808
|
+
for (const [key, attr] of Object.entries(schema.attributes || {})) {
|
|
809
|
+
const value = data[key];
|
|
810
|
+
if (value == null || typeof value !== "object") continue;
|
|
811
|
+
if (attr?.type === "dynamiczone" && Array.isArray(value)) {
|
|
812
|
+
data[key] = value.filter((row) => !(row && row[name] === true));
|
|
813
|
+
for (const row of data[key]) stripHidden(row, schemaOf(row?.__component), schemaOf, name, depth + 1);
|
|
814
|
+
} else if (attr?.type === "component") walk(value, schemaOf(attr.component));
|
|
815
|
+
else if (attr?.type === "relation" && attr.target) walk(value, schemaOf(attr.target));
|
|
816
|
+
}
|
|
817
|
+
return data;
|
|
818
|
+
}
|
|
819
|
+
var contentApi = (strapi) => {
|
|
820
|
+
try {
|
|
821
|
+
return strapi.requestContext?.get?.()?.state?.route?.info?.type === "content-api";
|
|
822
|
+
} catch {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var READS = ["findMany", "findOne", "findFirst"];
|
|
827
|
+
function registerHiddenStrip2(strapi, name, plugin) {
|
|
828
|
+
if (!name) return;
|
|
829
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
830
|
+
const stripping = async () => {
|
|
831
|
+
try {
|
|
832
|
+
return (await plugin.service("settings").get()).editor.hiddenBlocks === "strip";
|
|
833
|
+
} catch {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
const apply = async (uid, result) => result && contentApi(strapi) && await stripping() ? stripHidden(result, schemaOf(uid), schemaOf, name) : result;
|
|
838
|
+
if (strapi.documents?.use) {
|
|
839
|
+
strapi.documents.use(async (ctx, next) => {
|
|
840
|
+
const result = await next();
|
|
841
|
+
return READS.includes(ctx.action) ? apply(ctx.uid, result) : result;
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
strapi.entityService?.decorate?.((service) => ({
|
|
846
|
+
async findMany(uid, params) {
|
|
847
|
+
return apply(uid, await service.findMany(uid, params));
|
|
848
|
+
},
|
|
849
|
+
async findOne(uid, id, params) {
|
|
850
|
+
return apply(uid, await service.findOne(uid, id, params));
|
|
851
|
+
},
|
|
852
|
+
async findPage(uid, params) {
|
|
853
|
+
const page = await service.findPage(uid, params);
|
|
854
|
+
if (page?.results) await apply(uid, page.results);
|
|
855
|
+
return page;
|
|
856
|
+
}
|
|
857
|
+
}));
|
|
858
|
+
}
|
|
859
|
+
module2.exports = { DEFAULT_NAME, hiddenName: hiddenName2, zoneComponents, injectHidden: injectHidden2, hideFromBuilder: hideFromBuilder2, stripHidden, registerHiddenStrip: registerHiddenStrip2 };
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// server/diff.js
|
|
864
|
+
var require_diff = __commonJS({
|
|
865
|
+
"server/diff.js"(exports2, module2) {
|
|
866
|
+
"use strict";
|
|
867
|
+
var IGNORED = ["id", "documentId", "locale", "localizations", "createdAt", "updatedAt", "publishedAt", "firstPublishedAt", "createdBy", "updatedBy", "strapi_stage", "strapi_assignee", "__temp_key__", "__pivot"];
|
|
868
|
+
var morph = (attr) => String(attr?.relation || "").toLowerCase().includes("morph");
|
|
869
|
+
var kept = (name, attr) => attr && !IGNORED.includes(name) && attr.type !== "password" && !(attr.type === "relation" && (morph(attr) || attr.visible === false));
|
|
870
|
+
var empty = (value) => value === null || value === void 0 || value === "";
|
|
871
|
+
function snapshotPopulate(uid, schemaOf, stack = []) {
|
|
872
|
+
const schema = schemaOf(uid);
|
|
873
|
+
if (!schema || stack.includes(uid)) return {};
|
|
874
|
+
const scalars = (component) => Object.entries(schemaOf(component)?.attributes || {}).filter(([name, attr]) => kept(name, attr) && !["relation", "media", "component", "dynamiczone"].includes(attr.type)).map(([name]) => name);
|
|
875
|
+
const nested = (component) => ({ select: scalars(component).length ? scalars(component) : ["id"], populate: snapshotPopulate(component, schemaOf, [...stack, uid]) });
|
|
876
|
+
const out = {};
|
|
877
|
+
for (const [name, attr] of Object.entries(schema.attributes || {})) {
|
|
878
|
+
if (!kept(name, attr)) continue;
|
|
879
|
+
if (attr.type === "relation") out[name] = { select: ["documentId", "locale"] };
|
|
880
|
+
else if (attr.type === "media") out[name] = { select: ["id"] };
|
|
881
|
+
else if (attr.type === "component") out[name] = nested(attr.component);
|
|
882
|
+
else if (attr.type === "dynamiczone") out[name] = { on: Object.fromEntries((attr.components || []).map((component) => [component, nested(component)])) };
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
function canonical(data, schema, schemaOf, { relations = true } = {}, depth = 0) {
|
|
887
|
+
const out = {};
|
|
888
|
+
if (!data || typeof data !== "object" || depth > 20) return out;
|
|
889
|
+
const idOf = (file) => file && typeof file === "object" ? file.id : file;
|
|
890
|
+
const ref = (item) => item && typeof item === "object" && item.documentId ? { documentId: item.documentId, ...item.locale && { locale: item.locale } } : null;
|
|
891
|
+
for (const [name, attr] of Object.entries(schema?.attributes || {})) {
|
|
892
|
+
const value = data[name];
|
|
893
|
+
if (!kept(name, attr) || empty(value)) continue;
|
|
894
|
+
if (attr.type === "relation") {
|
|
895
|
+
if (!relations) continue;
|
|
896
|
+
const refs = (Array.isArray(value) ? value : [value]).map(ref).filter(Boolean);
|
|
897
|
+
if (refs.length) out[name] = Array.isArray(value) ? refs : refs[0];
|
|
898
|
+
} else if (attr.type === "media") {
|
|
899
|
+
const ids = (Array.isArray(value) ? value : [value]).map(idOf).filter((id) => !empty(id));
|
|
900
|
+
if (ids.length) out[name] = attr.multiple ? ids : ids[0];
|
|
901
|
+
} else if (attr.type === "component") {
|
|
902
|
+
const child = schemaOf(attr.component);
|
|
903
|
+
if (attr.repeatable) {
|
|
904
|
+
if (Array.isArray(value) && value.length) out[name] = value.map((item) => canonical(item, child, schemaOf, { relations }, depth + 1));
|
|
905
|
+
} else out[name] = canonical(value, child, schemaOf, { relations }, depth + 1);
|
|
906
|
+
} else if (attr.type === "dynamiczone") {
|
|
907
|
+
if (Array.isArray(value) && value.length) out[name] = value.filter((row) => row && typeof row.__component === "string").map((row) => ({ __component: row.__component, ...canonical(row, schemaOf(row.__component), schemaOf, { relations }, depth + 1) }));
|
|
908
|
+
} else out[name] = value;
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
var stable = (value) => JSON.stringify(value, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
|
|
913
|
+
var same = (a, b) => stable(a ?? null) === stable(b ?? null);
|
|
914
|
+
function diffRows(before = [], after = []) {
|
|
915
|
+
const a = before.map(stable), b = after.map(stable);
|
|
916
|
+
const n = a.length, m = b.length;
|
|
917
|
+
if (n * m > 25e4) return after.map((row, to) => ({ op: a[to] === b[to] ? "same" : "changed", from: to < n ? to : void 0, to, uid: row.__component }));
|
|
918
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
919
|
+
for (let i2 = n - 1; i2 >= 0; i2--) for (let j2 = m - 1; j2 >= 0; j2--) lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
920
|
+
const out = [];
|
|
921
|
+
const gap = (fromRows2, toRows2) => {
|
|
922
|
+
const removed = [...fromRows2];
|
|
923
|
+
for (const to of toRows2) {
|
|
924
|
+
const k = removed.findIndex((from) => before[from].__component === after[to].__component);
|
|
925
|
+
if (k === -1) out.push({ op: "added", to, uid: after[to].__component });
|
|
926
|
+
else out.push({ op: "changed", from: removed.splice(k, 1)[0], to, uid: after[to].__component });
|
|
927
|
+
}
|
|
928
|
+
for (const from of removed) out.push({ op: "removed", from, uid: before[from].__component });
|
|
929
|
+
};
|
|
930
|
+
let i = 0, j = 0, fromRows = [], toRows = [];
|
|
931
|
+
while (i < n || j < m) {
|
|
932
|
+
if (i < n && j < m && a[i] === b[j]) {
|
|
933
|
+
gap(fromRows, toRows);
|
|
934
|
+
fromRows = [];
|
|
935
|
+
toRows = [];
|
|
936
|
+
out.push({ op: "same", from: i, to: j, uid: after[j].__component });
|
|
937
|
+
i++;
|
|
938
|
+
j++;
|
|
939
|
+
} else if (j < m && (i >= n || lcs[i][j + 1] >= lcs[i + 1][j])) toRows.push(j++);
|
|
940
|
+
else fromRows.push(i++);
|
|
941
|
+
}
|
|
942
|
+
gap(fromRows, toRows);
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function summarize(before, after, schema) {
|
|
946
|
+
const zones = Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
947
|
+
const blocks = { total: zones.reduce((n, name) => n + (Array.isArray(after?.[name]) ? after[name].length : 0), 0), added: 0, removed: 0, changed: 0 };
|
|
948
|
+
if (!before) return { initial: true, fields: [], blocks: { ...blocks, added: blocks.total } };
|
|
949
|
+
const names = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after || {})])];
|
|
950
|
+
const fields = names.filter((name) => !zones.includes(name) && !same(before[name], after?.[name])).sort();
|
|
951
|
+
for (const name of zones) for (const { op } of diffRows(before[name], after?.[name])) if (op !== "same") blocks[op]++;
|
|
952
|
+
return { fields, blocks };
|
|
953
|
+
}
|
|
954
|
+
module2.exports = { IGNORED, kept, snapshotPopulate, canonical, stable, same, diffRows, summarize };
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// server/trash.js
|
|
959
|
+
var require_trash = __commonJS({
|
|
960
|
+
"server/trash.js"(exports2, module2) {
|
|
961
|
+
"use strict";
|
|
962
|
+
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
963
|
+
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
964
|
+
var { kept, canonical, stable } = require_diff();
|
|
965
|
+
var history = () => require_history();
|
|
966
|
+
var TRASH_UID = `plugin::${PLUGIN2}.trash`;
|
|
967
|
+
var STATUSES = ["trashed", "restored"];
|
|
968
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
969
|
+
var BATCH = 500;
|
|
970
|
+
var restoring = new AsyncLocalStorage();
|
|
971
|
+
var trashContentType2 = { schema: {
|
|
972
|
+
kind: "collectionType",
|
|
973
|
+
collectionName: "blockscene_trash",
|
|
974
|
+
info: { singularName: "trash", pluralName: "trashes", displayName: "Blockscene trash" },
|
|
975
|
+
options: { draftAndPublish: false },
|
|
976
|
+
pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
|
|
977
|
+
attributes: {
|
|
978
|
+
contentType: { type: "string", required: true },
|
|
979
|
+
relatedDocumentId: { type: "string", required: true },
|
|
980
|
+
// The document's main field (Content Manager setting) at delete time, for the list and the search.
|
|
981
|
+
title: { type: "string" },
|
|
982
|
+
// The locales the delete removed ([null] for a type without i18n).
|
|
983
|
+
locales: { type: "json" },
|
|
984
|
+
// [{ locale, status: 'draft' | 'published' | null (no Draft & Publish), snapshot }]: canonical snapshots (server/diff.js).
|
|
985
|
+
entries: { type: "json" },
|
|
986
|
+
// Links other documents' and blocks' unidirectional relations had to the deleted rows (see `incomingRows`).
|
|
987
|
+
incoming: { type: "json" },
|
|
988
|
+
actor: { type: "string", required: true },
|
|
989
|
+
actorName: { type: "string" },
|
|
990
|
+
deletedAt: { type: "datetime", required: true },
|
|
991
|
+
// deletedAt + trash retention at delete time; the nightly purge deletes the row after it.
|
|
992
|
+
expiresAt: { type: "datetime", required: true },
|
|
993
|
+
status: { type: "enumeration", enum: STATUSES, required: true, default: "trashed" },
|
|
994
|
+
restoredAs: { type: "string" },
|
|
995
|
+
restoredAt: { type: "datetime" },
|
|
996
|
+
size: { type: "integer" }
|
|
997
|
+
},
|
|
998
|
+
indexes: [{ name: "blockscene_trash_list", columns: ["status", "content_type", "deleted_at"] }, { name: "blockscene_trash_expiry", columns: ["expires_at"] }]
|
|
999
|
+
} };
|
|
1000
|
+
var expiresAt = (deletedAt, days) => new Date(new Date(deletedAt).getTime() + days * DAY);
|
|
1001
|
+
var isObject = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
1002
|
+
var blank = (value) => value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length;
|
|
1003
|
+
var toMany = (attr) => /ToMany$|^manyWay$/.test(String(attr?.relation || ""));
|
|
1004
|
+
var toOne = (attr) => ["oneToOne", "manyToOne"].includes(attr?.relation);
|
|
1005
|
+
var uniqueFields = (schema) => Object.entries(schema?.attributes || {}).filter(([name, attr]) => kept(name, attr) && (attr.unique === true || attr.type === "uid")).map(([name]) => name);
|
|
1006
|
+
function chooseEntries(entries) {
|
|
1007
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
1009
|
+
const locale = entry?.locale ?? null, current = chosen.get(locale);
|
|
1010
|
+
if (!current || current.status === "published" && entry.status !== "published") chosen.set(locale, entry);
|
|
1011
|
+
}
|
|
1012
|
+
return chosen;
|
|
1013
|
+
}
|
|
1014
|
+
function restorePayload(snapshot, schema, schemaOf, { media = /* @__PURE__ */ new Set(), relation = () => false } = {}) {
|
|
1015
|
+
const report = { removed: [], media: 0, relations: 0, blocks: 0 };
|
|
1016
|
+
const ref = (item) => ({ documentId: item.documentId, ...item.locale && { locale: item.locale } });
|
|
1017
|
+
const build = (data, schema2, top, depth) => {
|
|
1018
|
+
const out = {};
|
|
1019
|
+
if (!isObject(data) || depth > 20) return out;
|
|
1020
|
+
for (const [name, value] of Object.entries(data)) {
|
|
1021
|
+
if (name === "__component") continue;
|
|
1022
|
+
const attr = schema2?.attributes?.[name];
|
|
1023
|
+
const drop = () => {
|
|
1024
|
+
if (top) report.removed.push(name);
|
|
1025
|
+
};
|
|
1026
|
+
if (!kept(name, attr)) {
|
|
1027
|
+
drop();
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const list = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
1031
|
+
if (attr.type === "relation") {
|
|
1032
|
+
const refs = list.filter((item) => isObject(item) && typeof item.documentId === "string");
|
|
1033
|
+
const found = refs.filter((item) => relation(attr.target, item));
|
|
1034
|
+
report.relations += refs.length - found.length;
|
|
1035
|
+
out[name] = toMany(attr) ? found.map(ref) : found[0] ? ref(found[0]) : null;
|
|
1036
|
+
} else if (attr.type === "media") {
|
|
1037
|
+
const ids = list.filter(Number.isInteger), found = ids.filter((id) => media.has(id));
|
|
1038
|
+
report.media += ids.length - found.length;
|
|
1039
|
+
out[name] = attr.multiple ? found : found[0] ?? null;
|
|
1040
|
+
} else if (attr.type === "component") {
|
|
1041
|
+
const child = schemaOf(attr.component);
|
|
1042
|
+
if (!child) {
|
|
1043
|
+
drop();
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
out[name] = attr.repeatable ? list.filter(isObject).map((item) => build(item, child, false, depth + 1)) : isObject(value) ? build(value, child, false, depth + 1) : null;
|
|
1047
|
+
} else if (attr.type === "dynamiczone") {
|
|
1048
|
+
const rows = list.filter((row) => isObject(row) && typeof row.__component === "string");
|
|
1049
|
+
const allowed = rows.filter((row) => (attr.components || []).includes(row.__component) && schemaOf(row.__component));
|
|
1050
|
+
report.blocks += rows.length - allowed.length;
|
|
1051
|
+
out[name] = allowed.map((row) => ({ __component: row.__component, ...build(row, schemaOf(row.__component), false, depth + 1) }));
|
|
1052
|
+
} else if (attr.type === "enumeration" && !(attr.enum || []).includes(value)) drop();
|
|
1053
|
+
else out[name] = value;
|
|
1054
|
+
}
|
|
1055
|
+
return out;
|
|
1056
|
+
};
|
|
1057
|
+
return { data: build(snapshot, schema, true, 0), report };
|
|
1058
|
+
}
|
|
1059
|
+
function planRestore(entries, { schema, schemaOf, locales = null, defaultLocale = null, existing = null, singleTaken = false, taken = () => false, media, relation }) {
|
|
1060
|
+
const blocking = [], warnings = [], steps = [];
|
|
1061
|
+
const published = (Array.isArray(entries) ? entries : []).filter((entry) => entry?.status === "published").map((entry) => entry.locale ?? null);
|
|
1062
|
+
if (published.length) warnings.push({ code: "published", locales: published });
|
|
1063
|
+
if (singleTaken && !existing) blocking.push({ code: "singleExists" });
|
|
1064
|
+
for (const [locale, entry] of chooseEntries(entries)) {
|
|
1065
|
+
if (locales && !locales.includes(locale)) {
|
|
1066
|
+
warnings.push({ code: "localeRemoved", locale });
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (existing?.includes(locale)) {
|
|
1070
|
+
blocking.push({ code: "localeExists", locale });
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const { data, report } = restorePayload(entry.snapshot, schema, schemaOf, { media, relation });
|
|
1074
|
+
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) });
|
|
1075
|
+
const required = Object.entries(schema?.attributes || {}).filter(([name, attr]) => attr?.required && kept(name, attr) && blank(data[name])).map(([name]) => name);
|
|
1076
|
+
if (required.length) warnings.push({ code: "required", locale, fields: required });
|
|
1077
|
+
if (report.removed.length) warnings.push({ code: "fieldsRemoved", locale, fields: report.removed });
|
|
1078
|
+
for (const code of ["media", "relations", "blocks"]) if (report[code]) warnings.push({ code, locale, count: report[code] });
|
|
1079
|
+
steps.push({ locale, data });
|
|
1080
|
+
}
|
|
1081
|
+
if (!steps.length && !blocking.length) blocking.push({ code: "nothing" });
|
|
1082
|
+
steps.sort((a, b) => (b.locale === defaultLocale) - (a.locale === defaultLocale));
|
|
1083
|
+
return { blocking, warnings, steps };
|
|
1084
|
+
}
|
|
1085
|
+
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) })));
|
|
1086
|
+
function incomingRows(source, links, deleted, owners = /* @__PURE__ */ new Map()) {
|
|
1087
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = source.joinTable;
|
|
1088
|
+
return links.filter((link) => deleted.has(link[inverseJoinColumn.name]) && !(source.kind === "contentType" && source.owner === source.target && deleted.has(link[joinColumn.name]))).map((link) => {
|
|
1089
|
+
const target = deleted.get(link[inverseJoinColumn.name]), owner = owners.get(link[joinColumn.name]);
|
|
1090
|
+
return {
|
|
1091
|
+
owner: source.owner,
|
|
1092
|
+
kind: source.kind,
|
|
1093
|
+
field: source.field,
|
|
1094
|
+
ownerId: link[joinColumn.name],
|
|
1095
|
+
...source.kind === "contentType" && { ownerDocumentId: owner?.documentId ?? null, ownerLocale: owner?.locale ?? null },
|
|
1096
|
+
locale: target.locale ?? null,
|
|
1097
|
+
published: Boolean(target.published),
|
|
1098
|
+
order: orderColumnName ? link[orderColumnName] ?? null : null
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
function previewOf(snapshot, schema, schemaOf) {
|
|
1103
|
+
const fields = [], blocks = [];
|
|
1104
|
+
const text = (value) => {
|
|
1105
|
+
const s = typeof value === "string" ? value : stable(value);
|
|
1106
|
+
return s.length > 200 ? `${s.slice(0, 199)}\u2026` : s;
|
|
1107
|
+
};
|
|
1108
|
+
for (const [name, value] of Object.entries(isObject(snapshot) ? snapshot : {})) {
|
|
1109
|
+
const attr = schema?.attributes?.[name];
|
|
1110
|
+
if (attr?.type === "dynamiczone") {
|
|
1111
|
+
for (const row of Array.isArray(value) ? value : []) blocks.push({ zone: name, uid: row?.__component, name: schemaOf(row?.__component)?.info?.displayName || row?.__component });
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const count = Array.isArray(value) ? value.length : 1;
|
|
1115
|
+
fields.push({ name, type: attr?.type || "removed", ...["relation", "media", "component"].includes(attr?.type) ? { count } : { value: text(value) } });
|
|
1116
|
+
}
|
|
1117
|
+
return { fields, blocks };
|
|
1118
|
+
}
|
|
1119
|
+
function trashService2({ strapi }) {
|
|
1120
|
+
const events = () => strapi.plugin(PLUGIN2).service("history");
|
|
1121
|
+
const rows = () => strapi.db.query(TRASH_UID);
|
|
1122
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
1123
|
+
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
1124
|
+
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
1125
|
+
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);
|
|
1126
|
+
const table = (trx, name) => strapi.db.getConnection(name).transacting(trx);
|
|
1127
|
+
const lookups = async (uid, snapshots) => {
|
|
1128
|
+
const ids = /* @__PURE__ */ new Set(), refs = [];
|
|
1129
|
+
for (const snapshot of snapshots) {
|
|
1130
|
+
const found = history().refsOf(snapshot, schemaOf(uid), schemaOf);
|
|
1131
|
+
found.media.forEach((id) => ids.add(id));
|
|
1132
|
+
refs.push(...found.relations);
|
|
1133
|
+
}
|
|
1134
|
+
const files = ids.size ? await strapi.db.query("plugin::upload.file").findMany({ select: ["id"], where: { id: { $in: [...ids] } } }) : [];
|
|
1135
|
+
const present = /* @__PURE__ */ new Set();
|
|
1136
|
+
for (const target of [...new Set(refs.map((ref) => ref.target))]) {
|
|
1137
|
+
if (!strapi.contentTypes?.[target]) continue;
|
|
1138
|
+
const found = await strapi.db.query(target).findMany({ select: ["documentId", "locale"], where: {
|
|
1139
|
+
documentId: { $in: [...new Set(refs.filter((ref) => ref.target === target).map((ref) => ref.documentId))] },
|
|
1140
|
+
...draftAndPublish(target) && { publishedAt: { $null: true } }
|
|
1141
|
+
} });
|
|
1142
|
+
for (const row of found) {
|
|
1143
|
+
present.add(`${target}|${row.documentId}|${row.locale || ""}`);
|
|
1144
|
+
present.add(`${target}|${row.documentId}|*`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return { media: new Set(files.map((file) => file.id)), relation: (target, ref) => present.has(`${target}|${ref.documentId}|${localized(target) && ref.locale ? ref.locale : "*"}`) };
|
|
1148
|
+
};
|
|
1149
|
+
const resolveIncoming = async (uid, incoming, locales, trx) => {
|
|
1150
|
+
const out = { links: [], gone: 0, published: 0, replaced: 0 };
|
|
1151
|
+
for (const link of Array.isArray(incoming) ? incoming : []) {
|
|
1152
|
+
if (!locales.includes(link.locale)) continue;
|
|
1153
|
+
if (link.published) {
|
|
1154
|
+
out.published++;
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const attr = strapi.db.metadata.get(link.owner)?.attributes?.[link.field];
|
|
1158
|
+
if (!attr?.joinTable || attr.target !== uid) {
|
|
1159
|
+
out.gone++;
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
let ownerId = null;
|
|
1163
|
+
if (link.kind === "component") ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: { id: link.ownerId } }))?.id ?? null;
|
|
1164
|
+
else if (strapi.contentTypes?.[link.owner] && link.ownerDocumentId) ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: {
|
|
1165
|
+
documentId: link.ownerDocumentId,
|
|
1166
|
+
...localized(link.owner) && { locale: link.ownerLocale },
|
|
1167
|
+
...draftAndPublish(link.owner) && { publishedAt: { $null: true } }
|
|
1168
|
+
} }))?.id ?? null;
|
|
1169
|
+
if (!ownerId) {
|
|
1170
|
+
out.gone++;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const { joinTable } = attr;
|
|
1174
|
+
if (toOne(attr) && await table(trx, joinTable.name).where(joinTable.joinColumn.name, ownerId).first()) {
|
|
1175
|
+
out.replaced++;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
out.links.push({ ...link, ownerRowId: ownerId, joinTable });
|
|
1179
|
+
}
|
|
1180
|
+
return out;
|
|
1181
|
+
};
|
|
1182
|
+
const self = {
|
|
1183
|
+
TRASH_UID,
|
|
1184
|
+
// Before the delete, in its transaction: the links other documents and blocks have to the rows about to be deleted.
|
|
1185
|
+
async incoming(uid, deleted) {
|
|
1186
|
+
if (!deleted.size) return [];
|
|
1187
|
+
return strapi.db.transaction(async ({ trx }) => {
|
|
1188
|
+
const out = [];
|
|
1189
|
+
for (const source of incomingSources(uid, models())) {
|
|
1190
|
+
const links = await table(trx, source.joinTable.name).select("*").whereIn(source.joinTable.inverseJoinColumn.name, [...deleted.keys()]);
|
|
1191
|
+
if (!links.length) continue;
|
|
1192
|
+
const owners = source.kind === "contentType" && strapi.contentTypes?.[source.owner] ? new Map((await strapi.db.query(source.owner).findMany({
|
|
1193
|
+
select: ["id", "documentId", "locale"],
|
|
1194
|
+
where: { id: { $in: [...new Set(links.map((link) => link[source.joinTable.joinColumn.name]))] } }
|
|
1195
|
+
})).map((row) => [row.id, row])) : /* @__PURE__ */ new Map();
|
|
1196
|
+
out.push(...incomingRows({ ...source, target: uid }, links, deleted, owners));
|
|
1197
|
+
}
|
|
1198
|
+
return out;
|
|
1199
|
+
});
|
|
1200
|
+
},
|
|
1201
|
+
// After the delete, in its transaction: the trash row. drafts/published: the stored rows read before the delete.
|
|
1202
|
+
async record({ uid, documentId, locales, drafts, published, incoming, actor, days }) {
|
|
1203
|
+
const dp = draftAndPublish(uid);
|
|
1204
|
+
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) }));
|
|
1205
|
+
const main = await events().mainField(uid);
|
|
1206
|
+
const first = chooseEntries(entries);
|
|
1207
|
+
const source = first.get(await events().scope(uid).then((list) => list?.[0] ?? null).catch(() => null)) || [...first.values()][0];
|
|
1208
|
+
const title = main && source?.snapshot?.[main] != null ? String(source.snapshot[main]).slice(0, 255) : null;
|
|
1209
|
+
const deletedAt = /* @__PURE__ */ new Date();
|
|
1210
|
+
return rows().create({ data: {
|
|
1211
|
+
contentType: uid,
|
|
1212
|
+
relatedDocumentId: documentId,
|
|
1213
|
+
title,
|
|
1214
|
+
locales: locales || [...new Set(entries.map((entry) => entry.locale))],
|
|
1215
|
+
entries,
|
|
1216
|
+
incoming,
|
|
1217
|
+
...actor,
|
|
1218
|
+
deletedAt,
|
|
1219
|
+
expiresAt: expiresAt(deletedAt, days),
|
|
1220
|
+
status: "trashed",
|
|
1221
|
+
size: Buffer.byteLength(stable({ entries, incoming }))
|
|
1222
|
+
} });
|
|
1223
|
+
},
|
|
1224
|
+
// One page of trash rows, without their snapshots. `types`: the content types the user may read.
|
|
1225
|
+
async list({ contentType, status = "trashed", page = 1, pageSize = 20, types }) {
|
|
1226
|
+
const where = { status, contentType: contentType ? contentType : { $in: types } };
|
|
1227
|
+
if (contentType && !types.includes(contentType)) return { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 } };
|
|
1228
|
+
const [results, total] = await Promise.all([
|
|
1229
|
+
rows().findMany({
|
|
1230
|
+
select: ["id", "contentType", "relatedDocumentId", "title", "locales", "actor", "actorName", "deletedAt", "expiresAt", "status", "restoredAs", "restoredAt"],
|
|
1231
|
+
where,
|
|
1232
|
+
orderBy: [{ deletedAt: "desc" }, { id: "desc" }],
|
|
1233
|
+
offset: (page - 1) * pageSize,
|
|
1234
|
+
limit: pageSize
|
|
1235
|
+
}),
|
|
1236
|
+
rows().count({ where })
|
|
1237
|
+
]);
|
|
1238
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) } };
|
|
1239
|
+
},
|
|
1240
|
+
async find(id) {
|
|
1241
|
+
return rows().findOne({ where: { id } });
|
|
1242
|
+
},
|
|
1243
|
+
// One row for the preview: fields and blocks of each entry, and how many links it held.
|
|
1244
|
+
preview(row) {
|
|
1245
|
+
const schema = schemaOf(row.contentType);
|
|
1246
|
+
const { entries, incoming, ...meta } = row;
|
|
1247
|
+
return {
|
|
1248
|
+
...meta,
|
|
1249
|
+
incoming: (incoming || []).length,
|
|
1250
|
+
entries: (entries || []).map((entry) => ({ locale: entry.locale ?? null, status: entry.status ?? null, ...previewOf(entry.snapshot, schema, schemaOf) }))
|
|
1251
|
+
};
|
|
1252
|
+
},
|
|
1253
|
+
// The pre-check: every lookup a restore needs, then planRestore. Read-only; the restore runs it again in its transaction.
|
|
1254
|
+
async plan(row) {
|
|
1255
|
+
const uid = row.contentType, schema = schemaOf(uid);
|
|
1256
|
+
if (!schema || !strapi.contentTypes?.[uid]) return { blocking: [{ code: "typeGone" }], warnings: [], steps: [], incoming: { links: [], gone: 0, published: 0, replaced: 0 } };
|
|
1257
|
+
const i18n = localized(uid);
|
|
1258
|
+
const locales = i18n ? (await strapi.plugin("i18n").service("locales").find()).map((locale) => locale.code) : null;
|
|
1259
|
+
const defaultLocale = i18n ? await strapi.plugin("i18n").service("locales").getDefaultLocale() : null;
|
|
1260
|
+
const current = await strapi.db.query(uid).findMany({ select: ["locale"], where: { documentId: row.relatedDocumentId } });
|
|
1261
|
+
const existing = current.length ? [...new Set(current.map((entry) => i18n ? entry.locale : null))] : null;
|
|
1262
|
+
const singleTaken = schema.kind === "singleType" && !existing && Boolean(await strapi.db.query(uid).findOne({ select: ["id"] }));
|
|
1263
|
+
const chosen = [...chooseEntries(row.entries).values()];
|
|
1264
|
+
const { media, relation } = await lookups(uid, chosen.map((entry) => entry.snapshot));
|
|
1265
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1266
|
+
for (const entry of chosen) for (const field of uniqueFields(schema)) {
|
|
1267
|
+
const value = entry.snapshot?.[field];
|
|
1268
|
+
if (blank(value) || typeof value === "object") continue;
|
|
1269
|
+
const other = await strapi.db.query(uid).findOne({ select: ["id"], where: { [field]: value, ...i18n && { locale: entry.locale }, ...existing && { documentId: { $ne: row.relatedDocumentId } } } });
|
|
1270
|
+
if (other) taken.add(stable([field, entry.locale ?? null, value]));
|
|
1271
|
+
}
|
|
1272
|
+
const plan = planRestore(row.entries, { schema, schemaOf, locales, defaultLocale, existing, singleTaken, media, relation, taken: (field, locale, value) => taken.has(stable([field, locale ?? null, value])) });
|
|
1273
|
+
const incoming = await strapi.db.transaction(({ trx }) => resolveIncoming(uid, row.incoming, plan.steps.map((step) => step.locale), trx));
|
|
1274
|
+
return { ...plan, documentId: existing ? row.relatedDocumentId : null, incoming };
|
|
1275
|
+
},
|
|
1276
|
+
// What the pre-check endpoint returns: the plan without the data it would write.
|
|
1277
|
+
async check(row) {
|
|
1278
|
+
if (row.status !== "trashed") return { status: row.status, restoredAs: row.restoredAs, blocking: [{ code: "restored" }], warnings: [] };
|
|
1279
|
+
const { steps, incoming: { links, ...incoming }, ...plan } = await self.plan(row);
|
|
1280
|
+
return { ...plan, locales: steps.map((step) => step.locale), incoming: { ...incoming, reconnect: links.length } };
|
|
1281
|
+
},
|
|
1282
|
+
// Restore as a draft, in one transaction: claim the row (a second restore of the same row, concurrent or later, gets
|
|
1283
|
+
// `already` with the first one's document), check again, recreate (default locale first, the others under the same
|
|
1284
|
+
// documentId), put the links back, record `restore` events. A blocking problem throws `conflict` and nothing is written.
|
|
1285
|
+
async restore(id, { actor, userId }) {
|
|
1286
|
+
return restoring.run(true, () => strapi.db.transaction(async ({ trx }) => {
|
|
1287
|
+
const claimed = await rows().updateMany({ where: { id, status: "trashed" }, data: { status: "restored", restoredAt: /* @__PURE__ */ new Date() } });
|
|
1288
|
+
const row = await rows().findOne({ where: { id } });
|
|
1289
|
+
if (!row) return null;
|
|
1290
|
+
if (!claimed?.count) return { already: true, documentId: row.restoredAs || null };
|
|
1291
|
+
const plan = await self.plan(row);
|
|
1292
|
+
if (plan.blocking.length) {
|
|
1293
|
+
const error = new Error("conflict");
|
|
1294
|
+
error.conflict = { blocking: plan.blocking, warnings: plan.warnings };
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
const uid = row.contentType, documents = strapi.documents(uid);
|
|
1298
|
+
const creator = userId ? { createdBy: userId, updatedBy: userId } : {};
|
|
1299
|
+
let documentId = plan.documentId;
|
|
1300
|
+
for (const { locale, data } of plan.steps) {
|
|
1301
|
+
const params = { ...locale && { locale }, data: { ...data, ...creator } };
|
|
1302
|
+
if (documentId) await documents.update({ documentId, ...params });
|
|
1303
|
+
else documentId = (await documents.create(params)).documentId;
|
|
1304
|
+
}
|
|
1305
|
+
const restored = await events().rows(uid, documentId, localized(uid) ? plan.steps.map((step) => step.locale) : null);
|
|
1306
|
+
const rowOf = new Map(restored.map((entry) => [entry.locale ?? null, entry]));
|
|
1307
|
+
for (const link of plan.incoming.links) {
|
|
1308
|
+
const target = rowOf.get(link.locale);
|
|
1309
|
+
if (!target) continue;
|
|
1310
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = link.joinTable;
|
|
1311
|
+
await table(trx, link.joinTable.name).insert({ [joinColumn.name]: link.ownerRowId, [inverseJoinColumn.name]: target.id, ...orderColumnName && link.order != null && { [orderColumnName]: link.order } });
|
|
1312
|
+
}
|
|
1313
|
+
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 } });
|
|
1314
|
+
await rows().update({ where: { id }, data: { restoredAs: documentId } });
|
|
1315
|
+
const { links, ...incoming } = plan.incoming;
|
|
1316
|
+
return { documentId, locales: plan.steps.map((step) => step.locale), warnings: plan.warnings, incoming: { ...incoming, reconnected: links.length } };
|
|
1317
|
+
}));
|
|
1318
|
+
},
|
|
1319
|
+
// Delete forever (one row), recorded as a `purge` event. Never touches media files.
|
|
1320
|
+
async remove(row, actor) {
|
|
1321
|
+
return strapi.db.transaction(async () => {
|
|
1322
|
+
const { count } = await rows().deleteMany({ where: { id: row.id } });
|
|
1323
|
+
if (count) await events().record({ uid: row.contentType, documentId: row.relatedDocumentId, action: "purge", actor, extra: { trash: row.id, title: row.title } });
|
|
1324
|
+
return count;
|
|
1325
|
+
});
|
|
1326
|
+
},
|
|
1327
|
+
// Nightly, in batches, idempotent: rows past expiresAt are deleted (restored ones too). Never touches media files.
|
|
1328
|
+
async purge(now = Date.now()) {
|
|
1329
|
+
let count = 0;
|
|
1330
|
+
for (; ; ) {
|
|
1331
|
+
const ids = (await rows().findMany({ select: ["id"], where: { expiresAt: { $lt: new Date(now) } }, orderBy: { id: "asc" }, limit: BATCH })).map((row) => row.id);
|
|
1332
|
+
if (ids.length) await rows().deleteMany({ where: { id: { $in: ids } } });
|
|
1333
|
+
count += ids.length;
|
|
1334
|
+
if (ids.length < BATCH) return count;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
return self;
|
|
1339
|
+
}
|
|
1340
|
+
var ID = /^[1-9]\d{0,15}$/;
|
|
1341
|
+
var API_UID = /^api::[\w-]+\.[\w-]+$/;
|
|
1342
|
+
var int = (value, fallback, max) => {
|
|
1343
|
+
if (value === void 0 || value === "") return fallback;
|
|
1344
|
+
const n = Number(value);
|
|
1345
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1346
|
+
};
|
|
1347
|
+
var trashController2 = ({ strapi }) => {
|
|
1348
|
+
const service = () => strapi.plugin(PLUGIN2).service("trash");
|
|
1349
|
+
const { can, readableTypes, actorOf } = history();
|
|
1350
|
+
const load = async (ctx, action = "read") => {
|
|
1351
|
+
if (!ID.test(String(ctx.params?.id || ""))) {
|
|
1352
|
+
ctx.badRequest("Invalid trash entry");
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
const row = await service().find(Number(ctx.params.id));
|
|
1356
|
+
if (!row || !can(strapi, ctx, row.contentType, "read") || action !== "read" && !can(strapi, ctx, row.contentType, action)) {
|
|
1357
|
+
ctx.notFound();
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return row;
|
|
1361
|
+
};
|
|
1362
|
+
return {
|
|
1363
|
+
async list(ctx) {
|
|
1364
|
+
const { contentType = "", status = "trashed" } = ctx.query || {};
|
|
1365
|
+
const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 20, 100);
|
|
1366
|
+
if (typeof contentType !== "string" || contentType && !API_UID.test(contentType) || !STATUSES.includes(status) || Number.isNaN(page) || Number.isNaN(pageSize)) return ctx.badRequest("Invalid filters");
|
|
1367
|
+
const types = readableTypes(strapi, ctx);
|
|
1368
|
+
ctx.body = {
|
|
1369
|
+
...await service().list({ contentType, status, page, pageSize, types }),
|
|
1370
|
+
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) }))
|
|
1371
|
+
};
|
|
1372
|
+
},
|
|
1373
|
+
async find(ctx) {
|
|
1374
|
+
const row = await load(ctx);
|
|
1375
|
+
if (row) ctx.body = service().preview(row);
|
|
1376
|
+
},
|
|
1377
|
+
async check(ctx) {
|
|
1378
|
+
const row = await load(ctx, "create");
|
|
1379
|
+
if (row) ctx.body = await service().check(row);
|
|
1380
|
+
},
|
|
1381
|
+
async restore(ctx) {
|
|
1382
|
+
const row = await load(ctx, "create");
|
|
1383
|
+
if (!row) return;
|
|
1384
|
+
try {
|
|
1385
|
+
const result = await service().restore(row.id, { actor: actorOf(strapi), userId: ctx.state?.user?.id });
|
|
1386
|
+
if (!result) return ctx.notFound();
|
|
1387
|
+
ctx.body = result;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
if (!error?.conflict) throw error;
|
|
1390
|
+
ctx.status = 409;
|
|
1391
|
+
ctx.body = { data: null, error: { status: 409, name: "ConflictError", message: "The document cannot be restored as it is: nothing was written.", details: error.conflict } };
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
async remove(ctx) {
|
|
1395
|
+
const row = await load(ctx, "delete");
|
|
1396
|
+
if (!row) return;
|
|
1397
|
+
ctx.body = { deleted: await service().remove(row, actorOf(strapi)) };
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
};
|
|
1401
|
+
module2.exports = { TRASH_UID, STATUSES, restoring, trashContentType: trashContentType2, expiresAt, uniqueFields, chooseEntries, restorePayload, planRestore, incomingSources, incomingRows, previewOf, trashService: trashService2, trashController: trashController2 };
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// server/history.js
|
|
1406
|
+
var require_history = __commonJS({
|
|
1407
|
+
"server/history.js"(exports2, module2) {
|
|
1408
|
+
"use strict";
|
|
1409
|
+
var { createHash } = require("node:crypto");
|
|
1410
|
+
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
1411
|
+
var { snapshotPopulate, canonical, stable, summarize } = require_diff();
|
|
1412
|
+
var { TRASH_UID, restoring } = require_trash();
|
|
1413
|
+
var EVENT_UID = `plugin::${PLUGIN2}.event`;
|
|
1414
|
+
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore", "purge"];
|
|
1415
|
+
var CAPTURED = { create: "create", clone: "create", update: "update", publish: "publish", unpublish: "unpublish", discardDraft: "discard", delete: "delete" };
|
|
1416
|
+
var CRON2 = "blocksceneHistoryPurge";
|
|
1417
|
+
var BATCH = 500;
|
|
1418
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
1419
|
+
var ACTORS_LIMIT = 500;
|
|
1420
|
+
var SEARCH_LIMIT = 200;
|
|
1421
|
+
var eventContentType2 = { schema: {
|
|
1422
|
+
kind: "collectionType",
|
|
1423
|
+
collectionName: "blockscene_events",
|
|
1424
|
+
info: { singularName: "event", pluralName: "events", displayName: "Blockscene event" },
|
|
1425
|
+
options: { draftAndPublish: false },
|
|
1426
|
+
pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
|
|
1427
|
+
attributes: {
|
|
1428
|
+
contentType: { type: "string", required: true },
|
|
1429
|
+
relatedDocumentId: { type: "string", required: true },
|
|
1430
|
+
locale: { type: "string" },
|
|
1431
|
+
action: { type: "enumeration", enum: ACTIONS, required: true },
|
|
1432
|
+
// 'admin:<id>', 'token:<id>', 'user:<id>' (Users & Permissions) or 'system'; the name as it was at that moment.
|
|
1433
|
+
actor: { type: "string", required: true },
|
|
1434
|
+
actorName: { type: "string" },
|
|
1435
|
+
at: { type: "datetime", required: true },
|
|
1436
|
+
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed;
|
|
1437
|
+
// restore: + { restoredFrom, trash }; purge: { trash, title }.
|
|
1438
|
+
summary: { type: "json" },
|
|
1439
|
+
// Null when identical to the previous snapshot of the same locale (same hash) or purged.
|
|
1440
|
+
snapshot: { type: "json" },
|
|
1441
|
+
hash: { type: "string" },
|
|
1442
|
+
size: { type: "integer" }
|
|
1443
|
+
},
|
|
1444
|
+
indexes: [{ name: "blockscene_events_lookup", columns: ["content_type", "related_document_id", "locale", "at"] }]
|
|
1445
|
+
} };
|
|
1446
|
+
var fail2 = (message, Kind = "ApplicationError") => {
|
|
1447
|
+
let E;
|
|
1448
|
+
try {
|
|
1449
|
+
E = require("@strapi/utils").errors[Kind];
|
|
1450
|
+
} catch {
|
|
1451
|
+
E = class extends Error {
|
|
1452
|
+
constructor(m) {
|
|
1453
|
+
super(m);
|
|
1454
|
+
this.name = Kind;
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
throw new E(message);
|
|
1459
|
+
};
|
|
1460
|
+
var hashOf = (text) => createHash("sha256").update(text).digest("hex");
|
|
1461
|
+
var covers2 = (history, uid) => Boolean(history?.enabled) && uid.startsWith("api::") && (history.contentTypes === "all" || Array.isArray(history.contentTypes) && history.contentTypes.includes(uid));
|
|
1462
|
+
var retention = ({ retentionDays, eventDays }, now = Date.now()) => ({ snapshotsBefore: new Date(now - retentionDays * DAY), eventsBefore: new Date(now - eventDays * DAY) });
|
|
1463
|
+
function actorOf(strapi) {
|
|
1464
|
+
let ctx;
|
|
1465
|
+
try {
|
|
1466
|
+
ctx = strapi.requestContext?.get?.();
|
|
1467
|
+
} catch {
|
|
1468
|
+
ctx = null;
|
|
1469
|
+
}
|
|
1470
|
+
const strategy = ctx?.state?.auth?.strategy?.name, user = ctx?.state?.user, token = ctx?.state?.auth?.credentials;
|
|
1471
|
+
const name = (value) => String(value || "").slice(0, 255);
|
|
1472
|
+
if (strategy === "admin" && user?.id) return { actor: `admin:${user.id}`, actorName: name([user.firstname, user.lastname].filter(Boolean).join(" ") || user.username || user.email) };
|
|
1473
|
+
if (strategy === "api-token" && token?.id) return { actor: `token:${token.id}`, actorName: name(token.name) };
|
|
1474
|
+
if (user?.id) return { actor: `user:${user.id}`, actorName: name(user.username || user.email) };
|
|
1475
|
+
return { actor: "system", actorName: "" };
|
|
1476
|
+
}
|
|
1477
|
+
function refsOf(snapshot, schema, schemaOf) {
|
|
1478
|
+
const media = /* @__PURE__ */ new Set(), relations = [];
|
|
1479
|
+
const walk = (data, schema2, depth) => {
|
|
1480
|
+
if (!data || typeof data !== "object" || depth > 20) return;
|
|
1481
|
+
for (const [name, attr] of Object.entries(schema2?.attributes || {})) {
|
|
1482
|
+
const value = data[name];
|
|
1483
|
+
if (value == null) continue;
|
|
1484
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1485
|
+
if (attr.type === "media") list.forEach((id) => Number.isInteger(id) && media.add(id));
|
|
1486
|
+
else if (attr.type === "relation" && attr.target) list.forEach((ref) => ref?.documentId && relations.push({ target: attr.target, ...ref }));
|
|
1487
|
+
else if (attr.type === "component") list.forEach((item) => walk(item, schemaOf(attr.component), depth + 1));
|
|
1488
|
+
else if (attr.type === "dynamiczone") list.forEach((row) => walk(row, schemaOf(row?.__component), depth + 1));
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
walk(snapshot, schema, 0);
|
|
1492
|
+
return { media: [...media], relations };
|
|
1493
|
+
}
|
|
1494
|
+
function historyService2({ strapi }) {
|
|
1495
|
+
const plugin = () => strapi.plugin(PLUGIN2);
|
|
1496
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
1497
|
+
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
1498
|
+
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
1499
|
+
const events = () => strapi.db.query(EVENT_UID);
|
|
1500
|
+
const actors = async () => {
|
|
1501
|
+
const meta = strapi.db.metadata.get(EVENT_UID);
|
|
1502
|
+
const column = (name) => meta.attributes[name].columnName || name;
|
|
1503
|
+
return strapi.db.connection(meta.tableName).select({ actor: column("actor") }).max({ actorName: column("actorName") }).groupBy(column("actor")).orderBy(column("actor")).limit(ACTORS_LIMIT);
|
|
1504
|
+
};
|
|
1505
|
+
const scope = async (uid, locale) => {
|
|
1506
|
+
if (!localized(uid)) return [null];
|
|
1507
|
+
if (locale === "*") return null;
|
|
1508
|
+
if (Array.isArray(locale) && locale.length) return locale;
|
|
1509
|
+
if (typeof locale === "string" && locale) return [locale];
|
|
1510
|
+
return [await strapi.plugin("i18n").service("locales").getDefaultLocale()];
|
|
1511
|
+
};
|
|
1512
|
+
const rows = async (uid, documentId, locales, published = false) => !documentId ? [] : strapi.db.query(uid).findMany({
|
|
1513
|
+
where: {
|
|
1514
|
+
documentId,
|
|
1515
|
+
...locales && localized(uid) && { locale: { $in: locales } },
|
|
1516
|
+
...draftAndPublish(uid) && { publishedAt: published ? { $notNull: true } : { $null: true } }
|
|
1517
|
+
},
|
|
1518
|
+
populate: snapshotPopulate(uid, schemaOf)
|
|
1519
|
+
});
|
|
1520
|
+
const self = {
|
|
1521
|
+
EVENT_UID,
|
|
1522
|
+
async config() {
|
|
1523
|
+
return (await plugin().service("settings").get()).history;
|
|
1524
|
+
},
|
|
1525
|
+
covers: covers2,
|
|
1526
|
+
// One event. The snapshot is stored unless identical to the latest stored one of the same document and locale.
|
|
1527
|
+
async record({ uid, documentId, locale = null, action, entry, actor, missing = false, extra = null }) {
|
|
1528
|
+
let summary = missing ? { missing: true } : null, snapshot = null, hash = null, size = 0;
|
|
1529
|
+
if (entry) {
|
|
1530
|
+
const content = canonical(entry, schemaOf(uid), schemaOf);
|
|
1531
|
+
const text = stable(content);
|
|
1532
|
+
hash = hashOf(text);
|
|
1533
|
+
const previous = await events().findOne({ select: ["hash", "snapshot"], where: { contentType: uid, relatedDocumentId: documentId, locale, size: { $gt: 0 } }, orderBy: [{ at: "desc" }, { id: "desc" }] });
|
|
1534
|
+
summary = summarize(previous?.snapshot || null, content, schemaOf(uid));
|
|
1535
|
+
if (previous?.hash !== hash) {
|
|
1536
|
+
snapshot = content;
|
|
1537
|
+
size = Buffer.byteLength(text);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (extra) summary = { ...summary, ...extra };
|
|
1541
|
+
return events().create({ data: { contentType: uid, relatedDocumentId: documentId, locale, action, ...actor, at: /* @__PURE__ */ new Date(), summary, snapshot, hash, size } });
|
|
1542
|
+
},
|
|
1543
|
+
// After a save or publish: one event per affected entry (drafts, or the published versions after a publish).
|
|
1544
|
+
async captureSave({ uid, action, documentId, locales, published, actor }) {
|
|
1545
|
+
if (!documentId) return;
|
|
1546
|
+
for (const entry of await rows(uid, documentId, locales, published)) await self.record({ uid, documentId, locale: entry.locale ?? null, action, entry, actor });
|
|
1547
|
+
},
|
|
1548
|
+
scope,
|
|
1549
|
+
rows,
|
|
1550
|
+
// The Content Manager's main field of a content type when it is a plain value (what list views show), else null.
|
|
1551
|
+
async mainField(uid) {
|
|
1552
|
+
let name = null;
|
|
1553
|
+
try {
|
|
1554
|
+
name = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[uid]))?.settings?.mainField;
|
|
1555
|
+
} catch {
|
|
1556
|
+
name = null;
|
|
1557
|
+
}
|
|
1558
|
+
return name && name !== "id" && schemaOf(uid)?.attributes?.[name] && !["relation", "media", "component", "dynamiczone", "password", "json", "blocks"].includes(schemaOf(uid).attributes[name].type) ? name : null;
|
|
1559
|
+
},
|
|
1560
|
+
// Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
|
|
1561
|
+
async list({ uid, documentId, locale, actor = "", page = 1, pageSize = 25 }) {
|
|
1562
|
+
const scope2 = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
|
|
1563
|
+
const where = { ...scope2, ...actor && { actor } };
|
|
1564
|
+
const [list, kept, total, people] = await Promise.all([
|
|
1565
|
+
events().findMany({ select: ["id", "action", "actor", "actorName", "at", "summary", "hash", "size"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
|
|
1566
|
+
events().findMany({ select: ["hash"], where: { ...scope2, size: { $gt: 0 } } }),
|
|
1567
|
+
events().count({ where }),
|
|
1568
|
+
events().findMany({ select: ["actor", "actorName"], where: scope2, orderBy: [{ at: "desc" }, { id: "desc" }] })
|
|
1569
|
+
]);
|
|
1570
|
+
const hashes = new Set(kept.map((row) => row.hash));
|
|
1571
|
+
const actors2 = [...new Map(people.slice().reverse().map(({ actor: actor2, actorName }) => [actor2, { actor: actor2, actorName }])).values()];
|
|
1572
|
+
return {
|
|
1573
|
+
results: list.map(({ hash, size, ...event }) => ({ ...event, stored: Boolean(hash && hashes.has(hash)) })),
|
|
1574
|
+
pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) },
|
|
1575
|
+
actors: actors2,
|
|
1576
|
+
locale: locale || null
|
|
1577
|
+
};
|
|
1578
|
+
},
|
|
1579
|
+
// One event with its snapshot (from an identical event when this one was deduplicated) and what loading it needs:
|
|
1580
|
+
// the media files and relation targets that still exist (relation labels from the Content Manager's main field).
|
|
1581
|
+
async find(id) {
|
|
1582
|
+
const event = await events().findOne({ where: { id } });
|
|
1583
|
+
if (!event) return null;
|
|
1584
|
+
const { snapshot: own, ...meta } = event;
|
|
1585
|
+
const snapshot = own || event.hash && (await events().findOne({ select: ["snapshot"], where: { contentType: event.contentType, relatedDocumentId: event.relatedDocumentId, locale: event.locale, hash: event.hash, size: { $gt: 0 } } }))?.snapshot || null;
|
|
1586
|
+
if (!snapshot) return { event: meta, snapshot: null, media: {}, relations: {} };
|
|
1587
|
+
const refs = refsOf(snapshot, schemaOf(event.contentType), schemaOf);
|
|
1588
|
+
const files = refs.media.length ? await strapi.db.query("plugin::upload.file").findMany({ where: { id: { $in: refs.media } } }) : [];
|
|
1589
|
+
const relations = {};
|
|
1590
|
+
for (const target of [...new Set(refs.relations.map((ref) => ref.target))]) {
|
|
1591
|
+
if (!strapi.contentTypes?.[target]) continue;
|
|
1592
|
+
const mainField = await self.mainField(target), scalar = Boolean(mainField);
|
|
1593
|
+
const found = await strapi.db.query(target).findMany({
|
|
1594
|
+
select: ["id", "documentId", "locale", ...scalar ? [mainField] : []],
|
|
1595
|
+
where: { documentId: { $in: [...new Set(refs.relations.filter((ref) => ref.target === target).map((ref) => ref.documentId))] }, ...draftAndPublish(target) && { publishedAt: { $null: true } } }
|
|
1596
|
+
});
|
|
1597
|
+
relations[target] = found.map((row) => ({ id: row.id, documentId: row.documentId, locale: row.locale || null, label: scalar && row[mainField] != null ? String(row[mainField]) : void 0, mainField: scalar ? mainField : void 0 }));
|
|
1598
|
+
}
|
|
1599
|
+
return { event: meta, snapshot, media: Object.fromEntries(files.map((file) => [file.id, file])), relations };
|
|
1600
|
+
},
|
|
1601
|
+
// Content activity: one page of events (no snapshots), newest first, with each document's title and whether it still
|
|
1602
|
+
// exists (the list links to it). `types`: the content types the user may read. `q`: documents whose main field
|
|
1603
|
+
// contains it now, or whose trash entry's title does (at most SEARCH_LIMIT per content type).
|
|
1604
|
+
async activity({ actor, contentType, action, from, to, q, page, pageSize, types }) {
|
|
1605
|
+
const empty = { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 }, actors: [] };
|
|
1606
|
+
if (contentType && !types.includes(contentType)) return empty;
|
|
1607
|
+
const where = {
|
|
1608
|
+
contentType: contentType || { $in: types },
|
|
1609
|
+
...actor && { actor },
|
|
1610
|
+
...action && { action },
|
|
1611
|
+
...(from || to) && { at: { ...from && { $gte: from }, ...to && { $lte: to } } }
|
|
1612
|
+
};
|
|
1613
|
+
if (q) {
|
|
1614
|
+
const matches = [];
|
|
1615
|
+
for (const uid of contentType ? [contentType] : types) {
|
|
1616
|
+
const main = await self.mainField(uid);
|
|
1617
|
+
const found = new Set(main ? (await strapi.db.query(uid).findMany({ select: ["documentId"], where: { [main]: { $containsi: q } }, limit: SEARCH_LIMIT })).map((row) => row.documentId) : []);
|
|
1618
|
+
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);
|
|
1619
|
+
if (found.size) matches.push({ contentType: uid, relatedDocumentId: { $in: [...found] } });
|
|
1620
|
+
}
|
|
1621
|
+
if (!matches.length) return { ...empty, actors: await actors() };
|
|
1622
|
+
where.$or = matches;
|
|
1623
|
+
}
|
|
1624
|
+
const [list, total] = await Promise.all([
|
|
1625
|
+
events().findMany({ select: ["id", "contentType", "relatedDocumentId", "locale", "action", "actor", "actorName", "at", "summary"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
|
|
1626
|
+
events().count({ where })
|
|
1627
|
+
]);
|
|
1628
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1629
|
+
for (const uid of [...new Set(list.map((event) => event.contentType))]) {
|
|
1630
|
+
const ids = [...new Set(list.filter((event) => event.contentType === uid).map((event) => event.relatedDocumentId))];
|
|
1631
|
+
const main = strapi.contentTypes?.[uid] ? await self.mainField(uid) : null;
|
|
1632
|
+
const found = strapi.contentTypes?.[uid] ? await strapi.db.query(uid).findMany({ select: ["documentId", "locale", ...main ? [main] : []], where: { documentId: { $in: ids } } }) : [];
|
|
1633
|
+
for (const row of found) {
|
|
1634
|
+
const key = `${uid}|${row.documentId}`;
|
|
1635
|
+
docs.set(key, [...docs.get(key) || [], { locale: row.locale ?? null, title: main && row[main] != null ? String(row[main]) : null }]);
|
|
1636
|
+
}
|
|
1637
|
+
const gone = ids.filter((id) => !docs.has(`${uid}|${id}`));
|
|
1638
|
+
if (gone.length) {
|
|
1639
|
+
for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId", "title"], where: { contentType: uid, relatedDocumentId: { $in: gone } }, orderBy: { id: "desc" } }))
|
|
1640
|
+
if (!docs.has(`trash|${uid}|${row.relatedDocumentId}`)) docs.set(`trash|${uid}|${row.relatedDocumentId}`, row.title);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const results = list.map((event) => {
|
|
1644
|
+
const rows2 = docs.get(`${event.contentType}|${event.relatedDocumentId}`) || [];
|
|
1645
|
+
const own = rows2.find((row) => row.locale === (event.locale ?? null));
|
|
1646
|
+
if (event.action === "purge") return { ...event, exists: false, title: event.summary?.title ?? null };
|
|
1647
|
+
return { ...event, exists: Boolean(own), title: (own || rows2[0])?.title ?? docs.get(`trash|${event.contentType}|${event.relatedDocumentId}`) ?? null };
|
|
1648
|
+
});
|
|
1649
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }, actors: await actors() };
|
|
1650
|
+
},
|
|
1651
|
+
// Nightly, in batches, idempotent (two instances running it is harmless): events past eventDays are deleted;
|
|
1652
|
+
// snapshots past retentionDays, or beyond maxSnapshots per document (newest kept), are emptied. Never touches media.
|
|
1653
|
+
async purge(now = Date.now()) {
|
|
1654
|
+
const history = await self.config();
|
|
1655
|
+
const { snapshotsBefore, eventsBefore } = retention(history, now);
|
|
1656
|
+
const counts = { events: 0, expired: 0, excess: 0 };
|
|
1657
|
+
const batches = async (key, query, apply) => {
|
|
1658
|
+
for (; ; ) {
|
|
1659
|
+
const ids = (await events().findMany({ select: ["id"], limit: BATCH, ...query })).map((row) => row.id);
|
|
1660
|
+
if (ids.length) await apply({ id: { $in: ids } });
|
|
1661
|
+
counts[key] += ids.length;
|
|
1662
|
+
if (ids.length < BATCH) return;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
const empty = (where) => events().updateMany({ where, data: { snapshot: null, size: 0 } });
|
|
1666
|
+
await batches("events", { where: { at: { $lt: eventsBefore } }, orderBy: { id: "asc" } }, (where) => events().deleteMany({ where }));
|
|
1667
|
+
await batches("expired", { where: { at: { $lt: snapshotsBefore }, size: { $gt: 0 } }, orderBy: { id: "asc" } }, empty);
|
|
1668
|
+
const meta = strapi.db.metadata.get(EVENT_UID);
|
|
1669
|
+
const column = (name) => meta.attributes[name].columnName || name;
|
|
1670
|
+
const crowded = await strapi.db.connection(meta.tableName).select({ uid: column("contentType"), documentId: column("relatedDocumentId") }).where(column("size"), ">", 0).groupBy(column("contentType"), column("relatedDocumentId")).havingRaw("count(*) > ?", [history.maxSnapshots]);
|
|
1671
|
+
for (const { uid, documentId } of crowded) await batches("excess", {
|
|
1672
|
+
where: { contentType: uid, relatedDocumentId: documentId, size: { $gt: 0 } },
|
|
1673
|
+
orderBy: [{ at: "desc" }, { id: "desc" }],
|
|
1674
|
+
offset: history.maxSnapshots
|
|
1675
|
+
}, empty);
|
|
1676
|
+
return counts;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
return self;
|
|
1680
|
+
}
|
|
1681
|
+
function registerHistory2(strapi) {
|
|
1682
|
+
if (!strapi.documents?.use) return;
|
|
1683
|
+
const service = () => strapi.plugin(PLUGIN2).service("history");
|
|
1684
|
+
const warn = (message, error) => strapi.log.error(`[${PLUGIN2}] ${message}: ${error?.message || error}`);
|
|
1685
|
+
strapi.documents.use(async (ctx, next) => {
|
|
1686
|
+
const action = CAPTURED[ctx.action];
|
|
1687
|
+
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true || restoring.getStore()) return next();
|
|
1688
|
+
const history = service();
|
|
1689
|
+
const params = ctx.params || {};
|
|
1690
|
+
if (action === "delete") {
|
|
1691
|
+
const config = params.documentId ? await history.config() : null;
|
|
1692
|
+
if (!config || !history.covers(config, ctx.uid)) return next();
|
|
1693
|
+
return strapi.db.transaction(async () => {
|
|
1694
|
+
const actor2 = actorOf(strapi), trash = strapi.plugin(PLUGIN2).service("trash");
|
|
1695
|
+
const published = strapi.getModel(ctx.uid)?.options?.draftAndPublish === true;
|
|
1696
|
+
let locales2, entries, live, incoming;
|
|
1697
|
+
try {
|
|
1698
|
+
locales2 = await history.scope(ctx.uid, params.locale);
|
|
1699
|
+
entries = await history.rows(ctx.uid, params.documentId, locales2);
|
|
1700
|
+
live = published ? await history.rows(ctx.uid, params.documentId, locales2, true) : [];
|
|
1701
|
+
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 }])]));
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
warn("reading the document before a delete failed", error);
|
|
1704
|
+
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
1705
|
+
}
|
|
1706
|
+
const result2 = await next();
|
|
1707
|
+
try {
|
|
1708
|
+
for (const entry of entries) await history.record({ uid: ctx.uid, documentId: params.documentId, locale: entry.locale ?? null, action, entry, actor: actor2 });
|
|
1709
|
+
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 });
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
warn("recording a delete failed, the delete was rolled back", error);
|
|
1712
|
+
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
1713
|
+
}
|
|
1714
|
+
return result2;
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
let covered = false;
|
|
1718
|
+
try {
|
|
1719
|
+
covered = history.covers(await history.config(), ctx.uid);
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
warn("reading the history settings failed, this change is not recorded", error);
|
|
1722
|
+
}
|
|
1723
|
+
if (!covered) return next();
|
|
1724
|
+
const actor = actorOf(strapi);
|
|
1725
|
+
const result = await next();
|
|
1726
|
+
const documentId = ["create", "update"].includes(action) ? result?.documentId || params.documentId : params.documentId;
|
|
1727
|
+
const locales = ["publish", "unpublish", "discard"].includes(action) ? [...new Set((result?.entries || []).map((entry) => entry?.locale ?? null))] : ctx.action === "clone" ? null : await history.scope(ctx.uid, params.locale).catch(() => [result?.locale ?? null]);
|
|
1728
|
+
const steps = [[action, action === "publish"]];
|
|
1729
|
+
if (["create", "update"].includes(ctx.action) && params.status === "published" && strapi.getModel(ctx.uid)?.options?.draftAndPublish) steps.push(["publish", true]);
|
|
1730
|
+
for (const [step, published] of steps) {
|
|
1731
|
+
if (locales && !locales.length) continue;
|
|
1732
|
+
try {
|
|
1733
|
+
await history.captureSave({ uid: ctx.uid, action: step, documentId, locales, published, actor });
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
warn(`recording "${step}" of ${ctx.uid} ${documentId} failed, the change was saved without a version`, error);
|
|
1736
|
+
try {
|
|
1737
|
+
if (documentId) for (const locale of locales || [null]) await history.record({ uid: ctx.uid, documentId, locale, action: step, actor, missing: true });
|
|
1738
|
+
} catch (again) {
|
|
1739
|
+
warn("marking the missing version failed too", again);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return result;
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
function registerPurge2(strapi) {
|
|
1747
|
+
if (!strapi.documents?.use || !strapi.cron?.add) return;
|
|
1748
|
+
strapi.cron.add({ [CRON2]: { options: "0 3 * * *", task: async () => {
|
|
1749
|
+
try {
|
|
1750
|
+
const counts = await strapi.plugin(PLUGIN2).service("history").purge();
|
|
1751
|
+
if (counts.events || counts.expired || counts.excess) strapi.log.info(`[${PLUGIN2}] history purge: ${counts.events} events deleted, ${counts.expired + counts.excess} snapshots emptied`);
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
strapi.log.error(`[${PLUGIN2}] history purge failed: ${error?.message || error}`);
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
const count = await strapi.plugin(PLUGIN2).service("trash").purge();
|
|
1757
|
+
if (count) strapi.log.info(`[${PLUGIN2}] trash purge: ${count} expired entries deleted`);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
strapi.log.error(`[${PLUGIN2}] trash purge failed: ${error?.message || error}`);
|
|
1760
|
+
}
|
|
1761
|
+
} } });
|
|
1762
|
+
}
|
|
1763
|
+
var DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
1764
|
+
var LOCALE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
1765
|
+
var can = (strapi, ctx, uid, action = "read") => {
|
|
1766
|
+
try {
|
|
1767
|
+
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot[action]();
|
|
1768
|
+
} catch {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
var canRead = (strapi, ctx, uid) => can(strapi, ctx, uid);
|
|
1773
|
+
var readableTypes = (strapi, ctx) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::") && canRead(strapi, ctx, uid));
|
|
1774
|
+
var ACTOR = /^(system|(admin|token|user):\d{1,15})$/;
|
|
1775
|
+
var int = (value, fallback, max) => {
|
|
1776
|
+
if (value === void 0 || value === "") return fallback;
|
|
1777
|
+
const n = Number(value);
|
|
1778
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1779
|
+
};
|
|
1780
|
+
var date = (value) => {
|
|
1781
|
+
if (!value) return null;
|
|
1782
|
+
const time = Date.parse(value);
|
|
1783
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(time) ? new Date(time) : NaN;
|
|
1784
|
+
};
|
|
1785
|
+
var historyController2 = ({ strapi }) => ({
|
|
1786
|
+
async list(ctx) {
|
|
1787
|
+
const { uid, documentId } = ctx.params || {};
|
|
1788
|
+
const locale = ctx.query?.locale || "";
|
|
1789
|
+
const actor = ctx.query?.actor || "";
|
|
1790
|
+
const page = int(ctx.query?.page, 1, 1e6), pageSize = int(ctx.query?.pageSize, 25, 100);
|
|
1791
|
+
if (typeof actor !== "string" || actor && !ACTOR.test(actor) || [page, pageSize].some(Number.isNaN)) return ctx.badRequest("Invalid history filters");
|
|
1792
|
+
if (!String(uid).startsWith("api::") || !strapi.contentTypes?.[uid] || !DOCUMENT_ID.test(documentId || "") || typeof locale !== "string" || locale && !LOCALE.test(locale)) return ctx.badRequest("Invalid content type, document or locale");
|
|
1793
|
+
if (!canRead(strapi, ctx, uid)) return ctx.forbidden();
|
|
1794
|
+
const history = strapi.plugin(PLUGIN2).service("history");
|
|
1795
|
+
ctx.body = await history.list({ uid, documentId, locale: (await history.scope(uid, locale || void 0))[0], actor, page, pageSize });
|
|
1796
|
+
},
|
|
1797
|
+
async find(ctx) {
|
|
1798
|
+
const id = Number(ctx.params?.id);
|
|
1799
|
+
if (!Number.isInteger(id) || id <= 0) return ctx.badRequest("Invalid event");
|
|
1800
|
+
const found = await strapi.plugin(PLUGIN2).service("history").find(id);
|
|
1801
|
+
if (!found || !canRead(strapi, ctx, found.event.contentType)) return ctx.notFound();
|
|
1802
|
+
ctx.body = found;
|
|
1803
|
+
},
|
|
1804
|
+
async activity(ctx) {
|
|
1805
|
+
const { actor = "", contentType = "", action = "", q = "" } = ctx.query || {};
|
|
1806
|
+
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);
|
|
1807
|
+
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");
|
|
1808
|
+
const history = strapi.plugin(PLUGIN2).service("history");
|
|
1809
|
+
const types = readableTypes(strapi, ctx);
|
|
1810
|
+
ctx.body = {
|
|
1811
|
+
...await history.activity({ actor, contentType, action, from, to, q: q.trim(), page, pageSize, types }),
|
|
1812
|
+
enabled: Boolean((await history.config()).enabled) && strapi.plugin(PLUGIN2).config("disabled") !== true,
|
|
1813
|
+
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) }))
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
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 };
|
|
221
1818
|
}
|
|
222
1819
|
});
|
|
223
1820
|
|
|
224
1821
|
// server/index.js
|
|
225
|
-
var { PLUGIN, TEMPLATES, DEFAULTS, catalog, validateSettings, mergeSaved, safeUrl, fail } = require_settings();
|
|
226
|
-
var { safeGroups, validateGroups } = require_groups();
|
|
1822
|
+
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, settingsComponentUids, safeComponentCategories, validateSettings, validateFields, validateVariants, validateKits, schemaMetadata, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
1823
|
+
var { safeGroups, validateGroups, invalidComponents, layoutFields } = require_groups();
|
|
1824
|
+
var { hiddenName, injectHidden, hideFromBuilder, registerHiddenStrip } = require_hidden();
|
|
1825
|
+
var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
|
|
1826
|
+
var { trashContentType, trashService, trashController } = require_trash();
|
|
1827
|
+
var STRAPI5 = false;
|
|
1828
|
+
var withHidden = [];
|
|
227
1829
|
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
228
1830
|
var LEGACY_PLUGIN = "block-picker";
|
|
229
1831
|
async function readSettings(strapi) {
|
|
@@ -247,8 +1849,22 @@ async function resolveMedia(strapi, settings) {
|
|
|
247
1849
|
}
|
|
248
1850
|
return resolved;
|
|
249
1851
|
}
|
|
1852
|
+
var contentTypeUids = (strapi) => Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid, schema]) => uid.startsWith("api::") && zonesOf(schema).length).map(([uid, schema]) => [uid, Object.keys(schema.attributes || {})]));
|
|
1853
|
+
var apiUids = (strapi) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::"));
|
|
250
1854
|
var zonesOf = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => attr?.type === "dynamiczone").map(([name]) => name);
|
|
251
1855
|
var label = (error) => `row ${error.index + 1}: ${error.code === "closeBeforeOpen" ? `close marker ${error.uid} has no open marker before it` : error.code === "mismatch" ? `close marker ${error.uid} does not match the open group ${error.open} (expected ${error.expected})` : `group ${error.uid} is not closed (expected ${error.expected})`}`;
|
|
1856
|
+
var componentLabel = (error) => `row ${error.index + 1}: component "${error.uid}" is not allowed in this Dynamic Zone`;
|
|
1857
|
+
function checkComponents(strapi, uid, entry) {
|
|
1858
|
+
const schema = strapi.getModel(uid);
|
|
1859
|
+
for (const name of zonesOf(schema)) {
|
|
1860
|
+
if (!Array.isArray(entry?.[name])) continue;
|
|
1861
|
+
const errors = invalidComponents(entry[name], schema.attributes[name].components);
|
|
1862
|
+
if (errors.length) fail(
|
|
1863
|
+
`Block components in "${name}" are not allowed by its schema (${errors.map(componentLabel).join("; ")}).`,
|
|
1864
|
+
{ plugin: PLUGIN, field: name, errors: errors.map((error) => ({ path: [name, error.index], message: componentLabel(error), name: "ValidationError", code: "componentNotAllowed", uid: error.uid })) }
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
252
1868
|
function checkZones(strapi, uid, entry, groups) {
|
|
253
1869
|
for (const name of zonesOf(strapi.getModel(uid))) {
|
|
254
1870
|
const errors = validateGroups(Array.isArray(entry?.[name]) ? entry[name] : [], groups);
|
|
@@ -258,11 +1874,15 @@ function checkZones(strapi, uid, entry, groups) {
|
|
|
258
1874
|
);
|
|
259
1875
|
}
|
|
260
1876
|
}
|
|
261
|
-
function
|
|
1877
|
+
function registerZoneGuards(strapi) {
|
|
262
1878
|
const plugin = strapi.plugin(PLUGIN);
|
|
263
1879
|
const groups = safeGroups(plugin.config("groups"), componentUids(strapi));
|
|
264
1880
|
if (plugin.config("groups") && !groups) strapi.log.warn(`[${PLUGIN}] "groups" config ignored: expected { "<open component uid>": "<close component uid>" } with existing components; every block is treated as ordinary.`);
|
|
265
|
-
|
|
1881
|
+
const active = async (uid) => {
|
|
1882
|
+
if (plugin.config("disabled") === true) return false;
|
|
1883
|
+
const settings = await plugin.service("settings").get();
|
|
1884
|
+
return settings.editor.enabled && settings.contentTypes?.[uid]?.enabled !== false;
|
|
1885
|
+
};
|
|
266
1886
|
if (strapi.documents?.use) {
|
|
267
1887
|
const defaultLocale = async () => {
|
|
268
1888
|
try {
|
|
@@ -274,12 +1894,14 @@ function registerPublishGuard(strapi) {
|
|
|
274
1894
|
strapi.documents.use(async (ctx, next) => {
|
|
275
1895
|
const { action, params = {} } = ctx;
|
|
276
1896
|
const publishing = action === "publish" || (action === "create" || action === "update") && params.status === "published";
|
|
277
|
-
if (!
|
|
1897
|
+
if (!ctx.uid || !["create", "update", "publish"].includes(action)) return next();
|
|
278
1898
|
const model = strapi.getModel(ctx.uid);
|
|
279
1899
|
const zones = zonesOf(model);
|
|
280
|
-
if (!zones.length) return next();
|
|
1900
|
+
if (!zones.length || !await active(ctx.uid)) return next();
|
|
1901
|
+
if (action === "create" || action === "update") checkComponents(strapi, ctx.uid, params.data || {});
|
|
1902
|
+
if (!publishing) return next();
|
|
281
1903
|
if (action === "create") {
|
|
282
|
-
checkZones(strapi, ctx.uid, params.data || {}, groups);
|
|
1904
|
+
if (groups) checkZones(strapi, ctx.uid, params.data || {}, groups);
|
|
283
1905
|
return next();
|
|
284
1906
|
}
|
|
285
1907
|
const populate = Object.fromEntries(zones.map((name) => [name, true]));
|
|
@@ -294,7 +1916,8 @@ function registerPublishGuard(strapi) {
|
|
|
294
1916
|
}
|
|
295
1917
|
for (const draft of drafts) {
|
|
296
1918
|
const entry = action === "update" ? { ...draft || {}, ...Object.fromEntries(zones.filter((name) => Array.isArray(params.data?.[name])).map((name) => [name, params.data[name]])) } : draft;
|
|
297
|
-
|
|
1919
|
+
checkComponents(strapi, ctx.uid, entry);
|
|
1920
|
+
if (groups) checkZones(strapi, ctx.uid, entry, groups);
|
|
298
1921
|
}
|
|
299
1922
|
return next();
|
|
300
1923
|
});
|
|
@@ -303,20 +1926,31 @@ function registerPublishGuard(strapi) {
|
|
|
303
1926
|
const published = (data) => Boolean(data && data.publishedAt);
|
|
304
1927
|
strapi.db.lifecycles.subscribe({
|
|
305
1928
|
async beforeCreate(event) {
|
|
306
|
-
|
|
1929
|
+
const data = event.params?.data, uid = event.model.uid;
|
|
1930
|
+
if (!zonesOf(strapi.getModel(uid)).length || !await active(uid)) return;
|
|
1931
|
+
checkComponents(strapi, uid, data || {});
|
|
1932
|
+
if (groups && published(data)) checkZones(strapi, uid, data, groups);
|
|
307
1933
|
},
|
|
308
1934
|
async beforeUpdate(event) {
|
|
309
1935
|
const { data, where } = event.params || {};
|
|
310
1936
|
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
311
|
-
if (!
|
|
1937
|
+
if (!zones.length || !await active(event.model.uid)) return;
|
|
1938
|
+
checkComponents(strapi, event.model.uid, data || {});
|
|
1939
|
+
if (!published(data)) return;
|
|
312
1940
|
const entry = zones.every((name) => Array.isArray(data[name])) ? data : { ...await strapi.entityService.findOne(event.model.uid, where?.id, { populate: zones }), ...data };
|
|
313
|
-
|
|
1941
|
+
checkComponents(strapi, event.model.uid, entry);
|
|
1942
|
+
if (groups) checkZones(strapi, event.model.uid, entry, groups);
|
|
314
1943
|
},
|
|
315
1944
|
async beforeUpdateMany(event) {
|
|
316
1945
|
const { data, where } = event.params || {};
|
|
317
1946
|
const zones = zonesOf(strapi.getModel(event.model.uid));
|
|
318
|
-
if (!
|
|
319
|
-
|
|
1947
|
+
if (!zones.length || !await active(event.model.uid)) return;
|
|
1948
|
+
checkComponents(strapi, event.model.uid, data || {});
|
|
1949
|
+
if (!published(data)) return;
|
|
1950
|
+
for (const entry of await strapi.entityService.findMany(event.model.uid, { filters: where, populate: zones })) {
|
|
1951
|
+
checkComponents(strapi, event.model.uid, entry);
|
|
1952
|
+
if (groups) checkZones(strapi, event.model.uid, entry, groups);
|
|
1953
|
+
}
|
|
320
1954
|
}
|
|
321
1955
|
});
|
|
322
1956
|
}
|
|
@@ -332,30 +1966,142 @@ module.exports = {
|
|
|
332
1966
|
blockPreview: false,
|
|
333
1967
|
// Optional layout groups: OPEN component uid -> its CLOSE uid, e.g. { 'wrappers.join': 'wrappers.close' }.
|
|
334
1968
|
// Empty/absent: every Dynamic Zone component is an ordinary block. See README "Layout groups".
|
|
335
|
-
groups: null
|
|
1969
|
+
groups: null,
|
|
1970
|
+
// Optional Strapi component categories shown as configurable cards in Settings > Block gallery.
|
|
1971
|
+
// This does not change the components allowed by each Dynamic Zone.
|
|
1972
|
+
settingsComponentCategories: null,
|
|
1973
|
+
// Project defaults from code: same shape as the stored settings, e.g. require('./blockscene.json'). See README "Settings page".
|
|
1974
|
+
settings: null,
|
|
1975
|
+
// Edit view field texts from code (never stored): { uid: { attribute: { label, description, placeholder } } }. See README "Field labels".
|
|
1976
|
+
fields: null,
|
|
1977
|
+
// Initial block sets per content type, shown only on a new empty entry. See README "Starter kits".
|
|
1978
|
+
kits: null,
|
|
1979
|
+
// Other languages for the schema metadata (pluginOptions.blockscene, info.description), one flat map per locale:
|
|
1980
|
+
// { 'pt-BR': require('./blockscene/pt-BR.json') }. See README "Describing blocks and fields in the schema".
|
|
1981
|
+
translations: null,
|
|
1982
|
+
// "Hide on the site": boolean attribute added to every Dynamic Zone component (a DB column); false adds nothing.
|
|
1983
|
+
hiddenAttribute: "bsHidden"
|
|
336
1984
|
},
|
|
337
1985
|
validator: catalog
|
|
338
1986
|
},
|
|
1987
|
+
// Before the database schema sync, so the column exists. A name the host already uses with another type is skipped.
|
|
1988
|
+
register({ strapi }) {
|
|
1989
|
+
const plugin = strapi.plugin(PLUGIN);
|
|
1990
|
+
const name = hiddenName(plugin.config("hiddenAttribute"));
|
|
1991
|
+
withHidden = [];
|
|
1992
|
+
if (name === null) strapi.log.warn(`[${PLUGIN}] "hiddenAttribute" config ignored: expected a name like "bsHidden" or false. Hiding blocks is off.`);
|
|
1993
|
+
if (plugin.config("settingsComponentCategories") != null && !safeComponentCategories(plugin.config("settingsComponentCategories"))) strapi.log.warn(`[${PLUGIN}] "settingsComponentCategories" config ignored: expected 1 to 50 Strapi component category names.`);
|
|
1994
|
+
if (!name) return;
|
|
1995
|
+
const { added, skipped } = injectHidden(strapi.components, strapi.contentTypes, name);
|
|
1996
|
+
withHidden = added;
|
|
1997
|
+
if (skipped.length) strapi.log.warn(`[${PLUGIN}] "${name}" already exists with another type on ${skipped.join(", ")}; those blocks cannot be hidden.`);
|
|
1998
|
+
},
|
|
339
1999
|
async bootstrap({ strapi }) {
|
|
340
2000
|
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
341
2001
|
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
342
|
-
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN }
|
|
2002
|
+
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN },
|
|
2003
|
+
...STRAPI5 ? [
|
|
2004
|
+
["history.read", "Read version history"],
|
|
2005
|
+
["activity.read", "Read content activity"],
|
|
2006
|
+
["trash.read", "Read the trash"],
|
|
2007
|
+
["trash.restore", "Restore from the trash"],
|
|
2008
|
+
["trash.purge", "Delete from the trash forever"]
|
|
2009
|
+
].map(([uid, displayName]) => ({ section: "plugins", displayName, uid, pluginName: PLUGIN })) : []
|
|
343
2010
|
]);
|
|
344
|
-
|
|
2011
|
+
strapi.plugin(PLUGIN).service("settings").projectDefaults();
|
|
2012
|
+
strapi.plugin(PLUGIN).service("settings").fields();
|
|
2013
|
+
strapi.plugin(PLUGIN).service("settings").variants();
|
|
2014
|
+
strapi.plugin(PLUGIN).service("settings").kits();
|
|
2015
|
+
registerZoneGuards(strapi);
|
|
2016
|
+
registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
|
|
2017
|
+
if (withHidden.length) hideFromBuilder(typeof strapi.get === "function" ? strapi.get("services") : strapi.container.get("services"), withHidden, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")));
|
|
2018
|
+
if (STRAPI5) {
|
|
2019
|
+
registerHistory(strapi);
|
|
2020
|
+
registerPurge(strapi);
|
|
2021
|
+
}
|
|
345
2022
|
},
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
2023
|
+
destroy({ strapi }) {
|
|
2024
|
+
if (STRAPI5) strapi.cron?.remove?.(CRON);
|
|
2025
|
+
},
|
|
2026
|
+
// Version history events and the trash (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
2027
|
+
contentTypes: STRAPI5 ? { event: eventContentType, trash: trashContentType } : {},
|
|
2028
|
+
services: { settings: ({ strapi }) => {
|
|
2029
|
+
let project;
|
|
2030
|
+
const projectDefaults = () => {
|
|
2031
|
+
if (project !== void 0) return project;
|
|
2032
|
+
const code = strapi.plugin(PLUGIN).config("settings");
|
|
2033
|
+
project = null;
|
|
2034
|
+
if (code) try {
|
|
2035
|
+
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
|
|
354
2038
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
2039
|
+
return project;
|
|
2040
|
+
};
|
|
2041
|
+
let meta;
|
|
2042
|
+
const schemaMeta = () => {
|
|
2043
|
+
if (meta !== void 0) return meta;
|
|
2044
|
+
const notes = [];
|
|
2045
|
+
meta = schemaMetadata({ ...strapi.contentTypes, ...strapi.components }, strapi.plugin(PLUGIN).config("translations"), notes);
|
|
2046
|
+
if (notes.length) strapi.log?.warn(`[${PLUGIN}] schema metadata left out: ${notes.slice(0, 20).join("; ")}${notes.length > 20 ? `; and ${notes.length - 20} more` : ""}.`);
|
|
2047
|
+
return meta;
|
|
2048
|
+
};
|
|
2049
|
+
let fields;
|
|
2050
|
+
const fieldTexts = () => {
|
|
2051
|
+
if (fields !== void 0) return fields;
|
|
2052
|
+
const schemas = Object.fromEntries(Object.entries({ ...strapi.contentTypes, ...strapi.components }).map(([uid, schema]) => [uid, Object.keys(schema?.attributes || {})]));
|
|
2053
|
+
const stale = [];
|
|
2054
|
+
let code = {};
|
|
2055
|
+
try {
|
|
2056
|
+
code = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas, stale);
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
strapi.log?.warn(`[${PLUGIN}] "fields" config ignored: ${error.message}. Field labels fall back to the schema metadata and the Content Manager's own.`);
|
|
2059
|
+
}
|
|
2060
|
+
if (stale.length) strapi.log?.warn(`[${PLUGIN}] "fields" entries for fields the schema no longer has were skipped: ${stale.slice(0, 20).join(", ")}${stale.length > 20 ? ` and ${stale.length - 20} more` : ""}.`);
|
|
2061
|
+
fields = structuredClone(schemaMeta().fields);
|
|
2062
|
+
for (const [uid, entries] of Object.entries(code)) for (const [name, entry] of Object.entries(entries)) (fields[uid] ||= {})[name] = { ...fields[uid][name], ...entry };
|
|
2063
|
+
return fields;
|
|
2064
|
+
};
|
|
2065
|
+
let variants;
|
|
2066
|
+
const insertVariants = () => {
|
|
2067
|
+
if (variants !== void 0) return variants;
|
|
2068
|
+
const notes = [];
|
|
2069
|
+
variants = validateVariants(strapi.plugin(PLUGIN).config("components"), strapi.components || {}, notes);
|
|
2070
|
+
for (const note of notes) strapi.log?.warn(`[${PLUGIN}] components variants: ${note}.`);
|
|
2071
|
+
return variants;
|
|
2072
|
+
};
|
|
2073
|
+
let kits;
|
|
2074
|
+
const starterKits = () => {
|
|
2075
|
+
if (kits !== void 0) return kits;
|
|
2076
|
+
const notes = [];
|
|
2077
|
+
kits = validateKits(strapi.plugin(PLUGIN).config("kits"), strapi.contentTypes || {}, strapi.components || {}, safeGroups(strapi.plugin(PLUGIN).config("groups"), componentUids(strapi)), notes);
|
|
2078
|
+
for (const note of notes) strapi.log?.warn(`[${PLUGIN}] starter kits: ${note}.`);
|
|
2079
|
+
return kits;
|
|
2080
|
+
};
|
|
2081
|
+
const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
2082
|
+
return {
|
|
2083
|
+
projectDefaults,
|
|
2084
|
+
get,
|
|
2085
|
+
fields: fieldTexts,
|
|
2086
|
+
schema: schemaMeta,
|
|
2087
|
+
variants: insertVariants,
|
|
2088
|
+
kits: starterKits,
|
|
2089
|
+
async set(value) {
|
|
2090
|
+
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
2091
|
+
for (const [uid, entry] of Object.entries(next.components)) {
|
|
2092
|
+
if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
|
|
2093
|
+
}
|
|
2094
|
+
await store(strapi).set({ key: "settings", value: overrides(next, projectDefaults()) });
|
|
2095
|
+
return next;
|
|
2096
|
+
},
|
|
2097
|
+
// Back to the project defaults (or the built-in ones); the legacy copy goes too, or it would be migrated again.
|
|
2098
|
+
async reset() {
|
|
2099
|
+
await store(strapi).delete({ key: "settings" });
|
|
2100
|
+
await strapi.store({ type: "plugin", name: LEGACY_PLUGIN }).delete({ key: "settings" });
|
|
2101
|
+
return get();
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
}, ...STRAPI5 && { history: historyService, trash: trashService } },
|
|
359
2105
|
controllers: {
|
|
360
2106
|
catalog: ({ strapi }) => ({
|
|
361
2107
|
async find(ctx) {
|
|
@@ -367,53 +2113,124 @@ module.exports = {
|
|
|
367
2113
|
disabled: plugin.config("disabled"),
|
|
368
2114
|
blockPreview: plugin.config("blockPreview"),
|
|
369
2115
|
groups: plugin.config("groups"),
|
|
370
|
-
componentUids: componentUids(strapi)
|
|
2116
|
+
componentUids: componentUids(strapi),
|
|
2117
|
+
schemas: strapi.components,
|
|
2118
|
+
blocks: plugin.service("settings").schema().blocks
|
|
371
2119
|
});
|
|
372
2120
|
const settings = await plugin.service("settings").get();
|
|
373
2121
|
const media = await resolveMedia(strapi, settings);
|
|
2122
|
+
for (const [uid, list] of Object.entries(plugin.service("settings").variants())) base.components[uid] = { ...base.components[uid], variants: list };
|
|
374
2123
|
for (const [uid, entry] of Object.entries(settings.components)) {
|
|
375
|
-
base.components[uid] = {
|
|
2124
|
+
base.components[uid] = {
|
|
2125
|
+
...base.components[uid],
|
|
2126
|
+
...media[uid],
|
|
2127
|
+
template: entry.template,
|
|
2128
|
+
...entry.typology && { typology: entry.typology },
|
|
2129
|
+
...entry.tags && { tags: entry.tags },
|
|
2130
|
+
...typeof entry.showRowThumbnail === "boolean" && { showRowThumbnail: entry.showRowThumbnail }
|
|
2131
|
+
};
|
|
376
2132
|
}
|
|
2133
|
+
const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
|
|
2134
|
+
const types = Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid]) => !uid.startsWith("admin::")).map(([uid, schema]) => [uid, [schema.info?.displayName || "", ...Object.keys(schema.attributes || {})]]));
|
|
2135
|
+
const history = STRAPI5 && strapi.documents?.use && !base.disabled && settings.history.enabled ? { contentTypes: apiUids(strapi).filter((uid) => covers(settings.history, uid)) } : null;
|
|
2136
|
+
const layouts = Object.fromEntries(Object.entries(settings.components).filter(([uid, entry]) => entry.layout && base.groups?.[uid]).map(([uid, entry]) => [uid, entry.layout]));
|
|
377
2137
|
ctx.body = {
|
|
378
2138
|
...base,
|
|
2139
|
+
layouts,
|
|
2140
|
+
kits: plugin.service("settings").kits(),
|
|
379
2141
|
palette: settings.palette,
|
|
380
|
-
|
|
2142
|
+
contentTypes: settings.contentTypes,
|
|
2143
|
+
hiddenAttribute: hidden,
|
|
2144
|
+
fields: plugin.service("settings").fields(),
|
|
2145
|
+
types,
|
|
2146
|
+
history,
|
|
2147
|
+
editor: { ...settings.editor, enabled: settings.editor.enabled && !base.disabled, ...!hidden && { hiddenBlocks: "off" } }
|
|
381
2148
|
};
|
|
382
2149
|
}
|
|
383
2150
|
}),
|
|
384
2151
|
settings: ({ strapi }) => ({
|
|
385
2152
|
async find(ctx) {
|
|
386
2153
|
const settings = await strapi.plugin(PLUGIN).service("settings").get();
|
|
387
|
-
const
|
|
2154
|
+
const auto = catalog({ components: strapi.plugin(PLUGIN).config("components"), schemas: strapi.components, blocks: strapi.plugin(PLUGIN).service("settings").schema().blocks }).components;
|
|
2155
|
+
const visible = new Set(settingsComponentUids(strapi.contentTypes, strapi.components, settings.contentTypes, strapi.plugin(PLUGIN).config("settingsComponentCategories")));
|
|
2156
|
+
const components = Object.entries(strapi.components || {}).filter(([uid]) => visible.has(uid)).map(([uid, schema]) => ({
|
|
388
2157
|
uid,
|
|
389
2158
|
displayName: schema.info?.displayName || uid,
|
|
390
|
-
category: schema.category || uid.split(".")[0]
|
|
2159
|
+
category: schema.category || uid.split(".")[0],
|
|
2160
|
+
typology: auto[uid]?.typology,
|
|
2161
|
+
layoutFields: layoutFields(schema)
|
|
2162
|
+
}));
|
|
2163
|
+
const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
|
|
2164
|
+
uid,
|
|
2165
|
+
displayName: strapi.contentTypes[uid].info?.displayName || uid,
|
|
2166
|
+
kind: strapi.contentTypes[uid].kind,
|
|
2167
|
+
attributes: Object.entries(strapi.contentTypes[uid].attributes || {}).filter(([, attr]) => attr?.type !== "dynamiczone" && !attr?.private).map(([name, attr]) => ({ name, type: attr.type }))
|
|
391
2168
|
}));
|
|
392
2169
|
ctx.body = {
|
|
393
2170
|
settings,
|
|
394
2171
|
components,
|
|
2172
|
+
contentTypes,
|
|
395
2173
|
media: await resolveMedia(strapi, settings),
|
|
396
2174
|
templates: TEMPLATES,
|
|
2175
|
+
typologies: TYPOLOGIES,
|
|
397
2176
|
disabled: strapi.plugin(PLUGIN).config("disabled") === true,
|
|
2177
|
+
hiddenAttribute: hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")) || null,
|
|
398
2178
|
blockPreviewAvailable: strapi.plugin(PLUGIN).config("blockPreview") === true,
|
|
399
|
-
defaults: DEFAULTS
|
|
2179
|
+
defaults: DEFAULTS,
|
|
2180
|
+
projectDefaults: strapi.plugin(PLUGIN).service("settings").projectDefaults(),
|
|
2181
|
+
// Version history (Strapi 5 only): the content types it can cover.
|
|
2182
|
+
historyTypes: STRAPI5 && strapi.documents?.use ? apiUids(strapi).map((uid) => ({ uid, displayName: strapi.contentTypes[uid].info?.displayName || uid })) : null
|
|
400
2183
|
};
|
|
401
2184
|
},
|
|
402
2185
|
async update(ctx) {
|
|
403
2186
|
ctx.body = await strapi.plugin(PLUGIN).service("settings").set(ctx.request?.body);
|
|
2187
|
+
},
|
|
2188
|
+
async reset(ctx) {
|
|
2189
|
+
ctx.body = await strapi.plugin(PLUGIN).service("settings").reset();
|
|
2190
|
+
}
|
|
2191
|
+
}),
|
|
2192
|
+
prefs: ({ strapi }) => ({
|
|
2193
|
+
async find(ctx) {
|
|
2194
|
+
const id = ctx.state?.user?.id;
|
|
2195
|
+
if (!id) return ctx.unauthorized();
|
|
2196
|
+
ctx.body = mergePrefs(await store(strapi).get({ key: `prefs:${id}` }), componentUids(strapi));
|
|
2197
|
+
},
|
|
2198
|
+
async update(ctx) {
|
|
2199
|
+
const id = ctx.state?.user?.id;
|
|
2200
|
+
if (!id) return ctx.unauthorized();
|
|
2201
|
+
const value = validatePrefs(ctx.request?.body, componentUids(strapi));
|
|
2202
|
+
await store(strapi).set({ key: `prefs:${id}`, value });
|
|
2203
|
+
ctx.body = value;
|
|
404
2204
|
}
|
|
405
|
-
})
|
|
2205
|
+
}),
|
|
2206
|
+
...STRAPI5 && { history: historyController, trash: trashController }
|
|
406
2207
|
},
|
|
407
2208
|
routes: { admin: { type: "admin", routes: [
|
|
408
2209
|
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
409
|
-
...["
|
|
410
|
-
|
|
2210
|
+
...["GET", "PUT"].map((method) => ({ method, path: "/me/prefs", handler: `prefs.${method === "GET" ? "find" : "update"}`, config: { policies: ["admin::isAuthenticatedAdmin"] } })),
|
|
2211
|
+
...[["GET", "find", "read"], ["PUT", "update", "update"], ["DELETE", "reset", "update"]].map(([method, handler, action]) => ({
|
|
2212
|
+
method,
|
|
411
2213
|
path: "/settings",
|
|
412
2214
|
handler: `settings.${handler}`,
|
|
413
2215
|
config: { policies: ["admin::isAuthenticatedAdmin", {
|
|
414
2216
|
name: "admin::hasPermissions",
|
|
415
|
-
config: { actions: [`plugin::${PLUGIN}.settings.${
|
|
2217
|
+
config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
|
|
416
2218
|
}] }
|
|
417
|
-
}))
|
|
2219
|
+
})),
|
|
2220
|
+
...STRAPI5 ? [
|
|
2221
|
+
["GET", "/history/:uid/:documentId", "history.list", "history.read"],
|
|
2222
|
+
["GET", "/history-events/:id", "history.find", "history.read"],
|
|
2223
|
+
["GET", "/activity", "history.activity", "activity.read"],
|
|
2224
|
+
["GET", "/trash", "trash.list", "trash.read"],
|
|
2225
|
+
["GET", "/trash/:id", "trash.find", "trash.read"],
|
|
2226
|
+
["GET", "/trash/:id/check", "trash.check", "trash.restore"],
|
|
2227
|
+
["POST", "/trash/:id/restore", "trash.restore", "trash.restore"],
|
|
2228
|
+
["DELETE", "/trash/:id", "trash.remove", "trash.purge"]
|
|
2229
|
+
].map(([method, path, handler, action]) => ({
|
|
2230
|
+
method,
|
|
2231
|
+
path,
|
|
2232
|
+
handler,
|
|
2233
|
+
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.${action}`] } }] }
|
|
2234
|
+
})) : []
|
|
418
2235
|
] } }
|
|
419
2236
|
};
|