@aduptive/strapi-blockscene 2.0.0-alpha.6 → 2.0.0-alpha.8
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 +432 -52
- package/dist/admin.cjs +4187 -1538
- package/dist/admin.mjs +4226 -1554
- package/dist/server.cjs +765 -49
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -106,7 +106,41 @@ var require_groups = __commonJS({
|
|
|
106
106
|
const [, end] = groupRange(rows, start, g);
|
|
107
107
|
return [...rows.slice(0, start), ...rows.slice(end + 1)];
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
var MAX_COLUMNS = 12;
|
|
110
|
+
var parseColumns = (value) => {
|
|
111
|
+
const n = typeof value === "number" ? value : typeof value === "string" && /^\s*\d{1,3}\s*$/.test(value) ? Number(value) : NaN;
|
|
112
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
113
|
+
};
|
|
114
|
+
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));
|
|
115
|
+
var layoutFields2 = (schema) => Object.entries(schema?.attributes || {}).filter(([, attr]) => columnsAttribute(attr)).map(([name]) => name);
|
|
116
|
+
function validLayout(value, attributes) {
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
118
|
+
if (!Object.keys(value).every((key) => ["columnsField", "mobileColumnsField", "maxColumns"].includes(key))) return false;
|
|
119
|
+
const field = (name) => typeof name === "string" && /^[A-Za-z_][\w-]{0,63}$/.test(name) && (!attributes || columnsAttribute(attributes[name]));
|
|
120
|
+
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);
|
|
121
|
+
}
|
|
122
|
+
function layoutColumns(row, layout, schema) {
|
|
123
|
+
const max = layout.maxColumns || MAX_COLUMNS;
|
|
124
|
+
const read = (name) => Math.min(max, parseColumns(row?.[name]) ?? parseColumns(schema?.attributes?.[name]?.default) ?? 1);
|
|
125
|
+
return { desktop: read(layout.columnsField), mobile: layout.mobileColumnsField ? read(layout.mobileColumnsField) : null };
|
|
126
|
+
}
|
|
127
|
+
module2.exports = {
|
|
128
|
+
safeGroups: safeGroups2,
|
|
129
|
+
validateGroups: validateGroups2,
|
|
130
|
+
groupRange,
|
|
131
|
+
groupRows,
|
|
132
|
+
topLevelRanges,
|
|
133
|
+
moveGroup,
|
|
134
|
+
removeGroup,
|
|
135
|
+
blockKey,
|
|
136
|
+
isOpener,
|
|
137
|
+
isClose,
|
|
138
|
+
MAX_COLUMNS,
|
|
139
|
+
parseColumns,
|
|
140
|
+
layoutFields: layoutFields2,
|
|
141
|
+
validLayout,
|
|
142
|
+
layoutColumns
|
|
143
|
+
};
|
|
110
144
|
}
|
|
111
145
|
});
|
|
112
146
|
|
|
@@ -146,24 +180,37 @@ var require_settings = __commonJS({
|
|
|
146
180
|
duplicate: true,
|
|
147
181
|
clipboard: true,
|
|
148
182
|
// Edit view labels that are still the raw attribute name read as "Mobile columns count" (see README "Field labels").
|
|
149
|
-
friendlyLabels: true
|
|
183
|
+
friendlyLabels: true,
|
|
184
|
+
// Page preview pane (Strapi 5): which toolbar controls show, in order, and the width menu's entries.
|
|
185
|
+
previewToolbar: ["modes", "history", "devices", "status", "actions"],
|
|
186
|
+
previewDevices: ["fit", "mobile", "tablet", "desktop"]
|
|
150
187
|
},
|
|
151
188
|
// Per content type (only the ones with a Dynamic Zone): { enabled: false } turns the plugin off there;
|
|
152
189
|
// previewMode overrides editor.previewMode as the mode the edit view opens in. Absent means the global behaviour.
|
|
153
190
|
contentTypes: {},
|
|
154
191
|
// Version history (Strapi 5, see README "Version history"): off until turned on. contentTypes: 'all' (every api:: type)
|
|
155
|
-
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays
|
|
156
|
-
|
|
192
|
+
// or a list of uids. Snapshots are kept retentionDays and at most maxSnapshots per document; events eventDays; deleted
|
|
193
|
+
// documents stay in the trash trashDays (fixed per document when it is deleted).
|
|
194
|
+
history: { enabled: false, contentTypes: "all", retentionDays: 90, maxSnapshots: 100, eventDays: 365, trashDays: 90 }
|
|
157
195
|
};
|
|
158
196
|
var ICONS = ["text", "tag", "seo", "settings", "image", "link", "palette", "list", "globe", "info"];
|
|
159
197
|
var SIDEBAR_POSITIONS = ["left", "right", "bottom"];
|
|
160
198
|
var OPENS = ["modal", "drawer"];
|
|
161
199
|
var LABEL = /^[^<>]{1,40}$/;
|
|
200
|
+
var TOOLBAR = ["modes", "history", "devices", "status", "actions"];
|
|
201
|
+
var DEVICE_NAMES = ["fit", "mobile", "tablet", "desktop"];
|
|
202
|
+
var previewToolbar = (value) => Array.isArray(value) && value.every((id) => TOOLBAR.includes(id)) && new Set(value).size === value.length;
|
|
203
|
+
var DEVICE_LABEL = /^[^<>]{1,24}$/;
|
|
204
|
+
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;
|
|
205
|
+
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;
|
|
206
|
+
var cleanDevices = (value) => value.map((item) => typeof item === "string" ? item : { label: item.label.trim(), width: item.width });
|
|
162
207
|
var sidebarItem = (item, attributes) => item && typeof item === "object" && !Array.isArray(item) && Object.keys(item).every((key) => ["label", "icon", "open", "fields"].includes(key)) && typeof item.label === "string" && LABEL.test(item.label.trim()) && (item.icon === void 0 || item.icon === "" || ICONS.includes(item.icon)) && OPENS.includes(item.open) && Array.isArray(item.fields) && item.fields.length > 0 && item.fields.length <= 30 && item.fields.every((field) => typeof field === "string" && (!attributes || attributes.includes(field)));
|
|
163
208
|
var TYPE_KEYS = {
|
|
164
209
|
enabled: (value) => typeof value === "boolean",
|
|
165
210
|
previewMode: (value) => PREVIEW_MODES.includes(value),
|
|
166
211
|
sidebarPosition: (value) => SIDEBAR_POSITIONS.includes(value),
|
|
212
|
+
previewToolbar,
|
|
213
|
+
previewDevices,
|
|
167
214
|
sidebar: (value, attributes) => Array.isArray(value) && value.length <= 12 && value.every((item) => sidebarItem(item, attributes))
|
|
168
215
|
};
|
|
169
216
|
var typeMap = (types) => Array.isArray(types) ? Object.fromEntries(types.map((uid) => [uid, null])) : types || {};
|
|
@@ -180,7 +227,8 @@ var require_settings = __commonJS({
|
|
|
180
227
|
contentTypes: (value, uids) => value === "all" || Array.isArray(value) && value.length <= 500 && value.every((uid) => typeof uid === "string" && API_UID.test(uid) && (!uids || uids.includes(uid))),
|
|
181
228
|
retentionDays: days,
|
|
182
229
|
maxSnapshots: (value) => Number.isInteger(value) && value >= 1 && value <= 1e3,
|
|
183
|
-
eventDays: days
|
|
230
|
+
eventDays: days,
|
|
231
|
+
trashDays: days
|
|
184
232
|
};
|
|
185
233
|
var TYPOLOGIES2 = ["hero", "text", "media", "listing", "cards", "cta", "form", "layout"];
|
|
186
234
|
var TAG = /^[^<>\n]{1,24}$/;
|
|
@@ -223,7 +271,7 @@ var require_settings = __commonJS({
|
|
|
223
271
|
return [...facets];
|
|
224
272
|
}
|
|
225
273
|
var safeUrl2 = (value) => typeof value === "string" && (/^\/(?!\/)/.test(value) || /^https?:\/\//.test(value)) ? value : void 0;
|
|
226
|
-
var { safeGroups: safeGroups2 } = require_groups();
|
|
274
|
+
var { safeGroups: safeGroups2, validLayout } = require_groups();
|
|
227
275
|
var safeVersion = (value) => typeof value === "string" && /^[\w.-]{1,32}$/.test(value) ? value : void 0;
|
|
228
276
|
var fail2 = (message, details) => {
|
|
229
277
|
let ValidationError;
|
|
@@ -268,7 +316,7 @@ var require_settings = __commonJS({
|
|
|
268
316
|
groups: safeGroups2(config.groups, config.componentUids)
|
|
269
317
|
};
|
|
270
318
|
}
|
|
271
|
-
function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
319
|
+
function validateSettings2(input, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
|
|
272
320
|
const types = typeMap(contentTypeUids2);
|
|
273
321
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Settings must be an object");
|
|
274
322
|
if (JSON.stringify(input).length > 64 * 1024) fail2("Settings payload too large");
|
|
@@ -281,9 +329,9 @@ var require_settings = __commonJS({
|
|
|
281
329
|
}
|
|
282
330
|
for (const [key, value] of Object.entries(input.editor || {})) {
|
|
283
331
|
if (!(key in DEFAULTS2.editor)) fail2(`Unknown editor option "${key}"`);
|
|
284
|
-
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : typeof value === "boolean";
|
|
332
|
+
const valid = key === "initialState" ? INITIAL_STATES.includes(value) : key === "previewMode" ? PREVIEW_MODES.includes(value) : key === "hiddenBlocks" ? HIDDEN_MODES.includes(value) : key === "previewUrl" ? value === "" || typeof value === "string" && PREVIEW_URL.test(value) : key === "blockPreviewUrl" ? blockPreviewUrl(value) : key === "lazyFields" ? lazyFields(value) : key === "previewToolbar" ? previewToolbar(value) : key === "previewDevices" ? previewDevices(value) : typeof value === "boolean";
|
|
285
333
|
if (!valid) fail2(`Invalid value for editor option "${key}"`);
|
|
286
|
-
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : value;
|
|
334
|
+
out.editor[key] = key === "previewUrl" && value ? String(new URL(value).href).replace(/\/$/, "") : key === "lazyFields" ? [...new Set(value)] : key === "previewDevices" ? cleanDevices(value) : value;
|
|
287
335
|
}
|
|
288
336
|
const components = input.components || {};
|
|
289
337
|
if (typeof components !== "object" || Array.isArray(components)) fail2("components must be an object");
|
|
@@ -304,6 +352,9 @@ var require_settings = __commonJS({
|
|
|
304
352
|
} else if (key === "mediaId") {
|
|
305
353
|
if (!Number.isInteger(entry.mediaId) || entry.mediaId <= 0) fail2(`Invalid media for "${uid}"`);
|
|
306
354
|
clean.mediaId = entry.mediaId;
|
|
355
|
+
} else if (key === "layout") {
|
|
356
|
+
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`);
|
|
357
|
+
clean.layout = structuredClone(entry.layout);
|
|
307
358
|
} else fail2(`Unknown component setting "${key}"`);
|
|
308
359
|
}
|
|
309
360
|
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
@@ -317,7 +368,7 @@ var require_settings = __commonJS({
|
|
|
317
368
|
if (!(key in TYPE_KEYS)) fail2(`Unknown content type setting "${key}"`);
|
|
318
369
|
if (!TYPE_KEYS[key](value, types[uid])) fail2(`Invalid value for "${key}" of "${uid}"`);
|
|
319
370
|
}
|
|
320
|
-
if (Object.keys(entry).length) out.contentTypes[uid] = structuredClone(entry);
|
|
371
|
+
if (Object.keys(entry).length) out.contentTypes[uid] = { ...structuredClone(entry), ...entry.previewDevices && { previewDevices: cleanDevices(entry.previewDevices) } };
|
|
321
372
|
}
|
|
322
373
|
const history = input.history || {};
|
|
323
374
|
if (typeof history !== "object" || Array.isArray(history)) fail2("history must be an object");
|
|
@@ -329,7 +380,7 @@ var require_settings = __commonJS({
|
|
|
329
380
|
if (out.history.eventDays < out.history.retentionDays) fail2("history.eventDays must be at least history.retentionDays");
|
|
330
381
|
return out;
|
|
331
382
|
}
|
|
332
|
-
function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null) {
|
|
383
|
+
function mergeSaved2(saved, componentUids2, contentTypeUids2 = [], historyTypes = null, schemas = null) {
|
|
333
384
|
const types = typeMap(contentTypeUids2);
|
|
334
385
|
const out = structuredClone(DEFAULTS2);
|
|
335
386
|
if (!saved || typeof saved !== "object") return out;
|
|
@@ -342,6 +393,8 @@ var require_settings = __commonJS({
|
|
|
342
393
|
if (key === "hiddenBlocks" && !HIDDEN_MODES.includes(value)) continue;
|
|
343
394
|
if (key === "previewUrl" && value && !PREVIEW_URL.test(value)) continue;
|
|
344
395
|
if (key === "blockPreviewUrl" && !blockPreviewUrl(value)) continue;
|
|
396
|
+
if (key === "previewToolbar" && !previewToolbar(value)) continue;
|
|
397
|
+
if (key === "previewDevices" && !previewDevices(value)) continue;
|
|
345
398
|
if (key === "lazyFields") {
|
|
346
399
|
if (!Array.isArray(value)) continue;
|
|
347
400
|
out.editor[key] = [...new Set(value.filter((uid) => lazyFields([uid])))].slice(0, 20);
|
|
@@ -357,6 +410,7 @@ var require_settings = __commonJS({
|
|
|
357
410
|
clean.tags = cleanTags(clean.tags);
|
|
358
411
|
if (!clean.tags.length) delete clean.tags;
|
|
359
412
|
}
|
|
413
|
+
if ("layout" in clean && !validLayout(clean.layout, schemas && (schemas[uid]?.attributes || {}))) delete clean.layout;
|
|
360
414
|
if (Object.keys(clean).length) out.components[uid] = clean;
|
|
361
415
|
}
|
|
362
416
|
for (const [uid, entry] of Object.entries(saved.contentTypes || {})) {
|
|
@@ -374,25 +428,28 @@ var require_settings = __commonJS({
|
|
|
374
428
|
return out;
|
|
375
429
|
}
|
|
376
430
|
var ENTRY_KEYS = ["components", "contentTypes"];
|
|
431
|
+
var UNSET_WHEN_EMPTY = ["previewUrl", "blockPreviewUrl"];
|
|
432
|
+
var withoutEmpty = (editor) => Object.fromEntries(Object.entries(editor).filter(([key, value]) => !(UNSET_WHEN_EMPTY.includes(key) && value === "")));
|
|
377
433
|
function layer2(base, saved) {
|
|
378
434
|
if (!base) return saved;
|
|
379
435
|
const doc = saved && typeof saved === "object" ? saved : {};
|
|
380
|
-
|
|
436
|
+
const own = (key) => doc[key] && typeof doc[key] === "object" ? key === "editor" ? withoutEmpty(doc[key]) : doc[key] : {};
|
|
437
|
+
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => [key, { ...base[key], ...own(key) }]));
|
|
381
438
|
}
|
|
382
439
|
function overrides2(next, base) {
|
|
383
440
|
if (!base) return next;
|
|
384
441
|
return Object.fromEntries(Object.keys(DEFAULTS2).map((key) => {
|
|
385
|
-
const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]));
|
|
442
|
+
const entries = Object.entries(next[key] || {}).filter(([name, value]) => JSON.stringify(value) !== JSON.stringify(base[key]?.[name]) && !(key === "editor" && UNSET_WHEN_EMPTY.includes(name) && value === ""));
|
|
386
443
|
if (ENTRY_KEYS.includes(key)) {
|
|
387
444
|
for (const uid of Object.keys(base[key] || {})) if (!(uid in (next[key] || {}))) entries.push([uid, {}]);
|
|
388
445
|
}
|
|
389
446
|
return [key, Object.fromEntries(entries)];
|
|
390
447
|
}));
|
|
391
448
|
}
|
|
392
|
-
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120 };
|
|
449
|
+
var FIELD_TEXT = { label: 80, description: 300, placeholder: 120, help: 500 };
|
|
393
450
|
var LOCALE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
394
451
|
var plain = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
395
|
-
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,
|
|
452
|
+
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));
|
|
396
453
|
function validateFields2(input, schemas = {}, stale = []) {
|
|
397
454
|
if (input === null || input === void 0) return {};
|
|
398
455
|
if (!plain(input)) fail2("fields must be an object");
|
|
@@ -416,6 +473,87 @@ var require_settings = __commonJS({
|
|
|
416
473
|
}
|
|
417
474
|
return out;
|
|
418
475
|
}
|
|
476
|
+
var VARIANT = { id: /^[a-z0-9][a-z0-9_-]{0,39}$/, perComponent: 12, bytes: 16 * 1024, total: 256 * 1024, items: 100 };
|
|
477
|
+
var text = (value) => typeof value === "string";
|
|
478
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
479
|
+
var SCALARS = {
|
|
480
|
+
string: text,
|
|
481
|
+
text,
|
|
482
|
+
richtext: text,
|
|
483
|
+
email: text,
|
|
484
|
+
uid: text,
|
|
485
|
+
date: text,
|
|
486
|
+
datetime: text,
|
|
487
|
+
time: text,
|
|
488
|
+
timestamp: text,
|
|
489
|
+
integer: Number.isInteger,
|
|
490
|
+
biginteger: (value) => Number.isInteger(value) || text(value) && /^-?\d{1,19}$/.test(value),
|
|
491
|
+
float: finite,
|
|
492
|
+
decimal: finite,
|
|
493
|
+
boolean: (value) => typeof value === "boolean",
|
|
494
|
+
json: () => true,
|
|
495
|
+
customField: () => true,
|
|
496
|
+
blocks: Array.isArray,
|
|
497
|
+
enumeration: (value, attr) => (attr.enum || []).includes(value)
|
|
498
|
+
};
|
|
499
|
+
function variantValues(values, schema, schemas, path, skipped) {
|
|
500
|
+
if (!plain(values)) throw new Error(`${path} must be an object`);
|
|
501
|
+
const out = {};
|
|
502
|
+
for (const [name, value] of Object.entries(values)) {
|
|
503
|
+
if (name === "id" || name === "__component") continue;
|
|
504
|
+
const attr = schema.attributes?.[name], at = `${path}.${name}`;
|
|
505
|
+
if (!attr) throw new Error(`${at} is not an attribute of the component`);
|
|
506
|
+
if (attr.type === "media" || attr.type === "relation") {
|
|
507
|
+
skipped.push(at);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (attr.type === "component") {
|
|
511
|
+
const nested = schemas[attr.component], max = Math.min(attr.max ?? Infinity, VARIANT.items);
|
|
512
|
+
if (!nested) throw new Error(`${at}: unknown component "${attr.component}"`);
|
|
513
|
+
if (!attr.repeatable) out[name] = value === null ? null : variantValues(value, nested, schemas, at, skipped);
|
|
514
|
+
else if (!Array.isArray(value) || value.length > max) throw new Error(`${at} must be a list of at most ${max} items`);
|
|
515
|
+
else out[name] = value.map((item, index) => variantValues(item, nested, schemas, `${at}[${index}]`, skipped));
|
|
516
|
+
} else if (value === null || SCALARS[attr.type]?.(value, attr)) out[name] = structuredClone(value);
|
|
517
|
+
else throw new Error(`${at}: ${SCALARS[attr.type] ? `not a valid ${attr.type}` : `${attr.type} values are not supported`}`);
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
function validateVariants2(config, schemas = {}, notes = []) {
|
|
522
|
+
const out = {};
|
|
523
|
+
let total = 0;
|
|
524
|
+
for (const [uid, entry] of Object.entries(plain(config) ? config : {})) {
|
|
525
|
+
if (!plain(entry) || entry.variants === void 0) continue;
|
|
526
|
+
if (!schemas[uid]) {
|
|
527
|
+
notes.push(`${uid}: unknown component`);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (!Array.isArray(entry.variants) || entry.variants.length > VARIANT.perComponent) {
|
|
531
|
+
notes.push(`${uid}: variants must be a list of at most ${VARIANT.perComponent}`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const ids = /* @__PURE__ */ new Set();
|
|
535
|
+
entry.variants.forEach((variant, index) => {
|
|
536
|
+
const name = `${uid} variant "${variant?.id ?? index}"`;
|
|
537
|
+
try {
|
|
538
|
+
if (!plain(variant) || Object.keys(variant).some((key) => !["id", "label", "values"].includes(key))) throw new Error("expected { id, label, values }");
|
|
539
|
+
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');
|
|
540
|
+
if (!fieldText(variant.label, 60)) throw new Error('label must be a string of 1 to 60 characters, or { "<locale>": string }');
|
|
541
|
+
const skipped = [];
|
|
542
|
+
const values = variantValues(variant.values ?? {}, schemas[uid], schemas, "values", skipped);
|
|
543
|
+
const size = JSON.stringify(values).length;
|
|
544
|
+
if (size > VARIANT.bytes) throw new Error(`values over ${VARIANT.bytes / 1024} KB`);
|
|
545
|
+
if (total + size > VARIANT.total) throw new Error(`all variants together over ${VARIANT.total / 1024} KB`);
|
|
546
|
+
if (skipped.length) notes.push(`${name}: media and relation values are not inserted (${skipped.slice(0, 5).join(", ")}${skipped.length > 5 ? ", ..." : ""})`);
|
|
547
|
+
ids.add(variant.id);
|
|
548
|
+
total += size;
|
|
549
|
+
(out[uid] ||= []).push({ id: variant.id, label: structuredClone(variant.label), values });
|
|
550
|
+
} catch (error) {
|
|
551
|
+
notes.push(`${name} ignored: ${error.message}`);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
419
557
|
var PREFS = { starred: 200, recent: 20 };
|
|
420
558
|
function validatePrefs2(input, componentUids2) {
|
|
421
559
|
if (!input || typeof input !== "object" || Array.isArray(input)) fail2("Preferences must be an object");
|
|
@@ -428,7 +566,7 @@ var require_settings = __commonJS({
|
|
|
428
566
|
return out;
|
|
429
567
|
}
|
|
430
568
|
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) : []]));
|
|
431
|
-
module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
|
|
569
|
+
module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, validateVariants: validateVariants2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
|
|
432
570
|
}
|
|
433
571
|
});
|
|
434
572
|
|
|
@@ -613,6 +751,453 @@ var require_diff = __commonJS({
|
|
|
613
751
|
}
|
|
614
752
|
});
|
|
615
753
|
|
|
754
|
+
// server/trash.js
|
|
755
|
+
var require_trash = __commonJS({
|
|
756
|
+
"server/trash.js"(exports2, module2) {
|
|
757
|
+
"use strict";
|
|
758
|
+
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
759
|
+
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
760
|
+
var { kept, canonical, stable } = require_diff();
|
|
761
|
+
var history = () => require_history();
|
|
762
|
+
var TRASH_UID = `plugin::${PLUGIN2}.trash`;
|
|
763
|
+
var STATUSES = ["trashed", "restored"];
|
|
764
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
765
|
+
var BATCH = 500;
|
|
766
|
+
var restoring = new AsyncLocalStorage();
|
|
767
|
+
var trashContentType2 = { schema: {
|
|
768
|
+
kind: "collectionType",
|
|
769
|
+
collectionName: "blockscene_trash",
|
|
770
|
+
info: { singularName: "trash", pluralName: "trashes", displayName: "Blockscene trash" },
|
|
771
|
+
options: { draftAndPublish: false },
|
|
772
|
+
pluginOptions: { "content-manager": { visible: false }, "content-type-builder": { visible: false }, i18n: { localized: false } },
|
|
773
|
+
attributes: {
|
|
774
|
+
contentType: { type: "string", required: true },
|
|
775
|
+
relatedDocumentId: { type: "string", required: true },
|
|
776
|
+
// The document's main field (Content Manager setting) at delete time, for the list and the search.
|
|
777
|
+
title: { type: "string" },
|
|
778
|
+
// The locales the delete removed ([null] for a type without i18n).
|
|
779
|
+
locales: { type: "json" },
|
|
780
|
+
// [{ locale, status: 'draft' | 'published' | null (no Draft & Publish), snapshot }]: canonical snapshots (server/diff.js).
|
|
781
|
+
entries: { type: "json" },
|
|
782
|
+
// Links other documents' and blocks' unidirectional relations had to the deleted rows (see `incomingRows`).
|
|
783
|
+
incoming: { type: "json" },
|
|
784
|
+
actor: { type: "string", required: true },
|
|
785
|
+
actorName: { type: "string" },
|
|
786
|
+
deletedAt: { type: "datetime", required: true },
|
|
787
|
+
// deletedAt + trash retention at delete time; the nightly purge deletes the row after it.
|
|
788
|
+
expiresAt: { type: "datetime", required: true },
|
|
789
|
+
status: { type: "enumeration", enum: STATUSES, required: true, default: "trashed" },
|
|
790
|
+
restoredAs: { type: "string" },
|
|
791
|
+
restoredAt: { type: "datetime" },
|
|
792
|
+
size: { type: "integer" }
|
|
793
|
+
},
|
|
794
|
+
indexes: [{ name: "blockscene_trash_list", columns: ["status", "content_type", "deleted_at"] }, { name: "blockscene_trash_expiry", columns: ["expires_at"] }]
|
|
795
|
+
} };
|
|
796
|
+
var expiresAt = (deletedAt, days) => new Date(new Date(deletedAt).getTime() + days * DAY);
|
|
797
|
+
var isObject = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
798
|
+
var blank = (value) => value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length;
|
|
799
|
+
var toMany = (attr) => /ToMany$|^manyWay$/.test(String(attr?.relation || ""));
|
|
800
|
+
var toOne = (attr) => ["oneToOne", "manyToOne"].includes(attr?.relation);
|
|
801
|
+
var uniqueFields = (schema) => Object.entries(schema?.attributes || {}).filter(([name, attr]) => kept(name, attr) && (attr.unique === true || attr.type === "uid")).map(([name]) => name);
|
|
802
|
+
function chooseEntries(entries) {
|
|
803
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
805
|
+
const locale = entry?.locale ?? null, current = chosen.get(locale);
|
|
806
|
+
if (!current || current.status === "published" && entry.status !== "published") chosen.set(locale, entry);
|
|
807
|
+
}
|
|
808
|
+
return chosen;
|
|
809
|
+
}
|
|
810
|
+
function restorePayload(snapshot, schema, schemaOf, { media = /* @__PURE__ */ new Set(), relation = () => false } = {}) {
|
|
811
|
+
const report = { removed: [], media: 0, relations: 0, blocks: 0 };
|
|
812
|
+
const ref = (item) => ({ documentId: item.documentId, ...item.locale && { locale: item.locale } });
|
|
813
|
+
const build = (data, schema2, top, depth) => {
|
|
814
|
+
const out = {};
|
|
815
|
+
if (!isObject(data) || depth > 20) return out;
|
|
816
|
+
for (const [name, value] of Object.entries(data)) {
|
|
817
|
+
if (name === "__component") continue;
|
|
818
|
+
const attr = schema2?.attributes?.[name];
|
|
819
|
+
const drop = () => {
|
|
820
|
+
if (top) report.removed.push(name);
|
|
821
|
+
};
|
|
822
|
+
if (!kept(name, attr)) {
|
|
823
|
+
drop();
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const list = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
827
|
+
if (attr.type === "relation") {
|
|
828
|
+
const refs = list.filter((item) => isObject(item) && typeof item.documentId === "string");
|
|
829
|
+
const found = refs.filter((item) => relation(attr.target, item));
|
|
830
|
+
report.relations += refs.length - found.length;
|
|
831
|
+
out[name] = toMany(attr) ? found.map(ref) : found[0] ? ref(found[0]) : null;
|
|
832
|
+
} else if (attr.type === "media") {
|
|
833
|
+
const ids = list.filter(Number.isInteger), found = ids.filter((id) => media.has(id));
|
|
834
|
+
report.media += ids.length - found.length;
|
|
835
|
+
out[name] = attr.multiple ? found : found[0] ?? null;
|
|
836
|
+
} else if (attr.type === "component") {
|
|
837
|
+
const child = schemaOf(attr.component);
|
|
838
|
+
if (!child) {
|
|
839
|
+
drop();
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
out[name] = attr.repeatable ? list.filter(isObject).map((item) => build(item, child, false, depth + 1)) : isObject(value) ? build(value, child, false, depth + 1) : null;
|
|
843
|
+
} else if (attr.type === "dynamiczone") {
|
|
844
|
+
const rows = list.filter((row) => isObject(row) && typeof row.__component === "string");
|
|
845
|
+
const allowed = rows.filter((row) => (attr.components || []).includes(row.__component) && schemaOf(row.__component));
|
|
846
|
+
report.blocks += rows.length - allowed.length;
|
|
847
|
+
out[name] = allowed.map((row) => ({ __component: row.__component, ...build(row, schemaOf(row.__component), false, depth + 1) }));
|
|
848
|
+
} else if (attr.type === "enumeration" && !(attr.enum || []).includes(value)) drop();
|
|
849
|
+
else out[name] = value;
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
};
|
|
853
|
+
return { data: build(snapshot, schema, true, 0), report };
|
|
854
|
+
}
|
|
855
|
+
function planRestore(entries, { schema, schemaOf, locales = null, defaultLocale = null, existing = null, singleTaken = false, taken = () => false, media, relation }) {
|
|
856
|
+
const blocking = [], warnings = [], steps = [];
|
|
857
|
+
const published = (Array.isArray(entries) ? entries : []).filter((entry) => entry?.status === "published").map((entry) => entry.locale ?? null);
|
|
858
|
+
if (published.length) warnings.push({ code: "published", locales: published });
|
|
859
|
+
if (singleTaken && !existing) blocking.push({ code: "singleExists" });
|
|
860
|
+
for (const [locale, entry] of chooseEntries(entries)) {
|
|
861
|
+
if (locales && !locales.includes(locale)) {
|
|
862
|
+
warnings.push({ code: "localeRemoved", locale });
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (existing?.includes(locale)) {
|
|
866
|
+
blocking.push({ code: "localeExists", locale });
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const { data, report } = restorePayload(entry.snapshot, schema, schemaOf, { media, relation });
|
|
870
|
+
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) });
|
|
871
|
+
const required = Object.entries(schema?.attributes || {}).filter(([name, attr]) => attr?.required && kept(name, attr) && blank(data[name])).map(([name]) => name);
|
|
872
|
+
if (required.length) warnings.push({ code: "required", locale, fields: required });
|
|
873
|
+
if (report.removed.length) warnings.push({ code: "fieldsRemoved", locale, fields: report.removed });
|
|
874
|
+
for (const code of ["media", "relations", "blocks"]) if (report[code]) warnings.push({ code, locale, count: report[code] });
|
|
875
|
+
steps.push({ locale, data });
|
|
876
|
+
}
|
|
877
|
+
if (!steps.length && !blocking.length) blocking.push({ code: "nothing" });
|
|
878
|
+
steps.sort((a, b) => (b.locale === defaultLocale) - (a.locale === defaultLocale));
|
|
879
|
+
return { blocking, warnings, steps };
|
|
880
|
+
}
|
|
881
|
+
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) })));
|
|
882
|
+
function incomingRows(source, links, deleted, owners = /* @__PURE__ */ new Map()) {
|
|
883
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = source.joinTable;
|
|
884
|
+
return links.filter((link) => deleted.has(link[inverseJoinColumn.name]) && !(source.kind === "contentType" && source.owner === source.target && deleted.has(link[joinColumn.name]))).map((link) => {
|
|
885
|
+
const target = deleted.get(link[inverseJoinColumn.name]), owner = owners.get(link[joinColumn.name]);
|
|
886
|
+
return {
|
|
887
|
+
owner: source.owner,
|
|
888
|
+
kind: source.kind,
|
|
889
|
+
field: source.field,
|
|
890
|
+
ownerId: link[joinColumn.name],
|
|
891
|
+
...source.kind === "contentType" && { ownerDocumentId: owner?.documentId ?? null, ownerLocale: owner?.locale ?? null },
|
|
892
|
+
locale: target.locale ?? null,
|
|
893
|
+
published: Boolean(target.published),
|
|
894
|
+
order: orderColumnName ? link[orderColumnName] ?? null : null
|
|
895
|
+
};
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
function previewOf(snapshot, schema, schemaOf) {
|
|
899
|
+
const fields = [], blocks = [];
|
|
900
|
+
const text = (value) => {
|
|
901
|
+
const s = typeof value === "string" ? value : stable(value);
|
|
902
|
+
return s.length > 200 ? `${s.slice(0, 199)}\u2026` : s;
|
|
903
|
+
};
|
|
904
|
+
for (const [name, value] of Object.entries(isObject(snapshot) ? snapshot : {})) {
|
|
905
|
+
const attr = schema?.attributes?.[name];
|
|
906
|
+
if (attr?.type === "dynamiczone") {
|
|
907
|
+
for (const row of Array.isArray(value) ? value : []) blocks.push({ zone: name, uid: row?.__component, name: schemaOf(row?.__component)?.info?.displayName || row?.__component });
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const count = Array.isArray(value) ? value.length : 1;
|
|
911
|
+
fields.push({ name, type: attr?.type || "removed", ...["relation", "media", "component"].includes(attr?.type) ? { count } : { value: text(value) } });
|
|
912
|
+
}
|
|
913
|
+
return { fields, blocks };
|
|
914
|
+
}
|
|
915
|
+
function trashService2({ strapi }) {
|
|
916
|
+
const events = () => strapi.plugin(PLUGIN2).service("history");
|
|
917
|
+
const rows = () => strapi.db.query(TRASH_UID);
|
|
918
|
+
const schemaOf = (uid) => uid && (strapi.getModel?.(uid) || strapi.components?.[uid] || strapi.contentTypes?.[uid]);
|
|
919
|
+
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
920
|
+
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
921
|
+
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);
|
|
922
|
+
const table = (trx, name) => strapi.db.getConnection(name).transacting(trx);
|
|
923
|
+
const lookups = async (uid, snapshots) => {
|
|
924
|
+
const ids = /* @__PURE__ */ new Set(), refs = [];
|
|
925
|
+
for (const snapshot of snapshots) {
|
|
926
|
+
const found = history().refsOf(snapshot, schemaOf(uid), schemaOf);
|
|
927
|
+
found.media.forEach((id) => ids.add(id));
|
|
928
|
+
refs.push(...found.relations);
|
|
929
|
+
}
|
|
930
|
+
const files = ids.size ? await strapi.db.query("plugin::upload.file").findMany({ select: ["id"], where: { id: { $in: [...ids] } } }) : [];
|
|
931
|
+
const present = /* @__PURE__ */ new Set();
|
|
932
|
+
for (const target of [...new Set(refs.map((ref) => ref.target))]) {
|
|
933
|
+
if (!strapi.contentTypes?.[target]) continue;
|
|
934
|
+
const found = await strapi.db.query(target).findMany({ select: ["documentId", "locale"], where: {
|
|
935
|
+
documentId: { $in: [...new Set(refs.filter((ref) => ref.target === target).map((ref) => ref.documentId))] },
|
|
936
|
+
...draftAndPublish(target) && { publishedAt: { $null: true } }
|
|
937
|
+
} });
|
|
938
|
+
for (const row of found) {
|
|
939
|
+
present.add(`${target}|${row.documentId}|${row.locale || ""}`);
|
|
940
|
+
present.add(`${target}|${row.documentId}|*`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return { media: new Set(files.map((file) => file.id)), relation: (target, ref) => present.has(`${target}|${ref.documentId}|${localized(target) && ref.locale ? ref.locale : "*"}`) };
|
|
944
|
+
};
|
|
945
|
+
const resolveIncoming = async (uid, incoming, locales, trx) => {
|
|
946
|
+
const out = { links: [], gone: 0, published: 0, replaced: 0 };
|
|
947
|
+
for (const link of Array.isArray(incoming) ? incoming : []) {
|
|
948
|
+
if (!locales.includes(link.locale)) continue;
|
|
949
|
+
if (link.published) {
|
|
950
|
+
out.published++;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const attr = strapi.db.metadata.get(link.owner)?.attributes?.[link.field];
|
|
954
|
+
if (!attr?.joinTable || attr.target !== uid) {
|
|
955
|
+
out.gone++;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
let ownerId = null;
|
|
959
|
+
if (link.kind === "component") ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: { id: link.ownerId } }))?.id ?? null;
|
|
960
|
+
else if (strapi.contentTypes?.[link.owner] && link.ownerDocumentId) ownerId = (await strapi.db.query(link.owner).findOne({ select: ["id"], where: {
|
|
961
|
+
documentId: link.ownerDocumentId,
|
|
962
|
+
...localized(link.owner) && { locale: link.ownerLocale },
|
|
963
|
+
...draftAndPublish(link.owner) && { publishedAt: { $null: true } }
|
|
964
|
+
} }))?.id ?? null;
|
|
965
|
+
if (!ownerId) {
|
|
966
|
+
out.gone++;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const { joinTable } = attr;
|
|
970
|
+
if (toOne(attr) && await table(trx, joinTable.name).where(joinTable.joinColumn.name, ownerId).first()) {
|
|
971
|
+
out.replaced++;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
out.links.push({ ...link, ownerRowId: ownerId, joinTable });
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
};
|
|
978
|
+
const self = {
|
|
979
|
+
TRASH_UID,
|
|
980
|
+
// Before the delete, in its transaction: the links other documents and blocks have to the rows about to be deleted.
|
|
981
|
+
async incoming(uid, deleted) {
|
|
982
|
+
if (!deleted.size) return [];
|
|
983
|
+
return strapi.db.transaction(async ({ trx }) => {
|
|
984
|
+
const out = [];
|
|
985
|
+
for (const source of incomingSources(uid, models())) {
|
|
986
|
+
const links = await table(trx, source.joinTable.name).select("*").whereIn(source.joinTable.inverseJoinColumn.name, [...deleted.keys()]);
|
|
987
|
+
if (!links.length) continue;
|
|
988
|
+
const owners = source.kind === "contentType" && strapi.contentTypes?.[source.owner] ? new Map((await strapi.db.query(source.owner).findMany({
|
|
989
|
+
select: ["id", "documentId", "locale"],
|
|
990
|
+
where: { id: { $in: [...new Set(links.map((link) => link[source.joinTable.joinColumn.name]))] } }
|
|
991
|
+
})).map((row) => [row.id, row])) : /* @__PURE__ */ new Map();
|
|
992
|
+
out.push(...incomingRows({ ...source, target: uid }, links, deleted, owners));
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
});
|
|
996
|
+
},
|
|
997
|
+
// After the delete, in its transaction: the trash row. drafts/published: the stored rows read before the delete.
|
|
998
|
+
async record({ uid, documentId, locales, drafts, published, incoming, actor, days }) {
|
|
999
|
+
const dp = draftAndPublish(uid);
|
|
1000
|
+
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) }));
|
|
1001
|
+
const main = await events().mainField(uid);
|
|
1002
|
+
const first = chooseEntries(entries);
|
|
1003
|
+
const source = first.get(await events().scope(uid).then((list) => list?.[0] ?? null).catch(() => null)) || [...first.values()][0];
|
|
1004
|
+
const title = main && source?.snapshot?.[main] != null ? String(source.snapshot[main]).slice(0, 255) : null;
|
|
1005
|
+
const deletedAt = /* @__PURE__ */ new Date();
|
|
1006
|
+
return rows().create({ data: {
|
|
1007
|
+
contentType: uid,
|
|
1008
|
+
relatedDocumentId: documentId,
|
|
1009
|
+
title,
|
|
1010
|
+
locales: locales || [...new Set(entries.map((entry) => entry.locale))],
|
|
1011
|
+
entries,
|
|
1012
|
+
incoming,
|
|
1013
|
+
...actor,
|
|
1014
|
+
deletedAt,
|
|
1015
|
+
expiresAt: expiresAt(deletedAt, days),
|
|
1016
|
+
status: "trashed",
|
|
1017
|
+
size: Buffer.byteLength(stable({ entries, incoming }))
|
|
1018
|
+
} });
|
|
1019
|
+
},
|
|
1020
|
+
// One page of trash rows, without their snapshots. `types`: the content types the user may read.
|
|
1021
|
+
async list({ contentType, status = "trashed", page = 1, pageSize = 20, types }) {
|
|
1022
|
+
const where = { status, contentType: contentType ? contentType : { $in: types } };
|
|
1023
|
+
if (contentType && !types.includes(contentType)) return { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 } };
|
|
1024
|
+
const [results, total] = await Promise.all([
|
|
1025
|
+
rows().findMany({
|
|
1026
|
+
select: ["id", "contentType", "relatedDocumentId", "title", "locales", "actor", "actorName", "deletedAt", "expiresAt", "status", "restoredAs", "restoredAt"],
|
|
1027
|
+
where,
|
|
1028
|
+
orderBy: [{ deletedAt: "desc" }, { id: "desc" }],
|
|
1029
|
+
offset: (page - 1) * pageSize,
|
|
1030
|
+
limit: pageSize
|
|
1031
|
+
}),
|
|
1032
|
+
rows().count({ where })
|
|
1033
|
+
]);
|
|
1034
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) } };
|
|
1035
|
+
},
|
|
1036
|
+
async find(id) {
|
|
1037
|
+
return rows().findOne({ where: { id } });
|
|
1038
|
+
},
|
|
1039
|
+
// One row for the preview: fields and blocks of each entry, and how many links it held.
|
|
1040
|
+
preview(row) {
|
|
1041
|
+
const schema = schemaOf(row.contentType);
|
|
1042
|
+
const { entries, incoming, ...meta } = row;
|
|
1043
|
+
return {
|
|
1044
|
+
...meta,
|
|
1045
|
+
incoming: (incoming || []).length,
|
|
1046
|
+
entries: (entries || []).map((entry) => ({ locale: entry.locale ?? null, status: entry.status ?? null, ...previewOf(entry.snapshot, schema, schemaOf) }))
|
|
1047
|
+
};
|
|
1048
|
+
},
|
|
1049
|
+
// The pre-check: every lookup a restore needs, then planRestore. Read-only; the restore runs it again in its transaction.
|
|
1050
|
+
async plan(row) {
|
|
1051
|
+
const uid = row.contentType, schema = schemaOf(uid);
|
|
1052
|
+
if (!schema || !strapi.contentTypes?.[uid]) return { blocking: [{ code: "typeGone" }], warnings: [], steps: [], incoming: { links: [], gone: 0, published: 0, replaced: 0 } };
|
|
1053
|
+
const i18n = localized(uid);
|
|
1054
|
+
const locales = i18n ? (await strapi.plugin("i18n").service("locales").find()).map((locale) => locale.code) : null;
|
|
1055
|
+
const defaultLocale = i18n ? await strapi.plugin("i18n").service("locales").getDefaultLocale() : null;
|
|
1056
|
+
const current = await strapi.db.query(uid).findMany({ select: ["locale"], where: { documentId: row.relatedDocumentId } });
|
|
1057
|
+
const existing = current.length ? [...new Set(current.map((entry) => i18n ? entry.locale : null))] : null;
|
|
1058
|
+
const singleTaken = schema.kind === "singleType" && !existing && Boolean(await strapi.db.query(uid).findOne({ select: ["id"] }));
|
|
1059
|
+
const chosen = [...chooseEntries(row.entries).values()];
|
|
1060
|
+
const { media, relation } = await lookups(uid, chosen.map((entry) => entry.snapshot));
|
|
1061
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1062
|
+
for (const entry of chosen) for (const field of uniqueFields(schema)) {
|
|
1063
|
+
const value = entry.snapshot?.[field];
|
|
1064
|
+
if (blank(value) || typeof value === "object") continue;
|
|
1065
|
+
const other = await strapi.db.query(uid).findOne({ select: ["id"], where: { [field]: value, ...i18n && { locale: entry.locale }, ...existing && { documentId: { $ne: row.relatedDocumentId } } } });
|
|
1066
|
+
if (other) taken.add(stable([field, entry.locale ?? null, value]));
|
|
1067
|
+
}
|
|
1068
|
+
const plan = planRestore(row.entries, { schema, schemaOf, locales, defaultLocale, existing, singleTaken, media, relation, taken: (field, locale, value) => taken.has(stable([field, locale ?? null, value])) });
|
|
1069
|
+
const incoming = await strapi.db.transaction(({ trx }) => resolveIncoming(uid, row.incoming, plan.steps.map((step) => step.locale), trx));
|
|
1070
|
+
return { ...plan, documentId: existing ? row.relatedDocumentId : null, incoming };
|
|
1071
|
+
},
|
|
1072
|
+
// What the pre-check endpoint returns: the plan without the data it would write.
|
|
1073
|
+
async check(row) {
|
|
1074
|
+
if (row.status !== "trashed") return { status: row.status, restoredAs: row.restoredAs, blocking: [{ code: "restored" }], warnings: [] };
|
|
1075
|
+
const { steps, incoming: { links, ...incoming }, ...plan } = await self.plan(row);
|
|
1076
|
+
return { ...plan, locales: steps.map((step) => step.locale), incoming: { ...incoming, reconnect: links.length } };
|
|
1077
|
+
},
|
|
1078
|
+
// Restore as a draft, in one transaction: claim the row (a second restore of the same row, concurrent or later, gets
|
|
1079
|
+
// `already` with the first one's document), check again, recreate (default locale first, the others under the same
|
|
1080
|
+
// documentId), put the links back, record `restore` events. A blocking problem throws `conflict` and nothing is written.
|
|
1081
|
+
async restore(id, { actor, userId }) {
|
|
1082
|
+
return restoring.run(true, () => strapi.db.transaction(async ({ trx }) => {
|
|
1083
|
+
const claimed = await rows().updateMany({ where: { id, status: "trashed" }, data: { status: "restored", restoredAt: /* @__PURE__ */ new Date() } });
|
|
1084
|
+
const row = await rows().findOne({ where: { id } });
|
|
1085
|
+
if (!row) return null;
|
|
1086
|
+
if (!claimed?.count) return { already: true, documentId: row.restoredAs || null };
|
|
1087
|
+
const plan = await self.plan(row);
|
|
1088
|
+
if (plan.blocking.length) {
|
|
1089
|
+
const error = new Error("conflict");
|
|
1090
|
+
error.conflict = { blocking: plan.blocking, warnings: plan.warnings };
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
const uid = row.contentType, documents = strapi.documents(uid);
|
|
1094
|
+
const creator = userId ? { createdBy: userId, updatedBy: userId } : {};
|
|
1095
|
+
let documentId = plan.documentId;
|
|
1096
|
+
for (const { locale, data } of plan.steps) {
|
|
1097
|
+
const params = { ...locale && { locale }, data: { ...data, ...creator } };
|
|
1098
|
+
if (documentId) await documents.update({ documentId, ...params });
|
|
1099
|
+
else documentId = (await documents.create(params)).documentId;
|
|
1100
|
+
}
|
|
1101
|
+
const restored = await events().rows(uid, documentId, localized(uid) ? plan.steps.map((step) => step.locale) : null);
|
|
1102
|
+
const rowOf = new Map(restored.map((entry) => [entry.locale ?? null, entry]));
|
|
1103
|
+
for (const link of plan.incoming.links) {
|
|
1104
|
+
const target = rowOf.get(link.locale);
|
|
1105
|
+
if (!target) continue;
|
|
1106
|
+
const { joinColumn, inverseJoinColumn, orderColumnName } = link.joinTable;
|
|
1107
|
+
await table(trx, link.joinTable.name).insert({ [joinColumn.name]: link.ownerRowId, [inverseJoinColumn.name]: target.id, ...orderColumnName && link.order != null && { [orderColumnName]: link.order } });
|
|
1108
|
+
}
|
|
1109
|
+
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 } });
|
|
1110
|
+
await rows().update({ where: { id }, data: { restoredAs: documentId } });
|
|
1111
|
+
const { links, ...incoming } = plan.incoming;
|
|
1112
|
+
return { documentId, locales: plan.steps.map((step) => step.locale), warnings: plan.warnings, incoming: { ...incoming, reconnected: links.length } };
|
|
1113
|
+
}));
|
|
1114
|
+
},
|
|
1115
|
+
// Delete forever (one row), recorded as a `purge` event. Never touches media files.
|
|
1116
|
+
async remove(row, actor) {
|
|
1117
|
+
return strapi.db.transaction(async () => {
|
|
1118
|
+
const { count } = await rows().deleteMany({ where: { id: row.id } });
|
|
1119
|
+
if (count) await events().record({ uid: row.contentType, documentId: row.relatedDocumentId, action: "purge", actor, extra: { trash: row.id, title: row.title } });
|
|
1120
|
+
return count;
|
|
1121
|
+
});
|
|
1122
|
+
},
|
|
1123
|
+
// Nightly, in batches, idempotent: rows past expiresAt are deleted (restored ones too). Never touches media files.
|
|
1124
|
+
async purge(now = Date.now()) {
|
|
1125
|
+
let count = 0;
|
|
1126
|
+
for (; ; ) {
|
|
1127
|
+
const ids = (await rows().findMany({ select: ["id"], where: { expiresAt: { $lt: new Date(now) } }, orderBy: { id: "asc" }, limit: BATCH })).map((row) => row.id);
|
|
1128
|
+
if (ids.length) await rows().deleteMany({ where: { id: { $in: ids } } });
|
|
1129
|
+
count += ids.length;
|
|
1130
|
+
if (ids.length < BATCH) return count;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
return self;
|
|
1135
|
+
}
|
|
1136
|
+
var ID = /^[1-9]\d{0,15}$/;
|
|
1137
|
+
var API_UID = /^api::[\w-]+\.[\w-]+$/;
|
|
1138
|
+
var int = (value, fallback, max) => {
|
|
1139
|
+
if (value === void 0 || value === "") return fallback;
|
|
1140
|
+
const n = Number(value);
|
|
1141
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1142
|
+
};
|
|
1143
|
+
var trashController2 = ({ strapi }) => {
|
|
1144
|
+
const service = () => strapi.plugin(PLUGIN2).service("trash");
|
|
1145
|
+
const { can, readableTypes, actorOf } = history();
|
|
1146
|
+
const load = async (ctx, action = "read") => {
|
|
1147
|
+
if (!ID.test(String(ctx.params?.id || ""))) {
|
|
1148
|
+
ctx.badRequest("Invalid trash entry");
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
const row = await service().find(Number(ctx.params.id));
|
|
1152
|
+
if (!row || !can(strapi, ctx, row.contentType, "read") || action !== "read" && !can(strapi, ctx, row.contentType, action)) {
|
|
1153
|
+
ctx.notFound();
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
return row;
|
|
1157
|
+
};
|
|
1158
|
+
return {
|
|
1159
|
+
async list(ctx) {
|
|
1160
|
+
const { contentType = "", status = "trashed" } = ctx.query || {};
|
|
1161
|
+
const page = int(ctx.query?.page, 1, 1e4), pageSize = int(ctx.query?.pageSize, 20, 100);
|
|
1162
|
+
if (typeof contentType !== "string" || contentType && !API_UID.test(contentType) || !STATUSES.includes(status) || Number.isNaN(page) || Number.isNaN(pageSize)) return ctx.badRequest("Invalid filters");
|
|
1163
|
+
const types = readableTypes(strapi, ctx);
|
|
1164
|
+
ctx.body = {
|
|
1165
|
+
...await service().list({ contentType, status, page, pageSize, types }),
|
|
1166
|
+
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) }))
|
|
1167
|
+
};
|
|
1168
|
+
},
|
|
1169
|
+
async find(ctx) {
|
|
1170
|
+
const row = await load(ctx);
|
|
1171
|
+
if (row) ctx.body = service().preview(row);
|
|
1172
|
+
},
|
|
1173
|
+
async check(ctx) {
|
|
1174
|
+
const row = await load(ctx, "create");
|
|
1175
|
+
if (row) ctx.body = await service().check(row);
|
|
1176
|
+
},
|
|
1177
|
+
async restore(ctx) {
|
|
1178
|
+
const row = await load(ctx, "create");
|
|
1179
|
+
if (!row) return;
|
|
1180
|
+
try {
|
|
1181
|
+
const result = await service().restore(row.id, { actor: actorOf(strapi), userId: ctx.state?.user?.id });
|
|
1182
|
+
if (!result) return ctx.notFound();
|
|
1183
|
+
ctx.body = result;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
if (!error?.conflict) throw error;
|
|
1186
|
+
ctx.status = 409;
|
|
1187
|
+
ctx.body = { data: null, error: { status: 409, name: "ConflictError", message: "The document cannot be restored as it is: nothing was written.", details: error.conflict } };
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
async remove(ctx) {
|
|
1191
|
+
const row = await load(ctx, "delete");
|
|
1192
|
+
if (!row) return;
|
|
1193
|
+
ctx.body = { deleted: await service().remove(row, actorOf(strapi)) };
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
};
|
|
1197
|
+
module2.exports = { TRASH_UID, STATUSES, restoring, trashContentType: trashContentType2, expiresAt, uniqueFields, chooseEntries, restorePayload, planRestore, incomingSources, incomingRows, previewOf, trashService: trashService2, trashController: trashController2 };
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
616
1201
|
// server/history.js
|
|
617
1202
|
var require_history = __commonJS({
|
|
618
1203
|
"server/history.js"(exports2, module2) {
|
|
@@ -620,13 +1205,16 @@ var require_history = __commonJS({
|
|
|
620
1205
|
var { createHash } = require("node:crypto");
|
|
621
1206
|
var { PLUGIN: PLUGIN2 } = require_settings();
|
|
622
1207
|
var { snapshotPopulate, canonical, stable, summarize } = require_diff();
|
|
1208
|
+
var { TRASH_UID, restoring } = require_trash();
|
|
623
1209
|
var EVENT_UID = `plugin::${PLUGIN2}.event`;
|
|
624
|
-
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore"];
|
|
1210
|
+
var ACTIONS = ["create", "update", "publish", "unpublish", "discard", "delete", "restore", "purge"];
|
|
625
1211
|
var CAPTURED = { create: "create", clone: "create", update: "update", publish: "publish", unpublish: "unpublish", discardDraft: "discard", delete: "delete" };
|
|
626
1212
|
var CRON2 = "blocksceneHistoryPurge";
|
|
627
1213
|
var BATCH = 500;
|
|
628
1214
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
629
1215
|
var LIST_LIMIT = 100;
|
|
1216
|
+
var ACTORS_LIMIT = 500;
|
|
1217
|
+
var SEARCH_LIMIT = 200;
|
|
630
1218
|
var eventContentType2 = { schema: {
|
|
631
1219
|
kind: "collectionType",
|
|
632
1220
|
collectionName: "blockscene_events",
|
|
@@ -642,7 +1230,8 @@ var require_history = __commonJS({
|
|
|
642
1230
|
actor: { type: "string", required: true },
|
|
643
1231
|
actorName: { type: "string" },
|
|
644
1232
|
at: { type: "datetime", required: true },
|
|
645
|
-
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed
|
|
1233
|
+
// { fields: [names], blocks: { added, removed, changed }, initial? } vs the previous snapshot; { missing: true } when the capture failed;
|
|
1234
|
+
// restore: + { restoredFrom, trash }; purge: { trash, title }.
|
|
646
1235
|
summary: { type: "json" },
|
|
647
1236
|
// Null when identical to the previous snapshot of the same locale (same hash) or purged.
|
|
648
1237
|
snapshot: { type: "json" },
|
|
@@ -705,6 +1294,11 @@ var require_history = __commonJS({
|
|
|
705
1294
|
const localized = (uid) => Boolean(schemaOf(uid)?.pluginOptions?.i18n?.localized);
|
|
706
1295
|
const draftAndPublish = (uid) => schemaOf(uid)?.options?.draftAndPublish === true;
|
|
707
1296
|
const events = () => strapi.db.query(EVENT_UID);
|
|
1297
|
+
const actors = async () => {
|
|
1298
|
+
const meta = strapi.db.metadata.get(EVENT_UID);
|
|
1299
|
+
const column = (name) => meta.attributes[name].columnName || name;
|
|
1300
|
+
return strapi.db.connection(meta.tableName).select({ actor: column("actor") }).max({ actorName: column("actorName") }).groupBy(column("actor")).orderBy(column("actor")).limit(ACTORS_LIMIT);
|
|
1301
|
+
};
|
|
708
1302
|
const scope = async (uid, locale) => {
|
|
709
1303
|
if (!localized(uid)) return [null];
|
|
710
1304
|
if (locale === "*") return null;
|
|
@@ -727,7 +1321,7 @@ var require_history = __commonJS({
|
|
|
727
1321
|
},
|
|
728
1322
|
covers: covers2,
|
|
729
1323
|
// One event. The snapshot is stored unless identical to the latest stored one of the same document and locale.
|
|
730
|
-
async record({ uid, documentId, locale = null, action, entry, actor, missing = false }) {
|
|
1324
|
+
async record({ uid, documentId, locale = null, action, entry, actor, missing = false, extra = null }) {
|
|
731
1325
|
let summary = missing ? { missing: true } : null, snapshot = null, hash = null, size = 0;
|
|
732
1326
|
if (entry) {
|
|
733
1327
|
const content = canonical(entry, schemaOf(uid), schemaOf);
|
|
@@ -740,6 +1334,7 @@ var require_history = __commonJS({
|
|
|
740
1334
|
size = Buffer.byteLength(text);
|
|
741
1335
|
}
|
|
742
1336
|
}
|
|
1337
|
+
if (extra) summary = { ...summary, ...extra };
|
|
743
1338
|
return events().create({ data: { contentType: uid, relatedDocumentId: documentId, locale, action, ...actor, at: /* @__PURE__ */ new Date(), summary, snapshot, hash, size } });
|
|
744
1339
|
},
|
|
745
1340
|
// After a save or publish: one event per affected entry (drafts, or the published versions after a publish).
|
|
@@ -749,6 +1344,16 @@ var require_history = __commonJS({
|
|
|
749
1344
|
},
|
|
750
1345
|
scope,
|
|
751
1346
|
rows,
|
|
1347
|
+
// The Content Manager's main field of a content type when it is a plain value (what list views show), else null.
|
|
1348
|
+
async mainField(uid) {
|
|
1349
|
+
let name = null;
|
|
1350
|
+
try {
|
|
1351
|
+
name = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[uid]))?.settings?.mainField;
|
|
1352
|
+
} catch {
|
|
1353
|
+
name = null;
|
|
1354
|
+
}
|
|
1355
|
+
return name && name !== "id" && schemaOf(uid)?.attributes?.[name] && !["relation", "media", "component", "dynamiczone", "password", "json", "blocks"].includes(schemaOf(uid).attributes[name].type) ? name : null;
|
|
1356
|
+
},
|
|
752
1357
|
// Latest events of one document and locale, newest first; `stored`: its snapshot (or an identical one) can be loaded.
|
|
753
1358
|
async list({ uid, documentId, locale }) {
|
|
754
1359
|
const where = { contentType: uid, relatedDocumentId: documentId, locale: locale || null };
|
|
@@ -772,13 +1377,7 @@ var require_history = __commonJS({
|
|
|
772
1377
|
const relations = {};
|
|
773
1378
|
for (const target of [...new Set(refs.relations.map((ref) => ref.target))]) {
|
|
774
1379
|
if (!strapi.contentTypes?.[target]) continue;
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
mainField = (await strapi.plugin("content-manager").service("content-types").findConfiguration(strapi.contentTypes[target]))?.settings?.mainField;
|
|
778
|
-
} catch {
|
|
779
|
-
mainField = null;
|
|
780
|
-
}
|
|
781
|
-
const scalar = mainField && !["relation", "media", "component", "dynamiczone", "password"].includes(schemaOf(target)?.attributes?.[mainField]?.type) && mainField !== "id";
|
|
1380
|
+
const mainField = await self.mainField(target), scalar = Boolean(mainField);
|
|
782
1381
|
const found = await strapi.db.query(target).findMany({
|
|
783
1382
|
select: ["id", "documentId", "locale", ...scalar ? [mainField] : []],
|
|
784
1383
|
where: { documentId: { $in: [...new Set(refs.relations.filter((ref) => ref.target === target).map((ref) => ref.documentId))] }, ...draftAndPublish(target) && { publishedAt: { $null: true } } }
|
|
@@ -787,6 +1386,56 @@ var require_history = __commonJS({
|
|
|
787
1386
|
}
|
|
788
1387
|
return { event: meta, snapshot, media: Object.fromEntries(files.map((file) => [file.id, file])), relations };
|
|
789
1388
|
},
|
|
1389
|
+
// Content activity: one page of events (no snapshots), newest first, with each document's title and whether it still
|
|
1390
|
+
// exists (the list links to it). `types`: the content types the user may read. `q`: documents whose main field
|
|
1391
|
+
// contains it now, or whose trash entry's title does (at most SEARCH_LIMIT per content type).
|
|
1392
|
+
async activity({ actor, contentType, action, from, to, q, page, pageSize, types }) {
|
|
1393
|
+
const empty = { results: [], pagination: { page, pageSize, total: 0, pageCount: 0 }, actors: [] };
|
|
1394
|
+
if (contentType && !types.includes(contentType)) return empty;
|
|
1395
|
+
const where = {
|
|
1396
|
+
contentType: contentType || { $in: types },
|
|
1397
|
+
...actor && { actor },
|
|
1398
|
+
...action && { action },
|
|
1399
|
+
...(from || to) && { at: { ...from && { $gte: from }, ...to && { $lte: to } } }
|
|
1400
|
+
};
|
|
1401
|
+
if (q) {
|
|
1402
|
+
const matches = [];
|
|
1403
|
+
for (const uid of contentType ? [contentType] : types) {
|
|
1404
|
+
const main = await self.mainField(uid);
|
|
1405
|
+
const found = new Set(main ? (await strapi.db.query(uid).findMany({ select: ["documentId"], where: { [main]: { $containsi: q } }, limit: SEARCH_LIMIT })).map((row) => row.documentId) : []);
|
|
1406
|
+
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);
|
|
1407
|
+
if (found.size) matches.push({ contentType: uid, relatedDocumentId: { $in: [...found] } });
|
|
1408
|
+
}
|
|
1409
|
+
if (!matches.length) return { ...empty, actors: await actors() };
|
|
1410
|
+
where.$or = matches;
|
|
1411
|
+
}
|
|
1412
|
+
const [list, total] = await Promise.all([
|
|
1413
|
+
events().findMany({ select: ["id", "contentType", "relatedDocumentId", "locale", "action", "actor", "actorName", "at", "summary"], where, orderBy: [{ at: "desc" }, { id: "desc" }], offset: (page - 1) * pageSize, limit: pageSize }),
|
|
1414
|
+
events().count({ where })
|
|
1415
|
+
]);
|
|
1416
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const uid of [...new Set(list.map((event) => event.contentType))]) {
|
|
1418
|
+
const ids = [...new Set(list.filter((event) => event.contentType === uid).map((event) => event.relatedDocumentId))];
|
|
1419
|
+
const main = strapi.contentTypes?.[uid] ? await self.mainField(uid) : null;
|
|
1420
|
+
const found = strapi.contentTypes?.[uid] ? await strapi.db.query(uid).findMany({ select: ["documentId", "locale", ...main ? [main] : []], where: { documentId: { $in: ids } } }) : [];
|
|
1421
|
+
for (const row of found) {
|
|
1422
|
+
const key = `${uid}|${row.documentId}`;
|
|
1423
|
+
docs.set(key, [...docs.get(key) || [], { locale: row.locale ?? null, title: main && row[main] != null ? String(row[main]) : null }]);
|
|
1424
|
+
}
|
|
1425
|
+
const gone = ids.filter((id) => !docs.has(`${uid}|${id}`));
|
|
1426
|
+
if (gone.length) {
|
|
1427
|
+
for (const row of await strapi.db.query(TRASH_UID).findMany({ select: ["relatedDocumentId", "title"], where: { contentType: uid, relatedDocumentId: { $in: gone } }, orderBy: { id: "desc" } }))
|
|
1428
|
+
if (!docs.has(`trash|${uid}|${row.relatedDocumentId}`)) docs.set(`trash|${uid}|${row.relatedDocumentId}`, row.title);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const results = list.map((event) => {
|
|
1432
|
+
const rows2 = docs.get(`${event.contentType}|${event.relatedDocumentId}`) || [];
|
|
1433
|
+
const own = rows2.find((row) => row.locale === (event.locale ?? null));
|
|
1434
|
+
if (event.action === "purge") return { ...event, exists: false, title: event.summary?.title ?? null };
|
|
1435
|
+
return { ...event, exists: Boolean(own), title: (own || rows2[0])?.title ?? docs.get(`trash|${event.contentType}|${event.relatedDocumentId}`) ?? null };
|
|
1436
|
+
});
|
|
1437
|
+
return { results, pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) }, actors: await actors() };
|
|
1438
|
+
},
|
|
790
1439
|
// Nightly, in batches, idempotent (two instances running it is harmless): events past eventDays are deleted;
|
|
791
1440
|
// snapshots past retentionDays, or beyond maxSnapshots per document (newest kept), are emptied. Never touches media.
|
|
792
1441
|
async purge(now = Date.now()) {
|
|
@@ -823,16 +1472,21 @@ var require_history = __commonJS({
|
|
|
823
1472
|
const warn = (message, error) => strapi.log.error(`[${PLUGIN2}] ${message}: ${error?.message || error}`);
|
|
824
1473
|
strapi.documents.use(async (ctx, next) => {
|
|
825
1474
|
const action = CAPTURED[ctx.action];
|
|
826
|
-
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true) return next();
|
|
1475
|
+
if (!action || !String(ctx.uid || "").startsWith("api::") || strapi.plugin(PLUGIN2).config("disabled") === true || restoring.getStore()) return next();
|
|
827
1476
|
const history = service();
|
|
828
1477
|
const params = ctx.params || {};
|
|
829
1478
|
if (action === "delete") {
|
|
830
|
-
|
|
1479
|
+
const config = params.documentId ? await history.config() : null;
|
|
1480
|
+
if (!config || !history.covers(config, ctx.uid)) return next();
|
|
831
1481
|
return strapi.db.transaction(async () => {
|
|
832
|
-
const actor2 = actorOf(strapi);
|
|
833
|
-
|
|
1482
|
+
const actor2 = actorOf(strapi), trash = strapi.plugin(PLUGIN2).service("trash");
|
|
1483
|
+
const published = strapi.getModel(ctx.uid)?.options?.draftAndPublish === true;
|
|
1484
|
+
let locales2, entries, live, incoming;
|
|
834
1485
|
try {
|
|
835
|
-
|
|
1486
|
+
locales2 = await history.scope(ctx.uid, params.locale);
|
|
1487
|
+
entries = await history.rows(ctx.uid, params.documentId, locales2);
|
|
1488
|
+
live = published ? await history.rows(ctx.uid, params.documentId, locales2, true) : [];
|
|
1489
|
+
incoming = await trash.incoming(ctx.uid, new Map([...entries.map((row) => [row.id, { locale: row.locale, published: false }]), ...live.map((row) => [row.id, { locale: row.locale, published: true }])]));
|
|
836
1490
|
} catch (error) {
|
|
837
1491
|
warn("reading the document before a delete failed", error);
|
|
838
1492
|
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
@@ -840,6 +1494,7 @@ var require_history = __commonJS({
|
|
|
840
1494
|
const result2 = await next();
|
|
841
1495
|
try {
|
|
842
1496
|
for (const entry of entries) await history.record({ uid: ctx.uid, documentId: params.documentId, locale: entry.locale ?? null, action, entry, actor: actor2 });
|
|
1497
|
+
if (entries.length || live.length) await trash.record({ uid: ctx.uid, documentId: params.documentId, locales: locales2, drafts: entries, published: live, incoming, actor: actor2, days: config.trashDays });
|
|
843
1498
|
} catch (error) {
|
|
844
1499
|
warn("recording a delete failed, the delete was rolled back", error);
|
|
845
1500
|
fail2(`The version history could not record this delete, so nothing was deleted (${error.message}).`);
|
|
@@ -885,17 +1540,36 @@ var require_history = __commonJS({
|
|
|
885
1540
|
} catch (error) {
|
|
886
1541
|
strapi.log.error(`[${PLUGIN2}] history purge failed: ${error?.message || error}`);
|
|
887
1542
|
}
|
|
1543
|
+
try {
|
|
1544
|
+
const count = await strapi.plugin(PLUGIN2).service("trash").purge();
|
|
1545
|
+
if (count) strapi.log.info(`[${PLUGIN2}] trash purge: ${count} expired entries deleted`);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
strapi.log.error(`[${PLUGIN2}] trash purge failed: ${error?.message || error}`);
|
|
1548
|
+
}
|
|
888
1549
|
} } });
|
|
889
1550
|
}
|
|
890
1551
|
var DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
891
1552
|
var LOCALE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
892
|
-
var
|
|
1553
|
+
var can = (strapi, ctx, uid, action = "read") => {
|
|
893
1554
|
try {
|
|
894
|
-
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot
|
|
1555
|
+
return !strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: uid }).cannot[action]();
|
|
895
1556
|
} catch {
|
|
896
1557
|
return false;
|
|
897
1558
|
}
|
|
898
1559
|
};
|
|
1560
|
+
var canRead = (strapi, ctx, uid) => can(strapi, ctx, uid);
|
|
1561
|
+
var readableTypes = (strapi, ctx) => Object.keys(strapi.contentTypes || {}).filter((uid) => uid.startsWith("api::") && canRead(strapi, ctx, uid));
|
|
1562
|
+
var ACTOR = /^(system|(admin|token|user):\d{1,15})$/;
|
|
1563
|
+
var int = (value, fallback, max) => {
|
|
1564
|
+
if (value === void 0 || value === "") return fallback;
|
|
1565
|
+
const n = Number(value);
|
|
1566
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n : NaN;
|
|
1567
|
+
};
|
|
1568
|
+
var date = (value) => {
|
|
1569
|
+
if (!value) return null;
|
|
1570
|
+
const time = Date.parse(value);
|
|
1571
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(time) ? new Date(time) : NaN;
|
|
1572
|
+
};
|
|
899
1573
|
var historyController2 = ({ strapi }) => ({
|
|
900
1574
|
async list(ctx) {
|
|
901
1575
|
const { uid, documentId } = ctx.params || {};
|
|
@@ -911,17 +1585,30 @@ var require_history = __commonJS({
|
|
|
911
1585
|
const found = await strapi.plugin(PLUGIN2).service("history").find(id);
|
|
912
1586
|
if (!found || !canRead(strapi, ctx, found.event.contentType)) return ctx.notFound();
|
|
913
1587
|
ctx.body = found;
|
|
1588
|
+
},
|
|
1589
|
+
async activity(ctx) {
|
|
1590
|
+
const { actor = "", contentType = "", action = "", q = "" } = ctx.query || {};
|
|
1591
|
+
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);
|
|
1592
|
+
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");
|
|
1593
|
+
const history = strapi.plugin(PLUGIN2).service("history");
|
|
1594
|
+
const types = readableTypes(strapi, ctx);
|
|
1595
|
+
ctx.body = {
|
|
1596
|
+
...await history.activity({ actor, contentType, action, from, to, q: q.trim(), page, pageSize, types }),
|
|
1597
|
+
enabled: Boolean((await history.config()).enabled) && strapi.plugin(PLUGIN2).config("disabled") !== true,
|
|
1598
|
+
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) }))
|
|
1599
|
+
};
|
|
914
1600
|
}
|
|
915
1601
|
});
|
|
916
|
-
module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
|
|
1602
|
+
module2.exports = { EVENT_UID, ACTIONS, CAPTURED, CRON: CRON2, BATCH, eventContentType: eventContentType2, covers: covers2, retention, actorOf, refsOf, can, readableTypes, historyService: historyService2, registerHistory: registerHistory2, registerPurge: registerPurge2, historyController: historyController2 };
|
|
917
1603
|
}
|
|
918
1604
|
});
|
|
919
1605
|
|
|
920
1606
|
// server/index.js
|
|
921
|
-
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
922
|
-
var { safeGroups, validateGroups } = require_groups();
|
|
1607
|
+
var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
|
|
1608
|
+
var { safeGroups, validateGroups, layoutFields } = require_groups();
|
|
923
1609
|
var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
|
|
924
1610
|
var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
|
|
1611
|
+
var { trashContentType, trashService, trashController } = require_trash();
|
|
925
1612
|
var STRAPI5 = true;
|
|
926
1613
|
var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
|
|
927
1614
|
var LEGACY_PLUGIN = "block-picker";
|
|
@@ -1061,10 +1748,17 @@ module.exports = {
|
|
|
1061
1748
|
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
1062
1749
|
{ section: "plugins", displayName: "Read gallery settings", uid: "settings.read", pluginName: PLUGIN },
|
|
1063
1750
|
{ section: "plugins", displayName: "Change gallery settings", uid: "settings.update", pluginName: PLUGIN },
|
|
1064
|
-
...STRAPI5 ? [
|
|
1751
|
+
...STRAPI5 ? [
|
|
1752
|
+
["history.read", "Read version history"],
|
|
1753
|
+
["activity.read", "Read content activity"],
|
|
1754
|
+
["trash.read", "Read the trash"],
|
|
1755
|
+
["trash.restore", "Restore from the trash"],
|
|
1756
|
+
["trash.purge", "Delete from the trash forever"]
|
|
1757
|
+
].map(([uid, displayName]) => ({ section: "plugins", displayName, uid, pluginName: PLUGIN })) : []
|
|
1065
1758
|
]);
|
|
1066
1759
|
strapi.plugin(PLUGIN).service("settings").projectDefaults();
|
|
1067
1760
|
strapi.plugin(PLUGIN).service("settings").fields();
|
|
1761
|
+
strapi.plugin(PLUGIN).service("settings").variants();
|
|
1068
1762
|
registerPublishGuard(strapi);
|
|
1069
1763
|
registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
|
|
1070
1764
|
if (STRAPI5) {
|
|
@@ -1075,8 +1769,8 @@ module.exports = {
|
|
|
1075
1769
|
destroy({ strapi }) {
|
|
1076
1770
|
if (STRAPI5) strapi.cron?.remove?.(CRON);
|
|
1077
1771
|
},
|
|
1078
|
-
// Version history events (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1079
|
-
contentTypes: STRAPI5 ? { event: eventContentType } : {},
|
|
1772
|
+
// Version history events and the trash (Strapi 5): hidden from the Content Manager and the Content-Type Builder.
|
|
1773
|
+
contentTypes: STRAPI5 ? { event: eventContentType, trash: trashContentType } : {},
|
|
1080
1774
|
services: { settings: ({ strapi }) => {
|
|
1081
1775
|
let project;
|
|
1082
1776
|
const projectDefaults = () => {
|
|
@@ -1084,7 +1778,7 @@ module.exports = {
|
|
|
1084
1778
|
const code = strapi.plugin(PLUGIN).config("settings");
|
|
1085
1779
|
project = null;
|
|
1086
1780
|
if (code) try {
|
|
1087
|
-
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
1781
|
+
project = validateSettings(code, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
1088
1782
|
} catch (error) {
|
|
1089
1783
|
strapi.log?.warn(`[${PLUGIN}] "settings" config ignored: ${error.message}. Using the built-in defaults under the saved settings.`);
|
|
1090
1784
|
}
|
|
@@ -1104,13 +1798,22 @@ module.exports = {
|
|
|
1104
1798
|
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` : ""}.`);
|
|
1105
1799
|
return fields;
|
|
1106
1800
|
};
|
|
1107
|
-
|
|
1801
|
+
let variants;
|
|
1802
|
+
const insertVariants = () => {
|
|
1803
|
+
if (variants !== void 0) return variants;
|
|
1804
|
+
const notes = [];
|
|
1805
|
+
variants = validateVariants(strapi.plugin(PLUGIN).config("components"), strapi.components || {}, notes);
|
|
1806
|
+
for (const note of notes) strapi.log?.warn(`[${PLUGIN}] components variants: ${note}.`);
|
|
1807
|
+
return variants;
|
|
1808
|
+
};
|
|
1809
|
+
const get = async () => mergeSaved(layer(projectDefaults(), await readSettings(strapi)), componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
1108
1810
|
return {
|
|
1109
1811
|
projectDefaults,
|
|
1110
1812
|
get,
|
|
1111
1813
|
fields: fieldTexts,
|
|
1814
|
+
variants: insertVariants,
|
|
1112
1815
|
async set(value) {
|
|
1113
|
-
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi));
|
|
1816
|
+
const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
|
|
1114
1817
|
for (const [uid, entry] of Object.entries(next.components)) {
|
|
1115
1818
|
if (entry.mediaId && !await findMedia(strapi, entry.mediaId).catch(() => null)) fail(`Media for "${uid}" does not exist`);
|
|
1116
1819
|
}
|
|
@@ -1124,7 +1827,7 @@ module.exports = {
|
|
|
1124
1827
|
return get();
|
|
1125
1828
|
}
|
|
1126
1829
|
};
|
|
1127
|
-
}, ...STRAPI5 && { history: historyService } },
|
|
1830
|
+
}, ...STRAPI5 && { history: historyService, trash: trashService } },
|
|
1128
1831
|
controllers: {
|
|
1129
1832
|
catalog: ({ strapi }) => ({
|
|
1130
1833
|
async find(ctx) {
|
|
@@ -1141,6 +1844,7 @@ module.exports = {
|
|
|
1141
1844
|
});
|
|
1142
1845
|
const settings = await plugin.service("settings").get();
|
|
1143
1846
|
const media = await resolveMedia(strapi, settings);
|
|
1847
|
+
for (const [uid, list] of Object.entries(plugin.service("settings").variants())) base.components[uid] = { ...base.components[uid], variants: list };
|
|
1144
1848
|
for (const [uid, entry] of Object.entries(settings.components)) {
|
|
1145
1849
|
base.components[uid] = {
|
|
1146
1850
|
...base.components[uid],
|
|
@@ -1153,8 +1857,10 @@ module.exports = {
|
|
|
1153
1857
|
const hidden = hiddenName(plugin.config("hiddenAttribute")) || null;
|
|
1154
1858
|
const types = Object.fromEntries(Object.entries(strapi.contentTypes || {}).filter(([uid]) => !uid.startsWith("admin::")).map(([uid, schema]) => [uid, [schema.info?.displayName || "", ...Object.keys(schema.attributes || {})]]));
|
|
1155
1859
|
const history = STRAPI5 && strapi.documents?.use && !base.disabled && settings.history.enabled ? { contentTypes: apiUids(strapi).filter((uid) => covers(settings.history, uid)) } : null;
|
|
1860
|
+
const layouts = Object.fromEntries(Object.entries(settings.components).filter(([uid, entry]) => entry.layout && base.groups?.[uid]).map(([uid, entry]) => [uid, entry.layout]));
|
|
1156
1861
|
ctx.body = {
|
|
1157
1862
|
...base,
|
|
1863
|
+
layouts,
|
|
1158
1864
|
palette: settings.palette,
|
|
1159
1865
|
contentTypes: settings.contentTypes,
|
|
1160
1866
|
hiddenAttribute: hidden,
|
|
@@ -1173,7 +1879,8 @@ module.exports = {
|
|
|
1173
1879
|
uid,
|
|
1174
1880
|
displayName: schema.info?.displayName || uid,
|
|
1175
1881
|
category: schema.category || uid.split(".")[0],
|
|
1176
|
-
typology: auto[uid]?.typology
|
|
1882
|
+
typology: auto[uid]?.typology,
|
|
1883
|
+
layoutFields: layoutFields(schema)
|
|
1177
1884
|
}));
|
|
1178
1885
|
const contentTypes = Object.keys(contentTypeUids(strapi)).map((uid) => ({
|
|
1179
1886
|
uid,
|
|
@@ -1218,7 +1925,7 @@ module.exports = {
|
|
|
1218
1925
|
ctx.body = value;
|
|
1219
1926
|
}
|
|
1220
1927
|
}),
|
|
1221
|
-
...STRAPI5 && { history: historyController }
|
|
1928
|
+
...STRAPI5 && { history: historyController, trash: trashController }
|
|
1222
1929
|
},
|
|
1223
1930
|
routes: { admin: { type: "admin", routes: [
|
|
1224
1931
|
{ method: "GET", path: "/catalog", handler: "catalog.find", config: { policies: ["admin::isAuthenticatedAdmin"] } },
|
|
@@ -1232,11 +1939,20 @@ module.exports = {
|
|
|
1232
1939
|
config: { actions: [`plugin::${PLUGIN}.settings.${action}`] }
|
|
1233
1940
|
}] }
|
|
1234
1941
|
})),
|
|
1235
|
-
...STRAPI5 ? [
|
|
1236
|
-
|
|
1942
|
+
...STRAPI5 ? [
|
|
1943
|
+
["GET", "/history/:uid/:documentId", "history.list", "history.read"],
|
|
1944
|
+
["GET", "/history-events/:id", "history.find", "history.read"],
|
|
1945
|
+
["GET", "/activity", "history.activity", "activity.read"],
|
|
1946
|
+
["GET", "/trash", "trash.list", "trash.read"],
|
|
1947
|
+
["GET", "/trash/:id", "trash.find", "trash.read"],
|
|
1948
|
+
["GET", "/trash/:id/check", "trash.check", "trash.restore"],
|
|
1949
|
+
["POST", "/trash/:id/restore", "trash.restore", "trash.restore"],
|
|
1950
|
+
["DELETE", "/trash/:id", "trash.remove", "trash.purge"]
|
|
1951
|
+
].map(([method, path, handler, action]) => ({
|
|
1952
|
+
method,
|
|
1237
1953
|
path,
|
|
1238
|
-
handler
|
|
1239
|
-
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}
|
|
1954
|
+
handler,
|
|
1955
|
+
config: { policies: ["admin::isAuthenticatedAdmin", { name: "admin::hasPermissions", config: { actions: [`plugin::${PLUGIN}.${action}`] } }] }
|
|
1240
1956
|
})) : []
|
|
1241
1957
|
] } }
|
|
1242
1958
|
};
|