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
@@ -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 = Boolean(rawAttrs[targetField]);
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
- const comp = allComponents[attrDef.component];
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) => allComponents[compUid]?.attributes?.[targetField]
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) => uid.startsWith("api::") && !uid.includes("bulk-updater")
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 : void 0
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
- if (key === targetField && typeof val === "string" && val) {
3565
- const lower = val.toLowerCase();
3566
- counts[lower] = (counts[lower] || 0) + 1;
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 counts = {};
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 actualCount = counts[lowerOpt] || 0;
3579
- if (actualCount > max) {
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: actualCount,
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 parseCsv(csvText) {
3912
+ function parseCsvRows(csvText) {
3630
3913
  if (!csvText) return [];
3631
- let cleanText = csvText;
3632
- if (cleanText.charCodeAt(0) === 65279) {
3633
- cleanText = cleanText.slice(1);
3914
+ let text = csvText;
3915
+ if (text.charCodeAt(0) === 65279) {
3916
+ text = text.slice(1);
3634
3917
  }
3635
- const lines = [];
3636
- let current = "";
3918
+ const rows = [];
3919
+ let currentRow = [];
3920
+ let currentCell = "";
3637
3921
  let inQuotes = false;
3638
- for (let i = 0; i < cleanText.length; i++) {
3639
- const char = cleanText[i];
3640
- const nextChar = cleanText[i + 1];
3641
- if (char === '"') {
3642
- if (inQuotes && nextChar === '"') {
3643
- current += '"';
3644
- i++;
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
- inQuotes = !inQuotes;
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
- current += char;
3658
- }
3659
- }
3660
- if (current.trim().length > 0) {
3661
- lines.push(current);
3662
- }
3663
- if (lines.length < 1) return [];
3664
- function splitLine(line) {
3665
- const fields2 = [];
3666
- let field = "";
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
- } else if (c === "," && !inQ) {
3679
- fields2.push(field);
3680
- field = "";
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
- field += c;
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
- const headers = splitLine(lines[0]);
3689
- const rows = [];
3690
- for (let i = 1; i < lines.length; i++) {
3691
- const values = splitLine(lines[i]);
3692
- const rowObj = {};
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
- if (fieldQuotas[key] && typeof val === "string" && val) {
33807
- const lowerVal = val.toLowerCase();
33808
- counts[key] = counts[key] || {};
33809
- counts[key][lowerVal] = (counts[key][lowerVal] || 0) + 1;
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 fieldQuotas = {};
33827
- for (const rule of activeUiRules) {
33828
- if (rule.targetField && rule.quotas) {
33829
- fieldQuotas[rule.targetField] = {
33830
- quotas: rule.quotas,
33831
- ruleName: rule.name
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") && !fieldQuotas[attrName]) {
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
- const mergedQuotas = {
33849
- h1: 1,
33850
- h2: 3,
33851
- ...itemQuotas,
33852
- ...quotas
33853
- };
33854
- fieldQuotas[attrName] = {
33855
- quotas: mergedQuotas,
33856
- ruleName: `${attrName} quota rule`
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
- if (!fieldQuotas.headingLevel) {
33862
- fieldQuotas.headingLevel = {
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
- const hasQuotaFieldInPayload = Object.keys(fieldQuotas).some((k) => k in data);
33872
- if (hasQuotaFieldInPayload && context.uid) {
34236
+ if (context.uid && docId) {
33873
34237
  try {
33874
- const existing = await strapi2.documents(context.uid).findOne({
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 [fieldKey, ruleMeta] of Object.entries(fieldQuotas)) {
33887
- for (const [val, maxAllowed] of Object.entries(ruleMeta.quotas)) {
33888
- const lowerVal = val.toLowerCase();
33889
- const actualCount = counts[fieldKey]?.[lowerVal] || 0;
33890
- if (actualCount > maxAllowed) {
33891
- throw new ApplicationError(
33892
- `Validation Error (${ruleMeta.ruleName}): Option "${val.toUpperCase()}" on field "${fieldKey}" cannot appear more than ${maxAllowed} time(s) across this entry (found ${actualCount}).`
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
- return next();
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 {