server/controllers/rules.ts
Rama main · réplica de lectura del repositorio original.
import type express from 'express';
import { assert, asyncErr, KError } from '../helpers';
import { TransactionRule, TransactionRuleAction, TransactionRuleCondition } from '../models';
import type { TransactionRuleConditionType } from '../shared/types';
import {
hasForbiddenField,
hasForbiddenOrMissingField,
hasMissingField,
} from '../shared/validators';
import type { IdentifiedRequest, PreloadedRequest } from './routes';
export const conditionTypesList: TransactionRuleConditionType[] = [
'label_matches_text',
'label_matches_regexp',
'amount_equals',
];
export async function preload(
req: IdentifiedRequest<TransactionRule>,
res: express.Response,
nextHandler: () => void,
id: number
) {
try {
const { id: userId } = req.user;
const rule = await TransactionRule.find(userId, id);
if (!rule) {
throw new KError('Rule not found', 404);
}
req.preloaded = { rule };
nextHandler();
} catch (err) {
asyncErr(res, err, 'when preloading a rule');
}
}
export async function preloadOther(
req: IdentifiedRequest<TransactionRule>,
res: express.Response,
nextHandler: () => void,
id: number
) {
try {
const { id: userId } = req.user;
const rule = await TransactionRule.find(userId, id);
if (!rule) {
throw new KError('Rule not found', 404);
}
req.preloaded = { ...req.preloaded, other: rule };
nextHandler();
} catch (err) {
asyncErr(res, err, 'when preloading a rule');
}
}
export async function all(req: IdentifiedRequest<any>, res: express.Response) {
try {
const userId = req.user.id;
const rules = await TransactionRule.allOrdered(userId);
res.status(200).json(rules);
} catch (err) {
asyncErr(res, err, 'when retrieving rules');
}
}
function checkDependencies(actions: any[], conditions: any[], allowIds: boolean) {
const extraFields = allowIds ? ['id'] : [];
let error: string | null;
for (const action of actions) {
if (action.type === 'categorize') {
error = hasForbiddenOrMissingField(action, ['type', 'categoryId', ...extraFields]);
if (error) {
throw new KError(error, 400);
}
if (typeof action.categoryId !== 'number') {
throw new KError('missing category id for a categorize rule', 400);
}
continue;
}
throw new KError('invalid or missing action type', 400);
}
for (const condition of conditions) {
// The extra fields like id are not mandatory (ex: a new condition is added on the fly on update).
error = hasMissingField(condition, ['type', 'value']);
if (error) {
throw new KError(error, 400);
}
error = hasForbiddenField(condition, ['type', 'value', ...extraFields]);
if (error) {
throw new KError(error, 400);
}
if (!conditionTypesList.includes(condition.type)) {
throw new KError('invalid condition type', 400);
}
}
}
export async function create(req: IdentifiedRequest<any>, res: express.Response) {
try {
const userId = req.user.id;
const error = hasForbiddenOrMissingField(req.body, ['actions', 'conditions']);
if (error) {
throw new KError(error, 400);
}
checkDependencies(req.body.actions, req.body.conditions, false);
if (req.body.conditions.length < 1 || req.body.actions.length < 1) {
throw new KError('rule must have at least one condition and one action', 400);
}
const maxPosition = await TransactionRule.maxPosition(userId);
const position = maxPosition !== null ? maxPosition + 1 : 0;
const rule = {
position,
conditions: req.body.conditions,
actions: req.body.actions,
};
const created = await TransactionRule.create(userId, rule);
res.status(200).json(created);
} catch (err) {
asyncErr(res, err, 'when creating a rule');
}
}
// Can only update actions/conditions that were already inserted into the
// database. Doesn't allow removing or adding new actions and conditions (yet).
export async function update(req: PreloadedRequest<TransactionRule>, res: express.Response) {
try {
const { id: userId } = req.user;
const { rule } = req.preloaded;
const newFields = req.body;
const error = hasForbiddenField(newFields, ['actions', 'conditions']);
if (error) {
throw new KError(error, 400);
}
checkDependencies(newFields.actions, newFields.conditions, true);
// Retrieve current conditions
const currentConditions = await TransactionRuleCondition.byRuleId(userId, rule.id);
// Update the existing ones, and create the others
for (const condition of newFields.conditions) {
const dbCondition = currentConditions.find(current => current.type === condition.type);
if (!dbCondition) {
// Create it
await TransactionRuleCondition.create(userId, {
...condition,
ruleId: rule.id,
});
} else {
if (dbCondition.ruleId !== rule.id) {
throw new KError("a condition isn't tied to the given rule", 400);
}
await TransactionRuleCondition.update(userId, dbCondition.id, condition);
}
}
// Delete those that don't exist anymore
for (const current of currentConditions) {
const exists = newFields.conditions.some(
(c: Partial<TransactionRuleCondition>) => c.type === current.type
);
if (!exists) {
await TransactionRuleCondition.destroy(userId, current.id);
}
}
for (const action of newFields.actions) {
const dbAction = await TransactionRuleAction.find(userId, action.id);
if (!dbAction || dbAction.ruleId !== rule.id) {
throw new KError("an action isn't tied to the given rule", 400);
}
await TransactionRuleAction.update(userId, action.id, action);
}
res.status(200).end();
} catch (err) {
asyncErr(res, err, 'when updating a rule');
}
}
export async function swapPositions(req: PreloadedRequest<TransactionRule>, res: express.Response) {
try {
const { id: userId } = req.user;
const { rule, other } = req.preloaded;
assert(other.position !== rule.position, "position can't be the same!");
await TransactionRule.update(userId, rule.id, {
position: other.position,
});
await TransactionRule.update(userId, other.id, {
position: rule.position,
});
res.status(200).end();
} catch (err) {
asyncErr(res, err, 'when deleting a rule');
}
}
export async function destroy(req: PreloadedRequest<TransactionRule>, res: express.Response) {
try {
const { id: userId } = req.user;
const { rule } = req.preloaded;
await TransactionRule.destroy(userId, rule.id);
res.status(200).end();
} catch (err) {
asyncErr(res, err, 'when deleting a rule');
}
}