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 +29 -0
- package/package.json +1 -1
- package/src/domain/amount.ts +10 -1
- package/src/domain/dto.ts +77 -0
- package/src/index.ts +8 -0
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
package/src/domain/amount.ts
CHANGED
|
@@ -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.
|
|
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,
|