From d9066a49c4dbcfbb507ff65696a279b06cadbeae Mon Sep 17 00:00:00 2001
From: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Date: Wed, 14 Aug 2024 13:28:09 -0400
Subject: [PATCH] Remove some `any` types from the API (#3238)

* Remove some `any` types from the API

* Add release notes

* No backwards-incompatible changes

* Update tests to reflect API changes

* PR feedback: standardize on id for deletes

* PR feedback: restore partial updates
---
 packages/api/methods.test.ts                  | 22 +++++++------------
 packages/api/methods.ts                       |  4 ++--
 .../server/accounts/transaction-rules.test.ts |  2 +-
 .../src/server/accounts/transaction-rules.ts  |  7 +++---
 packages/loot-core/src/server/api.ts          | 21 ++++++++++--------
 packages/loot-core/src/server/rules/app.ts    |  6 ++---
 .../src/server/rules/types/handlers.ts        | 13 ++++++-----
 .../loot-core/src/types/api-handlers.d.ts     | 12 ++++++----
 .../loot-core/src/types/server-handlers.d.ts  |  2 +-
 upcoming-release-notes/3238.md                |  6 +++++
 10 files changed, 53 insertions(+), 42 deletions(-)
 create mode 100644 upcoming-release-notes/3238.md

diff --git a/packages/api/methods.test.ts b/packages/api/methods.test.ts
index 8e0a9b586..53b39fa5d 100644
--- a/packages/api/methods.test.ts
+++ b/packages/api/methods.test.ts
@@ -81,28 +81,22 @@ describe('API CRUD operations', () => {
     expect(groups).toEqual(
       expect.arrayContaining([
         expect.objectContaining({
-          hidden: 0,
+          hidden: false,
           id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
-          is_income: 0,
+          is_income: false,
           name: 'Usual Expenses',
-          sort_order: 16384,
-          tombstone: 0,
         }),
         expect.objectContaining({
-          hidden: 0,
+          hidden: false,
           id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
-          is_income: 0,
+          is_income: false,
           name: 'Investments and Savings',
-          sort_order: 32768,
-          tombstone: 0,
         }),
         expect.objectContaining({
-          hidden: 0,
+          hidden: false,
           id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
-          is_income: 1,
+          is_income: true,
           name: 'Income',
-          sort_order: 32768,
-          tombstone: 0,
         }),
       ]),
     );
@@ -563,10 +557,10 @@ describe('API CRUD operations', () => {
     );
 
     // delete rules
-    await api.deleteRule(rules[1]);
+    await api.deleteRule(rules[1].id);
     expect(await api.getRules()).toHaveLength(1);
 
-    await api.deleteRule(rules[0]);
+    await api.deleteRule(rules[0].id);
     expect(await api.getRules()).toHaveLength(0);
   });
 
diff --git a/packages/api/methods.ts b/packages/api/methods.ts
index fc3455063..a6dec0e74 100644
--- a/packages/api/methods.ts
+++ b/packages/api/methods.ts
@@ -205,8 +205,8 @@ export function updateRule(rule) {
   return send('api/rule-update', { rule });
 }
 
-export function deleteRule(id) {
-  return send('api/rule-delete', { id });
+export function deleteRule(id: string) {
+  return send('api/rule-delete', id);
 }
 
 export function holdBudgetForNextMonth(month, amount) {
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.test.ts b/packages/loot-core/src/server/accounts/transaction-rules.test.ts
index 367da4b57..3ca6834d9 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.test.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.test.ts
@@ -209,7 +209,7 @@ describe('Transaction rules', () => {
     expect(transaction.payee).toBe('Kroger');
     expect(transaction.category).toBe('food');
 
-    await deleteRule({ id });
+    await deleteRule(id);
     expect(getRules().length).toBe(0);
     transaction = runRules({
       payee: 'Kroger',
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts
index be667ce65..44a8e223c 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.ts
@@ -223,16 +223,17 @@ export async function updateRule(rule) {
   return db.update('rules', ruleModel.fromJS(rule));
 }
 
-export async function deleteRule<T extends { id: string }>(rule: T) {
+export async function deleteRule(id: string) {
   const schedule = await db.first('SELECT id FROM schedules WHERE rule = ?', [
-    rule.id,
+    id,
   ]);
 
   if (schedule) {
     return false;
   }
 
-  return db.delete_('rules', rule.id);
+  await db.delete_('rules', id);
+  return true;
 }
 
 // Sync projections
diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts
index 6d49fb813..a2e9348fd 100644
--- a/packages/loot-core/src/server/api.ts
+++ b/packages/loot-core/src/server/api.ts
@@ -44,12 +44,14 @@ let IMPORT_MODE = false;
 // we also need to notify the UI manually if stuff has changed (if
 // they are connecting to an already running instance, the UI should
 // update). The wrapper handles that.
-function withMutation(handler) {
-  return args => {
+function withMutation<Params extends Array<unknown>, ReturnType>(
+  handler: (...args: Params) => Promise<ReturnType>,
+) {
+  return (...args: Params) => {
     return runMutator(
       async () => {
         const latestTimestamp = getClock().timestamp.toString();
-        const result = await handler(args);
+        const result = await handler(...args);
 
         const rows = await db.all(
           'SELECT DISTINCT dataset FROM messages_crdt WHERE timestamp > ?',
@@ -464,7 +466,7 @@ handlers['api/transactions-add'] = withMutation(async function ({
     runTransfers,
     learnCategories,
   });
-  return 'ok';
+  return 'ok' as const;
 });
 
 handlers['api/transactions-get'] = async function ({
@@ -503,7 +505,7 @@ handlers['api/transaction-update'] = withMutation(async function ({
   }
 
   const { diff } = updateTransaction(transactions, { id, ...fields });
-  return handlers['transactions-batch-update'](diff);
+  return handlers['transactions-batch-update'](diff)['updated'];
 });
 
 handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
@@ -518,7 +520,7 @@ handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
   }
 
   const { diff } = deleteTransaction(transactions, id);
-  return handlers['transactions-batch-update'](diff);
+  return handlers['transactions-batch-update'](diff)['deleted'];
 });
 
 handlers['api/accounts-get'] = async function () {
@@ -590,7 +592,8 @@ handlers['api/categories-get'] = async function ({
 
 handlers['api/category-groups-get'] = async function () {
   checkFileOpen();
-  return handlers['get-category-groups']();
+  const groups = await handlers['get-category-groups']();
+  return groups.map(categoryGroupModel.toExternal);
 };
 
 handlers['api/category-group-create'] = withMutation(async function ({
@@ -711,7 +714,7 @@ handlers['api/rule-create'] = withMutation(async function ({ rule }) {
 
 handlers['api/rule-update'] = withMutation(async function ({ rule }) {
   checkFileOpen();
-  const updatedRule = handlers['rule-update'](rule);
+  const updatedRule = await handlers['rule-update'](rule);
 
   if ('error' in updatedRule) {
     throw APIError('Failed updating the rule', updatedRule.error);
@@ -720,7 +723,7 @@ handlers['api/rule-update'] = withMutation(async function ({ rule }) {
   return updatedRule;
 });
 
-handlers['api/rule-delete'] = withMutation(async function ({ id }) {
+handlers['api/rule-delete'] = withMutation(async function (id) {
   checkFileOpen();
   return handlers['rule-delete'](id);
 });
diff --git a/packages/loot-core/src/server/rules/app.ts b/packages/loot-core/src/server/rules/app.ts
index 01d37e1b7..df9e272bd 100644
--- a/packages/loot-core/src/server/rules/app.ts
+++ b/packages/loot-core/src/server/rules/app.ts
@@ -111,8 +111,8 @@ app.method(
 
 app.method(
   'rule-delete',
-  mutator(async function (rule) {
-    return rules.deleteRule(rule);
+  mutator(async function (id) {
+    return rules.deleteRule(id);
   }),
 );
 
@@ -123,7 +123,7 @@ app.method(
 
     await batchMessages(async () => {
       for (const id of ids) {
-        const res = await rules.deleteRule({ id });
+        const res = await rules.deleteRule(id);
         if (res === false) {
           someDeletionsFailed = true;
         }
diff --git a/packages/loot-core/src/server/rules/types/handlers.ts b/packages/loot-core/src/server/rules/types/handlers.ts
index 429acddc9..7884f6e4c 100644
--- a/packages/loot-core/src/server/rules/types/handlers.ts
+++ b/packages/loot-core/src/server/rules/types/handlers.ts
@@ -18,13 +18,16 @@ export interface RulesHandlers {
 
   'rule-add': (
     rule: Omit<RuleEntity, 'id'>,
-  ) => Promise<{ error: ValidationError } | { id: string }>;
+  ) => Promise<{ error: ValidationError } | RuleEntity>;
 
-  'rule-update': (
-    rule: Partial<RuleEntity>,
-  ) => Promise<{ error: ValidationError } | object>;
+  'rule-update': <
+    PartialRule extends Partial<Omit<RuleEntity, 'id'>> &
+      Pick<RuleEntity, 'id'>,
+  >(
+    rule: PartialRule,
+  ) => Promise<{ error: ValidationError } | PartialRule>;
 
-  'rule-delete': (rule: Required<RuleEntity>) => Promise<false | void>;
+  'rule-delete': (id: string) => Promise<boolean>;
 
   'rule-delete-all': (
     ids: string[],
diff --git a/packages/loot-core/src/types/api-handlers.d.ts b/packages/loot-core/src/types/api-handlers.d.ts
index cf26bac83..eb294292d 100644
--- a/packages/loot-core/src/types/api-handlers.d.ts
+++ b/packages/loot-core/src/types/api-handlers.d.ts
@@ -66,7 +66,7 @@ export interface ApiHandlers {
   'api/budget-hold-for-next-month': (arg: {
     month: string;
     amount: number;
-  }) => Promise<void>;
+  }) => Promise<boolean>;
 
   'api/budget-reset-hold': (arg: { month: string }) => Promise<void>;
 
@@ -76,7 +76,11 @@ export interface ApiHandlers {
     payees;
   }) => Promise<unknown>;
 
-  'api/transactions-import': (arg: { accountId; transactions }) => Promise<{
+  'api/transactions-import': (arg: {
+    accountId;
+    transactions;
+    isPreview?;
+  }) => Promise<{
     errors?: { message: string }[];
     added;
     updated;
@@ -102,7 +106,7 @@ export interface ApiHandlers {
 
   'api/transaction-delete': (arg: {
     id;
-  }) => Promise<Awaited<ReturnType<typeof batchUpdateTransactions>>['updated']>;
+  }) => Promise<Awaited<ReturnType<typeof batchUpdateTransactions>>['deleted']>;
 
   'api/sync': () => Promise<void>;
 
@@ -176,5 +180,5 @@ export interface ApiHandlers {
 
   'api/rule-update': (arg: { rule: RuleEntity }) => Promise<RuleEntity>;
 
-  'api/rule-delete': (arg: { id: string }) => Promise<boolean>;
+  'api/rule-delete': (id: string) => Promise<boolean>;
 }
diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts
index 300484f7d..f455b2bbb 100644
--- a/packages/loot-core/src/types/server-handlers.d.ts
+++ b/packages/loot-core/src/types/server-handlers.d.ts
@@ -108,7 +108,7 @@ export interface ServerHandlers {
 
   'payees-get-rule-counts': () => Promise<unknown>;
 
-  'payees-merge': (arg: { targetId; mergeIds }) => Promise<unknown>;
+  'payees-merge': (arg: { targetId; mergeIds }) => Promise<void>;
 
   'payees-batch-change': (arg: {
     added?;
diff --git a/upcoming-release-notes/3238.md b/upcoming-release-notes/3238.md
new file mode 100644
index 000000000..9ffe939d8
--- /dev/null
+++ b/upcoming-release-notes/3238.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [jfdoming]
+---
+
+Remove some `any` types from the API
-- 
GitLab