2026-06-19 00:53:13 +08:00
|
|
|
import type { ActivityConfig } from "./activity.server";
|
|
|
|
|
|
|
|
|
|
type AdminGraphqlClient = {
|
|
|
|
|
graphql: (query: string, options?: { variables?: Record<string, unknown> }) => Promise<Response>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MetaobjectField = {
|
|
|
|
|
key: string;
|
|
|
|
|
value: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const METAOBJECT_QUERY = `#graphql
|
|
|
|
|
query DreameActivityConfig($type: String!, $handle: String!) {
|
|
|
|
|
metaobjectByHandle(handle: { type: $type, handle: $handle }) {
|
|
|
|
|
fields {
|
|
|
|
|
key
|
|
|
|
|
value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
export async function getActivityConfig(admin: AdminGraphqlClient): Promise<ActivityConfig> {
|
2026-06-19 16:18:12 +08:00
|
|
|
const type = process.env.METAOBJECT_TYPE || "$app:dreame_activity";
|
2026-06-19 00:53:13 +08:00
|
|
|
const handle = process.env.METAOBJECT_HANDLE || "dreame-activity-config";
|
|
|
|
|
const response = await admin.graphql(METAOBJECT_QUERY, { variables: { type, handle } });
|
|
|
|
|
const payload = (await response.json()) as {
|
|
|
|
|
data?: { metaobjectByHandle?: { fields?: MetaobjectField[] } | null };
|
|
|
|
|
errors?: unknown;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fields = payload.data?.metaobjectByHandle?.fields;
|
|
|
|
|
if (!fields?.length) {
|
|
|
|
|
throw new Error(`Dreame activity metaobject not found: ${type}/${handle}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const values = Object.fromEntries(fields.map((field) => [field.key, field.value]));
|
|
|
|
|
const buttonText = parseButtonText(values.button_text);
|
|
|
|
|
const activityId = Number(values.activity_id);
|
|
|
|
|
|
|
|
|
|
if (!Number.isFinite(activityId)) {
|
|
|
|
|
throw new Error("Metaobject field activity_id is required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
activityId,
|
|
|
|
|
apiAppId: requireString(values.api_app_id, "api_app_id"),
|
|
|
|
|
apiSecret: requireString(values.api_secret, "api_secret"),
|
|
|
|
|
statusApiUrl: requireString(values.status_api_url, "status_api_url"),
|
|
|
|
|
pushApiUrl: requireString(values.push_api_url, "push_api_url"),
|
2026-06-19 16:18:12 +08:00
|
|
|
statusSuccessCode: parseOptionalNumber(values.status_success_code, "status_success_code"),
|
|
|
|
|
statusActiveRequired: parseOptionalBoolean(values.status_active_required, "status_active_required"),
|
|
|
|
|
pushSuccessCode: parseOptionalNumber(values.push_success_code, "push_success_code"),
|
2026-06-19 00:53:13 +08:00
|
|
|
redirectUrl: requireString(values.redirect_url, "redirect_url"),
|
|
|
|
|
buttonText,
|
|
|
|
|
shop: values.shop || undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireString(value: string | null | undefined, key: string): string {
|
|
|
|
|
if (!value) {
|
|
|
|
|
throw new Error(`Metaobject field ${key} is required`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseButtonText(value: string | null | undefined): Record<string, string> {
|
|
|
|
|
if (!value) {
|
|
|
|
|
throw new Error("Metaobject field button_text is required");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = JSON.parse(value) as Record<string, unknown>;
|
|
|
|
|
const entries = Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === "string");
|
|
|
|
|
if (!entries.length) {
|
|
|
|
|
throw new Error("Metaobject field button_text must contain at least one locale");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.fromEntries(entries);
|
|
|
|
|
}
|
2026-06-19 16:18:12 +08:00
|
|
|
|
|
|
|
|
function parseOptionalNumber(value: string | null | undefined, key: string): number | undefined {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = Number(value);
|
|
|
|
|
if (!Number.isFinite(parsed)) {
|
|
|
|
|
throw new Error(`Metaobject field ${key} must be a number`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseOptionalBoolean(value: string | null | undefined, key: string): boolean | undefined {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
|
|
|
if (["true", "1", "yes"].includes(normalized)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (["false", "0", "no"].includes(normalized)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(`Metaobject field ${key} must be a boolean`);
|
|
|
|
|
}
|