wenmar-open 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/batch.d.ts +5 -0
- package/dist/batch.js +7 -0
- package/dist/client.d.ts +80 -0
- package/dist/client.js +200 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +37 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/offline/client.d.ts +70 -0
- package/dist/offline/client.js +245 -0
- package/dist/offline/engine.d.ts +53 -0
- package/dist/offline/engine.js +129 -0
- package/dist/offline/index.d.ts +10 -0
- package/dist/offline/index.js +4 -0
- package/dist/offline/node.d.ts +20 -0
- package/dist/offline/node.js +78 -0
- package/dist/offline/store.d.ts +72 -0
- package/dist/offline/store.js +156 -0
- package/dist/offline/wasm-inline.js +2 -0
- package/dist/offline/wenmar_open.wasm +0 -0
- package/dist/schema.d.ts +984 -0
- package/dist/schema.js +3 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +1 -0
- package/package.json +62 -6
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { WenmarOpenError } from "../errors.js";
|
|
2
|
+
import { WenmarOpenOffline } from "./client.js";
|
|
3
|
+
import { syncStore } from "./store.js";
|
|
4
|
+
/** Loaded by name at run time, so nothing is resolved when this file is read. */
|
|
5
|
+
async function load(specifier) {
|
|
6
|
+
return (await import(specifier));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Opens a data file with Node's own SQLite (Node 22.16 or later) and
|
|
10
|
+
* returns a client that reads it. The file is opened read-only and checked:
|
|
11
|
+
* a file of another schema version fails here, not at the first decode.
|
|
12
|
+
*
|
|
13
|
+
* On Node 20, open the file with `better-sqlite3` and give it to
|
|
14
|
+
* `syncStore` instead.
|
|
15
|
+
*/
|
|
16
|
+
export async function openOffline(options = {}) {
|
|
17
|
+
let path = options.path;
|
|
18
|
+
if (path === undefined) {
|
|
19
|
+
try {
|
|
20
|
+
path = (await load("wenmar-open-data")).path;
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new WenmarOpenError({
|
|
24
|
+
code: "no_data",
|
|
25
|
+
message: "There is no data file. Install the wenmar-open-data package, or pass { path }.",
|
|
26
|
+
cause,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let sqlite;
|
|
31
|
+
try {
|
|
32
|
+
sqlite = await load("node:sqlite");
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
throw new WenmarOpenError({
|
|
36
|
+
code: "store_error",
|
|
37
|
+
message: "node:sqlite is not available. It needs Node 22.16 or later; on older versions open the file with better-sqlite3 and pass it to syncStore.",
|
|
38
|
+
cause,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
let database;
|
|
42
|
+
try {
|
|
43
|
+
database = new sqlite.DatabaseSync(path, { readOnly: true });
|
|
44
|
+
}
|
|
45
|
+
catch (cause) {
|
|
46
|
+
throw new WenmarOpenError({
|
|
47
|
+
code: "no_data",
|
|
48
|
+
message: `The data file at ${path} could not be opened.`,
|
|
49
|
+
details: { path },
|
|
50
|
+
cause,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const store = { ...syncStore(database), close: () => database.close() };
|
|
54
|
+
const client = new WenmarOpenOffline({
|
|
55
|
+
store,
|
|
56
|
+
...(options.wasm === undefined ? {} : { wasm: options.wasm }),
|
|
57
|
+
...(options.currentYear === undefined ? {} : { currentYear: options.currentYear }),
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await client.meta();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
database.close();
|
|
64
|
+
// SQLite opens a file that is not a database without complaint and
|
|
65
|
+
// fails on the first read; for a file on disk that means the file is
|
|
66
|
+
// not a data file, not that a connection dropped.
|
|
67
|
+
if (error instanceof WenmarOpenError && error.code === "store_error" && error.cause !== undefined) {
|
|
68
|
+
throw new WenmarOpenError({
|
|
69
|
+
code: "data_invalid",
|
|
70
|
+
message: `The file at ${path} could not be read as a Wenmar Open data file.`,
|
|
71
|
+
details: { path },
|
|
72
|
+
cause: error.cause,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return client;
|
|
78
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** A value bound to a statement: text, a number or null. */
|
|
2
|
+
export type Param = string | number | null;
|
|
3
|
+
/** One `SELECT`, with what binds to `?1`, `?2` and so on, in order. */
|
|
4
|
+
export interface Statement {
|
|
5
|
+
readonly sql: string;
|
|
6
|
+
readonly params: readonly Param[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A database holding a Wenmar Open data file. It is asked to run a list of
|
|
10
|
+
* `SELECT`s and gives back the rows of each, in the same order. A row is an
|
|
11
|
+
* array with one value per selected column, in the order selected: never an
|
|
12
|
+
* object, because two columns may share a name. It may answer at once or
|
|
13
|
+
* with a promise.
|
|
14
|
+
*/
|
|
15
|
+
export interface Store {
|
|
16
|
+
query(statements: readonly Statement[]): unknown[][][] | Promise<unknown[][][]>;
|
|
17
|
+
/** Called by `close()` on the client. A store the caller opened leaves this out. */
|
|
18
|
+
close?(): void;
|
|
19
|
+
}
|
|
20
|
+
/** A prepared statement of `node:sqlite` or of `better-sqlite3`. */
|
|
21
|
+
export interface SyncStatement {
|
|
22
|
+
all(...params: (Param | Record<string, Param>)[]): unknown[];
|
|
23
|
+
/** `node:sqlite`, Node 22.16 and later. */
|
|
24
|
+
setReturnArrays?(enabled: boolean): void;
|
|
25
|
+
/** `better-sqlite3`. */
|
|
26
|
+
raw?(enabled?: boolean): unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A database of `node:sqlite` (`DatabaseSync`) or of `better-sqlite3`. What
|
|
30
|
+
* `prepare` returns is used as a `SyncStatement`; it is typed loosely here
|
|
31
|
+
* so that both libraries' own declarations fit.
|
|
32
|
+
*/
|
|
33
|
+
export interface SyncDatabase {
|
|
34
|
+
prepare(sql: string): object;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gives every bare `?` the number SQLite gives it (one more than the
|
|
38
|
+
* largest number so far), so a statement that mixes `?` and `?3` has only
|
|
39
|
+
* numbered parameters. Text in quotes and comments is left alone.
|
|
40
|
+
*/
|
|
41
|
+
export declare function numberParameters(sql: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* A store over a database the caller opened with `node:sqlite` or
|
|
44
|
+
* `better-sqlite3`. The caller closes it.
|
|
45
|
+
*
|
|
46
|
+
* The two bind differently. `node:sqlite` binds values to `?1`, `?2` by
|
|
47
|
+
* position. `better-sqlite3` reads `?1` as a parameter named `1`, which a
|
|
48
|
+
* positional value does not fill, so there the values are given as one
|
|
49
|
+
* object keyed by number.
|
|
50
|
+
*/
|
|
51
|
+
export declare function syncStore(database: SyncDatabase): Store;
|
|
52
|
+
/** What `d1Store` uses of a D1 prepared statement. */
|
|
53
|
+
export interface D1Statement {
|
|
54
|
+
bind(...values: Param[]): D1Statement;
|
|
55
|
+
raw(): Promise<unknown[][]>;
|
|
56
|
+
}
|
|
57
|
+
/** A Cloudflare D1 binding, or a session made with `withSession()`. */
|
|
58
|
+
export interface D1Like {
|
|
59
|
+
prepare(sql: string): D1Statement;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A store over a Cloudflare D1 database that holds an imported data file.
|
|
63
|
+
* The statements of one step are sent together and each counts as one D1
|
|
64
|
+
* query.
|
|
65
|
+
*/
|
|
66
|
+
export declare function d1Store(database: D1Like): Store;
|
|
67
|
+
/**
|
|
68
|
+
* The rows of one statement, as the decoder reads them: each value a
|
|
69
|
+
* number, text or null. A database set to return every integer as a BigInt
|
|
70
|
+
* is accepted. Bytes are not: the data file holds none.
|
|
71
|
+
*/
|
|
72
|
+
export declare function cleanRows(rows: unknown, sql: string): (string | number | null)[][];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { WenmarOpenError } from "../errors.js";
|
|
2
|
+
/** How many prepared statements a store keeps. */
|
|
3
|
+
const MOST_PREPARED = 256;
|
|
4
|
+
/**
|
|
5
|
+
* Gives every bare `?` the number SQLite gives it (one more than the
|
|
6
|
+
* largest number so far), so a statement that mixes `?` and `?3` has only
|
|
7
|
+
* numbered parameters. Text in quotes and comments is left alone.
|
|
8
|
+
*/
|
|
9
|
+
export function numberParameters(sql) {
|
|
10
|
+
let out = "";
|
|
11
|
+
let largest = 0;
|
|
12
|
+
let index = 0;
|
|
13
|
+
while (index < sql.length) {
|
|
14
|
+
const char = sql.charAt(index);
|
|
15
|
+
const pair = sql.slice(index, index + 2);
|
|
16
|
+
let end = -1;
|
|
17
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
18
|
+
end = sql.indexOf(char, index + 1);
|
|
19
|
+
while (end !== -1 && sql.charAt(end + 1) === char)
|
|
20
|
+
end = sql.indexOf(char, end + 2);
|
|
21
|
+
end = end === -1 ? sql.length : end + 1;
|
|
22
|
+
}
|
|
23
|
+
else if (pair === "--") {
|
|
24
|
+
end = sql.indexOf("\n", index);
|
|
25
|
+
end = end === -1 ? sql.length : end;
|
|
26
|
+
}
|
|
27
|
+
else if (pair === "/*") {
|
|
28
|
+
end = sql.indexOf("*/", index + 2);
|
|
29
|
+
end = end === -1 ? sql.length : end + 2;
|
|
30
|
+
}
|
|
31
|
+
if (end !== -1) {
|
|
32
|
+
out += sql.slice(index, end);
|
|
33
|
+
index = end;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === "?") {
|
|
37
|
+
const digits = /^\d*/.exec(sql.slice(index + 1))?.[0] ?? "";
|
|
38
|
+
if (digits === "") {
|
|
39
|
+
largest += 1;
|
|
40
|
+
out += `?${largest}`;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
largest = Math.max(largest, Number(digits));
|
|
44
|
+
out += `?${digits}`;
|
|
45
|
+
}
|
|
46
|
+
index += 1 + digits.length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out += char;
|
|
50
|
+
index += 1;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A store over a database the caller opened with `node:sqlite` or
|
|
56
|
+
* `better-sqlite3`. The caller closes it.
|
|
57
|
+
*
|
|
58
|
+
* The two bind differently. `node:sqlite` binds values to `?1`, `?2` by
|
|
59
|
+
* position. `better-sqlite3` reads `?1` as a parameter named `1`, which a
|
|
60
|
+
* positional value does not fill, so there the values are given as one
|
|
61
|
+
* object keyed by number.
|
|
62
|
+
*/
|
|
63
|
+
export function syncStore(database) {
|
|
64
|
+
const prepared = new Map();
|
|
65
|
+
const prepare = (sql) => {
|
|
66
|
+
const kept = prepared.get(sql);
|
|
67
|
+
if (kept !== undefined)
|
|
68
|
+
return kept;
|
|
69
|
+
let run;
|
|
70
|
+
const probe = database.prepare(sql);
|
|
71
|
+
if (typeof probe.setReturnArrays === "function") {
|
|
72
|
+
probe.setReturnArrays(true);
|
|
73
|
+
run = (params) => probe.all(...params);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const statement = typeof probe.raw === "function" ? database.prepare(numberParameters(sql)) : probe;
|
|
77
|
+
if (typeof statement.raw !== "function") {
|
|
78
|
+
throw new WenmarOpenError({
|
|
79
|
+
code: "store_error",
|
|
80
|
+
message: "This database cannot return rows as arrays. Use node:sqlite from Node 22.16 or later, or better-sqlite3.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
statement.raw(true);
|
|
84
|
+
run = (params) => (params.length === 0
|
|
85
|
+
? statement.all()
|
|
86
|
+
: statement.all(Object.fromEntries(params.map((value, index) => [index + 1, value]))));
|
|
87
|
+
}
|
|
88
|
+
if (prepared.size >= MOST_PREPARED)
|
|
89
|
+
prepared.clear();
|
|
90
|
+
prepared.set(sql, run);
|
|
91
|
+
return run;
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
query: (statements) => statements.map(({ sql, params }) => prepare(sql)(params)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A store over a Cloudflare D1 database that holds an imported data file.
|
|
99
|
+
* The statements of one step are sent together and each counts as one D1
|
|
100
|
+
* query.
|
|
101
|
+
*/
|
|
102
|
+
export function d1Store(database) {
|
|
103
|
+
return {
|
|
104
|
+
query: (statements) => Promise.all(statements.map(({ sql, params }) => database.prepare(sql).bind(...params).raw())),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function invalid(message, details) {
|
|
108
|
+
return new WenmarOpenError({ code: "data_invalid", message, details });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The rows of one statement, as the decoder reads them: each value a
|
|
112
|
+
* number, text or null. A database set to return every integer as a BigInt
|
|
113
|
+
* is accepted. Bytes are not: the data file holds none.
|
|
114
|
+
*/
|
|
115
|
+
export function cleanRows(rows, sql) {
|
|
116
|
+
if (!Array.isArray(rows)) {
|
|
117
|
+
throw new WenmarOpenError({
|
|
118
|
+
code: "store_error",
|
|
119
|
+
message: "The store did not return a list of rows for a statement.",
|
|
120
|
+
details: { sql },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return rows.map((row) => {
|
|
124
|
+
if (!Array.isArray(row)) {
|
|
125
|
+
throw new WenmarOpenError({
|
|
126
|
+
code: "store_error",
|
|
127
|
+
message: "The store must return each row as an array of values, in the order selected.",
|
|
128
|
+
details: { sql },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return row.map((value, column) => {
|
|
132
|
+
if (value === null || value === undefined)
|
|
133
|
+
return null;
|
|
134
|
+
if (typeof value === "string")
|
|
135
|
+
return value;
|
|
136
|
+
if (typeof value === "number") {
|
|
137
|
+
if (Number.isFinite(value))
|
|
138
|
+
return value;
|
|
139
|
+
throw invalid("The database returned a number that is not finite.", { sql, column });
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === "bigint") {
|
|
142
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
143
|
+
return Number(value);
|
|
144
|
+
}
|
|
145
|
+
throw invalid("The database returned a whole number too large to read exactly.", { sql, column });
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === "boolean")
|
|
148
|
+
return value ? 1 : 0;
|
|
149
|
+
throw invalid("The database returned a value that is not a number, text or null.", {
|
|
150
|
+
sql,
|
|
151
|
+
column,
|
|
152
|
+
kind: ArrayBuffer.isView(value) || value instanceof ArrayBuffer ? "bytes" : typeof value,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|