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