@camstack/system 1.2.29 → 1.2.31
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/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +135 -92
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +135 -92
- package/dist/builtins/backup-orchestrator/destination-policy.d.ts +12 -14
- package/dist/builtins/backup-orchestrator/schedule-store.d.ts +6 -6
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/filter-compiler.d.ts +61 -0
- package/dist/builtins/sqlite-storage/integration-registry.d.ts +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +49 -8
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +117 -85
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +117 -85
- package/dist/builtins/storage-orchestrator/location-store.d.ts +17 -24
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +0 -6
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +132 -121
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +132 -121
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CPmFYLqq.js → dist-CDv5YTHQ.js} +93 -1
- package/dist/{dist-B7hOpr5i.mjs → dist-CNmXITJ-.mjs} +93 -1
- package/dist/index.js +406 -224
- package/dist/index.mjs +406 -224
- package/dist/kernel/addon-installer.d.ts +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One filter compiler for both the read and the bulk-mutation paths.
|
|
3
|
+
*
|
|
4
|
+
* It exists because those two paths must NOT behave the same way, and until
|
|
5
|
+
* now the only compiler lived inline in `query` — where being forgiving is
|
|
6
|
+
* correct. Reusing it verbatim for `deleteWhere` would have shipped two
|
|
7
|
+
* data-loss shapes:
|
|
8
|
+
*
|
|
9
|
+
* - **A dropped predicate.** A field that is not a column of a structured
|
|
10
|
+
* table is skipped, so `{ deviceId: 615, kind: 'motion' }` can compile to
|
|
11
|
+
* `WHERE "deviceId" = ?` — on a SELECT that returns extra rows, on a DELETE
|
|
12
|
+
* it removes rows the caller meant to keep.
|
|
13
|
+
* - **No predicate at all.** An empty filter compiles to an empty WHERE,
|
|
14
|
+
* which on a DELETE is the entire collection.
|
|
15
|
+
*
|
|
16
|
+
* `select` mode keeps the historical behaviour, because every existing caller
|
|
17
|
+
* was written against it. `mutate` mode refuses both, before any SQL exists.
|
|
18
|
+
* Emptying a collection is a legitimate thing to want, and it should be a
|
|
19
|
+
* differently-named operation rather than a filter that happens to be empty.
|
|
20
|
+
*/
|
|
21
|
+
/** The part of a declared collection this compiler needs. */
|
|
22
|
+
export interface CollectionShape {
|
|
23
|
+
readonly primaryKey: string;
|
|
24
|
+
readonly columns: ReadonlySet<string>;
|
|
25
|
+
/**
|
|
26
|
+
* The single JSON blob column for a KV-shaped collection (fields inside it
|
|
27
|
+
* are reachable via `json_extract`), or `null` for a structured table whose
|
|
28
|
+
* fields are real columns.
|
|
29
|
+
*/
|
|
30
|
+
readonly kvBlobColumn: string | null;
|
|
31
|
+
}
|
|
32
|
+
export type FilterMode = 'select' | 'mutate';
|
|
33
|
+
/** Marshals a JS value to what the driver should bind. */
|
|
34
|
+
export type SerializeValue = (value: unknown) => unknown;
|
|
35
|
+
export interface QueryFilterInput {
|
|
36
|
+
readonly where?: Readonly<Record<string, unknown>>;
|
|
37
|
+
readonly whereIn?: Readonly<Record<string, readonly unknown[]>>;
|
|
38
|
+
readonly whereBetween?: Readonly<Record<string, readonly [unknown, unknown]>>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The predicate-only filter a bulk mutation accepts — no ordering, no limit.
|
|
42
|
+
* Structurally the mutation half of {@link QueryFilterInput}.
|
|
43
|
+
*/
|
|
44
|
+
export type MutationFilterInput = QueryFilterInput;
|
|
45
|
+
export interface CompiledFilter {
|
|
46
|
+
/** Either `''` or a leading-space ` WHERE …` clause, ready to concatenate. */
|
|
47
|
+
readonly whereSql: string;
|
|
48
|
+
readonly params: readonly unknown[];
|
|
49
|
+
}
|
|
50
|
+
/** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
|
|
51
|
+
export declare class UnsafeFilterError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* SQL expression for `field` on this collection, or `null` when it cannot be
|
|
56
|
+
* expressed: a real column → `"field"`; a KV blob field → `json_extract`;
|
|
57
|
+
* anything else on a structured table → unresolvable. Exported because
|
|
58
|
+
* `histogram` needs the same resolution for its bucketed column.
|
|
59
|
+
*/
|
|
60
|
+
export declare function fieldExprFor(field: string, shape: CollectionShape): string | null;
|
|
61
|
+
export declare function compileFilter(filter: QueryFilterInput | undefined, shape: CollectionShape, mode: FilterMode, serialize: SerializeValue): CompiledFilter;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Integration, PersistedDevice, CreateIntegrationInput, CreateDeviceInput, IIntegrationRegistry,
|
|
1
|
+
import { Integration, PersistedDevice, CreateIntegrationInput, CreateDeviceInput, IIntegrationRegistry, ISettingsStoreProvider } from '@camstack/types';
|
|
2
2
|
export declare class IntegrationRegistry implements IIntegrationRegistry {
|
|
3
|
-
private readonly
|
|
4
|
-
constructor(backend:
|
|
3
|
+
private readonly store;
|
|
4
|
+
constructor(backend: ISettingsStoreProvider);
|
|
5
5
|
initialize(): Promise<void>;
|
|
6
6
|
createIntegration(input: CreateIntegrationInput): Promise<Integration>;
|
|
7
7
|
getIntegration(id: string): Promise<Integration | null>;
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { default as Database } from 'better-sqlite3';
|
|
2
|
-
import { DataStoreEngineInfo, ISettingsBackend, SettingsRecord,
|
|
2
|
+
import { DataStoreEngineInfo, ISettingsBackend, SettingsRecord, SettingsGetInput, SettingsSetInput, SettingsQueryInput, SettingsInsertInput, SettingsUpdateInput, SettingsDeleteInput, SettingsCountInput, SettingsIsEmptyInput, SettingsHistogramInput, HistogramBucket, CollectionColumn, CollectionIndex } from '@camstack/types';
|
|
3
|
+
import { MutationFilterInput } from './filter-compiler.js';
|
|
4
|
+
/** Input for {@link SqliteSettingsBackend.deleteWhere}. */
|
|
5
|
+
interface SettingsDeleteWhereInput {
|
|
6
|
+
readonly namespace?: string;
|
|
7
|
+
readonly collection: string;
|
|
8
|
+
readonly filter: MutationFilterInput;
|
|
9
|
+
}
|
|
10
|
+
interface SettingsDeleteWhereResult {
|
|
11
|
+
readonly deleted: number;
|
|
12
|
+
}
|
|
13
|
+
/** Input for {@link SqliteSettingsBackend.updateWhere}. */
|
|
14
|
+
interface SettingsUpdateWhereInput {
|
|
15
|
+
readonly namespace?: string;
|
|
16
|
+
readonly collection: string;
|
|
17
|
+
readonly filter: MutationFilterInput;
|
|
18
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
19
|
+
}
|
|
20
|
+
interface SettingsUpdateWhereResult {
|
|
21
|
+
readonly updated: number;
|
|
22
|
+
}
|
|
3
23
|
/**
|
|
4
24
|
* SQLite implementation of ISettingsBackend.
|
|
5
25
|
*
|
|
@@ -51,6 +71,23 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
51
71
|
insert<T extends object = Record<string, unknown>>({ namespace, collection, record, }: SettingsInsertInput<T>): Promise<void>;
|
|
52
72
|
update({ namespace, collection, id, data }: SettingsUpdateInput): Promise<void>;
|
|
53
73
|
delete({ namespace, collection, key }: SettingsDeleteInput): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Bulk delete. One statement, whatever the row count — this is what every
|
|
76
|
+
* retention path in the system was hand-rolling as a select-then-delete
|
|
77
|
+
* loop. The filter is compiled in `mutate` mode, so an unresolvable
|
|
78
|
+
* predicate or an empty one throws before any SQL exists.
|
|
79
|
+
*/
|
|
80
|
+
deleteWhere({ namespace, collection, filter, }: SettingsDeleteWhereInput): Promise<SettingsDeleteWhereResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Bulk update. Same filter contract as {@link deleteWhere}.
|
|
83
|
+
*
|
|
84
|
+
* Every key of `data` must be a REAL column. A KV-shaped collection keeps
|
|
85
|
+
* its fields inside a JSON blob, and setting one would need `json_set` with
|
|
86
|
+
* read-modify-write semantics this method does not have — so it refuses
|
|
87
|
+
* rather than writing a column that does not exist, which is what the
|
|
88
|
+
* uncapped `tableUpdate` would have done.
|
|
89
|
+
*/
|
|
90
|
+
updateWhere({ namespace, collection, filter, data, }: SettingsUpdateWhereInput): Promise<SettingsUpdateWhereResult>;
|
|
54
91
|
count({ namespace, collection, filter }: SettingsCountInput): Promise<number>;
|
|
55
92
|
histogram({ namespace, collection, field, bucketSize, origin, filter, }: SettingsHistogramInput): Promise<readonly HistogramBucket[]>;
|
|
56
93
|
isEmpty({ namespace, collection }: SettingsIsEmptyInput): Promise<boolean>;
|
|
@@ -88,6 +125,12 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
88
125
|
private getAllScoped;
|
|
89
126
|
private setScopedKey;
|
|
90
127
|
private scopedName;
|
|
128
|
+
/**
|
|
129
|
+
* Project a declared collection onto what {@link compileFilter} needs. A
|
|
130
|
+
* collection whose only column is `data` is KV-shaped: its fields live
|
|
131
|
+
* inside that blob and are reached with `json_extract`.
|
|
132
|
+
*/
|
|
133
|
+
private shapeOf;
|
|
91
134
|
declareCollection(input: {
|
|
92
135
|
namespace?: string;
|
|
93
136
|
collection: string;
|
|
@@ -96,13 +139,11 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
96
139
|
}): Promise<void>;
|
|
97
140
|
/** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
|
|
98
141
|
private serializeColumnValue;
|
|
99
|
-
ensureTable
|
|
100
|
-
tableInsert
|
|
101
|
-
tableUpdate
|
|
102
|
-
tableDelete
|
|
103
|
-
|
|
104
|
-
tableGet(table: string, filter: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
105
|
-
tableCount(table: string, filter?: Record<string, unknown>): Promise<number>;
|
|
142
|
+
private ensureTable;
|
|
143
|
+
private tableInsert;
|
|
144
|
+
private tableUpdate;
|
|
145
|
+
private tableDelete;
|
|
146
|
+
private tableCount;
|
|
106
147
|
private buildWhere;
|
|
107
148
|
}
|
|
108
149
|
export default SqliteSettingsBackend;
|
|
@@ -3,10 +3,66 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CDv5YTHQ.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let better_sqlite3 = require("better-sqlite3");
|
|
9
9
|
better_sqlite3 = require_chunk.__toESM(better_sqlite3);
|
|
10
|
+
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
11
|
+
/** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
|
|
12
|
+
var UnsafeFilterError = class extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "UnsafeFilterError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* SQL expression for `field` on this collection, or `null` when it cannot be
|
|
20
|
+
* expressed: a real column → `"field"`; a KV blob field → `json_extract`;
|
|
21
|
+
* anything else on a structured table → unresolvable. Exported because
|
|
22
|
+
* `histogram` needs the same resolution for its bucketed column.
|
|
23
|
+
*/
|
|
24
|
+
function fieldExprFor(field, shape) {
|
|
25
|
+
if (field === shape.primaryKey || shape.columns.has(field)) return `"${field}"`;
|
|
26
|
+
if (shape.kvBlobColumn !== null) return `json_extract("${shape.kvBlobColumn}", '$.${field}')`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function compileFilter(filter, shape, mode, serialize) {
|
|
30
|
+
const clauses = [];
|
|
31
|
+
const params = [];
|
|
32
|
+
const resolve = (field) => {
|
|
33
|
+
const expr = fieldExprFor(field, shape);
|
|
34
|
+
if (expr === null && mode === "mutate") throw new UnsafeFilterError(`filter refers to "${field}", which this collection cannot express — refusing to run a bulk mutation with a dropped predicate`);
|
|
35
|
+
return expr;
|
|
36
|
+
};
|
|
37
|
+
for (const [field, value] of Object.entries(filter?.where ?? {})) {
|
|
38
|
+
const expr = resolve(field);
|
|
39
|
+
if (expr === null) continue;
|
|
40
|
+
clauses.push(`${expr} = ?`);
|
|
41
|
+
params.push(serialize(value));
|
|
42
|
+
}
|
|
43
|
+
for (const [field, values] of Object.entries(filter?.whereIn ?? {})) {
|
|
44
|
+
const expr = resolve(field);
|
|
45
|
+
if (expr === null) continue;
|
|
46
|
+
if (values.length === 0) {
|
|
47
|
+
if (mode === "mutate") throw new UnsafeFilterError(`filter has an empty whereIn list for "${field}" — it matches nothing, which is more likely a caller bug than an intent`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
clauses.push(`${expr} IN (${values.map(() => "?").join(", ")})`);
|
|
51
|
+
for (const v of values) params.push(serialize(v));
|
|
52
|
+
}
|
|
53
|
+
for (const [field, [low, high]] of Object.entries(filter?.whereBetween ?? {})) {
|
|
54
|
+
const expr = resolve(field);
|
|
55
|
+
if (expr === null) continue;
|
|
56
|
+
clauses.push(`${expr} BETWEEN ? AND ?`);
|
|
57
|
+
params.push(serialize(low), serialize(high));
|
|
58
|
+
}
|
|
59
|
+
if (clauses.length === 0 && mode === "mutate") throw new UnsafeFilterError("filter compiled to no predicate — that would affect every row in the collection; use the explicit clear operation if that is the intent");
|
|
60
|
+
return {
|
|
61
|
+
whereSql: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
|
|
62
|
+
params
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
10
66
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
11
67
|
function parseRowData(raw) {
|
|
12
68
|
return require_dist.asJsonObject(require_dist.parseJsonUnknown(raw)) ?? {};
|
|
@@ -189,6 +245,41 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
189
245
|
const decl = this.requireDeclared(scoped);
|
|
190
246
|
await this.tableDelete(scoped, { [decl.primaryKey]: key });
|
|
191
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Bulk delete. One statement, whatever the row count — this is what every
|
|
250
|
+
* retention path in the system was hand-rolling as a select-then-delete
|
|
251
|
+
* loop. The filter is compiled in `mutate` mode, so an unresolvable
|
|
252
|
+
* predicate or an empty one throws before any SQL exists.
|
|
253
|
+
*/
|
|
254
|
+
async deleteWhere({ namespace, collection, filter }) {
|
|
255
|
+
const scoped = this.scopedName(namespace, collection);
|
|
256
|
+
const decl = this.requireDeclared(scoped);
|
|
257
|
+
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
258
|
+
return { deleted: this.getDb().prepare(`DELETE FROM "${scoped}"${whereSql}`).run(...params).changes };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Bulk update. Same filter contract as {@link deleteWhere}.
|
|
262
|
+
*
|
|
263
|
+
* Every key of `data` must be a REAL column. A KV-shaped collection keeps
|
|
264
|
+
* its fields inside a JSON blob, and setting one would need `json_set` with
|
|
265
|
+
* read-modify-write semantics this method does not have — so it refuses
|
|
266
|
+
* rather than writing a column that does not exist, which is what the
|
|
267
|
+
* uncapped `tableUpdate` would have done.
|
|
268
|
+
*/
|
|
269
|
+
async updateWhere({ namespace, collection, filter, data }) {
|
|
270
|
+
const scoped = this.scopedName(namespace, collection);
|
|
271
|
+
const decl = this.requireDeclared(scoped);
|
|
272
|
+
const setClauses = [];
|
|
273
|
+
const setValues = [];
|
|
274
|
+
for (const [key, value] of Object.entries(data)) {
|
|
275
|
+
if (key !== decl.primaryKey && !decl.columns.has(key)) throw new UnsafeFilterError(`updateWhere cannot set "${key}" on "${scoped}" — it is not a column of this collection`);
|
|
276
|
+
setClauses.push(`"${key}" = ?`);
|
|
277
|
+
setValues.push(this.serializeColumnValue(value));
|
|
278
|
+
}
|
|
279
|
+
if (setClauses.length === 0) throw new UnsafeFilterError("updateWhere was given no fields to set");
|
|
280
|
+
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
281
|
+
return { updated: this.getDb().prepare(`UPDATE "${scoped}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...params).changes };
|
|
282
|
+
}
|
|
192
283
|
async count({ namespace, collection, filter }) {
|
|
193
284
|
const scoped = this.scopedName(namespace, collection);
|
|
194
285
|
const decl = this.requireDeclared(scoped);
|
|
@@ -216,30 +307,10 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
216
307
|
async histogram({ namespace, collection, field, bucketSize, origin, filter }) {
|
|
217
308
|
const scoped = this.scopedName(namespace, collection);
|
|
218
309
|
const decl = this.requireDeclared(scoped);
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (isKvShape) return `json_extract("data", '$.${f}')`;
|
|
224
|
-
return "";
|
|
225
|
-
};
|
|
226
|
-
const col = fieldExpr(field);
|
|
227
|
-
if (!col) return [];
|
|
228
|
-
const params = [];
|
|
229
|
-
const clauses = [];
|
|
230
|
-
if (filter?.where) for (const [f, value] of Object.entries(filter.where)) {
|
|
231
|
-
const expr = fieldExpr(f);
|
|
232
|
-
if (!expr) continue;
|
|
233
|
-
clauses.push(`${expr} = ?`);
|
|
234
|
-
params.push(this.serializeColumnValue(value));
|
|
235
|
-
}
|
|
236
|
-
if (filter?.whereBetween) for (const [f, [lo, hi]] of Object.entries(filter.whereBetween)) {
|
|
237
|
-
const expr = fieldExpr(f);
|
|
238
|
-
if (!expr) continue;
|
|
239
|
-
clauses.push(`${expr} BETWEEN ? AND ?`);
|
|
240
|
-
params.push(this.serializeColumnValue(lo), this.serializeColumnValue(hi));
|
|
241
|
-
}
|
|
242
|
-
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
310
|
+
const shape = this.shapeOf(decl);
|
|
311
|
+
const col = fieldExprFor(field, shape);
|
|
312
|
+
if (col === null) return [];
|
|
313
|
+
const { whereSql: where, params } = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
243
314
|
const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
|
|
244
315
|
return this.getDb().prepare(sql).all(origin, bucketSize, ...params).map((r) => ({
|
|
245
316
|
bucket: r.bucket,
|
|
@@ -255,43 +326,13 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
255
326
|
const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
|
|
256
327
|
let sql = `SELECT ${[`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ")} FROM "${table}"`;
|
|
257
328
|
const params = [];
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
* - real column → `"field"`
|
|
263
|
-
* - KV blob field (e.g. `username` inside `data`) → `json_extract`
|
|
264
|
-
* - structured table + non-column → empty string (filter dropped,
|
|
265
|
-
* matches legacy structured-table behaviour)
|
|
266
|
-
*/
|
|
267
|
-
const fieldExpr = (f) => {
|
|
268
|
-
if (isColumn(f)) return `"${f}"`;
|
|
269
|
-
if (isKvShape) return `json_extract("data", '$.${f}')`;
|
|
270
|
-
return "";
|
|
271
|
-
};
|
|
272
|
-
if (filter?.where) for (const [field, value] of Object.entries(filter.where)) {
|
|
273
|
-
const expr = fieldExpr(field);
|
|
274
|
-
if (!expr) continue;
|
|
275
|
-
whereClauses.push(`${expr} = ?`);
|
|
276
|
-
params.push(this.serializeColumnValue(value));
|
|
277
|
-
}
|
|
278
|
-
if (filter?.whereIn) for (const [field, values] of Object.entries(filter.whereIn)) {
|
|
279
|
-
const expr = fieldExpr(field);
|
|
280
|
-
if (!expr) continue;
|
|
281
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
282
|
-
whereClauses.push(`${expr} IN (${placeholders})`);
|
|
283
|
-
for (const v of values) params.push(this.serializeColumnValue(v));
|
|
284
|
-
}
|
|
285
|
-
if (filter?.whereBetween) for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
|
|
286
|
-
const expr = fieldExpr(field);
|
|
287
|
-
if (!expr) continue;
|
|
288
|
-
whereClauses.push(`${expr} BETWEEN ? AND ?`);
|
|
289
|
-
params.push(this.serializeColumnValue(low), this.serializeColumnValue(high));
|
|
290
|
-
}
|
|
291
|
-
if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
329
|
+
const shape = this.shapeOf(decl);
|
|
330
|
+
const compiled = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
331
|
+
sql += compiled.whereSql;
|
|
332
|
+
params.push(...compiled.params);
|
|
292
333
|
if (filter?.orderBy) {
|
|
293
|
-
const expr =
|
|
294
|
-
if (expr) {
|
|
334
|
+
const expr = fieldExprFor(filter.orderBy.field, shape);
|
|
335
|
+
if (expr !== null) {
|
|
295
336
|
const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
296
337
|
sql += ` ORDER BY ${expr} ${dir}`;
|
|
297
338
|
}
|
|
@@ -467,6 +508,19 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
467
508
|
scopedName(namespace, collection) {
|
|
468
509
|
return namespace ? `${namespace}:${collection}` : collection;
|
|
469
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Project a declared collection onto what {@link compileFilter} needs. A
|
|
513
|
+
* collection whose only column is `data` is KV-shaped: its fields live
|
|
514
|
+
* inside that blob and are reached with `json_extract`.
|
|
515
|
+
*/
|
|
516
|
+
shapeOf(decl) {
|
|
517
|
+
const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
|
|
518
|
+
return {
|
|
519
|
+
primaryKey: decl.primaryKey,
|
|
520
|
+
columns: decl.columns,
|
|
521
|
+
kvBlobColumn: isKvShape ? "data" : null
|
|
522
|
+
};
|
|
523
|
+
}
|
|
470
524
|
async declareCollection(input) {
|
|
471
525
|
const table = this.scopedName(input.namespace, input.collection);
|
|
472
526
|
if (this.declaredCollections.has(table)) return;
|
|
@@ -482,7 +536,8 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
482
536
|
type: c.type === "JSON" ? "TEXT" : c.type === "BOOLEAN" ? "INTEGER" : c.type,
|
|
483
537
|
...c.primaryKey !== void 0 ? { primaryKey: c.primaryKey } : {},
|
|
484
538
|
...c.notNull !== void 0 ? { notNull: c.notNull } : {},
|
|
485
|
-
...c.unique !== void 0 ? { unique: c.unique } : {}
|
|
539
|
+
...c.unique !== void 0 ? { unique: c.unique } : {},
|
|
540
|
+
...c.defaultValue !== void 0 ? { defaultValue: c.defaultValue } : {}
|
|
486
541
|
})),
|
|
487
542
|
...input.indexes ? { indexes: input.indexes.map((i) => ({
|
|
488
543
|
name: i.name,
|
|
@@ -567,29 +622,6 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
567
622
|
const { whereSql, whereValues } = this.buildWhere(filter);
|
|
568
623
|
return this.getDb().prepare(`DELETE FROM "${table}"${whereSql}`).run(...whereValues).changes;
|
|
569
624
|
}
|
|
570
|
-
async tableQuery(table, options) {
|
|
571
|
-
let sql = `SELECT * FROM "${table}"`;
|
|
572
|
-
const values = [];
|
|
573
|
-
if (options?.where) {
|
|
574
|
-
const { whereSql, whereValues } = this.buildWhere(options.where);
|
|
575
|
-
sql += whereSql;
|
|
576
|
-
values.push(...whereValues);
|
|
577
|
-
}
|
|
578
|
-
if (options?.orderBy) sql += ` ORDER BY "${options.orderBy.field}" ${options.orderBy.direction === "desc" ? "DESC" : "ASC"}`;
|
|
579
|
-
if (options?.limit !== void 0) {
|
|
580
|
-
sql += ` LIMIT ?`;
|
|
581
|
-
values.push(options.limit);
|
|
582
|
-
}
|
|
583
|
-
if (options?.offset !== void 0) {
|
|
584
|
-
sql += ` OFFSET ?`;
|
|
585
|
-
values.push(options.offset);
|
|
586
|
-
}
|
|
587
|
-
return this.getDb().prepare(sql).all(...values).flatMap((r) => require_dist.asJsonObject(r) ? [require_dist.asJsonObject(r)] : []);
|
|
588
|
-
}
|
|
589
|
-
async tableGet(table, filter) {
|
|
590
|
-
const { whereSql, whereValues } = this.buildWhere(filter);
|
|
591
|
-
return require_dist.asJsonObject(this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues));
|
|
592
|
-
}
|
|
593
625
|
async tableCount(table, filter) {
|
|
594
626
|
let sql = `SELECT COUNT(*) as count FROM "${table}"`;
|
|
595
627
|
const values = [];
|
|
@@ -1,6 +1,62 @@
|
|
|
1
|
-
import { Dt as parseJsonUnknown, at as errMsg, c as RUNTIME_DEFAULTS, gt as asJsonObject, ot as BaseAddon, x as dataStoreProviderCapability } from "../../dist-
|
|
1
|
+
import { Dt as parseJsonUnknown, at as errMsg, c as RUNTIME_DEFAULTS, gt as asJsonObject, ot as BaseAddon, x as dataStoreProviderCapability } from "../../dist-CNmXITJ-.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
5
|
+
/** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
|
|
6
|
+
var UnsafeFilterError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "UnsafeFilterError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* SQL expression for `field` on this collection, or `null` when it cannot be
|
|
14
|
+
* expressed: a real column → `"field"`; a KV blob field → `json_extract`;
|
|
15
|
+
* anything else on a structured table → unresolvable. Exported because
|
|
16
|
+
* `histogram` needs the same resolution for its bucketed column.
|
|
17
|
+
*/
|
|
18
|
+
function fieldExprFor(field, shape) {
|
|
19
|
+
if (field === shape.primaryKey || shape.columns.has(field)) return `"${field}"`;
|
|
20
|
+
if (shape.kvBlobColumn !== null) return `json_extract("${shape.kvBlobColumn}", '$.${field}')`;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function compileFilter(filter, shape, mode, serialize) {
|
|
24
|
+
const clauses = [];
|
|
25
|
+
const params = [];
|
|
26
|
+
const resolve = (field) => {
|
|
27
|
+
const expr = fieldExprFor(field, shape);
|
|
28
|
+
if (expr === null && mode === "mutate") throw new UnsafeFilterError(`filter refers to "${field}", which this collection cannot express — refusing to run a bulk mutation with a dropped predicate`);
|
|
29
|
+
return expr;
|
|
30
|
+
};
|
|
31
|
+
for (const [field, value] of Object.entries(filter?.where ?? {})) {
|
|
32
|
+
const expr = resolve(field);
|
|
33
|
+
if (expr === null) continue;
|
|
34
|
+
clauses.push(`${expr} = ?`);
|
|
35
|
+
params.push(serialize(value));
|
|
36
|
+
}
|
|
37
|
+
for (const [field, values] of Object.entries(filter?.whereIn ?? {})) {
|
|
38
|
+
const expr = resolve(field);
|
|
39
|
+
if (expr === null) continue;
|
|
40
|
+
if (values.length === 0) {
|
|
41
|
+
if (mode === "mutate") throw new UnsafeFilterError(`filter has an empty whereIn list for "${field}" — it matches nothing, which is more likely a caller bug than an intent`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
clauses.push(`${expr} IN (${values.map(() => "?").join(", ")})`);
|
|
45
|
+
for (const v of values) params.push(serialize(v));
|
|
46
|
+
}
|
|
47
|
+
for (const [field, [low, high]] of Object.entries(filter?.whereBetween ?? {})) {
|
|
48
|
+
const expr = resolve(field);
|
|
49
|
+
if (expr === null) continue;
|
|
50
|
+
clauses.push(`${expr} BETWEEN ? AND ?`);
|
|
51
|
+
params.push(serialize(low), serialize(high));
|
|
52
|
+
}
|
|
53
|
+
if (clauses.length === 0 && mode === "mutate") throw new UnsafeFilterError("filter compiled to no predicate — that would affect every row in the collection; use the explicit clear operation if that is the intent");
|
|
54
|
+
return {
|
|
55
|
+
whereSql: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
|
|
56
|
+
params
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
4
60
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
5
61
|
function parseRowData(raw) {
|
|
6
62
|
return asJsonObject(parseJsonUnknown(raw)) ?? {};
|
|
@@ -183,6 +239,41 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
183
239
|
const decl = this.requireDeclared(scoped);
|
|
184
240
|
await this.tableDelete(scoped, { [decl.primaryKey]: key });
|
|
185
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Bulk delete. One statement, whatever the row count — this is what every
|
|
244
|
+
* retention path in the system was hand-rolling as a select-then-delete
|
|
245
|
+
* loop. The filter is compiled in `mutate` mode, so an unresolvable
|
|
246
|
+
* predicate or an empty one throws before any SQL exists.
|
|
247
|
+
*/
|
|
248
|
+
async deleteWhere({ namespace, collection, filter }) {
|
|
249
|
+
const scoped = this.scopedName(namespace, collection);
|
|
250
|
+
const decl = this.requireDeclared(scoped);
|
|
251
|
+
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
252
|
+
return { deleted: this.getDb().prepare(`DELETE FROM "${scoped}"${whereSql}`).run(...params).changes };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Bulk update. Same filter contract as {@link deleteWhere}.
|
|
256
|
+
*
|
|
257
|
+
* Every key of `data` must be a REAL column. A KV-shaped collection keeps
|
|
258
|
+
* its fields inside a JSON blob, and setting one would need `json_set` with
|
|
259
|
+
* read-modify-write semantics this method does not have — so it refuses
|
|
260
|
+
* rather than writing a column that does not exist, which is what the
|
|
261
|
+
* uncapped `tableUpdate` would have done.
|
|
262
|
+
*/
|
|
263
|
+
async updateWhere({ namespace, collection, filter, data }) {
|
|
264
|
+
const scoped = this.scopedName(namespace, collection);
|
|
265
|
+
const decl = this.requireDeclared(scoped);
|
|
266
|
+
const setClauses = [];
|
|
267
|
+
const setValues = [];
|
|
268
|
+
for (const [key, value] of Object.entries(data)) {
|
|
269
|
+
if (key !== decl.primaryKey && !decl.columns.has(key)) throw new UnsafeFilterError(`updateWhere cannot set "${key}" on "${scoped}" — it is not a column of this collection`);
|
|
270
|
+
setClauses.push(`"${key}" = ?`);
|
|
271
|
+
setValues.push(this.serializeColumnValue(value));
|
|
272
|
+
}
|
|
273
|
+
if (setClauses.length === 0) throw new UnsafeFilterError("updateWhere was given no fields to set");
|
|
274
|
+
const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
|
|
275
|
+
return { updated: this.getDb().prepare(`UPDATE "${scoped}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...params).changes };
|
|
276
|
+
}
|
|
186
277
|
async count({ namespace, collection, filter }) {
|
|
187
278
|
const scoped = this.scopedName(namespace, collection);
|
|
188
279
|
const decl = this.requireDeclared(scoped);
|
|
@@ -210,30 +301,10 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
210
301
|
async histogram({ namespace, collection, field, bucketSize, origin, filter }) {
|
|
211
302
|
const scoped = this.scopedName(namespace, collection);
|
|
212
303
|
const decl = this.requireDeclared(scoped);
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (isKvShape) return `json_extract("data", '$.${f}')`;
|
|
218
|
-
return "";
|
|
219
|
-
};
|
|
220
|
-
const col = fieldExpr(field);
|
|
221
|
-
if (!col) return [];
|
|
222
|
-
const params = [];
|
|
223
|
-
const clauses = [];
|
|
224
|
-
if (filter?.where) for (const [f, value] of Object.entries(filter.where)) {
|
|
225
|
-
const expr = fieldExpr(f);
|
|
226
|
-
if (!expr) continue;
|
|
227
|
-
clauses.push(`${expr} = ?`);
|
|
228
|
-
params.push(this.serializeColumnValue(value));
|
|
229
|
-
}
|
|
230
|
-
if (filter?.whereBetween) for (const [f, [lo, hi]] of Object.entries(filter.whereBetween)) {
|
|
231
|
-
const expr = fieldExpr(f);
|
|
232
|
-
if (!expr) continue;
|
|
233
|
-
clauses.push(`${expr} BETWEEN ? AND ?`);
|
|
234
|
-
params.push(this.serializeColumnValue(lo), this.serializeColumnValue(hi));
|
|
235
|
-
}
|
|
236
|
-
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
304
|
+
const shape = this.shapeOf(decl);
|
|
305
|
+
const col = fieldExprFor(field, shape);
|
|
306
|
+
if (col === null) return [];
|
|
307
|
+
const { whereSql: where, params } = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
237
308
|
const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
|
|
238
309
|
return this.getDb().prepare(sql).all(origin, bucketSize, ...params).map((r) => ({
|
|
239
310
|
bucket: r.bucket,
|
|
@@ -249,43 +320,13 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
249
320
|
const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
|
|
250
321
|
let sql = `SELECT ${[`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ")} FROM "${table}"`;
|
|
251
322
|
const params = [];
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
* - real column → `"field"`
|
|
257
|
-
* - KV blob field (e.g. `username` inside `data`) → `json_extract`
|
|
258
|
-
* - structured table + non-column → empty string (filter dropped,
|
|
259
|
-
* matches legacy structured-table behaviour)
|
|
260
|
-
*/
|
|
261
|
-
const fieldExpr = (f) => {
|
|
262
|
-
if (isColumn(f)) return `"${f}"`;
|
|
263
|
-
if (isKvShape) return `json_extract("data", '$.${f}')`;
|
|
264
|
-
return "";
|
|
265
|
-
};
|
|
266
|
-
if (filter?.where) for (const [field, value] of Object.entries(filter.where)) {
|
|
267
|
-
const expr = fieldExpr(field);
|
|
268
|
-
if (!expr) continue;
|
|
269
|
-
whereClauses.push(`${expr} = ?`);
|
|
270
|
-
params.push(this.serializeColumnValue(value));
|
|
271
|
-
}
|
|
272
|
-
if (filter?.whereIn) for (const [field, values] of Object.entries(filter.whereIn)) {
|
|
273
|
-
const expr = fieldExpr(field);
|
|
274
|
-
if (!expr) continue;
|
|
275
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
276
|
-
whereClauses.push(`${expr} IN (${placeholders})`);
|
|
277
|
-
for (const v of values) params.push(this.serializeColumnValue(v));
|
|
278
|
-
}
|
|
279
|
-
if (filter?.whereBetween) for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
|
|
280
|
-
const expr = fieldExpr(field);
|
|
281
|
-
if (!expr) continue;
|
|
282
|
-
whereClauses.push(`${expr} BETWEEN ? AND ?`);
|
|
283
|
-
params.push(this.serializeColumnValue(low), this.serializeColumnValue(high));
|
|
284
|
-
}
|
|
285
|
-
if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
323
|
+
const shape = this.shapeOf(decl);
|
|
324
|
+
const compiled = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
|
|
325
|
+
sql += compiled.whereSql;
|
|
326
|
+
params.push(...compiled.params);
|
|
286
327
|
if (filter?.orderBy) {
|
|
287
|
-
const expr =
|
|
288
|
-
if (expr) {
|
|
328
|
+
const expr = fieldExprFor(filter.orderBy.field, shape);
|
|
329
|
+
if (expr !== null) {
|
|
289
330
|
const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
290
331
|
sql += ` ORDER BY ${expr} ${dir}`;
|
|
291
332
|
}
|
|
@@ -461,6 +502,19 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
461
502
|
scopedName(namespace, collection) {
|
|
462
503
|
return namespace ? `${namespace}:${collection}` : collection;
|
|
463
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Project a declared collection onto what {@link compileFilter} needs. A
|
|
507
|
+
* collection whose only column is `data` is KV-shaped: its fields live
|
|
508
|
+
* inside that blob and are reached with `json_extract`.
|
|
509
|
+
*/
|
|
510
|
+
shapeOf(decl) {
|
|
511
|
+
const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
|
|
512
|
+
return {
|
|
513
|
+
primaryKey: decl.primaryKey,
|
|
514
|
+
columns: decl.columns,
|
|
515
|
+
kvBlobColumn: isKvShape ? "data" : null
|
|
516
|
+
};
|
|
517
|
+
}
|
|
464
518
|
async declareCollection(input) {
|
|
465
519
|
const table = this.scopedName(input.namespace, input.collection);
|
|
466
520
|
if (this.declaredCollections.has(table)) return;
|
|
@@ -476,7 +530,8 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
476
530
|
type: c.type === "JSON" ? "TEXT" : c.type === "BOOLEAN" ? "INTEGER" : c.type,
|
|
477
531
|
...c.primaryKey !== void 0 ? { primaryKey: c.primaryKey } : {},
|
|
478
532
|
...c.notNull !== void 0 ? { notNull: c.notNull } : {},
|
|
479
|
-
...c.unique !== void 0 ? { unique: c.unique } : {}
|
|
533
|
+
...c.unique !== void 0 ? { unique: c.unique } : {},
|
|
534
|
+
...c.defaultValue !== void 0 ? { defaultValue: c.defaultValue } : {}
|
|
480
535
|
})),
|
|
481
536
|
...input.indexes ? { indexes: input.indexes.map((i) => ({
|
|
482
537
|
name: i.name,
|
|
@@ -561,29 +616,6 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
561
616
|
const { whereSql, whereValues } = this.buildWhere(filter);
|
|
562
617
|
return this.getDb().prepare(`DELETE FROM "${table}"${whereSql}`).run(...whereValues).changes;
|
|
563
618
|
}
|
|
564
|
-
async tableQuery(table, options) {
|
|
565
|
-
let sql = `SELECT * FROM "${table}"`;
|
|
566
|
-
const values = [];
|
|
567
|
-
if (options?.where) {
|
|
568
|
-
const { whereSql, whereValues } = this.buildWhere(options.where);
|
|
569
|
-
sql += whereSql;
|
|
570
|
-
values.push(...whereValues);
|
|
571
|
-
}
|
|
572
|
-
if (options?.orderBy) sql += ` ORDER BY "${options.orderBy.field}" ${options.orderBy.direction === "desc" ? "DESC" : "ASC"}`;
|
|
573
|
-
if (options?.limit !== void 0) {
|
|
574
|
-
sql += ` LIMIT ?`;
|
|
575
|
-
values.push(options.limit);
|
|
576
|
-
}
|
|
577
|
-
if (options?.offset !== void 0) {
|
|
578
|
-
sql += ` OFFSET ?`;
|
|
579
|
-
values.push(options.offset);
|
|
580
|
-
}
|
|
581
|
-
return this.getDb().prepare(sql).all(...values).flatMap((r) => asJsonObject(r) ? [asJsonObject(r)] : []);
|
|
582
|
-
}
|
|
583
|
-
async tableGet(table, filter) {
|
|
584
|
-
const { whereSql, whereValues } = this.buildWhere(filter);
|
|
585
|
-
return asJsonObject(this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues));
|
|
586
|
-
}
|
|
587
619
|
async tableCount(table, filter) {
|
|
588
620
|
let sql = `SELECT COUNT(*) as count FROM "${table}"`;
|
|
589
621
|
const values = [];
|