pluts 0.1.1 → 0.1.2

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 CHANGED
@@ -45,6 +45,35 @@ Pluts persists over a SQLite-backed Durable Object's own storage (`ctx.storage.s
45
45
  - `migrate(sql)` applies the schema via `sql.exec(...)`. It is idempotent and safe to run on every cold start; existing tables and indexes are left untouched. There is no separate migrations tracking table and no code generator to run.
46
46
  - A ledger is hosted _inside_ a Durable Object: the DO's private SQLite database (declared via `new_sqlite_classes` in `wrangler.jsonc`) is the ledger. One DO instance = one isolated ledger. `migrate(ctx.storage.sql)` self-provisions each one, typically in the DO constructor under `blockConcurrencyWhile`.
47
47
 
48
+ ### Serialization at boundaries
49
+
50
+ Pluts domain objects cannot cross either exit from a Durable Object:
51
+
52
+ - **Workers RPC** serializes method arguments/returns with structured clone. `bigint` is clone-safe, but class instances like `Amount` and `Entry` are rejected with `DataCloneError`.
53
+ - **JSON REST** uses `JSON.stringify`, which throws `TypeError` on `bigint`.
54
+
55
+ Returning a raw `Entry` (or any object carrying `Amount`/`Account` instances) from a DO RPC method or HTTP handler fails at runtime.
56
+
57
+ **RPC methods must return DTOs, never domain objects.** Use the mappers exported from `pluts`:
58
+
59
+ - `toEntryDTO(entry)` → `EntryDTO`
60
+ - `toAmountLineDTO(line)` → `AmountLineDTO`
61
+ - `toAccountDTO(account)` → `AccountDTO`
62
+
63
+ These produce deep-plain objects whose monetary fields are fixed-precision decimal strings (`"10.00"`), safe for both structured clone and `JSON.stringify`.
64
+
65
+ `Amount#toJSON()` returns the major-units decimal string, which makes `JSON.stringify` safe for objects containing an `Amount` — but it does **not** help Workers RPC, because structured clone ignores `toJSON`. Use the DTO mappers at RPC boundaries.
66
+
67
+ ```ts
68
+ // Inside a Durable Object RPC method:
69
+ async postEntry(input: EntryInput): Promise<EntryDTO> {
70
+ const payload = buildEntry(input, (name) => this.repository.getAccountByName(name));
71
+ const entry = await this.repository.insertEntry(payload);
72
+ return toEntryDTO(entry);
73
+ }
74
+ ```
75
+
76
+
48
77
  ### SQL implementation and portability
49
78
 
50
79
  Pluts's production persistence is implemented against Cloudflare Durable Objects' embedded SQLite via the synchronous `SqlStorage` API. The concrete implementation lives in `src/db/sqlite-storage-repository.ts` and runs the schema statements defined in `src/db/schema.ts` using `migrate(ctx.storage.sql)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluts",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A double-entry accounting ledger for TypeScript, targeting Cloudflare Durable Objects (SQLite-backed storage).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -129,8 +129,17 @@ export class Amount {
129
129
  return `${whole}.${frac.toString().padStart(SCALE, "0")}`;
130
130
  }
131
131
 
132
+ /**
133
+ * Serialize as a major-units decimal string (e.g. "10.00").
134
+ *
135
+ * This makes `JSON.stringify` safe for objects containing an {@link Amount}
136
+ * (a raw `bigint` would otherwise throw a TypeError). It does NOT help
137
+ * Workers RPC: structured clone ignores `toJSON` and rejects class
138
+ * instances, so RPC boundaries must use the DTO mappers (`toEntryDTO`, etc.)
139
+ * instead.
140
+ */
132
141
  toJSON(): string {
133
- return this.minor.toString();
142
+ return this.toMajor();
134
143
  }
135
144
 
136
145
  toString(): string {
@@ -0,0 +1,77 @@
1
+ import type { Account } from "./account.js";
2
+ import type { AmountRecord, Entry } from "./entry.js";
3
+
4
+ /**
5
+ * Plain, boundary-safe projections of the pluts domain objects.
6
+ *
7
+ * The pluts domain types (Account, AmountRecord, Entry) carry `class`
8
+ * instances and `bigint` values, which cannot cross the two exits from a
9
+ * Durable Object: Workers RPC (structured clone rejects class instances) and
10
+ * JSON REST (JSON.stringify throws on bigint). These mappers produce deep-plain
11
+ * objects whose monetary fields are fixed-precision decimal strings, suitable
12
+ * for both. Use them at every RPC/REST boundary; never return a raw domain
13
+ * object across one.
14
+ */
15
+
16
+ export interface AccountDTO {
17
+ id: string;
18
+ name: string;
19
+ type: string;
20
+ contra: boolean;
21
+ createdAt: string;
22
+ /** Optional pre-computed balance, formatted in major units. */
23
+ balance?: string;
24
+ }
25
+
26
+ export interface AmountLineDTO {
27
+ id: string;
28
+ kind: "debit" | "credit";
29
+ account: AccountDTO;
30
+ /** Major-units decimal string, e.g. "10.00". */
31
+ amount: string;
32
+ entryId: string;
33
+ }
34
+
35
+ export interface EntryDTO {
36
+ id: string;
37
+ description: string;
38
+ date: string;
39
+ debitAmounts: AmountLineDTO[];
40
+ creditAmounts: AmountLineDTO[];
41
+ postedAt: string;
42
+ }
43
+
44
+ function freeze<T>(value: T): T {
45
+ return Object.freeze(value);
46
+ }
47
+
48
+ export function toAccountDTO(account: Account): AccountDTO {
49
+ return freeze({
50
+ id: account.id,
51
+ name: account.name,
52
+ type: account.type,
53
+ contra: account.contra,
54
+ createdAt: account.createdAt,
55
+ });
56
+ }
57
+
58
+ export function toAmountLineDTO(line: AmountRecord): AmountLineDTO {
59
+ return freeze({
60
+ id: line.id,
61
+ kind: line.kind,
62
+ account: toAccountDTO(line.account),
63
+ amount: line.amount.toMajor(),
64
+ entryId: line.entryId,
65
+ });
66
+ }
67
+
68
+ export function toEntryDTO(entry: Entry): EntryDTO {
69
+ return freeze({
70
+ id: entry.id,
71
+ description: entry.description,
72
+ date: entry.date,
73
+ debitAmounts: entry.debitAmounts.map(toAmountLineDTO),
74
+ creditAmounts: entry.creditAmounts.map(toAmountLineDTO),
75
+ postedAt: entry.postedAt,
76
+ });
77
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,14 @@ export {
11
11
  computeBalance,
12
12
  } from "./domain/account.js";
13
13
  export { Amount, formatAmount, SCALE } from "./domain/amount.js";
14
+ export {
15
+ type AccountDTO,
16
+ type AmountLineDTO,
17
+ type EntryDTO,
18
+ toAccountDTO,
19
+ toAmountLineDTO,
20
+ toEntryDTO,
21
+ } from "./domain/dto.js";
14
22
  export {
15
23
  type AmountKind,
16
24
  AmountRecord,