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.mjs
CHANGED
|
@@ -675,6 +675,149 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
677
|
});
|
|
678
|
+
const FIELDS_TO_IGNORE = [
|
|
679
|
+
"createdAt",
|
|
680
|
+
"updatedAt",
|
|
681
|
+
"publishedAt",
|
|
682
|
+
"createdBy",
|
|
683
|
+
"updatedBy",
|
|
684
|
+
"strapi_stage",
|
|
685
|
+
"strapi_assignee"
|
|
686
|
+
];
|
|
687
|
+
function buildHistoryPopulate(attributes, allComponents, depth2 = 0) {
|
|
688
|
+
if (depth2 > 5) return void 0;
|
|
689
|
+
const populate2 = {};
|
|
690
|
+
for (const [key, def] of Object.entries(attributes || {})) {
|
|
691
|
+
if (def.type === "component" && def.component) {
|
|
692
|
+
const comp = allComponents[def.component];
|
|
693
|
+
if (comp?.attributes) {
|
|
694
|
+
const subPopulate = buildHistoryPopulate(comp.attributes, allComponents, depth2 + 1);
|
|
695
|
+
if (subPopulate && Object.keys(subPopulate).length > 0) {
|
|
696
|
+
populate2[key] = {
|
|
697
|
+
populate: subPopulate
|
|
698
|
+
};
|
|
699
|
+
} else {
|
|
700
|
+
populate2[key] = true;
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
populate2[key] = true;
|
|
704
|
+
}
|
|
705
|
+
} else if (def.type === "dynamiczone") {
|
|
706
|
+
populate2[key] = true;
|
|
707
|
+
} else if (def.type === "relation" || def.type === "media") {
|
|
708
|
+
populate2[key] = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return Object.keys(populate2).length > 0 ? populate2 : void 0;
|
|
712
|
+
}
|
|
713
|
+
let cachedAdminUserId = null;
|
|
714
|
+
async function recordContentHistoryVersion(strapi2, options) {
|
|
715
|
+
const { uid, documentId, userId, statusOverride } = options;
|
|
716
|
+
if (!uid || !uid.startsWith("api::") || !documentId) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
721
|
+
if (!historyQuery) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const model = strapi2.getModel?.(uid);
|
|
725
|
+
if (!model) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
let entry = null;
|
|
729
|
+
try {
|
|
730
|
+
entry = await strapi2.db.query(uid).findOne({
|
|
731
|
+
where: { documentId },
|
|
732
|
+
populate: true
|
|
733
|
+
});
|
|
734
|
+
} catch {
|
|
735
|
+
try {
|
|
736
|
+
const populateOption = buildHistoryPopulate(model.attributes || {}, strapi2.components || {});
|
|
737
|
+
entry = await strapi2.documents(uid).findOne({
|
|
738
|
+
documentId,
|
|
739
|
+
...populateOption ? { populate: populateOption } : {}
|
|
740
|
+
});
|
|
741
|
+
} catch {
|
|
742
|
+
entry = null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (!entry) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const attributesSchema = model.attributes || {};
|
|
749
|
+
const sanitizedSchema = Object.fromEntries(
|
|
750
|
+
Object.entries(attributesSchema).filter(([k]) => !FIELDS_TO_IGNORE.includes(k))
|
|
751
|
+
);
|
|
752
|
+
const sanitizedData = Object.fromEntries(
|
|
753
|
+
Object.entries(entry).filter(([k]) => !FIELDS_TO_IGNORE.includes(k) && k !== "id")
|
|
754
|
+
);
|
|
755
|
+
let status2 = statusOverride || "published";
|
|
756
|
+
if (!statusOverride) {
|
|
757
|
+
const hasDraftAndPublish = Boolean(model.options?.draftAndPublish);
|
|
758
|
+
if (hasDraftAndPublish) {
|
|
759
|
+
try {
|
|
760
|
+
const docMetaService = strapi2.plugin("content-manager")?.service("document-metadata");
|
|
761
|
+
if (docMetaService) {
|
|
762
|
+
const meta = await docMetaService.getMetadata(uid, entry);
|
|
763
|
+
status2 = docMetaService.getStatus(entry, meta.availableStatus);
|
|
764
|
+
} else {
|
|
765
|
+
status2 = entry.publishedAt ? "published" : "draft";
|
|
766
|
+
}
|
|
767
|
+
} catch {
|
|
768
|
+
status2 = entry.publishedAt ? "published" : "draft";
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
status2 = "published";
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
let authorId = userId;
|
|
775
|
+
if (!authorId) {
|
|
776
|
+
authorId = strapi2.requestContext?.get?.()?.state?.user?.id || null;
|
|
777
|
+
}
|
|
778
|
+
let numericUserId = authorId ? Number(authorId) : null;
|
|
779
|
+
if (!numericUserId || isNaN(numericUserId)) {
|
|
780
|
+
if (cachedAdminUserId) {
|
|
781
|
+
numericUserId = cachedAdminUserId;
|
|
782
|
+
} else {
|
|
783
|
+
try {
|
|
784
|
+
const firstAdmin = await strapi2.db?.query?.("admin::user")?.findOne?.({ select: ["id"] });
|
|
785
|
+
if (firstAdmin?.id) {
|
|
786
|
+
cachedAdminUserId = Number(firstAdmin.id);
|
|
787
|
+
numericUserId = cachedAdminUserId;
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
await historyQuery.create({
|
|
794
|
+
data: {
|
|
795
|
+
contentType: uid,
|
|
796
|
+
relatedDocumentId: documentId,
|
|
797
|
+
locale: entry.locale || null,
|
|
798
|
+
status: status2,
|
|
799
|
+
data: sanitizedData,
|
|
800
|
+
schema: sanitizedSchema,
|
|
801
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
802
|
+
...numericUserId && !isNaN(numericUserId) ? { createdBy: numericUserId } : {}
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
} catch (err) {
|
|
806
|
+
strapi2.log?.warn?.(`[bulk-updater] Non-blocking Content History notice for ${uid}/${documentId}: ${err?.message}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
async function recordContentHistoryVersionsBatch(strapi2, items) {
|
|
810
|
+
if (!items || items.length === 0) return;
|
|
811
|
+
const BATCH_SIZE = 10;
|
|
812
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
813
|
+
const slice = items.slice(i, i + BATCH_SIZE);
|
|
814
|
+
await Promise.all(slice.map((item) => recordContentHistoryVersion(strapi2, item)));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const contentHistoryService = {
|
|
818
|
+
recordContentHistoryVersion,
|
|
819
|
+
recordContentHistoryVersionsBatch
|
|
820
|
+
};
|
|
678
821
|
const queryCache = /* @__PURE__ */ new Map();
|
|
679
822
|
const modelsCache = /* @__PURE__ */ new Map();
|
|
680
823
|
const MAX_QUERY_CACHE_SIZE = 500;
|
|
@@ -1051,6 +1194,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1051
1194
|
}
|
|
1052
1195
|
}
|
|
1053
1196
|
invalidateCollectionCache(target.uid);
|
|
1197
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1198
|
+
uid: target.uid,
|
|
1199
|
+
documentId: target.documentId,
|
|
1200
|
+
userId: currentUser?.id,
|
|
1201
|
+
statusOverride: publishOnApproval && isDraftAndPublishComp ? "published" : void 0
|
|
1202
|
+
});
|
|
1054
1203
|
return {
|
|
1055
1204
|
uid: target.uid,
|
|
1056
1205
|
collectionName: displayName,
|
|
@@ -1136,6 +1285,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1136
1285
|
if (attrDef.target) {
|
|
1137
1286
|
invalidateCollectionCache(attrDef.target);
|
|
1138
1287
|
}
|
|
1288
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1289
|
+
uid: target.uid,
|
|
1290
|
+
documentId: target.documentId,
|
|
1291
|
+
userId: currentUser?.id,
|
|
1292
|
+
statusOverride: publishOnApproval && isDraftAndPublishRel ? "published" : void 0
|
|
1293
|
+
});
|
|
1139
1294
|
return {
|
|
1140
1295
|
uid: target.uid,
|
|
1141
1296
|
collectionName: displayName,
|
|
@@ -1169,6 +1324,12 @@ async function executeBatchInternal(strapi2, {
|
|
|
1169
1324
|
strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
|
|
1170
1325
|
}
|
|
1171
1326
|
}
|
|
1327
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1328
|
+
uid: target.uid,
|
|
1329
|
+
documentId: target.documentId,
|
|
1330
|
+
userId: currentUser?.id,
|
|
1331
|
+
statusOverride: publishOnApproval && isDraftAndPublishScalar ? "published" : void 0
|
|
1332
|
+
});
|
|
1172
1333
|
return {
|
|
1173
1334
|
uid: target.uid,
|
|
1174
1335
|
collectionName: displayName,
|
|
@@ -1339,6 +1500,12 @@ async function executePublishBatchInternal(strapi2, {
|
|
|
1339
1500
|
documentId: target.documentId
|
|
1340
1501
|
});
|
|
1341
1502
|
invalidateCollectionCache(target.uid);
|
|
1503
|
+
await recordContentHistoryVersion(strapi2, {
|
|
1504
|
+
uid: target.uid,
|
|
1505
|
+
documentId: target.documentId,
|
|
1506
|
+
userId: currentUser?.id,
|
|
1507
|
+
statusOverride: "published"
|
|
1508
|
+
});
|
|
1342
1509
|
return {
|
|
1343
1510
|
uid: target.uid,
|
|
1344
1511
|
collectionName: displayName,
|
|
@@ -3194,7 +3361,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3194
3361
|
/**
|
|
3195
3362
|
* Helper to revert diff items back to original values
|
|
3196
3363
|
*/
|
|
3197
|
-
async revertDiffItems(diffs) {
|
|
3364
|
+
async revertDiffItems(diffs, userId) {
|
|
3198
3365
|
let revertedCount = 0;
|
|
3199
3366
|
const rollbackDetails = [];
|
|
3200
3367
|
for (const item of diffs) {
|
|
@@ -3252,6 +3419,15 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3252
3419
|
} catch {
|
|
3253
3420
|
}
|
|
3254
3421
|
}
|
|
3422
|
+
try {
|
|
3423
|
+
await recordContentHistoryVersion(strapi2, {
|
|
3424
|
+
uid: item.uid,
|
|
3425
|
+
documentId: item.documentId,
|
|
3426
|
+
userId,
|
|
3427
|
+
statusOverride: ct?.options?.draftAndPublish ? "published" : void 0
|
|
3428
|
+
});
|
|
3429
|
+
} catch {
|
|
3430
|
+
}
|
|
3255
3431
|
try {
|
|
3256
3432
|
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
3257
3433
|
if (bulkService?.invalidateCache) {
|
|
@@ -3302,7 +3478,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3302
3478
|
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
3303
3479
|
}
|
|
3304
3480
|
const diffs2 = Array.isArray(record2.diffs) ? record2.diffs : [];
|
|
3305
|
-
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2);
|
|
3481
|
+
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2, currentUser?.id);
|
|
3306
3482
|
record2.isRolledBack = true;
|
|
3307
3483
|
record2.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3308
3484
|
record2.rolledBackBy = currentUser?.email || "Admin";
|
|
@@ -3350,7 +3526,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
3350
3526
|
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
3351
3527
|
}
|
|
3352
3528
|
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
3353
|
-
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs);
|
|
3529
|
+
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs, currentUser?.id);
|
|
3354
3530
|
const updateData = {};
|
|
3355
3531
|
if (f.isRolledBack) updateData[f.isRolledBack] = true;
|
|
3356
3532
|
if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3391,7 +3567,8 @@ const DEFAULT_RULES = [
|
|
|
3391
3567
|
{
|
|
3392
3568
|
id: "rule_seo_heading_h1",
|
|
3393
3569
|
name: "SEO Single H1 Page Rule",
|
|
3394
|
-
description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry.",
|
|
3570
|
+
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.",
|
|
3571
|
+
collectionUid: "*",
|
|
3395
3572
|
targetField: "headingLevel",
|
|
3396
3573
|
quotas: {
|
|
3397
3574
|
h1: 1,
|
|
@@ -3400,10 +3577,56 @@ const DEFAULT_RULES = [
|
|
|
3400
3577
|
exceededAction: "disable",
|
|
3401
3578
|
enabled: true,
|
|
3402
3579
|
scope: "entry",
|
|
3580
|
+
includeRichTextHtml: true,
|
|
3581
|
+
richTextFields: [],
|
|
3403
3582
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3404
3583
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3405
3584
|
}
|
|
3406
3585
|
];
|
|
3586
|
+
function extractHeadingsFromRichText(content) {
|
|
3587
|
+
const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3588
|
+
if (!content) return counts;
|
|
3589
|
+
if (Array.isArray(content)) {
|
|
3590
|
+
for (const node of content) {
|
|
3591
|
+
if (node && typeof node === "object") {
|
|
3592
|
+
if (node.type === "heading" && typeof node.level === "number") {
|
|
3593
|
+
const tag = `h${node.level}`;
|
|
3594
|
+
if (counts[tag] !== void 0) counts[tag]++;
|
|
3595
|
+
}
|
|
3596
|
+
if (Array.isArray(node.children)) {
|
|
3597
|
+
const sub = extractHeadingsFromRichText(node.children);
|
|
3598
|
+
for (const k of Object.keys(counts)) counts[k] += sub[k];
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
return counts;
|
|
3603
|
+
}
|
|
3604
|
+
if (typeof content === "string") {
|
|
3605
|
+
for (let level = 1; level <= 6; level++) {
|
|
3606
|
+
const tag = `h${level}`;
|
|
3607
|
+
const htmlMatches = content.match(new RegExp(`<${tag}\\b[^>]*>`, "gi"));
|
|
3608
|
+
if (htmlMatches) {
|
|
3609
|
+
counts[tag] += htmlMatches.length;
|
|
3610
|
+
}
|
|
3611
|
+
const mdRegex = new RegExp(`(?:^|\\n)${"#".repeat(level)}\\s+[^\\n]+`, "g");
|
|
3612
|
+
const mdMatches = content.match(mdRegex);
|
|
3613
|
+
if (mdMatches) {
|
|
3614
|
+
counts[tag] += mdMatches.length;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return counts;
|
|
3619
|
+
}
|
|
3620
|
+
function matchesPath(normalizedPath, leafKey, targetField) {
|
|
3621
|
+
if (!targetField) return false;
|
|
3622
|
+
const pLower = (normalizedPath || "").toLowerCase().trim();
|
|
3623
|
+
const kLower = (leafKey || "").toLowerCase().trim();
|
|
3624
|
+
const tLower = targetField.toLowerCase().trim();
|
|
3625
|
+
if (!tLower.includes(".")) {
|
|
3626
|
+
return kLower === tLower || pLower === tLower || pLower.endsWith(`.${tLower}`);
|
|
3627
|
+
}
|
|
3628
|
+
return pLower === tLower || pLower.endsWith(`.${tLower}`) || pLower.endsWith(tLower);
|
|
3629
|
+
}
|
|
3407
3630
|
const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
3408
3631
|
async getStore() {
|
|
3409
3632
|
return strapi2.store({
|
|
@@ -3432,6 +3655,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3432
3655
|
savedRule = {
|
|
3433
3656
|
...rules[index2],
|
|
3434
3657
|
...ruleData,
|
|
3658
|
+
collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
|
|
3659
|
+
includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
|
|
3660
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
|
|
3435
3661
|
updatedAt: now
|
|
3436
3662
|
};
|
|
3437
3663
|
rules[index2] = savedRule;
|
|
@@ -3440,11 +3666,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3440
3666
|
id: ruleData.id,
|
|
3441
3667
|
name: ruleData.name || "New Rule",
|
|
3442
3668
|
description: ruleData.description || "",
|
|
3669
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3443
3670
|
targetField: ruleData.targetField || "headingLevel",
|
|
3444
3671
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3445
3672
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3446
3673
|
enabled: ruleData.enabled ?? true,
|
|
3447
3674
|
scope: ruleData.scope || "entry",
|
|
3675
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3676
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3448
3677
|
createdAt: now,
|
|
3449
3678
|
updatedAt: now
|
|
3450
3679
|
};
|
|
@@ -3456,11 +3685,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3456
3685
|
id,
|
|
3457
3686
|
name: ruleData.name || "Custom Quota Rule",
|
|
3458
3687
|
description: ruleData.description || "",
|
|
3688
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3459
3689
|
targetField: ruleData.targetField || "headingLevel",
|
|
3460
3690
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3461
3691
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3462
3692
|
enabled: ruleData.enabled ?? true,
|
|
3463
3693
|
scope: ruleData.scope || "entry",
|
|
3694
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3695
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3464
3696
|
createdAt: now,
|
|
3465
3697
|
updatedAt: now
|
|
3466
3698
|
};
|
|
@@ -3504,21 +3736,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3504
3736
|
let totalScanned = 0;
|
|
3505
3737
|
const allContentTypes = strapi2.contentTypes || {};
|
|
3506
3738
|
const allComponents = strapi2.components || {};
|
|
3739
|
+
function componentHasField(compUid, targetField2) {
|
|
3740
|
+
const comp = allComponents[compUid];
|
|
3741
|
+
if (!comp?.attributes) return false;
|
|
3742
|
+
for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
|
|
3743
|
+
if (matchesPath(attrName, attrName, targetField2)) return true;
|
|
3744
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3745
|
+
if (componentHasField(attrDef.component, targetField2)) return true;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
return false;
|
|
3749
|
+
}
|
|
3507
3750
|
function schemaHasField(ctSchema) {
|
|
3508
3751
|
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
3509
3752
|
const rawAttrs = ctSchema.attributes;
|
|
3510
3753
|
const populateAttrs = {};
|
|
3511
|
-
let hasField =
|
|
3754
|
+
let hasField = false;
|
|
3512
3755
|
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
3756
|
+
if (matchesPath(attrKey, attrKey, targetField)) {
|
|
3757
|
+
hasField = true;
|
|
3758
|
+
}
|
|
3513
3759
|
if (attrDef.type === "component" && attrDef.component) {
|
|
3514
|
-
|
|
3515
|
-
if (comp?.attributes?.[targetField]) {
|
|
3760
|
+
if (componentHasField(attrDef.component, targetField)) {
|
|
3516
3761
|
hasField = true;
|
|
3517
3762
|
populateAttrs[attrKey] = true;
|
|
3518
3763
|
}
|
|
3519
3764
|
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
3520
3765
|
const matchingComp = attrDef.components.some(
|
|
3521
|
-
(compUid) =>
|
|
3766
|
+
(compUid) => componentHasField(compUid, targetField)
|
|
3522
3767
|
);
|
|
3523
3768
|
if (matchingComp) {
|
|
3524
3769
|
hasField = true;
|
|
@@ -3528,19 +3773,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3528
3773
|
}
|
|
3529
3774
|
return { hasField, populateAttrs };
|
|
3530
3775
|
}
|
|
3531
|
-
const candidateUids = Object.keys(allContentTypes).filter(
|
|
3532
|
-
(uid
|
|
3533
|
-
|
|
3776
|
+
const candidateUids = Object.keys(allContentTypes).filter((uid) => {
|
|
3777
|
+
if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
|
|
3778
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
|
|
3779
|
+
return false;
|
|
3780
|
+
}
|
|
3781
|
+
return true;
|
|
3782
|
+
});
|
|
3534
3783
|
const relevantCollections = [];
|
|
3535
3784
|
for (const uid of candidateUids) {
|
|
3536
3785
|
const ct = allContentTypes[uid];
|
|
3537
3786
|
if (!ct) continue;
|
|
3538
3787
|
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
3539
|
-
if (hasField) {
|
|
3788
|
+
if (hasField || rule.collectionUid && rule.collectionUid === uid) {
|
|
3540
3789
|
relevantCollections.push({
|
|
3541
3790
|
uid,
|
|
3542
3791
|
displayName: ct.info?.displayName || uid,
|
|
3543
|
-
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs :
|
|
3792
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
|
|
3544
3793
|
});
|
|
3545
3794
|
}
|
|
3546
3795
|
}
|
|
@@ -3557,35 +3806,69 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3557
3806
|
});
|
|
3558
3807
|
if (!entries || entries.length === 0) break;
|
|
3559
3808
|
for (const entry of entries) {
|
|
3560
|
-
let traverse = function(obj) {
|
|
3809
|
+
let traverse = function(obj, currentPath = "") {
|
|
3561
3810
|
if (!obj || typeof obj !== "object") return;
|
|
3562
3811
|
for (const key of Object.keys(obj)) {
|
|
3563
3812
|
const val = obj[key];
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3813
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
3814
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
3815
|
+
if (matchesPath(normalizedPath, key, targetField) && typeof val === "string" && val.trim()) {
|
|
3816
|
+
const lower = val.toLowerCase().trim();
|
|
3817
|
+
structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
|
|
3818
|
+
}
|
|
3819
|
+
if (rule.includeRichTextHtml) {
|
|
3820
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
3821
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3822
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3823
|
+
if (count > 0) {
|
|
3824
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
3828
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3829
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3830
|
+
if (count > 0) {
|
|
3831
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3567
3836
|
}
|
|
3568
3837
|
if (typeof val === "object" && val !== null) {
|
|
3569
|
-
traverse(val);
|
|
3838
|
+
traverse(val, newPath);
|
|
3570
3839
|
}
|
|
3571
3840
|
}
|
|
3572
3841
|
};
|
|
3573
3842
|
totalScanned++;
|
|
3574
|
-
const
|
|
3843
|
+
const structuredCounts = {};
|
|
3844
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3575
3845
|
traverse(entry);
|
|
3576
3846
|
for (const [optKey, max] of Object.entries(quotas)) {
|
|
3577
|
-
const lowerOpt = optKey.toLowerCase();
|
|
3578
|
-
const
|
|
3579
|
-
|
|
3847
|
+
const lowerOpt = optKey.toLowerCase().trim();
|
|
3848
|
+
const structCount = structuredCounts[lowerOpt] || 0;
|
|
3849
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
|
|
3850
|
+
const combinedCount = structCount + rtCount;
|
|
3851
|
+
if (combinedCount > max) {
|
|
3580
3852
|
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
3853
|
+
let detail = "";
|
|
3854
|
+
if (rtCount > 0 && structCount > 0) {
|
|
3855
|
+
detail = `${structCount} structured + ${rtCount} in rich text HTML`;
|
|
3856
|
+
} else if (rtCount > 0) {
|
|
3857
|
+
detail = `${rtCount} in rich text HTML / Markdown`;
|
|
3858
|
+
} else {
|
|
3859
|
+
detail = `${structCount} in structured component`;
|
|
3860
|
+
}
|
|
3581
3861
|
violations.push({
|
|
3582
3862
|
uid,
|
|
3583
3863
|
collectionDisplayName: displayName,
|
|
3584
3864
|
documentId: entry.documentId,
|
|
3585
3865
|
entryLabel,
|
|
3586
3866
|
violatingOption: optKey.toUpperCase(),
|
|
3587
|
-
foundCount:
|
|
3867
|
+
foundCount: combinedCount,
|
|
3868
|
+
structuredCount: structCount,
|
|
3869
|
+
richTextCount: rtCount,
|
|
3588
3870
|
maxAllowed: max,
|
|
3871
|
+
detail,
|
|
3589
3872
|
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
3590
3873
|
});
|
|
3591
3874
|
}
|
|
@@ -3626,77 +3909,93 @@ function escapeCsvValue(val) {
|
|
|
3626
3909
|
}
|
|
3627
3910
|
return str2;
|
|
3628
3911
|
}
|
|
3629
|
-
function
|
|
3912
|
+
function parseCsvRows(csvText) {
|
|
3630
3913
|
if (!csvText) return [];
|
|
3631
|
-
let
|
|
3632
|
-
if (
|
|
3633
|
-
|
|
3914
|
+
let text = csvText;
|
|
3915
|
+
if (text.charCodeAt(0) === 65279) {
|
|
3916
|
+
text = text.slice(1);
|
|
3634
3917
|
}
|
|
3635
|
-
const
|
|
3636
|
-
let
|
|
3918
|
+
const rows = [];
|
|
3919
|
+
let currentRow = [];
|
|
3920
|
+
let currentCell = "";
|
|
3637
3921
|
let inQuotes = false;
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
const
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3922
|
+
let cellIsQuoted = false;
|
|
3923
|
+
for (let i = 0; i < text.length; i++) {
|
|
3924
|
+
const char = text[i];
|
|
3925
|
+
const nextChar = text[i + 1];
|
|
3926
|
+
if (inQuotes) {
|
|
3927
|
+
if (char === '"') {
|
|
3928
|
+
if (nextChar === '"') {
|
|
3929
|
+
currentCell += '"';
|
|
3930
|
+
i++;
|
|
3931
|
+
} else {
|
|
3932
|
+
inQuotes = false;
|
|
3933
|
+
}
|
|
3645
3934
|
} else {
|
|
3646
|
-
|
|
3647
|
-
}
|
|
3648
|
-
} else if ((char === "\r" || char === "\n") && !inQuotes) {
|
|
3649
|
-
if (char === "\r" && nextChar === "\n") {
|
|
3650
|
-
i++;
|
|
3651
|
-
}
|
|
3652
|
-
if (current.trim().length > 0) {
|
|
3653
|
-
lines.push(current);
|
|
3935
|
+
currentCell += char;
|
|
3654
3936
|
}
|
|
3655
|
-
current = "";
|
|
3656
3937
|
} else {
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
let inQ = false;
|
|
3668
|
-
for (let i = 0; i < line.length; i++) {
|
|
3669
|
-
const c = line[i];
|
|
3670
|
-
const nc = line[i + 1];
|
|
3671
|
-
if (c === '"') {
|
|
3672
|
-
if (inQ && nc === '"') {
|
|
3673
|
-
field += '"';
|
|
3938
|
+
if (char === '"' && !cellIsQuoted && currentCell.trim() === "") {
|
|
3939
|
+
inQuotes = true;
|
|
3940
|
+
cellIsQuoted = true;
|
|
3941
|
+
currentCell = "";
|
|
3942
|
+
} else if (char === ",") {
|
|
3943
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3944
|
+
currentCell = "";
|
|
3945
|
+
cellIsQuoted = false;
|
|
3946
|
+
} else if (char === "\r") {
|
|
3947
|
+
if (nextChar === "\n") {
|
|
3674
3948
|
i++;
|
|
3675
|
-
} else {
|
|
3676
|
-
inQ = !inQ;
|
|
3677
3949
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3950
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3951
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3952
|
+
rows.push(currentRow);
|
|
3953
|
+
}
|
|
3954
|
+
currentRow = [];
|
|
3955
|
+
currentCell = "";
|
|
3956
|
+
cellIsQuoted = false;
|
|
3957
|
+
} else if (char === "\n") {
|
|
3958
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3959
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3960
|
+
rows.push(currentRow);
|
|
3961
|
+
}
|
|
3962
|
+
currentRow = [];
|
|
3963
|
+
currentCell = "";
|
|
3964
|
+
cellIsQuoted = false;
|
|
3681
3965
|
} else {
|
|
3682
|
-
|
|
3966
|
+
if (!(cellIsQuoted && !inQuotes && (char === " " || char === " "))) {
|
|
3967
|
+
currentCell += char;
|
|
3968
|
+
}
|
|
3683
3969
|
}
|
|
3684
3970
|
}
|
|
3685
|
-
fields2.push(field);
|
|
3686
|
-
return fields2.map((f) => f.trim());
|
|
3687
3971
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
headers.forEach((h, index2) => {
|
|
3694
|
-
rowObj[h] = values[index2] !== void 0 ? values[index2] : "";
|
|
3695
|
-
});
|
|
3696
|
-
rows.push(rowObj);
|
|
3972
|
+
if (currentCell !== "" || cellIsQuoted || currentRow.length > 0) {
|
|
3973
|
+
currentRow.push(cellIsQuoted ? currentCell : currentCell.trim());
|
|
3974
|
+
if (currentRow.length > 1 || currentRow.length === 1 && currentRow[0] !== "") {
|
|
3975
|
+
rows.push(currentRow);
|
|
3976
|
+
}
|
|
3697
3977
|
}
|
|
3698
3978
|
return rows;
|
|
3699
3979
|
}
|
|
3980
|
+
function parseCsv(csvText) {
|
|
3981
|
+
const rows = parseCsvRows(csvText);
|
|
3982
|
+
if (rows.length < 2) return [];
|
|
3983
|
+
const headers = rows[0].map((h) => h.trim());
|
|
3984
|
+
const records = [];
|
|
3985
|
+
for (let i = 1; i < rows.length; i++) {
|
|
3986
|
+
const row = rows[i];
|
|
3987
|
+
const record = {};
|
|
3988
|
+
headers.forEach((h, idx) => {
|
|
3989
|
+
let val = row[idx] !== void 0 ? row[idx] : "";
|
|
3990
|
+
if (typeof val === "string" && /^'[=+@-]/.test(val)) {
|
|
3991
|
+
val = val.substring(1);
|
|
3992
|
+
}
|
|
3993
|
+
record[h] = val;
|
|
3994
|
+
});
|
|
3995
|
+
records.push(record);
|
|
3996
|
+
}
|
|
3997
|
+
return records;
|
|
3998
|
+
}
|
|
3700
3999
|
function discoverExportFields(ct, allComponents, maxDepth = 4) {
|
|
3701
4000
|
const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
|
|
3702
4001
|
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
@@ -4190,6 +4489,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4190
4489
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4191
4490
|
}
|
|
4192
4491
|
}
|
|
4492
|
+
try {
|
|
4493
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4494
|
+
uid,
|
|
4495
|
+
documentId: docId,
|
|
4496
|
+
userId: user?.id,
|
|
4497
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4498
|
+
});
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4193
4501
|
stats.updated++;
|
|
4194
4502
|
stats.diffs.push({
|
|
4195
4503
|
uid,
|
|
@@ -4225,6 +4533,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4225
4533
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4226
4534
|
}
|
|
4227
4535
|
}
|
|
4536
|
+
try {
|
|
4537
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4538
|
+
uid,
|
|
4539
|
+
documentId: docId,
|
|
4540
|
+
userId: user?.id,
|
|
4541
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4542
|
+
});
|
|
4543
|
+
} catch {
|
|
4544
|
+
}
|
|
4228
4545
|
stats.updated++;
|
|
4229
4546
|
stats.diffs.push({
|
|
4230
4547
|
uid,
|
|
@@ -4251,6 +4568,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4251
4568
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
4252
4569
|
}
|
|
4253
4570
|
}
|
|
4571
|
+
try {
|
|
4572
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4573
|
+
uid,
|
|
4574
|
+
documentId: newDocId,
|
|
4575
|
+
userId: user?.id,
|
|
4576
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4577
|
+
});
|
|
4578
|
+
} catch {
|
|
4579
|
+
}
|
|
4254
4580
|
stats.created++;
|
|
4255
4581
|
if (!docId) {
|
|
4256
4582
|
stats.newWithoutDocId++;
|
|
@@ -4289,6 +4615,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4289
4615
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
4290
4616
|
}
|
|
4291
4617
|
}
|
|
4618
|
+
try {
|
|
4619
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4620
|
+
uid,
|
|
4621
|
+
documentId: docId,
|
|
4622
|
+
userId: user?.id,
|
|
4623
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4624
|
+
});
|
|
4625
|
+
} catch {
|
|
4626
|
+
}
|
|
4292
4627
|
stats.updated++;
|
|
4293
4628
|
stats.diffs.push({
|
|
4294
4629
|
uid,
|
|
@@ -4315,6 +4650,15 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
4315
4650
|
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
4316
4651
|
}
|
|
4317
4652
|
}
|
|
4653
|
+
try {
|
|
4654
|
+
await recordContentHistoryVersion(strapi2, {
|
|
4655
|
+
uid,
|
|
4656
|
+
documentId: newDocId,
|
|
4657
|
+
userId: user?.id,
|
|
4658
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4659
|
+
});
|
|
4660
|
+
} catch {
|
|
4661
|
+
}
|
|
4318
4662
|
stats.created++;
|
|
4319
4663
|
if (!docId) {
|
|
4320
4664
|
stats.newWithoutDocId++;
|
|
@@ -33795,21 +34139,41 @@ const index = {
|
|
|
33795
34139
|
},
|
|
33796
34140
|
bootstrap({ strapi: strapi2 }) {
|
|
33797
34141
|
strapi2.documents.use(async (context, next) => {
|
|
34142
|
+
const params = context.params;
|
|
33798
34143
|
if (["create", "update"].includes(context.action)) {
|
|
33799
|
-
const params = context.params;
|
|
33800
34144
|
const data = params?.data;
|
|
33801
34145
|
if (data && typeof data === "object") {
|
|
33802
|
-
let scanDataTree = function(obj) {
|
|
34146
|
+
let scanDataTree = function(obj, currentPath = "") {
|
|
33803
34147
|
if (!obj || typeof obj !== "object") return;
|
|
33804
34148
|
for (const key of Object.keys(obj)) {
|
|
33805
34149
|
const val = obj[key];
|
|
33806
|
-
|
|
33807
|
-
|
|
33808
|
-
|
|
33809
|
-
|
|
34150
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
34151
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
34152
|
+
for (const rule of effectiveRules) {
|
|
34153
|
+
if (matchesPath(normalizedPath, key, rule.targetField) && typeof val === "string" && val.trim()) {
|
|
34154
|
+
const lowerVal = val.toLowerCase().trim();
|
|
34155
|
+
structuredCounts[rule.id] = structuredCounts[rule.id] || {};
|
|
34156
|
+
structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
|
|
34157
|
+
}
|
|
34158
|
+
}
|
|
34159
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
34160
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34161
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34162
|
+
if (count > 0) {
|
|
34163
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34164
|
+
}
|
|
34165
|
+
}
|
|
34166
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
34167
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34168
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34169
|
+
if (count > 0) {
|
|
34170
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34171
|
+
}
|
|
34172
|
+
}
|
|
34173
|
+
continue;
|
|
33810
34174
|
}
|
|
33811
34175
|
if (typeof val === "object" && val !== null) {
|
|
33812
|
-
scanDataTree(val);
|
|
34176
|
+
scanDataTree(val, newPath);
|
|
33813
34177
|
}
|
|
33814
34178
|
}
|
|
33815
34179
|
};
|
|
@@ -33823,20 +34187,22 @@ const index = {
|
|
|
33823
34187
|
} catch (err) {
|
|
33824
34188
|
strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
|
|
33825
34189
|
}
|
|
33826
|
-
const
|
|
33827
|
-
|
|
33828
|
-
|
|
33829
|
-
|
|
33830
|
-
|
|
33831
|
-
|
|
33832
|
-
}
|
|
33833
|
-
|
|
33834
|
-
|
|
34190
|
+
const effectiveRules = [
|
|
34191
|
+
...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
|
|
34192
|
+
id: r.id || r.targetField,
|
|
34193
|
+
name: r.name || r.targetField,
|
|
34194
|
+
collectionUid: r.collectionUid || "*",
|
|
34195
|
+
targetField: r.targetField,
|
|
34196
|
+
quotas: r.quotas || {},
|
|
34197
|
+
includeRichTextHtml: Boolean(r.includeRichTextHtml),
|
|
34198
|
+
richTextFields: r.richTextFields || []
|
|
34199
|
+
}))
|
|
34200
|
+
];
|
|
33835
34201
|
const allComponents = strapi2.components || {};
|
|
33836
34202
|
for (const comp of Object.values(allComponents)) {
|
|
33837
34203
|
const attrs = comp.attributes || {};
|
|
33838
34204
|
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
33839
|
-
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !
|
|
34205
|
+
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !effectiveRules.some((r) => matchesPath(attrName, attrName, r.targetField))) {
|
|
33840
34206
|
const quotas = attrDef.options?.quotas || {};
|
|
33841
34207
|
const items = attrDef.options?.items || [];
|
|
33842
34208
|
const itemQuotas = {};
|
|
@@ -33845,35 +34211,33 @@ const index = {
|
|
|
33845
34211
|
itemQuotas[item.value] = item.maxAllowed;
|
|
33846
34212
|
}
|
|
33847
34213
|
}
|
|
33848
|
-
|
|
33849
|
-
|
|
33850
|
-
|
|
33851
|
-
|
|
33852
|
-
|
|
33853
|
-
|
|
33854
|
-
|
|
33855
|
-
|
|
33856
|
-
|
|
33857
|
-
|
|
34214
|
+
effectiveRules.push({
|
|
34215
|
+
id: `schema_${attrName}`,
|
|
34216
|
+
name: `${attrName} quota rule`,
|
|
34217
|
+
collectionUid: "*",
|
|
34218
|
+
targetField: attrName,
|
|
34219
|
+
quotas: {
|
|
34220
|
+
h1: 1,
|
|
34221
|
+
h2: 3,
|
|
34222
|
+
...itemQuotas,
|
|
34223
|
+
...quotas
|
|
34224
|
+
},
|
|
34225
|
+
includeRichTextHtml: true,
|
|
34226
|
+
richTextFields: []
|
|
34227
|
+
});
|
|
33858
34228
|
}
|
|
33859
34229
|
}
|
|
33860
34230
|
}
|
|
33861
|
-
|
|
33862
|
-
|
|
33863
|
-
quotas: { h1: 1, h2: 3 },
|
|
33864
|
-
ruleName: "Default SEO Heading Rule"
|
|
33865
|
-
};
|
|
33866
|
-
}
|
|
33867
|
-
const counts = {};
|
|
34231
|
+
const structuredCounts = {};
|
|
34232
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
33868
34233
|
let dataToScan = data;
|
|
33869
34234
|
if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
|
|
33870
34235
|
const docId = params?.documentId || params?.where?.documentId;
|
|
33871
|
-
|
|
33872
|
-
if (hasQuotaFieldInPayload && context.uid) {
|
|
34236
|
+
if (context.uid && docId) {
|
|
33873
34237
|
try {
|
|
33874
|
-
const existing = await strapi2.
|
|
33875
|
-
documentId: docId,
|
|
33876
|
-
populate:
|
|
34238
|
+
const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
|
|
34239
|
+
where: { documentId: docId },
|
|
34240
|
+
populate: true
|
|
33877
34241
|
});
|
|
33878
34242
|
if (existing) {
|
|
33879
34243
|
dataToScan = { ...existing, ...data };
|
|
@@ -33883,21 +34247,135 @@ const index = {
|
|
|
33883
34247
|
}
|
|
33884
34248
|
}
|
|
33885
34249
|
scanDataTree(dataToScan);
|
|
33886
|
-
for (const
|
|
33887
|
-
|
|
33888
|
-
|
|
33889
|
-
const
|
|
33890
|
-
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
34250
|
+
for (const rule of effectiveRules) {
|
|
34251
|
+
const ruleCounts = structuredCounts[rule.id] || {};
|
|
34252
|
+
for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
|
|
34253
|
+
const lowerVal = quotaKey.toLowerCase().trim();
|
|
34254
|
+
const structCount = ruleCounts[lowerVal] || 0;
|
|
34255
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
|
|
34256
|
+
const totalFound = structCount + rtCount;
|
|
34257
|
+
if (totalFound > maxAllowed) {
|
|
34258
|
+
let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
|
|
34259
|
+
if (rtCount > 0 && structCount > 0) {
|
|
34260
|
+
detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
|
|
34261
|
+
} else if (rtCount > 0) {
|
|
34262
|
+
detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
|
|
34263
|
+
} else {
|
|
34264
|
+
detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
|
|
34265
|
+
}
|
|
34266
|
+
throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
|
|
33894
34267
|
}
|
|
33895
34268
|
}
|
|
33896
34269
|
}
|
|
33897
34270
|
}
|
|
33898
34271
|
}
|
|
33899
|
-
|
|
34272
|
+
const result = await next();
|
|
34273
|
+
return result;
|
|
33900
34274
|
});
|
|
34275
|
+
strapi2.server.use(async (ctx, next) => {
|
|
34276
|
+
if (!ctx.path.startsWith("/content-manager/history-versions")) {
|
|
34277
|
+
await next();
|
|
34278
|
+
return;
|
|
34279
|
+
}
|
|
34280
|
+
await next();
|
|
34281
|
+
if (ctx.status === 404 || ctx.status === 405) {
|
|
34282
|
+
if (ctx.method === "GET" && ctx.path === "/content-manager/history-versions") {
|
|
34283
|
+
const { contentType, documentId, locale: locale2, page = 1, pageSize = 20 } = ctx.query;
|
|
34284
|
+
if (!contentType) return;
|
|
34285
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
34286
|
+
if (!historyQuery) return;
|
|
34287
|
+
const pageNum = Math.max(1, parseInt(page, 10) || 1);
|
|
34288
|
+
const limit = Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20));
|
|
34289
|
+
const whereClause = {
|
|
34290
|
+
contentType,
|
|
34291
|
+
...documentId ? { relatedDocumentId: documentId } : {},
|
|
34292
|
+
...locale2 ? { locale: locale2 } : {}
|
|
34293
|
+
};
|
|
34294
|
+
const [results, total] = await Promise.all([
|
|
34295
|
+
historyQuery.findMany({
|
|
34296
|
+
where: whereClause,
|
|
34297
|
+
populate: ["createdBy"],
|
|
34298
|
+
orderBy: { createdAt: "desc" },
|
|
34299
|
+
limit,
|
|
34300
|
+
offset: (pageNum - 1) * limit
|
|
34301
|
+
}),
|
|
34302
|
+
historyQuery.count({ where: whereClause })
|
|
34303
|
+
]);
|
|
34304
|
+
const sanitizedResults = (results || []).map((ver) => ({
|
|
34305
|
+
...ver,
|
|
34306
|
+
createdBy: ver.createdBy ? {
|
|
34307
|
+
id: ver.createdBy.id,
|
|
34308
|
+
firstname: ver.createdBy.firstname,
|
|
34309
|
+
lastname: ver.createdBy.lastname,
|
|
34310
|
+
username: ver.createdBy.username,
|
|
34311
|
+
email: ver.createdBy.email
|
|
34312
|
+
} : void 0,
|
|
34313
|
+
meta: {
|
|
34314
|
+
unknownAttributes: { added: {}, removed: {} }
|
|
34315
|
+
}
|
|
34316
|
+
}));
|
|
34317
|
+
ctx.status = 200;
|
|
34318
|
+
ctx.body = {
|
|
34319
|
+
data: sanitizedResults,
|
|
34320
|
+
meta: {
|
|
34321
|
+
pagination: {
|
|
34322
|
+
page: pageNum,
|
|
34323
|
+
pageSize: limit,
|
|
34324
|
+
pageCount: Math.ceil(total / limit) || 1,
|
|
34325
|
+
total
|
|
34326
|
+
}
|
|
34327
|
+
}
|
|
34328
|
+
};
|
|
34329
|
+
} else if (ctx.method === "PUT" && ctx.path.endsWith("/restore")) {
|
|
34330
|
+
const match = ctx.path.match(/\/content-manager\/history-versions\/([^/]+)\/restore/);
|
|
34331
|
+
const versionId = match?.[1];
|
|
34332
|
+
if (!versionId) return;
|
|
34333
|
+
const historyQuery = strapi2.db?.query?.("plugin::content-manager.history-version");
|
|
34334
|
+
const version = await historyQuery?.findOne?.({ where: { id: versionId } });
|
|
34335
|
+
if (!version) {
|
|
34336
|
+
ctx.status = 404;
|
|
34337
|
+
ctx.body = { error: "Version not found" };
|
|
34338
|
+
return;
|
|
34339
|
+
}
|
|
34340
|
+
const restored = await strapi2.documents(version.contentType).update({
|
|
34341
|
+
documentId: version.relatedDocumentId,
|
|
34342
|
+
locale: version.locale || void 0,
|
|
34343
|
+
data: version.data
|
|
34344
|
+
});
|
|
34345
|
+
await recordContentHistoryVersion(strapi2, {
|
|
34346
|
+
uid: version.contentType,
|
|
34347
|
+
documentId: version.relatedDocumentId,
|
|
34348
|
+
statusOverride: version.status || "published"
|
|
34349
|
+
});
|
|
34350
|
+
ctx.status = 200;
|
|
34351
|
+
ctx.body = {
|
|
34352
|
+
data: {
|
|
34353
|
+
documentId: restored?.documentId || version.relatedDocumentId
|
|
34354
|
+
}
|
|
34355
|
+
};
|
|
34356
|
+
}
|
|
34357
|
+
}
|
|
34358
|
+
});
|
|
34359
|
+
try {
|
|
34360
|
+
const knex = strapi2.db?.connection;
|
|
34361
|
+
if (knex) {
|
|
34362
|
+
if (knex.client?.config?.client === "sqlite3" || knex.client?.dialect === "sqlite3") {
|
|
34363
|
+
knex.raw("PRAGMA busy_timeout = 10000;").catch(() => {
|
|
34364
|
+
});
|
|
34365
|
+
}
|
|
34366
|
+
knex.schema.hasTable("strapi_history_versions").then((hasTable) => {
|
|
34367
|
+
if (hasTable) {
|
|
34368
|
+
knex.raw(`
|
|
34369
|
+
CREATE INDEX IF NOT EXISTS idx_history_versions_lookup
|
|
34370
|
+
ON strapi_history_versions (content_type, related_document_id, created_at)
|
|
34371
|
+
`).catch(() => {
|
|
34372
|
+
});
|
|
34373
|
+
}
|
|
34374
|
+
}).catch(() => {
|
|
34375
|
+
});
|
|
34376
|
+
}
|
|
34377
|
+
} catch {
|
|
34378
|
+
}
|
|
33901
34379
|
},
|
|
33902
34380
|
routes,
|
|
33903
34381
|
contentTypes,
|
|
@@ -33911,7 +34389,8 @@ const index = {
|
|
|
33911
34389
|
bulkUpdater: bulkUpdaterService,
|
|
33912
34390
|
audit: auditService,
|
|
33913
34391
|
validationRules: validationRulesService,
|
|
33914
|
-
csv: csvService
|
|
34392
|
+
csv: csvService,
|
|
34393
|
+
contentHistory: contentHistoryService
|
|
33915
34394
|
}
|
|
33916
34395
|
};
|
|
33917
34396
|
export {
|