270 lines
6.6 KiB
TypeScript
270 lines
6.6 KiB
TypeScript
import { FeatureService } from "@/internal/features/FeatureService.js";
|
|
import RecaseError, {
|
|
formatZodError,
|
|
handleRequestError,
|
|
} from "@/utils/errorUtils.js";
|
|
import {
|
|
AggregateType,
|
|
CreditSystemConfig,
|
|
Feature,
|
|
FeatureResponseSchema,
|
|
FeatureType,
|
|
MeteredConfig,
|
|
} from "@autumn/shared";
|
|
import { CreateFeatureSchema } from "@autumn/shared";
|
|
import express from "express";
|
|
import { generateId } from "@/utils/genUtils.js";
|
|
import { ErrCode } from "@/errors/errCodes.js";
|
|
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
|
|
|
|
export const featureApiRouter = express.Router();
|
|
|
|
const defaultMeteredConfig = (data: any) => {
|
|
let config = data.config;
|
|
config.aggregate = {
|
|
type: AggregateType.Sum,
|
|
property: "value",
|
|
};
|
|
return config;
|
|
};
|
|
|
|
const validateMeteredConfig = (config: MeteredConfig) => {
|
|
let newConfig = { ...config };
|
|
|
|
if (config.filters.length == 0 || config.filters[0].value.length == 0) {
|
|
throw new RecaseError({
|
|
message: `Event name is required for metered feature`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (config.aggregate?.type == AggregateType.Count) {
|
|
newConfig.aggregate = {
|
|
type: AggregateType.Count,
|
|
property: null,
|
|
}; // to continue testing support for count...
|
|
} else {
|
|
newConfig.aggregate = {
|
|
type: AggregateType.Sum,
|
|
property: "value",
|
|
};
|
|
}
|
|
|
|
if (!newConfig.group_by) {
|
|
newConfig.group_by = null;
|
|
}
|
|
|
|
return newConfig;
|
|
};
|
|
|
|
const validateCreditSystem = (config: CreditSystemConfig) => {
|
|
let schema = config.schema;
|
|
if (!schema || schema.length == 0) {
|
|
throw new RecaseError({
|
|
message: `At least one metered feature is required for credit system`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// Check if multiple of the same feature
|
|
const meteredFeatureIds = schema.map(
|
|
(schemaItem) => schemaItem.metered_feature_id
|
|
);
|
|
console.log("Metered feature ids:", meteredFeatureIds);
|
|
const uniqueMeteredFeatureIds = Array.from(new Set(meteredFeatureIds));
|
|
if (meteredFeatureIds.length !== uniqueMeteredFeatureIds.length) {
|
|
throw new RecaseError({
|
|
message: `Credit system contains multiple of the same metered_feature_id`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
let newConfig = { ...config };
|
|
for (let i = 0; i < newConfig.schema.length; i++) {
|
|
newConfig.schema[i].feature_amount = 1;
|
|
|
|
let creditAmount = parseFloat(newConfig.schema[i].credit_amount.toString());
|
|
if (isNaN(creditAmount)) {
|
|
throw new RecaseError({
|
|
message: `Credit amount should be a number`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
newConfig.schema[i].credit_amount = creditAmount;
|
|
}
|
|
|
|
return newConfig;
|
|
};
|
|
|
|
export const validateFeature = (data: any) => {
|
|
let featureType = data.type;
|
|
|
|
let config = data.config;
|
|
if (featureType == FeatureType.Metered) {
|
|
config = validateMeteredConfig(config);
|
|
} else if (featureType == FeatureType.CreditSystem) {
|
|
config = validateCreditSystem(config);
|
|
}
|
|
|
|
try {
|
|
const parsedFeature = CreateFeatureSchema.parse({ ...data, config });
|
|
return parsedFeature;
|
|
} catch (error: any) {
|
|
throw new RecaseError({
|
|
message: `Invalid feature: ${formatZodError(error)}`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const initNewFeature = ({
|
|
data,
|
|
orgId,
|
|
env,
|
|
}: {
|
|
data: any;
|
|
orgId: string;
|
|
env: any;
|
|
}) => {
|
|
return {
|
|
...data,
|
|
org_id: orgId,
|
|
env,
|
|
created_at: Date.now(),
|
|
internal_id: generateId("fe"),
|
|
};
|
|
};
|
|
|
|
featureApiRouter.get("", async (req: any, res) => {
|
|
let features = await FeatureService.getFromReq(req);
|
|
res
|
|
.status(200)
|
|
.json(features.map((feature) => FeatureResponseSchema.parse(feature)));
|
|
});
|
|
|
|
featureApiRouter.post("", async (req: any, res) => {
|
|
let data = req.body;
|
|
|
|
try {
|
|
let parsedFeature = validateFeature(data);
|
|
|
|
let feature: Feature = {
|
|
internal_id: generateId("fe"),
|
|
org_id: req.orgId,
|
|
created_at: Date.now(),
|
|
env: req.env,
|
|
...parsedFeature,
|
|
};
|
|
|
|
let insertedFeature = await FeatureService.insert({
|
|
sb: req.sb,
|
|
data: feature,
|
|
});
|
|
|
|
res.status(200).json(insertedFeature);
|
|
} catch (error) {
|
|
handleRequestError({ req, error, res, action: "Create feature" });
|
|
}
|
|
});
|
|
|
|
featureApiRouter.post("/:feature_id", async (req: any, res) => {
|
|
let featureId = req.params.feature_id;
|
|
|
|
let data = req.body;
|
|
|
|
try {
|
|
let feature = await FeatureService.getById({
|
|
sb: req.sb,
|
|
orgId: req.orgId,
|
|
env: req.env,
|
|
featureId,
|
|
});
|
|
|
|
if (!feature) {
|
|
throw new RecaseError({
|
|
message: `Feature ${featureId} not found`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
await FeatureService.updateStrict({
|
|
sb: req.sb,
|
|
orgId: req.orgId,
|
|
env: req.env,
|
|
featureId,
|
|
|
|
updates: {
|
|
name: data.name || undefined,
|
|
config:
|
|
feature.type == FeatureType.CreditSystem
|
|
? validateCreditSystem(data.config)
|
|
: feature.type == FeatureType.Metered
|
|
? validateMeteredConfig(data.config)
|
|
: data.config,
|
|
},
|
|
});
|
|
|
|
res.status(200).json({ success: true, feature_id: featureId });
|
|
} catch (error) {
|
|
handleRequestError({ req, error, res, action: "Update feature" });
|
|
}
|
|
});
|
|
|
|
featureApiRouter.delete("/:featureId", async (req: any, res) => {
|
|
let orgId = req.orgId;
|
|
let { featureId } = req.params;
|
|
|
|
try {
|
|
const { feature, creditSystems } =
|
|
await FeatureService.getWithCreditSystems({
|
|
sb: req.sb,
|
|
orgId,
|
|
featureId,
|
|
env: req.env,
|
|
});
|
|
|
|
if (creditSystems.length > 0) {
|
|
throw new RecaseError({
|
|
message: `Feature ${featureId} is used by credit system ${creditSystems[0].id}`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// Get prices that use this feature
|
|
const ents: any[] = await EntitlementService.getByFeature({
|
|
sb: req.sb,
|
|
orgId,
|
|
internalFeatureId: feature.internal_id,
|
|
env: req.env,
|
|
withProduct: true,
|
|
});
|
|
|
|
if (ents.length > 0) {
|
|
throw new RecaseError({
|
|
message: `Feature ${featureId} is used in ${ents[0].product.name}`,
|
|
code: ErrCode.InvalidFeature,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
await FeatureService.deleteStrict({
|
|
sb: req.sb,
|
|
orgId,
|
|
featureId,
|
|
env: req.env,
|
|
});
|
|
|
|
res.status(200).json({ message: "Feature deleted" });
|
|
} catch (error) {
|
|
handleRequestError({ req, error, res, action: "Delete feature" });
|
|
}
|
|
});
|