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.
Files changed (31) hide show
  1. package/dist/_chunks/{BulkUpdater-C2lw4D0D.js → BulkUpdater-BosdSbGF.js} +182 -143
  2. package/dist/_chunks/BulkUpdater-BosdSbGF.js.map +1 -0
  3. package/dist/_chunks/{BulkUpdater-CgsqzdyX.mjs → BulkUpdater-DUrmamj-.mjs} +183 -144
  4. package/dist/_chunks/BulkUpdater-DUrmamj-.mjs.map +1 -0
  5. package/dist/_chunks/{ScopedQuotaSelectInput-ChWrQIBV.mjs → ScopedQuotaSelectInput-Cy3eEj9-.mjs} +50 -23
  6. package/dist/_chunks/ScopedQuotaSelectInput-Cy3eEj9-.mjs.map +1 -0
  7. package/dist/_chunks/{ScopedQuotaSelectInput-s4oUMLgK.js → ScopedQuotaSelectInput-CyGlYa9x.js} +50 -23
  8. package/dist/_chunks/ScopedQuotaSelectInput-CyGlYa9x.js.map +1 -0
  9. package/dist/_chunks/{SettingsPage-BAvuOb7V.js → SettingsPage-B6Ia7SVS.js} +2 -2
  10. package/dist/_chunks/{SettingsPage-BAvuOb7V.js.map → SettingsPage-B6Ia7SVS.js.map} +1 -1
  11. package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs → SettingsPage-Bdse5MP4.mjs} +2 -2
  12. package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs.map → SettingsPage-Bdse5MP4.mjs.map} +1 -1
  13. package/dist/_chunks/{ValidationRulesTab-BLZWPGrZ.js → ValidationRulesTab-BqGz1VLo.js} +83 -16
  14. package/dist/_chunks/ValidationRulesTab-BqGz1VLo.js.map +1 -0
  15. package/dist/_chunks/{ValidationRulesTab-Dt53h-Ke.mjs → ValidationRulesTab-Dqkson67.mjs} +84 -17
  16. package/dist/_chunks/ValidationRulesTab-Dqkson67.mjs.map +1 -0
  17. package/dist/admin/index.js +45 -3
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +44 -4
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/server/index.js +607 -128
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/index.mjs +607 -128
  24. package/dist/server/index.mjs.map +1 -1
  25. package/package.json +3 -3
  26. package/dist/_chunks/BulkUpdater-C2lw4D0D.js.map +0 -1
  27. package/dist/_chunks/BulkUpdater-CgsqzdyX.mjs.map +0 -1
  28. package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs.map +0 -1
  29. package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js.map +0 -1
  30. package/dist/_chunks/ValidationRulesTab-BLZWPGrZ.js.map +0 -1
  31. package/dist/_chunks/ValidationRulesTab-Dt53h-Ke.mjs.map +0 -1
@@ -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 = Boolean(rawAttrs[targetField]);
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
- const comp = allComponents[attrDef.component];
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) => allComponents[compUid]?.attributes?.[targetField]
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) => uid.startsWith("api::") && !uid.includes("bulk-updater")
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 : void 0
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
- if (key === targetField && typeof val === "string" && val) {
3578
- const lower = val.toLowerCase();
3579
- counts[lower] = (counts[lower] || 0) + 1;
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 counts = {};
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 actualCount = counts[lowerOpt] || 0;
3592
- if (actualCount > max) {
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: actualCount,
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 parseCsv(csvText) {
3925
+ function parseCsvRows(csvText) {
3643
3926
  if (!csvText) return [];
3644
- let cleanText = csvText;
3645
- if (cleanText.charCodeAt(0) === 65279) {
3646
- cleanText = cleanText.slice(1);
3927
+ let text = csvText;
3928
+ if (text.charCodeAt(0) === 65279) {
3929
+ text = text.slice(1);
3647
3930
  }
3648
- const lines = [];
3649
- let current = "";
3931
+ const rows = [];
3932
+ let currentRow = [];
3933
+ let currentCell = "";
3650
3934
  let inQuotes = false;
3651
- for (let i = 0; i < cleanText.length; i++) {
3652
- const char = cleanText[i];
3653
- const nextChar = cleanText[i + 1];
3654
- if (char === '"') {
3655
- if (inQuotes && nextChar === '"') {
3656
- current += '"';
3657
- i++;
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
- inQuotes = !inQuotes;
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
- current += char;
3671
- }
3672
- }
3673
- if (current.trim().length > 0) {
3674
- lines.push(current);
3675
- }
3676
- if (lines.length < 1) return [];
3677
- function splitLine(line) {
3678
- const fields2 = [];
3679
- let field = "";
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
- } else if (c === "," && !inQ) {
3692
- fields2.push(field);
3693
- field = "";
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
- field += c;
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
- const headers = splitLine(lines[0]);
3702
- const rows = [];
3703
- for (let i = 1; i < lines.length; i++) {
3704
- const values = splitLine(lines[i]);
3705
- const rowObj = {};
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
- if (fieldQuotas[key] && typeof val === "string" && val) {
33820
- const lowerVal = val.toLowerCase();
33821
- counts[key] = counts[key] || {};
33822
- counts[key][lowerVal] = (counts[key][lowerVal] || 0) + 1;
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 fieldQuotas = {};
33840
- for (const rule of activeUiRules) {
33841
- if (rule.targetField && rule.quotas) {
33842
- fieldQuotas[rule.targetField] = {
33843
- quotas: rule.quotas,
33844
- ruleName: rule.name
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") && !fieldQuotas[attrName]) {
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
- const mergedQuotas = {
33862
- h1: 1,
33863
- h2: 3,
33864
- ...itemQuotas,
33865
- ...quotas
33866
- };
33867
- fieldQuotas[attrName] = {
33868
- quotas: mergedQuotas,
33869
- ruleName: `${attrName} quota rule`
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
- if (!fieldQuotas.headingLevel) {
33875
- fieldQuotas.headingLevel = {
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
- const hasQuotaFieldInPayload = Object.keys(fieldQuotas).some((k) => k in data);
33885
- if (hasQuotaFieldInPayload && context.uid) {
34249
+ if (context.uid && docId) {
33886
34250
  try {
33887
- const existing = await strapi2.documents(context.uid).findOne({
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 [fieldKey, ruleMeta] of Object.entries(fieldQuotas)) {
33900
- for (const [val, maxAllowed] of Object.entries(ruleMeta.quotas)) {
33901
- const lowerVal = val.toLowerCase();
33902
- const actualCount = counts[fieldKey]?.[lowerVal] || 0;
33903
- if (actualCount > maxAllowed) {
33904
- throw new ApplicationError(
33905
- `Validation Error (${ruleMeta.ruleName}): Option "${val.toUpperCase()}" on field "${fieldKey}" cannot appear more than ${maxAllowed} time(s) across this entry (found ${actualCount}).`
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
- return next();
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;