@actual-app/core 26.3.0 → 26.4.0-nightly.20260317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/build-browser CHANGED
File without changes
File without changes
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@actual-app/core",
3
- "version": "26.3.0",
3
+ "version": "26.4.0-nightly.20260317",
4
4
  "description": "",
5
5
  "license": "ISC",
6
+ "typesVersions": {
7
+ "*": {
8
+ "types/*": [
9
+ "./src/types/*"
10
+ ]
11
+ }
12
+ },
6
13
  "imports": {
7
14
  "#*": [
8
15
  "./src/*.ts",
@@ -67,7 +74,7 @@
67
74
  "test": "npm-run-all -cp 'test:*'",
68
75
  "test:node": "ENV=node vitest --run",
69
76
  "test:web": "ENV=web vitest --run -c vitest.web.config.ts",
70
- "typecheck": "tsc -b && tsc-strict"
77
+ "typecheck": "tsgo -b && tsc-strict"
71
78
  },
72
79
  "dependencies": {
73
80
  "@jlongster/sql.js": "^1.6.7",
@@ -76,7 +83,7 @@
76
83
  "@rschedule/json-tools": "^1.5.0",
77
84
  "@rschedule/standard-date-adapter": "^1.5.0",
78
85
  "absurd-sql": "0.0.54",
79
- "adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
86
+ "adm-zip": "^0.5.16",
80
87
  "better-sqlite3": "^12.6.2",
81
88
  "csv-parse": "^6.1.0",
82
89
  "csv-stringify": "^6.6.0",
@@ -88,13 +95,12 @@
88
95
  "mitt": "^3.0.1",
89
96
  "promise-retry": "^2.0.1",
90
97
  "slash": "5.1.0",
91
- "typescript": "^5.9.3",
92
98
  "typescript-strict-plugin": "^2.4.4",
93
99
  "ua-parser-js": "^2.0.9",
94
100
  "uuid": "^13.0.0"
95
101
  },
96
102
  "devDependencies": {
97
- "@actual-app/crdt": "workspace:*",
103
+ "@actual-app/crdt": "2.1.0",
98
104
  "@swc/core": "^1.15.11",
99
105
  "@types/adm-zip": "^0.5.7",
100
106
  "@types/better-sqlite3": "^7.6.13",
@@ -102,10 +108,12 @@
102
108
  "@types/jlongster__sql.js": "npm:@types/sql.js@latest",
103
109
  "@types/node": "^22.19.10",
104
110
  "@types/pegjs": "^0.10.6",
111
+ "@typescript/native-preview": "^7.0.0-dev.20260309.1",
105
112
  "assert": "^2.1.0",
106
113
  "browserify-zlib": "^0.2.0",
107
114
  "buffer": "^6.0.3",
108
115
  "cross-env": "^10.1.0",
116
+ "crypto-browserify": "^3.12.1",
109
117
  "fake-indexeddb": "^6.2.5",
110
118
  "fast-check": "4.5.3",
111
119
  "i18next": "^25.8.4",
@@ -117,12 +125,13 @@
117
125
  "peggy": "5.0.6",
118
126
  "rollup-plugin-visualizer": "^6.0.5",
119
127
  "stream-browserify": "^3.0.0",
128
+ "timers-browserify": "^2.0.12",
120
129
  "ts-node": "^10.9.2",
121
- "typescript": "^5.9.3",
122
- "vite": "^7.3.1",
130
+ "util": "^0.12.5",
131
+ "vite": "^8.0.0",
123
132
  "vite-plugin-node-polyfills": "^0.25.0",
124
133
  "vite-plugin-peggy-loader": "^2.0.1",
125
- "vitest": "^4.0.18",
134
+ "vitest": "^4.1.0",
126
135
  "yargs": "^18.0.0"
127
136
  }
128
- }
137
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  } from '../../types/models';
23
23
  import { aqlQuery } from '../aql';
24
24
  import * as db from '../db';
25
+ import { TRANSACTION_SORT_INCREMENT } from '../db/sort';
25
26
  import { runMutator } from '../mutators';
26
27
  import { post } from '../post';
27
28
  import { getServer } from '../server-config';
@@ -633,7 +634,7 @@ export async function reconcileTransactions(
633
634
  // Maintain the sort order of the server
634
635
  const now = Date.now();
635
636
  added.forEach((t, index) => {
636
- t.sort_order ??= now - index;
637
+ t.sort_order ??= now - index * TRANSACTION_SORT_INCREMENT;
637
638
  });
638
639
 
639
640
  if (!isPreview) {
@@ -897,6 +898,14 @@ export async function addTransactions(
897
898
 
898
899
  await createNewPayees(payeesToCreate, added);
899
900
 
901
+ // Assign decreasing sort_order values to preserve import file order.
902
+ // Transactions are displayed in sort_order DESC order, so first transaction
903
+ // in the file should have the highest sort_order.
904
+ const now = Date.now();
905
+ added.forEach((t, index) => {
906
+ t.sort_order ??= now - index * TRANSACTION_SORT_INCREMENT;
907
+ });
908
+
900
909
  let newTransactions;
901
910
  if (runTransfers || learnCategories) {
902
911
  const res = await batchUpdateTransactions({
@@ -33,7 +33,12 @@ import {
33
33
  } from '../models';
34
34
  import { batchMessages, sendMessages } from '../sync';
35
35
 
36
- import { shoveSortOrders, SORT_INCREMENT } from './sort';
36
+ import {
37
+ shoveSortOrders,
38
+ shoveSortOrdersDescending,
39
+ SORT_INCREMENT,
40
+ TRANSACTION_SORT_INCREMENT,
41
+ } from './sort';
37
42
  import type {
38
43
  DbAccount,
39
44
  DbBank,
@@ -810,6 +815,142 @@ export async function deleteTransaction(transaction) {
810
815
  return delete_('transactions', transaction.id);
811
816
  }
812
817
 
818
+ /**
819
+ * Move a transaction to a new position within the same date.
820
+ * Uses the same midpoint/shove algorithm as category reordering.
821
+ *
822
+ * @param id - The ID of the transaction to move
823
+ * @param accountId - The account the transaction belongs to
824
+ * @param targetId - The ID of the transaction to place AFTER, or null to place at top
825
+ */
826
+ export async function moveTransaction(
827
+ id: string,
828
+ accountId: string,
829
+ targetId: string | null,
830
+ ) {
831
+ await batchMessages(async () => {
832
+ const transaction = await getTransaction(id);
833
+ if (!transaction) {
834
+ throw new Error(`Transaction not found: ${id}`);
835
+ }
836
+
837
+ // Validate that the transaction belongs to the specified account
838
+ if (transaction.account !== accountId) {
839
+ throw new Error(
840
+ `Transaction ${id} does not belong to account ${accountId}`,
841
+ );
842
+ }
843
+
844
+ // Convert date string (YYYY-MM-DD) to integer format (YYYYMMDD) for SQL query
845
+ const dateInt = parseInt(transaction.date.replace(/-/g, ''), 10);
846
+
847
+ // Get transactions to reorder against.
848
+ // If this is a child transaction, scope to siblings with the same parent_id.
849
+ // Otherwise, get all parent transactions for the same date (excluding children).
850
+ // Query in DESC order to match UI display order.
851
+ const isChild = transaction.is_child && transaction.parent_id;
852
+ const transactions = await all<{ id: string; sort_order: number }>(
853
+ isChild
854
+ ? `SELECT vt.id, vt.sort_order
855
+ FROM v_transactions vt
856
+ WHERE vt.parent_id = ?
857
+ ORDER BY sort_order DESC, id`
858
+ : `SELECT vt.id, vt.sort_order
859
+ FROM v_transactions vt
860
+ WHERE vt.account = ?
861
+ AND vt.date = ?
862
+ AND vt.is_child = 0
863
+ ORDER BY sort_order DESC, id`,
864
+ isChild ? [transaction.parent_id] : [accountId, dateInt],
865
+ );
866
+
867
+ // Calculate new sort_order using the descending shove algorithm
868
+ // - If targetId is null, place at TOP (highest sort_order)
869
+ // - Otherwise, place AFTER target (sort_order between target and next)
870
+ const { sort_order: newSortOrder, updates } = shoveSortOrdersDescending(
871
+ transactions,
872
+ targetId,
873
+ id,
874
+ TRANSACTION_SORT_INCREMENT,
875
+ );
876
+
877
+ // Apply updates to shuffle other transactions if needed
878
+ for (const info of updates) {
879
+ await update('transactions', info);
880
+ // Only move subtransactions for parent transactions
881
+ if (!isChild) {
882
+ await moveSubtransactions(info.id, info.sort_order, accountId, dateInt);
883
+ }
884
+ }
885
+
886
+ // Update the moved transaction
887
+ await update('transactions', { id, sort_order: newSortOrder });
888
+ // Only move subtransactions for parent transactions
889
+ if (!isChild) {
890
+ await moveSubtransactions(id, newSortOrder, accountId, dateInt);
891
+ }
892
+ });
893
+ }
894
+
895
+ /**
896
+ * Update sort_order of child/subtransactions to maintain relative ordering.
897
+ * Children are distributed evenly between the parent's sort_order and the
898
+ * next sibling's sort_order to avoid collisions.
899
+ *
900
+ * This ensures split transaction children move with their parent.
901
+ *
902
+ * @param parentId - The ID of the parent transaction
903
+ * @param parentSortOrder - The sort_order of the parent transaction
904
+ * @param accountId - The account ID to scope the next sibling lookup
905
+ * @param txnDate - The transaction date (as integer YYYYMMDD) to scope the next sibling lookup
906
+ */
907
+ async function moveSubtransactions(
908
+ parentId: string,
909
+ parentSortOrder: number,
910
+ accountId: string,
911
+ txnDate: number,
912
+ ) {
913
+ const subtransactions = await all<{ id: string }>(
914
+ 'SELECT id FROM v_transactions WHERE parent_id = ? ORDER BY sort_order DESC',
915
+ [parentId],
916
+ );
917
+
918
+ if (subtransactions.length === 0) {
919
+ return;
920
+ }
921
+
922
+ // Find the next sibling's sort_order (transaction with lower sort_order that isn't a child)
923
+ // Scoped to the same account and date to avoid picking transactions from other contexts
924
+ const nextSibling = await first<{ sort_order: number }>(
925
+ `SELECT sort_order FROM v_transactions
926
+ WHERE parent_id IS NULL
927
+ AND is_child = 0
928
+ AND sort_order < ?
929
+ AND account = ?
930
+ AND date = ?
931
+ ORDER BY sort_order DESC
932
+ LIMIT 1`,
933
+ [parentSortOrder, accountId, txnDate],
934
+ );
935
+
936
+ // Calculate the available gap for distributing children
937
+ // Use a sensible fallback if no next sibling exists
938
+ const nextSiblingSortOrder =
939
+ nextSibling?.sort_order ?? parentSortOrder - TRANSACTION_SORT_INCREMENT;
940
+ const gap = parentSortOrder - nextSiblingSortOrder;
941
+
942
+ // Distribute children evenly within the gap
943
+ // Avoid rounding to prevent duplicate sort_order values in tight gaps
944
+ for (const [index, sub] of subtransactions.entries()) {
945
+ const newSortOrder =
946
+ parentSortOrder - ((index + 1) * gap) / (subtransactions.length + 1);
947
+ await update('transactions', {
948
+ id: sub.id,
949
+ sort_order: newSortOrder,
950
+ });
951
+ }
952
+ }
953
+
813
954
  function toSqlQueryParameters(params: unknown[]) {
814
955
  return params.map(() => '?').join(',');
815
956
  }
@@ -1,13 +1,21 @@
1
1
  export const SORT_INCREMENT = 16384;
2
2
 
3
- function midpoint<T extends { sort_order: number }>(items: T[], to: number) {
3
+ // Smaller increment for transactions - allows more reordering within a date
4
+ // before needing to resequence. Used by moveTransaction().
5
+ export const TRANSACTION_SORT_INCREMENT = 1024;
6
+
7
+ function midpoint<T extends { sort_order: number }>(
8
+ items: T[],
9
+ to: number,
10
+ sortIncrement: number = SORT_INCREMENT,
11
+ ) {
4
12
  const below = items[to - 1];
5
13
  const above = items[to];
6
14
 
7
15
  if (!below) {
8
16
  return above.sort_order / 2;
9
17
  } else if (!above) {
10
- return below.sort_order + SORT_INCREMENT;
18
+ return below.sort_order + sortIncrement;
11
19
  } else {
12
20
  return (below.sort_order + above.sort_order) / 2;
13
21
  }
@@ -16,6 +24,7 @@ function midpoint<T extends { sort_order: number }>(items: T[], to: number) {
16
24
  export function shoveSortOrders<T extends { id: string; sort_order: number }>(
17
25
  items: T[],
18
26
  targetId: string | null = null,
27
+ sortIncrement: number = SORT_INCREMENT,
19
28
  ) {
20
29
  const to = items.findIndex(item => item.id === targetId);
21
30
  const target = items[to];
@@ -27,17 +36,17 @@ export function shoveSortOrders<T extends { id: string; sort_order: number }>(
27
36
  let order;
28
37
  if (items.length > 0) {
29
38
  // Add a new increment to whatever is the latest sort order
30
- order = items[items.length - 1].sort_order + SORT_INCREMENT;
39
+ order = items[items.length - 1].sort_order + sortIncrement;
31
40
  } else {
32
41
  // If no items exist, the default is to use the first increment
33
- order = SORT_INCREMENT;
42
+ order = sortIncrement;
34
43
  }
35
44
 
36
45
  return { updates, sort_order: order };
37
46
  } else {
38
47
  if (target.sort_order - (before ? before.sort_order : 0) <= 2) {
39
48
  let next = to;
40
- let order = Math.floor(items[next].sort_order) + SORT_INCREMENT;
49
+ let order = Math.floor(items[next].sort_order) + sortIncrement;
41
50
  while (next < items.length) {
42
51
  // No need to update it if it's already greater than the current
43
52
  // order. This can happen because there may already be large
@@ -49,10 +58,132 @@ export function shoveSortOrders<T extends { id: string; sort_order: number }>(
49
58
  updates.push({ id: items[next].id, sort_order: order });
50
59
 
51
60
  next++;
52
- order += SORT_INCREMENT;
61
+ order += sortIncrement;
53
62
  }
54
63
  }
55
64
 
56
- return { updates, sort_order: midpoint(items, to) };
65
+ return { updates, sort_order: midpoint(items, to, sortIncrement) };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Midpoint calculation for descending-ordered lists.
71
+ * Places the new item between the target and the next item (lower sort_order).
72
+ *
73
+ * @param items - Array of items sorted by sort_order descending
74
+ * @param targetIdx - Index of the target item
75
+ * @param sortIncrement - The increment to use when no next item exists
76
+ * @param nextIdx - Optional index of the next item (to skip movingId)
77
+ */
78
+ function midpointDescending<T extends { sort_order: number }>(
79
+ items: T[],
80
+ targetIdx: number,
81
+ sortIncrement: number = SORT_INCREMENT,
82
+ nextIdx: number | null = null,
83
+ ): number {
84
+ const target = items[targetIdx];
85
+ const next = nextIdx === null ? items[targetIdx + 1] : items[nextIdx];
86
+
87
+ if (!next) {
88
+ // Target is at the bottom - place below it
89
+ return target.sort_order - sortIncrement;
90
+ } else {
91
+ // Use midpoint between target and next
92
+ return Math.round((target.sort_order + next.sort_order) / 2);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Calculate sort_order for placing an item after a target in a descending-ordered list.
98
+ * Mirrors shoveSortOrders but for lists sorted by sort_order DESC (higher values first).
99
+ *
100
+ * Use this when:
101
+ * - Items are displayed with highest sort_order at top
102
+ * - You want to place an item AFTER the target (visually below it)
103
+ * - Items need to be shoved DOWN (decreased sort_order) to make room
104
+ *
105
+ * @param items - Array of items sorted by sort_order descending
106
+ * @param targetId - ID of the item to place after, or null to place at top
107
+ * @param movingId - ID of the item being moved (skipped when shoving)
108
+ * @param sortIncrement - The increment to use when shoving items
109
+ * @returns Object with sort_order for the new position and updates array for shoved items
110
+ */
111
+ export function shoveSortOrdersDescending<
112
+ T extends { id: string; sort_order: number },
113
+ >(
114
+ items: T[],
115
+ targetId: string | null = null,
116
+ movingId: string | null = null,
117
+ sortIncrement: number = SORT_INCREMENT,
118
+ ): {
119
+ sort_order: number;
120
+ updates: Array<{ id: string; sort_order: number }>;
121
+ } {
122
+ const updates: Array<{ id: string; sort_order: number }> = [];
123
+
124
+ // If no target is specified, place at the top (highest sort_order)
125
+ if (!targetId) {
126
+ let order;
127
+ if (items.length > 0) {
128
+ order = items[0].sort_order + sortIncrement;
129
+ } else {
130
+ order = sortIncrement;
131
+ }
132
+ return { updates, sort_order: order };
133
+ }
134
+
135
+ const targetIdx = items.findIndex(item => item.id === targetId);
136
+
137
+ // Target not found, place at end (lowest sort_order)
138
+ if (targetIdx === -1) {
139
+ const order =
140
+ items.length > 0
141
+ ? items[items.length - 1].sort_order - sortIncrement
142
+ : sortIncrement;
143
+ return { updates, sort_order: order };
57
144
  }
145
+
146
+ const target = items[targetIdx];
147
+ // Find the next item, skipping the moving item to avoid computing gap against itself
148
+ const nextIdx = items.findIndex(
149
+ (item, idx) => idx > targetIdx && (!movingId || item.id !== movingId),
150
+ );
151
+ const next = nextIdx === -1 ? null : items[nextIdx];
152
+
153
+ // Check if there's room between target and next item
154
+ const gap = target.sort_order - (next ? next.sort_order : 0);
155
+ if (gap > 2) {
156
+ // There's room - use midpoint
157
+ // When nextIdx === -1, there's no next item (excluding the moving item),
158
+ // so place directly below target instead of calling midpointDescending
159
+ // which would incorrectly use items[targetIdx + 1] (the moving item itself)
160
+ const newSortOrder =
161
+ nextIdx === -1
162
+ ? target.sort_order - sortIncrement
163
+ : midpointDescending(items, targetIdx, sortIncrement, nextIdx);
164
+ return {
165
+ updates,
166
+ sort_order: newSortOrder,
167
+ };
168
+ }
169
+
170
+ // Need to shove items down (decrease sort_order) to make room
171
+ let newOrder = target.sort_order - sortIncrement;
172
+ for (let i = targetIdx + 1; i < items.length; i++) {
173
+ // Skip the item being moved
174
+ if (movingId && items[i].id === movingId) continue;
175
+
176
+ // Only update if we need to make room
177
+ if (items[i].sort_order >= newOrder) {
178
+ updates.push({ id: items[i].id, sort_order: newOrder });
179
+ newOrder = newOrder - sortIncrement;
180
+ } else {
181
+ break;
182
+ }
183
+ }
184
+
185
+ return {
186
+ updates,
187
+ sort_order: Math.round(target.sort_order - sortIncrement / 2),
188
+ };
58
189
  }
File without changes
@@ -8,6 +8,7 @@ import type {
8
8
  } from '../../types/models';
9
9
  import { createApp } from '../app';
10
10
  import { aqlQuery } from '../aql';
11
+ import * as db from '../db';
11
12
  import { mutator } from '../mutators';
12
13
  import { undoable } from '../undo';
13
14
 
@@ -23,6 +24,7 @@ export type TransactionHandlers = {
23
24
  'transaction-add': typeof addTransaction;
24
25
  'transaction-update': typeof updateTransaction;
25
26
  'transaction-delete': typeof deleteTransaction;
27
+ 'transaction-move': typeof moveTransaction;
26
28
  'transactions-parse-file': typeof parseTransactionsFile;
27
29
  'transactions-export': typeof exportTransactions;
28
30
  'transactions-export-query': typeof exportTransactionsQuery;
@@ -64,6 +66,36 @@ async function deleteTransaction(transaction: Pick<TransactionEntity, 'id'>) {
64
66
  return {};
65
67
  }
66
68
 
69
+ async function moveTransaction({
70
+ id,
71
+ accountId,
72
+ targetId,
73
+ }: {
74
+ id: string;
75
+ accountId: string;
76
+ targetId: string | null;
77
+ }) {
78
+ // Fetch the transaction to validate it exists and verify account
79
+ const transaction = await db.getTransaction(id);
80
+ if (!transaction) {
81
+ throw new Error(`Transaction not found: ${id}`);
82
+ }
83
+
84
+ // Validate that the provided accountId matches the transaction's actual account
85
+ // This prevents sort order calculations against the wrong account
86
+ if (transaction.account !== accountId) {
87
+ throw new Error(
88
+ `Account mismatch: transaction belongs to account ${transaction.account}, not ${accountId}`,
89
+ );
90
+ }
91
+
92
+ // Child transactions can be reordered within their parent's children
93
+ // The db.moveTransaction handles the sibling-scoped reordering for children
94
+
95
+ await db.moveTransaction(id, accountId, targetId);
96
+ return {};
97
+ }
98
+
67
99
  async function parseTransactionsFile({
68
100
  filepath,
69
101
  options,
@@ -129,6 +161,7 @@ app.method('transactions-merge', mutator(undoable(mergeTransactions)));
129
161
  app.method('transaction-add', mutator(addTransaction));
130
162
  app.method('transaction-update', mutator(updateTransaction));
131
163
  app.method('transaction-delete', mutator(deleteTransaction));
164
+ app.method('transaction-move', mutator(undoable(moveTransaction)));
132
165
  app.method('transactions-parse-file', mutator(parseTransactionsFile));
133
166
  app.method('transactions-export', mutator(exportTransactions));
134
167
  app.method('transactions-export-query', mutator(exportTransactionsQuery));
@@ -52,7 +52,7 @@ exports[`File import > CAMT.053 import works 1`] = `
52
52
  "raw_synced_data": null,
53
53
  "reconciled": 0,
54
54
  "schedule": null,
55
- "sort_order": 123456788,
55
+ "sort_order": 123455765,
56
56
  "starting_balance_flag": 0,
57
57
  "tombstone": 0,
58
58
  "transferred_id": null,
@@ -78,7 +78,7 @@ exports[`File import > CAMT.053 import works 1`] = `
78
78
  "raw_synced_data": null,
79
79
  "reconciled": 0,
80
80
  "schedule": null,
81
- "sort_order": 123456787,
81
+ "sort_order": 123454741,
82
82
  "starting_balance_flag": 0,
83
83
  "tombstone": 0,
84
84
  "transferred_id": null,
@@ -107,7 +107,7 @@ exports[`File import > CAMT.053 import works 1`] = `
107
107
  "raw_synced_data": null,
108
108
  "reconciled": 0,
109
109
  "schedule": null,
110
- "sort_order": 123456786,
110
+ "sort_order": 123453717,
111
111
  "starting_balance_flag": 0,
112
112
  "tombstone": 0,
113
113
  "transferred_id": null,
@@ -133,7 +133,7 @@ exports[`File import > CAMT.053 import works 1`] = `
133
133
  "raw_synced_data": null,
134
134
  "reconciled": 0,
135
135
  "schedule": null,
136
- "sort_order": 123456785,
136
+ "sort_order": 123452693,
137
137
  "starting_balance_flag": 0,
138
138
  "tombstone": 0,
139
139
  "transferred_id": null,
@@ -159,7 +159,7 @@ exports[`File import > CAMT.053 import works 1`] = `
159
159
  "raw_synced_data": null,
160
160
  "reconciled": 0,
161
161
  "schedule": null,
162
- "sort_order": 123456784,
162
+ "sort_order": 123451669,
163
163
  "starting_balance_flag": 0,
164
164
  "tombstone": 0,
165
165
  "transferred_id": null,
@@ -185,7 +185,7 @@ exports[`File import > CAMT.053 import works 1`] = `
185
185
  "raw_synced_data": null,
186
186
  "reconciled": 0,
187
187
  "schedule": null,
188
- "sort_order": 123456783,
188
+ "sort_order": 123450645,
189
189
  "starting_balance_flag": 0,
190
190
  "tombstone": 0,
191
191
  "transferred_id": null,
@@ -242,7 +242,7 @@ exports[`File import > handles html escaped plaintext 1`] = `
242
242
  "raw_synced_data": null,
243
243
  "reconciled": 0,
244
244
  "schedule": null,
245
- "sort_order": 123456788,
245
+ "sort_order": 123455765,
246
246
  "starting_balance_flag": 0,
247
247
  "tombstone": 0,
248
248
  "transferred_id": null,
@@ -330,7 +330,7 @@ exports[`File import > import notes are respected when importing > transactions
330
330
  "raw_synced_data": null,
331
331
  "reconciled": 0,
332
332
  "schedule": null,
333
- "sort_order": 123456788,
333
+ "sort_order": 123455765,
334
334
  "starting_balance_flag": 0,
335
335
  "tombstone": 0,
336
336
  "transferred_id": null,
@@ -356,7 +356,7 @@ exports[`File import > import notes are respected when importing > transactions
356
356
  "raw_synced_data": null,
357
357
  "reconciled": 0,
358
358
  "schedule": null,
359
- "sort_order": 123456787,
359
+ "sort_order": 123454741,
360
360
  "starting_balance_flag": 0,
361
361
  "tombstone": 0,
362
362
  "transferred_id": null,
@@ -382,7 +382,7 @@ exports[`File import > import notes are respected when importing > transactions
382
382
  "raw_synced_data": null,
383
383
  "reconciled": 0,
384
384
  "schedule": null,
385
- "sort_order": 123456786,
385
+ "sort_order": 123453717,
386
386
  "starting_balance_flag": 0,
387
387
  "tombstone": 0,
388
388
  "transferred_id": null,
@@ -408,7 +408,7 @@ exports[`File import > import notes are respected when importing > transactions
408
408
  "raw_synced_data": null,
409
409
  "reconciled": 0,
410
410
  "schedule": null,
411
- "sort_order": 123456785,
411
+ "sort_order": 123452693,
412
412
  "starting_balance_flag": 0,
413
413
  "tombstone": 0,
414
414
  "transferred_id": null,
@@ -434,7 +434,7 @@ exports[`File import > import notes are respected when importing > transactions
434
434
  "raw_synced_data": null,
435
435
  "reconciled": 0,
436
436
  "schedule": null,
437
- "sort_order": 123456784,
437
+ "sort_order": 123451669,
438
438
  "starting_balance_flag": 0,
439
439
  "tombstone": 0,
440
440
  "transferred_id": null,
@@ -460,7 +460,7 @@ exports[`File import > import notes are respected when importing > transactions
460
460
  "raw_synced_data": null,
461
461
  "reconciled": 0,
462
462
  "schedule": null,
463
- "sort_order": 123456783,
463
+ "sort_order": 123450645,
464
464
  "starting_balance_flag": 0,
465
465
  "tombstone": 0,
466
466
  "transferred_id": null,
@@ -486,7 +486,7 @@ exports[`File import > import notes are respected when importing > transactions
486
486
  "raw_synced_data": null,
487
487
  "reconciled": 0,
488
488
  "schedule": null,
489
- "sort_order": 123456782,
489
+ "sort_order": 123449621,
490
490
  "starting_balance_flag": 0,
491
491
  "tombstone": 0,
492
492
  "transferred_id": null,
@@ -512,7 +512,7 @@ exports[`File import > import notes are respected when importing > transactions
512
512
  "raw_synced_data": null,
513
513
  "reconciled": 0,
514
514
  "schedule": null,
515
- "sort_order": 123456781,
515
+ "sort_order": 123448597,
516
516
  "starting_balance_flag": 0,
517
517
  "tombstone": 0,
518
518
  "transferred_id": null,
@@ -538,7 +538,7 @@ exports[`File import > import notes are respected when importing > transactions
538
538
  "raw_synced_data": null,
539
539
  "reconciled": 0,
540
540
  "schedule": null,
541
- "sort_order": 123456780,
541
+ "sort_order": 123447573,
542
542
  "starting_balance_flag": 0,
543
543
  "tombstone": 0,
544
544
  "transferred_id": null,
@@ -626,7 +626,7 @@ exports[`File import > ofx import works 1`] = `
626
626
  "raw_synced_data": null,
627
627
  "reconciled": 0,
628
628
  "schedule": null,
629
- "sort_order": 123456788,
629
+ "sort_order": 123455765,
630
630
  "starting_balance_flag": 0,
631
631
  "tombstone": 0,
632
632
  "transferred_id": null,
@@ -652,7 +652,7 @@ exports[`File import > ofx import works 1`] = `
652
652
  "raw_synced_data": null,
653
653
  "reconciled": 0,
654
654
  "schedule": null,
655
- "sort_order": 123456787,
655
+ "sort_order": 123454741,
656
656
  "starting_balance_flag": 0,
657
657
  "tombstone": 0,
658
658
  "transferred_id": null,
@@ -678,7 +678,7 @@ exports[`File import > ofx import works 1`] = `
678
678
  "raw_synced_data": null,
679
679
  "reconciled": 0,
680
680
  "schedule": null,
681
- "sort_order": 123456786,
681
+ "sort_order": 123453717,
682
682
  "starting_balance_flag": 0,
683
683
  "tombstone": 0,
684
684
  "transferred_id": null,
@@ -704,7 +704,7 @@ exports[`File import > ofx import works 1`] = `
704
704
  "raw_synced_data": null,
705
705
  "reconciled": 0,
706
706
  "schedule": null,
707
- "sort_order": 123456785,
707
+ "sort_order": 123452693,
708
708
  "starting_balance_flag": 0,
709
709
  "tombstone": 0,
710
710
  "transferred_id": null,
@@ -730,7 +730,7 @@ exports[`File import > ofx import works 1`] = `
730
730
  "raw_synced_data": null,
731
731
  "reconciled": 0,
732
732
  "schedule": null,
733
- "sort_order": 123456784,
733
+ "sort_order": 123451669,
734
734
  "starting_balance_flag": 0,
735
735
  "tombstone": 0,
736
736
  "transferred_id": null,
@@ -756,7 +756,7 @@ exports[`File import > ofx import works 1`] = `
756
756
  "raw_synced_data": null,
757
757
  "reconciled": 0,
758
758
  "schedule": null,
759
- "sort_order": 123456783,
759
+ "sort_order": 123450645,
760
760
  "starting_balance_flag": 0,
761
761
  "tombstone": 0,
762
762
  "transferred_id": null,
@@ -782,7 +782,7 @@ exports[`File import > ofx import works 1`] = `
782
782
  "raw_synced_data": null,
783
783
  "reconciled": 0,
784
784
  "schedule": null,
785
- "sort_order": 123456782,
785
+ "sort_order": 123449621,
786
786
  "starting_balance_flag": 0,
787
787
  "tombstone": 0,
788
788
  "transferred_id": null,
@@ -808,7 +808,7 @@ exports[`File import > ofx import works 1`] = `
808
808
  "raw_synced_data": null,
809
809
  "reconciled": 0,
810
810
  "schedule": null,
811
- "sort_order": 123456781,
811
+ "sort_order": 123448597,
812
812
  "starting_balance_flag": 0,
813
813
  "tombstone": 0,
814
814
  "transferred_id": null,
@@ -834,7 +834,7 @@ exports[`File import > ofx import works 1`] = `
834
834
  "raw_synced_data": null,
835
835
  "reconciled": 0,
836
836
  "schedule": null,
837
- "sort_order": 123456780,
837
+ "sort_order": 123447573,
838
838
  "starting_balance_flag": 0,
839
839
  "tombstone": 0,
840
840
  "transferred_id": null,
@@ -891,7 +891,7 @@ exports[`File import > qfx import works 1`] = `
891
891
  "raw_synced_data": null,
892
892
  "reconciled": 0,
893
893
  "schedule": null,
894
- "sort_order": 123456788,
894
+ "sort_order": 123455765,
895
895
  "starting_balance_flag": 0,
896
896
  "tombstone": 0,
897
897
  "transferred_id": null,
@@ -917,7 +917,7 @@ exports[`File import > qfx import works 1`] = `
917
917
  "raw_synced_data": null,
918
918
  "reconciled": 0,
919
919
  "schedule": null,
920
- "sort_order": 123456787,
920
+ "sort_order": 123454741,
921
921
  "starting_balance_flag": 0,
922
922
  "tombstone": 0,
923
923
  "transferred_id": null,
@@ -943,7 +943,7 @@ exports[`File import > qfx import works 1`] = `
943
943
  "raw_synced_data": null,
944
944
  "reconciled": 0,
945
945
  "schedule": null,
946
- "sort_order": 123456786,
946
+ "sort_order": 123453717,
947
947
  "starting_balance_flag": 0,
948
948
  "tombstone": 0,
949
949
  "transferred_id": null,
@@ -969,7 +969,7 @@ exports[`File import > qfx import works 1`] = `
969
969
  "raw_synced_data": null,
970
970
  "reconciled": 0,
971
971
  "schedule": null,
972
- "sort_order": 123456785,
972
+ "sort_order": 123452693,
973
973
  "starting_balance_flag": 0,
974
974
  "tombstone": 0,
975
975
  "transferred_id": null,
@@ -995,7 +995,7 @@ exports[`File import > qfx import works 1`] = `
995
995
  "raw_synced_data": null,
996
996
  "reconciled": 0,
997
997
  "schedule": null,
998
- "sort_order": 123456784,
998
+ "sort_order": 123451669,
999
999
  "starting_balance_flag": 0,
1000
1000
  "tombstone": 0,
1001
1001
  "transferred_id": null,
@@ -1021,7 +1021,7 @@ exports[`File import > qfx import works 1`] = `
1021
1021
  "raw_synced_data": null,
1022
1022
  "reconciled": 0,
1023
1023
  "schedule": null,
1024
- "sort_order": 123456783,
1024
+ "sort_order": 123450645,
1025
1025
  "starting_balance_flag": 0,
1026
1026
  "tombstone": 0,
1027
1027
  "transferred_id": null,
@@ -1047,7 +1047,7 @@ exports[`File import > qfx import works 1`] = `
1047
1047
  "raw_synced_data": null,
1048
1048
  "reconciled": 0,
1049
1049
  "schedule": null,
1050
- "sort_order": 123456782,
1050
+ "sort_order": 123449621,
1051
1051
  "starting_balance_flag": 0,
1052
1052
  "tombstone": 0,
1053
1053
  "transferred_id": null,
@@ -1073,7 +1073,7 @@ exports[`File import > qfx import works 1`] = `
1073
1073
  "raw_synced_data": null,
1074
1074
  "reconciled": 0,
1075
1075
  "schedule": null,
1076
- "sort_order": 123456781,
1076
+ "sort_order": 123448597,
1077
1077
  "starting_balance_flag": 0,
1078
1078
  "tombstone": 0,
1079
1079
  "transferred_id": null,
@@ -1099,7 +1099,7 @@ exports[`File import > qfx import works 1`] = `
1099
1099
  "raw_synced_data": null,
1100
1100
  "reconciled": 0,
1101
1101
  "schedule": null,
1102
- "sort_order": 123456780,
1102
+ "sort_order": 123447573,
1103
1103
  "starting_balance_flag": 0,
1104
1104
  "tombstone": 0,
1105
1105
  "transferred_id": null,
@@ -1156,7 +1156,7 @@ exports[`File import > qif import works 1`] = `
1156
1156
  "raw_synced_data": null,
1157
1157
  "reconciled": 0,
1158
1158
  "schedule": null,
1159
- "sort_order": 123456788,
1159
+ "sort_order": 123455765,
1160
1160
  "starting_balance_flag": 0,
1161
1161
  "tombstone": 0,
1162
1162
  "transferred_id": null,
@@ -1182,7 +1182,7 @@ exports[`File import > qif import works 1`] = `
1182
1182
  "raw_synced_data": null,
1183
1183
  "reconciled": 0,
1184
1184
  "schedule": null,
1185
- "sort_order": 123456787,
1185
+ "sort_order": 123454741,
1186
1186
  "starting_balance_flag": 0,
1187
1187
  "tombstone": 0,
1188
1188
  "transferred_id": null,
@@ -1208,7 +1208,7 @@ exports[`File import > qif import works 1`] = `
1208
1208
  "raw_synced_data": null,
1209
1209
  "reconciled": 0,
1210
1210
  "schedule": null,
1211
- "sort_order": 123456786,
1211
+ "sort_order": 123453717,
1212
1212
  "starting_balance_flag": 0,
1213
1213
  "tombstone": 0,
1214
1214
  "transferred_id": null,
@@ -1234,7 +1234,7 @@ exports[`File import > qif import works 1`] = `
1234
1234
  "raw_synced_data": null,
1235
1235
  "reconciled": 0,
1236
1236
  "schedule": null,
1237
- "sort_order": 123456785,
1237
+ "sort_order": 123452693,
1238
1238
  "starting_balance_flag": 0,
1239
1239
  "tombstone": 0,
1240
1240
  "transferred_id": null,
@@ -1260,7 +1260,7 @@ exports[`File import > qif import works 1`] = `
1260
1260
  "raw_synced_data": null,
1261
1261
  "reconciled": 0,
1262
1262
  "schedule": null,
1263
- "sort_order": 123456784,
1263
+ "sort_order": 123451669,
1264
1264
  "starting_balance_flag": 0,
1265
1265
  "tombstone": 0,
1266
1266
  "transferred_id": null,
@@ -1286,7 +1286,7 @@ exports[`File import > qif import works 1`] = `
1286
1286
  "raw_synced_data": null,
1287
1287
  "reconciled": 0,
1288
1288
  "schedule": null,
1289
- "sort_order": 123456783,
1289
+ "sort_order": 123450645,
1290
1290
  "starting_balance_flag": 0,
1291
1291
  "tombstone": 0,
1292
1292
  "transferred_id": null,
@@ -1312,7 +1312,7 @@ exports[`File import > qif import works 1`] = `
1312
1312
  "raw_synced_data": null,
1313
1313
  "reconciled": 0,
1314
1314
  "schedule": null,
1315
- "sort_order": 123456782,
1315
+ "sort_order": 123449621,
1316
1316
  "starting_balance_flag": 0,
1317
1317
  "tombstone": 0,
1318
1318
  "transferred_id": null,
@@ -1338,7 +1338,7 @@ exports[`File import > qif import works 1`] = `
1338
1338
  "raw_synced_data": null,
1339
1339
  "reconciled": 0,
1340
1340
  "schedule": null,
1341
- "sort_order": 123456781,
1341
+ "sort_order": 123448597,
1342
1342
  "starting_balance_flag": 0,
1343
1343
  "tombstone": 0,
1344
1344
  "transferred_id": null,
@@ -1364,7 +1364,7 @@ exports[`File import > qif import works 1`] = `
1364
1364
  "raw_synced_data": null,
1365
1365
  "reconciled": 0,
1366
1366
  "schedule": null,
1367
- "sort_order": 123456780,
1367
+ "sort_order": 123447573,
1368
1368
  "starting_balance_flag": 0,
1369
1369
  "tombstone": 0,
1370
1370
  "transferred_id": null,
@@ -1390,7 +1390,7 @@ exports[`File import > qif import works 1`] = `
1390
1390
  "raw_synced_data": null,
1391
1391
  "reconciled": 0,
1392
1392
  "schedule": null,
1393
- "sort_order": 123456779,
1393
+ "sort_order": 123446549,
1394
1394
  "starting_balance_flag": 0,
1395
1395
  "tombstone": 0,
1396
1396
  "transferred_id": null,
@@ -1416,7 +1416,7 @@ exports[`File import > qif import works 1`] = `
1416
1416
  "raw_synced_data": null,
1417
1417
  "reconciled": 0,
1418
1418
  "schedule": null,
1419
- "sort_order": 123456778,
1419
+ "sort_order": 123445525,
1420
1420
  "starting_balance_flag": 0,
1421
1421
  "tombstone": 0,
1422
1422
  "transferred_id": null,
@@ -1442,7 +1442,7 @@ exports[`File import > qif import works 1`] = `
1442
1442
  "raw_synced_data": null,
1443
1443
  "reconciled": 0,
1444
1444
  "schedule": null,
1445
- "sort_order": 123456777,
1445
+ "sort_order": 123444501,
1446
1446
  "starting_balance_flag": 0,
1447
1447
  "tombstone": 0,
1448
1448
  "transferred_id": null,
@@ -1468,7 +1468,7 @@ exports[`File import > qif import works 1`] = `
1468
1468
  "raw_synced_data": null,
1469
1469
  "reconciled": 0,
1470
1470
  "schedule": null,
1471
- "sort_order": 123456776,
1471
+ "sort_order": 123443477,
1472
1472
  "starting_balance_flag": 0,
1473
1473
  "tombstone": 0,
1474
1474
  "transferred_id": null,
@@ -1494,7 +1494,7 @@ exports[`File import > qif import works 1`] = `
1494
1494
  "raw_synced_data": null,
1495
1495
  "reconciled": 0,
1496
1496
  "schedule": null,
1497
- "sort_order": 123456775,
1497
+ "sort_order": 123442453,
1498
1498
  "starting_balance_flag": 0,
1499
1499
  "tombstone": 0,
1500
1500
  "transferred_id": null,
@@ -1520,7 +1520,7 @@ exports[`File import > qif import works 1`] = `
1520
1520
  "raw_synced_data": null,
1521
1521
  "reconciled": 0,
1522
1522
  "schedule": null,
1523
- "sort_order": 123456774,
1523
+ "sort_order": 123441429,
1524
1524
  "starting_balance_flag": 0,
1525
1525
  "tombstone": 0,
1526
1526
  "transferred_id": null,
@@ -1546,7 +1546,7 @@ exports[`File import > qif import works 1`] = `
1546
1546
  "raw_synced_data": null,
1547
1547
  "reconciled": 0,
1548
1548
  "schedule": null,
1549
- "sort_order": 123456773,
1549
+ "sort_order": 123440405,
1550
1550
  "starting_balance_flag": 0,
1551
1551
  "tombstone": 0,
1552
1552
  "transferred_id": null,
@@ -1572,7 +1572,7 @@ exports[`File import > qif import works 1`] = `
1572
1572
  "raw_synced_data": null,
1573
1573
  "reconciled": 0,
1574
1574
  "schedule": null,
1575
- "sort_order": 123456772,
1575
+ "sort_order": 123439381,
1576
1576
  "starting_balance_flag": 0,
1577
1577
  "tombstone": 0,
1578
1578
  "transferred_id": null,
@@ -11,7 +11,10 @@ export function isNonProductionEnvironment() {
11
11
  }
12
12
 
13
13
  export function isElectron() {
14
- if (navigator.userAgent.indexOf('Electron') >= 0) {
14
+ if (
15
+ typeof navigator !== 'undefined' &&
16
+ navigator.userAgent.indexOf('Electron') >= 0
17
+ ) {
15
18
  return true;
16
19
  }
17
20
  return false;
@@ -0,0 +1,7 @@
1
+ export const isPlaywright = false;
2
+
3
+ export const OS: 'windows' | 'mac' | 'linux' | 'unknown' = 'unknown';
4
+ export const env: 'web' | 'mobile' | 'unknown' = 'unknown';
5
+ export const isBrowser: boolean = false;
6
+
7
+ export const isIOSAgent = false;
@@ -50,6 +50,8 @@ export function makeChild<T extends GenericTransactionEntity>(
50
50
  parent.starting_balance_flag != null
51
51
  ? parent.starting_balance_flag
52
52
  : null,
53
+ sort_order:
54
+ 'sort_order' in data ? data.sort_order : (parent.sort_order ?? null),
53
55
  is_child: true,
54
56
  parent_id: parent.id,
55
57
  error: null,
@@ -65,7 +67,7 @@ function makeNonChild<T extends GenericTransactionEntity>(
65
67
  ...data,
66
68
  cleared: parent.cleared != null ? parent.cleared : null,
67
69
  reconciled: parent.reconciled != null ? parent.reconciled : null,
68
- sort_order: parent.sort_order || null,
70
+ sort_order: parent.sort_order ?? null,
69
71
  starting_balance_flag: null,
70
72
  is_child: false,
71
73
  parent_id: null,
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "composite": true,
5
5
  "noEmit": false,
6
6
  "declaration": true,
7
+ "strict": false, // TODO: fix issues and remove this
7
8
  "declarationMap": true,
8
9
  "emitDeclarationOnly": true,
9
10
  "outDir": "lib-dist/decl",
package/vite.config.ts CHANGED
@@ -9,7 +9,6 @@ import peggyLoader from 'vite-plugin-peggy-loader';
9
9
  export default defineConfig(({ mode }) => {
10
10
  const isDev = mode === 'development';
11
11
  const outDir = path.resolve(__dirname, 'lib-dist/browser');
12
- const crdtDir = path.resolve(__dirname, '../crdt');
13
12
 
14
13
  return {
15
14
  mode,
@@ -25,7 +24,7 @@ export default defineConfig(({ mode }) => {
25
24
  fileName: () =>
26
25
  isDev ? 'kcab.worker.dev.js' : `kcab.worker.[hash].js`,
27
26
  },
28
- rollupOptions: {
27
+ rolldownOptions: {
29
28
  onwarn(warning, warn) {
30
29
  // Suppress sourcemap warnings from peggy-loader
31
30
  if (
@@ -62,12 +61,6 @@ export default defineConfig(({ mode }) => {
62
61
  },
63
62
  resolve: {
64
63
  extensions: ['.js', '.ts', '.tsx', '.json'],
65
- alias: [
66
- {
67
- find: /^@actual-app\/crdt(\/.*)?$/,
68
- replacement: path.resolve(crdtDir, 'src') + '$1',
69
- },
70
- ],
71
64
  },
72
65
  define: {
73
66
  'process.env': '{}',
@@ -83,6 +76,9 @@ export default defineConfig(({ mode }) => {
83
76
  'process',
84
77
  'stream',
85
78
  'path',
79
+ 'crypto',
80
+ 'timers',
81
+ 'util',
86
82
  'zlib',
87
83
  'fs',
88
84
  'assert',
@@ -100,8 +96,11 @@ export default defineConfig(({ mode }) => {
100
96
  'buffer',
101
97
  'process',
102
98
  'assert',
99
+ 'crypto-browserify',
103
100
  'path-browserify',
104
101
  'stream-browserify',
102
+ 'timers-browserify',
103
+ 'util',
105
104
  'browserify-zlib',
106
105
  ],
107
106
  },
@@ -6,7 +6,6 @@ import peggyLoader from 'vite-plugin-peggy-loader';
6
6
 
7
7
  export default defineConfig(({ mode }) => {
8
8
  const outDir = path.resolve(__dirname, 'lib-dist/electron');
9
- const crdtDir = path.resolve(__dirname, '../crdt');
10
9
 
11
10
  return {
12
11
  mode,
@@ -21,7 +20,7 @@ export default defineConfig(({ mode }) => {
21
20
  formats: ['cjs'],
22
21
  },
23
22
  sourcemap: true,
24
- rollupOptions: {
23
+ rolldownOptions: {
25
24
  output: {
26
25
  entryFileNames: 'bundle.desktop.js',
27
26
  format: 'cjs',
@@ -45,10 +44,6 @@ export default defineConfig(({ mode }) => {
45
44
  find: 'handlebars',
46
45
  replacement: require.resolve('handlebars/dist/handlebars.js'),
47
46
  },
48
- {
49
- find: /^@actual-app\/crdt(\/.*)?$/,
50
- replacement: path.resolve(crdtDir, 'src') + '$1',
51
- },
52
47
  ],
53
48
  },
54
49
  plugins: [
package/vitest.config.ts CHANGED
@@ -31,12 +31,6 @@ export default defineConfig({
31
31
  maxWorkers: 2,
32
32
  },
33
33
  resolve: {
34
- alias: [
35
- {
36
- find: /^@actual-app\/crdt(\/.*)?$/,
37
- replacement: path.resolve(path.join(__dirname, '../crdt/src$1')),
38
- },
39
- ],
40
34
  extensions: resolveExtensions,
41
35
  },
42
36
  plugins: [peggyLoader()],
@@ -26,12 +26,6 @@ export default defineConfig({
26
26
  maxWorkers: 2,
27
27
  },
28
28
  resolve: {
29
- alias: [
30
- {
31
- find: /^@actual-app\/crdt(\/.*)?$/,
32
- replacement: path.resolve('../../../crdt/src$1'),
33
- },
34
- ],
35
29
  extensions: resolveExtensions,
36
30
  },
37
31
  plugins: [peggyLoader()],