strapi-plugin-bulk-updater 1.3.3 → 1.3.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/dist/_chunks/{BulkUpdater-C2lw4D0D.js → BulkUpdater-BosdSbGF.js} +182 -143
- package/dist/_chunks/BulkUpdater-BosdSbGF.js.map +1 -0
- package/dist/_chunks/{BulkUpdater-CgsqzdyX.mjs → BulkUpdater-DUrmamj-.mjs} +183 -144
- package/dist/_chunks/BulkUpdater-DUrmamj-.mjs.map +1 -0
- package/dist/_chunks/{ScopedQuotaSelectInput-ChWrQIBV.mjs → ScopedQuotaSelectInput-Cy3eEj9-.mjs} +50 -23
- package/dist/_chunks/ScopedQuotaSelectInput-Cy3eEj9-.mjs.map +1 -0
- package/dist/_chunks/{ScopedQuotaSelectInput-s4oUMLgK.js → ScopedQuotaSelectInput-CyGlYa9x.js} +50 -23
- package/dist/_chunks/ScopedQuotaSelectInput-CyGlYa9x.js.map +1 -0
- package/dist/_chunks/{SettingsPage-BAvuOb7V.js → SettingsPage-B6Ia7SVS.js} +2 -2
- package/dist/_chunks/{SettingsPage-BAvuOb7V.js.map → SettingsPage-B6Ia7SVS.js.map} +1 -1
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs → SettingsPage-Bdse5MP4.mjs} +2 -2
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs.map → SettingsPage-Bdse5MP4.mjs.map} +1 -1
- package/dist/_chunks/{ValidationRulesTab-BLZWPGrZ.js → ValidationRulesTab-BqGz1VLo.js} +83 -16
- package/dist/_chunks/ValidationRulesTab-BqGz1VLo.js.map +1 -0
- package/dist/_chunks/{ValidationRulesTab-Dt53h-Ke.mjs → ValidationRulesTab-Dqkson67.mjs} +84 -17
- package/dist/_chunks/ValidationRulesTab-Dqkson67.mjs.map +1 -0
- package/dist/admin/index.js +45 -3
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +44 -4
- package/dist/admin/index.mjs.map +1 -1
- package/dist/server/index.js +607 -128
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +607 -128
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/BulkUpdater-C2lw4D0D.js.map +0 -1
- package/dist/_chunks/BulkUpdater-CgsqzdyX.mjs.map +0 -1
- package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs.map +0 -1
- package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js.map +0 -1
- package/dist/_chunks/ValidationRulesTab-BLZWPGrZ.js.map +0 -1
- package/dist/_chunks/ValidationRulesTab-Dt53h-Ke.mjs.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -688,6 +688,149 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
688
688
|
}
|
|
689
689
|
}
|
|
690
690
|
});
|
|
691
|
+
const FIELDS_TO_IGNORE = [
|
|
692
|
+
"createdAt",
|
|
693
|
+
"updatedAt",
|
|
694
|
+
"publishedAt",
|
|
695
|
+
"createdBy",
|
|
696
|
+
"updatedBy",
|
|
697
|
+
"strapi_stage",
|
|
698
|
+
"strapi_assignee"
|
|
699
|
+
];
|
|
700
|
+
function buildHistoryPopulate(attributes, allComponents, depth2 = 0) {
|
|
701
|
+
if (depth2 > 5) return void 0;
|
|
702
|
+
const populate2 = {};
|
|
703
|
+
for (const [key, def] of Object.entries(attributes || {})) {
|
|
704
|
+
if (def.type === "component" && def.component) {
|
|
705
|
+
const comp = allComponents[def.component];
|
|
706
|
+
if (comp?.attributes) {
|
|
707
|
+
const subPopulate = buildHistoryPopulate(comp.attributes, allComponents, depth2 + 1);
|
|
708
|
+
if (subPopulate && Object.keys(subPopulate).length > 0) {
|
|
709
|
+
populate2[key] = {
|
|
710
|
+
populate: subPopulate
|
|
711
|
+
};
|
|
712
|
+
} else {
|
|
713
|
+
populate2[key] = true;
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
populate2[key] = true;
|
|
717
|
+
}
|
|
718
|
+
} else if (def.type === "dynamiczone") {
|
|
719
|
+
populate2[key] = true;
|
|
720
|
+
} else if (def.type === "relation" || def.type === "media") {
|
|
721
|
+
populate2[key] = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return Object.keys(populate2).length > 0 ? populate2 : void 0;
|
|
725
|
+
}
|
|
726
|
+
let cachedAdminUserId = null;
|
|
727
|
+
async function recordContentHistoryVersion(strapi2, options) {
|
|
728
|
+
const { uid, documentId, userId, statusOverride } = options;
|
|
729
|
+
if (!uid || !uid.startsWith("api::") || !documentId) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
734
|
+
if (!historyQuery) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const model = strapi2.getModel?.(uid);
|
|
738
|
+
if (!model) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
let entry = null;
|
|
742
|
+
try {
|
|
743
|
+
entry = await strapi2.db.query(uid).findOne({
|
|
744
|
+
where: { documentId },
|
|
745
|
+
populate: true
|
|
746
|
+
});
|
|
747
|
+
} catch {
|
|
748
|
+
try {
|
|
749
|
+
const populateOption = buildHistoryPopulate(model.attributes || {}, strapi2.components || {});
|
|
750
|
+
entry = await strapi2.documents(uid).findOne({
|
|
751
|
+
documentId,
|
|
752
|
+
...populateOption ? { populate: populateOption } : {}
|
|
753
|
+
});
|
|
754
|
+
} catch {
|
|
755
|
+
entry = null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (!entry) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const attributesSchema = model.attributes || {};
|
|
762
|
+
const sanitizedSchema = Object.fromEntries(
|
|
763
|
+
Object.entries(attributesSchema).filter(([k]) => !FIELDS_TO_IGNORE.includes(k))
|
|
764
|
+
);
|
|
765
|
+
const sanitizedData = Object.fromEntries(
|
|
766
|
+
Object.entries(entry).filter(([k]) => !FIELDS_TO_IGNORE.includes(k) && k !== "id")
|
|
767
|
+
);
|
|
768
|
+
let status2 = statusOverride || "published";
|
|
769
|
+
if (!statusOverride) {
|
|
770
|
+
const hasDraftAndPublish = Boolean(model.options?.draftAndPublish);
|
|
771
|
+
if (hasDraftAndPublish) {
|
|
772
|
+
try {
|
|
773
|
+
const docMetaService = strapi2.plugin("content-manager")?.service("document-metadata");
|
|
774
|
+
if (docMetaService) {
|
|
775
|
+
const meta = await docMetaService.getMetadata(uid, entry);
|
|
776
|
+
status2 = docMetaService.getStatus(entry, meta.availableStatus);
|
|
777
|
+
} else {
|
|
778
|
+
status2 = entry.publishedAt ? "published" : "draft";
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
status2 = entry.publishedAt ? "published" : "draft";
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
status2 = "published";
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
let authorId = userId;
|
|
788
|
+
if (!authorId) {
|
|
789
|
+
authorId = strapi2.requestContext?.get?.()?.state?.user?.id || null;
|
|
790
|
+
}
|
|
791
|
+
let numericUserId = authorId ? Number(authorId) : null;
|
|
792
|
+
if (!numericUserId || isNaN(numericUserId)) {
|
|
793
|
+
if (cachedAdminUserId) {
|
|
794
|
+
numericUserId = cachedAdminUserId;
|
|
795
|
+
} else {
|
|
796
|
+
try {
|
|
797
|
+
const firstAdmin = await strapi2.db?.query?.("admin::user")?.findOne?.({ select: ["id"] });
|
|
798
|
+
if (firstAdmin?.id) {
|
|
799
|
+
cachedAdminUserId = Number(firstAdmin.id);
|
|
800
|
+
numericUserId = cachedAdminUserId;
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
await historyQuery.create({
|
|
807
|
+
data: {
|
|
808
|
+
contentType: uid,
|
|
809
|
+
relatedDocumentId: documentId,
|
|
810
|
+
locale: entry.locale || null,
|
|
811
|
+
status: status2,
|
|
812
|
+
data: sanitizedData,
|
|
813
|
+
schema: sanitizedSchema,
|
|
814
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
815
|
+
...numericUserId && !isNaN(numericUserId) ? { createdBy: numericUserId } : {}
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
} catch (err) {
|
|
819
|
+
strapi2.log?.warn?.(`[bulk-updater] Non-blocking Content History notice for ${uid}/${documentId}: ${err?.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function recordContentHistoryVersionsBatch(strapi2, items) {
|
|
823
|
+
if (!items || items.length === 0) return;
|
|
824
|
+
const BATCH_SIZE = 10;
|
|
825
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
826
|
+
const slice = items.slice(i, i + BATCH_SIZE);
|
|
827
|
+
await Promise.all(slice.map((item) => recordContentHistoryVersion(strapi2, item)));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const contentHistoryService = {
|
|
831
|
+
recordContentHistoryVersion,
|
|
832
|
+
recordContentHistoryVersionsBatch
|
|
833
|
+
};
|
|
691
834
|
const queryCache = /* @__PURE__ */ new Map();
|
|
692
835
|
const modelsCache = /* @__PURE__ */ new Map();
|
|
693
836
|
const MAX_QUERY_CACHE_SIZE = 500;
|
|
@@ -1064,6 +1207,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1064
1207
|
}
|
|
1065
1208
|
}
|
|
1066
1209
|
invalidateCollectionCache(target.uid);
|
|
1210
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1211
|
+
uid: target.uid,
|
|
1212
|
+
documentId: target.documentId,
|
|
1213
|
+
userId: currentUser?.id,
|
|
1214
|
+
statusOverride: publishOnApproval && isDraftAndPublishComp ? "published" : void 0
|
|
1215
|
+
});
|
|
1067
1216
|
return {
|
|
1068
1217
|
uid: target.uid,
|
|
1069
1218
|
collectionName: displayName,
|
|
@@ -1149,6 +1298,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1149
1298
|
if (attrDef.target) {
|
|
1150
1299
|
invalidateCollectionCache(attrDef.target);
|
|
1151
1300
|
}
|
|
1301
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1302
|
+
uid: target.uid,
|
|
1303
|
+
documentId: target.documentId,
|
|
1304
|
+
userId: currentUser?.id,
|
|
1305
|
+
statusOverride: publishOnApproval && isDraftAndPublishRel ? "published" : void 0
|
|
1306
|
+
});
|
|
1152
1307
|
return {
|
|
1153
1308
|
uid: target.uid,
|
|
1154
1309
|
collectionName: displayName,
|
|
@@ -1182,6 +1337,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1182
1337
|
strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
|
|
1183
1338
|
}
|
|
1184
1339
|
}
|
|
1340
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1341
|
+
uid: target.uid,
|
|
1342
|
+
documentId: target.documentId,
|
|
1343
|
+
userId: currentUser?.id,
|
|
1344
|
+
statusOverride: publishOnApproval && isDraftAndPublishScalar ? "published" : void 0
|
|
1345
|
+
});
|
|
1185
1346
|
return {
|
|
1186
1347
|
uid: target.uid,
|
|
1187
1348
|
collectionName: displayName,
|
|
@@ -1352,6 +1513,12 @@ async function executePublishBatchInternal(strapi2, {
|
|
|
1352
1513
|
documentId: target.documentId
|
|
1353
1514
|
});
|
|
1354
1515
|
invalidateCollectionCache(target.uid);
|
|
1516
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1517
|
+
uid: target.uid,
|
|
1518
|
+
documentId: target.documentId,
|
|
1519
|
+
userId: currentUser?.id,
|
|
1520
|
+
statusOverride: "published"
|
|
1521
|
+
});
|
|
1355
1522
|
return {
|
|
1356
1523
|
uid: target.uid,
|
|
1357
1524
|
collectionName: displayName,
|
|
@@ -3207,7 +3374,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3207
3374
|
/**
|
|
3208
3375
|
* Helper to revert diff items back to original values
|
|
3209
3376
|
*/
|
|
3210
|
-
async revertDiffItems(diffs) {
|
|
3377
|
+
async revertDiffItems(diffs, userId) {
|
|
3211
3378
|
let revertedCount = 0;
|
|
3212
3379
|
const rollbackDetails = [];
|
|
3213
3380
|
for (const item of diffs) {
|
|
@@ -3265,6 +3432,15 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3265
3432
|
} catch {
|
|
3266
3433
|
}
|
|
3267
3434
|
}
|
|
3435
|
+
try {
|
|
3436
|
+
await recordContentHistoryVersion(strapi2, {
|
|
3437
|
+
uid: item.uid,
|
|
3438
|
+
documentId: item.documentId,
|
|
3439
|
+
userId,
|
|
3440
|
+
statusOverride: ct?.options?.draftAndPublish ? "published" : void 0
|
|
3441
|
+
});
|
|
3442
|
+
} catch {
|
|
3443
|
+
}
|
|
3268
3444
|
try {
|
|
3269
3445
|
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
3270
3446
|
if (bulkService?.invalidateCache) {
|
|
@@ -3315,7 +3491,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3315
3491
|
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
3316
3492
|
}
|
|
3317
3493
|
const diffs2 = Array.isArray(record2.diffs) ? record2.diffs : [];
|
|
3318
|
-
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2);
|
|
3494
|
+
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2, currentUser?.id);
|
|
3319
3495
|
record2.isRolledBack = true;
|
|
3320
3496
|
record2.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3321
3497
|
record2.rolledBackBy = currentUser?.email || "Admin";
|
|
@@ -3363,7 +3539,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3363
3539
|
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
3364
3540
|
}
|
|
3365
3541
|
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
3366
|
-
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs);
|
|
3542
|
+
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs, currentUser?.id);
|
|
3367
3543
|
const updateData = {};
|
|
3368
3544
|
if (f.isRolledBack) updateData[f.isRolledBack] = true;
|
|
3369
3545
|
if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3404,7 +3580,8 @@ const DEFAULT_RULES = [
|
|
|
3404
3580
|
{
|
|
3405
3581
|
id: "rule_seo_heading_h1",
|
|
3406
3582
|
name: "SEO Single H1 Page Rule",
|
|
3407
|
-
description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry.",
|
|
3583
|
+
description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry, combined with rich text HTML.",
|
|
3584
|
+
collectionUid: "*",
|
|
3408
3585
|
targetField: "headingLevel",
|
|
3409
3586
|
quotas: {
|
|
3410
3587
|
h1: 1,
|
|
@@ -3413,10 +3590,56 @@ const DEFAULT_RULES = [
|
|
|
3413
3590
|
exceededAction: "disable",
|
|
3414
3591
|
enabled: true,
|
|
3415
3592
|
scope: "entry",
|
|
3593
|
+
includeRichTextHtml: true,
|
|
3594
|
+
richTextFields: [],
|
|
3416
3595
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3417
3596
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3418
3597
|
}
|
|
3419
3598
|
];
|
|
3599
|
+
function extractHeadingsFromRichText(content) {
|
|
3600
|
+
const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3601
|
+
if (!content) return counts;
|
|
3602
|
+
if (Array.isArray(content)) {
|
|
3603
|
+
for (const node of content) {
|
|
3604
|
+
if (node && typeof node === "object") {
|
|
3605
|
+
if (node.type === "heading" && typeof node.level === "number") {
|
|
3606
|
+
const tag = `h${node.level}`;
|
|
3607
|
+
if (counts[tag] !== void 0) counts[tag]++;
|
|
3608
|
+
}
|
|
3609
|
+
if (Array.isArray(node.children)) {
|
|
3610
|
+
const sub = extractHeadingsFromRichText(node.children);
|
|
3611
|
+
for (const k of Object.keys(counts)) counts[k] += sub[k];
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
return counts;
|
|
3616
|
+
}
|
|
3617
|
+
if (typeof content === "string") {
|
|
3618
|
+
for (let level = 1; level <= 6; level++) {
|
|
3619
|
+
const tag = `h${level}`;
|
|
3620
|
+
const htmlMatches = content.match(new RegExp(`<${tag}\\b[^>]*>`, "gi"));
|
|
3621
|
+
if (htmlMatches) {
|
|
3622
|
+
counts[tag] += htmlMatches.length;
|
|
3623
|
+
}
|
|
3624
|
+
const mdRegex = new RegExp(`(?:^|\\n)${"#".repeat(level)}\\s+[^\\n]+`, "g");
|
|
3625
|
+
const mdMatches = content.match(mdRegex);
|
|
3626
|
+
if (mdMatches) {
|
|
3627
|
+
counts[tag] += mdMatches.length;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
return counts;
|
|
3632
|
+
}
|
|
3633
|
+
function matchesPath(normalizedPath, leafKey, targetField) {
|
|
3634
|
+
if (!targetField) return false;
|
|
3635
|
+
const pLower = (normalizedPath || "").toLowerCase().trim();
|
|
3636
|
+
const kLower = (leafKey || "").toLowerCase().trim();
|
|
3637
|
+
const tLower = targetField.toLowerCase().trim();
|
|
3638
|
+
if (!tLower.includes(".")) {
|
|
3639
|
+
return kLower === tLower || pLower === tLower || pLower.endsWith(`.${tLower}`);
|
|
3640
|
+
}
|
|
3641
|
+
return pLower === tLower || pLower.endsWith(`.${tLower}`) || pLower.endsWith(tLower);
|
|
3642
|
+
}
|
|
3420
3643
|
const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
3421
3644
|
async getStore() {
|
|
3422
3645
|
return strapi2.store({
|
|
@@ -3445,6 +3668,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3445
3668
|
savedRule = {
|
|
3446
3669
|
...rules[index2],
|
|
3447
3670
|
...ruleData,
|
|
3671
|
+
collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
|
|
3672
|
+
includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
|
|
3673
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
|
|
3448
3674
|
updatedAt: now
|
|
3449
3675
|
};
|
|
3450
3676
|
rules[index2] = savedRule;
|
|
@@ -3453,11 +3679,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3453
3679
|
id: ruleData.id,
|
|
3454
3680
|
name: ruleData.name || "New Rule",
|
|
3455
3681
|
description: ruleData.description || "",
|
|
3682
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3456
3683
|
targetField: ruleData.targetField || "headingLevel",
|
|
3457
3684
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3458
3685
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3459
3686
|
enabled: ruleData.enabled ?? true,
|
|
3460
3687
|
scope: ruleData.scope || "entry",
|
|
3688
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3689
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3461
3690
|
createdAt: now,
|
|
3462
3691
|
updatedAt: now
|
|
3463
3692
|
};
|
|
@@ -3469,11 +3698,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3469
3698
|
id,
|
|
3470
3699
|
name: ruleData.name || "Custom Quota Rule",
|
|
3471
3700
|
description: ruleData.description || "",
|
|
3701
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3472
3702
|
targetField: ruleData.targetField || "headingLevel",
|
|
3473
3703
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3474
3704
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3475
3705
|
enabled: ruleData.enabled ?? true,
|
|
3476
3706
|
scope: ruleData.scope || "entry",
|
|
3707
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3708
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3477
3709
|
createdAt: now,
|
|
3478
3710
|
updatedAt: now
|
|
3479
3711
|
};
|
|
@@ -3517,21 +3749,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3517
3749
|
let totalScanned = 0;
|
|
3518
3750
|
const allContentTypes = strapi2.contentTypes || {};
|
|
3519
3751
|
const allComponents = strapi2.components || {};
|
|
3752
|
+
function componentHasField(compUid, targetField2) {
|
|
3753
|
+
const comp = allComponents[compUid];
|
|
3754
|
+
if (!comp?.attributes) return false;
|
|
3755
|
+
for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
|
|
3756
|
+
if (matchesPath(attrName, attrName, targetField2)) return true;
|
|
3757
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3758
|
+
if (componentHasField(attrDef.component, targetField2)) return true;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
return false;
|
|
3762
|
+
}
|
|
3520
3763
|
function schemaHasField(ctSchema) {
|
|
3521
3764
|
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
3522
3765
|
const rawAttrs = ctSchema.attributes;
|
|
3523
3766
|
const populateAttrs = {};
|
|
3524
|
-
let hasField =
|
|
3767
|
+
let hasField = false;
|
|
3525
3768
|
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
3769
|
+
if (matchesPath(attrKey, attrKey, targetField)) {
|
|
3770
|
+
hasField = true;
|
|
3771
|
+
}
|
|
3526
3772
|
if (attrDef.type === "component" && attrDef.component) {
|
|
3527
|
-
|
|
3528
|
-
if (comp?.attributes?.[targetField]) {
|
|
3773
|
+
if (componentHasField(attrDef.component, targetField)) {
|
|
3529
3774
|
hasField = true;
|
|
3530
3775
|
populateAttrs[attrKey] = true;
|
|
3531
3776
|
}
|
|
3532
3777
|
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
3533
3778
|
const matchingComp = attrDef.components.some(
|
|
3534
|
-
(compUid) =>
|
|
3779
|
+
(compUid) => componentHasField(compUid, targetField)
|
|
3535
3780
|
);
|
|
3536
3781
|
if (matchingComp) {
|
|
3537
3782
|
hasField = true;
|
|
@@ -3541,19 +3786,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3541
3786
|
}
|
|
3542
3787
|
return { hasField, populateAttrs };
|
|
3543
3788
|
}
|
|
3544
|
-
const candidateUids = Object.keys(allContentTypes).filter(
|
|
3545
|
-
(uid
|
|
3546
|
-
|
|
3789
|
+
const candidateUids = Object.keys(allContentTypes).filter((uid) => {
|
|
3790
|
+
if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
|
|
3791
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
|
|
3792
|
+
return false;
|
|
3793
|
+
}
|
|
3794
|
+
return true;
|
|
3795
|
+
});
|
|
3547
3796
|
const relevantCollections = [];
|
|
3548
3797
|
for (const uid of candidateUids) {
|
|
3549
3798
|
const ct = allContentTypes[uid];
|
|
3550
3799
|
if (!ct) continue;
|
|
3551
3800
|
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
3552
|
-
if (hasField) {
|
|
3801
|
+
if (hasField || rule.collectionUid && rule.collectionUid === uid) {
|
|
3553
3802
|
relevantCollections.push({
|
|
3554
3803
|
uid,
|
|
3555
3804
|
displayName: ct.info?.displayName || uid,
|
|
3556
|
-
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs :
|
|
3805
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
|
|
3557
3806
|
});
|
|
3558
3807
|
}
|
|
3559
3808
|
}
|
|
@@ -3570,35 +3819,69 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3570
3819
|
});
|
|
3571
3820
|
if (!entries || entries.length === 0) break;
|
|
3572
3821
|
for (const entry of entries) {
|
|
3573
|
-
let traverse = function(obj) {
|
|
3822
|
+
let traverse = function(obj, currentPath = "") {
|
|
3574
3823
|
if (!obj || typeof obj !== "object") return;
|
|
3575
3824
|
for (const key of Object.keys(obj)) {
|
|
3576
3825
|
const val = obj[key];
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3826
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
3827
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
3828
|
+
if (matchesPath(normalizedPath, key, targetField) && typeof val === "string" && val.trim()) {
|
|
3829
|
+
const lower = val.toLowerCase().trim();
|
|
3830
|
+
structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
|
|
3831
|
+
}
|
|
3832
|
+
if (rule.includeRichTextHtml) {
|
|
3833
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
3834
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3835
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3836
|
+
if (count > 0) {
|
|
3837
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
3841
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3842
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3843
|
+
if (count > 0) {
|
|
3844
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3580
3849
|
}
|
|
3581
3850
|
if (typeof val === "object" && val !== null) {
|
|
3582
|
-
traverse(val);
|
|
3851
|
+
traverse(val, newPath);
|
|
3583
3852
|
}
|
|
3584
3853
|
}
|
|
3585
3854
|
};
|
|
3586
3855
|
totalScanned++;
|
|
3587
|
-
const
|
|
3856
|
+
const structuredCounts = {};
|
|
3857
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3588
3858
|
traverse(entry);
|
|
3589
3859
|
for (const [optKey, max] of Object.entries(quotas)) {
|
|
3590
|
-
const lowerOpt = optKey.toLowerCase();
|
|
3591
|
-
const
|
|
3592
|
-
|
|
3860
|
+
const lowerOpt = optKey.toLowerCase().trim();
|
|
3861
|
+
const structCount = structuredCounts[lowerOpt] || 0;
|
|
3862
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
|
|
3863
|
+
const combinedCount = structCount + rtCount;
|
|
3864
|
+
if (combinedCount > max) {
|
|
3593
3865
|
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
3866
|
+
let detail = "";
|
|
3867
|
+
if (rtCount > 0 && structCount > 0) {
|
|
3868
|
+
detail = `${structCount} structured + ${rtCount} in rich text HTML`;
|
|
3869
|
+
} else if (rtCount > 0) {
|
|
3870
|
+
detail = `${rtCount} in rich text HTML / Markdown`;
|
|
3871
|
+
} else {
|
|
3872
|
+
detail = `${structCount} in structured component`;
|
|
3873
|
+
}
|
|
3594
3874
|
violations.push({
|
|
3595
3875
|
uid,
|
|
3596
3876
|
collectionDisplayName: displayName,
|
|
3597
3877
|
documentId: entry.documentId,
|
|
3598
3878
|
entryLabel,
|
|
3599
3879
|
violatingOption: optKey.toUpperCase(),
|
|
3600
|
-
foundCount:
|
|
3880
|
+
foundCount: combinedCount,
|
|
3881
|
+
structuredCount: structCount,
|
|
3882
|
+
richTextCount: rtCount,
|
|
3601
3883
|
maxAllowed: max,
|
|
3884
|
+
detail,
|
|
3602
3885
|
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
3603
3886
|
});
|
|
3604
3887
|
}
|
|
@@ -3639,77 +3922,93 @@ function escapeCsvValue(val) {
|
|
|
3639
3922
|
}
|
|
3640
3923
|
return str2;
|
|
3641
3924
|
}
|
|
3642
|
-
function
|
|
3925
|
+
function parseCsvRows(csvText) {
|
|
3643
3926
|
if (!csvText) return [];
|
|
3644
|
-
let
|
|
3645
|
-
if (
|
|
3646
|
-
|
|
3927
|
+
let text = csvText;
|
|
3928
|
+
if (text.charCodeAt(0) === 65279) {
|
|
3929
|
+
text = text.slice(1);
|
|
3647
3930
|
}
|
|
3648
|
-
const
|
|
3649
|
-
let
|
|
3931
|
+
const rows = [];
|
|
3932
|
+
let currentRow = [];
|
|
3933
|
+
let currentCell = "";
|
|
3650
3934
|
let inQuotes = false;
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3935
|
+
let cellIsQuoted = false;
|
|
3936
|
+
for (let i = 0; i < text.length; i++) {
|
|
3937
|
+
const char = text[i];
|
|
3938
|
+
const nextChar = text[i + 1];
|
|
3939
|
+
if (inQuotes) {
|
|
3940
|
+
if (char === '"') {
|
|
3941
|
+
if (nextChar === '"') {
|
|
3942
|
+
currentCell += '"';
|
|
3943
|
+
i++;
|
|
3944
|
+
} else {
|
|
3945
|
+
inQuotes = false;
|
|
3946
|
+
}
|
|
3658
3947
|
} else {
|
|
3659
|
-
|
|
3660
|
-
}
|
|
3661
|
-
} else if ((char === "\r" || char === "\n") && !inQuotes) {
|
|
3662
|
-
if (char === "\r" && nextChar === "\n") {
|
|
3663
|
-
i++;
|
|
3664
|
-
}
|
|
3665
|
-
if (current.trim().length > 0) {
|
|
3666
|
-
lines.push(current);
|
|
3948
|
+
currentCell += char;
|
|
3667
3949
|
}
|
|
3668
|
-
current = "";
|
|
3669
3950
|
} else {
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
let inQ = false;
|
|
3681
|
-
for (let i = 0; i < line.length; i++) {
|
|
3682
|
-
const c = line[i];
|
|
3683
|
-
const nc = line[i + 1];
|
|
3684
|
-
if (c === '"') {
|
|
3685
|
-
if (inQ && nc === '"') {
|
|
3686
|
-
field += '"';
|
|
3951
|
+
if (char === '"' && !cellIsQuoted && currentCell.trim() === "") {
|
|
3952
|
+
inQuotes = true;
|
|
3953
|
+
cellIsQuoted = true;
|
|
3954
|
+
currentCell = "";
|
|
3955
|
+
} else if (char === ",") {
|
|
3956
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3957
|
+
currentCell = "";
|
|
3958
|
+
cellIsQuoted = false;
|
|
3959
|
+
} else if (char === "\r") {
|
|
3960
|
+
if (nextChar === "\n") {
|
|
3687
3961
|
i++;
|
|
3688
|
-
} else {
|
|
3689
|
-
inQ = !inQ;
|
|
3690
3962
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3963
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3964
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3965
|
+
rows.push(currentRow);
|
|
3966
|
+
}
|
|
3967
|
+
currentRow = [];
|
|
3968
|
+
currentCell = "";
|
|
3969
|
+
cellIsQuoted = false;
|
|
3970
|
+
} else if (char === "\n") {
|
|
3971
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3972
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3973
|
+
rows.push(currentRow);
|
|
3974
|
+
}
|
|
3975
|
+
currentRow = [];
|
|
3976
|
+
currentCell = "";
|
|
3977
|
+
cellIsQuoted = false;
|
|
3694
3978
|
} else {
|
|
3695
|
-
|
|
3979
|
+
if (!(cellIsQuoted && !inQuotes && (char === " " || char === " "))) {
|
|
3980
|
+
currentCell += char;
|
|
3981
|
+
}
|
|
3696
3982
|
}
|
|
3697
3983
|
}
|
|
3698
|
-
fields2.push(field);
|
|
3699
|
-
return fields2.map((f) => f.trim());
|
|
3700
3984
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
headers.forEach((h, index2) => {
|
|
3707
|
-
rowObj[h] = values[index2] !== void 0 ? values[index2] : "";
|
|
3708
|
-
});
|
|
3709
|
-
rows.push(rowObj);
|
|
3985
|
+
if (currentCell !== "" || cellIsQuoted || currentRow.length > 0) {
|
|
3986
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3987
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3988
|
+
rows.push(currentRow);
|
|
3989
|
+
}
|
|
3710
3990
|
}
|
|
3711
3991
|
return rows;
|
|
3712
3992
|
}
|
|
3993
|
+
function parseCsv(csvText) {
|
|
3994
|
+
const rows = parseCsvRows(csvText);
|
|
3995
|
+
if (rows.length < 2) return [];
|
|
3996
|
+
const headers = rows[0].map((h) => h.trim());
|
|
3997
|
+
const records = [];
|
|
3998
|
+
for (let i = 1; i < rows.length; i++) {
|
|
3999
|
+
const row = rows[i];
|
|
4000
|
+
const record = {};
|
|
4001
|
+
headers.forEach((h, idx) => {
|
|
4002
|
+
let val = row[idx] !== void 0 ? row[idx] : "";
|
|
4003
|
+
if (typeof val === "string" && /^'[=+@-]/.test(val)) {
|
|
4004
|
+
val = val.substring(1);
|
|
4005
|
+
}
|
|
4006
|
+
record[h] = val;
|
|
4007
|
+
});
|
|
4008
|
+
records.push(record);
|
|
4009
|
+
}
|
|
4010
|
+
return records;
|
|
4011
|
+
}
|
|
3713
4012
|
function discoverExportFields(ct, allComponents, maxDepth = 4) {
|
|
3714
4013
|
const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
|
|
3715
4014
|
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
@@ -4203,6 +4502,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4203
4502
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4204
4503
|
}
|
|
4205
4504
|
}
|
|
4505
|
+
try {
|
|
4506
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4507
|
+
uid,
|
|
4508
|
+
documentId: docId,
|
|
4509
|
+
userId: user?.id,
|
|
4510
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4511
|
+
});
|
|
4512
|
+
} catch {
|
|
4513
|
+
}
|
|
4206
4514
|
stats.updated++;
|
|
4207
4515
|
stats.diffs.push({
|
|
4208
4516
|
uid,
|
|
@@ -4238,6 +4546,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4238
4546
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4239
4547
|
}
|
|
4240
4548
|
}
|
|
4549
|
+
try {
|
|
4550
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4551
|
+
uid,
|
|
4552
|
+
documentId: docId,
|
|
4553
|
+
userId: user?.id,
|
|
4554
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4555
|
+
});
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4241
4558
|
stats.updated++;
|
|
4242
4559
|
stats.diffs.push({
|
|
4243
4560
|
uid,
|
|
@@ -4264,6 +4581,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4264
4581
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
4265
4582
|
}
|
|
4266
4583
|
}
|
|
4584
|
+
try {
|
|
4585
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4586
|
+
uid,
|
|
4587
|
+
documentId: newDocId,
|
|
4588
|
+
userId: user?.id,
|
|
4589
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4590
|
+
});
|
|
4591
|
+
} catch {
|
|
4592
|
+
}
|
|
4267
4593
|
stats.created++;
|
|
4268
4594
|
if (!docId) {
|
|
4269
4595
|
stats.newWithoutDocId++;
|
|
@@ -4302,6 +4628,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4302
4628
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4303
4629
|
}
|
|
4304
4630
|
}
|
|
4631
|
+
try {
|
|
4632
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4633
|
+
uid,
|
|
4634
|
+
documentId: docId,
|
|
4635
|
+
userId: user?.id,
|
|
4636
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4637
|
+
});
|
|
4638
|
+
} catch {
|
|
4639
|
+
}
|
|
4305
4640
|
stats.updated++;
|
|
4306
4641
|
stats.diffs.push({
|
|
4307
4642
|
uid,
|
|
@@ -4328,6 +4663,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4328
4663
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
4329
4664
|
}
|
|
4330
4665
|
}
|
|
4666
|
+
try {
|
|
4667
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4668
|
+
uid,
|
|
4669
|
+
documentId: newDocId,
|
|
4670
|
+
userId: user?.id,
|
|
4671
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4672
|
+
});
|
|
4673
|
+
} catch {
|
|
4674
|
+
}
|
|
4331
4675
|
stats.created++;
|
|
4332
4676
|
if (!docId) {
|
|
4333
4677
|
stats.newWithoutDocId++;
|
|
@@ -33808,21 +34152,41 @@ const index = {
|
|
|
33808
34152
|
},
|
|
33809
34153
|
bootstrap({ strapi: strapi2 }) {
|
|
33810
34154
|
strapi2.documents.use(async (context, next) => {
|
|
34155
|
+
const params = context.params;
|
|
33811
34156
|
if (["create", "update"].includes(context.action)) {
|
|
33812
|
-
const params = context.params;
|
|
33813
34157
|
const data = params?.data;
|
|
33814
34158
|
if (data && typeof data === "object") {
|
|
33815
|
-
let scanDataTree = function(obj) {
|
|
34159
|
+
let scanDataTree = function(obj, currentPath = "") {
|
|
33816
34160
|
if (!obj || typeof obj !== "object") return;
|
|
33817
34161
|
for (const key of Object.keys(obj)) {
|
|
33818
34162
|
const val = obj[key];
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
34163
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
34164
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
34165
|
+
for (const rule of effectiveRules) {
|
|
34166
|
+
if (matchesPath(normalizedPath, key, rule.targetField) && typeof val === "string" && val.trim()) {
|
|
34167
|
+
const lowerVal = val.toLowerCase().trim();
|
|
34168
|
+
structuredCounts[rule.id] = structuredCounts[rule.id] || {};
|
|
34169
|
+
structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
|
|
34170
|
+
}
|
|
34171
|
+
}
|
|
34172
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
34173
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34174
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34175
|
+
if (count > 0) {
|
|
34176
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34177
|
+
}
|
|
34178
|
+
}
|
|
34179
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
34180
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34181
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34182
|
+
if (count > 0) {
|
|
34183
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34184
|
+
}
|
|
34185
|
+
}
|
|
34186
|
+
continue;
|
|
33823
34187
|
}
|
|
33824
34188
|
if (typeof val === "object" && val !== null) {
|
|
33825
|
-
scanDataTree(val);
|
|
34189
|
+
scanDataTree(val, newPath);
|
|
33826
34190
|
}
|
|
33827
34191
|
}
|
|
33828
34192
|
};
|
|
@@ -33836,20 +34200,22 @@ const index = {
|
|
|
33836
34200
|
} catch (err) {
|
|
33837
34201
|
strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
|
|
33838
34202
|
}
|
|
33839
|
-
const
|
|
33840
|
-
|
|
33841
|
-
|
|
33842
|
-
|
|
33843
|
-
|
|
33844
|
-
|
|
33845
|
-
}
|
|
33846
|
-
|
|
33847
|
-
|
|
34203
|
+
const effectiveRules = [
|
|
34204
|
+
...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
|
|
34205
|
+
id: r.id || r.targetField,
|
|
34206
|
+
name: r.name || r.targetField,
|
|
34207
|
+
collectionUid: r.collectionUid || "*",
|
|
34208
|
+
targetField: r.targetField,
|
|
34209
|
+
quotas: r.quotas || {},
|
|
34210
|
+
includeRichTextHtml: Boolean(r.includeRichTextHtml),
|
|
34211
|
+
richTextFields: r.richTextFields || []
|
|
34212
|
+
}))
|
|
34213
|
+
];
|
|
33848
34214
|
const allComponents = strapi2.components || {};
|
|
33849
34215
|
for (const comp of Object.values(allComponents)) {
|
|
33850
34216
|
const attrs = comp.attributes || {};
|
|
33851
34217
|
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
33852
|
-
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !
|
|
34218
|
+
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !effectiveRules.some((r) => matchesPath(attrName, attrName, r.targetField))) {
|
|
33853
34219
|
const quotas = attrDef.options?.quotas || {};
|
|
33854
34220
|
const items = attrDef.options?.items || [];
|
|
33855
34221
|
const itemQuotas = {};
|
|
@@ -33858,35 +34224,33 @@ const index = {
|
|
|
33858
34224
|
itemQuotas[item.value] = item.maxAllowed;
|
|
33859
34225
|
}
|
|
33860
34226
|
}
|
|
33861
|
-
|
|
33862
|
-
|
|
33863
|
-
|
|
33864
|
-
|
|
33865
|
-
|
|
33866
|
-
|
|
33867
|
-
|
|
33868
|
-
|
|
33869
|
-
|
|
33870
|
-
|
|
34227
|
+
effectiveRules.push({
|
|
34228
|
+
id: `schema_${attrName}`,
|
|
34229
|
+
name: `${attrName} quota rule`,
|
|
34230
|
+
collectionUid: "*",
|
|
34231
|
+
targetField: attrName,
|
|
34232
|
+
quotas: {
|
|
34233
|
+
h1: 1,
|
|
34234
|
+
h2: 3,
|
|
34235
|
+
...itemQuotas,
|
|
34236
|
+
...quotas
|
|
34237
|
+
},
|
|
34238
|
+
includeRichTextHtml: true,
|
|
34239
|
+
richTextFields: []
|
|
34240
|
+
});
|
|
33871
34241
|
}
|
|
33872
34242
|
}
|
|
33873
34243
|
}
|
|
33874
|
-
|
|
33875
|
-
|
|
33876
|
-
quotas: { h1: 1, h2: 3 },
|
|
33877
|
-
ruleName: "Default SEO Heading Rule"
|
|
33878
|
-
};
|
|
33879
|
-
}
|
|
33880
|
-
const counts = {};
|
|
34244
|
+
const structuredCounts = {};
|
|
34245
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
33881
34246
|
let dataToScan = data;
|
|
33882
34247
|
if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
|
|
33883
34248
|
const docId = params?.documentId || params?.where?.documentId;
|
|
33884
|
-
|
|
33885
|
-
if (hasQuotaFieldInPayload && context.uid) {
|
|
34249
|
+
if (context.uid && docId) {
|
|
33886
34250
|
try {
|
|
33887
|
-
const existing = await strapi2.
|
|
33888
|
-
documentId: docId,
|
|
33889
|
-
populate:
|
|
34251
|
+
const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
|
|
34252
|
+
where: { documentId: docId },
|
|
34253
|
+
populate: true
|
|
33890
34254
|
});
|
|
33891
34255
|
if (existing) {
|
|
33892
34256
|
dataToScan = { ...existing, ...data };
|
|
@@ -33896,21 +34260,135 @@ const index = {
|
|
|
33896
34260
|
}
|
|
33897
34261
|
}
|
|
33898
34262
|
scanDataTree(dataToScan);
|
|
33899
|
-
for (const
|
|
33900
|
-
|
|
33901
|
-
|
|
33902
|
-
const
|
|
33903
|
-
|
|
33904
|
-
|
|
33905
|
-
|
|
33906
|
-
|
|
34263
|
+
for (const rule of effectiveRules) {
|
|
34264
|
+
const ruleCounts = structuredCounts[rule.id] || {};
|
|
34265
|
+
for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
|
|
34266
|
+
const lowerVal = quotaKey.toLowerCase().trim();
|
|
34267
|
+
const structCount = ruleCounts[lowerVal] || 0;
|
|
34268
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
|
|
34269
|
+
const totalFound = structCount + rtCount;
|
|
34270
|
+
if (totalFound > maxAllowed) {
|
|
34271
|
+
let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
|
|
34272
|
+
if (rtCount > 0 && structCount > 0) {
|
|
34273
|
+
detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
|
|
34274
|
+
} else if (rtCount > 0) {
|
|
34275
|
+
detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
|
|
34276
|
+
} else {
|
|
34277
|
+
detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
|
|
34278
|
+
}
|
|
34279
|
+
throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
|
|
33907
34280
|
}
|
|
33908
34281
|
}
|
|
33909
34282
|
}
|
|
33910
34283
|
}
|
|
33911
34284
|
}
|
|
33912
|
-
|
|
34285
|
+
const result = await next();
|
|
34286
|
+
return result;
|
|
33913
34287
|
});
|
|
34288
|
+
strapi2.server.use(async (ctx, next) => {
|
|
34289
|
+
if (!ctx.path.startsWith("/content-manager/history-versions")) {
|
|
34290
|
+
await next();
|
|
34291
|
+
return;
|
|
34292
|
+
}
|
|
34293
|
+
await next();
|
|
34294
|
+
if (ctx.status === 404 || ctx.status === 405) {
|
|
34295
|
+
if (ctx.method === "GET" && ctx.path === "/content-manager/history-versions") {
|
|
34296
|
+
const { contentType, documentId, locale: locale2, page = 1, pageSize = 20 } = ctx.query;
|
|
34297
|
+
if (!contentType) return;
|
|
34298
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
34299
|
+
if (!historyQuery) return;
|
|
34300
|
+
const pageNum = Math.max(1, parseInt(page, 10) || 1);
|
|
34301
|
+
const limit = Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20));
|
|
34302
|
+
const whereClause = {
|
|
34303
|
+
contentType,
|
|
34304
|
+
...documentId ? { relatedDocumentId: documentId } : {},
|
|
34305
|
+
...locale2 ? { locale: locale2 } : {}
|
|
34306
|
+
};
|
|
34307
|
+
const [results, total] = await Promise.all([
|
|
34308
|
+
historyQuery.findMany({
|
|
34309
|
+
where: whereClause,
|
|
34310
|
+
populate: ["createdBy"],
|
|
34311
|
+
orderBy: { createdAt: "desc" },
|
|
34312
|
+
limit,
|
|
34313
|
+
offset: (pageNum - 1) * limit
|
|
34314
|
+
}),
|
|
34315
|
+
historyQuery.count({ where: whereClause })
|
|
34316
|
+
]);
|
|
34317
|
+
const sanitizedResults = (results || []).map((ver) => ({
|
|
34318
|
+
...ver,
|
|
34319
|
+
createdBy: ver.createdBy ? {
|
|
34320
|
+
id: ver.createdBy.id,
|
|
34321
|
+
firstname: ver.createdBy.firstname,
|
|
34322
|
+
lastname: ver.createdBy.lastname,
|
|
34323
|
+
username: ver.createdBy.username,
|
|
34324
|
+
email: ver.createdBy.email
|
|
34325
|
+
} : void 0,
|
|
34326
|
+
meta: {
|
|
34327
|
+
unknownAttributes: { added: {}, removed: {} }
|
|
34328
|
+
}
|
|
34329
|
+
}));
|
|
34330
|
+
ctx.status = 200;
|
|
34331
|
+
ctx.body = {
|
|
34332
|
+
data: sanitizedResults,
|
|
34333
|
+
meta: {
|
|
34334
|
+
pagination: {
|
|
34335
|
+
page: pageNum,
|
|
34336
|
+
pageSize: limit,
|
|
34337
|
+
pageCount: Math.ceil(total / limit) || 1,
|
|
34338
|
+
total
|
|
34339
|
+
}
|
|
34340
|
+
}
|
|
34341
|
+
};
|
|
34342
|
+
} else if (ctx.method === "PUT" && ctx.path.endsWith("/restore")) {
|
|
34343
|
+
const match = ctx.path.match(/\/content-manager\/history-versions\/([^/]+)\/restore/);
|
|
34344
|
+
const versionId = match?.[1];
|
|
34345
|
+
if (!versionId) return;
|
|
34346
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
34347
|
+
const version = await historyQuery?.findOne?.({ where: { id: versionId } });
|
|
34348
|
+
if (!version) {
|
|
34349
|
+
ctx.status = 404;
|
|
34350
|
+
ctx.body = { error: "Version not found" };
|
|
34351
|
+
return;
|
|
34352
|
+
}
|
|
34353
|
+
const restored = await strapi2.documents(version.contentType).update({
|
|
34354
|
+
documentId: version.relatedDocumentId,
|
|
34355
|
+
locale: version.locale || void 0,
|
|
34356
|
+
data: version.data
|
|
34357
|
+
});
|
|
34358
|
+
await recordContentHistoryVersion(strapi2, {
|
|
34359
|
+
uid: version.contentType,
|
|
34360
|
+
documentId: version.relatedDocumentId,
|
|
34361
|
+
statusOverride: version.status || "published"
|
|
34362
|
+
});
|
|
34363
|
+
ctx.status = 200;
|
|
34364
|
+
ctx.body = {
|
|
34365
|
+
data: {
|
|
34366
|
+
documentId: restored?.documentId || version.relatedDocumentId
|
|
34367
|
+
}
|
|
34368
|
+
};
|
|
34369
|
+
}
|
|
34370
|
+
}
|
|
34371
|
+
});
|
|
34372
|
+
try {
|
|
34373
|
+
const knex = strapi2.db?.connection;
|
|
34374
|
+
if (knex) {
|
|
34375
|
+
if (knex.client?.config?.client === "sqlite3" || knex.client?.dialect === "sqlite3") {
|
|
34376
|
+
knex.raw("PRAGMA busy_timeout = 10000;").catch(() => {
|
|
34377
|
+
});
|
|
34378
|
+
}
|
|
34379
|
+
knex.schema.hasTable("strapi_history_versions").then((hasTable) => {
|
|
34380
|
+
if (hasTable) {
|
|
34381
|
+
knex.raw(`
|
|
34382
|
+
CREATE INDEX IF NOT EXISTS idx_history_versions_lookup
|
|
34383
|
+
ON strapi_history_versions (content_type, related_document_id, created_at)
|
|
34384
|
+
`).catch(() => {
|
|
34385
|
+
});
|
|
34386
|
+
}
|
|
34387
|
+
}).catch(() => {
|
|
34388
|
+
});
|
|
34389
|
+
}
|
|
34390
|
+
} catch {
|
|
34391
|
+
}
|
|
33914
34392
|
},
|
|
33915
34393
|
routes,
|
|
33916
34394
|
contentTypes,
|
|
@@ -33924,7 +34402,8 @@ const index = {
|
|
|
33924
34402
|
bulkUpdater: bulkUpdaterService,
|
|
33925
34403
|
audit: auditService,
|
|
33926
34404
|
validationRules: validationRulesService,
|
|
33927
|
-
csv: csvService
|
|
34405
|
+
csv: csvService,
|
|
34406
|
+
contentHistory: contentHistoryService
|
|
33928
34407
|
}
|
|
33929
34408
|
};
|
|
33930
34409
|
module.exports = index;
|