cb/server/laf-cloud/functions/abConfigAdmin.ts

157 lines
4.5 KiB
TypeScript

import cloud from '@lafjs/cloud'
const db = cloud.database();
const COLLECTION = "ab_config";
const DEFAULT_ADMIN_TOKEN = "abtest-admin-token";
export default async function (ctx: FunctionContext) {
const action = ctx.body?.action || ctx.query?.action || "list";
const adminToken = ctx.body?.adminToken || ctx.query?.adminToken;
if (!checkAdminToken(adminToken)) {
return { code: 0, data: null, msg: "unauthorized" };
}
switch (action) {
case "list":
return await listConfigs();
case "save":
return await saveConfig(ctx.body?.config);
case "disable":
return await setEnabled(ctx.body?.experimentId, false);
case "enable":
return await setEnabled(ctx.body?.experimentId, true);
case "delete":
return await deleteConfig(ctx.body?.experimentId);
default:
return { code: 0, data: null, msg: "invalid action" };
}
}
function checkAdminToken(adminToken: string) {
const env = (globalThis as any).process?.env || {};
const token = env.AB_ADMIN_TOKEN || DEFAULT_ADMIN_TOKEN;
return !!adminToken && adminToken === token;
}
async function listConfigs() {
const ret = await db.collection(COLLECTION).get();
return { code: 1, data: ret.data || [], msg: "ok" };
}
async function saveConfig(config: any) {
if (typeof config == "string") {
try {
config = JSON.parse(config);
} catch {
return { code: 0, data: null, msg: "config json invalid" };
}
}
if (Array.isArray(config)) {
const list = [];
for (let i = 0; i < config.length; i++) {
const ret = await saveOneConfig(config[i]);
if (ret.code !== 1) {
return ret;
}
list.push(ret.data);
}
return { code: 1, data: list, msg: "saved" };
}
return await saveOneConfig(config);
}
async function saveOneConfig(config: any) {
const error = validateConfig(config);
if (error) {
return { code: 0, data: null, msg: error };
}
const now = Date.now();
const data = {
experimentId: config.experimentId,
layer: String(config.layer || ""),
name: config.name || config.experimentId,
enabled: config.enabled !== false,
resetAssignments: config.resetAssignments === true,
startTime: Number(config.startTime || 0),
endTime: Number(config.endTime || 0),
variants: normalizeVariants(config.variants),
updatedAt: now,
};
const old = await db.collection(COLLECTION).where({ experimentId: data.experimentId }).getOne();
if (old.data) {
await db.collection(COLLECTION).where({ _id: old.data._id }).update(data);
return { code: 1, data: data, msg: "updated" };
}
await db.collection(COLLECTION).add({ ...data, createdAt: now });
return { code: 1, data: data, msg: "created" };
}
async function setEnabled(experimentId: string, enabled: boolean) {
if (!experimentId) {
return { code: 0, data: null, msg: "experimentId required" };
}
const ret = await db.collection(COLLECTION).where({ experimentId: experimentId }).update({
enabled: enabled,
updatedAt: Date.now(),
});
return { code: 1, data: ret, msg: "ok" };
}
async function deleteConfig(experimentId: string) {
if (!experimentId) {
return { code: 0, data: null, msg: "experimentId required" };
}
const ret = await db.collection(COLLECTION).where({ experimentId: experimentId }).remove({ multi: true });
return { code: 1, data: ret, msg: "ok" };
}
function validateConfig(config: any) {
if (!config) {
return "config required";
}
if (!config.experimentId) {
return "experimentId required";
}
if (!config.layer) {
return "layer required";
}
if (!Array.isArray(config.variants) || config.variants.length < 2) {
return "variants length must be >= 2";
}
let total = 0;
const groups = {};
for (let i = 0; i < config.variants.length; i++) {
const variant = config.variants[i];
if (!variant.group) {
return "variant.group required";
}
if (groups[variant.group]) {
return "variant.group duplicated";
}
groups[variant.group] = true;
const ratio = Number(variant.ratio);
if (!(ratio > 0)) {
return "variant.ratio must be positive";
}
total += ratio;
}
if (total <= 0) {
return "ratio total must be positive";
}
if (config.startTime && config.endTime && Number(config.endTime) <= Number(config.startTime)) {
return "endTime must be greater than startTime";
}
return "";
}
function normalizeVariants(variants: any[]) {
return variants.map(variant => ({
group: variant.group,
ratio: Number(variant.ratio),
}));
}