Skip to content

Commit 9e25328

Browse files
committed
fix(payload-plugin-ab): publish variants and recompute manifest on duplicate
1 parent 0c931b8 commit 9e25328

2 files changed

Lines changed: 99 additions & 88 deletions

File tree

Lines changed: 97 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,121 @@
11
import type { CollectionSlug, PayloadHandler } from "payload";
22
import { AB_PASS_PERCENTAGE_FIELD, AB_VARIANT_OF_FIELD, DEFAULT_SLUG_FIELD } from "../constants";
3+
import { recomputeManifestForParent } from "../utils/recomputeManifest";
4+
import type { AbTestingPluginConfig } from "../types/config";
35

46
// 6-char alphanumeric hash — no external dep needed
57
function nanoid(): string {
68
return Math.random().toString(36).slice(2, 8);
79
}
810

9-
export const duplicateVariantHandler: PayloadHandler = async (req) => {
10-
if (!req.payload) {
11-
return Response.json({ error: "Payload not available" }, { status: 500 });
12-
}
11+
export const buildDuplicateVariantHandler =
12+
<TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): PayloadHandler =>
13+
async (req) => {
14+
if (!req.payload) {
15+
return Response.json({ error: "Payload not available" }, { status: 500 });
16+
}
1317

14-
let body: { collectionSlug?: string; docId?: string; slugField?: string };
15-
try {
16-
if (!req.json) {
18+
let body: { collectionSlug?: string; docId?: string; slugField?: string };
19+
try {
20+
if (!req.json) {
21+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
22+
}
23+
body = await req.json();
24+
} catch {
1725
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
1826
}
19-
body = await req.json();
20-
} catch {
21-
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
22-
}
23-
24-
const { collectionSlug, docId, slugField = DEFAULT_SLUG_FIELD } = body;
25-
26-
if (!collectionSlug || !docId) {
27-
return Response.json({ error: "collectionSlug and docId are required" }, { status: 400 });
28-
}
29-
30-
let parentDoc: Record<string, unknown>;
31-
try {
32-
parentDoc = (await req.payload.findByID({
33-
collection: collectionSlug as CollectionSlug,
34-
id: docId,
35-
depth: 0,
36-
overrideAccess: false,
37-
req,
38-
})) as Record<string, unknown>;
39-
40-
if (!parentDoc) {
27+
28+
const { collectionSlug, docId, slugField = DEFAULT_SLUG_FIELD } = body;
29+
30+
if (!collectionSlug || !docId) {
31+
return Response.json({ error: "collectionSlug and docId are required" }, { status: 400 });
32+
}
33+
34+
const abConfig = pluginConfig.collections[collectionSlug];
35+
if (!abConfig) {
36+
return Response.json(
37+
{ error: `Collection "${collectionSlug}" is not configured for A/B testing` },
38+
{ status: 400 },
39+
);
40+
}
41+
42+
let parentDoc: Record<string, unknown>;
43+
try {
44+
parentDoc = (await req.payload.findByID({
45+
collection: collectionSlug as CollectionSlug,
46+
id: docId,
47+
depth: 0,
48+
overrideAccess: false,
49+
req,
50+
})) as Record<string, unknown>;
51+
52+
if (!parentDoc) {
53+
return Response.json({ error: "Parent document not found" }, { status: 404 });
54+
}
55+
} catch {
4156
return Response.json({ error: "Parent document not found" }, { status: 404 });
4257
}
43-
} catch {
44-
return Response.json({ error: "Parent document not found" }, { status: 404 });
45-
}
46-
47-
const originalSlug = (parentDoc[slugField] as string) ?? docId;
48-
49-
let existingVariants: Array<Record<string, unknown>>;
50-
try {
51-
const result = await req.payload.find({
52-
collection: collectionSlug as CollectionSlug,
53-
where: { [AB_VARIANT_OF_FIELD]: { equals: docId } },
54-
depth: 0,
55-
limit: 100,
56-
overrideAccess: true,
57-
req,
58-
});
59-
existingVariants = result.docs as Array<Record<string, unknown>>;
60-
} catch (err) {
61-
const message = err instanceof Error ? err.message : "Failed to read existing variants";
62-
return Response.json({ error: message }, { status: 500 });
63-
}
64-
65-
const totalSlots = existingVariants.length + 2;
66-
const perSlot = Math.max(1, Math.floor(100 / totalSlots));
67-
68-
const transactionID = (await req.payload.db.beginTransaction?.()) ?? undefined;
69-
if (transactionID) {
70-
req.transactionID = transactionID;
71-
}
72-
73-
try {
74-
for (const variant of existingVariants) {
75-
await req.payload.update({
58+
59+
const originalSlug = (parentDoc[slugField] as string) ?? docId;
60+
61+
let existingVariants: Array<Record<string, unknown>>;
62+
try {
63+
const result = await req.payload.find({
7664
collection: collectionSlug as CollectionSlug,
77-
id: variant.id as string,
78-
data: { [AB_PASS_PERCENTAGE_FIELD]: perSlot },
79-
draft: true,
65+
where: { [AB_VARIANT_OF_FIELD]: { equals: docId } },
66+
depth: 0,
67+
limit: 100,
8068
overrideAccess: true,
8169
req,
8270
});
71+
existingVariants = result.docs as Array<Record<string, unknown>>;
72+
} catch (err) {
73+
const message = err instanceof Error ? err.message : "Failed to read existing variants";
74+
return Response.json({ error: message }, { status: 500 });
8375
}
8476

85-
const newDoc = (await req.payload.duplicate({
86-
collection: collectionSlug as CollectionSlug,
87-
id: docId,
88-
data: {
89-
[slugField]: `${originalSlug}--${nanoid()}`,
90-
[AB_VARIANT_OF_FIELD]: docId,
91-
[AB_PASS_PERCENTAGE_FIELD]: perSlot,
92-
},
93-
draft: true,
94-
overrideAccess: true,
95-
req,
96-
})) as Record<string, unknown>;
77+
const totalSlots = existingVariants.length + 2;
78+
const perSlot = Math.max(1, Math.floor(100 / totalSlots));
9779

80+
const transactionID = (await req.payload.db.beginTransaction?.()) ?? undefined;
9881
if (transactionID) {
99-
await req.payload.db.commitTransaction?.(transactionID);
82+
req.transactionID = transactionID;
10083
}
10184

102-
return Response.json({ id: newDoc.id, slug: newDoc[slugField], passPercentage: perSlot }, { status: 201 });
103-
} catch (err) {
104-
if (transactionID) {
105-
await req.payload.db.rollbackTransaction?.(transactionID);
85+
try {
86+
for (const variant of existingVariants) {
87+
await req.payload.db.updateOne({
88+
collection: collectionSlug,
89+
id: variant.id as string | number,
90+
data: { [AB_PASS_PERCENTAGE_FIELD]: perSlot },
91+
req,
92+
});
93+
}
94+
95+
const newDoc = (await req.payload.duplicate({
96+
collection: collectionSlug as CollectionSlug,
97+
id: docId,
98+
data: {
99+
[slugField]: `${originalSlug}--${nanoid()}`,
100+
[AB_VARIANT_OF_FIELD]: docId,
101+
[AB_PASS_PERCENTAGE_FIELD]: perSlot,
102+
},
103+
overrideAccess: true,
104+
req,
105+
})) as Record<string, unknown>;
106+
107+
await recomputeManifestForParent(docId, collectionSlug, abConfig, pluginConfig, req);
108+
109+
if (transactionID) {
110+
await req.payload.db.commitTransaction?.(transactionID);
111+
}
112+
113+
return Response.json({ id: newDoc.id, slug: newDoc[slugField], passPercentage: perSlot }, { status: 201 });
114+
} catch (err) {
115+
if (transactionID) {
116+
await req.payload.db.rollbackTransaction?.(transactionID);
117+
}
118+
const message = err instanceof Error ? err.message : "Failed to create variant";
119+
return Response.json({ error: message }, { status: 500 });
106120
}
107-
const message = err instanceof Error ? err.message : "Failed to create variant";
108-
return Response.json({ error: message }, { status: 500 });
109-
}
110-
};
121+
};

packages/payload-plugin-ab/src/plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { injectAdminFields } from "./utils/injectAdminFields";
44
import { buildParentAfterChangeHook } from "./hooks/buildParentAfterChangeHook";
55
import { buildParentAfterDeleteHook } from "./hooks/buildParentAfterDeleteHook";
66
import { buildParentBeforeChangeHook } from "./hooks/buildParentBeforeChangeHook";
7-
import { duplicateVariantHandler } from "./endpoints/duplicateVariant";
7+
import { buildDuplicateVariantHandler } from "./endpoints/duplicateVariant";
88

99
export const abTestingPlugin =
1010
<TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): Plugin =>
@@ -52,7 +52,7 @@ export const abTestingPlugin =
5252
{
5353
path: "/_ab/duplicate",
5454
method: "post",
55-
handler: duplicateVariantHandler,
55+
handler: buildDuplicateVariantHandler(pluginConfig),
5656
},
5757
],
5858
};

0 commit comments

Comments
 (0)