Skip to content

Commit

Permalink
♻️ moving rule server actions to separate file (actualbudget#1677)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatissJanis authored Sep 22, 2023
1 parent 88b4911 commit b665b52
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 189 deletions.
60 changes: 38 additions & 22 deletions packages/desktop-client/src/components/rules/ActionExpression.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react';

import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
import { type ScheduleEntity } from 'loot-core/src/types/models';
import {
type LinkScheduleRuleActionEntity,
type RuleActionEntity,
type SetRuleActionEntity,
} from 'loot-core/src/types/models';

import { type CSSProperties, theme } from '../../style';
import Text from '../common/Text';
Expand All @@ -14,20 +18,13 @@ let valueStyle = {
color: theme.pageTextPositive,
};

type ActionExpressionProps = {
field: unknown;
op: unknown;
value: unknown;
options: unknown;
type ActionExpressionProps = RuleActionEntity & {
style?: CSSProperties;
};

export default function ActionExpression({
field,
op,
value,
options,
style,
...props
}: ActionExpressionProps) {
return (
<View
Expand All @@ -44,19 +41,38 @@ export default function ActionExpression({
...style,
}}
>
{op === 'set' ? (
<>
<Text>{friendlyOp(op)}</Text>{' '}
<Text style={valueStyle}>{mapField(field, options)}</Text>{' '}
<Text>to </Text>
<Value value={value} field={field} />
</>
) : op === 'link-schedule' ? (
<>
<Text>{friendlyOp(op)}</Text>{' '}
<ScheduleValue value={value as ScheduleEntity} />
</>
{props.op === 'set' ? (
<SetActionExpression {...props} />
) : props.op === 'link-schedule' ? (
<LinkScheduleActionExpression {...props} />
) : null}
</View>
);
}

function SetActionExpression({
op,
field,
value,
options,
}: SetRuleActionEntity) {
return (
<>
<Text>{friendlyOp(op)}</Text>{' '}
<Text style={valueStyle}>{mapField(field, options)}</Text>{' '}
<Text>to </Text>
<Value value={value} field={field} />
</>
);
}

function LinkScheduleActionExpression({
op,
value,
}: LinkScheduleRuleActionEntity) {
return (
<>
<Text>{friendlyOp(op)}</Text> <ScheduleValue value={value} />
</>
);
}
5 changes: 1 addition & 4 deletions packages/desktop-client/src/components/rules/RuleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ const RuleRow = memo(
{rule.actions.map((action, i) => (
<ActionExpression
key={i}
field={action.field}
op={action.op}
value={action.value}
options={action.options}
{...action}
style={i !== 0 && { marginTop: 3 }}
/>
))}
Expand Down
21 changes: 16 additions & 5 deletions packages/loot-core/src/server/accounts/transaction-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getApproxNumberThreshold,
} from '../../shared/rules';
import { partitionByField, fastSetMerge } from '../../shared/util';
import { type RuleActionEntity, type RuleEntity } from '../../types/models';
import { schemaConfig } from '../aql';
import * as db from '../db';
import { getMappings } from '../db/mappings';
Expand All @@ -27,6 +28,7 @@ import {
migrateIds,
iterateIds,
} from './rules';
import { batchUpdateTransactions } from './transactions';

// TODO: Detect if it looks like the user is creating a rename rule
// and prompt to create it in the pre phase instead
Expand Down Expand Up @@ -202,7 +204,9 @@ export function getRules() {
return [...allRules.values()];
}

export async function insertRule(rule) {
export async function insertRule(
rule: Omit<RuleEntity, 'id'> & { id?: string },
) {
rule = ruleModel.validate(rule);
return db.insertWithUUID('rules', ruleModel.fromJS(rule));
}
Expand All @@ -212,7 +216,7 @@ export async function updateRule(rule) {
return db.update('rules', ruleModel.fromJS(rule));
}

export async function deleteRule(rule) {
export async function deleteRule<T extends { id: string }>(rule: T) {
let schedule = await db.first('SELECT id FROM schedules WHERE rule = ?', [
rule.id,
]);
Expand Down Expand Up @@ -477,14 +481,21 @@ export function conditionsToAQL(conditions, { recurDateBounds = 100 } = {}) {
return { filters, errors };
}

export function applyActions(transactionIds, actions, handlers) {
export function applyActions(
transactionIds: string[],
actions: Array<Action | RuleActionEntity>,
) {
let parsedActions = actions
.map(action => {
if (action instanceof Action) {
return action;
}

try {
if (action.op === 'link-schedule') {
return new Action(action.op, null, action.value, null, FIELD_TYPES);
}

return new Action(
action.op,
action.field,
Expand Down Expand Up @@ -512,7 +523,7 @@ export function applyActions(transactionIds, actions, handlers) {
return update;
});

return handlers['transactions-batch-update']({ updated });
return batchUpdateTransactions({ updated });
}

export function getRulesForPayee(payeeId) {
Expand Down Expand Up @@ -583,7 +594,7 @@ function* getOneOfSetterRules(
return null;
}

export async function updatePayeeRenameRule(fromNames, to) {
export async function updatePayeeRenameRule(fromNames: string[], to: string) {
let renameRule = getOneOfSetterRules('pre', 'imported_payee', 'payee', {
actionValue: to,
}).next().value;
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/server/accounts/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function batchUpdateTransactions({
}>;
learnCategories?: boolean;
detectOrphanPayees?: boolean;
}): Promise<{ added: TransactionEntity[]; updated: unknown[] }> {
}) {
// Track the ids of each type of transaction change (see below for why)
let addedIds = [];
let updatedIds = updated ? updated.map(u => u.id) : [];
Expand Down Expand Up @@ -126,7 +126,7 @@ export async function batchUpdateTransactions({
// to the client so that can apply them. Note that added
// transactions just return the full transaction.
let resultAdded = allAdded;
let resultUpdated: unknown[];
let resultUpdated: Awaited<ReturnType<typeof transfer.onUpdate>>[];

await batchMessages(async () => {
await Promise.all(allAdded.map(t => transfer.onInsert(t)));
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/server/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export class SyncError extends Error {
export class TransactionError extends Error {}

export class RuleError extends Error {
type;
type: string;

constructor(name, message) {
constructor(name: string, message: string) {
super('RuleError: ' + message);
this.type = name;
}
Expand Down
129 changes: 3 additions & 126 deletions packages/loot-core/src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import * as sqlite from '../platform/server/sqlite';
import { isNonProductionEnvironment } from '../shared/environment';
import * as monthUtils from '../shared/months';
import q, { Query } from '../shared/query';
import { FIELD_TYPES as ruleFieldTypes } from '../shared/rules';
import { amountToInteger, stringToInteger } from '../shared/util';
import { Handlers } from '../types/handlers';

import { exportToCSV, exportQueryToCSV } from './accounts/export-to-csv';
import * as link from './accounts/link';
import { parseFile } from './accounts/parse-file';
import { getStartingBalancePayee } from './accounts/payees';
import { Condition, Action, rankRules } from './accounts/rules';
import * as bankSync from './accounts/sync';
import * as rules from './accounts/transaction-rules';
import { batchUpdateTransactions } from './accounts/transactions';
Expand All @@ -40,7 +38,7 @@ import * as cloudStorage from './cloud-storage';
import * as db from './db';
import * as mappings from './db/mappings';
import * as encryption from './encryption';
import { APIError, TransactionError, PostError, RuleError } from './errors';
import { APIError, TransactionError, PostError } from './errors';
import filtersApp from './filters/app';
import { handleBudgetImport } from './importers';
import app from './main-app';
Expand All @@ -49,6 +47,7 @@ import notesApp from './notes/app';
import * as Platform from './platform';
import { get, post } from './post';
import * as prefs from './prefs';
import rulesApp from './rules/app';
import schedulesApp from './schedules/app';
import { getServer, setServer } from './server-config';
import * as sheet from './sheet';
Expand Down Expand Up @@ -499,128 +498,6 @@ handlers['payees-get-rules'] = async function ({ id }) {
return rules.getRulesForPayee(id).map(rule => rule.serialize());
};

function validateRule(rule) {
// Returns an array of errors, the array is the same link as the
// passed-in `array`, or null if there are no errors
function runValidation(array, validate) {
let result = array.map(item => {
try {
validate(item);
} catch (e) {
if (e instanceof RuleError) {
console.warn('Invalid rule', e);
return e.type;
}
throw e;
}
return null;
});

return result.some(Boolean) ? result : null;
}

let conditionErrors = runValidation(
rule.conditions,
cond =>
new Condition(
cond.op,
cond.field,
cond.value,
cond.options,
ruleFieldTypes,
),
);

let actionErrors = runValidation(
rule.actions,
action =>
new Action(
action.op,
action.field,
action.value,
action.options,
ruleFieldTypes,
),
);

if (conditionErrors || actionErrors) {
return {
conditionErrors,
actionErrors,
};
}

return null;
}

handlers['rule-validate'] = async function (rule) {
let error = validateRule(rule);
return { error };
};

handlers['rule-add'] = mutator(async function (rule) {
let error = validateRule(rule);
if (error) {
return { error };
}

let id = await rules.insertRule(rule);
return { id };
});

handlers['rule-update'] = mutator(async function (rule) {
let error = validateRule(rule);
if (error) {
return { error };
}

await rules.updateRule(rule);
return {};
});

handlers['rule-delete'] = mutator(async function (rule) {
return rules.deleteRule(rule);
});

handlers['rule-delete-all'] = mutator(async function (ids) {
let someDeletionsFailed = false;

await batchMessages(async () => {
for (let id of ids) {
let res = await rules.deleteRule({ id });
if (res === false) {
someDeletionsFailed = true;
}
}
});

return { someDeletionsFailed };
});

handlers['rule-apply-actions'] = mutator(async function ({
transactionIds,
actions,
}) {
return rules.applyActions(transactionIds, actions, handlers);
});

handlers['rule-add-payee-rename'] = mutator(async function ({ fromNames, to }) {
return rules.updatePayeeRenameRule(fromNames, to);
});

handlers['rules-get'] = async function () {
return rankRules(rules.getRules()).map(rule => rule.serialize());
};

handlers['rule-get'] = async function ({ id }) {
let rule = rules.getRules().find(rule => rule.id === id);
return rule ? rule.serialize() : null;
};

handlers['rules-run'] = async function ({ transaction }) {
return rules.runRules(transaction);
};

handlers['make-filters-from-conditions'] = async function ({ conditions }) {
return rules.conditionsToAQL(conditions);
};
Expand Down Expand Up @@ -2252,7 +2129,7 @@ injectAPI.override((name, args) => runHandler(app.handlers[name], args));

// A hack for now until we clean up everything
app.handlers = handlers;
app.combine(schedulesApp, budgetApp, notesApp, toolsApp, filtersApp);
app.combine(schedulesApp, budgetApp, notesApp, toolsApp, filtersApp, rulesApp);

function getDefaultDocumentDir() {
if (Platform.isMobile) {
Expand Down
Loading

0 comments on commit b665b52

Please sign in to comment.