@zachariaz/strapi-plugin-content-variants 0.1.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.
Files changed (43) hide show
  1. package/README.md +600 -0
  2. package/dist/_chunks/Segments-BREqC60L.js +330 -0
  3. package/dist/_chunks/Segments-BgxnvvtR.mjs +330 -0
  4. package/dist/_chunks/en-Bnfrhhim.js +62 -0
  5. package/dist/_chunks/en-e_966kWj.mjs +62 -0
  6. package/dist/_chunks/index-DVoZM8JU.js +1036 -0
  7. package/dist/_chunks/index-Dj2sexmk.mjs +1020 -0
  8. package/dist/admin/index.js +3 -0
  9. package/dist/admin/index.mjs +4 -0
  10. package/dist/admin/src/components/Initializer.d.ts +5 -0
  11. package/dist/admin/src/components/SegmentPickerAction.d.ts +7 -0
  12. package/dist/admin/src/components/VariantInfoAction.d.ts +8 -0
  13. package/dist/admin/src/components/VariantPanel.d.ts +13 -0
  14. package/dist/admin/src/components/VariantPickerAction.d.ts +20 -0
  15. package/dist/admin/src/contentManagerHooks/editView.d.ts +6 -0
  16. package/dist/admin/src/contentManagerHooks/listView.d.ts +22 -0
  17. package/dist/admin/src/hooks/useSegments.d.ts +17 -0
  18. package/dist/admin/src/hooks/useVariantFamily.d.ts +19 -0
  19. package/dist/admin/src/hooks/useVariantLinks.d.ts +44 -0
  20. package/dist/admin/src/index.d.ts +11 -0
  21. package/dist/admin/src/pages/Settings/Segments.d.ts +2 -0
  22. package/dist/admin/src/pluginId.d.ts +2 -0
  23. package/dist/admin/src/utils/batchLinkFetcher.d.ts +11 -0
  24. package/dist/admin/src/utils/variants.d.ts +13 -0
  25. package/dist/server/index.js +895 -0
  26. package/dist/server/index.mjs +896 -0
  27. package/dist/server/src/bootstrap.d.ts +17 -0
  28. package/dist/server/src/config/index.d.ts +5 -0
  29. package/dist/server/src/content-types/index.d.ts +121 -0
  30. package/dist/server/src/controllers/index.d.ts +24 -0
  31. package/dist/server/src/controllers/segment.d.ts +11 -0
  32. package/dist/server/src/controllers/variant-link.d.ts +18 -0
  33. package/dist/server/src/destroy.d.ts +5 -0
  34. package/dist/server/src/index.d.ts +244 -0
  35. package/dist/server/src/register.d.ts +5 -0
  36. package/dist/server/src/routes/admin.d.ts +12 -0
  37. package/dist/server/src/routes/content-api.d.ts +19 -0
  38. package/dist/server/src/routes/index.d.ts +25 -0
  39. package/dist/server/src/services/index.d.ts +62 -0
  40. package/dist/server/src/services/segment.d.ts +14 -0
  41. package/dist/server/src/services/variant-link.d.ts +60 -0
  42. package/dist/server/src/services/variant-resolver.d.ts +23 -0
  43. package/package.json +104 -0
@@ -0,0 +1,895 @@
1
+ "use strict";
2
+ const register = ({ strapi }) => {
3
+ };
4
+ const PLUGIN_ID$1 = "content-variants";
5
+ const VARIANT_LINK_UID$1 = "plugin::content-variants.variant-link";
6
+ const bootstrap = async ({ strapi }) => {
7
+ strapi.server.use(async (ctx, next) => {
8
+ const segment = ctx.query?.segment;
9
+ const includeVariants = ctx.query?.includeVariants;
10
+ if (segment) ctx.state.segment = segment;
11
+ if (includeVariants) ctx.state.includeVariants = includeVariants;
12
+ await next();
13
+ if (segment) {
14
+ ctx.set("Vary", "segment");
15
+ ctx.set("Cache-Control", "public, max-age=60, s-maxage=300");
16
+ } else if (includeVariants === "true") {
17
+ ctx.set("Vary", "includeVariants");
18
+ ctx.set("Cache-Control", "public, max-age=300, s-maxage=600");
19
+ }
20
+ });
21
+ strapi.documents.use(async (ctx, next) => {
22
+ const result = await next();
23
+ const action = ctx.action;
24
+ if (action !== "findMany" && action !== "findOne") {
25
+ return result;
26
+ }
27
+ const contentTypeUid = ctx.uid;
28
+ if (!contentTypeUid) return result;
29
+ if (typeof contentTypeUid === "string" && contentTypeUid.startsWith("plugin::content-variants.")) {
30
+ return result;
31
+ }
32
+ const params = ctx.params || {};
33
+ const segment = params.segment || params.filters?.segment || ctx.state?.segment;
34
+ const includeVariants = params.includeVariants || ctx.state?.includeVariants;
35
+ const isSegmentMode = typeof segment === "string" && segment.length > 0;
36
+ const isEnrichedMode = includeVariants === "true" && !isSegmentMode;
37
+ if (!isSegmentMode && !isEnrichedMode) {
38
+ return result;
39
+ }
40
+ const resolver = strapi.plugin(PLUGIN_ID$1).service("variant-resolver");
41
+ const variantLinkService2 = strapi.plugin(PLUGIN_ID$1).service("variant-link");
42
+ const isVariantEnabledUid = (uid) => {
43
+ try {
44
+ const ct = strapi.contentType(uid);
45
+ return !!ct?.pluginOptions?.[PLUGIN_ID$1]?.enabled;
46
+ } catch {
47
+ return false;
48
+ }
49
+ };
50
+ const topLevelVariantEnabled = isVariantEnabledUid(contentTypeUid);
51
+ const getVariantIds = async () => {
52
+ return variantLinkService2.findVariantDocumentIds(contentTypeUid);
53
+ };
54
+ const resolveDoc = async (base, targetUid) => {
55
+ if (!base || !base.documentId) return base;
56
+ const links = await strapi.documents(VARIANT_LINK_UID$1).findMany({
57
+ filters: {
58
+ $and: [
59
+ { baseContentType: { $eq: targetUid } },
60
+ { baseDocumentId: { $eq: base.documentId } }
61
+ ]
62
+ },
63
+ populate: ["assignments.segment"]
64
+ });
65
+ if (!Array.isArray(links) || links.length === 0) {
66
+ return base;
67
+ }
68
+ let bestLink = null;
69
+ let bestPriority = Infinity;
70
+ for (const link of links) {
71
+ for (const assignment of link.assignments || []) {
72
+ if (assignment.segment?.slug === segment) {
73
+ const p = assignment.priority ?? 0;
74
+ if (p < bestPriority) {
75
+ bestPriority = p;
76
+ bestLink = link;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ if (!bestLink || !bestLink.variantDocumentId) {
82
+ return base;
83
+ }
84
+ const variant = await strapi.documents(targetUid).findOne({
85
+ documentId: bestLink.variantDocumentId,
86
+ ...params.locale ? { locale: params.locale } : {},
87
+ ...params.status ? { status: params.status } : {}
88
+ });
89
+ if (!variant) {
90
+ return base;
91
+ }
92
+ return resolver.resolveDocument(base, variant, {
93
+ segmentSlug: segment,
94
+ contentTypeUid: targetUid
95
+ });
96
+ };
97
+ const enrichDoc = async (base, targetUid) => {
98
+ if (!base || !base.documentId) return base;
99
+ const links = await strapi.documents(VARIANT_LINK_UID$1).findMany({
100
+ filters: {
101
+ $and: [
102
+ { baseContentType: { $eq: targetUid } },
103
+ { baseDocumentId: { $eq: base.documentId } }
104
+ ]
105
+ },
106
+ populate: ["assignments.segment"]
107
+ });
108
+ if (!Array.isArray(links) || links.length === 0) {
109
+ return { ...base, _variants: [] };
110
+ }
111
+ const variants = [];
112
+ for (const link of links) {
113
+ const variantDoc = await strapi.documents(targetUid).findOne({
114
+ documentId: link.variantDocumentId,
115
+ ...params.locale ? { locale: params.locale } : {},
116
+ ...params.status ? { status: params.status } : {}
117
+ });
118
+ if (!variantDoc) continue;
119
+ const fields = resolver.extractVariantFields(variantDoc, targetUid);
120
+ const segments = (link.assignments || []).filter((a) => a.segment).map((a) => ({
121
+ name: a.segment.name,
122
+ slug: a.segment.slug
123
+ }));
124
+ variants.push({
125
+ documentId: link.variantDocumentId,
126
+ segments,
127
+ fields
128
+ });
129
+ }
130
+ return { ...base, _variants: variants };
131
+ };
132
+ const resolvePopulated = async (doc, ownerUid) => {
133
+ if (!doc || typeof doc !== "object") return;
134
+ let contentType;
135
+ try {
136
+ contentType = strapi.contentType(ownerUid);
137
+ } catch {
138
+ return;
139
+ }
140
+ const attributes2 = contentType?.attributes || {};
141
+ for (const [fieldName, attr] of Object.entries(attributes2)) {
142
+ const attribute = attr;
143
+ if (attribute.type === "relation" && attribute.target) {
144
+ const populated = doc[fieldName];
145
+ if (!populated || typeof populated !== "object") continue;
146
+ const targetUid = attribute.target;
147
+ if (targetUid.startsWith("plugin::content-variants.")) continue;
148
+ const targetVariantEnabled = isVariantEnabledUid(targetUid);
149
+ const isArray = Array.isArray(populated);
150
+ const items = isArray ? populated : [populated];
151
+ const processed = [];
152
+ for (const item of items) {
153
+ let resolved = item;
154
+ if (targetVariantEnabled && item?.documentId) {
155
+ if (isSegmentMode) {
156
+ resolved = await resolveDoc(item, targetUid);
157
+ } else if (isEnrichedMode) {
158
+ resolved = await enrichDoc(item, targetUid);
159
+ }
160
+ }
161
+ await resolvePopulated(resolved, targetUid);
162
+ processed.push(resolved);
163
+ }
164
+ doc[fieldName] = isArray ? processed : processed[0];
165
+ continue;
166
+ }
167
+ if (attribute.type === "component" && attribute.component) {
168
+ const populated = doc[fieldName];
169
+ if (!populated) continue;
170
+ const componentUid = attribute.component;
171
+ if (attribute.repeatable && Array.isArray(populated)) {
172
+ for (const item of populated) {
173
+ await resolvePopulated(item, componentUid);
174
+ }
175
+ } else if (typeof populated === "object") {
176
+ await resolvePopulated(populated, componentUid);
177
+ }
178
+ continue;
179
+ }
180
+ if (attribute.type === "dynamiczone") {
181
+ const populated = doc[fieldName];
182
+ if (!Array.isArray(populated)) continue;
183
+ for (const item of populated) {
184
+ if (item?.__component) {
185
+ await resolvePopulated(item, item.__component);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ };
191
+ const filterVariants = async (docs) => {
192
+ const variantIds = await getVariantIds();
193
+ if (variantIds.size === 0) return docs;
194
+ return docs.filter((doc) => !variantIds.has(doc.documentId));
195
+ };
196
+ const processDocArray = async (docs) => {
197
+ const filtered = topLevelVariantEnabled ? await filterVariants(docs) : docs;
198
+ const processed = [];
199
+ for (const item of filtered) {
200
+ let doc = item;
201
+ if (topLevelVariantEnabled) {
202
+ if (isSegmentMode) {
203
+ doc = await resolveDoc(item, contentTypeUid);
204
+ } else if (isEnrichedMode) {
205
+ doc = await enrichDoc(item, contentTypeUid);
206
+ }
207
+ }
208
+ await resolvePopulated(doc, contentTypeUid);
209
+ processed.push(doc);
210
+ }
211
+ return processed;
212
+ };
213
+ if (action === "findOne" && result && typeof result === "object" && !Array.isArray(result)) {
214
+ let doc = result;
215
+ if (topLevelVariantEnabled) {
216
+ if (isSegmentMode) {
217
+ doc = await resolveDoc(result, contentTypeUid);
218
+ } else if (isEnrichedMode) {
219
+ doc = await enrichDoc(result, contentTypeUid);
220
+ }
221
+ }
222
+ await resolvePopulated(doc, contentTypeUid);
223
+ return doc;
224
+ }
225
+ if (action === "findMany") {
226
+ if (Array.isArray(result)) {
227
+ return processDocArray(result);
228
+ }
229
+ const resultObj = result;
230
+ if (resultObj?.results && Array.isArray(resultObj.results)) {
231
+ return {
232
+ ...resultObj,
233
+ results: await processDocArray(resultObj.results)
234
+ };
235
+ }
236
+ }
237
+ return result;
238
+ });
239
+ };
240
+ const destroy = ({ strapi }) => {
241
+ };
242
+ const config = {
243
+ default: {},
244
+ validator(config2) {
245
+ }
246
+ };
247
+ const kind$2 = "collectionType";
248
+ const collectionName$2 = "segments";
249
+ const info$2 = {
250
+ singularName: "segment",
251
+ pluralName: "segments",
252
+ displayName: "Segment"
253
+ };
254
+ const options$2 = {
255
+ draftAndPublish: false
256
+ };
257
+ const pluginOptions$2 = {};
258
+ const attributes$2 = {
259
+ name: {
260
+ type: "string",
261
+ required: true,
262
+ unique: true
263
+ },
264
+ slug: {
265
+ type: "string",
266
+ required: true,
267
+ unique: true
268
+ },
269
+ description: {
270
+ type: "text"
271
+ },
272
+ externalId: {
273
+ type: "string"
274
+ }
275
+ };
276
+ const segmentSchema = {
277
+ kind: kind$2,
278
+ collectionName: collectionName$2,
279
+ info: info$2,
280
+ options: options$2,
281
+ pluginOptions: pluginOptions$2,
282
+ attributes: attributes$2
283
+ };
284
+ const kind$1 = "collectionType";
285
+ const collectionName$1 = "content_variant_links";
286
+ const info$1 = {
287
+ singularName: "variant-link",
288
+ pluralName: "variant-links",
289
+ displayName: "Variant Link"
290
+ };
291
+ const options$1 = {
292
+ draftAndPublish: false
293
+ };
294
+ const pluginOptions$1 = {
295
+ "content-manager": {
296
+ visible: false
297
+ },
298
+ "content-type-builder": {
299
+ visible: false
300
+ }
301
+ };
302
+ const attributes$1 = {
303
+ baseContentType: {
304
+ type: "string",
305
+ required: true
306
+ },
307
+ baseDocumentId: {
308
+ type: "string",
309
+ required: true
310
+ },
311
+ variantDocumentId: {
312
+ type: "string",
313
+ required: true
314
+ },
315
+ label: {
316
+ type: "string"
317
+ },
318
+ assignments: {
319
+ type: "relation",
320
+ relation: "oneToMany",
321
+ target: "plugin::content-variants.variant-assignment",
322
+ mappedBy: "variantLink"
323
+ }
324
+ };
325
+ const variantLinkSchema = {
326
+ kind: kind$1,
327
+ collectionName: collectionName$1,
328
+ info: info$1,
329
+ options: options$1,
330
+ pluginOptions: pluginOptions$1,
331
+ attributes: attributes$1
332
+ };
333
+ const kind = "collectionType";
334
+ const collectionName = "content_variant_assignments";
335
+ const info = {
336
+ singularName: "variant-assignment",
337
+ pluralName: "variant-assignments",
338
+ displayName: "Variant Assignment"
339
+ };
340
+ const options = {
341
+ draftAndPublish: false
342
+ };
343
+ const pluginOptions = {
344
+ "content-manager": {
345
+ visible: false
346
+ },
347
+ "content-type-builder": {
348
+ visible: false
349
+ }
350
+ };
351
+ const attributes = {
352
+ variantLink: {
353
+ type: "relation",
354
+ relation: "manyToOne",
355
+ target: "plugin::content-variants.variant-link",
356
+ inversedBy: "assignments"
357
+ },
358
+ segment: {
359
+ type: "relation",
360
+ relation: "oneToOne",
361
+ target: "plugin::content-variants.segment"
362
+ },
363
+ priority: {
364
+ type: "integer",
365
+ "default": 0,
366
+ min: 0
367
+ }
368
+ };
369
+ const variantAssignmentSchema = {
370
+ kind,
371
+ collectionName,
372
+ info,
373
+ options,
374
+ pluginOptions,
375
+ attributes
376
+ };
377
+ const contentTypes = {
378
+ segment: { schema: segmentSchema },
379
+ "variant-link": { schema: variantLinkSchema },
380
+ "variant-assignment": { schema: variantAssignmentSchema }
381
+ };
382
+ const segmentController = ({ strapi }) => ({
383
+ async find(ctx) {
384
+ const entities = await strapi.plugin("content-variants").service("segment").find(ctx.query);
385
+ ctx.body = entities;
386
+ },
387
+ async findOne(ctx) {
388
+ const { id } = ctx.params;
389
+ const entity = await strapi.plugin("content-variants").service("segment").findOne(id);
390
+ ctx.body = entity;
391
+ },
392
+ async create(ctx) {
393
+ const entity = await strapi.plugin("content-variants").service("segment").create(ctx.request.body);
394
+ ctx.body = entity;
395
+ },
396
+ async update(ctx) {
397
+ const { id } = ctx.params;
398
+ const entity = await strapi.plugin("content-variants").service("segment").update(id, ctx.request.body);
399
+ ctx.body = entity;
400
+ },
401
+ async delete(ctx) {
402
+ const { id } = ctx.params;
403
+ const entity = await strapi.plugin("content-variants").service("segment").delete(id);
404
+ ctx.body = entity;
405
+ }
406
+ });
407
+ const variantLinkController = ({ strapi }) => ({
408
+ async find(ctx) {
409
+ const entities = await strapi.plugin("content-variants").service("variant-link").find(ctx.query);
410
+ ctx.body = entities;
411
+ },
412
+ async findOne(ctx) {
413
+ const { id } = ctx.params;
414
+ const entity = await strapi.plugin("content-variants").service("variant-link").findOne(id);
415
+ ctx.body = entity;
416
+ },
417
+ async create(ctx) {
418
+ const entity = await strapi.plugin("content-variants").service("variant-link").create(ctx.request.body);
419
+ ctx.body = entity;
420
+ },
421
+ async update(ctx) {
422
+ const { id } = ctx.params;
423
+ const entity = await strapi.plugin("content-variants").service("variant-link").update(id, ctx.request.body);
424
+ ctx.body = entity;
425
+ },
426
+ async delete(ctx) {
427
+ const { id } = ctx.params;
428
+ const entity = await strapi.plugin("content-variants").service("variant-link").delete(id);
429
+ ctx.body = entity;
430
+ },
431
+ async findFamily(ctx) {
432
+ const entity = await strapi.plugin("content-variants").service("variant-link").findFamily(ctx.query);
433
+ ctx.body = entity;
434
+ },
435
+ async findBatch(ctx) {
436
+ const entity = await strapi.plugin("content-variants").service("variant-link").findBatch(ctx.request.body);
437
+ ctx.body = entity;
438
+ },
439
+ /**
440
+ * Clones base content document (current locale only), creates the variant document,
441
+ * then creates variant-link + variant-assignments for selected segments.
442
+ */
443
+ async createWithVariant(ctx) {
444
+ const entity = await strapi.plugin("content-variants").service("variant-link").createWithVariant(ctx.request.body);
445
+ ctx.body = entity;
446
+ }
447
+ });
448
+ const controllers = {
449
+ segment: segmentController,
450
+ "variant-link": variantLinkController
451
+ };
452
+ const admin = {
453
+ type: "admin",
454
+ routes: [
455
+ {
456
+ method: "GET",
457
+ path: "/segments",
458
+ handler: "segment.find",
459
+ config: { policies: [] }
460
+ },
461
+ {
462
+ method: "GET",
463
+ path: "/segments/:id",
464
+ handler: "segment.findOne",
465
+ config: { policies: [] }
466
+ },
467
+ {
468
+ method: "POST",
469
+ path: "/segments",
470
+ handler: "segment.create",
471
+ config: { policies: [] }
472
+ },
473
+ {
474
+ method: "PUT",
475
+ path: "/segments/:id",
476
+ handler: "segment.update",
477
+ config: { policies: [] }
478
+ },
479
+ {
480
+ method: "DELETE",
481
+ path: "/segments/:id",
482
+ handler: "segment.delete",
483
+ config: { policies: [] }
484
+ },
485
+ {
486
+ method: "GET",
487
+ path: "/links/family",
488
+ handler: "variant-link.findFamily",
489
+ config: { policies: [] }
490
+ },
491
+ {
492
+ method: "POST",
493
+ path: "/links/batch",
494
+ handler: "variant-link.findBatch",
495
+ config: { policies: [] }
496
+ },
497
+ {
498
+ method: "POST",
499
+ path: "/links/with-variant",
500
+ handler: "variant-link.createWithVariant",
501
+ config: { policies: [] }
502
+ },
503
+ {
504
+ method: "GET",
505
+ path: "/links",
506
+ handler: "variant-link.find",
507
+ config: { policies: [] }
508
+ },
509
+ {
510
+ method: "GET",
511
+ path: "/links/:id",
512
+ handler: "variant-link.findOne",
513
+ config: { policies: [] }
514
+ },
515
+ {
516
+ method: "POST",
517
+ path: "/links",
518
+ handler: "variant-link.create",
519
+ config: { policies: [] }
520
+ },
521
+ {
522
+ method: "PUT",
523
+ path: "/links/:id",
524
+ handler: "variant-link.update",
525
+ config: { policies: [] }
526
+ },
527
+ {
528
+ method: "DELETE",
529
+ path: "/links/:id",
530
+ handler: "variant-link.delete",
531
+ config: { policies: [] }
532
+ }
533
+ ]
534
+ };
535
+ const contentApi = {
536
+ type: "content-api",
537
+ routes: [
538
+ {
539
+ method: "GET",
540
+ path: "/segments",
541
+ handler: "segment.find",
542
+ config: {
543
+ policies: []
544
+ }
545
+ }
546
+ ]
547
+ };
548
+ const routes = {
549
+ admin,
550
+ "content-api": contentApi
551
+ };
552
+ const UID = "plugin::content-variants.segment";
553
+ function slugify(text) {
554
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-");
555
+ }
556
+ const segmentService = ({ strapi }) => ({
557
+ async find(query = {}) {
558
+ return strapi.documents(UID).findMany(query);
559
+ },
560
+ async findOne(documentId) {
561
+ return strapi.documents(UID).findOne({ documentId });
562
+ },
563
+ async create(data) {
564
+ if (!data.slug && data.name) {
565
+ data.slug = slugify(data.name);
566
+ }
567
+ return strapi.documents(UID).create({ data });
568
+ },
569
+ async update(documentId, data) {
570
+ return strapi.documents(UID).update({ documentId, data });
571
+ },
572
+ async delete(documentId) {
573
+ return strapi.documents(UID).delete({ documentId });
574
+ }
575
+ });
576
+ const PLUGIN_ID = "content-variants";
577
+ const variantResolverService = ({ strapi }) => ({
578
+ /**
579
+ * Returns the list of field names marked as variant on a content type.
580
+ */
581
+ getVariantFieldNames(contentTypeUid) {
582
+ const contentType = strapi.contentType(contentTypeUid);
583
+ if (!contentType) return [];
584
+ const attributes2 = contentType.attributes || {};
585
+ const names = [];
586
+ for (const [fieldName, attr] of Object.entries(attributes2)) {
587
+ const attribute = attr;
588
+ if (attribute?.pluginOptions?.[PLUGIN_ID]?.variant) {
589
+ names.push(fieldName);
590
+ }
591
+ }
592
+ return names;
593
+ },
594
+ /**
595
+ * Extracts only the variant-marked field values from a document.
596
+ */
597
+ extractVariantFields(document, contentTypeUid) {
598
+ const fieldNames = this.getVariantFieldNames(contentTypeUid);
599
+ const fields = {};
600
+ for (const name of fieldNames) {
601
+ if (name in document) {
602
+ fields[name] = document[name];
603
+ }
604
+ }
605
+ return fields;
606
+ },
607
+ /**
608
+ * Resolve variant fields: start with base document, overlay variant-marked
609
+ * fields from the variant document.
610
+ */
611
+ resolveDocument(base, variant, options2) {
612
+ if (!base) return base;
613
+ if (!variant) return base;
614
+ const contentType = strapi.contentType(options2.contentTypeUid);
615
+ if (!contentType) return base;
616
+ const ctOptions = contentType.pluginOptions;
617
+ if (!ctOptions?.[PLUGIN_ID]?.enabled) return base;
618
+ const resolved = { ...base };
619
+ const fieldNames = this.getVariantFieldNames(options2.contentTypeUid);
620
+ for (const fieldName of fieldNames) {
621
+ if (fieldName in variant) {
622
+ resolved[fieldName] = variant[fieldName];
623
+ }
624
+ }
625
+ return resolved;
626
+ }
627
+ });
628
+ const VARIANT_LINK_UID = "plugin::content-variants.variant-link";
629
+ const VARIANT_ASSIGNMENT_UID = "plugin::content-variants.variant-assignment";
630
+ const variantLinkService = ({ strapi }) => ({
631
+ async find(query = {}) {
632
+ const { baseContentType, baseDocumentId, variantDocumentId, populate, ...rest } = query;
633
+ const conditions = [];
634
+ if (baseContentType) {
635
+ conditions.push({ baseContentType: { $eq: baseContentType } });
636
+ }
637
+ if (baseDocumentId) {
638
+ conditions.push({ baseDocumentId: { $eq: baseDocumentId } });
639
+ }
640
+ if (variantDocumentId) {
641
+ conditions.push({ variantDocumentId: { $eq: variantDocumentId } });
642
+ }
643
+ return strapi.documents(VARIANT_LINK_UID).findMany({
644
+ ...rest,
645
+ filters: conditions.length > 0 ? { $and: conditions } : void 0,
646
+ populate: populate || ["assignments.segment"]
647
+ });
648
+ },
649
+ async findOne(documentId) {
650
+ return strapi.documents(VARIANT_LINK_UID).findOne({
651
+ documentId,
652
+ populate: ["assignments.segment"]
653
+ });
654
+ },
655
+ async create(data) {
656
+ return strapi.documents(VARIANT_LINK_UID).create({ data });
657
+ },
658
+ async update(documentId, data) {
659
+ return strapi.documents(VARIANT_LINK_UID).update({ documentId, data });
660
+ },
661
+ async delete(documentId) {
662
+ const link = await strapi.documents(VARIANT_LINK_UID).findOne({
663
+ documentId
664
+ });
665
+ const result = await strapi.documents(VARIANT_LINK_UID).delete({ documentId });
666
+ if (link?.variantDocumentId && link?.baseContentType) {
667
+ try {
668
+ await strapi.documents(link.baseContentType).delete({
669
+ documentId: link.variantDocumentId
670
+ });
671
+ } catch {
672
+ }
673
+ }
674
+ return result;
675
+ },
676
+ /**
677
+ * Resolves the full "variant family" for any document (base or variant).
678
+ * Returns { baseDocumentId, isVariant, currentDocumentId, baseStatus, links[] }.
679
+ * Each link is enriched with `variantStatus` ('draft' | 'published' | 'modified').
680
+ */
681
+ async findFamily(query) {
682
+ const { contentType, documentId } = query;
683
+ if (!contentType || !documentId) {
684
+ throw new Error("contentType and documentId are required");
685
+ }
686
+ const asVariant = await strapi.documents(VARIANT_LINK_UID).findMany({
687
+ filters: {
688
+ $and: [
689
+ { baseContentType: { $eq: contentType } },
690
+ { variantDocumentId: { $eq: documentId } }
691
+ ]
692
+ },
693
+ populate: ["assignments.segment"]
694
+ });
695
+ let baseDocumentId;
696
+ let isVariant;
697
+ if (asVariant.length > 0) {
698
+ baseDocumentId = asVariant[0].baseDocumentId;
699
+ isVariant = true;
700
+ } else {
701
+ baseDocumentId = documentId;
702
+ isVariant = false;
703
+ }
704
+ const links = await strapi.documents(VARIANT_LINK_UID).findMany({
705
+ filters: {
706
+ $and: [
707
+ { baseContentType: { $eq: contentType } },
708
+ { baseDocumentId: { $eq: baseDocumentId } }
709
+ ]
710
+ },
711
+ populate: ["assignments.segment"]
712
+ });
713
+ const resolveStatus = async (docId) => {
714
+ try {
715
+ const published = await strapi.documents(contentType).findOne({
716
+ documentId: docId,
717
+ status: "published"
718
+ });
719
+ if (!published) return "draft";
720
+ const draft = await strapi.documents(contentType).findOne({
721
+ documentId: docId
722
+ });
723
+ if (draft && new Date(draft.updatedAt) > new Date(published.updatedAt)) {
724
+ return "modified";
725
+ }
726
+ return "published";
727
+ } catch {
728
+ return "draft";
729
+ }
730
+ };
731
+ const allDocIds = [baseDocumentId, ...links.map((l) => l.variantDocumentId)].filter(Boolean);
732
+ const statuses = await Promise.all(allDocIds.map(resolveStatus));
733
+ const statusMap = {};
734
+ allDocIds.forEach((id, i) => {
735
+ statusMap[id] = statuses[i];
736
+ });
737
+ const enrichedLinks = links.map((link) => ({
738
+ ...link,
739
+ variantStatus: statusMap[link.variantDocumentId] || "draft"
740
+ }));
741
+ return {
742
+ baseDocumentId,
743
+ isVariant,
744
+ currentDocumentId: documentId,
745
+ baseStatus: statusMap[baseDocumentId] || "draft",
746
+ links: enrichedLinks
747
+ };
748
+ },
749
+ /**
750
+ * Returns a Set of all variantDocumentIds for a given content type.
751
+ * Used to filter variant clones out of public API list results.
752
+ */
753
+ async findVariantDocumentIds(contentType) {
754
+ const links = await strapi.documents(VARIANT_LINK_UID).findMany({
755
+ filters: { baseContentType: { $eq: contentType } },
756
+ fields: ["variantDocumentId"]
757
+ });
758
+ return new Set(links.map((l) => l.variantDocumentId));
759
+ },
760
+ /**
761
+ * Batch fetch variant links for multiple document IDs (base or variant).
762
+ * Searches both baseDocumentId and variantDocumentId fields.
763
+ * Returns a map of documentId → links[].
764
+ */
765
+ async findBatch(query) {
766
+ const { baseContentType, documentIds } = query;
767
+ if (!baseContentType || !Array.isArray(documentIds) || documentIds.length === 0) {
768
+ return {};
769
+ }
770
+ const links = await strapi.documents(VARIANT_LINK_UID).findMany({
771
+ filters: {
772
+ $and: [
773
+ { baseContentType: { $eq: baseContentType } },
774
+ {
775
+ $or: [
776
+ { baseDocumentId: { $in: documentIds } },
777
+ { variantDocumentId: { $in: documentIds } }
778
+ ]
779
+ }
780
+ ]
781
+ },
782
+ populate: ["assignments.segment"]
783
+ });
784
+ const grouped = {};
785
+ for (const id of documentIds) {
786
+ grouped[id] = [];
787
+ }
788
+ const idSet = new Set(documentIds);
789
+ for (const link of links) {
790
+ if (idSet.has(link.baseDocumentId)) {
791
+ grouped[link.baseDocumentId].push(link);
792
+ }
793
+ if (idSet.has(link.variantDocumentId)) {
794
+ grouped[link.variantDocumentId].push(link);
795
+ }
796
+ }
797
+ return grouped;
798
+ },
799
+ /**
800
+ * Creates a new variant document by cloning the base document (current locale),
801
+ * then creates the variant-link and its variant-assignments (segments + priority).
802
+ */
803
+ async createWithVariant(payload) {
804
+ const { baseContentType, baseDocumentId, locale, label, segments } = payload || {};
805
+ if (!baseContentType || !baseDocumentId) {
806
+ throw new Error("baseContentType and baseDocumentId are required");
807
+ }
808
+ if (!Array.isArray(segments) || segments.length === 0) {
809
+ throw new Error("segments[] is required");
810
+ }
811
+ const existingLinks = await strapi.documents(VARIANT_LINK_UID).findMany({
812
+ filters: {
813
+ $and: [
814
+ { baseContentType: { $eq: baseContentType } },
815
+ { baseDocumentId: { $eq: baseDocumentId } }
816
+ ]
817
+ },
818
+ populate: ["assignments.segment"]
819
+ });
820
+ const assignedSegmentIds = /* @__PURE__ */ new Set();
821
+ for (const link of existingLinks) {
822
+ for (const assignment of link.assignments || []) {
823
+ if (assignment.segment?.documentId) {
824
+ assignedSegmentIds.add(assignment.segment.documentId);
825
+ }
826
+ }
827
+ }
828
+ const duplicateSegments = segments.filter((s) => assignedSegmentIds.has(s.documentId));
829
+ if (duplicateSegments.length > 0) {
830
+ const names = duplicateSegments.map((s) => s.name || s.documentId).join(", ");
831
+ throw new Error(`Segments already assigned to existing variants: ${names}`);
832
+ }
833
+ const base = await strapi.documents(baseContentType).findOne({
834
+ documentId: baseDocumentId,
835
+ ...locale ? { locale } : {}
836
+ });
837
+ const contentType = strapi.contentType(baseContentType);
838
+ const attributes2 = contentType?.attributes || {};
839
+ const clonedData = {};
840
+ for (const attrKey of Object.keys(attributes2)) {
841
+ if (Object.prototype.hasOwnProperty.call(base, attrKey) && typeof base[attrKey] !== "undefined") {
842
+ clonedData[attrKey] = base[attrKey];
843
+ }
844
+ }
845
+ const variant = await strapi.documents(baseContentType).create({
846
+ data: clonedData
847
+ });
848
+ const variantDocumentId = variant?.documentId;
849
+ if (!variantDocumentId) {
850
+ throw new Error("Variant documentId was not returned");
851
+ }
852
+ const segmentNames = segments.map((s) => s.name).filter(Boolean);
853
+ const computedLabel = label?.trim() || (segmentNames.length > 0 ? `Variant for ${segmentNames.join(", ")}` : "Variant");
854
+ const variantLink = await strapi.documents(VARIANT_LINK_UID).create({
855
+ data: {
856
+ baseContentType,
857
+ baseDocumentId,
858
+ variantDocumentId,
859
+ label: computedLabel
860
+ }
861
+ });
862
+ const variantLinkDocumentId = variantLink?.documentId;
863
+ if (!variantLinkDocumentId) {
864
+ throw new Error("variant-link documentId was not returned");
865
+ }
866
+ await Promise.all(
867
+ segments.map(
868
+ (seg) => strapi.documents(VARIANT_ASSIGNMENT_UID).create({
869
+ data: {
870
+ priority: 0,
871
+ variantLink: { documentId: variantLinkDocumentId },
872
+ segment: { documentId: seg.documentId }
873
+ }
874
+ })
875
+ )
876
+ );
877
+ return { variantDocumentId, variantLinkDocumentId };
878
+ }
879
+ });
880
+ const services = {
881
+ segment: segmentService,
882
+ "variant-resolver": variantResolverService,
883
+ "variant-link": variantLinkService
884
+ };
885
+ const index = {
886
+ register,
887
+ bootstrap,
888
+ destroy,
889
+ config,
890
+ contentTypes,
891
+ controllers,
892
+ routes,
893
+ services
894
+ };
895
+ module.exports = index;