strapi-plugin-field-clearer 1.0.0

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.
@@ -0,0 +1,711 @@
1
+ const bootstrap = ({ strapi }) => {
2
+ };
3
+ const destroy = ({ strapi }) => {
4
+ };
5
+ const register = ({ strapi }) => {
6
+ };
7
+ const config = {
8
+ default: {
9
+ // Default allowed content types - users can override in their config/plugins.js
10
+ allowedContentTypes: []
11
+ },
12
+ validator(config2) {
13
+ if (config2.allowedContentTypes !== void 0) {
14
+ if (!Array.isArray(config2.allowedContentTypes)) {
15
+ throw new Error("field-clearer: allowedContentTypes must be an array");
16
+ }
17
+ for (const contentType of config2.allowedContentTypes) {
18
+ if (typeof contentType !== "string") {
19
+ throw new Error("field-clearer: each allowedContentTypes entry must be a string");
20
+ }
21
+ if (!/^(api|plugin)::[a-z0-9-]+\.[a-z0-9-]+$/.test(contentType)) {
22
+ throw new Error(
23
+ `field-clearer: invalid content type format "${contentType}". Expected format: "api::collection-name.collection-name" or "plugin::plugin-name.content-type"`
24
+ );
25
+ }
26
+ }
27
+ }
28
+ }
29
+ };
30
+ const contentTypes = {};
31
+ const PLUGIN_ID = "field-clearer";
32
+ const VALID_FIELD_PATH_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*(\[\d+(,\d+)*\])?(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
33
+ const MAX_FIELD_PATH_LENGTH = 100;
34
+ const MAX_DOCUMENT_ID_LENGTH = 50;
35
+ const getAllowedContentTypes = (strapi) => {
36
+ const config2 = strapi.config.get(`plugin::${PLUGIN_ID}`);
37
+ return config2?.allowedContentTypes || [];
38
+ };
39
+ const validateFieldPath = (fieldPath) => {
40
+ if (typeof fieldPath !== "string") {
41
+ return { valid: false, error: "fieldPath must be a string" };
42
+ }
43
+ const trimmed = fieldPath.trim();
44
+ if (!trimmed) {
45
+ return { valid: false, error: "fieldPath cannot be empty" };
46
+ }
47
+ if (trimmed.length > MAX_FIELD_PATH_LENGTH) {
48
+ return { valid: false, error: `fieldPath exceeds maximum length of ${MAX_FIELD_PATH_LENGTH}` };
49
+ }
50
+ if (!VALID_FIELD_PATH_REGEX.test(trimmed)) {
51
+ return { valid: false, error: 'Invalid fieldPath format. Examples: "coupons", "coupons.freebies", "coupons[1].freebies", "coupons[0,2].freebies"' };
52
+ }
53
+ return { valid: true };
54
+ };
55
+ const validateDocumentId = (documentId) => {
56
+ if (typeof documentId !== "string") {
57
+ return { valid: false, error: "documentId must be a string" };
58
+ }
59
+ const trimmed = documentId.trim();
60
+ if (!trimmed) {
61
+ return { valid: false, error: "documentId cannot be empty" };
62
+ }
63
+ if (trimmed.length > MAX_DOCUMENT_ID_LENGTH) {
64
+ return { valid: false, error: `documentId exceeds maximum length of ${MAX_DOCUMENT_ID_LENGTH}` };
65
+ }
66
+ if (!/^[a-zA-Z0-9]+$/.test(trimmed)) {
67
+ return { valid: false, error: "documentId contains invalid characters" };
68
+ }
69
+ return { valid: true };
70
+ };
71
+ const controller = ({ strapi }) => ({
72
+ /**
73
+ * Get plugin configuration for admin UI
74
+ * GET /field-clearer/config
75
+ */
76
+ async getConfig(ctx) {
77
+ const allowedContentTypes = getAllowedContentTypes(strapi);
78
+ return ctx.send({ allowedContentTypes });
79
+ },
80
+ /**
81
+ * Preview what will be deleted (dry run)
82
+ * POST /field-clearer/preview-field
83
+ * Body: { contentType, documentId, fieldPath }
84
+ */
85
+ async previewField(ctx) {
86
+ try {
87
+ const { contentType, documentId, fieldPath } = ctx.request.body;
88
+ if (!contentType || typeof contentType !== "string") {
89
+ return ctx.badRequest("contentType is required and must be a string");
90
+ }
91
+ const allowedContentTypes = getAllowedContentTypes(strapi);
92
+ if (allowedContentTypes.length === 0) {
93
+ return ctx.forbidden("No content types are configured. Please configure allowedContentTypes in config/plugins.js");
94
+ }
95
+ if (!allowedContentTypes.includes(contentType)) {
96
+ return ctx.forbidden(`Content type "${contentType}" is not allowed for this operation`);
97
+ }
98
+ const docIdValidation = validateDocumentId(documentId);
99
+ if (!docIdValidation.valid) {
100
+ return ctx.badRequest(docIdValidation.error);
101
+ }
102
+ const fieldPathValidation = validateFieldPath(fieldPath);
103
+ if (!fieldPathValidation.valid) {
104
+ return ctx.badRequest(fieldPathValidation.error);
105
+ }
106
+ const result = await strapi.plugin(PLUGIN_ID).service("service").previewField(contentType, documentId.trim(), fieldPath.trim());
107
+ return ctx.send(result);
108
+ } catch (error) {
109
+ strapi.log.error("[field-clearer] Preview field error:", error);
110
+ return ctx.badRequest(error instanceof Error ? error.message : "An error occurred");
111
+ }
112
+ },
113
+ /**
114
+ * Clear a field from a document
115
+ * POST /field-clearer/clear-field
116
+ * Body: { contentType, documentId, fieldPath }
117
+ *
118
+ * Examples:
119
+ * - fieldPath: "coupons" → clears all coupons
120
+ * - fieldPath: "coupons.freebies" → clears freebies inside each coupon
121
+ * - fieldPath: "coupons[1].freebies" → clears freebies from 2nd coupon only
122
+ */
123
+ async clearField(ctx) {
124
+ try {
125
+ const { contentType, documentId, fieldPath } = ctx.request.body;
126
+ if (!contentType || typeof contentType !== "string") {
127
+ return ctx.badRequest("contentType is required and must be a string");
128
+ }
129
+ const allowedContentTypes = getAllowedContentTypes(strapi);
130
+ if (allowedContentTypes.length === 0) {
131
+ return ctx.forbidden("No content types are configured. Please configure allowedContentTypes in config/plugins.js");
132
+ }
133
+ if (!allowedContentTypes.includes(contentType)) {
134
+ return ctx.forbidden(`Content type "${contentType}" is not allowed for this operation`);
135
+ }
136
+ const docIdValidation = validateDocumentId(documentId);
137
+ if (!docIdValidation.valid) {
138
+ return ctx.badRequest(docIdValidation.error);
139
+ }
140
+ const fieldPathValidation = validateFieldPath(fieldPath);
141
+ if (!fieldPathValidation.valid) {
142
+ return ctx.badRequest(fieldPathValidation.error);
143
+ }
144
+ strapi.log.info(`[field-clearer] Clearing field "${fieldPath}" on ${contentType} (documentId: ${documentId})`);
145
+ const result = await strapi.plugin(PLUGIN_ID).service("service").clearField(contentType, documentId.trim(), fieldPath.trim());
146
+ strapi.log.info(`[field-clearer] Successfully cleared "${fieldPath}" - ${result.clearedCount} items`);
147
+ return ctx.send(result);
148
+ } catch (error) {
149
+ strapi.log.error("[field-clearer] Clear field error:", error);
150
+ return ctx.badRequest(error instanceof Error ? error.message : "An error occurred");
151
+ }
152
+ }
153
+ });
154
+ const controllers = {
155
+ controller
156
+ };
157
+ const middlewares = {};
158
+ const policies = {};
159
+ const routes = [
160
+ {
161
+ method: "GET",
162
+ path: "/config",
163
+ handler: "controller.getConfig",
164
+ config: {
165
+ policies: ["admin::isAuthenticatedAdmin"]
166
+ }
167
+ },
168
+ {
169
+ method: "POST",
170
+ path: "/preview-field",
171
+ handler: "controller.previewField",
172
+ config: {
173
+ policies: ["admin::isAuthenticatedAdmin"]
174
+ }
175
+ },
176
+ {
177
+ method: "POST",
178
+ path: "/clear-field",
179
+ handler: "controller.clearField",
180
+ config: {
181
+ policies: ["admin::isAuthenticatedAdmin"]
182
+ }
183
+ }
184
+ ];
185
+ const parseFieldPath = (path) => {
186
+ const bracketMatch = path.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+(?:,\d+)*)\](?:\.([a-zA-Z_][a-zA-Z0-9_]*))?$/);
187
+ if (bracketMatch) {
188
+ const [, fieldName, indicesStr, nestedField] = bracketMatch;
189
+ const indices = indicesStr.split(",").map(Number);
190
+ return { fieldName, nestedField, indices };
191
+ }
192
+ const parts = path.split(".");
193
+ if (parts.length === 1) {
194
+ return { fieldName: parts[0], indices: null };
195
+ }
196
+ if (parts.length === 2) {
197
+ return { fieldName: parts[0], nestedField: parts[1], indices: null };
198
+ }
199
+ throw new Error(`Invalid path format: "${path}"`);
200
+ };
201
+ const service = ({ strapi }) => ({
202
+ /**
203
+ * Preview what will be deleted (dry run - no actual deletion)
204
+ * Returns field info and items that would be deleted
205
+ */
206
+ async previewField(contentType, documentId, fieldPath) {
207
+ if (!contentType || typeof contentType !== "string") {
208
+ throw new Error("Invalid contentType provided");
209
+ }
210
+ if (!documentId || typeof documentId !== "string") {
211
+ throw new Error("Invalid documentId provided");
212
+ }
213
+ if (!fieldPath || typeof fieldPath !== "string") {
214
+ throw new Error("Invalid fieldPath provided");
215
+ }
216
+ const trimmedPath = fieldPath.trim();
217
+ if (!trimmedPath) {
218
+ throw new Error("Field path cannot be empty");
219
+ }
220
+ const { fieldName, nestedField, indices } = parseFieldPath(trimmedPath);
221
+ if (!fieldName) {
222
+ throw new Error("Field path cannot be empty");
223
+ }
224
+ if (!nestedField) {
225
+ return this.previewTopLevelField(contentType, documentId, fieldName);
226
+ }
227
+ return this.previewNestedField(contentType, documentId, fieldName, nestedField, indices);
228
+ },
229
+ /**
230
+ * Preview a top-level field deletion
231
+ */
232
+ async previewTopLevelField(contentType, documentId, fieldName) {
233
+ let document;
234
+ try {
235
+ document = await strapi.documents(contentType).findOne({
236
+ documentId,
237
+ populate: "*"
238
+ });
239
+ } catch (error) {
240
+ const message = error instanceof Error ? error.message : "Unknown error";
241
+ throw new Error(`Failed to fetch document: ${message}`);
242
+ }
243
+ if (!document) {
244
+ throw new Error("Document not found");
245
+ }
246
+ const fieldValue = document[fieldName];
247
+ if (fieldValue === void 0) {
248
+ throw new Error(`Field "${fieldName}" does not exist on this content type`);
249
+ }
250
+ const isEmpty = this.isFieldEmpty(fieldValue);
251
+ const itemCount = this.countFieldItems(fieldValue);
252
+ const items = this.extractPreviewItems(fieldValue, fieldName);
253
+ return {
254
+ fieldPath: fieldName,
255
+ fieldType: this.getFieldType(fieldValue),
256
+ isEmpty,
257
+ itemCount,
258
+ items,
259
+ message: isEmpty ? `Field "${fieldName}" is already empty` : `Will delete ${itemCount} item${itemCount !== 1 ? "s" : ""} from "${fieldName}"`
260
+ };
261
+ },
262
+ /**
263
+ * Preview a nested field deletion
264
+ * @param indices - Optional array of component indices to target (0-based). If null, targets all components.
265
+ */
266
+ async previewNestedField(contentType, documentId, componentField, nestedField, indices = null) {
267
+ let document;
268
+ try {
269
+ document = await strapi.documents(contentType).findOne({
270
+ documentId,
271
+ populate: {
272
+ [componentField]: {
273
+ populate: "*"
274
+ }
275
+ }
276
+ });
277
+ } catch (error) {
278
+ const message = error instanceof Error ? error.message : "Unknown error";
279
+ throw new Error(`Failed to fetch document: ${message}`);
280
+ }
281
+ if (!document) {
282
+ throw new Error("Document not found");
283
+ }
284
+ const components = document[componentField];
285
+ const displayPath = indices ? `${componentField}[${indices.join(",")}].${nestedField}` : `${componentField}.${nestedField}`;
286
+ if (components === void 0) {
287
+ throw new Error(`Field "${componentField}" does not exist on this content type`);
288
+ }
289
+ if (components === null) {
290
+ return {
291
+ fieldPath: displayPath,
292
+ fieldType: "unknown",
293
+ isEmpty: true,
294
+ itemCount: 0,
295
+ items: [],
296
+ message: `"${componentField}" is null on this document`
297
+ };
298
+ }
299
+ if (Array.isArray(components) && components.length === 0) {
300
+ return {
301
+ fieldPath: displayPath,
302
+ fieldType: "unknown",
303
+ isEmpty: true,
304
+ itemCount: 0,
305
+ items: [],
306
+ message: `No "${componentField}" found on this document`
307
+ };
308
+ }
309
+ const componentArray = Array.isArray(components) ? components : [components];
310
+ if (indices) {
311
+ for (const idx of indices) {
312
+ if (idx < 0 || idx >= componentArray.length) {
313
+ throw new Error(`Index ${idx} is out of range. "${componentField}" has ${componentArray.length} item${componentArray.length !== 1 ? "s" : ""} (indices 0-${componentArray.length - 1})`);
314
+ }
315
+ }
316
+ }
317
+ const targetIndices = indices || componentArray.map((_, i) => i);
318
+ let totalCount = 0;
319
+ let nestedFieldExists = false;
320
+ const allItems = [];
321
+ let fieldType = "unknown";
322
+ for (const i of targetIndices) {
323
+ const comp = componentArray[i];
324
+ if (comp && comp[nestedField] !== void 0) {
325
+ nestedFieldExists = true;
326
+ const value = comp[nestedField];
327
+ const count = this.countFieldItems(value);
328
+ totalCount += count;
329
+ fieldType = this.getFieldType(value);
330
+ const items = this.extractPreviewItems(value, nestedField);
331
+ items.forEach((item) => {
332
+ allItems.push({
333
+ ...item,
334
+ componentIndex: i,
335
+ componentHandle: comp.handle || comp.title || comp.name || `#${i + 1}`
336
+ });
337
+ });
338
+ }
339
+ }
340
+ if (!nestedFieldExists) {
341
+ throw new Error(`Field "${nestedField}" does not exist inside "${componentField}"`);
342
+ }
343
+ const isEmpty = totalCount === 0;
344
+ const targetDescription = indices ? `${targetIndices.length} selected "${componentField}"` : `${componentArray.length} "${componentField}"`;
345
+ return {
346
+ fieldPath: displayPath,
347
+ fieldType,
348
+ isEmpty,
349
+ itemCount: totalCount,
350
+ componentCount: targetIndices.length,
351
+ totalComponentCount: componentArray.length,
352
+ targetIndices,
353
+ items: allItems,
354
+ message: isEmpty ? `"${nestedField}" is already empty in ${targetDescription}` : `Will delete ${totalCount} item${totalCount !== 1 ? "s" : ""} from "${nestedField}" across ${targetDescription}`
355
+ };
356
+ },
357
+ /**
358
+ * Get a human-readable field type
359
+ */
360
+ getFieldType(value) {
361
+ if (value === null || value === void 0) {
362
+ return "empty";
363
+ }
364
+ if (Array.isArray(value)) {
365
+ if (value.length === 0) return "array (empty)";
366
+ const first = value[0];
367
+ if (first && typeof first === "object") {
368
+ if (first.documentId) return "relation (array)";
369
+ if (first.id) return "component (repeatable)";
370
+ }
371
+ return "array";
372
+ }
373
+ if (typeof value === "object") {
374
+ if (value.documentId) return "relation (single)";
375
+ if (value.url) return "media";
376
+ if (value.id) return "component (single)";
377
+ return "object";
378
+ }
379
+ return typeof value;
380
+ },
381
+ /**
382
+ * Extract preview items from a field value
383
+ */
384
+ extractPreviewItems(value, fieldName) {
385
+ if (value === null || value === void 0) {
386
+ return [];
387
+ }
388
+ if (Array.isArray(value)) {
389
+ return value.map((item, index2) => {
390
+ if (typeof item === "object" && item !== null) {
391
+ return {
392
+ index: index2,
393
+ id: item.id || item.documentId,
394
+ label: item.title || item.name || item.handle || item.code || item.documentId || `Item ${index2 + 1}`,
395
+ type: item.documentId ? "relation" : "component"
396
+ };
397
+ }
398
+ return {
399
+ index: index2,
400
+ label: String(item).substring(0, 50),
401
+ type: typeof item
402
+ };
403
+ });
404
+ }
405
+ if (typeof value === "object") {
406
+ return [{
407
+ id: value.id || value.documentId,
408
+ label: value.title || value.name || value.handle || value.url || "Single item",
409
+ type: value.documentId ? "relation" : value.url ? "media" : "component"
410
+ }];
411
+ }
412
+ const strValue = String(value);
413
+ return [{
414
+ label: strValue.length > 100 ? strValue.substring(0, 100) + "..." : strValue,
415
+ type: typeof value
416
+ }];
417
+ },
418
+ /**
419
+ * Clear a field from a document using a path
420
+ *
421
+ * Supported field types:
422
+ * - string, text, richtext, enumeration
423
+ * - integer, decimal, float
424
+ * - boolean
425
+ * - CKEditor (customField)
426
+ * - media (single/multiple)
427
+ * - relation (oneToOne, oneToMany, manyToOne, manyToMany)
428
+ * - component (single/repeatable)
429
+ *
430
+ * Examples:
431
+ * - "coupons" → clears entire coupons array (sets to [])
432
+ * - "coupons.freebies" → clears freebies field inside each coupon component
433
+ * - "all_offers_unlocked_message" → clears CKEditor field
434
+ *
435
+ * @param contentType - e.g., 'api::cart.cart'
436
+ * @param documentId - the document's documentId
437
+ * @param fieldPath - e.g., 'coupons' or 'coupons.freebies'
438
+ */
439
+ async clearField(contentType, documentId, fieldPath) {
440
+ if (!contentType || typeof contentType !== "string") {
441
+ throw new Error("Invalid contentType provided");
442
+ }
443
+ if (!documentId || typeof documentId !== "string") {
444
+ throw new Error("Invalid documentId provided");
445
+ }
446
+ if (!fieldPath || typeof fieldPath !== "string") {
447
+ throw new Error("Invalid fieldPath provided");
448
+ }
449
+ const trimmedPath = fieldPath.trim();
450
+ if (!trimmedPath) {
451
+ throw new Error("Field path cannot be empty");
452
+ }
453
+ const { fieldName, nestedField, indices } = parseFieldPath(trimmedPath);
454
+ if (!fieldName) {
455
+ throw new Error("Field path cannot be empty");
456
+ }
457
+ if (!nestedField) {
458
+ return this.clearTopLevelField(contentType, documentId, fieldName);
459
+ }
460
+ return this.clearNestedField(contentType, documentId, fieldName, nestedField, indices);
461
+ },
462
+ /**
463
+ * Clear a top-level field (e.g., "coupons" → sets coupons to [] or null)
464
+ * Works for all field types: relations, components, scalars, media, CKEditor
465
+ */
466
+ async clearTopLevelField(contentType, documentId, fieldName) {
467
+ if (!fieldName || typeof fieldName !== "string") {
468
+ throw new Error("Invalid field name provided");
469
+ }
470
+ let document;
471
+ try {
472
+ document = await strapi.documents(contentType).findOne({
473
+ documentId,
474
+ populate: "*"
475
+ });
476
+ } catch (error) {
477
+ const message = error instanceof Error ? error.message : "Unknown error";
478
+ throw new Error(`Failed to fetch document: ${message}`);
479
+ }
480
+ if (!document) {
481
+ throw new Error("Document not found");
482
+ }
483
+ const fieldValue = document[fieldName];
484
+ if (fieldValue === void 0) {
485
+ throw new Error(`Field "${fieldName}" does not exist on this content type`);
486
+ }
487
+ const isEmpty = this.isFieldEmpty(fieldValue);
488
+ if (isEmpty) {
489
+ return { message: `Field "${fieldName}" is already empty`, clearedCount: 0 };
490
+ }
491
+ const clearedCount = this.countFieldItems(fieldValue);
492
+ const internalId = document.id;
493
+ if (!internalId) {
494
+ throw new Error("Document internal ID not found");
495
+ }
496
+ const newValue = this.getEmptyValue(fieldValue);
497
+ try {
498
+ await strapi.entityService.update(contentType, internalId, {
499
+ data: {
500
+ [fieldName]: newValue
501
+ }
502
+ });
503
+ } catch (error) {
504
+ const message = error instanceof Error ? error.message : "Unknown error";
505
+ throw new Error(`Failed to update document: ${message}`);
506
+ }
507
+ return {
508
+ message: `Successfully cleared "${fieldName}" (${clearedCount} item${clearedCount !== 1 ? "s" : ""})`,
509
+ clearedCount,
510
+ path: fieldName
511
+ };
512
+ },
513
+ /**
514
+ * Clear a nested field inside component(s) (e.g., "coupons.freebies" or "coupons[1].freebies")
515
+ * Works for all field types inside components
516
+ * @param indices - Optional array of component indices to target (0-based). If null, targets all components.
517
+ */
518
+ async clearNestedField(contentType, documentId, componentField, nestedField, indices = null) {
519
+ if (!componentField || typeof componentField !== "string") {
520
+ throw new Error("Invalid component field name provided");
521
+ }
522
+ if (!nestedField || typeof nestedField !== "string") {
523
+ throw new Error("Invalid nested field name provided");
524
+ }
525
+ let document;
526
+ try {
527
+ document = await strapi.documents(contentType).findOne({
528
+ documentId,
529
+ populate: {
530
+ [componentField]: {
531
+ populate: "*"
532
+ }
533
+ }
534
+ });
535
+ } catch (error) {
536
+ const message = error instanceof Error ? error.message : "Unknown error";
537
+ throw new Error(`Failed to fetch document: ${message}`);
538
+ }
539
+ if (!document) {
540
+ throw new Error("Document not found");
541
+ }
542
+ const components = document[componentField];
543
+ const displayPath = indices ? `${componentField}[${indices.join(",")}].${nestedField}` : `${componentField}.${nestedField}`;
544
+ if (components === void 0) {
545
+ throw new Error(`Field "${componentField}" does not exist on this content type`);
546
+ }
547
+ if (components === null) {
548
+ return { message: `"${componentField}" is null on this document`, clearedCount: 0 };
549
+ }
550
+ if (Array.isArray(components) && components.length === 0) {
551
+ return { message: `No "${componentField}" found on this document`, clearedCount: 0 };
552
+ }
553
+ const componentArray = Array.isArray(components) ? components : [components];
554
+ const isRepeatable = Array.isArray(components);
555
+ if (indices) {
556
+ for (const idx of indices) {
557
+ if (idx < 0 || idx >= componentArray.length) {
558
+ throw new Error(`Index ${idx} is out of range. "${componentField}" has ${componentArray.length} item${componentArray.length !== 1 ? "s" : ""} (indices 0-${componentArray.length - 1})`);
559
+ }
560
+ }
561
+ }
562
+ const targetIndices = indices ? new Set(indices) : null;
563
+ for (let i = 0; i < componentArray.length; i++) {
564
+ const comp = componentArray[i];
565
+ if (!comp || typeof comp !== "object") {
566
+ throw new Error(`Invalid component structure at index ${i}`);
567
+ }
568
+ if (!comp.id) {
569
+ throw new Error(`Component at index ${i} is missing required 'id' field`);
570
+ }
571
+ }
572
+ let totalCleared = 0;
573
+ let nestedFieldExists = false;
574
+ for (let i = 0; i < componentArray.length; i++) {
575
+ const comp = componentArray[i];
576
+ if (targetIndices && !targetIndices.has(i)) continue;
577
+ if (comp && comp[nestedField] !== void 0) {
578
+ nestedFieldExists = true;
579
+ const items = comp[nestedField];
580
+ totalCleared += this.countFieldItems(items);
581
+ }
582
+ }
583
+ if (!nestedFieldExists) {
584
+ throw new Error(`Field "${nestedField}" does not exist inside "${componentField}"`);
585
+ }
586
+ const targetDescription = indices ? `${indices.length} selected "${componentField}"` : `${componentArray.length} "${componentField}"`;
587
+ if (totalCleared === 0) {
588
+ return {
589
+ message: `"${nestedField}" is already empty in ${targetDescription}`,
590
+ clearedCount: 0
591
+ };
592
+ }
593
+ const updatedComponents = componentArray.map((comp, idx) => {
594
+ if (!comp) return null;
595
+ const updated = { id: comp.id };
596
+ for (const [key, value] of Object.entries(comp)) {
597
+ if (key === "id") {
598
+ continue;
599
+ }
600
+ if (key === nestedField && (!targetIndices || targetIndices.has(idx))) {
601
+ continue;
602
+ }
603
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
604
+ updated[key] = value;
605
+ }
606
+ }
607
+ if (!targetIndices || targetIndices.has(idx)) {
608
+ const originalValue = comp[nestedField];
609
+ updated[nestedField] = this.getEmptyValue(originalValue);
610
+ } else {
611
+ const originalValue = comp[nestedField];
612
+ if (Array.isArray(originalValue)) {
613
+ updated[nestedField] = originalValue.map((item) => {
614
+ if (item && typeof item === "object") {
615
+ return item.documentId ? { documentId: item.documentId } : { id: item.id };
616
+ }
617
+ return item;
618
+ });
619
+ } else if (originalValue && typeof originalValue === "object") {
620
+ updated[nestedField] = originalValue.documentId ? { documentId: originalValue.documentId } : { id: originalValue.id };
621
+ } else {
622
+ updated[nestedField] = originalValue;
623
+ }
624
+ }
625
+ return updated;
626
+ }).filter(Boolean);
627
+ const internalId = document.id;
628
+ if (!internalId) {
629
+ throw new Error("Document internal ID not found");
630
+ }
631
+ const updateData = isRepeatable ? updatedComponents : updatedComponents[0];
632
+ try {
633
+ await strapi.entityService.update(contentType, internalId, {
634
+ data: {
635
+ [componentField]: updateData
636
+ }
637
+ });
638
+ } catch (error) {
639
+ const message = error instanceof Error ? error.message : "Unknown error";
640
+ throw new Error(`Failed to update document: ${message}`);
641
+ }
642
+ return {
643
+ message: `Successfully cleared "${nestedField}" from ${targetDescription} (${totalCleared} item${totalCleared !== 1 ? "s" : ""})`,
644
+ clearedCount: totalCleared,
645
+ path: displayPath
646
+ };
647
+ },
648
+ /**
649
+ * Check if a field value is considered empty
650
+ */
651
+ isFieldEmpty(value) {
652
+ if (value === null || value === void 0) {
653
+ return true;
654
+ }
655
+ if (typeof value === "string" && value.trim() === "") {
656
+ return true;
657
+ }
658
+ if (Array.isArray(value) && value.length === 0) {
659
+ return true;
660
+ }
661
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
662
+ return true;
663
+ }
664
+ return false;
665
+ },
666
+ /**
667
+ * Count items in a field for reporting
668
+ */
669
+ countFieldItems(value) {
670
+ if (value === null || value === void 0) {
671
+ return 0;
672
+ }
673
+ if (Array.isArray(value)) {
674
+ return value.length;
675
+ }
676
+ if (typeof value === "string" && value.trim() === "") {
677
+ return 0;
678
+ }
679
+ if (typeof value === "object" && Object.keys(value).length === 0) {
680
+ return 0;
681
+ }
682
+ return 1;
683
+ },
684
+ /**
685
+ * Get the appropriate empty value based on the original value type
686
+ */
687
+ getEmptyValue(value) {
688
+ if (Array.isArray(value)) {
689
+ return [];
690
+ }
691
+ return null;
692
+ }
693
+ });
694
+ const services = {
695
+ service
696
+ };
697
+ const index = {
698
+ register,
699
+ bootstrap,
700
+ destroy,
701
+ config,
702
+ controllers,
703
+ routes,
704
+ services,
705
+ contentTypes,
706
+ policies,
707
+ middlewares
708
+ };
709
+ export {
710
+ index as default
711
+ };