@mulmoclaude/accounting-plugin 2.1.0 → 3.0.0
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/README.md +37 -0
- package/dist/server/context.d.ts +45 -3
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/eventPublisher.d.ts +9 -4
- package/dist/server/eventPublisher.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/router.d.ts +27 -3
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/snapshotCache.d.ts +3 -3
- package/dist/server/snapshotCache.d.ts.map +1 -1
- package/dist/server.cjs +149 -80
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +149 -80
- package/dist/server.js.map +1 -1
- package/dist/shared/api.d.ts +12 -0
- package/dist/shared/api.d.ts.map +1 -1
- package/dist/shared/channels.d.ts +22 -2
- package/dist/shared/channels.d.ts.map +1 -1
- package/dist/{shared-BZAAF-I4.js → shared-BccLo0Ux.js} +39 -5
- package/dist/shared-BccLo0Ux.js.map +1 -0
- package/dist/{shared-BpFVwa6Y.cjs → shared-Dgt-i23T.cjs} +50 -4
- package/dist/shared-Dgt-i23T.cjs.map +1 -0
- package/dist/shared.cjs +3 -1
- package/dist/shared.js +2 -2
- package/dist/style.css +19 -10
- package/dist/vue/View.vue.d.ts +5 -0
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/api.d.ts +132 -100
- package/dist/vue/api.d.ts.map +1 -1
- package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -1
- package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -1
- package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
- package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
- package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -1
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
- package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
- package/dist/vue/hostContext.d.ts +15 -0
- package/dist/vue/hostContext.d.ts.map +1 -1
- package/dist/vue/useAccountingChannel.d.ts +13 -3
- package/dist/vue/useAccountingChannel.d.ts.map +1 -1
- package/dist/vue.cjs +227 -98
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +228 -99
- package/dist/vue.js.map +1 -1
- package/package.json +4 -3
- package/dist/shared-BZAAF-I4.js.map +0 -1
- package/dist/shared-BpFVwa6Y.cjs.map +0 -1
package/dist/server.cjs
CHANGED
|
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
const require_shared = require("./shared-
|
|
24
|
+
const require_shared = require("./shared-Dgt-i23T.cjs");
|
|
25
25
|
let express = require("express");
|
|
26
26
|
let node_crypto = require("node:crypto");
|
|
27
27
|
let node_fs = require("node:fs");
|
|
@@ -84,11 +84,41 @@ function configureAccountingServer(context) {
|
|
|
84
84
|
}
|
|
85
85
|
/** Default workspace root for io calls that don't pass one explicitly.
|
|
86
86
|
* Throws if the host never configured the server — a real wiring bug
|
|
87
|
-
* (unit tests always pass an explicit root, so they never hit this)
|
|
87
|
+
* (unit tests always pass an explicit root, so they never hit this) —
|
|
88
|
+
* and also under STRICT mode (`workspaceRoot: null`), where there is no
|
|
89
|
+
* ambient root to fall back to and a missing option is a bug rather
|
|
90
|
+
* than a default. */
|
|
88
91
|
function defaultWorkspaceRoot() {
|
|
89
92
|
if (!deps) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests");
|
|
93
|
+
if (deps.workspaceRoot === null) throw new Error("@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.");
|
|
90
94
|
return deps.workspaceRoot;
|
|
91
95
|
}
|
|
96
|
+
/** The opaque channel scope for a root, or `null` when the host declared
|
|
97
|
+
* none (single-root hosts, and a multi-root host's default root). Used
|
|
98
|
+
* by the event publisher; never contains a path.
|
|
99
|
+
*
|
|
100
|
+
* A missing root throws under explicit-root mode, exactly as
|
|
101
|
+
* `defaultWorkspaceRoot()` does. Returning `null` there would be worse
|
|
102
|
+
* than the io-path bug it mirrors: the event would go out on the
|
|
103
|
+
* UNSCOPED channel name, which every project's default-scope
|
|
104
|
+
* subscriber receives. The publisher catches it and logs, so a dropped
|
|
105
|
+
* root costs one event and a warning rather than a silent cross-project
|
|
106
|
+
* notification. */
|
|
107
|
+
function channelScopeFor(workspaceRoot) {
|
|
108
|
+
if (!deps?.channelScopeForRoot) return null;
|
|
109
|
+
return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());
|
|
110
|
+
}
|
|
111
|
+
/** Whether the host declared project scoping at all.
|
|
112
|
+
*
|
|
113
|
+
* The difference matters where a card records the project it belongs
|
|
114
|
+
* to: on a scoped host, "no scope" is a REAL answer (the default root)
|
|
115
|
+
* and must be recorded as `null`, not left out — an omitted field means
|
|
116
|
+
* "ask the host what is active now", which is precisely the drift a
|
|
117
|
+
* pinned card exists to prevent. On an unscoped host the field is left
|
|
118
|
+
* out entirely and every payload stays byte-identical to today's. */
|
|
119
|
+
function isProjectScopedHost() {
|
|
120
|
+
return Boolean(deps?.channelScopeForRoot);
|
|
121
|
+
}
|
|
92
122
|
var consoleLogger = {
|
|
93
123
|
error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? ""),
|
|
94
124
|
warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? ""),
|
|
@@ -1165,9 +1195,16 @@ var pubsub = null;
|
|
|
1165
1195
|
function initAccountingEventPublisher(instance) {
|
|
1166
1196
|
pubsub = instance;
|
|
1167
1197
|
}
|
|
1168
|
-
|
|
1198
|
+
/** `channelFor` is called INSIDE the guard, not before it: resolving the
|
|
1199
|
+
* scope can throw (a write that reached the publisher with no root
|
|
1200
|
+
* under explicit-root mode), and a lost event with a warning beats
|
|
1201
|
+
* either crashing the write or publishing to the unscoped channel that
|
|
1202
|
+
* every project can hear. */
|
|
1203
|
+
function safePublish(channelFor, payload) {
|
|
1169
1204
|
if (!pubsub) return;
|
|
1205
|
+
let channel = "(unresolved)";
|
|
1170
1206
|
try {
|
|
1207
|
+
channel = channelFor();
|
|
1171
1208
|
pubsub.publish(channel, payload);
|
|
1172
1209
|
} catch (err) {
|
|
1173
1210
|
log.warn("accounting", "publish failed; subscribers will miss this event", {
|
|
@@ -1178,15 +1215,20 @@ function safePublish(channel, payload) {
|
|
|
1178
1215
|
}
|
|
1179
1216
|
/** Per-book change notification. `period` should be the entry's
|
|
1180
1217
|
* YYYY-MM bucket (or the earliest invalidated month for snapshot
|
|
1181
|
-
* events).
|
|
1182
|
-
|
|
1183
|
-
|
|
1218
|
+
* events).
|
|
1219
|
+
*
|
|
1220
|
+
* `workspaceRoot` is the root the write happened under; the channel
|
|
1221
|
+
* name is namespaced by the host's opaque scope for it (see
|
|
1222
|
+
* `channelScopeFor`). Omit it — as a single-root host's service calls
|
|
1223
|
+
* do — and the name is what it has always been. */
|
|
1224
|
+
function publishBookChange(bookId, payload, workspaceRoot) {
|
|
1225
|
+
safePublish(() => require_shared.bookChannel(bookId, channelScopeFor(workspaceRoot)), payload);
|
|
1184
1226
|
}
|
|
1185
1227
|
/** Fired when the *list* of books changes (createBook, deleteBook).
|
|
1186
1228
|
* Payload is intentionally empty — subscribers refetch from
|
|
1187
|
-
* /api/accounting. */
|
|
1188
|
-
function publishBooksChanged() {
|
|
1189
|
-
safePublish(require_shared.
|
|
1229
|
+
* /api/accounting. Scoped by root like `publishBookChange`. */
|
|
1230
|
+
function publishBooksChanged(workspaceRoot) {
|
|
1231
|
+
safePublish(() => require_shared.booksChannel(channelScopeFor(workspaceRoot)), {});
|
|
1190
1232
|
}
|
|
1191
1233
|
//#endregion
|
|
1192
1234
|
//#region src/server/snapshotCache.ts
|
|
@@ -1255,18 +1297,39 @@ async function rebuildAllSnapshots(bookId, workspaceRoot) {
|
|
|
1255
1297
|
return { rebuilt: periods };
|
|
1256
1298
|
}
|
|
1257
1299
|
var rebuildQueues = /* @__PURE__ */ new Map();
|
|
1300
|
+
/** Canonical key for the root a rebuild belongs to. An absent root
|
|
1301
|
+
* resolves to the host's configured one, so a call that passes it
|
|
1302
|
+
* explicitly and one that relies on the default share a queue. Under
|
|
1303
|
+
* explicit-root mode there is no default and `defaultWorkspaceRoot()`
|
|
1304
|
+
* throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`
|
|
1305
|
+
* that quietly keyed a queue nobody scheduled would return while the
|
|
1306
|
+
* real rebuild is still writing, which is how `deleteBook` races a
|
|
1307
|
+
* `writeSnapshot` back into existence.
|
|
1308
|
+
*
|
|
1309
|
+
* Lexical resolution only — symlinks are deliberately NOT resolved,
|
|
1310
|
+
* matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,
|
|
1311
|
+
* so one project has one identity across both packages. A host that
|
|
1312
|
+
* hands the engine a real path in one call and a symlink alias in the
|
|
1313
|
+
* next would split the queue; the fix belongs in the host's own root
|
|
1314
|
+
* resolution, which is where the two packages agree it lives. */
|
|
1315
|
+
function rootKey(workspaceRoot) {
|
|
1316
|
+
return node_path.default.resolve(workspaceRoot ?? defaultWorkspaceRoot());
|
|
1317
|
+
}
|
|
1318
|
+
function queueKey(bookId, workspaceRoot) {
|
|
1319
|
+
return `${rootKey(workspaceRoot)}\u0000${bookId}`;
|
|
1320
|
+
}
|
|
1258
1321
|
function minPeriod(lhs, rhs) {
|
|
1259
1322
|
if (lhs === null) return rhs;
|
|
1260
1323
|
return lhs < rhs ? lhs : rhs;
|
|
1261
1324
|
}
|
|
1262
|
-
function isInvalidatedDuringRebuild(
|
|
1263
|
-
const queue = rebuildQueues.get(
|
|
1325
|
+
function isInvalidatedDuringRebuild(key, period) {
|
|
1326
|
+
const queue = rebuildQueues.get(key);
|
|
1264
1327
|
return queue !== void 0 && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
|
|
1265
1328
|
}
|
|
1266
|
-
function isCancelled(
|
|
1267
|
-
return rebuildQueues.get(
|
|
1329
|
+
function isCancelled(key) {
|
|
1330
|
+
return rebuildQueues.get(key)?.cancelled === true;
|
|
1268
1331
|
}
|
|
1269
|
-
async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1332
|
+
async function runRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1270
1333
|
const startedAt = Date.now();
|
|
1271
1334
|
log.info("accounting", "snapshot rebuild started", {
|
|
1272
1335
|
bookId,
|
|
@@ -1275,25 +1338,25 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1275
1338
|
publishBookChange(bookId, {
|
|
1276
1339
|
kind: require_shared.BOOK_EVENT_KINDS.snapshotsRebuilding,
|
|
1277
1340
|
period: fromPeriod
|
|
1278
|
-
});
|
|
1341
|
+
}, workspaceRoot);
|
|
1279
1342
|
const targets = (await listJournalPeriods(bookId, workspaceRoot)).filter((monthKey) => monthKey >= fromPeriod);
|
|
1280
1343
|
let written = 0;
|
|
1281
1344
|
for (const monthKey of targets) {
|
|
1282
|
-
if (isCancelled(
|
|
1283
|
-
if (isInvalidatedDuringRebuild(
|
|
1345
|
+
if (isCancelled(key)) break;
|
|
1346
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1284
1347
|
const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
|
|
1285
|
-
if (isCancelled(
|
|
1286
|
-
if (isInvalidatedDuringRebuild(
|
|
1348
|
+
if (isCancelled(key)) break;
|
|
1349
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1287
1350
|
await writeSnapshot(bookId, {
|
|
1288
1351
|
period: monthKey,
|
|
1289
1352
|
balances,
|
|
1290
1353
|
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1291
1354
|
}, workspaceRoot);
|
|
1292
|
-
if (isCancelled(
|
|
1355
|
+
if (isCancelled(key)) {
|
|
1293
1356
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1294
1357
|
break;
|
|
1295
1358
|
}
|
|
1296
|
-
if (isInvalidatedDuringRebuild(
|
|
1359
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) {
|
|
1297
1360
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1298
1361
|
break;
|
|
1299
1362
|
}
|
|
@@ -1301,7 +1364,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1301
1364
|
publishBookChange(bookId, {
|
|
1302
1365
|
kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady,
|
|
1303
1366
|
period: monthKey
|
|
1304
|
-
});
|
|
1367
|
+
}, workspaceRoot);
|
|
1305
1368
|
}
|
|
1306
1369
|
log.info("accounting", "snapshot rebuild done", {
|
|
1307
1370
|
bookId,
|
|
@@ -1309,7 +1372,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1309
1372
|
durationMs: Date.now() - startedAt
|
|
1310
1373
|
});
|
|
1311
1374
|
}
|
|
1312
|
-
function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1375
|
+
function startRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1313
1376
|
const entry = {
|
|
1314
1377
|
running: Promise.resolve(),
|
|
1315
1378
|
pendingFromPeriod: null,
|
|
@@ -1318,27 +1381,27 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1318
1381
|
runningFromPeriod: fromPeriod,
|
|
1319
1382
|
cancelled: false
|
|
1320
1383
|
};
|
|
1321
|
-
entry.running = runRebuild(bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1384
|
+
entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1322
1385
|
log.error("accounting", "snapshot rebuild failed", {
|
|
1323
1386
|
bookId,
|
|
1324
1387
|
fromPeriod,
|
|
1325
1388
|
error: require_shared.errorMessage(err)
|
|
1326
1389
|
});
|
|
1327
1390
|
}).then(() => {
|
|
1328
|
-
const current = rebuildQueues.get(
|
|
1391
|
+
const current = rebuildQueues.get(key);
|
|
1329
1392
|
if (!current) return;
|
|
1330
1393
|
if (current.cancelled) {
|
|
1331
|
-
rebuildQueues.delete(
|
|
1394
|
+
rebuildQueues.delete(key);
|
|
1332
1395
|
return;
|
|
1333
1396
|
}
|
|
1334
1397
|
if (current.pendingFromPeriod !== null) {
|
|
1335
1398
|
const nextFrom = current.pendingFromPeriod;
|
|
1336
1399
|
const nextRoot = current.pendingWorkspaceRoot;
|
|
1337
1400
|
const carriedCount = current.coalescedWriteCount;
|
|
1338
|
-
const successor = startRebuild(bookId, nextFrom, nextRoot);
|
|
1401
|
+
const successor = startRebuild(key, bookId, nextFrom, nextRoot);
|
|
1339
1402
|
successor.coalescedWriteCount += carriedCount;
|
|
1340
|
-
rebuildQueues.set(
|
|
1341
|
-
} else rebuildQueues.delete(
|
|
1403
|
+
rebuildQueues.set(key, successor);
|
|
1404
|
+
} else rebuildQueues.delete(key);
|
|
1342
1405
|
});
|
|
1343
1406
|
return entry;
|
|
1344
1407
|
}
|
|
@@ -1347,9 +1410,10 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1347
1410
|
* follow-up rebuild that covers the minimum `fromPeriod` seen.
|
|
1348
1411
|
* Returns immediately — the rebuild runs on its own promise chain. */
|
|
1349
1412
|
function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1350
|
-
const
|
|
1413
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1414
|
+
const existing = rebuildQueues.get(key);
|
|
1351
1415
|
if (!existing) {
|
|
1352
|
-
rebuildQueues.set(
|
|
1416
|
+
rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));
|
|
1353
1417
|
return;
|
|
1354
1418
|
}
|
|
1355
1419
|
existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
|
|
@@ -1360,9 +1424,10 @@ function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1360
1424
|
* `bookId`. Also called by `deleteBook` after `cancelRebuild` to
|
|
1361
1425
|
* ensure a previously running rebuild has fully stopped before the
|
|
1362
1426
|
* caller removes the book's directory on disk. */
|
|
1363
|
-
async function awaitRebuildIdle(bookId) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1427
|
+
async function awaitRebuildIdle(bookId, workspaceRoot) {
|
|
1428
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1429
|
+
while (rebuildQueues.has(key)) {
|
|
1430
|
+
const entry = rebuildQueues.get(key);
|
|
1366
1431
|
if (!entry) return;
|
|
1367
1432
|
await entry.running;
|
|
1368
1433
|
}
|
|
@@ -1372,8 +1437,8 @@ async function awaitRebuildIdle(bookId) {
|
|
|
1372
1437
|
* `removeBookDir` cannot race with a `writeSnapshot` that would
|
|
1373
1438
|
* re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
|
|
1374
1439
|
* to wait for the in-flight rebuild to finish bailing. */
|
|
1375
|
-
function cancelRebuild(bookId) {
|
|
1376
|
-
const entry = rebuildQueues.get(bookId);
|
|
1440
|
+
function cancelRebuild(bookId, workspaceRoot) {
|
|
1441
|
+
const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));
|
|
1377
1442
|
if (!entry) return;
|
|
1378
1443
|
entry.cancelled = true;
|
|
1379
1444
|
entry.pendingFromPeriod = null;
|
|
@@ -1727,7 +1792,7 @@ async function createBook(input, workspaceRoot) {
|
|
|
1727
1792
|
await ensureBookDir(bookId, workspaceRoot);
|
|
1728
1793
|
await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
|
|
1729
1794
|
await writeConfig({ books: [...config.books, book] }, workspaceRoot);
|
|
1730
|
-
publishBooksChanged();
|
|
1795
|
+
publishBooksChanged(workspaceRoot);
|
|
1731
1796
|
return { book };
|
|
1732
1797
|
}
|
|
1733
1798
|
async function updateBook(input, workspaceRoot) {
|
|
@@ -1744,7 +1809,7 @@ async function updateBook(input, workspaceRoot) {
|
|
|
1744
1809
|
};
|
|
1745
1810
|
if (country === "") delete next.country;
|
|
1746
1811
|
await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
|
|
1747
|
-
publishBooksChanged();
|
|
1812
|
+
publishBooksChanged(workspaceRoot);
|
|
1748
1813
|
return { book: next };
|
|
1749
1814
|
}
|
|
1750
1815
|
async function deleteBook(input, workspaceRoot) {
|
|
@@ -1752,11 +1817,11 @@ async function deleteBook(input, workspaceRoot) {
|
|
|
1752
1817
|
const config = await loadOrInitConfig(workspaceRoot);
|
|
1753
1818
|
const target = findBook(config, input.bookId);
|
|
1754
1819
|
if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
|
|
1755
|
-
cancelRebuild(input.bookId);
|
|
1756
|
-
await awaitRebuildIdle(input.bookId);
|
|
1820
|
+
cancelRebuild(input.bookId, workspaceRoot);
|
|
1821
|
+
await awaitRebuildIdle(input.bookId, workspaceRoot);
|
|
1757
1822
|
await removeBookDir(input.bookId, workspaceRoot);
|
|
1758
1823
|
await writeConfig({ books: config.books.filter((book) => book.id !== input.bookId) }, workspaceRoot);
|
|
1759
|
-
publishBooksChanged();
|
|
1824
|
+
publishBooksChanged(workspaceRoot);
|
|
1760
1825
|
return {
|
|
1761
1826
|
deletedBookId: input.bookId,
|
|
1762
1827
|
deletedBookName: target.name
|
|
@@ -1806,7 +1871,7 @@ async function upsertAccount(input, workspaceRoot) {
|
|
|
1806
1871
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1807
1872
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1808
1873
|
}
|
|
1809
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts });
|
|
1874
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts }, workspaceRoot);
|
|
1810
1875
|
return {
|
|
1811
1876
|
bookId,
|
|
1812
1877
|
account,
|
|
@@ -1855,7 +1920,7 @@ async function addEntries(input, workspaceRoot) {
|
|
|
1855
1920
|
publishBookChange(bookId, {
|
|
1856
1921
|
kind: require_shared.BOOK_EVENT_KINDS.journal,
|
|
1857
1922
|
period: earliestPeriod
|
|
1858
|
-
});
|
|
1923
|
+
}, workspaceRoot);
|
|
1859
1924
|
return {
|
|
1860
1925
|
bookId,
|
|
1861
1926
|
entries: built
|
|
@@ -1884,7 +1949,7 @@ async function voidEntry(input, workspaceRoot) {
|
|
|
1884
1949
|
publishBookChange(bookId, {
|
|
1885
1950
|
kind: require_shared.BOOK_EVENT_KINDS.journal,
|
|
1886
1951
|
period: fromPeriod
|
|
1887
|
-
});
|
|
1952
|
+
}, workspaceRoot);
|
|
1888
1953
|
return {
|
|
1889
1954
|
bookId,
|
|
1890
1955
|
reverseEntry: reverse,
|
|
@@ -1948,7 +2013,7 @@ async function setOpeningBalances(input, workspaceRoot) {
|
|
|
1948
2013
|
await appendJournal(bookId, opening, workspaceRoot);
|
|
1949
2014
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1950
2015
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1951
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening });
|
|
2016
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening }, workspaceRoot);
|
|
1952
2017
|
return {
|
|
1953
2018
|
bookId,
|
|
1954
2019
|
openingEntry: opening,
|
|
@@ -2083,7 +2148,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
|
|
|
2083
2148
|
async function rebuildSnapshots(input, workspaceRoot) {
|
|
2084
2149
|
const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
|
|
2085
2150
|
const result = await rebuildAllSnapshots(bookId, workspaceRoot);
|
|
2086
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady });
|
|
2151
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
|
|
2087
2152
|
return {
|
|
2088
2153
|
bookId,
|
|
2089
2154
|
rebuilt: result.rebuilt
|
|
@@ -2110,19 +2175,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
|
|
|
2110
2175
|
}
|
|
2111
2176
|
//#endregion
|
|
2112
2177
|
//#region src/server/router.ts
|
|
2113
|
-
async function handleOpenBook(rest) {
|
|
2178
|
+
async function handleOpenBook(rest, root) {
|
|
2114
2179
|
if (typeof rest.bookId !== "string" || rest.bookId === "") throw new AccountingError(400, "openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.");
|
|
2115
|
-
const list = await listBooks();
|
|
2180
|
+
const list = await listBooks(root);
|
|
2116
2181
|
if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
|
|
2117
2182
|
const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
|
|
2183
|
+
const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
|
|
2118
2184
|
return {
|
|
2119
2185
|
kind: "accounting-app",
|
|
2120
2186
|
bookId: rest.bookId,
|
|
2121
2187
|
initialTab,
|
|
2122
|
-
books: list.books
|
|
2188
|
+
books: list.books,
|
|
2189
|
+
...scopeField
|
|
2123
2190
|
};
|
|
2124
2191
|
}
|
|
2125
|
-
async function handleGetReport(rest) {
|
|
2192
|
+
async function handleGetReport(rest, root) {
|
|
2126
2193
|
const kind = typeof rest.kind === "string" ? rest.kind : "";
|
|
2127
2194
|
const periodInput = optionalReportPeriod(rest.period);
|
|
2128
2195
|
const bookId = optionalString(rest.bookId);
|
|
@@ -2133,14 +2200,14 @@ async function handleGetReport(rest) {
|
|
|
2133
2200
|
return getBalanceSheetReport({
|
|
2134
2201
|
bookId,
|
|
2135
2202
|
period: periodInput
|
|
2136
|
-
});
|
|
2203
|
+
}, root);
|
|
2137
2204
|
}
|
|
2138
2205
|
if (kind === "pl") {
|
|
2139
2206
|
if (!periodInput) throw periodRequired("getReport pl");
|
|
2140
2207
|
return getProfitLossReport({
|
|
2141
2208
|
bookId,
|
|
2142
2209
|
period: periodInput
|
|
2143
|
-
});
|
|
2210
|
+
}, root);
|
|
2144
2211
|
}
|
|
2145
2212
|
if (kind === "ledger") {
|
|
2146
2213
|
if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
|
|
@@ -2148,79 +2215,79 @@ async function handleGetReport(rest) {
|
|
|
2148
2215
|
bookId,
|
|
2149
2216
|
accountCode: rest.accountCode,
|
|
2150
2217
|
period: periodInput
|
|
2151
|
-
});
|
|
2218
|
+
}, root);
|
|
2152
2219
|
}
|
|
2153
2220
|
throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
|
|
2154
2221
|
}
|
|
2155
2222
|
var ACTION_HANDLERS = {
|
|
2156
2223
|
[require_shared.ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
|
|
2157
|
-
[require_shared.ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
|
|
2158
|
-
[require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest) => {
|
|
2224
|
+
[require_shared.ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
|
|
2225
|
+
[require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
|
|
2159
2226
|
const result = await createBook({
|
|
2160
2227
|
name: typeof rest.name === "string" ? rest.name : "",
|
|
2161
2228
|
currency: typeof rest.currency === "string" ? rest.currency : void 0,
|
|
2162
2229
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2163
2230
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2164
|
-
});
|
|
2231
|
+
}, root);
|
|
2165
2232
|
return {
|
|
2166
2233
|
bookId: result.book.id,
|
|
2167
2234
|
...result
|
|
2168
2235
|
};
|
|
2169
2236
|
},
|
|
2170
|
-
[require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
|
|
2237
|
+
[require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
|
|
2171
2238
|
const result = await updateBook({
|
|
2172
2239
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2173
2240
|
name: typeof rest.name === "string" ? rest.name : void 0,
|
|
2174
2241
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2175
2242
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2176
|
-
});
|
|
2243
|
+
}, root);
|
|
2177
2244
|
return {
|
|
2178
2245
|
bookId: result.book.id,
|
|
2179
2246
|
...result
|
|
2180
2247
|
};
|
|
2181
2248
|
},
|
|
2182
|
-
[require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
|
|
2249
|
+
[require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
|
|
2183
2250
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2184
2251
|
confirm: rest.confirm === true
|
|
2185
|
-
}),
|
|
2186
|
-
[require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
|
|
2187
|
-
[require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest) => upsertAccount({
|
|
2252
|
+
}, root),
|
|
2253
|
+
[require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),
|
|
2254
|
+
[require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({
|
|
2188
2255
|
bookId: optionalString(rest.bookId),
|
|
2189
2256
|
account: rest.account
|
|
2190
|
-
}),
|
|
2191
|
-
[require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
|
|
2257
|
+
}, root),
|
|
2258
|
+
[require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
|
|
2192
2259
|
bookId: optionalString(rest.bookId),
|
|
2193
2260
|
entries: rest.entries
|
|
2194
|
-
}),
|
|
2195
|
-
[require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
|
|
2261
|
+
}, root),
|
|
2262
|
+
[require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
|
|
2196
2263
|
bookId: optionalString(rest.bookId),
|
|
2197
2264
|
entryId: typeof rest.entryId === "string" ? rest.entryId : "",
|
|
2198
2265
|
reason: optionalString(rest.reason),
|
|
2199
2266
|
voidDate: optionalString(rest.voidDate)
|
|
2200
|
-
}),
|
|
2201
|
-
[require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
|
|
2267
|
+
}, root),
|
|
2268
|
+
[require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
|
|
2202
2269
|
bookId: optionalString(rest.bookId),
|
|
2203
2270
|
from: optionalString(rest.from),
|
|
2204
2271
|
to: optionalString(rest.to),
|
|
2205
2272
|
accountCode: optionalString(rest.accountCode)
|
|
2206
|
-
}),
|
|
2207
|
-
[require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
|
|
2208
|
-
[require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) => setOpeningBalances({
|
|
2273
|
+
}, root),
|
|
2274
|
+
[require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),
|
|
2275
|
+
[require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) => setOpeningBalances({
|
|
2209
2276
|
bookId: optionalString(rest.bookId),
|
|
2210
2277
|
asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
|
|
2211
2278
|
lines: rest.lines ?? [],
|
|
2212
2279
|
memo: optionalString(rest.memo)
|
|
2213
|
-
}),
|
|
2280
|
+
}, root),
|
|
2214
2281
|
[require_shared.ACCOUNTING_ACTIONS.getReport]: handleGetReport,
|
|
2215
|
-
[require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
|
|
2282
|
+
[require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
|
|
2216
2283
|
bookId: optionalString(rest.bookId),
|
|
2217
2284
|
metric: rest.metric,
|
|
2218
2285
|
granularity: rest.granularity,
|
|
2219
2286
|
from: rest.from,
|
|
2220
2287
|
to: rest.to,
|
|
2221
2288
|
accountCode: rest.accountCode
|
|
2222
|
-
}),
|
|
2223
|
-
[require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
|
|
2289
|
+
}, root),
|
|
2290
|
+
[require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
|
|
2224
2291
|
};
|
|
2225
2292
|
var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
|
|
2226
2293
|
require_shared.ACCOUNTING_ACTIONS.openBook,
|
|
@@ -2297,11 +2364,11 @@ function previewMessage(action, fields) {
|
|
|
2297
2364
|
const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
|
|
2298
2365
|
return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
|
|
2299
2366
|
}
|
|
2300
|
-
async function dispatch(body) {
|
|
2367
|
+
async function dispatch(body, root) {
|
|
2301
2368
|
const { action, ...rest } = body;
|
|
2302
2369
|
const handler = ownEntry(ACTION_HANDLERS, action);
|
|
2303
2370
|
if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
|
|
2304
|
-
const result = await handler(rest);
|
|
2371
|
+
const result = await handler(rest, root);
|
|
2305
2372
|
const handlerFields = require_shared.isRecord(result) ? result : { value: result };
|
|
2306
2373
|
const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
|
|
2307
2374
|
action,
|
|
@@ -2318,8 +2385,9 @@ async function dispatch(body) {
|
|
|
2318
2385
|
/** Build the accounting Express router. The host injects its workspace
|
|
2319
2386
|
* root + logger via `configureAccountingServer(...)` and pub/sub via
|
|
2320
2387
|
* `initAccountingEventPublisher(...)`, then mounts the returned router
|
|
2321
|
-
* with `app.use(...)`.
|
|
2322
|
-
|
|
2388
|
+
* with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than
|
|
2389
|
+
* one root; omit it and every request uses the configured one. */
|
|
2390
|
+
function createAccountingRouter(options = {}) {
|
|
2323
2391
|
const router = (0, express.Router)();
|
|
2324
2392
|
router.post(require_shared.ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
|
|
2325
2393
|
const { body } = req;
|
|
@@ -2331,7 +2399,8 @@ function createAccountingRouter() {
|
|
|
2331
2399
|
const { action } = body;
|
|
2332
2400
|
log.info("accounting", "POST dispatch: start", { action });
|
|
2333
2401
|
try {
|
|
2334
|
-
const
|
|
2402
|
+
const root = options.resolveWorkspaceRoot?.(req);
|
|
2403
|
+
const result = await dispatch(body, root);
|
|
2335
2404
|
log.info("accounting", "POST dispatch: ok", { action });
|
|
2336
2405
|
res.json(result);
|
|
2337
2406
|
} catch (err) {
|