strapi-plugin-bulk-updater 1.2.11 → 1.2.13
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-DS6gm_qd.js → BulkUpdater-CWJYmuGU.js} +484 -10
- package/dist/_chunks/BulkUpdater-CWJYmuGU.js.map +1 -0
- package/dist/_chunks/{BulkUpdater-B_COVQmo.mjs → BulkUpdater-Cfig2f7d.mjs} +484 -10
- package/dist/_chunks/BulkUpdater-Cfig2f7d.mjs.map +1 -0
- package/dist/_chunks/{SettingsPage-BRGTrW9H.js → SettingsPage-BAvuOb7V.js} +2 -2
- package/dist/_chunks/{SettingsPage-BRGTrW9H.js.map → SettingsPage-BAvuOb7V.js.map} +1 -1
- package/dist/_chunks/{SettingsPage-ieu-Out8.mjs → SettingsPage-BWo4l6Gd.mjs} +2 -2
- package/dist/_chunks/{SettingsPage-ieu-Out8.mjs.map → SettingsPage-BWo4l6Gd.mjs.map} +1 -1
- package/dist/_chunks/{ValidationRulesTab-BoqhoTjo.js → ValidationRulesTab-BLZWPGrZ.js} +3 -2
- package/dist/_chunks/{ValidationRulesTab-BoqhoTjo.js.map → ValidationRulesTab-BLZWPGrZ.js.map} +1 -1
- package/dist/_chunks/{ValidationRulesTab-BGcqM9cN.mjs → ValidationRulesTab-Dt53h-Ke.mjs} +3 -2
- package/dist/_chunks/{ValidationRulesTab-BGcqM9cN.mjs.map → ValidationRulesTab-Dt53h-Ke.mjs.map} +1 -1
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.mjs +2 -2
- package/dist/server/index.js +670 -81
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +670 -81
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/_chunks/BulkUpdater-B_COVQmo.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-DS6gm_qd.js.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -59,6 +59,14 @@ const routes = {
|
|
|
59
59
|
policies: []
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/batch-publish",
|
|
65
|
+
handler: "bulkUpdater.batchPublish",
|
|
66
|
+
config: {
|
|
67
|
+
policies: []
|
|
68
|
+
}
|
|
69
|
+
},
|
|
62
70
|
{
|
|
63
71
|
method: "POST",
|
|
64
72
|
path: "/preview",
|
|
@@ -195,6 +203,22 @@ const routes = {
|
|
|
195
203
|
policies: []
|
|
196
204
|
}
|
|
197
205
|
},
|
|
206
|
+
{
|
|
207
|
+
method: "POST",
|
|
208
|
+
path: "/csv/export/:uid",
|
|
209
|
+
handler: "csv.exportCollection",
|
|
210
|
+
config: {
|
|
211
|
+
policies: []
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
method: "POST",
|
|
216
|
+
path: "/csv/export-bulk",
|
|
217
|
+
handler: "csv.exportBulk",
|
|
218
|
+
config: {
|
|
219
|
+
policies: []
|
|
220
|
+
}
|
|
221
|
+
},
|
|
198
222
|
{
|
|
199
223
|
method: "GET",
|
|
200
224
|
path: "/csv/export-violations/:ruleId",
|
|
@@ -353,6 +377,17 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
353
377
|
ctx.body = { success: false, error: err.message };
|
|
354
378
|
}
|
|
355
379
|
},
|
|
380
|
+
async batchPublish(ctx) {
|
|
381
|
+
try {
|
|
382
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
383
|
+
const currentUser = ctx.state.user;
|
|
384
|
+
const result = await service.batchPublish(ctx.request.body || {}, currentUser);
|
|
385
|
+
ctx.body = result;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
ctx.status = 400;
|
|
388
|
+
ctx.body = { success: false, error: err.message };
|
|
389
|
+
}
|
|
390
|
+
},
|
|
356
391
|
async previewBatch(ctx) {
|
|
357
392
|
try {
|
|
358
393
|
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
@@ -563,20 +598,46 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
563
598
|
async exportCollection(ctx) {
|
|
564
599
|
try {
|
|
565
600
|
const { uid } = ctx.params;
|
|
566
|
-
const { search, attribute, filterOperator, filterQuery, limit } =
|
|
601
|
+
const { search, attribute, filterOperator, filterQuery, limit, documentIds } = {
|
|
602
|
+
...ctx.query,
|
|
603
|
+
...ctx.request?.body || {}
|
|
604
|
+
};
|
|
567
605
|
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
606
|
+
let parsedDocIds;
|
|
607
|
+
if (documentIds) {
|
|
608
|
+
parsedDocIds = Array.isArray(documentIds) ? documentIds : String(documentIds).split(",").map((s) => s.trim()).filter(Boolean);
|
|
609
|
+
}
|
|
568
610
|
const result = await csvService2.exportCollectionCsv(uid, {
|
|
569
611
|
search: search ? String(search) : void 0,
|
|
570
612
|
attribute: attribute ? String(attribute) : void 0,
|
|
571
613
|
filterOperator: filterOperator ? String(filterOperator) : void 0,
|
|
572
614
|
filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
|
|
573
|
-
limit: limit ? Number(limit) : void 0
|
|
615
|
+
limit: limit ? Number(limit) : void 0,
|
|
616
|
+
documentIds: parsedDocIds
|
|
574
617
|
});
|
|
575
618
|
ctx.body = {
|
|
576
619
|
success: true,
|
|
577
620
|
filename: result.filename,
|
|
578
621
|
csvContent: result.csvContent,
|
|
579
|
-
count: result.count
|
|
622
|
+
count: result.count,
|
|
623
|
+
fields: result.fields
|
|
624
|
+
};
|
|
625
|
+
} catch (err) {
|
|
626
|
+
ctx.status = 500;
|
|
627
|
+
ctx.body = { success: false, error: err.message };
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
async exportBulk(ctx) {
|
|
631
|
+
try {
|
|
632
|
+
const { selectedEntries } = ctx.request.body || {};
|
|
633
|
+
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
634
|
+
const result = await csvService2.exportBulkCsv(selectedEntries);
|
|
635
|
+
ctx.body = {
|
|
636
|
+
success: true,
|
|
637
|
+
filename: result.filename,
|
|
638
|
+
csvContent: result.csvContent,
|
|
639
|
+
count: result.count,
|
|
640
|
+
isMultiCollection: result.isMultiCollection
|
|
580
641
|
};
|
|
581
642
|
} catch (err) {
|
|
582
643
|
ctx.status = 500;
|
|
@@ -1093,6 +1154,164 @@ async function executeBatchInternal(strapi2, {
|
|
|
1093
1154
|
results
|
|
1094
1155
|
};
|
|
1095
1156
|
}
|
|
1157
|
+
async function executePublishBatchInternal(strapi2, {
|
|
1158
|
+
selectedEntries = [],
|
|
1159
|
+
selectedCollections = []
|
|
1160
|
+
}, currentUser, existingBatchId) {
|
|
1161
|
+
const targetsMap = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const colUid of selectedCollections) {
|
|
1163
|
+
if (!strapi2.contentTypes[colUid]) continue;
|
|
1164
|
+
const entries = await strapi2.documents(colUid).findMany();
|
|
1165
|
+
for (const entry of entries || []) {
|
|
1166
|
+
const key = `${colUid}@@@${entry.documentId}`;
|
|
1167
|
+
targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
for (const item of selectedEntries) {
|
|
1171
|
+
if (!item.uid || !item.documentId) continue;
|
|
1172
|
+
const key = `${item.uid}@@@${item.documentId}`;
|
|
1173
|
+
targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
|
|
1174
|
+
}
|
|
1175
|
+
if (targetsMap.size === 0) {
|
|
1176
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
1177
|
+
}
|
|
1178
|
+
const batchId = existingBatchId || `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
1179
|
+
const results = [];
|
|
1180
|
+
let publishedCount = 0;
|
|
1181
|
+
let skippedCount = 0;
|
|
1182
|
+
const jobState = {
|
|
1183
|
+
id: batchId,
|
|
1184
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1185
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1186
|
+
createdBy: currentUser ? {
|
|
1187
|
+
id: currentUser.id,
|
|
1188
|
+
email: currentUser.email,
|
|
1189
|
+
firstname: currentUser.firstname,
|
|
1190
|
+
lastname: currentUser.lastname,
|
|
1191
|
+
username: currentUser.username
|
|
1192
|
+
} : { email: "admin@system.local" },
|
|
1193
|
+
status: "processing",
|
|
1194
|
+
executionMode: "immediate",
|
|
1195
|
+
attribute: "(publish)",
|
|
1196
|
+
value: "published",
|
|
1197
|
+
totalTargets: targetsMap.size,
|
|
1198
|
+
processedCount: 0,
|
|
1199
|
+
updatedCount: 0,
|
|
1200
|
+
skippedCount: 0,
|
|
1201
|
+
failedCount: 0,
|
|
1202
|
+
isLive: false,
|
|
1203
|
+
publishOnApproval: true,
|
|
1204
|
+
results: []
|
|
1205
|
+
};
|
|
1206
|
+
batchJobsMap.set(batchId, jobState);
|
|
1207
|
+
const targets = Array.from(targetsMap.values());
|
|
1208
|
+
const CHUNK_SIZE = 25;
|
|
1209
|
+
for (let i = 0; i < targets.length; i += CHUNK_SIZE) {
|
|
1210
|
+
const chunk = targets.slice(i, i + CHUNK_SIZE);
|
|
1211
|
+
const chunkPromises = chunk.map(async (target) => {
|
|
1212
|
+
try {
|
|
1213
|
+
const contentType = strapi2.contentTypes[target.uid];
|
|
1214
|
+
const displayName = contentType?.info?.displayName || target.uid;
|
|
1215
|
+
const isDraftAndPublish = Boolean(contentType?.options?.draftAndPublish);
|
|
1216
|
+
let existingDoc = null;
|
|
1217
|
+
try {
|
|
1218
|
+
existingDoc = await strapi2.documents(target.uid).findOne({
|
|
1219
|
+
documentId: target.documentId
|
|
1220
|
+
});
|
|
1221
|
+
} catch {
|
|
1222
|
+
existingDoc = null;
|
|
1223
|
+
}
|
|
1224
|
+
const entryLabel = existingDoc?.name || existingDoc?.title || existingDoc?.label || existingDoc?.documentId || `Entry #${target.documentId}`;
|
|
1225
|
+
if (!isDraftAndPublish) {
|
|
1226
|
+
return {
|
|
1227
|
+
uid: target.uid,
|
|
1228
|
+
collectionName: displayName,
|
|
1229
|
+
documentId: target.documentId,
|
|
1230
|
+
label: entryLabel,
|
|
1231
|
+
attribute: "(publish)",
|
|
1232
|
+
status: "skipped",
|
|
1233
|
+
message: `Skipped: Draft & Publish is disabled for collection ${displayName}`
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
const isAlreadyPublished = Boolean(existingDoc?.publishedAt);
|
|
1237
|
+
await strapi2.documents(target.uid).publish({
|
|
1238
|
+
documentId: target.documentId
|
|
1239
|
+
});
|
|
1240
|
+
invalidateCollectionCache(target.uid);
|
|
1241
|
+
return {
|
|
1242
|
+
uid: target.uid,
|
|
1243
|
+
collectionName: displayName,
|
|
1244
|
+
documentId: target.documentId,
|
|
1245
|
+
label: entryLabel,
|
|
1246
|
+
attribute: "(publish)",
|
|
1247
|
+
oldValue: isAlreadyPublished ? "published" : "draft",
|
|
1248
|
+
newValue: "published",
|
|
1249
|
+
status: "updated",
|
|
1250
|
+
message: isAlreadyPublished ? "Re-published latest version live" : "Successfully published to live"
|
|
1251
|
+
};
|
|
1252
|
+
} catch (pubErr) {
|
|
1253
|
+
return {
|
|
1254
|
+
uid: target.uid,
|
|
1255
|
+
collectionName: strapi2.contentTypes[target.uid]?.info?.displayName || target.uid,
|
|
1256
|
+
documentId: target.documentId,
|
|
1257
|
+
label: `Entry #${target.documentId}`,
|
|
1258
|
+
attribute: "(publish)",
|
|
1259
|
+
status: "error",
|
|
1260
|
+
message: pubErr.message || "Failed to publish entry"
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
1265
|
+
for (const res of chunkResults) {
|
|
1266
|
+
if (res.status === "updated") publishedCount++;
|
|
1267
|
+
else if (res.status === "skipped") skippedCount++;
|
|
1268
|
+
results.push(res);
|
|
1269
|
+
}
|
|
1270
|
+
jobState.processedCount = results.length;
|
|
1271
|
+
jobState.updatedCount = publishedCount;
|
|
1272
|
+
jobState.skippedCount = skippedCount;
|
|
1273
|
+
jobState.failedCount = results.filter((r) => r.status === "error").length;
|
|
1274
|
+
}
|
|
1275
|
+
const affectedUids = new Set(targets.map((t) => t.uid));
|
|
1276
|
+
affectedUids.forEach((uid) => {
|
|
1277
|
+
invalidateCollectionCache(uid);
|
|
1278
|
+
});
|
|
1279
|
+
const failedCount = results.filter((r) => r.status === "error").length;
|
|
1280
|
+
jobState.failedCount = failedCount;
|
|
1281
|
+
jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1282
|
+
jobState.isLive = publishedCount > 0;
|
|
1283
|
+
jobState.status = failedCount === 0 && publishedCount > 0 ? "successful" : failedCount > 0 && publishedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
|
|
1284
|
+
jobState.results = results.map((r) => ({
|
|
1285
|
+
...r,
|
|
1286
|
+
isLive: r.status === "updated"
|
|
1287
|
+
}));
|
|
1288
|
+
const batchSummary = {
|
|
1289
|
+
batchId,
|
|
1290
|
+
attribute: "(publish)",
|
|
1291
|
+
value: "published",
|
|
1292
|
+
updatedCount: publishedCount,
|
|
1293
|
+
skippedCount,
|
|
1294
|
+
totalTargets: targetsMap.size,
|
|
1295
|
+
publishOnApproval: true
|
|
1296
|
+
};
|
|
1297
|
+
await strapi2.plugin("bulk-updater").service("audit").recordBatch(
|
|
1298
|
+
batchSummary,
|
|
1299
|
+
results,
|
|
1300
|
+
currentUser
|
|
1301
|
+
);
|
|
1302
|
+
return {
|
|
1303
|
+
success: true,
|
|
1304
|
+
batchId,
|
|
1305
|
+
publishedCount,
|
|
1306
|
+
updatedCount: publishedCount,
|
|
1307
|
+
skippedCount,
|
|
1308
|
+
failedCount,
|
|
1309
|
+
totalTargets: targetsMap.size,
|
|
1310
|
+
isLive: publishedCount > 0,
|
|
1311
|
+
status: jobState.status,
|
|
1312
|
+
results
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1096
1315
|
const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
1097
1316
|
if (!runnerInterval) {
|
|
1098
1317
|
runnerInterval = setInterval(async () => {
|
|
@@ -1104,7 +1323,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1104
1323
|
for (const [id, batch] of pendingBatches.entries()) {
|
|
1105
1324
|
if (batch.status === "queued_offpeak" && !isBusinessHours) {
|
|
1106
1325
|
strapi2.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
|
|
1107
|
-
const result = await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
|
|
1326
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, batch.createdBy, id) : await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
|
|
1108
1327
|
batch.status = "executed";
|
|
1109
1328
|
batch.executionResult = result;
|
|
1110
1329
|
}
|
|
@@ -1201,6 +1420,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1201
1420
|
uid,
|
|
1202
1421
|
kind: contentType.kind,
|
|
1203
1422
|
isSingleType,
|
|
1423
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1204
1424
|
singularName: contentType.info?.singularName || uid,
|
|
1205
1425
|
pluralName: contentType.info?.pluralName || uid,
|
|
1206
1426
|
displayName: contentType.info?.displayName || uid,
|
|
@@ -1382,6 +1602,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1382
1602
|
uid,
|
|
1383
1603
|
displayName: contentType.info?.displayName || uid,
|
|
1384
1604
|
isSingleType,
|
|
1605
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1385
1606
|
entries: [],
|
|
1386
1607
|
pagination: {
|
|
1387
1608
|
page: 1,
|
|
@@ -1473,6 +1694,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1473
1694
|
uid,
|
|
1474
1695
|
displayName: contentType.info?.displayName || uid,
|
|
1475
1696
|
isSingleType: true,
|
|
1697
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1476
1698
|
entries: singleDoc ? [singleDoc] : [],
|
|
1477
1699
|
pagination: {
|
|
1478
1700
|
page: 1,
|
|
@@ -1516,6 +1738,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1516
1738
|
uid,
|
|
1517
1739
|
displayName: contentType.info?.displayName || uid,
|
|
1518
1740
|
isSingleType: false,
|
|
1741
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1519
1742
|
entries: (entries || []).map((entry) => ({
|
|
1520
1743
|
id: entry.id,
|
|
1521
1744
|
documentId: entry.documentId,
|
|
@@ -2016,6 +2239,199 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2016
2239
|
currentUser
|
|
2017
2240
|
);
|
|
2018
2241
|
},
|
|
2242
|
+
/**
|
|
2243
|
+
* Bulk Publish selected entries or collections
|
|
2244
|
+
*/
|
|
2245
|
+
async batchPublish({
|
|
2246
|
+
selectedEntries = [],
|
|
2247
|
+
selectedCollections = [],
|
|
2248
|
+
executionMode = "immediate",
|
|
2249
|
+
makerNotes = "",
|
|
2250
|
+
forceImmediate = false
|
|
2251
|
+
}, currentUser) {
|
|
2252
|
+
const executionConfig = getExecutionWindowConfig(strapi2);
|
|
2253
|
+
const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
|
|
2254
|
+
if (executionMode === "maker_checker") {
|
|
2255
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2256
|
+
if (summary.totalTargets === 0) {
|
|
2257
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2258
|
+
}
|
|
2259
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2260
|
+
const batch = {
|
|
2261
|
+
id: batchId,
|
|
2262
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2263
|
+
createdBy: currentUser ? {
|
|
2264
|
+
id: currentUser.id,
|
|
2265
|
+
email: currentUser.email,
|
|
2266
|
+
firstname: currentUser.firstname,
|
|
2267
|
+
lastname: currentUser.lastname,
|
|
2268
|
+
username: currentUser.username
|
|
2269
|
+
} : { email: "admin@system.local" },
|
|
2270
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2271
|
+
summary: {
|
|
2272
|
+
attribute: "(publish)",
|
|
2273
|
+
value: "published",
|
|
2274
|
+
totalTargets: summary.totalTargets,
|
|
2275
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2276
|
+
publishOnApproval: true
|
|
2277
|
+
},
|
|
2278
|
+
status: "pending_approval",
|
|
2279
|
+
executionMode: "maker_checker",
|
|
2280
|
+
makerNotes
|
|
2281
|
+
};
|
|
2282
|
+
pendingBatches.set(batchId, batch);
|
|
2283
|
+
batchJobsMap.set(batchId, {
|
|
2284
|
+
id: batchId,
|
|
2285
|
+
createdAt: batch.createdAt,
|
|
2286
|
+
createdBy: batch.createdBy,
|
|
2287
|
+
status: "pending_approval",
|
|
2288
|
+
executionMode: "maker_checker",
|
|
2289
|
+
attribute: "(publish)",
|
|
2290
|
+
value: "published",
|
|
2291
|
+
totalTargets: summary.totalTargets,
|
|
2292
|
+
processedCount: 0,
|
|
2293
|
+
updatedCount: 0,
|
|
2294
|
+
skippedCount: 0,
|
|
2295
|
+
failedCount: 0,
|
|
2296
|
+
isLive: false,
|
|
2297
|
+
publishOnApproval: true,
|
|
2298
|
+
makerNotes,
|
|
2299
|
+
results: []
|
|
2300
|
+
});
|
|
2301
|
+
return {
|
|
2302
|
+
success: true,
|
|
2303
|
+
pending: true,
|
|
2304
|
+
batchId,
|
|
2305
|
+
status: "pending_approval",
|
|
2306
|
+
message: "Bulk publish batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
|
|
2307
|
+
batch
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
if (executionMode === "queue_offpeak") {
|
|
2311
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2312
|
+
if (summary.totalTargets === 0) {
|
|
2313
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2314
|
+
}
|
|
2315
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2316
|
+
const batch = {
|
|
2317
|
+
id: batchId,
|
|
2318
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2319
|
+
createdBy: currentUser ? {
|
|
2320
|
+
id: currentUser.id,
|
|
2321
|
+
email: currentUser.email,
|
|
2322
|
+
firstname: currentUser.firstname,
|
|
2323
|
+
lastname: currentUser.lastname,
|
|
2324
|
+
username: currentUser.username
|
|
2325
|
+
} : { email: "admin@system.local" },
|
|
2326
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2327
|
+
summary: {
|
|
2328
|
+
attribute: "(publish)",
|
|
2329
|
+
value: "published",
|
|
2330
|
+
totalTargets: summary.totalTargets,
|
|
2331
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2332
|
+
publishOnApproval: true
|
|
2333
|
+
},
|
|
2334
|
+
status: "queued_offpeak",
|
|
2335
|
+
executionMode: "queue_offpeak",
|
|
2336
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2337
|
+
makerNotes
|
|
2338
|
+
};
|
|
2339
|
+
pendingBatches.set(batchId, batch);
|
|
2340
|
+
batchJobsMap.set(batchId, {
|
|
2341
|
+
id: batchId,
|
|
2342
|
+
createdAt: batch.createdAt,
|
|
2343
|
+
createdBy: batch.createdBy,
|
|
2344
|
+
status: "queued_offpeak",
|
|
2345
|
+
executionMode: "queue_offpeak",
|
|
2346
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2347
|
+
attribute: "(publish)",
|
|
2348
|
+
value: "published",
|
|
2349
|
+
totalTargets: summary.totalTargets,
|
|
2350
|
+
processedCount: 0,
|
|
2351
|
+
updatedCount: 0,
|
|
2352
|
+
skippedCount: 0,
|
|
2353
|
+
failedCount: 0,
|
|
2354
|
+
isLive: false,
|
|
2355
|
+
publishOnApproval: true,
|
|
2356
|
+
makerNotes,
|
|
2357
|
+
results: []
|
|
2358
|
+
});
|
|
2359
|
+
return {
|
|
2360
|
+
success: true,
|
|
2361
|
+
pending: true,
|
|
2362
|
+
batchId,
|
|
2363
|
+
status: "queued_offpeak",
|
|
2364
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2365
|
+
message: `Bulk publish batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
|
|
2366
|
+
batch
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
|
|
2370
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2371
|
+
if (summary.totalTargets === 0) {
|
|
2372
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2373
|
+
}
|
|
2374
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2375
|
+
const batch = {
|
|
2376
|
+
id: batchId,
|
|
2377
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2378
|
+
createdBy: currentUser ? {
|
|
2379
|
+
id: currentUser.id,
|
|
2380
|
+
email: currentUser.email,
|
|
2381
|
+
firstname: currentUser.firstname,
|
|
2382
|
+
lastname: currentUser.lastname,
|
|
2383
|
+
username: currentUser.username
|
|
2384
|
+
} : { email: "admin@system.local" },
|
|
2385
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2386
|
+
summary: {
|
|
2387
|
+
attribute: "(publish)",
|
|
2388
|
+
value: "published",
|
|
2389
|
+
totalTargets: summary.totalTargets,
|
|
2390
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2391
|
+
publishOnApproval: true
|
|
2392
|
+
},
|
|
2393
|
+
status: "queued_offpeak",
|
|
2394
|
+
executionMode: "queue_offpeak",
|
|
2395
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2396
|
+
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
2397
|
+
};
|
|
2398
|
+
pendingBatches.set(batchId, batch);
|
|
2399
|
+
batchJobsMap.set(batchId, {
|
|
2400
|
+
id: batchId,
|
|
2401
|
+
createdAt: batch.createdAt,
|
|
2402
|
+
createdBy: batch.createdBy,
|
|
2403
|
+
status: "queued_offpeak",
|
|
2404
|
+
executionMode: "queue_offpeak",
|
|
2405
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2406
|
+
attribute: "(publish)",
|
|
2407
|
+
value: "published",
|
|
2408
|
+
totalTargets: summary.totalTargets,
|
|
2409
|
+
processedCount: 0,
|
|
2410
|
+
updatedCount: 0,
|
|
2411
|
+
skippedCount: 0,
|
|
2412
|
+
failedCount: 0,
|
|
2413
|
+
isLive: false,
|
|
2414
|
+
publishOnApproval: true,
|
|
2415
|
+
policyMessage,
|
|
2416
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
2417
|
+
results: []
|
|
2418
|
+
});
|
|
2419
|
+
return {
|
|
2420
|
+
success: true,
|
|
2421
|
+
pending: true,
|
|
2422
|
+
batchId,
|
|
2423
|
+
status: "queued_offpeak",
|
|
2424
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2425
|
+
message: policyMessage,
|
|
2426
|
+
batch
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
return await executePublishBatchInternal(
|
|
2430
|
+
strapi2,
|
|
2431
|
+
{ selectedEntries, selectedCollections },
|
|
2432
|
+
currentUser
|
|
2433
|
+
);
|
|
2434
|
+
},
|
|
2019
2435
|
/**
|
|
2020
2436
|
* Get Pending & Queued Batches
|
|
2021
2437
|
*/
|
|
@@ -2059,7 +2475,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2059
2475
|
const { isBusinessHours } = checkBusinessHours(config);
|
|
2060
2476
|
if (options.executeImmediately || !isBusinessHours) {
|
|
2061
2477
|
batch.status = "approved";
|
|
2062
|
-
const result = await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
|
|
2478
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, checkerUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
|
|
2063
2479
|
batch.status = "executed";
|
|
2064
2480
|
batch.executionResult = result;
|
|
2065
2481
|
return {
|
|
@@ -2120,7 +2536,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2120
2536
|
if (batch.status === "executed") {
|
|
2121
2537
|
throw new Error(`Batch ${batchId} has already been executed.`);
|
|
2122
2538
|
}
|
|
2123
|
-
const result = await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
|
|
2539
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, currentUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
|
|
2124
2540
|
batch.status = "executed";
|
|
2125
2541
|
batch.executionResult = result;
|
|
2126
2542
|
return {
|
|
@@ -2220,7 +2636,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2220
2636
|
function escapeCsvVal(val) {
|
|
2221
2637
|
if (val === null || val === void 0) return "";
|
|
2222
2638
|
let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
2223
|
-
|
|
2639
|
+
str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2640
|
+
if (/^[=+@-]$/.test(str2.charAt(0))) {
|
|
2641
|
+
str2 = `'${str2}`;
|
|
2642
|
+
}
|
|
2643
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
|
|
2224
2644
|
return `"${str2.replace(/"/g, '""')}"`;
|
|
2225
2645
|
}
|
|
2226
2646
|
return str2;
|
|
@@ -2264,7 +2684,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2264
2684
|
return {
|
|
2265
2685
|
success: true,
|
|
2266
2686
|
filename,
|
|
2267
|
-
csvContent: rows.join("\n"),
|
|
2687
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
2268
2688
|
summary: {
|
|
2269
2689
|
batchId: batch.id,
|
|
2270
2690
|
status: batch.status,
|
|
@@ -3088,23 +3508,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3088
3508
|
function escapeCsvValue(val) {
|
|
3089
3509
|
if (val === null || val === void 0) return "";
|
|
3090
3510
|
let str2 = "";
|
|
3091
|
-
if (
|
|
3511
|
+
if (val instanceof Date) {
|
|
3512
|
+
str2 = val.toISOString();
|
|
3513
|
+
} else if (typeof val === "object") {
|
|
3092
3514
|
str2 = JSON.stringify(val);
|
|
3093
3515
|
} else {
|
|
3094
3516
|
str2 = String(val);
|
|
3095
3517
|
}
|
|
3096
|
-
|
|
3518
|
+
str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3519
|
+
if (/^[=+@-]$/.test(str2.charAt(0))) {
|
|
3520
|
+
str2 = `'${str2}`;
|
|
3521
|
+
}
|
|
3522
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
|
|
3097
3523
|
return `"${str2.replace(/"/g, '""')}"`;
|
|
3098
3524
|
}
|
|
3099
3525
|
return str2;
|
|
3100
3526
|
}
|
|
3101
3527
|
function parseCsv(csvText) {
|
|
3528
|
+
if (!csvText) return [];
|
|
3529
|
+
let cleanText = csvText;
|
|
3530
|
+
if (cleanText.charCodeAt(0) === 65279) {
|
|
3531
|
+
cleanText = cleanText.slice(1);
|
|
3532
|
+
}
|
|
3102
3533
|
const lines = [];
|
|
3103
3534
|
let current = "";
|
|
3104
3535
|
let inQuotes = false;
|
|
3105
|
-
for (let i = 0; i <
|
|
3106
|
-
const char =
|
|
3107
|
-
const nextChar =
|
|
3536
|
+
for (let i = 0; i < cleanText.length; i++) {
|
|
3537
|
+
const char = cleanText[i];
|
|
3538
|
+
const nextChar = cleanText[i + 1];
|
|
3108
3539
|
if (char === '"') {
|
|
3109
3540
|
if (inQuotes && nextChar === '"') {
|
|
3110
3541
|
current += '"';
|
|
@@ -3164,6 +3595,103 @@ function parseCsv(csvText) {
|
|
|
3164
3595
|
}
|
|
3165
3596
|
return rows;
|
|
3166
3597
|
}
|
|
3598
|
+
function discoverExportFields(ct, allComponents, maxDepth = 4) {
|
|
3599
|
+
const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
|
|
3600
|
+
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
3601
|
+
function walkComponent(compUid, pathPrefix, labelPrefix, depth2) {
|
|
3602
|
+
if (depth2 >= maxDepth) return;
|
|
3603
|
+
const comp = allComponents[compUid];
|
|
3604
|
+
if (!comp?.attributes) return;
|
|
3605
|
+
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
3606
|
+
if (excluded.includes(subKey)) continue;
|
|
3607
|
+
const keyPath = `${pathPrefix}.${subKey}`;
|
|
3608
|
+
const displayLabel = `${labelPrefix} > ${subKey}`;
|
|
3609
|
+
if (subDef.type === "component" && subDef.component) {
|
|
3610
|
+
if (!subDef.repeatable) {
|
|
3611
|
+
walkComponent(subDef.component, keyPath, displayLabel, depth2 + 1);
|
|
3612
|
+
} else {
|
|
3613
|
+
fields2.push({ key: keyPath, label: displayLabel, type: "component" });
|
|
3614
|
+
}
|
|
3615
|
+
} else {
|
|
3616
|
+
fields2.push({ key: keyPath, label: displayLabel, type: subDef.type });
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
for (const [attrName, attrDef] of Object.entries(ct.attributes || {})) {
|
|
3621
|
+
if (excluded.includes(attrName)) continue;
|
|
3622
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3623
|
+
if (!attrDef.repeatable) {
|
|
3624
|
+
walkComponent(attrDef.component, attrName, attrName, 0);
|
|
3625
|
+
} else {
|
|
3626
|
+
fields2.push({ key: attrName, label: attrName, type: "component" });
|
|
3627
|
+
}
|
|
3628
|
+
} else {
|
|
3629
|
+
fields2.push({ key: attrName, label: attrName, type: attrDef.type });
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
return fields2;
|
|
3633
|
+
}
|
|
3634
|
+
function buildDeepPopulate(attributes, allComponents, depth2 = 0) {
|
|
3635
|
+
if (depth2 > 4) return true;
|
|
3636
|
+
const populate2 = {};
|
|
3637
|
+
for (const [key, def] of Object.entries(attributes || {})) {
|
|
3638
|
+
if (def.type === "component" && def.component) {
|
|
3639
|
+
const comp = allComponents[def.component];
|
|
3640
|
+
if (comp?.attributes) {
|
|
3641
|
+
populate2[key] = {
|
|
3642
|
+
populate: buildDeepPopulate(comp.attributes, allComponents, depth2 + 1)
|
|
3643
|
+
};
|
|
3644
|
+
} else {
|
|
3645
|
+
populate2[key] = true;
|
|
3646
|
+
}
|
|
3647
|
+
} else if (def.type === "dynamiczone") {
|
|
3648
|
+
populate2[key] = true;
|
|
3649
|
+
} else if (def.type === "relation" || def.type === "media") {
|
|
3650
|
+
populate2[key] = true;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return Object.keys(populate2).length > 0 ? populate2 : true;
|
|
3654
|
+
}
|
|
3655
|
+
function getNestedExportValue(entry, path2) {
|
|
3656
|
+
if (!entry || !path2) return "";
|
|
3657
|
+
const parts = path2.split(".");
|
|
3658
|
+
let current = entry;
|
|
3659
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3660
|
+
const part = parts[i];
|
|
3661
|
+
if (current === null || current === void 0) return "";
|
|
3662
|
+
if (Array.isArray(current)) {
|
|
3663
|
+
const subPath = parts.slice(i).join(".");
|
|
3664
|
+
return current.map((item) => {
|
|
3665
|
+
if (!item) return "";
|
|
3666
|
+
if (typeof item === "object") {
|
|
3667
|
+
const val = getNestedExportValue(item, subPath);
|
|
3668
|
+
return typeof val === "object" ? JSON.stringify(val) : String(val ?? "");
|
|
3669
|
+
}
|
|
3670
|
+
return String(item);
|
|
3671
|
+
}).filter(Boolean).join("; ");
|
|
3672
|
+
}
|
|
3673
|
+
current = current[part];
|
|
3674
|
+
}
|
|
3675
|
+
if (current === null || current === void 0) return "";
|
|
3676
|
+
if (typeof current === "object" && current.url) {
|
|
3677
|
+
return current.url;
|
|
3678
|
+
}
|
|
3679
|
+
if (typeof current === "object" && !Array.isArray(current)) {
|
|
3680
|
+
if (current.documentId || current.name || current.title) {
|
|
3681
|
+
return current.name || current.title || current.documentId;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
if (Array.isArray(current)) {
|
|
3685
|
+
return current.map((item) => {
|
|
3686
|
+
if (!item) return "";
|
|
3687
|
+
if (typeof item === "object") {
|
|
3688
|
+
return item.name || item.title || item.documentId || JSON.stringify(item);
|
|
3689
|
+
}
|
|
3690
|
+
return String(item);
|
|
3691
|
+
}).filter(Boolean).join("; ");
|
|
3692
|
+
}
|
|
3693
|
+
return current;
|
|
3694
|
+
}
|
|
3167
3695
|
const csvService = ({ strapi: strapi2 }) => ({
|
|
3168
3696
|
/**
|
|
3169
3697
|
* Generates a sample template CSV with schema attribute headers and sample values
|
|
@@ -3212,9 +3740,7 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
3212
3740
|
}
|
|
3213
3741
|
sampleRow.push(sampleVal);
|
|
3214
3742
|
}
|
|
3215
|
-
const csvContent =
|
|
3216
|
-
${sampleRow.map(escapeCsvValue).join(",")}
|
|
3217
|
-
`;
|
|
3743
|
+
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
3218
3744
|
return {
|
|
3219
3745
|
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
3220
3746
|
csvContent,
|
|
@@ -3222,65 +3748,60 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3222
3748
|
};
|
|
3223
3749
|
},
|
|
3224
3750
|
/**
|
|
3225
|
-
* Exports entries of a collection to CSV
|
|
3751
|
+
* Exports entries of a collection to CSV with deep nested component support,
|
|
3752
|
+
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
3226
3753
|
*/
|
|
3227
3754
|
async exportCollectionCsv(uid, options = {}) {
|
|
3228
3755
|
const ct = strapi2.contentTypes[uid];
|
|
3229
3756
|
if (!ct) {
|
|
3230
3757
|
throw new Error(`Content type ${uid} not found`);
|
|
3231
3758
|
}
|
|
3232
|
-
const
|
|
3233
|
-
const
|
|
3234
|
-
const exportFields = ["documentId"];
|
|
3235
|
-
const compFieldsToPopulate = [];
|
|
3236
|
-
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
3237
|
-
if (excluded.includes(attrName)) continue;
|
|
3238
|
-
if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
|
|
3239
|
-
if (attrDef.type === "component") {
|
|
3240
|
-
if (!attrDef.repeatable && attrDef.component) {
|
|
3241
|
-
const comp = strapi2.components[attrDef.component];
|
|
3242
|
-
if (comp?.attributes) {
|
|
3243
|
-
compFieldsToPopulate.push(attrName);
|
|
3244
|
-
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
3245
|
-
if (excluded.includes(subKey)) continue;
|
|
3246
|
-
if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
|
|
3247
|
-
exportFields.push(`${attrName}.${subKey}`);
|
|
3248
|
-
}
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
continue;
|
|
3252
|
-
}
|
|
3253
|
-
exportFields.push(attrName);
|
|
3254
|
-
}
|
|
3759
|
+
const allComponents = strapi2.components || {};
|
|
3760
|
+
const fields2 = discoverExportFields(ct, allComponents);
|
|
3255
3761
|
const filters2 = {};
|
|
3256
|
-
const { attribute, filterOperator, filterQuery, search } = options;
|
|
3257
|
-
if (
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
filters2[
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3762
|
+
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
3763
|
+
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
3764
|
+
filters2.documentId = { $in: documentIds };
|
|
3765
|
+
}
|
|
3766
|
+
if (attribute && filterOperator && filterOperator !== "none") {
|
|
3767
|
+
const isNested = attribute.includes(".");
|
|
3768
|
+
if (filterOperator === "empty") {
|
|
3769
|
+
if (isNested) {
|
|
3770
|
+
const [parent, child] = attribute.split(".");
|
|
3771
|
+
filters2[parent] = { [child]: { $null: true } };
|
|
3772
|
+
} else {
|
|
3773
|
+
filters2[attribute] = { $null: true };
|
|
3774
|
+
}
|
|
3775
|
+
} else if (filterOperator === "not_empty") {
|
|
3776
|
+
if (isNested) {
|
|
3777
|
+
const [parent, child] = attribute.split(".");
|
|
3778
|
+
filters2[parent] = { [child]: { $notNull: true } };
|
|
3779
|
+
} else {
|
|
3780
|
+
filters2[attribute] = { $notNull: true };
|
|
3781
|
+
}
|
|
3782
|
+
} else if (filterQuery !== void 0 && filterQuery !== "") {
|
|
3783
|
+
const opMap = {
|
|
3784
|
+
equals: "$eq",
|
|
3785
|
+
notEquals: "$ne",
|
|
3786
|
+
contains: "$containsi",
|
|
3787
|
+
notContains: "$notContainsi",
|
|
3788
|
+
startsWith: "$startsWith",
|
|
3789
|
+
endsWith: "$endsWith"
|
|
3790
|
+
};
|
|
3791
|
+
const strapiOp = opMap[filterOperator] || "$eq";
|
|
3792
|
+
if (isNested) {
|
|
3793
|
+
const [parent, child] = attribute.split(".");
|
|
3794
|
+
filters2[parent] = { [child]: { [strapiOp]: filterQuery } };
|
|
3795
|
+
} else {
|
|
3796
|
+
filters2[attribute] = { [strapiOp]: filterQuery };
|
|
3797
|
+
}
|
|
3277
3798
|
}
|
|
3278
3799
|
}
|
|
3279
3800
|
if (search && search.trim() !== "") {
|
|
3280
3801
|
const q = search.trim();
|
|
3281
3802
|
const searchConditions = [];
|
|
3282
3803
|
const searchFields = ["name", "title", "email", "slug", "description"].filter(
|
|
3283
|
-
(f) =>
|
|
3804
|
+
(f) => ct.attributes?.[f] && ["string", "text", "email"].includes(ct.attributes[f].type)
|
|
3284
3805
|
);
|
|
3285
3806
|
for (const field of searchFields) {
|
|
3286
3807
|
searchConditions.push({ [field]: { $containsi: q } });
|
|
@@ -3293,34 +3814,37 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3293
3814
|
}
|
|
3294
3815
|
}
|
|
3295
3816
|
}
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3817
|
+
const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
|
|
3818
|
+
let entries = [];
|
|
3819
|
+
if (ct.kind === "singleType") {
|
|
3820
|
+
const doc = await strapi2.documents(uid).findFirst({
|
|
3821
|
+
populate: deepPopulate
|
|
3822
|
+
});
|
|
3823
|
+
entries = doc ? [doc] : [];
|
|
3824
|
+
} else {
|
|
3825
|
+
entries = await strapi2.documents(uid).findMany({
|
|
3826
|
+
filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
|
|
3827
|
+
limit: options.limit || 5e3,
|
|
3828
|
+
sort: { createdAt: "desc" },
|
|
3829
|
+
populate: deepPopulate
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3303
3832
|
const lines = [];
|
|
3304
|
-
lines.push(
|
|
3833
|
+
lines.push(fields2.map((f) => escapeCsvValue(f.key)).join(","));
|
|
3305
3834
|
for (const entry of entries || []) {
|
|
3306
|
-
const rowValues =
|
|
3307
|
-
|
|
3308
|
-
if (field.includes(".")) {
|
|
3309
|
-
const [parent, child] = field.split(".");
|
|
3310
|
-
val = entry[parent]?.[child];
|
|
3311
|
-
} else {
|
|
3312
|
-
val = entry[field];
|
|
3313
|
-
}
|
|
3835
|
+
const rowValues = fields2.map((f) => {
|
|
3836
|
+
const val = getNestedExportValue(entry, f.key);
|
|
3314
3837
|
return escapeCsvValue(val);
|
|
3315
3838
|
});
|
|
3316
3839
|
lines.push(rowValues.join(","));
|
|
3317
3840
|
}
|
|
3318
|
-
const csvContent = lines.join("\n");
|
|
3841
|
+
const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
|
|
3319
3842
|
const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3320
3843
|
return {
|
|
3321
3844
|
filename,
|
|
3322
3845
|
csvContent,
|
|
3323
|
-
count: entries ? entries.length : 0
|
|
3846
|
+
count: entries ? entries.length : 0,
|
|
3847
|
+
fields: fields2.map((f) => f.key)
|
|
3324
3848
|
};
|
|
3325
3849
|
},
|
|
3326
3850
|
/**
|
|
@@ -3360,11 +3884,76 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3360
3884
|
const filename = `violations_${safeRuleName}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3361
3885
|
return {
|
|
3362
3886
|
filename,
|
|
3363
|
-
csvContent: lines.join("\n"),
|
|
3887
|
+
csvContent: "\uFEFF" + lines.join("\r\n") + "\r\n",
|
|
3364
3888
|
count: scanReport.violations.length,
|
|
3365
3889
|
ruleName: scanReport.rule.name
|
|
3366
3890
|
};
|
|
3367
3891
|
},
|
|
3892
|
+
/**
|
|
3893
|
+
* Bulk Export: Exports selected entries across multiple collections or a multi-collection batch.
|
|
3894
|
+
*/
|
|
3895
|
+
async exportBulkCsv(selectedEntries) {
|
|
3896
|
+
if (!selectedEntries || selectedEntries.length === 0) {
|
|
3897
|
+
throw new Error("No entries selected for bulk export");
|
|
3898
|
+
}
|
|
3899
|
+
const byUid = {};
|
|
3900
|
+
for (const item of selectedEntries) {
|
|
3901
|
+
if (!byUid[item.uid]) byUid[item.uid] = [];
|
|
3902
|
+
byUid[item.uid].push(item.documentId);
|
|
3903
|
+
}
|
|
3904
|
+
const uids = Object.keys(byUid);
|
|
3905
|
+
if (uids.length === 1) {
|
|
3906
|
+
const uid = uids[0];
|
|
3907
|
+
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
|
|
3908
|
+
}
|
|
3909
|
+
const rows = [];
|
|
3910
|
+
const headers = [
|
|
3911
|
+
"Collection UID",
|
|
3912
|
+
"Collection Name",
|
|
3913
|
+
"Document ID",
|
|
3914
|
+
"Record Title / Name",
|
|
3915
|
+
"Primary Identifier",
|
|
3916
|
+
"Created At",
|
|
3917
|
+
"Updated At",
|
|
3918
|
+
"Published Status"
|
|
3919
|
+
];
|
|
3920
|
+
rows.push(headers.map(escapeCsvValue).join(","));
|
|
3921
|
+
let totalExported = 0;
|
|
3922
|
+
for (const uid of uids) {
|
|
3923
|
+
const ct = strapi2.contentTypes[uid];
|
|
3924
|
+
if (!ct) continue;
|
|
3925
|
+
const displayName = ct.info?.displayName || uid;
|
|
3926
|
+
const docIds = byUid[uid];
|
|
3927
|
+
const entries = await strapi2.documents(uid).findMany({
|
|
3928
|
+
filters: { documentId: { $in: docIds } },
|
|
3929
|
+
limit: docIds.length
|
|
3930
|
+
});
|
|
3931
|
+
for (const entry of entries || []) {
|
|
3932
|
+
const titleOrName = entry.title || entry.name || entry.subject || entry.email || `Entry #${entry.documentId}`;
|
|
3933
|
+
const identifier = entry.slug || entry.email || entry.username || entry.documentId;
|
|
3934
|
+
const isPublished = Boolean(entry.publishedAt);
|
|
3935
|
+
const row = [
|
|
3936
|
+
uid,
|
|
3937
|
+
displayName,
|
|
3938
|
+
entry.documentId,
|
|
3939
|
+
titleOrName,
|
|
3940
|
+
identifier,
|
|
3941
|
+
entry.createdAt || "",
|
|
3942
|
+
entry.updatedAt || "",
|
|
3943
|
+
isPublished ? "Published" : "Draft"
|
|
3944
|
+
];
|
|
3945
|
+
rows.push(row.map(escapeCsvValue).join(","));
|
|
3946
|
+
totalExported++;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
const filename = `bulk_export_selected_${totalExported}_entries_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3950
|
+
return {
|
|
3951
|
+
filename,
|
|
3952
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
3953
|
+
count: totalExported,
|
|
3954
|
+
isMultiCollection: true
|
|
3955
|
+
};
|
|
3956
|
+
},
|
|
3368
3957
|
/**
|
|
3369
3958
|
* Imports CSV records to update existing records or insert/override records
|
|
3370
3959
|
*/
|