@boxxy/store-sqlite 0.1.0 → 0.1.3

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.
Files changed (54) hide show
  1. package/README.md +24 -3
  2. package/dist/codecs/parse-json-text.d.ts +1 -0
  3. package/dist/codecs/parse-store-row.d.ts +2 -0
  4. package/dist/codecs/table-codecs.d.ts +10 -0
  5. package/dist/codecs/to-column-name.d.ts +1 -0
  6. package/dist/codecs/to-store-columns.d.ts +3 -0
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/create-sqlite-claim-store.d.ts +5 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/migrations/create-tables-migration.d.ts +2 -0
  11. package/dist/migrations/rename-relations-migration.d.ts +2 -0
  12. package/dist/migrations/run-store-migrations.d.ts +3 -0
  13. package/dist/migrations/v1-tables-migration.d.ts +2 -0
  14. package/dist/reads/build-sqlite-reader.d.ts +4 -0
  15. package/dist/reads/is-latest-of-session.d.ts +3 -0
  16. package/dist/reads/read-item-views.d.ts +5 -0
  17. package/dist/reads/read-limit-fills.d.ts +4 -0
  18. package/dist/reads/read-wakes.d.ts +4 -0
  19. package/dist/test-utils/create-claim-bench.d.ts +8 -0
  20. package/dist/test-utils/create-test-claim-store.d.ts +2 -0
  21. package/dist/types.d.ts +172 -0
  22. package/dist/with-busy-retry.d.ts +1 -0
  23. package/dist/with-transaction.d.ts +3 -0
  24. package/dist/writes/apply-sqlite-change.d.ts +4 -0
  25. package/dist/writes/create-sqlite-tx.d.ts +4 -0
  26. package/dist/writes/take-write-lock.d.ts +3 -0
  27. package/package.json +15 -1
  28. package/src/codecs/parse-json-text.ts +5 -0
  29. package/src/codecs/parse-store-row.ts +36 -0
  30. package/src/codecs/table-codecs.ts +148 -0
  31. package/src/codecs/to-column-name.ts +3 -0
  32. package/src/codecs/to-store-columns.ts +44 -0
  33. package/src/constants.ts +1 -0
  34. package/src/create-sqlite-claim-store.ts +114 -0
  35. package/src/index.ts +1 -1
  36. package/src/migrations/create-tables-migration.ts +146 -0
  37. package/src/migrations/rename-relations-migration.ts +17 -0
  38. package/src/migrations/run-store-migrations.ts +42 -0
  39. package/src/migrations/v1-tables-migration.ts +184 -0
  40. package/src/reads/build-sqlite-reader.ts +484 -0
  41. package/src/reads/is-latest-of-session.ts +21 -0
  42. package/src/reads/read-item-views.ts +92 -0
  43. package/src/reads/read-limit-fills.ts +32 -0
  44. package/src/reads/read-wakes.ts +34 -0
  45. package/src/test-utils/create-claim-bench.ts +66 -0
  46. package/src/test-utils/create-test-claim-store.ts +23 -0
  47. package/src/types.ts +173 -0
  48. package/src/with-busy-retry.ts +32 -0
  49. package/src/with-transaction.ts +29 -0
  50. package/src/writes/apply-sqlite-change.ts +223 -0
  51. package/src/writes/create-sqlite-tx.ts +229 -0
  52. package/src/writes/take-write-lock.ts +8 -0
  53. package/dist/package-name.d.ts +0 -1
  54. package/src/package-name.ts +0 -1
package/README.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://www.npmjs.com/package/@boxxy/store-sqlite"><img src="https://img.shields.io/npm/v/@boxxy/store-sqlite" alt="npm version"></a>
6
6
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
7
7
  </p>
8
+
9
+ <p>
10
+ <a href="../../docs/design/boxxy/README.md">Documentation</a>
11
+ </p>
8
12
  </div>
9
13
 
10
- boxxy's store on SQLite: the schema, the migrations and the transactions.
14
+ boxxy's store on SQLite: the schema, the migrations and the transactions behind the `ClaimStore`
15
+ port.
11
16
 
12
- This version is a placeholder that exports only its package name, as `packageName`. The
13
- [boxxy README](https://github.com/zgeoff/boxxy#workspace) lists the workspace's packages.
17
+ `createSQLiteClaimStore(path)` opens one SQLite file, creates it when it's missing, runs the
18
+ migrations the file hasn't run, and returns the store with a `close` function.
14
19
 
15
20
  ## Install
16
21
 
@@ -21,3 +26,19 @@ bun add @boxxy/store-sqlite
21
26
  ```
22
27
 
23
28
  The package runs on [Bun](https://bun.sh) 1.3 or newer.
29
+
30
+ ## Features
31
+
32
+ - Items, levers, attempts, leases, limit seats, notes, notices and the outbox in one file, with each
33
+ change applied whole or not at all
34
+ ([one write path](../../docs/design/boxxy/slice-1.md#one-write-path)).
35
+ - `exclusive` calls that run one at a time in a `BEGIN IMMEDIATE` transaction, and `read` calls that
36
+ see the last commit ([store rules](../../docs/design/boxxy/slice-1.md#store-rules)).
37
+ - Each item's position and state derived on each read, from the rows and the loaded rules
38
+ ([v1](../../docs/design/boxxy/v1.md)).
39
+
40
+ ## Documentation
41
+
42
+ The [v1 tables migration](./src/migrations/v1-tables-migration.ts) creates each table, and the store
43
+ section of [`02-types.ts`](../../docs/design/boxxy/sketches/v1/core/02-types.ts) gives the port, the
44
+ seat take and the seat sync.
@@ -0,0 +1 @@
1
+ export declare function parseJSONText(text: string): unknown;
@@ -0,0 +1,2 @@
1
+ import type { TableName, Tables } from '@boxxy/model';
2
+ export declare function parseStoreRow<T extends TableName>(table: T, record: object): Tables[T];
@@ -0,0 +1,10 @@
1
+ import type { TableName, Tables } from '@boxxy/model';
2
+ interface TableCodec<Row> {
3
+ readonly fields: readonly (keyof Row & string)[];
4
+ readonly json: readonly (keyof Row & string)[];
5
+ readonly flags: readonly (keyof Row & string)[];
6
+ }
7
+ export declare const tableCodecs: {
8
+ readonly [T in TableName]: TableCodec<Tables[T]>;
9
+ };
10
+ export {};
@@ -0,0 +1 @@
1
+ export declare function toColumnName(field: string): string;
@@ -0,0 +1,3 @@
1
+ import type { TableName } from '@boxxy/model';
2
+ export type StoreValue = string | number | null;
3
+ export declare function toStoreColumns(table: TableName, fields: object): [string, StoreValue][];
@@ -0,0 +1 @@
1
+ export declare const lockWaitLimitMs = 5000;
@@ -0,0 +1,5 @@
1
+ import type { ClaimStore } from '@boxxy/model';
2
+ export interface SQLiteClaimStore extends ClaimStore {
3
+ close: () => Promise<void>;
4
+ }
5
+ export declare function createSQLiteClaimStore(path: string): Promise<SQLiteClaimStore>;
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { packageName } from './package-name.ts';
1
+ export { createSQLiteClaimStore, type SQLiteClaimStore } from './create-sqlite-claim-store.ts';
@@ -0,0 +1,2 @@
1
+ import type { Migration } from 'kysely/migration';
2
+ export declare const createTablesMigration: Migration;
@@ -0,0 +1,2 @@
1
+ import type { Migration } from 'kysely/migration';
2
+ export declare const renameRelationsMigration: Migration;
@@ -0,0 +1,3 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { StoreDatabase } from '../types.ts';
3
+ export declare function runStoreMigrations(db: Kysely<StoreDatabase>): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import type { Migration } from 'kysely/migration';
2
+ export declare const v1TablesMigration: Migration;
@@ -0,0 +1,4 @@
1
+ import type { StoreReader } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export declare function buildSQLiteReader(db: Kysely<StoreDatabase>): StoreReader;
@@ -0,0 +1,3 @@
1
+ import type { Expression, ExpressionBuilder, SqlBool } from 'kysely';
2
+ import type { StoreDatabase } from '../types.ts';
3
+ export declare function isLatestOfSession(builder: ExpressionBuilder<StoreDatabase, 'attempts'>): Expression<SqlBool>;
@@ -0,0 +1,5 @@
1
+ import type { Instant, ItemRules, ItemWithLevers } from '@boxxy/model';
2
+ import type { Expression, ExpressionBuilder, Kysely, SqlBool } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export type ItemScope = (builder: ExpressionBuilder<StoreDatabase, 'items'>) => Expression<SqlBool>;
5
+ export declare function readItemViews(db: Kysely<StoreDatabase>, scope: ItemScope, now: Instant, rules: ItemRules): Promise<ItemWithLevers[]>;
@@ -0,0 +1,4 @@
1
+ import type { LimitFill } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export declare function readLimitFills(db: Kysely<StoreDatabase>): Promise<LimitFill[]>;
@@ -0,0 +1,4 @@
1
+ import type { Wake } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export declare function readWakes(db: Kysely<StoreDatabase>): Promise<Wake[]>;
@@ -0,0 +1,8 @@
1
+ import type { ItemId } from '@boxxy/model';
2
+ export interface ClaimBenchOptions {
3
+ readonly itemCount: number;
4
+ }
5
+ export interface ClaimBench extends AsyncDisposable {
6
+ readonly claim: () => Promise<ItemId>;
7
+ }
8
+ export declare function createClaimBench(options: ClaimBenchOptions): Promise<ClaimBench>;
@@ -0,0 +1,2 @@
1
+ import type { SQLiteClaimStore } from '../create-sqlite-claim-store.ts';
2
+ export declare function createTestClaimStore(): Promise<SQLiteClaimStore>;
@@ -0,0 +1,172 @@
1
+ import type { Generated } from 'kysely';
2
+ export interface StoreDatabase {
3
+ items: {
4
+ id: string;
5
+ kind: string;
6
+ repo: string;
7
+ title: string;
8
+ outcome: string;
9
+ acceptance: string;
10
+ fields: string;
11
+ labels: string;
12
+ found_in_attempt: string | null;
13
+ filed_by: string;
14
+ filed_at: number;
15
+ closed_at: number | null;
16
+ close_reason: string | null;
17
+ kept_session: string | null;
18
+ };
19
+ relations: {
20
+ source: string;
21
+ target: string;
22
+ type: string;
23
+ added_by: string;
24
+ added_at: number;
25
+ removed_at: number | null;
26
+ };
27
+ levers: {
28
+ seq: number;
29
+ kind: string;
30
+ item: string;
31
+ until: number | null;
32
+ expires_at: number | null;
33
+ reason: string | null;
34
+ phase: string | null;
35
+ actor: string;
36
+ at: number;
37
+ };
38
+ attempts: {
39
+ id: string;
40
+ item: string;
41
+ phase: string;
42
+ number: number;
43
+ runner: string;
44
+ recipe_hash: string;
45
+ rules_version: string;
46
+ pool: string;
47
+ lease_gen: number;
48
+ spawn_request: string;
49
+ spawn_request_hash: string;
50
+ state: string;
51
+ failure: string | null;
52
+ atc_session: string | null;
53
+ caller: string | null;
54
+ claimed_at: number;
55
+ started_at: number | null;
56
+ ended_at: number | null;
57
+ fields: string;
58
+ mode: string;
59
+ round: number;
60
+ input: string | null;
61
+ hold_gen: number;
62
+ close_out: string | null;
63
+ close_out_hash: string | null;
64
+ close_out_fact: string | null;
65
+ closing: string | null;
66
+ closing_at: number | null;
67
+ suspend_requested_at: number | null;
68
+ slept_at: number | null;
69
+ wake_requested_at: number | null;
70
+ woken_at: number | null;
71
+ outcome: string | null;
72
+ returned_outcome: string | null;
73
+ strayed_at: number | null;
74
+ session_stopped_at: number | null;
75
+ session_forgotten_at: number | null;
76
+ };
77
+ attempt_sessions: {
78
+ attempt: string;
79
+ agent_session: string;
80
+ atc_session: string;
81
+ product: string;
82
+ source: string;
83
+ seen_at: number;
84
+ };
85
+ leases: {
86
+ item: string;
87
+ phase: string;
88
+ attempt: string;
89
+ gen: number;
90
+ taken_at: number;
91
+ released_at: number | null;
92
+ };
93
+ limits: {
94
+ name: string;
95
+ max: number;
96
+ counts: string;
97
+ };
98
+ limit_seats: {
99
+ limit: string;
100
+ attempt: string;
101
+ hold_gen: number;
102
+ over_cap: number;
103
+ taken_at: number;
104
+ released_at: number | null;
105
+ };
106
+ decisions: {
107
+ id: string;
108
+ attempt: string;
109
+ item: string;
110
+ kind: string;
111
+ pool: string;
112
+ rules_version: string;
113
+ snapshot_hash: string;
114
+ store_version: number;
115
+ pepys_offsets: string;
116
+ faults: string;
117
+ limits: string;
118
+ at: number;
119
+ };
120
+ evidence: {
121
+ fact_id: string;
122
+ attempt: string;
123
+ kind: string;
124
+ ref: string;
125
+ observed: string | null;
126
+ source: string;
127
+ at: number;
128
+ };
129
+ pull_requests: {
130
+ repo: string;
131
+ number: number;
132
+ attempt: string;
133
+ github_id: number;
134
+ url: string;
135
+ state: string;
136
+ merged_at: number | null;
137
+ merge_commit: string | null;
138
+ checked_at: number;
139
+ };
140
+ runners: {
141
+ id: string;
142
+ heartbeat_at: number;
143
+ paused: number;
144
+ };
145
+ notes: {
146
+ item: string;
147
+ seq: number;
148
+ text: string;
149
+ by: string;
150
+ at: number;
151
+ };
152
+ notices: {
153
+ id: Generated<number>;
154
+ item: string;
155
+ reason: string;
156
+ at: number;
157
+ tries: number;
158
+ sent_at: number | null;
159
+ };
160
+ outbox: {
161
+ seq: Generated<number>;
162
+ fact_id: string;
163
+ type: string;
164
+ entity: string;
165
+ actor: string;
166
+ at: number;
167
+ payload: string;
168
+ sent_at: number | null;
169
+ parked_at: number | null;
170
+ parked_reason: string | null;
171
+ };
172
+ }
@@ -0,0 +1 @@
1
+ export declare function withBusyRetry(waitLimitMs: number, work: () => unknown): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { StoreDatabase } from './types.ts';
3
+ export declare function withTransaction<T>(db: Kysely<StoreDatabase>, begin: (db: Kysely<StoreDatabase>) => Promise<unknown>, work: () => Promise<T>): Promise<T>;
@@ -0,0 +1,4 @@
1
+ import type { ApplyResult, StoreChange } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export declare function applySQLiteChange(db: Kysely<StoreDatabase>, change: StoreChange): Promise<ApplyResult>;
@@ -0,0 +1,4 @@
1
+ import type { StoreTx } from '@boxxy/model';
2
+ import type { Kysely } from 'kysely';
3
+ import type { StoreDatabase } from '../types.ts';
4
+ export declare function createSQLiteTx(db: Kysely<StoreDatabase>): StoreTx;
@@ -0,0 +1,3 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { StoreDatabase } from '../types.ts';
3
+ export declare function takeWriteLock(db: Kysely<StoreDatabase>, waitLimitMs: number): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxxy/store-sqlite",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "boxxy's store on SQLite",
5
5
  "homepage": "https://github.com/zgeoff/boxxy/tree/main/adapters/store-sqlite#readme",
6
6
  "bugs": "https://github.com/zgeoff/boxxy/issues",
@@ -26,9 +26,23 @@
26
26
  "access": "public"
27
27
  },
28
28
  "scripts": {
29
+ "bench": "rig bench",
29
30
  "build": "rm -rf dist && tsrs -p tsconfig.build.json",
30
31
  "typecheck": "tsrs --noEmit"
31
32
  },
33
+ "dependencies": {
34
+ "@boxxy/content-hash": "0.1.3",
35
+ "@boxxy/model": "0.1.3",
36
+ "@zgeoff/kysely-bun-sqlite": "0.3.2",
37
+ "kysely": "0.29.8",
38
+ "tiny-invariant": "1.3.3",
39
+ "ts-pattern": "5.9.2",
40
+ "zod": "4.6.7"
41
+ },
42
+ "devDependencies": {
43
+ "@boxxy/dispatch": "0.1.3",
44
+ "@zgeoff/rig": "0.2.3"
45
+ },
32
46
  "engines": {
33
47
  "bun": ">=1.3",
34
48
  "node": ">=24"
@@ -0,0 +1,5 @@
1
+ export function parseJSONText(text: string): unknown {
2
+ const parsed: unknown = JSON.parse(text);
3
+
4
+ return parsed;
5
+ }
@@ -0,0 +1,36 @@
1
+ import { rowSchemas } from '@boxxy/model';
2
+ import type { TableName, Tables } from '@boxxy/model';
3
+ import { tableCodecs } from './table-codecs.ts';
4
+ import { toColumnName } from './to-column-name.ts';
5
+
6
+ export function parseStoreRow<T extends TableName>(table: T, record: object): Tables[T] {
7
+ const codec = tableCodecs[table];
8
+
9
+ const columns = new Map<string, unknown>(Object.entries(record));
10
+ const json = new Set<string>(codec.json);
11
+ const flags = new Set<string>(codec.flags);
12
+
13
+ const fields = Object.fromEntries(
14
+ codec.fields.map((field) => {
15
+ const value = columns.get(toColumnName(field));
16
+
17
+ return [field, parseStoreValue(value, json.has(field), flags.has(field))];
18
+ }),
19
+ );
20
+
21
+ return rowSchemas[table].parse(fields);
22
+ }
23
+
24
+ function parseStoreValue(value: unknown, isJSON: boolean, isFlag: boolean): unknown {
25
+ if (isJSON && typeof value === 'string') {
26
+ const parsed: unknown = JSON.parse(value);
27
+
28
+ return parsed;
29
+ }
30
+
31
+ if (isFlag) {
32
+ return value === 1;
33
+ }
34
+
35
+ return value;
36
+ }
@@ -0,0 +1,148 @@
1
+ import type { TableName, Tables } from '@boxxy/model';
2
+
3
+ interface TableCodec<Row> {
4
+ readonly fields: readonly (keyof Row & string)[];
5
+
6
+ readonly json: readonly (keyof Row & string)[];
7
+
8
+ readonly flags: readonly (keyof Row & string)[];
9
+ }
10
+
11
+ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } = {
12
+ items: {
13
+ fields: [
14
+ 'id',
15
+ 'kind',
16
+ 'repo',
17
+ 'title',
18
+ 'outcome',
19
+ 'acceptance',
20
+ 'fields',
21
+ 'labels',
22
+ 'foundInAttempt',
23
+ 'filedBy',
24
+ 'filedAt',
25
+ 'closedAt',
26
+ 'closeReason',
27
+ 'keptSession',
28
+ ],
29
+ json: ['acceptance', 'fields', 'labels', 'keptSession'],
30
+ flags: [],
31
+ },
32
+ relations: {
33
+ fields: ['source', 'target', 'type', 'addedBy', 'addedAt', 'removedAt'],
34
+ json: [],
35
+ flags: [],
36
+ },
37
+ levers: {
38
+ fields: ['seq', 'kind', 'item', 'until', 'expiresAt', 'reason', 'phase', 'actor', 'at'],
39
+ json: [],
40
+ flags: [],
41
+ },
42
+ attempts: {
43
+ fields: [
44
+ 'id',
45
+ 'item',
46
+ 'phase',
47
+ 'number',
48
+ 'runner',
49
+ 'recipeHash',
50
+ 'rulesVersion',
51
+ 'pool',
52
+ 'leaseGen',
53
+ 'spawnRequest',
54
+ 'spawnRequestHash',
55
+ 'state',
56
+ 'failure',
57
+ 'atcSession',
58
+ 'caller',
59
+ 'claimedAt',
60
+ 'startedAt',
61
+ 'endedAt',
62
+ 'fields',
63
+ 'mode',
64
+ 'round',
65
+ 'input',
66
+ 'holdGen',
67
+ 'closeOut',
68
+ 'closeOutHash',
69
+ 'closeOutFact',
70
+ 'closing',
71
+ 'closingAt',
72
+ 'suspendRequestedAt',
73
+ 'sleptAt',
74
+ 'wakeRequestedAt',
75
+ 'wokenAt',
76
+ 'outcome',
77
+ 'returnedOutcome',
78
+ 'strayedAt',
79
+ 'sessionStoppedAt',
80
+ 'sessionForgottenAt',
81
+ ],
82
+ json: ['spawnRequest', 'caller', 'fields', 'input', 'closeOut'],
83
+ flags: [],
84
+ },
85
+ attempt_sessions: {
86
+ fields: ['attempt', 'agentSession', 'atcSession', 'product', 'source', 'seenAt'],
87
+ json: [],
88
+ flags: [],
89
+ },
90
+ leases: {
91
+ fields: ['item', 'phase', 'attempt', 'gen', 'takenAt', 'releasedAt'],
92
+ json: [],
93
+ flags: [],
94
+ },
95
+ decisions: {
96
+ fields: [
97
+ 'id',
98
+ 'attempt',
99
+ 'item',
100
+ 'kind',
101
+ 'pool',
102
+ 'rulesVersion',
103
+ 'snapshotHash',
104
+ 'storeVersion',
105
+ 'pepysOffsets',
106
+ 'faults',
107
+ 'limits',
108
+ 'at',
109
+ ],
110
+ json: ['pepysOffsets', 'faults', 'limits'],
111
+ flags: [],
112
+ },
113
+ evidence: {
114
+ fields: ['factId', 'attempt', 'kind', 'ref', 'observed', 'source', 'at'],
115
+ json: [],
116
+ flags: [],
117
+ },
118
+ pull_requests: {
119
+ fields: [
120
+ 'repo',
121
+ 'number',
122
+ 'attempt',
123
+ 'githubId',
124
+ 'url',
125
+ 'state',
126
+ 'mergedAt',
127
+ 'mergeCommit',
128
+ 'checkedAt',
129
+ ],
130
+ json: [],
131
+ flags: [],
132
+ },
133
+ runners: {
134
+ fields: ['id', 'heartbeatAt', 'paused'],
135
+ json: [],
136
+ flags: ['paused'],
137
+ },
138
+ notes: {
139
+ fields: ['item', 'seq', 'text', 'by', 'at'],
140
+ json: [],
141
+ flags: [],
142
+ },
143
+ notices: {
144
+ fields: ['id', 'item', 'reason', 'at', 'tries', 'sentAt'],
145
+ json: [],
146
+ flags: [],
147
+ },
148
+ };
@@ -0,0 +1,3 @@
1
+ export function toColumnName(field: string): string {
2
+ return field.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
3
+ }
@@ -0,0 +1,44 @@
1
+ import { formatCanonicalJson } from '@boxxy/content-hash';
2
+ import { toJSONValue } from '@boxxy/model';
3
+ import type { TableName } from '@boxxy/model';
4
+ import invariant from 'tiny-invariant';
5
+ import { tableCodecs } from './table-codecs.ts';
6
+ import { toColumnName } from './to-column-name.ts';
7
+
8
+ export type StoreValue = string | number | null;
9
+
10
+ // a field outside the table's codec fails, so no caller's key reaches the SQL as a column name
11
+ export function toStoreColumns(table: TableName, fields: object): [string, StoreValue][] {
12
+ const codec = tableCodecs[table];
13
+
14
+ const known = new Set<string>(codec.fields);
15
+ const json = new Set<string>(codec.json);
16
+
17
+ return Object.entries(fields).map(([field, value]: [string, unknown]) => {
18
+ invariant(known.has(field), `${table} has no field ${field}`);
19
+
20
+ return [toColumnName(field), toStoreValue(value, json.has(field))];
21
+ });
22
+ }
23
+
24
+ function toStoreValue(value: unknown, isJSON: boolean): StoreValue {
25
+ if (value === null) {
26
+ return null;
27
+ }
28
+
29
+ if (isJSON) {
30
+ // an update compares the stored text, so two objects with their keys in another order match
31
+ return formatCanonicalJson(toJSONValue(value));
32
+ }
33
+
34
+ if (typeof value === 'boolean') {
35
+ return value ? 1 : 0;
36
+ }
37
+
38
+ invariant(
39
+ typeof value === 'string' || typeof value === 'number',
40
+ `a ${typeof value} needs a JSON column`,
41
+ );
42
+
43
+ return value;
44
+ }
@@ -0,0 +1 @@
1
+ export const lockWaitLimitMs = 5000;