wenmar-open 0.1.0 → 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/README.md +136 -0
- package/dist/batch.d.ts +5 -0
- package/dist/batch.js +7 -0
- package/dist/client.d.ts +1 -5
- package/dist/client.js +1 -6
- package/dist/errors.d.ts +12 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- 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 +1 -1
- package/package.json +23 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { WenmarOpenError } from "../errors.js";
|
|
2
|
+
const EXPORTS = ["memory", "wo_alloc", "wo_call", "wo_result", "wo_result_len"];
|
|
3
|
+
function bytesOf(base64) {
|
|
4
|
+
const binary = atob(base64);
|
|
5
|
+
const bytes = new Uint8Array(binary.length);
|
|
6
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
7
|
+
bytes[index] = binary.charCodeAt(index);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compiles the decoder. With no source it is the copy inside the package,
|
|
12
|
+
* which is read as text and never fetched, so nothing here needs a file
|
|
13
|
+
* system or a network.
|
|
14
|
+
*/
|
|
15
|
+
export async function compile(source) {
|
|
16
|
+
if (source instanceof WebAssembly.Module)
|
|
17
|
+
return source;
|
|
18
|
+
try {
|
|
19
|
+
const bytes = source ?? bytesOf((await import("./wasm-inline.js")).WASM_BASE64);
|
|
20
|
+
return await WebAssembly.compile(bytes);
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new WenmarOpenError({
|
|
24
|
+
code: "internal_error",
|
|
25
|
+
message: "The decoder could not be loaded. Where bytes may not be compiled, as in Cloudflare Workers, pass { wasm } from: import wasm from \"wenmar-open/offline.wasm\".",
|
|
26
|
+
cause,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** One running copy of the decoder. */
|
|
31
|
+
export class Engine {
|
|
32
|
+
#exports;
|
|
33
|
+
#stopped = false;
|
|
34
|
+
constructor(exports) {
|
|
35
|
+
this.#exports = exports;
|
|
36
|
+
}
|
|
37
|
+
static async start(module) {
|
|
38
|
+
let instance;
|
|
39
|
+
try {
|
|
40
|
+
instance = await WebAssembly.instantiate(module, {});
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw new WenmarOpenError({
|
|
44
|
+
code: "internal_error",
|
|
45
|
+
message: "The decoder could not be started: the wasm given is not the one of this package.",
|
|
46
|
+
cause,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const exports = instance.exports;
|
|
50
|
+
for (const name of EXPORTS) {
|
|
51
|
+
if (exports[name] === undefined) {
|
|
52
|
+
throw new WenmarOpenError({
|
|
53
|
+
code: "internal_error",
|
|
54
|
+
message: "The decoder could not be started: the wasm given is not the one of this package.",
|
|
55
|
+
details: { missing: name },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return new Engine(exports);
|
|
60
|
+
}
|
|
61
|
+
/** Whether a call stopped this copy. A stopped copy is never used again. */
|
|
62
|
+
get stopped() {
|
|
63
|
+
return this.#stopped;
|
|
64
|
+
}
|
|
65
|
+
#read() {
|
|
66
|
+
const { memory, wo_result, wo_result_len } = this.#exports;
|
|
67
|
+
// The memory may have grown during the call, so it is looked at afresh.
|
|
68
|
+
return new TextDecoder().decode(new Uint8Array(memory.buffer, wo_result(), wo_result_len()));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One request, one answer. A request that cannot be written as JSON is
|
|
72
|
+
* refused before the decoder sees it. A stop inside the decoder is thrown
|
|
73
|
+
* as an error, and this copy is not used again.
|
|
74
|
+
*/
|
|
75
|
+
call(request) {
|
|
76
|
+
if (this.#stopped) {
|
|
77
|
+
throw new WenmarOpenError({ code: "internal_error", message: "The decoder has stopped." });
|
|
78
|
+
}
|
|
79
|
+
let input;
|
|
80
|
+
try {
|
|
81
|
+
input = new TextEncoder().encode(JSON.stringify(request));
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
throw new WenmarOpenError({
|
|
85
|
+
code: "validation_failed",
|
|
86
|
+
message: "The question could not be written as JSON.",
|
|
87
|
+
cause,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const { memory, wo_alloc, wo_call } = this.#exports;
|
|
91
|
+
try {
|
|
92
|
+
const pointer = wo_alloc(input.length);
|
|
93
|
+
new Uint8Array(memory.buffer, pointer, input.length).set(input);
|
|
94
|
+
wo_call(pointer, input.length);
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
// WebAssembly cannot unwind, so a failure inside it ends the call
|
|
98
|
+
// here. What it was is left where the answer would have been.
|
|
99
|
+
this.#stopped = true;
|
|
100
|
+
let panic;
|
|
101
|
+
try {
|
|
102
|
+
panic = this.#read();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
panic = undefined;
|
|
106
|
+
}
|
|
107
|
+
// An answer is always a JSON object and a panic's message never is:
|
|
108
|
+
// text that starts as an object is an answer left from before.
|
|
109
|
+
if (panic === "" || panic?.startsWith("{"))
|
|
110
|
+
panic = undefined;
|
|
111
|
+
throw new WenmarOpenError({
|
|
112
|
+
code: "internal_error",
|
|
113
|
+
message: "The decoder stopped unexpectedly. Please report the VIN or the question that caused it.",
|
|
114
|
+
details: panic === undefined ? {} : { panic },
|
|
115
|
+
cause,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(this.#read());
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
throw new WenmarOpenError({
|
|
123
|
+
code: "internal_error",
|
|
124
|
+
message: "The decoder gave an answer that could not be read.",
|
|
125
|
+
cause,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { MAX_BATCH, isBatchError } from "../batch.js";
|
|
2
|
+
export { WenmarOpenError } from "../errors.js";
|
|
3
|
+
export type { ApiErrorCode, ClientErrorCode, ErrorCode, OfflineErrorCode, WenmarOpenErrorInit, } from "../errors.js";
|
|
4
|
+
export type { DecodeOptions, RequestOptions } from "../client.js";
|
|
5
|
+
export type { BatchItem, CheckDigit, Engine, EngineOption, EnginesQuery, Entry, ErrorBody, Make, MakesQuery, Manufacturer, Meta, Model, ModelsQuery, Plant, Safety, SearchQuery, Selection, Submodel, SubmodelsQuery, VinDecode, Warning, YearsQuery, } from "../types.js";
|
|
6
|
+
export { MAX_STEPS, WenmarOpenOffline } from "./client.js";
|
|
7
|
+
export type { OfflineOptions } from "./client.js";
|
|
8
|
+
export type { CompiledWasm, WasmSource } from "./engine.js";
|
|
9
|
+
export { d1Store, syncStore } from "./store.js";
|
|
10
|
+
export type { D1Like, D1Statement, Param, Statement, Store, SyncDatabase, SyncStatement } from "./store.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WenmarOpenOffline } from "./client.js";
|
|
2
|
+
import type { WasmSource } from "./engine.js";
|
|
3
|
+
export interface NodeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The data file. Default: the one in the `wenmar-open-data` package,
|
|
6
|
+
* which must then be installed.
|
|
7
|
+
*/
|
|
8
|
+
path?: string;
|
|
9
|
+
wasm?: WasmSource;
|
|
10
|
+
currentYear?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Opens a data file with Node's own SQLite (Node 22.16 or later) and
|
|
14
|
+
* returns a client that reads it. The file is opened read-only and checked:
|
|
15
|
+
* a file of another schema version fails here, not at the first decode.
|
|
16
|
+
*
|
|
17
|
+
* On Node 20, open the file with `better-sqlite3` and give it to
|
|
18
|
+
* `syncStore` instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function openOffline(options?: NodeOptions): Promise<WenmarOpenOffline>;
|
|
@@ -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
|
+
}
|