fix: tests

This commit is contained in:
John Yeo
2025-10-19 10:23:49 +01:00
parent 5d759eba8d
commit 2eeb83ed42
19 changed files with 297 additions and 146 deletions

View File

@@ -5,9 +5,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"
# If contains setup then run $MOCHA_SETUP
if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
# if [[ "$1" == *"setup"* ]]; then
# MOCHA_PARALLEL=true $MOCHA_SETUP
# fi
$MOCHA_CMD \
'tests/attach/basic/*.ts' \

View File

@@ -7,20 +7,19 @@ source "$(dirname "$0")/config.sh"
if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
# $MOCHA_CMD 'tests/advanced/referrals/*.ts' 'tests/advanced/coupons/*.ts'
# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
# 'tests/advanced/coupons/*.ts' \
# 'tests/attach/updateQuantity/*.ts' \
# 'tests/advanced/referrals/*.ts' \
# 'tests/advanced/referrals/paid/*.ts' \
# 'tests/advanced/rollovers/*.ts' \
# 'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/referrals/paid/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
# 'tests/advanced/usageLimit/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'

View File

@@ -1,4 +1,4 @@
import { type AppEnv, type Organization } from "@autumn/shared";
import type { AppEnv, Organization } from "@autumn/shared";
import chalk from "chalk";
import { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
@@ -266,6 +266,7 @@ export const handleStripeWebhookEvent = async ({
});
} catch (error) {
logger.error(`Stripe webhook, error refreshing cache!`, { error });
return { success: true };
}
return { success: true };

View File

@@ -41,7 +41,10 @@ export const handleConnectWebhook = async (c: Context<HonoEnv>) => {
}
const accountId = event.account;
if (!accountId) return c.json({ error: "Account ID not found" }, 200);
if (!accountId) {
logger.error(`Account ID not found in webhook event`);
return c.json({ error: "Account ID not found" }, 200);
}
const { org, features } = await OrgService.getByAccountId({
db,
@@ -78,6 +81,6 @@ export const handleConnectWebhook = async (c: Context<HonoEnv>) => {
return c.json({ message: "Webhook received" }, 200);
} catch (error) {
logger.error(`Stripe webhook, error: ${error}`, { error });
return c.json({ message: "Internal server error" }, 500);
return c.json({ message: "Webhook received, internal server error" }, 200); // 200 to avoid retries / shutdown of webhook...
}
};

View File

@@ -0,0 +1,46 @@
import {
type ApiPlatformOrg,
type ListPlatformOrgsQuery,
ListPlatformOrgsQuerySchema,
organizations,
} from "@autumn/shared";
import { eq } from "drizzle-orm";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { toPlatformOrg } from "./platformOrgUtils.js";
/**
* Route: GET /platform/orgs - List organizations created by master org
*/
export const handleListPlatformOrgs = createRoute({
query: ListPlatformOrgsQuerySchema,
handler: async (c) => {
const query = c.req.valid("query") as ListPlatformOrgsQuery;
const ctx = c.get("ctx");
const { db, org: masterOrg } = ctx;
const orgs = await db
.select()
.from(organizations)
.where(eq(organizations.created_by, masterOrg.id))
.limit(query.limit)
.offset(query.offset);
const orgsList: ApiPlatformOrg[] = orgs.map((org) =>
toPlatformOrg({
org: {
slug: org.slug,
name: org.name,
createdAt: org.createdAt,
},
masterOrgId: masterOrg.id,
}),
);
return c.json({
list: orgsList,
total: orgs.length,
limit: query.limit,
offset: query.offset,
});
},
});

View File

@@ -9,6 +9,7 @@ import {
import { eq } from "drizzle-orm";
import { cte } from "@/db/cteUtils/buildCte.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { toPlatformOrg } from "./platformOrgUtils.js";
/**
* Route: GET /platform/users - List users created by master org
@@ -19,7 +20,7 @@ export const listPlatformUsers = createRoute({
const query = c.req.valid("query") as ListPlatformUsersQuery;
const ctx = c.get("ctx");
const { db, org, logger } = ctx;
const { db, org } = ctx;
const shouldExpandOrgs = query.expand?.includes("organizations");
@@ -54,11 +55,16 @@ export const listPlatformUsers = createRoute({
created_at: new Date(user.created_at).getTime(),
...(shouldExpandOrgs &&
user.organizations && {
organizations: user.organizations.map((org: any) => ({
slug: cleanOrgSlug(org.slug, org.id),
name: org.name,
created_at: new Date(org.createdAt).getTime(),
})),
organizations: user.organizations.map((org: any) =>
toPlatformOrg({
org: {
slug: org.slug,
name: org.name,
createdAt: org.createdAt,
},
masterOrgId: ctx.org?.id || "",
}),
),
}),
}));
@@ -70,23 +76,3 @@ export const listPlatformUsers = createRoute({
});
},
});
/**
* Remove master org ID prefix from organization slug
*/
function cleanOrgSlug(slug: string, orgId: string): string {
let cleanedSlug = slug;
const prefix = `${orgId}_`;
if (cleanedSlug.startsWith(prefix)) {
cleanedSlug = cleanedSlug.slice(prefix.length);
}
// Handle the case where slug is prepended with "slug_orgId"
const altPrefix1 = `_${orgId}`;
const altPrefix2 = `|${orgId}`;
if (cleanedSlug.endsWith(altPrefix1)) {
cleanedSlug = cleanedSlug.slice(0, -altPrefix1.length);
} else if (cleanedSlug.endsWith(altPrefix2)) {
cleanedSlug = cleanedSlug = cleanedSlug.slice(0, -altPrefix2.length);
}
return cleanedSlug;
}

View File

@@ -0,0 +1,44 @@
import type { ApiPlatformOrg } from "@autumn/shared";
/**
* Remove master org ID prefix from organization slug
*/
function cleanOrgSlug({
slug,
orgId,
}: {
slug: string;
orgId: string;
}): string {
let cleanedSlug = slug;
const prefix = `${orgId}_`;
if (cleanedSlug.startsWith(prefix)) {
cleanedSlug = cleanedSlug.slice(prefix.length);
}
// Handle the case where slug is prepended with "slug_orgId"
const altPrefix1 = `_${orgId}`;
const altPrefix2 = `|${orgId}`;
if (cleanedSlug.endsWith(altPrefix1)) {
cleanedSlug = cleanedSlug.slice(0, -altPrefix1.length);
} else if (cleanedSlug.endsWith(altPrefix2)) {
cleanedSlug = cleanedSlug = cleanedSlug.slice(0, -altPrefix2.length);
}
return cleanedSlug;
}
/**
* Convert raw org data to ApiPlatformOrg format
*/
export function toPlatformOrg({
org,
masterOrgId,
}: {
org: { slug: string; name: string; createdAt: string | Date };
masterOrgId: string;
}): ApiPlatformOrg {
return {
slug: cleanOrgSlug({ slug: org.slug, orgId: masterOrgId }),
name: org.name,
created_at: new Date(org.createdAt).getTime(),
};
}

View File

@@ -1,9 +1,10 @@
import { Autumn } from "autumn-js";
import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { listPlatformUsers } from "../platformLegacy/handlers/handleListPlatformUsers.js";
import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js";
import { handleGetPlatformOAuth } from "./handlers/handleGetPlatformOAuth.js";
import { handleListPlatformOrgs } from "./handlers/handleListPlatformOrgs.js";
import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js";
import { handleUpdateOrganizationStripe } from "./handlers/handleUpdateOrganizationStripe.js";
const platformBetaRouter = new Hono<HonoEnv>();
@@ -61,7 +62,7 @@ platformBetaRouter.use("*", async (c, next) => {
* POST /organization
* Creates a new organization for platform users
*/
platformBetaRouter.post("/organization", ...handleCreatePlatformOrg);
platformBetaRouter.post("/organizations", ...handleCreatePlatformOrg);
/**
* POST /oauth_url
@@ -80,4 +81,5 @@ platformBetaRouter.post(
platformBetaRouter.get("/users", ...listPlatformUsers);
platformBetaRouter.get("/organizations", ...handleListPlatformOrgs);
export { platformBetaRouter };

View File

@@ -57,7 +57,6 @@ const reward: CreateReward = {
describe(
chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`),
() => {
let logger: any;
const customerId = testCase;
let stripeCli: Stripe;
let testClockId: string;
@@ -67,7 +66,7 @@ describe(
let env: AppEnv;
let db: DrizzleCli;
let couponAmount = reward.discount_config!.discount_value;
let couponAmount = reward.discount_config?.discount_value ?? 0;
before(async function () {
await setupBefore(this);

View File

@@ -1,35 +1,34 @@
import chalk from "chalk";
import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { features } from "tests/global.js";
import { setupBefore } from "tests/before.js";
/** biome-ignore-all lint/suspicious/noExportsInTest: needed */
import {
AppEnv,
type AppEnv,
BillingInterval,
EntInterval,
ProductItemFeatureType,
UsageModel,
} from "@autumn/shared";
import { createProducts } from "tests/utils/productUtils.js";
import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js";
import { expect } from "chai";
import chalk from "chalk";
import { addMonths } from "date-fns";
import { setupBefore } from "tests/before.js";
import { features } from "tests/global.js";
import {
getLifetimeFreeCusEnt,
getUsageCusEnt,
} from "tests/utils/cusProductUtils/cusEntSearchUtils.js";
import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructFeatureItem,
constructFeaturePriceItem,
} from "@/internal/products/product-items/productItemUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import { addMonths } from "date-fns";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly
let pro = {
const pro = {
id: "multiFeature3Pro",
name: "Multi Feature 3 Pro",
items: {
@@ -62,19 +61,19 @@ export const getLifetimeAndUsageCusEnts = async ({
env: AppEnv;
featureId: string;
}) => {
let mainCusProduct = await getMainCusProduct({
const mainCusProduct = await getMainCusProduct({
customerId,
db,
orgId,
env,
});
let lifetimeCusEnt = getLifetimeFreeCusEnt({
const lifetimeCusEnt = getLifetimeFreeCusEnt({
cusProduct: mainCusProduct!,
featureId,
});
let usageCusEnt = getUsageCusEnt({
const usageCusEnt = getUsageCusEnt({
cusProduct: mainCusProduct!,
featureId,
});
@@ -86,8 +85,8 @@ export const getLifetimeAndUsageCusEnts = async ({
describe(`${chalk.yellowBright(
"multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock",
)}`, () => {
let autumn: AutumnInt = new AutumnInt();
let customerId = "multiFeature3Customer";
const autumn: AutumnInt = new AutumnInt();
const customerId = "multiFeature3Customer";
let totalUsage = 0;
@@ -95,17 +94,16 @@ describe(`${chalk.yellowBright(
before(async function () {
await setupBefore(this);
let { customer, testClockId: _testClockId } =
await initCustomerWithTestClock({
customerId,
db: this.db,
org: this.org,
env: this.env,
});
const res = await initCustomerV2({
autumn,
customerId,
db: this.db,
org: this.org,
env: this.env,
attachPm: "success",
});
testClockId = _testClockId;
autumn = this.autumn;
testClockId = res.testClockId;
await createProducts({
autumn,
@@ -122,7 +120,7 @@ describe(`${chalk.yellowBright(
product_id: pro.id,
});
let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
customerId,
db: this.db,
orgId: this.org.id,
@@ -135,7 +133,7 @@ describe(`${chalk.yellowBright(
expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
});
let overageValue = 30;
const overageValue = 30;
it("should use lifetime allowance + overage", async function () {
let value = pro.items.lifetime.included_usage as number;
value += overageValue;
@@ -150,7 +148,7 @@ describe(`${chalk.yellowBright(
await timeout(3000);
let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
customerId,
db: this.db,
orgId: this.org.id,
@@ -163,14 +161,14 @@ describe(`${chalk.yellowBright(
});
it("cycle 1:should have correct usage after first cycle", async function () {
let advanceTo = addMonths(new Date(), 1).getTime();
const advanceTo = addMonths(new Date(), 1).getTime();
await advanceTestClock({
stripeCli: this.stripeCli,
testClockId,
advanceTo,
});
let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
customerId,
db: this.db,
orgId: this.org.id,

View File

@@ -1,6 +1,8 @@
import {
type AppEnv,
type Customer,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
@@ -12,8 +14,9 @@ import { setupBefore } from "tests/before.js";
import { timeout } from "tests/utils/genUtils.js";
import { initCustomer } from "tests/utils/init.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
import { products, referralPrograms } from "../../global.js";
// UNCOMMENT FROM HERE
@@ -29,18 +32,21 @@ describe(`${chalk.yellowBright(
const redemptions: RewardRedemption[] = [];
let mainCustomer: Customer;
let org: Organization;
let env: AppEnv;
before(async function () {
await setupBefore(this);
stripeCli = this.stripeCli;
org = this.org;
env = this.env;
const { testClockId: testClockId1, customer } =
await initCustomerWithTestClock({
customerId: mainCustomerId,
db: this.db,
org: this.org,
env: this.env,
});
const { testClockId: testClockId1, customer } = await initCustomerV2({
customerId: mainCustomerId,
db: this.db,
org: this.org,
env: this.env,
autumn,
});
testClockId = testClockId1;
mainCustomer = customer;
@@ -95,8 +101,17 @@ describe(`${chalk.yellowBright(
}
// Check stripe customer
const stripeCus = (await stripeCli.customers.retrieve(
const legacyStripe = createStripeCli({
org: org,
env: env,
legacyVersion: true,
});
const stripeCus = (await legacyStripe.customers.retrieve(
mainCustomer.processor?.id,
{
expand: ["discount"],
},
)) as Stripe.Customer;
assert.notEqual(stripeCus.discount, null);

View File

@@ -1,31 +1,31 @@
import { features, products, referralPrograms } from "../../global.js";
import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared";
import { assert } from "chai";
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { Customer, ReferralCode, RewardRedemption } from "@autumn/shared";
import { timeout } from "tests/utils/genUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import { Stripe } from "stripe";
import { initCustomer } from "tests/utils/init.js";
import { compareProductEntitlements } from "tests/utils/compare.js";
import { addDays, addHours } from "date-fns";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import type { Stripe } from "stripe";
import { setupBefore } from "tests/before.js";
import { compareProductEntitlements } from "tests/utils/compare.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { timeout } from "tests/utils/genUtils.js";
import { initCustomer } from "tests/utils/init.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { features, products, referralPrograms } from "../../global.js";
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
"referrals4: Testing free product referrals with trial",
)}`, () => {
let mainCustomerId = "main-referral-4";
const mainCustomerId = "main-referral-4";
// let redeemers = ["referral4-r1", "referral4-r2"];
let redeemerId = "referral4-r1";
const redeemerId = "referral4-r1";
let autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
let referralCode: ReferralCode;
let redemptions: RewardRedemption[] = [];
const redemptions: RewardRedemption[] = [];
let mainCustomer: Customer;
let redeemer: Customer;
@@ -48,7 +48,7 @@ describe(`${chalk.yellowBright(
product_id: products.proWithTrial.id,
});
let { testClockId: testClockId1, customer } =
const { testClockId: testClockId1, customer } =
await initCustomerWithTestClock({
customerId: redeemerId,
db: this.db,
@@ -60,7 +60,7 @@ describe(`${chalk.yellowBright(
redeemer = customer;
});
it("should create referral code", async function () {
it("should create referral code", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.freeProduct.id,
@@ -69,8 +69,8 @@ describe(`${chalk.yellowBright(
assert.exists(referralCode.code);
});
it("should create redemption for each redeemer and fail if redeemed again", async function () {
let redemption: RewardRedemption = await autumn.referrals.redeem({
it("should create redemption for each redeemer and fail if redeemed again", async () => {
const redemption: RewardRedemption = await autumn.referrals.redeem({
customerId: redeemerId,
code: referralCode.code,
});
@@ -78,7 +78,7 @@ describe(`${chalk.yellowBright(
redemptions.push(redemption);
});
it("should not be triggered because of trial", async function () {
it("should not be triggered because of trial", async () => {
await autumn.attach({
customer_id: redeemerId,
product_id: products.proWithTrial.id,
@@ -87,13 +87,13 @@ describe(`${chalk.yellowBright(
await timeout(3000);
// Get redemption object
let redemption = await autumn.redemptions.get(redemptions[0].id);
const redemption = await autumn.redemptions.get(redemptions[0].id);
assert.equal(redemption.triggered, false);
});
it("should be triggered after trial ends", async function () {
let advanceTo = addHours(
it("should be triggered after trial ends", async () => {
const advanceTo = addHours(
addDays(new Date(), 7),
hoursToFinalizeInvoice,
).getTime();
@@ -104,7 +104,7 @@ describe(`${chalk.yellowBright(
waitForSeconds: 30,
});
let redemption = await autumn.redemptions.get(redemptions[0].id);
const redemption = await autumn.redemptions.get(redemptions[0].id);
assert.equal(redemption.triggered, true);

View File

@@ -1,3 +1,4 @@
import type { AppEnv, Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
@@ -7,6 +8,7 @@ import { compareMainProduct } from "tests/utils/compare.js";
import { timeout } from "tests/utils/genUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
@@ -51,7 +53,7 @@ const testCase = "basic3";
describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
let db, org, env;
let db: DrizzleCli, org: Organization, env: AppEnv;
before(async function () {
await setupBefore(this);

View File

@@ -38,19 +38,6 @@ const pro = constructProduct({
trial: true,
});
const ops = [
{
entityId: "1",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
];
const testCase = "multiInvoice1";
describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => {
const customerId = testCase;
@@ -98,7 +85,7 @@ describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invo
testClockId = testClockId1!;
});
it("should run multi attach through checkout and have correct sub", async () => {
it("should run multi attach through invoice checkout flow", async () => {
const productsList = [
{
product_id: pro.id,

View File

@@ -6,6 +6,7 @@ import {
type ProductOptions,
type ProductV2,
} from "@autumn/shared";
import type { Customer, Entity } from "autumn-js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
@@ -75,8 +76,10 @@ export const expectMultiAttachCorrect = async ({
await timeout(5000);
}
await timeout(2500);
for (const result of results) {
let customer;
let customer: Customer | Entity;
if (result.entityId) {
customer = await autumn.entities.get(customerId, result.entityId);
} else {
@@ -84,7 +87,7 @@ export const expectMultiAttachCorrect = async ({
}
expectProductAttached({
customer,
customer: customer as Customer,
product: result.product,
status: result.status,
entityId: result.entityId,

View File

@@ -27,3 +27,48 @@ export function queryStringArray<T extends z.ZodTypeAny>(schema: T) {
return val;
}, z.array(schema));
}
/**
* Helper to handle query string integers that come in as strings and need to be converted to numbers.
* Query parameters are always strings, so this helper parses them to integers for validation.
*
* @example
* ```ts
* const schema = z.object({
* limit: queryInteger({ min: 1, max: 100 }).default(10),
* offset: queryInteger({ min: 0 }).default(0),
* });
* ```
*/
export function queryInteger(options?: {
min?: number;
max?: number;
error?: string;
}) {
let schema = z.number().int({ message: options?.error });
if (options?.min !== undefined) {
schema = schema.min(options.min, {
message: options?.error || `must be at least ${options.min}`,
});
}
if (options?.max !== undefined) {
schema = schema.max(options.max, {
message: options?.error || `must be at most ${options.max}`,
});
}
return z.preprocess((val) => {
// If already a number, return as-is
if (typeof val === "number") {
return val;
}
// Parse string to integer
if (typeof val === "string") {
const parsed = Number.parseInt(val, 10);
return Number.isNaN(parsed) ? val : parsed;
}
return val;
}, schema);
}

View File

@@ -1,22 +1,13 @@
import { queryStringArray } from "@api/common/queryHelpers.js";
import { queryInteger, queryStringArray } from "@api/common/queryHelpers.js";
import { z } from "zod/v4";
/**
* Query params for GET /platform/users endpoint
*/
export const ListPlatformUsersQuerySchema = z.object({
limit: z
.number()
.int({ error: "limit must be an integer" })
.min(1, { error: "limit must be at least 1" })
.max(100, { error: "limit must be at most 100" })
.default(10),
limit: queryInteger({ min: 1, max: 100 }).default(10),
offset: z
.number({ error: "offset must be a number" })
.int({ error: "offset must be an integer" })
.min(0, { error: "offset must be at least 0" })
.default(0),
offset: queryInteger({ min: 0 }).default(0),
expand: queryStringArray(z.enum(["organizations"]))
.optional()
@@ -72,3 +63,28 @@ export const ListPlatformUsersResponseSchema = z.object({
export type ListPlatformUsersResponse = z.infer<
typeof ListPlatformUsersResponseSchema
>;
/**
* Query params for GET /platform/orgs endpoint
*/
export const ListPlatformOrgsQuerySchema = z.object({
limit: queryInteger({ min: 1, max: 100 }).default(10),
offset: queryInteger({ min: 0 }).default(0),
});
export type ListPlatformOrgsQuery = z.infer<typeof ListPlatformOrgsQuerySchema>;
/**
* Response schema for GET /platform/orgs
*/
export const ListPlatformOrgsResponseSchema = z.object({
list: z.array(ApiPlatformOrgSchema),
total: z.number().describe("Total number of organizations returned"),
limit: z.number().describe("Limit used in the query"),
offset: z.number().describe("Offset used in the query"),
});
export type ListPlatformOrgsResponse = z.infer<
typeof ListPlatformOrgsResponseSchema
>;

View File

@@ -1,6 +1,9 @@
import { ApiVersion, API_VERSIONS } from "./ApiVersion.js";
import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js";
import type { VersionMetadata } from "./versionRegistry.js";
import { getVersionMetadata, getVersionsSorted } from "./versionRegistryUtils.js";
import {
getVersionMetadata,
getVersionsSorted,
} from "./versionRegistryUtils.js";
/**
* ApiVersionClass - Encapsulates version comparison logic

View File

@@ -1,4 +1,4 @@
import { API_VERSIONS, ApiVersion } from "./ApiVersion.js";
import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js";
import { VERSION_REGISTRY, type VersionMetadata } from "./versionRegistry.js";
/**
@@ -24,7 +24,9 @@ export function getVersionMetadata({
return VERSION_REGISTRY[version];
}
export function isValidVersion(params: { version: string }): params is { version: ApiVersion } {
export function isValidVersion(params: {
version: string;
}): params is { version: ApiVersion } {
return API_VERSIONS.includes(params.version as ApiVersion);
}