@ix-xs/node-comfort 2.1.0 → 2.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/CHANGELOG.md +153 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +369 -59
- package/src/Func.js +1 -1
- package/src/Http.js +266 -75
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +3 -2
- package/src/Obj.js +225 -36
- package/src/SQLite.js +155 -98
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +124 -23
- package/src/Time.js +98 -24
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +30 -23
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +1 -1
- package/types/src/Http.d.ts +45 -8
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +105 -13
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +43 -10
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +12 -7
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- package/types/src/internal/terminal.d.ts.map +1 -0
package/src/SQLite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SQLite made pleasant, on the engine built into Node.js
|
|
4
|
+
* SQLite made pleasant, on the engine built into Node.js: CRUD with
|
|
5
5
|
* rich filters, JSON and boolean columns, transactions, migrations and
|
|
6
6
|
* backups. Methods are synchronous, like the engine, but `await` still
|
|
7
7
|
* works. Errors are `SQLiteError`s. Values are always bound as parameters
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* });
|
|
20
20
|
* db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
21
21
|
* db.getAll("users", { email: { like: "%@example.com" } }, { orderBy: "email", limit: 20 });
|
|
22
|
+
*
|
|
23
|
+
* // Values from a request can't turn into operators:
|
|
24
|
+
* const safe = new nc.SQLite("data/app.sqlite", { operators: "tagged" });
|
|
25
|
+
* safe.getAll("users", { age: nc.SQLite.op.gte(18), role: nc.SQLite.op.in(["admin", "editor"]) });
|
|
22
26
|
*/
|
|
23
27
|
|
|
24
28
|
const nodePath = require("node:path");
|
|
@@ -48,14 +52,15 @@ const { toMs } = require("./internal/shared.js");
|
|
|
48
52
|
* @property {unknown} [defaultValue] Objects work for JSON columns, booleans for BOOLEAN ones. `"CURRENT_TIMESTAMP"`, `"CURRENT_DATE"` and `"CURRENT_TIME"` mean the moment of the insert.
|
|
49
53
|
* @property {string} [defaultExpression] A default computed by SQLite, like `"(unixepoch())"` or `"(lower(hex(randomblob(16))))"`. Written into the table as is: never build it from user input.
|
|
50
54
|
* @property {unknown[]} [values] Allowed values.
|
|
51
|
-
* @property {string} [check] A `CHECK` expression, like `"price >= 0"`.
|
|
55
|
+
* @property {string} [check] A `CHECK` expression, like `"price >= 0"`. Written into the table as is: never build it from user input.
|
|
52
56
|
* @property {{ table: string, column: string }} [references] Foreign key target.
|
|
53
57
|
* @property {ForeignKeyAction} [onDelete] When the referenced row is deleted.
|
|
54
58
|
* @property {ForeignKeyAction} [onUpdate] When the referenced key changes.
|
|
55
59
|
*/
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
* A constraint on several columns.
|
|
62
|
+
* A constraint on several columns. The `expression` of a `check` is written
|
|
63
|
+
* into the table as is: never build it from user input.
|
|
59
64
|
* @typedef {{ type: "unique", columns: string[] } | { type: "primaryKey", columns: string[] } | { type: "check", expression: string } | { type: "foreignKey", columns: string[], references: { table: string, columns: string[] }, onDelete?: ForeignKeyAction, onUpdate?: ForeignKeyAction }} TableConstraint
|
|
60
65
|
*/
|
|
61
66
|
|
|
@@ -65,7 +70,7 @@ const { toMs } = require("./internal/shared.js");
|
|
|
65
70
|
* @property {string[]} columns Several columns make a composite index.
|
|
66
71
|
* @property {boolean} [unique]
|
|
67
72
|
* @property {string} [name] Defaults to `idx_<table>_<columns>`.
|
|
68
|
-
* @property {string} [where] Makes it a partial index, like `"deleted_at IS NULL"`.
|
|
73
|
+
* @property {string} [where] Makes it a partial index, like `"deleted_at IS NULL"`. Written into the index as is: never build it from user input.
|
|
69
74
|
*/
|
|
70
75
|
|
|
71
76
|
/**
|
|
@@ -101,6 +106,13 @@ const { toMs } = require("./internal/shared.js");
|
|
|
101
106
|
* `IS NULL`, an array means "one of", and an object uses operators. Keys
|
|
102
107
|
* combine with AND; use `$or` and `$and` for the rest.
|
|
103
108
|
*
|
|
109
|
+
* A value taken from a request must be checked first (parsed JSON like
|
|
110
|
+
* `{ "ne": "" }` would act as an operator), or the database opened with
|
|
111
|
+
* `operators: "tagged"`, where only `SQLite.op` makes operators.
|
|
112
|
+
*
|
|
113
|
+
* On a read, a missing value drops its condition: check required values
|
|
114
|
+
* first (writes, and reads with `operators: "tagged"`, throw instead).
|
|
115
|
+
*
|
|
104
116
|
* A key with dots looks inside a JSON column: `"settings.theme"` is the
|
|
105
117
|
* `theme` key of the `settings` column, `"tags.0"` the first item of `tags`.
|
|
106
118
|
*
|
|
@@ -108,6 +120,7 @@ const { toMs } = require("./internal/shared.js");
|
|
|
108
120
|
* { status: "active", age: { gte: 18, lt: 65 }, role: ["admin", "editor"], deleted_at: null }
|
|
109
121
|
* { $or: [{ email: { like: "%@acme.com" } }, { vip: true }] }
|
|
110
122
|
* { "settings.theme": "dark", "settings.beta": true }
|
|
123
|
+
* { age: nc.SQLite.op.gte(18), role: nc.SQLite.op.in(["admin", "editor"]) }
|
|
111
124
|
*
|
|
112
125
|
* @template [R=Record<string, any>] Row type; its keys are suggested as columns.
|
|
113
126
|
* @typedef {{ [K in keyof R]?: WhereValue } & { [path: `${string}.${string}`]: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
|
|
@@ -115,7 +128,33 @@ const { toMs } = require("./internal/shared.js");
|
|
|
115
128
|
|
|
116
129
|
/**
|
|
117
130
|
* The condition for one column: a value, `null`, an array, or operators.
|
|
118
|
-
* @typedef {null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators} WhereValue
|
|
131
|
+
* @typedef {null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators | TaggedOperator} WhereValue
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* An operator made by `SQLite.op`. It carries a mark that JSON can't
|
|
136
|
+
* produce, so it's an operator with `operators: "tagged"` too. Several can
|
|
137
|
+
* be spread into one object: `{ ...op.gte(18), ...op.lt(65) }`.
|
|
138
|
+
* @typedef {Readonly<WhereOperators>} TaggedOperator
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The helpers of `SQLite.op`, one per filter operator.
|
|
143
|
+
* @typedef {object} OperatorHelpers
|
|
144
|
+
* @property {(value: unknown) => TaggedOperator} eq Equal. `null` means `IS NULL`.
|
|
145
|
+
* @property {(value: unknown) => TaggedOperator} ne Not equal. Rows where the column is `NULL` match too.
|
|
146
|
+
* @property {(value: number | string | bigint | Date) => TaggedOperator} gt Greater than.
|
|
147
|
+
* @property {(value: number | string | bigint | Date) => TaggedOperator} gte Greater than or equal.
|
|
148
|
+
* @property {(value: number | string | bigint | Date) => TaggedOperator} lt Less than.
|
|
149
|
+
* @property {(value: number | string | bigint | Date) => TaggedOperator} lte Less than or equal.
|
|
150
|
+
* @property {(values: readonly unknown[]) => TaggedOperator} in One of these values.
|
|
151
|
+
* @property {(values: readonly unknown[]) => TaggedOperator} notIn None of these values.
|
|
152
|
+
* @property {(pattern: string) => TaggedOperator} like A `LIKE` pattern: `%` any text, `_` one character.
|
|
153
|
+
* @property {(pattern: string) => TaggedOperator} notLike
|
|
154
|
+
* @property {(pattern: string) => TaggedOperator} glob A case-sensitive pattern: `*` any text, `?` one character.
|
|
155
|
+
* @property {(min: unknown, max: unknown) => TaggedOperator} between From `min` to `max`, both included.
|
|
156
|
+
* @property {() => TaggedOperator} isNull `IS NULL`.
|
|
157
|
+
* @property {() => TaggedOperator} notNull `IS NOT NULL`.
|
|
119
158
|
*/
|
|
120
159
|
|
|
121
160
|
/**
|
|
@@ -138,12 +177,13 @@ const { toMs } = require("./internal/shared.js");
|
|
|
138
177
|
* @typedef {object} SQLiteOptions
|
|
139
178
|
* @property {boolean} [json] Parse `JSON` columns. Defaults to `true`.
|
|
140
179
|
* @property {boolean} [booleans] Return `BOOLEAN` columns as `true`/`false`. Defaults to `true`.
|
|
141
|
-
* @property {boolean} [dates] Return `DATE`, `DATETIME` and `TIMESTAMP` columns as `Date`s.
|
|
180
|
+
* @property {boolean} [dates] Return `DATE`, `DATETIME` and `TIMESTAMP` columns as `Date`s. Defaults to `false`.
|
|
142
181
|
* @property {boolean} [wal] Write-ahead logging: faster, and reads don't wait for writes. Defaults to `true`.
|
|
143
182
|
* @property {boolean} [foreignKeys] Enforce foreign keys. Defaults to `true`.
|
|
144
183
|
* @property {number | string} [busyTimeout] How long to wait for a lock held by another connection, in ms or like `"5s"`. Defaults to 5 seconds.
|
|
145
184
|
* @property {"OFF" | "NORMAL" | "FULL" | "EXTRA"} [synchronous] How hard SQLite pushes each commit to the disk. With `wal`, `"NORMAL"` writes several times faster and can't corrupt the file; a power cut may undo the last commits. Defaults to SQLite's own setting, `"FULL"`.
|
|
146
185
|
* @property {boolean} [readOnly]
|
|
186
|
+
* @property {"plain" | "tagged"} [operators] What makes an object in a filter an operator. `"plain"`, the default: any object with an operator key, like `{ gte: 18 }`, and an array means "one of". `"tagged"`: only the operators made by `SQLite.op`, any other object or array is a value compared as JSON text, so a value taken from a request can never become an operator.
|
|
147
187
|
*/
|
|
148
188
|
|
|
149
189
|
/**
|
|
@@ -215,7 +255,7 @@ const { toMs } = require("./internal/shared.js");
|
|
|
215
255
|
|
|
216
256
|
/**
|
|
217
257
|
* A prepared query and what's needed to turn its rows into objects.
|
|
218
|
-
* @typedef {{ stmt: import("node:sqlite").StatementSync, names: string[]
|
|
258
|
+
* @typedef {{ stmt: import("node:sqlite").StatementSync, names: string[], kinds: Uint8Array | undefined }} _Reader
|
|
219
259
|
*/
|
|
220
260
|
|
|
221
261
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -229,6 +269,8 @@ const SOURCE_EXISTS = EXISTS("'table', 'view'");
|
|
|
229
269
|
const JSON_KIND = 1;
|
|
230
270
|
const BOOLEAN_KIND = 2;
|
|
231
271
|
const DATE_KIND = 3;
|
|
272
|
+
// Marks the operators made by SQLite.op. JSON can't produce a symbol key, so a parsed request never carries it.
|
|
273
|
+
const TAGGED = Symbol("nc.SQLite.op");
|
|
232
274
|
const OPERATORS = new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike", "glob", "between", "isNull"]);
|
|
233
275
|
const COMPARATORS = /** @type {Record<string, string>} */ ({ gt: ">", gte: ">=", lt: "<", lte: "<=", like: "LIKE", notLike: "NOT LIKE", glob: "GLOB" });
|
|
234
276
|
const ACTIONS = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
|
|
@@ -268,7 +310,7 @@ const _engine = () => {
|
|
|
268
310
|
try {
|
|
269
311
|
_DatabaseSync = require("node:sqlite").DatabaseSync;
|
|
270
312
|
} catch (error) {
|
|
271
|
-
throw new SQLiteError(`node:sqlite is not available in Node.js ${process.version}: nc.SQLite requires Node.js 22.
|
|
313
|
+
throw new SQLiteError(`node:sqlite is not available in Node.js ${process.version}: nc.SQLite requires Node.js 22.23.4, 24.22.1, 26.12.1 or newer`, { cause: error });
|
|
272
314
|
} finally {
|
|
273
315
|
process.emitWarning = original;
|
|
274
316
|
}
|
|
@@ -407,6 +449,31 @@ const _insertVerb = (options) => {
|
|
|
407
449
|
return mode === "ignore" ? "INSERT OR IGNORE" : "INSERT OR REPLACE";
|
|
408
450
|
};
|
|
409
451
|
|
|
452
|
+
/**
|
|
453
|
+
* @param {string} name
|
|
454
|
+
* @param {unknown} operand
|
|
455
|
+
* @returns {TaggedOperator}
|
|
456
|
+
*/
|
|
457
|
+
const _tag = (name, operand) => /** @type {TaggedOperator} */ (Object.freeze({ [name]: operand, [TAGGED]: true }));
|
|
458
|
+
|
|
459
|
+
/** @type {OperatorHelpers} */
|
|
460
|
+
const OP = Object.freeze({
|
|
461
|
+
eq: (value) => _tag("eq", value),
|
|
462
|
+
ne: (value) => _tag("ne", value),
|
|
463
|
+
gt: (value) => _tag("gt", value),
|
|
464
|
+
gte: (value) => _tag("gte", value),
|
|
465
|
+
lt: (value) => _tag("lt", value),
|
|
466
|
+
lte: (value) => _tag("lte", value),
|
|
467
|
+
in: (values) => _tag("in", values),
|
|
468
|
+
notIn: (values) => _tag("notIn", values),
|
|
469
|
+
like: (pattern) => _tag("like", pattern),
|
|
470
|
+
notLike: (pattern) => _tag("notLike", pattern),
|
|
471
|
+
glob: (pattern) => _tag("glob", pattern),
|
|
472
|
+
between: (min, max) => _tag("between", [min, max]),
|
|
473
|
+
isNull: () => _tag("isNull", true),
|
|
474
|
+
notNull: () => _tag("isNull", false),
|
|
475
|
+
});
|
|
476
|
+
|
|
410
477
|
/**
|
|
411
478
|
* Removes quoted strings and names, to look for `;` safely.
|
|
412
479
|
* @param {string} sql
|
|
@@ -420,6 +487,23 @@ const _stripQuoted = (sql) => sql.replace(/'(?:''|[^'])*'|"(?:""|[^"])*"/g, "");
|
|
|
420
487
|
* @template [R=Record<string, any>] The row type read methods return by default. Each call can override it.
|
|
421
488
|
*/
|
|
422
489
|
class SQLite {
|
|
490
|
+
/**
|
|
491
|
+
* Filter operators that only code can make. Plain objects like
|
|
492
|
+
* `{ gte: 18 }` still work, unless the database was opened with
|
|
493
|
+
* `operators: "tagged"`: then only these are operators, and a value
|
|
494
|
+
* taken from a request stays a value.
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* const { op } = nc.SQLite;
|
|
498
|
+
* const db = new nc.SQLite("data/app.sqlite", { operators: "tagged" });
|
|
499
|
+
* db.getAll("users", { age: op.between(18, 65), role: op.in(["admin", "editor"]), deleted_at: op.isNull() });
|
|
500
|
+
* // { "ne": "" } in the body is compared as text, not run, and a missing token throws
|
|
501
|
+
* db.get("users", { email, token: body.token });
|
|
502
|
+
*
|
|
503
|
+
* @type {OperatorHelpers}
|
|
504
|
+
*/
|
|
505
|
+
static op = OP;
|
|
506
|
+
|
|
423
507
|
/** @type {import("node:sqlite").DatabaseSync} */
|
|
424
508
|
#db;
|
|
425
509
|
/** @type {Map<string, import("node:sqlite").StatementSync>} */
|
|
@@ -429,14 +513,12 @@ class SQLite {
|
|
|
429
513
|
// Tables and views known to exist, so that each call doesn't ask SQLite again.
|
|
430
514
|
/** @type {Set<string>} */
|
|
431
515
|
#known = new Set();
|
|
432
|
-
/** @type {Map<string, Map<string, number>>} */
|
|
433
|
-
#columnKinds = new Map();
|
|
434
|
-
/** @type {Map<string, string | undefined>} */
|
|
435
|
-
#sources = new Map();
|
|
436
516
|
/** @type {{ json: boolean, booleans: boolean, dates: boolean }} */
|
|
437
517
|
#read;
|
|
438
518
|
#depth = 0;
|
|
439
519
|
#open = true;
|
|
520
|
+
// With operators: "tagged", only SQLite.op makes operators.
|
|
521
|
+
#tagged = false;
|
|
440
522
|
/** @type {string} */
|
|
441
523
|
#path;
|
|
442
524
|
|
|
@@ -454,6 +536,9 @@ class SQLite {
|
|
|
454
536
|
* @throws {SQLiteError} If the file can't be opened.
|
|
455
537
|
*/
|
|
456
538
|
constructor(path = "db.sqlite", options = {}) {
|
|
539
|
+
const operators = options.operators ?? "plain";
|
|
540
|
+
if (operators !== "plain" && operators !== "tagged") throw new SQLiteError(`Invalid operators "${String(operators)}" (use "plain" or "tagged")`);
|
|
541
|
+
this.#tagged = operators === "tagged";
|
|
457
542
|
const DatabaseSync = _engine();
|
|
458
543
|
const memory = path === ":memory:" || path === "";
|
|
459
544
|
const target = memory ? ":memory:" : nodePath.resolve(process.cwd(), path);
|
|
@@ -494,9 +579,9 @@ class SQLite {
|
|
|
494
579
|
}
|
|
495
580
|
|
|
496
581
|
/**
|
|
497
|
-
* A prepared query.
|
|
498
|
-
*
|
|
499
|
-
*
|
|
582
|
+
* A prepared query. Rows are fetched as arrays and turned into objects here:
|
|
583
|
+
* it's faster than copying the objects Node returns, and Node says which
|
|
584
|
+
* column each value comes from.
|
|
500
585
|
* @param {string} sql
|
|
501
586
|
* @param {boolean} [keep=true] `false` for a statement used once, like an iteration.
|
|
502
587
|
* @returns {_Reader}
|
|
@@ -510,14 +595,9 @@ class SQLite {
|
|
|
510
595
|
} catch (error) {
|
|
511
596
|
throw _wrap(error, sql);
|
|
512
597
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
modern.setReturnArrays(true);
|
|
517
|
-
this.#describe(reader);
|
|
518
|
-
} else {
|
|
519
|
-
reader.table = this.#sourceTable(sql);
|
|
520
|
-
}
|
|
598
|
+
stmt.setReturnArrays(true);
|
|
599
|
+
reader = { stmt, names: [], kinds: undefined };
|
|
600
|
+
this.#describe(reader);
|
|
521
601
|
if (keep) {
|
|
522
602
|
if (this.#readers.size >= 500) this.#readers.delete(/** @type {string} */ (this.#readers.keys().next().value));
|
|
523
603
|
this.#readers.set(sql, reader);
|
|
@@ -530,8 +610,7 @@ class SQLite {
|
|
|
530
610
|
* @param {_Reader} reader
|
|
531
611
|
*/
|
|
532
612
|
#describe(reader) {
|
|
533
|
-
|
|
534
|
-
const columns = /** @type {any} */ (reader.stmt).columns();
|
|
613
|
+
const columns = reader.stmt.columns();
|
|
535
614
|
reader.names = columns.map((column) => column.name);
|
|
536
615
|
reader.kinds = undefined;
|
|
537
616
|
for (let i = 0; i < columns.length; i++) {
|
|
@@ -549,16 +628,6 @@ class SQLite {
|
|
|
549
628
|
#row(reader, raw) {
|
|
550
629
|
/** @type {Record<string, unknown>} */
|
|
551
630
|
const row = {};
|
|
552
|
-
if (reader.names === undefined) {
|
|
553
|
-
// Node before 22.16: a row is an object without a prototype, copied into a plain one.
|
|
554
|
-
for (const key in raw) row[key] = raw[key];
|
|
555
|
-
if (reader.table === undefined) return row;
|
|
556
|
-
for (const [key, kind] of this.#kinds(reader.table)) {
|
|
557
|
-
const value = row[key];
|
|
558
|
-
if (value !== null && value !== undefined) row[key] = _convert(kind, value);
|
|
559
|
-
}
|
|
560
|
-
return row;
|
|
561
|
-
}
|
|
562
631
|
// A statement prepared before the table changed can come back with other columns.
|
|
563
632
|
if (raw.length !== reader.names.length) this.#describe(reader);
|
|
564
633
|
const names = reader.names;
|
|
@@ -621,71 +690,79 @@ class SQLite {
|
|
|
621
690
|
* @returns {void}
|
|
622
691
|
*/
|
|
623
692
|
#invalidate() {
|
|
624
|
-
this.#columnKinds.clear();
|
|
625
693
|
this.#statements.clear();
|
|
626
694
|
this.#readers.clear();
|
|
627
695
|
this.#known.clear();
|
|
628
|
-
this.#sources.clear();
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* The columns of a table that are converted when read. Only used on Node
|
|
633
|
-
* versions that don't describe the columns of a query.
|
|
634
|
-
* @param {string} table
|
|
635
|
-
* @returns {Map<string, number>}
|
|
636
|
-
*/
|
|
637
|
-
#kinds(table) {
|
|
638
|
-
let kinds = this.#columnKinds.get(table);
|
|
639
|
-
if (!kinds) {
|
|
640
|
-
kinds = new Map();
|
|
641
|
-
for (const col of /** @type {Array<{ name: string, type: string }>} */ (/** @type {unknown} */ (this.#prepare(`PRAGMA table_info(${_id(table, "table name")})`).all()))) {
|
|
642
|
-
const kind = _kindOf(String(col.type).toUpperCase(), this.#read);
|
|
643
|
-
if (kind) kinds.set(col.name, kind);
|
|
644
|
-
}
|
|
645
|
-
this.#columnKinds.set(table, kinds);
|
|
646
|
-
}
|
|
647
|
-
return kinds;
|
|
648
696
|
}
|
|
649
697
|
|
|
650
698
|
/**
|
|
651
699
|
* @param {Where<any> | undefined} where
|
|
700
|
+
* @param {string} [write] The method, for a filter that changes rows: then no condition may be skipped, and the filter must not match every row.
|
|
701
|
+
* @param {string} [read] The method, for a read: with operators: "tagged", no condition may be skipped either.
|
|
652
702
|
* @returns {{ sql: string, params: any[] }}
|
|
653
703
|
*/
|
|
654
|
-
#where(where) {
|
|
704
|
+
#where(where, write, read) {
|
|
655
705
|
if (!where || !Object.keys(where).length) return { sql: "", params: [] };
|
|
706
|
+
const tagged = this.#tagged;
|
|
707
|
+
// where a missing value throws rather than drops its condition (a token left out would let anyone in)
|
|
708
|
+
const strict = write ?? (tagged ? read ?? "read" : undefined);
|
|
656
709
|
/** @type {any[]} */
|
|
657
710
|
const params = [];
|
|
658
711
|
/**
|
|
659
712
|
* @param {Where<any>} filter
|
|
713
|
+
* @param {string} at Where this filter is, for error messages.
|
|
660
714
|
* @returns {string}
|
|
661
715
|
*/
|
|
662
|
-
const build = (filter) => {
|
|
716
|
+
const build = (filter, at) => {
|
|
663
717
|
if (!_isPlain(filter)) throw new SQLiteError("A where filter must be a plain object");
|
|
664
718
|
/** @type {string[]} */
|
|
665
719
|
const parts = [];
|
|
666
720
|
for (const [key, value] of Object.entries(filter)) {
|
|
667
|
-
if (value === undefined)
|
|
721
|
+
if (value === undefined) {
|
|
722
|
+
// a missing value would drop its condition, and the query would reach more rows
|
|
723
|
+
if (strict) throw new SQLiteError(`${strict}(): ${at}.${key} is undefined`);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
668
726
|
if (key === "$or" || key === "$and") {
|
|
669
727
|
if (!Array.isArray(value)) throw new SQLiteError(`"${key}" expects an array of filters`);
|
|
670
|
-
|
|
728
|
+
if (write) {
|
|
729
|
+
if (!value.length) throw new SQLiteError(`${write}(): ${at}.${key} is empty`);
|
|
730
|
+
const subs = value.map((sub, i) => {
|
|
731
|
+
const sql = build(sub, `${at}.${key}[${i}]`);
|
|
732
|
+
if (!sql) throw new SQLiteError(`${write}(): ${at}.${key}[${i}] matches every row`);
|
|
733
|
+
return `(${sql})`;
|
|
734
|
+
});
|
|
735
|
+
parts.push(`(${subs.join(key === "$or" ? " OR " : " AND ")})`);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const subs = value.map((sub, i) => build(sub, strict ? `${at}.${key}[${i}]` : at) || "1");
|
|
671
739
|
parts.push(subs.length ? `(${subs.map((s) => `(${s})`).join(key === "$or" ? " OR " : " AND ")})` : key === "$or" ? "0" : "1");
|
|
672
740
|
continue;
|
|
673
741
|
}
|
|
674
742
|
const col = _column(key);
|
|
675
743
|
if (value === null) parts.push(`${col} IS NULL`);
|
|
676
|
-
else if (Array.isArray(value)) parts.push(this.#op(col, "in", value, params));
|
|
677
|
-
else if (_isPlain(value) && Object.keys(value).some((k) => OPERATORS.has(k))) {
|
|
744
|
+
else if (Array.isArray(value) && !tagged) parts.push(this.#op(col, "in", value, params));
|
|
745
|
+
else if (_isPlain(value) && (/** @type {any} */ (value)[TAGGED] === true || (!tagged && Object.keys(value).some((k) => OPERATORS.has(k))))) {
|
|
678
746
|
// unknown keys next to operators are typos: #op() reports them
|
|
679
|
-
for (const [op, operand] of Object.entries(value))
|
|
747
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
748
|
+
if (operand === undefined) {
|
|
749
|
+
if (strict) throw new SQLiteError(`${strict}(): ${at}.${key}.${op} is undefined`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const sql = this.#op(col, op, operand, params);
|
|
753
|
+
// "1" (an empty notIn) holds for every row: for a write, it's no condition at all
|
|
754
|
+
if (!write || sql !== "1") parts.push(sql);
|
|
755
|
+
}
|
|
680
756
|
} else {
|
|
681
|
-
// other objects and arrays compare as JSON text
|
|
757
|
+
// other objects and arrays (all of them with operators: "tagged") compare as JSON text
|
|
682
758
|
parts.push(`${col} = ?`);
|
|
683
759
|
params.push(_toSQL(value));
|
|
684
760
|
}
|
|
685
761
|
}
|
|
686
762
|
return parts.join(" AND ");
|
|
687
763
|
};
|
|
688
|
-
const sql = build(where);
|
|
764
|
+
const sql = build(where, "where");
|
|
765
|
+
if (write && !sql) throw new SQLiteError(`${write}() refuses a where filter that matches every row${write === "delete" ? " (use clearTable() to delete every row)" : ""}`);
|
|
689
766
|
return { sql: sql ? ` WHERE ${sql}` : "", params };
|
|
690
767
|
}
|
|
691
768
|
|
|
@@ -789,25 +866,6 @@ class SQLite {
|
|
|
789
866
|
}
|
|
790
867
|
}
|
|
791
868
|
|
|
792
|
-
/**
|
|
793
|
-
* The table a simple `SELECT` reads from, for type conversions.
|
|
794
|
-
* @param {string} sql
|
|
795
|
-
* @returns {string | undefined}
|
|
796
|
-
*/
|
|
797
|
-
#sourceTable(sql) {
|
|
798
|
-
if (this.#sources.has(sql)) return this.#sources.get(sql);
|
|
799
|
-
/** @type {string | undefined} */
|
|
800
|
-
let table;
|
|
801
|
-
if (!/\bjoin\b/i.test(sql)) {
|
|
802
|
-
const match = /^\s*select\b[\s\S]*?\bfrom\s+"?([A-Za-z_][A-Za-z0-9_]*)"?(?:\s+(?:as\s+)?(?!where\b|order\b|limit\b|group\b)[A-Za-z_]\w*)?\s*(?:where\b|order\b|limit\b|group\b|;|$)/i.exec(sql);
|
|
803
|
-
// A view has column types too: SQLite takes them from the tables it reads.
|
|
804
|
-
if (match && (this.#known.has(match[1]) || this.#prepare(SOURCE_EXISTS).get(match[1]) !== undefined)) table = match[1];
|
|
805
|
-
}
|
|
806
|
-
if (this.#sources.size >= 500) this.#sources.clear();
|
|
807
|
-
this.#sources.set(sql, table);
|
|
808
|
-
return table;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
869
|
/**
|
|
812
870
|
* Runs SQL that doesn't return rows. Without parameters, you can pass
|
|
813
871
|
* several statements at once.
|
|
@@ -844,8 +902,7 @@ class SQLite {
|
|
|
844
902
|
|
|
845
903
|
/**
|
|
846
904
|
* Runs a query and returns the first row. JSON and boolean columns are
|
|
847
|
-
* converted like in `get()`, joins and renamed columns included.
|
|
848
|
-
* before 22.16, that only works for a query on a single table.
|
|
905
|
+
* converted like in `get()`, joins and renamed columns included.
|
|
849
906
|
*
|
|
850
907
|
* @example
|
|
851
908
|
* const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
@@ -1126,7 +1183,7 @@ class SQLite {
|
|
|
1126
1183
|
#select(tableName, where, options, single) {
|
|
1127
1184
|
const table = this.#table(tableName);
|
|
1128
1185
|
const columns = options.columns?.length ? options.columns.map((c) => _id(c, "column name")).join(", ") : "*";
|
|
1129
|
-
const clause = this.#where(where);
|
|
1186
|
+
const clause = this.#where(where, undefined, single ? "get" : "getAll");
|
|
1130
1187
|
let sql = `SELECT ${columns} FROM ${table}${clause.sql}${this.#orderBy(options)}`;
|
|
1131
1188
|
if (single) {
|
|
1132
1189
|
sql += " LIMIT 1";
|
|
@@ -1176,7 +1233,7 @@ class SQLite {
|
|
|
1176
1233
|
* @throws {SQLiteError}
|
|
1177
1234
|
*/
|
|
1178
1235
|
count(tableName, where = {}) {
|
|
1179
|
-
const clause = this.#where(where);
|
|
1236
|
+
const clause = this.#where(where, undefined, "count");
|
|
1180
1237
|
const sql = `SELECT COUNT(*) AS total FROM ${this.#table(tableName)}${clause.sql}`;
|
|
1181
1238
|
const stmt = this.#prepare(sql);
|
|
1182
1239
|
try {
|
|
@@ -1198,7 +1255,7 @@ class SQLite {
|
|
|
1198
1255
|
* @throws {SQLiteError}
|
|
1199
1256
|
*/
|
|
1200
1257
|
exists(tableName, where = {}) {
|
|
1201
|
-
const clause = this.#where(where);
|
|
1258
|
+
const clause = this.#where(where, undefined, "exists");
|
|
1202
1259
|
const sql = `SELECT 1 FROM ${this.#table(tableName)}${clause.sql} LIMIT 1`;
|
|
1203
1260
|
const stmt = this.#prepare(sql);
|
|
1204
1261
|
try {
|
|
@@ -1283,7 +1340,8 @@ class SQLite {
|
|
|
1283
1340
|
|
|
1284
1341
|
/**
|
|
1285
1342
|
* Updates the rows that match. The filter is required, so a forgotten
|
|
1286
|
-
* condition can't rewrite the whole table
|
|
1343
|
+
* condition can't rewrite the whole table: an `undefined` value in it, an
|
|
1344
|
+
* empty `$and` or `$or`, or a filter that matches every row throws.
|
|
1287
1345
|
*
|
|
1288
1346
|
* @example
|
|
1289
1347
|
* db.update("users", { active: false }, { last_login: { lt: "2025-01-01" } });
|
|
@@ -1299,7 +1357,7 @@ class SQLite {
|
|
|
1299
1357
|
if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("update() requires a non-empty where filter");
|
|
1300
1358
|
const [cols, values] = this.#values(data);
|
|
1301
1359
|
if (!cols.length) throw new SQLiteError("update() requires at least one column to change");
|
|
1302
|
-
const clause = this.#where(where);
|
|
1360
|
+
const clause = this.#where(where, "update");
|
|
1303
1361
|
const { changes } = this.#run(`UPDATE ${table} SET ${cols.map((c) => `${c} = ?`).join(", ")}${clause.sql}`, [...values, ...clause.params]);
|
|
1304
1362
|
return { ok: true, changes };
|
|
1305
1363
|
}
|
|
@@ -1351,7 +1409,8 @@ class SQLite {
|
|
|
1351
1409
|
|
|
1352
1410
|
/**
|
|
1353
1411
|
* Deletes the rows that match. The filter is required; `clearTable()`
|
|
1354
|
-
* deletes everything on purpose.
|
|
1412
|
+
* deletes everything on purpose. An `undefined` value in the filter, an
|
|
1413
|
+
* empty `$and` or `$or`, or a filter that matches every row throws.
|
|
1355
1414
|
*
|
|
1356
1415
|
* @example
|
|
1357
1416
|
* db.delete("sessions", { expires_at: { lt: new Date() } });
|
|
@@ -1364,7 +1423,7 @@ class SQLite {
|
|
|
1364
1423
|
delete(tableName, where) {
|
|
1365
1424
|
const table = this.#table(tableName);
|
|
1366
1425
|
if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("delete() requires a non-empty where filter (use clearTable() to delete every row)");
|
|
1367
|
-
const clause = this.#where(where);
|
|
1426
|
+
const clause = this.#where(where, "delete");
|
|
1368
1427
|
const { changes } = this.#run(`DELETE FROM ${table}${clause.sql}`, clause.params);
|
|
1369
1428
|
return { ok: true, changes };
|
|
1370
1429
|
}
|
|
@@ -1373,7 +1432,7 @@ class SQLite {
|
|
|
1373
1432
|
* Adds to number columns in one statement, so two requests at the same
|
|
1374
1433
|
* moment both count. Reading the value, adding to it and writing it back
|
|
1375
1434
|
* would lose one of them. Negative numbers subtract; an empty column
|
|
1376
|
-
* counts as 0.
|
|
1435
|
+
* counts as 0. The filter is checked like the one of `update()`.
|
|
1377
1436
|
*
|
|
1378
1437
|
* @example
|
|
1379
1438
|
* db.increment("posts", { views: 1 }, { id: post.id });
|
|
@@ -1396,7 +1455,7 @@ class SQLite {
|
|
|
1396
1455
|
const col = _id(column, "column name");
|
|
1397
1456
|
return `${col} = COALESCE(${col}, 0) + ?`;
|
|
1398
1457
|
});
|
|
1399
|
-
const clause = this.#where(where);
|
|
1458
|
+
const clause = this.#where(where, "increment");
|
|
1400
1459
|
const { changes } = this.#run(`UPDATE ${table} SET ${set.join(", ")}${clause.sql}`, [...entries.map(([, amount]) => amount), ...clause.params]);
|
|
1401
1460
|
return { ok: true, changes };
|
|
1402
1461
|
}
|
|
@@ -1492,9 +1551,7 @@ class SQLite {
|
|
|
1492
1551
|
|
|
1493
1552
|
/** Whether a transaction is open. */
|
|
1494
1553
|
get inTransaction() {
|
|
1495
|
-
|
|
1496
|
-
const native = /** @type {{ isTransaction?: boolean }} */ (/** @type {unknown} */ (this.#db)).isTransaction;
|
|
1497
|
-
return typeof native === "boolean" ? native : this.#depth > 0;
|
|
1554
|
+
return this.#db.isTransaction;
|
|
1498
1555
|
}
|
|
1499
1556
|
|
|
1500
1557
|
/**
|