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.mjs
CHANGED
|
@@ -46,6 +46,14 @@ const routes = {
|
|
|
46
46
|
policies: []
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
method: "POST",
|
|
51
|
+
path: "/batch-publish",
|
|
52
|
+
handler: "bulkUpdater.batchPublish",
|
|
53
|
+
config: {
|
|
54
|
+
policies: []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
49
57
|
{
|
|
50
58
|
method: "POST",
|
|
51
59
|
path: "/preview",
|
|
@@ -182,6 +190,22 @@ const routes = {
|
|
|
182
190
|
policies: []
|
|
183
191
|
}
|
|
184
192
|
},
|
|
193
|
+
{
|
|
194
|
+
method: "POST",
|
|
195
|
+
path: "/csv/export/:uid",
|
|
196
|
+
handler: "csv.exportCollection",
|
|
197
|
+
config: {
|
|
198
|
+
policies: []
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
method: "POST",
|
|
203
|
+
path: "/csv/export-bulk",
|
|
204
|
+
handler: "csv.exportBulk",
|
|
205
|
+
config: {
|
|
206
|
+
policies: []
|
|
207
|
+
}
|
|
208
|
+
},
|
|
185
209
|
{
|
|
186
210
|
method: "GET",
|
|
187
211
|
path: "/csv/export-violations/:ruleId",
|
|
@@ -340,6 +364,17 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
340
364
|
ctx.body = { success: false, error: err.message };
|
|
341
365
|
}
|
|
342
366
|
},
|
|
367
|
+
async batchPublish(ctx) {
|
|
368
|
+
try {
|
|
369
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
370
|
+
const currentUser = ctx.state.user;
|
|
371
|
+
const result = await service.batchPublish(ctx.request.body || {}, currentUser);
|
|
372
|
+
ctx.body = result;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
ctx.status = 400;
|
|
375
|
+
ctx.body = { success: false, error: err.message };
|
|
376
|
+
}
|
|
377
|
+
},
|
|
343
378
|
async previewBatch(ctx) {
|
|
344
379
|
try {
|
|
345
380
|
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
@@ -550,20 +585,46 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
550
585
|
async exportCollection(ctx) {
|
|
551
586
|
try {
|
|
552
587
|
const { uid } = ctx.params;
|
|
553
|
-
const { search, attribute, filterOperator, filterQuery, limit } =
|
|
588
|
+
const { search, attribute, filterOperator, filterQuery, limit, documentIds } = {
|
|
589
|
+
...ctx.query,
|
|
590
|
+
...ctx.request?.body || {}
|
|
591
|
+
};
|
|
554
592
|
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
593
|
+
let parsedDocIds;
|
|
594
|
+
if (documentIds) {
|
|
595
|
+
parsedDocIds = Array.isArray(documentIds) ? documentIds : String(documentIds).split(",").map((s) => s.trim()).filter(Boolean);
|
|
596
|
+
}
|
|
555
597
|
const result = await csvService2.exportCollectionCsv(uid, {
|
|
556
598
|
search: search ? String(search) : void 0,
|
|
557
599
|
attribute: attribute ? String(attribute) : void 0,
|
|
558
600
|
filterOperator: filterOperator ? String(filterOperator) : void 0,
|
|
559
601
|
filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
|
|
560
|
-
limit: limit ? Number(limit) : void 0
|
|
602
|
+
limit: limit ? Number(limit) : void 0,
|
|
603
|
+
documentIds: parsedDocIds
|
|
561
604
|
});
|
|
562
605
|
ctx.body = {
|
|
563
606
|
success: true,
|
|
564
607
|
filename: result.filename,
|
|
565
608
|
csvContent: result.csvContent,
|
|
566
|
-
count: result.count
|
|
609
|
+
count: result.count,
|
|
610
|
+
fields: result.fields
|
|
611
|
+
};
|
|
612
|
+
} catch (err) {
|
|
613
|
+
ctx.status = 500;
|
|
614
|
+
ctx.body = { success: false, error: err.message };
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
async exportBulk(ctx) {
|
|
618
|
+
try {
|
|
619
|
+
const { selectedEntries } = ctx.request.body || {};
|
|
620
|
+
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
621
|
+
const result = await csvService2.exportBulkCsv(selectedEntries);
|
|
622
|
+
ctx.body = {
|
|
623
|
+
success: true,
|
|
624
|
+
filename: result.filename,
|
|
625
|
+
csvContent: result.csvContent,
|
|
626
|
+
count: result.count,
|
|
627
|
+
isMultiCollection: result.isMultiCollection
|
|
567
628
|
};
|
|
568
629
|
} catch (err) {
|
|
569
630
|
ctx.status = 500;
|
|
@@ -1080,6 +1141,164 @@ async function executeBatchInternal(strapi2, {
|
|
|
1080
1141
|
results
|
|
1081
1142
|
};
|
|
1082
1143
|
}
|
|
1144
|
+
async function executePublishBatchInternal(strapi2, {
|
|
1145
|
+
selectedEntries = [],
|
|
1146
|
+
selectedCollections = []
|
|
1147
|
+
}, currentUser, existingBatchId) {
|
|
1148
|
+
const targetsMap = /* @__PURE__ */ new Map();
|
|
1149
|
+
for (const colUid of selectedCollections) {
|
|
1150
|
+
if (!strapi2.contentTypes[colUid]) continue;
|
|
1151
|
+
const entries = await strapi2.documents(colUid).findMany();
|
|
1152
|
+
for (const entry of entries || []) {
|
|
1153
|
+
const key = `${colUid}@@@${entry.documentId}`;
|
|
1154
|
+
targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const item of selectedEntries) {
|
|
1158
|
+
if (!item.uid || !item.documentId) continue;
|
|
1159
|
+
const key = `${item.uid}@@@${item.documentId}`;
|
|
1160
|
+
targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
|
|
1161
|
+
}
|
|
1162
|
+
if (targetsMap.size === 0) {
|
|
1163
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
1164
|
+
}
|
|
1165
|
+
const batchId = existingBatchId || `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
1166
|
+
const results = [];
|
|
1167
|
+
let publishedCount = 0;
|
|
1168
|
+
let skippedCount = 0;
|
|
1169
|
+
const jobState = {
|
|
1170
|
+
id: batchId,
|
|
1171
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1172
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1173
|
+
createdBy: currentUser ? {
|
|
1174
|
+
id: currentUser.id,
|
|
1175
|
+
email: currentUser.email,
|
|
1176
|
+
firstname: currentUser.firstname,
|
|
1177
|
+
lastname: currentUser.lastname,
|
|
1178
|
+
username: currentUser.username
|
|
1179
|
+
} : { email: "admin@system.local" },
|
|
1180
|
+
status: "processing",
|
|
1181
|
+
executionMode: "immediate",
|
|
1182
|
+
attribute: "(publish)",
|
|
1183
|
+
value: "published",
|
|
1184
|
+
totalTargets: targetsMap.size,
|
|
1185
|
+
processedCount: 0,
|
|
1186
|
+
updatedCount: 0,
|
|
1187
|
+
skippedCount: 0,
|
|
1188
|
+
failedCount: 0,
|
|
1189
|
+
isLive: false,
|
|
1190
|
+
publishOnApproval: true,
|
|
1191
|
+
results: []
|
|
1192
|
+
};
|
|
1193
|
+
batchJobsMap.set(batchId, jobState);
|
|
1194
|
+
const targets = Array.from(targetsMap.values());
|
|
1195
|
+
const CHUNK_SIZE = 25;
|
|
1196
|
+
for (let i = 0; i < targets.length; i += CHUNK_SIZE) {
|
|
1197
|
+
const chunk = targets.slice(i, i + CHUNK_SIZE);
|
|
1198
|
+
const chunkPromises = chunk.map(async (target) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const contentType = strapi2.contentTypes[target.uid];
|
|
1201
|
+
const displayName = contentType?.info?.displayName || target.uid;
|
|
1202
|
+
const isDraftAndPublish = Boolean(contentType?.options?.draftAndPublish);
|
|
1203
|
+
let existingDoc = null;
|
|
1204
|
+
try {
|
|
1205
|
+
existingDoc = await strapi2.documents(target.uid).findOne({
|
|
1206
|
+
documentId: target.documentId
|
|
1207
|
+
});
|
|
1208
|
+
} catch {
|
|
1209
|
+
existingDoc = null;
|
|
1210
|
+
}
|
|
1211
|
+
const entryLabel = existingDoc?.name || existingDoc?.title || existingDoc?.label || existingDoc?.documentId || `Entry #${target.documentId}`;
|
|
1212
|
+
if (!isDraftAndPublish) {
|
|
1213
|
+
return {
|
|
1214
|
+
uid: target.uid,
|
|
1215
|
+
collectionName: displayName,
|
|
1216
|
+
documentId: target.documentId,
|
|
1217
|
+
label: entryLabel,
|
|
1218
|
+
attribute: "(publish)",
|
|
1219
|
+
status: "skipped",
|
|
1220
|
+
message: `Skipped: Draft & Publish is disabled for collection ${displayName}`
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
const isAlreadyPublished = Boolean(existingDoc?.publishedAt);
|
|
1224
|
+
await strapi2.documents(target.uid).publish({
|
|
1225
|
+
documentId: target.documentId
|
|
1226
|
+
});
|
|
1227
|
+
invalidateCollectionCache(target.uid);
|
|
1228
|
+
return {
|
|
1229
|
+
uid: target.uid,
|
|
1230
|
+
collectionName: displayName,
|
|
1231
|
+
documentId: target.documentId,
|
|
1232
|
+
label: entryLabel,
|
|
1233
|
+
attribute: "(publish)",
|
|
1234
|
+
oldValue: isAlreadyPublished ? "published" : "draft",
|
|
1235
|
+
newValue: "published",
|
|
1236
|
+
status: "updated",
|
|
1237
|
+
message: isAlreadyPublished ? "Re-published latest version live" : "Successfully published to live"
|
|
1238
|
+
};
|
|
1239
|
+
} catch (pubErr) {
|
|
1240
|
+
return {
|
|
1241
|
+
uid: target.uid,
|
|
1242
|
+
collectionName: strapi2.contentTypes[target.uid]?.info?.displayName || target.uid,
|
|
1243
|
+
documentId: target.documentId,
|
|
1244
|
+
label: `Entry #${target.documentId}`,
|
|
1245
|
+
attribute: "(publish)",
|
|
1246
|
+
status: "error",
|
|
1247
|
+
message: pubErr.message || "Failed to publish entry"
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
1252
|
+
for (const res of chunkResults) {
|
|
1253
|
+
if (res.status === "updated") publishedCount++;
|
|
1254
|
+
else if (res.status === "skipped") skippedCount++;
|
|
1255
|
+
results.push(res);
|
|
1256
|
+
}
|
|
1257
|
+
jobState.processedCount = results.length;
|
|
1258
|
+
jobState.updatedCount = publishedCount;
|
|
1259
|
+
jobState.skippedCount = skippedCount;
|
|
1260
|
+
jobState.failedCount = results.filter((r) => r.status === "error").length;
|
|
1261
|
+
}
|
|
1262
|
+
const affectedUids = new Set(targets.map((t) => t.uid));
|
|
1263
|
+
affectedUids.forEach((uid) => {
|
|
1264
|
+
invalidateCollectionCache(uid);
|
|
1265
|
+
});
|
|
1266
|
+
const failedCount = results.filter((r) => r.status === "error").length;
|
|
1267
|
+
jobState.failedCount = failedCount;
|
|
1268
|
+
jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1269
|
+
jobState.isLive = publishedCount > 0;
|
|
1270
|
+
jobState.status = failedCount === 0 && publishedCount > 0 ? "successful" : failedCount > 0 && publishedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
|
|
1271
|
+
jobState.results = results.map((r) => ({
|
|
1272
|
+
...r,
|
|
1273
|
+
isLive: r.status === "updated"
|
|
1274
|
+
}));
|
|
1275
|
+
const batchSummary = {
|
|
1276
|
+
batchId,
|
|
1277
|
+
attribute: "(publish)",
|
|
1278
|
+
value: "published",
|
|
1279
|
+
updatedCount: publishedCount,
|
|
1280
|
+
skippedCount,
|
|
1281
|
+
totalTargets: targetsMap.size,
|
|
1282
|
+
publishOnApproval: true
|
|
1283
|
+
};
|
|
1284
|
+
await strapi2.plugin("bulk-updater").service("audit").recordBatch(
|
|
1285
|
+
batchSummary,
|
|
1286
|
+
results,
|
|
1287
|
+
currentUser
|
|
1288
|
+
);
|
|
1289
|
+
return {
|
|
1290
|
+
success: true,
|
|
1291
|
+
batchId,
|
|
1292
|
+
publishedCount,
|
|
1293
|
+
updatedCount: publishedCount,
|
|
1294
|
+
skippedCount,
|
|
1295
|
+
failedCount,
|
|
1296
|
+
totalTargets: targetsMap.size,
|
|
1297
|
+
isLive: publishedCount > 0,
|
|
1298
|
+
status: jobState.status,
|
|
1299
|
+
results
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1083
1302
|
const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
1084
1303
|
if (!runnerInterval) {
|
|
1085
1304
|
runnerInterval = setInterval(async () => {
|
|
@@ -1091,7 +1310,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1091
1310
|
for (const [id, batch] of pendingBatches.entries()) {
|
|
1092
1311
|
if (batch.status === "queued_offpeak" && !isBusinessHours) {
|
|
1093
1312
|
strapi2.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
|
|
1094
|
-
const result = await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
|
|
1313
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, batch.createdBy, id) : await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
|
|
1095
1314
|
batch.status = "executed";
|
|
1096
1315
|
batch.executionResult = result;
|
|
1097
1316
|
}
|
|
@@ -1188,6 +1407,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1188
1407
|
uid,
|
|
1189
1408
|
kind: contentType.kind,
|
|
1190
1409
|
isSingleType,
|
|
1410
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1191
1411
|
singularName: contentType.info?.singularName || uid,
|
|
1192
1412
|
pluralName: contentType.info?.pluralName || uid,
|
|
1193
1413
|
displayName: contentType.info?.displayName || uid,
|
|
@@ -1369,6 +1589,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1369
1589
|
uid,
|
|
1370
1590
|
displayName: contentType.info?.displayName || uid,
|
|
1371
1591
|
isSingleType,
|
|
1592
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1372
1593
|
entries: [],
|
|
1373
1594
|
pagination: {
|
|
1374
1595
|
page: 1,
|
|
@@ -1460,6 +1681,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1460
1681
|
uid,
|
|
1461
1682
|
displayName: contentType.info?.displayName || uid,
|
|
1462
1683
|
isSingleType: true,
|
|
1684
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1463
1685
|
entries: singleDoc ? [singleDoc] : [],
|
|
1464
1686
|
pagination: {
|
|
1465
1687
|
page: 1,
|
|
@@ -1503,6 +1725,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1503
1725
|
uid,
|
|
1504
1726
|
displayName: contentType.info?.displayName || uid,
|
|
1505
1727
|
isSingleType: false,
|
|
1728
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
1506
1729
|
entries: (entries || []).map((entry) => ({
|
|
1507
1730
|
id: entry.id,
|
|
1508
1731
|
documentId: entry.documentId,
|
|
@@ -2003,6 +2226,199 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2003
2226
|
currentUser
|
|
2004
2227
|
);
|
|
2005
2228
|
},
|
|
2229
|
+
/**
|
|
2230
|
+
* Bulk Publish selected entries or collections
|
|
2231
|
+
*/
|
|
2232
|
+
async batchPublish({
|
|
2233
|
+
selectedEntries = [],
|
|
2234
|
+
selectedCollections = [],
|
|
2235
|
+
executionMode = "immediate",
|
|
2236
|
+
makerNotes = "",
|
|
2237
|
+
forceImmediate = false
|
|
2238
|
+
}, currentUser) {
|
|
2239
|
+
const executionConfig = getExecutionWindowConfig(strapi2);
|
|
2240
|
+
const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
|
|
2241
|
+
if (executionMode === "maker_checker") {
|
|
2242
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2243
|
+
if (summary.totalTargets === 0) {
|
|
2244
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2245
|
+
}
|
|
2246
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2247
|
+
const batch = {
|
|
2248
|
+
id: batchId,
|
|
2249
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2250
|
+
createdBy: currentUser ? {
|
|
2251
|
+
id: currentUser.id,
|
|
2252
|
+
email: currentUser.email,
|
|
2253
|
+
firstname: currentUser.firstname,
|
|
2254
|
+
lastname: currentUser.lastname,
|
|
2255
|
+
username: currentUser.username
|
|
2256
|
+
} : { email: "admin@system.local" },
|
|
2257
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2258
|
+
summary: {
|
|
2259
|
+
attribute: "(publish)",
|
|
2260
|
+
value: "published",
|
|
2261
|
+
totalTargets: summary.totalTargets,
|
|
2262
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2263
|
+
publishOnApproval: true
|
|
2264
|
+
},
|
|
2265
|
+
status: "pending_approval",
|
|
2266
|
+
executionMode: "maker_checker",
|
|
2267
|
+
makerNotes
|
|
2268
|
+
};
|
|
2269
|
+
pendingBatches.set(batchId, batch);
|
|
2270
|
+
batchJobsMap.set(batchId, {
|
|
2271
|
+
id: batchId,
|
|
2272
|
+
createdAt: batch.createdAt,
|
|
2273
|
+
createdBy: batch.createdBy,
|
|
2274
|
+
status: "pending_approval",
|
|
2275
|
+
executionMode: "maker_checker",
|
|
2276
|
+
attribute: "(publish)",
|
|
2277
|
+
value: "published",
|
|
2278
|
+
totalTargets: summary.totalTargets,
|
|
2279
|
+
processedCount: 0,
|
|
2280
|
+
updatedCount: 0,
|
|
2281
|
+
skippedCount: 0,
|
|
2282
|
+
failedCount: 0,
|
|
2283
|
+
isLive: false,
|
|
2284
|
+
publishOnApproval: true,
|
|
2285
|
+
makerNotes,
|
|
2286
|
+
results: []
|
|
2287
|
+
});
|
|
2288
|
+
return {
|
|
2289
|
+
success: true,
|
|
2290
|
+
pending: true,
|
|
2291
|
+
batchId,
|
|
2292
|
+
status: "pending_approval",
|
|
2293
|
+
message: "Bulk publish batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
|
|
2294
|
+
batch
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
if (executionMode === "queue_offpeak") {
|
|
2298
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2299
|
+
if (summary.totalTargets === 0) {
|
|
2300
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2301
|
+
}
|
|
2302
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2303
|
+
const batch = {
|
|
2304
|
+
id: batchId,
|
|
2305
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2306
|
+
createdBy: currentUser ? {
|
|
2307
|
+
id: currentUser.id,
|
|
2308
|
+
email: currentUser.email,
|
|
2309
|
+
firstname: currentUser.firstname,
|
|
2310
|
+
lastname: currentUser.lastname,
|
|
2311
|
+
username: currentUser.username
|
|
2312
|
+
} : { email: "admin@system.local" },
|
|
2313
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2314
|
+
summary: {
|
|
2315
|
+
attribute: "(publish)",
|
|
2316
|
+
value: "published",
|
|
2317
|
+
totalTargets: summary.totalTargets,
|
|
2318
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2319
|
+
publishOnApproval: true
|
|
2320
|
+
},
|
|
2321
|
+
status: "queued_offpeak",
|
|
2322
|
+
executionMode: "queue_offpeak",
|
|
2323
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2324
|
+
makerNotes
|
|
2325
|
+
};
|
|
2326
|
+
pendingBatches.set(batchId, batch);
|
|
2327
|
+
batchJobsMap.set(batchId, {
|
|
2328
|
+
id: batchId,
|
|
2329
|
+
createdAt: batch.createdAt,
|
|
2330
|
+
createdBy: batch.createdBy,
|
|
2331
|
+
status: "queued_offpeak",
|
|
2332
|
+
executionMode: "queue_offpeak",
|
|
2333
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2334
|
+
attribute: "(publish)",
|
|
2335
|
+
value: "published",
|
|
2336
|
+
totalTargets: summary.totalTargets,
|
|
2337
|
+
processedCount: 0,
|
|
2338
|
+
updatedCount: 0,
|
|
2339
|
+
skippedCount: 0,
|
|
2340
|
+
failedCount: 0,
|
|
2341
|
+
isLive: false,
|
|
2342
|
+
publishOnApproval: true,
|
|
2343
|
+
makerNotes,
|
|
2344
|
+
results: []
|
|
2345
|
+
});
|
|
2346
|
+
return {
|
|
2347
|
+
success: true,
|
|
2348
|
+
pending: true,
|
|
2349
|
+
batchId,
|
|
2350
|
+
status: "queued_offpeak",
|
|
2351
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2352
|
+
message: `Bulk publish batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
|
|
2353
|
+
batch
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
|
|
2357
|
+
const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
|
|
2358
|
+
if (summary.totalTargets === 0) {
|
|
2359
|
+
throw new Error("No collections or entries were selected to publish.");
|
|
2360
|
+
}
|
|
2361
|
+
const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2362
|
+
const batch = {
|
|
2363
|
+
id: batchId,
|
|
2364
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2365
|
+
createdBy: currentUser ? {
|
|
2366
|
+
id: currentUser.id,
|
|
2367
|
+
email: currentUser.email,
|
|
2368
|
+
firstname: currentUser.firstname,
|
|
2369
|
+
lastname: currentUser.lastname,
|
|
2370
|
+
username: currentUser.username
|
|
2371
|
+
} : { email: "admin@system.local" },
|
|
2372
|
+
payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
|
|
2373
|
+
summary: {
|
|
2374
|
+
attribute: "(publish)",
|
|
2375
|
+
value: "published",
|
|
2376
|
+
totalTargets: summary.totalTargets,
|
|
2377
|
+
collectionBreakdown: summary.collectionBreakdown,
|
|
2378
|
+
publishOnApproval: true
|
|
2379
|
+
},
|
|
2380
|
+
status: "queued_offpeak",
|
|
2381
|
+
executionMode: "queue_offpeak",
|
|
2382
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2383
|
+
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
2384
|
+
};
|
|
2385
|
+
pendingBatches.set(batchId, batch);
|
|
2386
|
+
batchJobsMap.set(batchId, {
|
|
2387
|
+
id: batchId,
|
|
2388
|
+
createdAt: batch.createdAt,
|
|
2389
|
+
createdBy: batch.createdBy,
|
|
2390
|
+
status: "queued_offpeak",
|
|
2391
|
+
executionMode: "queue_offpeak",
|
|
2392
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2393
|
+
attribute: "(publish)",
|
|
2394
|
+
value: "published",
|
|
2395
|
+
totalTargets: summary.totalTargets,
|
|
2396
|
+
processedCount: 0,
|
|
2397
|
+
updatedCount: 0,
|
|
2398
|
+
skippedCount: 0,
|
|
2399
|
+
failedCount: 0,
|
|
2400
|
+
isLive: false,
|
|
2401
|
+
publishOnApproval: true,
|
|
2402
|
+
policyMessage,
|
|
2403
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
2404
|
+
results: []
|
|
2405
|
+
});
|
|
2406
|
+
return {
|
|
2407
|
+
success: true,
|
|
2408
|
+
pending: true,
|
|
2409
|
+
batchId,
|
|
2410
|
+
status: "queued_offpeak",
|
|
2411
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
2412
|
+
message: policyMessage,
|
|
2413
|
+
batch
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
return await executePublishBatchInternal(
|
|
2417
|
+
strapi2,
|
|
2418
|
+
{ selectedEntries, selectedCollections },
|
|
2419
|
+
currentUser
|
|
2420
|
+
);
|
|
2421
|
+
},
|
|
2006
2422
|
/**
|
|
2007
2423
|
* Get Pending & Queued Batches
|
|
2008
2424
|
*/
|
|
@@ -2046,7 +2462,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2046
2462
|
const { isBusinessHours } = checkBusinessHours(config);
|
|
2047
2463
|
if (options.executeImmediately || !isBusinessHours) {
|
|
2048
2464
|
batch.status = "approved";
|
|
2049
|
-
const result = await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
|
|
2465
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, checkerUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
|
|
2050
2466
|
batch.status = "executed";
|
|
2051
2467
|
batch.executionResult = result;
|
|
2052
2468
|
return {
|
|
@@ -2107,7 +2523,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2107
2523
|
if (batch.status === "executed") {
|
|
2108
2524
|
throw new Error(`Batch ${batchId} has already been executed.`);
|
|
2109
2525
|
}
|
|
2110
|
-
const result = await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
|
|
2526
|
+
const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, currentUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
|
|
2111
2527
|
batch.status = "executed";
|
|
2112
2528
|
batch.executionResult = result;
|
|
2113
2529
|
return {
|
|
@@ -2207,7 +2623,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2207
2623
|
function escapeCsvVal(val) {
|
|
2208
2624
|
if (val === null || val === void 0) return "";
|
|
2209
2625
|
let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
2210
|
-
|
|
2626
|
+
str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2627
|
+
if (/^[=+@-]$/.test(str2.charAt(0))) {
|
|
2628
|
+
str2 = `'${str2}`;
|
|
2629
|
+
}
|
|
2630
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
|
|
2211
2631
|
return `"${str2.replace(/"/g, '""')}"`;
|
|
2212
2632
|
}
|
|
2213
2633
|
return str2;
|
|
@@ -2251,7 +2671,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
2251
2671
|
return {
|
|
2252
2672
|
success: true,
|
|
2253
2673
|
filename,
|
|
2254
|
-
csvContent: rows.join("\n"),
|
|
2674
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
2255
2675
|
summary: {
|
|
2256
2676
|
batchId: batch.id,
|
|
2257
2677
|
status: batch.status,
|
|
@@ -3075,23 +3495,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3075
3495
|
function escapeCsvValue(val) {
|
|
3076
3496
|
if (val === null || val === void 0) return "";
|
|
3077
3497
|
let str2 = "";
|
|
3078
|
-
if (
|
|
3498
|
+
if (val instanceof Date) {
|
|
3499
|
+
str2 = val.toISOString();
|
|
3500
|
+
} else if (typeof val === "object") {
|
|
3079
3501
|
str2 = JSON.stringify(val);
|
|
3080
3502
|
} else {
|
|
3081
3503
|
str2 = String(val);
|
|
3082
3504
|
}
|
|
3083
|
-
|
|
3505
|
+
str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3506
|
+
if (/^[=+@-]$/.test(str2.charAt(0))) {
|
|
3507
|
+
str2 = `'${str2}`;
|
|
3508
|
+
}
|
|
3509
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
|
|
3084
3510
|
return `"${str2.replace(/"/g, '""')}"`;
|
|
3085
3511
|
}
|
|
3086
3512
|
return str2;
|
|
3087
3513
|
}
|
|
3088
3514
|
function parseCsv(csvText) {
|
|
3515
|
+
if (!csvText) return [];
|
|
3516
|
+
let cleanText = csvText;
|
|
3517
|
+
if (cleanText.charCodeAt(0) === 65279) {
|
|
3518
|
+
cleanText = cleanText.slice(1);
|
|
3519
|
+
}
|
|
3089
3520
|
const lines = [];
|
|
3090
3521
|
let current = "";
|
|
3091
3522
|
let inQuotes = false;
|
|
3092
|
-
for (let i = 0; i <
|
|
3093
|
-
const char =
|
|
3094
|
-
const nextChar =
|
|
3523
|
+
for (let i = 0; i < cleanText.length; i++) {
|
|
3524
|
+
const char = cleanText[i];
|
|
3525
|
+
const nextChar = cleanText[i + 1];
|
|
3095
3526
|
if (char === '"') {
|
|
3096
3527
|
if (inQuotes && nextChar === '"') {
|
|
3097
3528
|
current += '"';
|
|
@@ -3151,6 +3582,103 @@ function parseCsv(csvText) {
|
|
|
3151
3582
|
}
|
|
3152
3583
|
return rows;
|
|
3153
3584
|
}
|
|
3585
|
+
function discoverExportFields(ct, allComponents, maxDepth = 4) {
|
|
3586
|
+
const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
|
|
3587
|
+
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
3588
|
+
function walkComponent(compUid, pathPrefix, labelPrefix, depth2) {
|
|
3589
|
+
if (depth2 >= maxDepth) return;
|
|
3590
|
+
const comp = allComponents[compUid];
|
|
3591
|
+
if (!comp?.attributes) return;
|
|
3592
|
+
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
3593
|
+
if (excluded.includes(subKey)) continue;
|
|
3594
|
+
const keyPath = `${pathPrefix}.${subKey}`;
|
|
3595
|
+
const displayLabel = `${labelPrefix} > ${subKey}`;
|
|
3596
|
+
if (subDef.type === "component" && subDef.component) {
|
|
3597
|
+
if (!subDef.repeatable) {
|
|
3598
|
+
walkComponent(subDef.component, keyPath, displayLabel, depth2 + 1);
|
|
3599
|
+
} else {
|
|
3600
|
+
fields2.push({ key: keyPath, label: displayLabel, type: "component" });
|
|
3601
|
+
}
|
|
3602
|
+
} else {
|
|
3603
|
+
fields2.push({ key: keyPath, label: displayLabel, type: subDef.type });
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
for (const [attrName, attrDef] of Object.entries(ct.attributes || {})) {
|
|
3608
|
+
if (excluded.includes(attrName)) continue;
|
|
3609
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3610
|
+
if (!attrDef.repeatable) {
|
|
3611
|
+
walkComponent(attrDef.component, attrName, attrName, 0);
|
|
3612
|
+
} else {
|
|
3613
|
+
fields2.push({ key: attrName, label: attrName, type: "component" });
|
|
3614
|
+
}
|
|
3615
|
+
} else {
|
|
3616
|
+
fields2.push({ key: attrName, label: attrName, type: attrDef.type });
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
return fields2;
|
|
3620
|
+
}
|
|
3621
|
+
function buildDeepPopulate(attributes, allComponents, depth2 = 0) {
|
|
3622
|
+
if (depth2 > 4) return true;
|
|
3623
|
+
const populate2 = {};
|
|
3624
|
+
for (const [key, def] of Object.entries(attributes || {})) {
|
|
3625
|
+
if (def.type === "component" && def.component) {
|
|
3626
|
+
const comp = allComponents[def.component];
|
|
3627
|
+
if (comp?.attributes) {
|
|
3628
|
+
populate2[key] = {
|
|
3629
|
+
populate: buildDeepPopulate(comp.attributes, allComponents, depth2 + 1)
|
|
3630
|
+
};
|
|
3631
|
+
} else {
|
|
3632
|
+
populate2[key] = true;
|
|
3633
|
+
}
|
|
3634
|
+
} else if (def.type === "dynamiczone") {
|
|
3635
|
+
populate2[key] = true;
|
|
3636
|
+
} else if (def.type === "relation" || def.type === "media") {
|
|
3637
|
+
populate2[key] = true;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return Object.keys(populate2).length > 0 ? populate2 : true;
|
|
3641
|
+
}
|
|
3642
|
+
function getNestedExportValue(entry, path2) {
|
|
3643
|
+
if (!entry || !path2) return "";
|
|
3644
|
+
const parts = path2.split(".");
|
|
3645
|
+
let current = entry;
|
|
3646
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3647
|
+
const part = parts[i];
|
|
3648
|
+
if (current === null || current === void 0) return "";
|
|
3649
|
+
if (Array.isArray(current)) {
|
|
3650
|
+
const subPath = parts.slice(i).join(".");
|
|
3651
|
+
return current.map((item) => {
|
|
3652
|
+
if (!item) return "";
|
|
3653
|
+
if (typeof item === "object") {
|
|
3654
|
+
const val = getNestedExportValue(item, subPath);
|
|
3655
|
+
return typeof val === "object" ? JSON.stringify(val) : String(val ?? "");
|
|
3656
|
+
}
|
|
3657
|
+
return String(item);
|
|
3658
|
+
}).filter(Boolean).join("; ");
|
|
3659
|
+
}
|
|
3660
|
+
current = current[part];
|
|
3661
|
+
}
|
|
3662
|
+
if (current === null || current === void 0) return "";
|
|
3663
|
+
if (typeof current === "object" && current.url) {
|
|
3664
|
+
return current.url;
|
|
3665
|
+
}
|
|
3666
|
+
if (typeof current === "object" && !Array.isArray(current)) {
|
|
3667
|
+
if (current.documentId || current.name || current.title) {
|
|
3668
|
+
return current.name || current.title || current.documentId;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
if (Array.isArray(current)) {
|
|
3672
|
+
return current.map((item) => {
|
|
3673
|
+
if (!item) return "";
|
|
3674
|
+
if (typeof item === "object") {
|
|
3675
|
+
return item.name || item.title || item.documentId || JSON.stringify(item);
|
|
3676
|
+
}
|
|
3677
|
+
return String(item);
|
|
3678
|
+
}).filter(Boolean).join("; ");
|
|
3679
|
+
}
|
|
3680
|
+
return current;
|
|
3681
|
+
}
|
|
3154
3682
|
const csvService = ({ strapi: strapi2 }) => ({
|
|
3155
3683
|
/**
|
|
3156
3684
|
* Generates a sample template CSV with schema attribute headers and sample values
|
|
@@ -3199,9 +3727,7 @@ const csvService = ({ strapi: strapi2 }) => ({
|
|
|
3199
3727
|
}
|
|
3200
3728
|
sampleRow.push(sampleVal);
|
|
3201
3729
|
}
|
|
3202
|
-
const csvContent =
|
|
3203
|
-
${sampleRow.map(escapeCsvValue).join(",")}
|
|
3204
|
-
`;
|
|
3730
|
+
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
3205
3731
|
return {
|
|
3206
3732
|
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
3207
3733
|
csvContent,
|
|
@@ -3209,65 +3735,60 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3209
3735
|
};
|
|
3210
3736
|
},
|
|
3211
3737
|
/**
|
|
3212
|
-
* Exports entries of a collection to CSV
|
|
3738
|
+
* Exports entries of a collection to CSV with deep nested component support,
|
|
3739
|
+
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
3213
3740
|
*/
|
|
3214
3741
|
async exportCollectionCsv(uid, options = {}) {
|
|
3215
3742
|
const ct = strapi2.contentTypes[uid];
|
|
3216
3743
|
if (!ct) {
|
|
3217
3744
|
throw new Error(`Content type ${uid} not found`);
|
|
3218
3745
|
}
|
|
3219
|
-
const
|
|
3220
|
-
const
|
|
3221
|
-
const exportFields = ["documentId"];
|
|
3222
|
-
const compFieldsToPopulate = [];
|
|
3223
|
-
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
3224
|
-
if (excluded.includes(attrName)) continue;
|
|
3225
|
-
if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
|
|
3226
|
-
if (attrDef.type === "component") {
|
|
3227
|
-
if (!attrDef.repeatable && attrDef.component) {
|
|
3228
|
-
const comp = strapi2.components[attrDef.component];
|
|
3229
|
-
if (comp?.attributes) {
|
|
3230
|
-
compFieldsToPopulate.push(attrName);
|
|
3231
|
-
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
3232
|
-
if (excluded.includes(subKey)) continue;
|
|
3233
|
-
if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
|
|
3234
|
-
exportFields.push(`${attrName}.${subKey}`);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
continue;
|
|
3239
|
-
}
|
|
3240
|
-
exportFields.push(attrName);
|
|
3241
|
-
}
|
|
3746
|
+
const allComponents = strapi2.components || {};
|
|
3747
|
+
const fields2 = discoverExportFields(ct, allComponents);
|
|
3242
3748
|
const filters2 = {};
|
|
3243
|
-
const { attribute, filterOperator, filterQuery, search } = options;
|
|
3244
|
-
if (
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
filters2[
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3749
|
+
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
3750
|
+
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
3751
|
+
filters2.documentId = { $in: documentIds };
|
|
3752
|
+
}
|
|
3753
|
+
if (attribute && filterOperator && filterOperator !== "none") {
|
|
3754
|
+
const isNested = attribute.includes(".");
|
|
3755
|
+
if (filterOperator === "empty") {
|
|
3756
|
+
if (isNested) {
|
|
3757
|
+
const [parent, child] = attribute.split(".");
|
|
3758
|
+
filters2[parent] = { [child]: { $null: true } };
|
|
3759
|
+
} else {
|
|
3760
|
+
filters2[attribute] = { $null: true };
|
|
3761
|
+
}
|
|
3762
|
+
} else if (filterOperator === "not_empty") {
|
|
3763
|
+
if (isNested) {
|
|
3764
|
+
const [parent, child] = attribute.split(".");
|
|
3765
|
+
filters2[parent] = { [child]: { $notNull: true } };
|
|
3766
|
+
} else {
|
|
3767
|
+
filters2[attribute] = { $notNull: true };
|
|
3768
|
+
}
|
|
3769
|
+
} else if (filterQuery !== void 0 && filterQuery !== "") {
|
|
3770
|
+
const opMap = {
|
|
3771
|
+
equals: "$eq",
|
|
3772
|
+
notEquals: "$ne",
|
|
3773
|
+
contains: "$containsi",
|
|
3774
|
+
notContains: "$notContainsi",
|
|
3775
|
+
startsWith: "$startsWith",
|
|
3776
|
+
endsWith: "$endsWith"
|
|
3777
|
+
};
|
|
3778
|
+
const strapiOp = opMap[filterOperator] || "$eq";
|
|
3779
|
+
if (isNested) {
|
|
3780
|
+
const [parent, child] = attribute.split(".");
|
|
3781
|
+
filters2[parent] = { [child]: { [strapiOp]: filterQuery } };
|
|
3782
|
+
} else {
|
|
3783
|
+
filters2[attribute] = { [strapiOp]: filterQuery };
|
|
3784
|
+
}
|
|
3264
3785
|
}
|
|
3265
3786
|
}
|
|
3266
3787
|
if (search && search.trim() !== "") {
|
|
3267
3788
|
const q = search.trim();
|
|
3268
3789
|
const searchConditions = [];
|
|
3269
3790
|
const searchFields = ["name", "title", "email", "slug", "description"].filter(
|
|
3270
|
-
(f) =>
|
|
3791
|
+
(f) => ct.attributes?.[f] && ["string", "text", "email"].includes(ct.attributes[f].type)
|
|
3271
3792
|
);
|
|
3272
3793
|
for (const field of searchFields) {
|
|
3273
3794
|
searchConditions.push({ [field]: { $containsi: q } });
|
|
@@ -3280,34 +3801,37 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3280
3801
|
}
|
|
3281
3802
|
}
|
|
3282
3803
|
}
|
|
3283
|
-
const
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3804
|
+
const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
|
|
3805
|
+
let entries = [];
|
|
3806
|
+
if (ct.kind === "singleType") {
|
|
3807
|
+
const doc = await strapi2.documents(uid).findFirst({
|
|
3808
|
+
populate: deepPopulate
|
|
3809
|
+
});
|
|
3810
|
+
entries = doc ? [doc] : [];
|
|
3811
|
+
} else {
|
|
3812
|
+
entries = await strapi2.documents(uid).findMany({
|
|
3813
|
+
filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
|
|
3814
|
+
limit: options.limit || 5e3,
|
|
3815
|
+
sort: { createdAt: "desc" },
|
|
3816
|
+
populate: deepPopulate
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3290
3819
|
const lines = [];
|
|
3291
|
-
lines.push(
|
|
3820
|
+
lines.push(fields2.map((f) => escapeCsvValue(f.key)).join(","));
|
|
3292
3821
|
for (const entry of entries || []) {
|
|
3293
|
-
const rowValues =
|
|
3294
|
-
|
|
3295
|
-
if (field.includes(".")) {
|
|
3296
|
-
const [parent, child] = field.split(".");
|
|
3297
|
-
val = entry[parent]?.[child];
|
|
3298
|
-
} else {
|
|
3299
|
-
val = entry[field];
|
|
3300
|
-
}
|
|
3822
|
+
const rowValues = fields2.map((f) => {
|
|
3823
|
+
const val = getNestedExportValue(entry, f.key);
|
|
3301
3824
|
return escapeCsvValue(val);
|
|
3302
3825
|
});
|
|
3303
3826
|
lines.push(rowValues.join(","));
|
|
3304
3827
|
}
|
|
3305
|
-
const csvContent = lines.join("\n");
|
|
3828
|
+
const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
|
|
3306
3829
|
const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3307
3830
|
return {
|
|
3308
3831
|
filename,
|
|
3309
3832
|
csvContent,
|
|
3310
|
-
count: entries ? entries.length : 0
|
|
3833
|
+
count: entries ? entries.length : 0,
|
|
3834
|
+
fields: fields2.map((f) => f.key)
|
|
3311
3835
|
};
|
|
3312
3836
|
},
|
|
3313
3837
|
/**
|
|
@@ -3347,11 +3871,76 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
3347
3871
|
const filename = `violations_${safeRuleName}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3348
3872
|
return {
|
|
3349
3873
|
filename,
|
|
3350
|
-
csvContent: lines.join("\n"),
|
|
3874
|
+
csvContent: "\uFEFF" + lines.join("\r\n") + "\r\n",
|
|
3351
3875
|
count: scanReport.violations.length,
|
|
3352
3876
|
ruleName: scanReport.rule.name
|
|
3353
3877
|
};
|
|
3354
3878
|
},
|
|
3879
|
+
/**
|
|
3880
|
+
* Bulk Export: Exports selected entries across multiple collections or a multi-collection batch.
|
|
3881
|
+
*/
|
|
3882
|
+
async exportBulkCsv(selectedEntries) {
|
|
3883
|
+
if (!selectedEntries || selectedEntries.length === 0) {
|
|
3884
|
+
throw new Error("No entries selected for bulk export");
|
|
3885
|
+
}
|
|
3886
|
+
const byUid = {};
|
|
3887
|
+
for (const item of selectedEntries) {
|
|
3888
|
+
if (!byUid[item.uid]) byUid[item.uid] = [];
|
|
3889
|
+
byUid[item.uid].push(item.documentId);
|
|
3890
|
+
}
|
|
3891
|
+
const uids = Object.keys(byUid);
|
|
3892
|
+
if (uids.length === 1) {
|
|
3893
|
+
const uid = uids[0];
|
|
3894
|
+
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
|
|
3895
|
+
}
|
|
3896
|
+
const rows = [];
|
|
3897
|
+
const headers = [
|
|
3898
|
+
"Collection UID",
|
|
3899
|
+
"Collection Name",
|
|
3900
|
+
"Document ID",
|
|
3901
|
+
"Record Title / Name",
|
|
3902
|
+
"Primary Identifier",
|
|
3903
|
+
"Created At",
|
|
3904
|
+
"Updated At",
|
|
3905
|
+
"Published Status"
|
|
3906
|
+
];
|
|
3907
|
+
rows.push(headers.map(escapeCsvValue).join(","));
|
|
3908
|
+
let totalExported = 0;
|
|
3909
|
+
for (const uid of uids) {
|
|
3910
|
+
const ct = strapi2.contentTypes[uid];
|
|
3911
|
+
if (!ct) continue;
|
|
3912
|
+
const displayName = ct.info?.displayName || uid;
|
|
3913
|
+
const docIds = byUid[uid];
|
|
3914
|
+
const entries = await strapi2.documents(uid).findMany({
|
|
3915
|
+
filters: { documentId: { $in: docIds } },
|
|
3916
|
+
limit: docIds.length
|
|
3917
|
+
});
|
|
3918
|
+
for (const entry of entries || []) {
|
|
3919
|
+
const titleOrName = entry.title || entry.name || entry.subject || entry.email || `Entry #${entry.documentId}`;
|
|
3920
|
+
const identifier = entry.slug || entry.email || entry.username || entry.documentId;
|
|
3921
|
+
const isPublished = Boolean(entry.publishedAt);
|
|
3922
|
+
const row = [
|
|
3923
|
+
uid,
|
|
3924
|
+
displayName,
|
|
3925
|
+
entry.documentId,
|
|
3926
|
+
titleOrName,
|
|
3927
|
+
identifier,
|
|
3928
|
+
entry.createdAt || "",
|
|
3929
|
+
entry.updatedAt || "",
|
|
3930
|
+
isPublished ? "Published" : "Draft"
|
|
3931
|
+
];
|
|
3932
|
+
rows.push(row.map(escapeCsvValue).join(","));
|
|
3933
|
+
totalExported++;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
const filename = `bulk_export_selected_${totalExported}_entries_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3937
|
+
return {
|
|
3938
|
+
filename,
|
|
3939
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
3940
|
+
count: totalExported,
|
|
3941
|
+
isMultiCollection: true
|
|
3942
|
+
};
|
|
3943
|
+
},
|
|
3355
3944
|
/**
|
|
3356
3945
|
* Imports CSV records to update existing records or insert/override records
|
|
3357
3946
|
*/
|