@shaferllc/keel 0.81.1 → 0.82.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/dist/core/database.d.ts +83 -0
- package/dist/core/database.js +248 -4
- package/dist/core/index.d.ts +1 -1
- package/docs/ai-manifest.json +13 -1
- package/docs/changelog.md +2095 -0
- package/docs/database.md +233 -59
- package/llms-full.txt +233 -59
- package/llms.txt +1 -0
- package/package.json +1 -1
package/dist/core/database.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface Paginated<T> {
|
|
|
22
22
|
currentPage: number;
|
|
23
23
|
lastPage: number;
|
|
24
24
|
}
|
|
25
|
+
/** A page without a total, returned by `simplePaginate()`. */
|
|
26
|
+
export interface SimplePaginated<T> {
|
|
27
|
+
data: T[];
|
|
28
|
+
perPage: number;
|
|
29
|
+
currentPage: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
}
|
|
25
32
|
/** The bridge to your database driver. */
|
|
26
33
|
export interface Connection {
|
|
27
34
|
/** Run a SELECT (or any row-returning query) and return the rows. */
|
|
@@ -85,16 +92,28 @@ export declare class QueryBuilder<T extends Row = Row> {
|
|
|
85
92
|
private groups;
|
|
86
93
|
private havings;
|
|
87
94
|
private _distinct;
|
|
95
|
+
private _randomOrder;
|
|
96
|
+
private _lock?;
|
|
88
97
|
constructor(table: string, getSource: () => Source);
|
|
89
98
|
/** Run a row-returning query on this builder's connection, dialect-adjusted. */
|
|
90
99
|
private runSelect;
|
|
91
100
|
/** Run a write on this builder's connection, dialect-adjusted. */
|
|
92
101
|
private runWrite;
|
|
93
102
|
select(...columns: string[]): this;
|
|
103
|
+
/** Append columns to the SELECT list without replacing it. */
|
|
104
|
+
addSelect(...columns: string[]): this;
|
|
105
|
+
/** A raw SELECT expression, e.g. `selectRaw("COUNT(*) AS n")`. */
|
|
106
|
+
selectRaw(sql: string): this;
|
|
107
|
+
/** Wrap a callback's conditions in parentheses, joined to the rest with `boolean`. */
|
|
108
|
+
private whereGroup;
|
|
109
|
+
where(group: (query: QueryBuilder) => void): this;
|
|
94
110
|
where(column: string, value: unknown): this;
|
|
95
111
|
where(column: string, operator: Operator, value: unknown): this;
|
|
112
|
+
orWhere(group: (query: QueryBuilder) => void): this;
|
|
96
113
|
orWhere(column: string, value: unknown): this;
|
|
97
114
|
orWhere(column: string, operator: Operator, value: unknown): this;
|
|
115
|
+
/** Negate a simple comparison: `whereNot("status", "active")`. */
|
|
116
|
+
whereNot(column: string, opOrValue: unknown, value?: unknown): this;
|
|
98
117
|
whereIn(column: string, values: unknown[]): this;
|
|
99
118
|
whereNull(column: string): this;
|
|
100
119
|
whereNotNull(column: string): this;
|
|
@@ -103,30 +122,78 @@ export declare class QueryBuilder<T extends Row = Row> {
|
|
|
103
122
|
whereLike(column: string, pattern: string): this;
|
|
104
123
|
/** Compare two columns (no binding): `whereColumn("updated_at", ">", "created_at")`. */
|
|
105
124
|
whereColumn(first: string, opOrSecond: string, second?: string): this;
|
|
125
|
+
whereNotBetween(column: string, [min, max]: [unknown, unknown]): this;
|
|
106
126
|
/** A raw WHERE fragment with its own bindings. */
|
|
107
127
|
whereRaw(sql: string, bindings?: unknown[]): this;
|
|
128
|
+
orWhereIn(column: string, values: unknown[]): this;
|
|
129
|
+
orWhereNotIn(column: string, values: unknown[]): this;
|
|
130
|
+
orWhereNull(column: string): this;
|
|
131
|
+
orWhereNotNull(column: string): this;
|
|
132
|
+
orWhereBetween(column: string, [min, max]: [unknown, unknown]): this;
|
|
133
|
+
orWhereColumn(first: string, opOrSecond: string, second?: string): this;
|
|
134
|
+
orWhereLike(column: string, pattern: string): this;
|
|
135
|
+
orWhereRaw(sql: string, bindings?: unknown[]): this;
|
|
108
136
|
join(table: string, first: string, opOrSecond: string, second?: string): this;
|
|
109
137
|
leftJoin(table: string, first: string, opOrSecond: string, second?: string): this;
|
|
138
|
+
rightJoin(table: string, first: string, opOrSecond: string, second?: string): this;
|
|
139
|
+
crossJoin(table: string): this;
|
|
110
140
|
groupBy(...columns: string[]): this;
|
|
141
|
+
groupByRaw(sql: string): this;
|
|
111
142
|
having(column: string, opOrValue: unknown, value?: unknown): this;
|
|
143
|
+
havingRaw(sql: string, bindings?: unknown[]): this;
|
|
144
|
+
havingBetween(column: string, [min, max]: [unknown, unknown]): this;
|
|
112
145
|
distinct(): this;
|
|
113
146
|
/** Apply `then` only when `condition` is truthy (optionally `otherwise`). */
|
|
114
147
|
when(condition: unknown, then: (query: this, value: unknown) => void, otherwise?: (query: this, value: unknown) => void): this;
|
|
148
|
+
/** The inverse of `when` — apply `then` only when `condition` is falsy. */
|
|
149
|
+
unless(condition: unknown, then: (query: this, value: unknown) => void, otherwise?: (query: this, value: unknown) => void): this;
|
|
115
150
|
orderBy(column: string, direction?: "asc" | "desc"): this;
|
|
151
|
+
orderByDesc(column: string): this;
|
|
116
152
|
/** A raw ORDER BY fragment, e.g. `orderByRaw("LENGTH(name) DESC")`. */
|
|
117
153
|
orderByRaw(sql: string): this;
|
|
154
|
+
/** Drop existing ordering, optionally setting a new one. */
|
|
155
|
+
reorder(column?: string, direction?: "asc" | "desc"): this;
|
|
156
|
+
/** Order rows randomly (dialect-aware `RANDOM()` / `RAND()`). */
|
|
157
|
+
inRandomOrder(): this;
|
|
118
158
|
/** Newest-first / oldest-first by a timestamp column (default `created_at`). */
|
|
119
159
|
latest(column?: string): this;
|
|
120
160
|
oldest(column?: string): this;
|
|
121
161
|
limit(n: number): this;
|
|
122
162
|
offset(n: number): this;
|
|
163
|
+
/** Alias of `limit`. */
|
|
164
|
+
take(n: number): this;
|
|
165
|
+
/** Alias of `offset`. */
|
|
166
|
+
skip(n: number): this;
|
|
167
|
+
/** Limit/offset for a given page (1-based). */
|
|
168
|
+
forPage(page: number, perPage?: number): this;
|
|
169
|
+
/** Add `FOR UPDATE` (write lock) to the SELECT; ignored on sqlite. */
|
|
170
|
+
lockForUpdate(): this;
|
|
171
|
+
/** Add `FOR SHARE` (read lock) to the SELECT; ignored on sqlite. */
|
|
172
|
+
sharedLock(): this;
|
|
123
173
|
private whereClause;
|
|
124
174
|
private havingClause;
|
|
125
175
|
private joinSql;
|
|
176
|
+
/** Compile the SELECT into `?`-placeholder SQL plus its bindings. */
|
|
177
|
+
private compileSelect;
|
|
126
178
|
get(): Promise<T[]>;
|
|
127
179
|
first(): Promise<T | null>;
|
|
180
|
+
/** The first row, or throw `NotFoundException` when there is none. */
|
|
181
|
+
firstOrFail(): Promise<T>;
|
|
182
|
+
/** Find by primary key (default `id`), or null. */
|
|
183
|
+
find(id: unknown, key?: string): Promise<T | null>;
|
|
184
|
+
/** Exactly one row: throws if none, or if more than one matches. */
|
|
185
|
+
sole(): Promise<T>;
|
|
186
|
+
/** The SQL this query would run, with `?` placeholders (does not execute). */
|
|
187
|
+
toSql(): string;
|
|
188
|
+
/** The bindings this query would run with. */
|
|
189
|
+
getBindings(): unknown[];
|
|
190
|
+
/** Log the compiled SQL + bindings, then return the builder for chaining. */
|
|
191
|
+
dump(): this;
|
|
192
|
+
/** Dump the compiled SQL + bindings and throw, halting execution. */
|
|
193
|
+
dd(): never;
|
|
128
194
|
count(): Promise<number>;
|
|
129
195
|
exists(): Promise<boolean>;
|
|
196
|
+
doesntExist(): Promise<boolean>;
|
|
130
197
|
private aggregate;
|
|
131
198
|
sum(column: string): Promise<number>;
|
|
132
199
|
avg(column: string): Promise<number>;
|
|
@@ -136,16 +203,32 @@ export declare class QueryBuilder<T extends Row = Row> {
|
|
|
136
203
|
value<V = unknown>(column: string): Promise<V | null>;
|
|
137
204
|
/** An array of a single column across all matching rows. */
|
|
138
205
|
pluck<V = unknown>(column: string): Promise<V[]>;
|
|
206
|
+
/** Join a single column's values into a string. */
|
|
207
|
+
implode(column: string, glue?: string): Promise<string>;
|
|
139
208
|
/** A page of results plus pagination metadata. */
|
|
140
209
|
paginate(page?: number, perPage?: number): Promise<Paginated<T>>;
|
|
210
|
+
/**
|
|
211
|
+
* A lighter page: no `COUNT` query. Fetches one extra row to know whether a
|
|
212
|
+
* next page exists — cheaper for "load more" UIs that don't need a total.
|
|
213
|
+
*/
|
|
214
|
+
simplePaginate(page?: number, perPage?: number): Promise<SimplePaginated<T>>;
|
|
141
215
|
insert(data: Row): Promise<WriteResult>;
|
|
142
216
|
insertGetId(data: Row): Promise<number | string | undefined>;
|
|
143
217
|
update(data: Row): Promise<WriteResult>;
|
|
144
218
|
delete(): Promise<WriteResult>;
|
|
219
|
+
/** Empty the table. */
|
|
220
|
+
truncate(): Promise<WriteResult>;
|
|
221
|
+
/** Update the first matching row, or insert `{ ...match, ...values }` if none. */
|
|
222
|
+
updateOrInsert(match: Row, values?: Row): Promise<WriteResult>;
|
|
145
223
|
/** Atomically add to a numeric column (optionally setting other columns too). */
|
|
146
224
|
increment(column: string, amount?: number, extra?: Row): Promise<WriteResult>;
|
|
147
225
|
/** Atomically subtract from a numeric column. */
|
|
148
226
|
decrement(column: string, amount?: number, extra?: Row): Promise<WriteResult>;
|
|
227
|
+
/** Increment several columns by 1 (or per-column amounts) in one statement. */
|
|
228
|
+
incrementEach(columns: string[] | Record<string, number>, extra?: Row): Promise<WriteResult>;
|
|
229
|
+
/** Decrement several columns by 1 (or per-column amounts) in one statement. */
|
|
230
|
+
decrementEach(columns: string[] | Record<string, number>, extra?: Row): Promise<WriteResult>;
|
|
231
|
+
private stepEach;
|
|
149
232
|
/** Insert one or more rows, ignoring any that violate a unique constraint. */
|
|
150
233
|
insertOrIgnore(rows: Row | Row[]): Promise<WriteResult>;
|
|
151
234
|
/**
|
package/dist/core/database.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
import { instrument, currentRequestId } from "./instrumentation.js";
|
|
14
|
+
import { NotFoundException } from "./exceptions.js";
|
|
14
15
|
/**
|
|
15
16
|
* Run a row-returning query on a source and report it to the instrumentation
|
|
16
17
|
* stream (`db.query`) once it settles. Emitting here — the one place every
|
|
@@ -126,6 +127,8 @@ export class QueryBuilder {
|
|
|
126
127
|
groups = [];
|
|
127
128
|
havings = [];
|
|
128
129
|
_distinct = false;
|
|
130
|
+
_randomOrder = false;
|
|
131
|
+
_lock;
|
|
129
132
|
// The connection is resolved lazily, when a query actually runs — so building
|
|
130
133
|
// a query never throws, and an unregistered connection surfaces as a rejected
|
|
131
134
|
// read/write rather than a synchronous error at construction.
|
|
@@ -145,16 +148,47 @@ export class QueryBuilder {
|
|
|
145
148
|
this.columns = columns.length ? columns.join(", ") : "*";
|
|
146
149
|
return this;
|
|
147
150
|
}
|
|
151
|
+
/** Append columns to the SELECT list without replacing it. */
|
|
152
|
+
addSelect(...columns) {
|
|
153
|
+
if (!columns.length)
|
|
154
|
+
return this;
|
|
155
|
+
this.columns = this.columns === "*" ? columns.join(", ") : `${this.columns}, ${columns.join(", ")}`;
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
/** A raw SELECT expression, e.g. `selectRaw("COUNT(*) AS n")`. */
|
|
159
|
+
selectRaw(sql) {
|
|
160
|
+
return this.addSelect(sql);
|
|
161
|
+
}
|
|
162
|
+
/** Wrap a callback's conditions in parentheses, joined to the rest with `boolean`. */
|
|
163
|
+
whereGroup(fn, boolean) {
|
|
164
|
+
const sub = new QueryBuilder(this.table, this.getSource);
|
|
165
|
+
fn(sub);
|
|
166
|
+
if (sub.wheres.length) {
|
|
167
|
+
const inner = sub.wheres.map((w, i) => (i === 0 ? "" : `${w.boolean} `) + w.sql).join(" ");
|
|
168
|
+
this.wheres.push({ boolean, sql: `(${inner})`, bindings: sub.wheres.flatMap((w) => w.bindings) });
|
|
169
|
+
}
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
148
172
|
where(column, opOrValue, value) {
|
|
173
|
+
if (typeof column === "function")
|
|
174
|
+
return this.whereGroup(column, "AND");
|
|
149
175
|
const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
|
|
150
176
|
this.wheres.push({ boolean: "AND", sql: `${column} ${op} ?`, bindings: [val] });
|
|
151
177
|
return this;
|
|
152
178
|
}
|
|
153
179
|
orWhere(column, opOrValue, value) {
|
|
180
|
+
if (typeof column === "function")
|
|
181
|
+
return this.whereGroup(column, "OR");
|
|
154
182
|
const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
|
|
155
183
|
this.wheres.push({ boolean: "OR", sql: `${column} ${op} ?`, bindings: [val] });
|
|
156
184
|
return this;
|
|
157
185
|
}
|
|
186
|
+
/** Negate a simple comparison: `whereNot("status", "active")`. */
|
|
187
|
+
whereNot(column, opOrValue, value) {
|
|
188
|
+
const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
|
|
189
|
+
this.wheres.push({ boolean: "AND", sql: `NOT (${column} ${op} ?)`, bindings: [val] });
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
158
192
|
whereIn(column, values) {
|
|
159
193
|
const marks = values.map(() => "?").join(", ");
|
|
160
194
|
this.wheres.push({ boolean: "AND", sql: `${column} IN (${marks})`, bindings: values });
|
|
@@ -187,11 +221,51 @@ export class QueryBuilder {
|
|
|
187
221
|
this.wheres.push({ boolean: "AND", sql: `${first} ${op} ${col}`, bindings: [] });
|
|
188
222
|
return this;
|
|
189
223
|
}
|
|
224
|
+
whereNotBetween(column, [min, max]) {
|
|
225
|
+
this.wheres.push({ boolean: "AND", sql: `${column} NOT BETWEEN ? AND ?`, bindings: [min, max] });
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
190
228
|
/** A raw WHERE fragment with its own bindings. */
|
|
191
229
|
whereRaw(sql, bindings = []) {
|
|
192
230
|
this.wheres.push({ boolean: "AND", sql, bindings });
|
|
193
231
|
return this;
|
|
194
232
|
}
|
|
233
|
+
/* --------------------------- OR variants --------------------------- */
|
|
234
|
+
orWhereIn(column, values) {
|
|
235
|
+
const marks = values.map(() => "?").join(", ");
|
|
236
|
+
this.wheres.push({ boolean: "OR", sql: `${column} IN (${marks})`, bindings: values });
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
orWhereNotIn(column, values) {
|
|
240
|
+
const marks = values.map(() => "?").join(", ");
|
|
241
|
+
this.wheres.push({ boolean: "OR", sql: `${column} NOT IN (${marks})`, bindings: values });
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
orWhereNull(column) {
|
|
245
|
+
this.wheres.push({ boolean: "OR", sql: `${column} IS NULL`, bindings: [] });
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
orWhereNotNull(column) {
|
|
249
|
+
this.wheres.push({ boolean: "OR", sql: `${column} IS NOT NULL`, bindings: [] });
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
orWhereBetween(column, [min, max]) {
|
|
253
|
+
this.wheres.push({ boolean: "OR", sql: `${column} BETWEEN ? AND ?`, bindings: [min, max] });
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
orWhereColumn(first, opOrSecond, second) {
|
|
257
|
+
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
258
|
+
this.wheres.push({ boolean: "OR", sql: `${first} ${op} ${col}`, bindings: [] });
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
orWhereLike(column, pattern) {
|
|
262
|
+
this.wheres.push({ boolean: "OR", sql: `${column} LIKE ?`, bindings: [pattern] });
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
orWhereRaw(sql, bindings = []) {
|
|
266
|
+
this.wheres.push({ boolean: "OR", sql, bindings });
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
195
269
|
join(table, first, opOrSecond, second) {
|
|
196
270
|
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
197
271
|
this.joins.push(`INNER JOIN ${table} ON ${first} ${op} ${col}`);
|
|
@@ -202,15 +276,36 @@ export class QueryBuilder {
|
|
|
202
276
|
this.joins.push(`LEFT JOIN ${table} ON ${first} ${op} ${col}`);
|
|
203
277
|
return this;
|
|
204
278
|
}
|
|
279
|
+
rightJoin(table, first, opOrSecond, second) {
|
|
280
|
+
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
281
|
+
this.joins.push(`RIGHT JOIN ${table} ON ${first} ${op} ${col}`);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
crossJoin(table) {
|
|
285
|
+
this.joins.push(`CROSS JOIN ${table}`);
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
205
288
|
groupBy(...columns) {
|
|
206
289
|
this.groups.push(...columns);
|
|
207
290
|
return this;
|
|
208
291
|
}
|
|
292
|
+
groupByRaw(sql) {
|
|
293
|
+
this.groups.push(sql);
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
209
296
|
having(column, opOrValue, value) {
|
|
210
297
|
const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
|
|
211
298
|
this.havings.push({ boolean: "AND", sql: `${column} ${op} ?`, bindings: [val] });
|
|
212
299
|
return this;
|
|
213
300
|
}
|
|
301
|
+
havingRaw(sql, bindings = []) {
|
|
302
|
+
this.havings.push({ boolean: "AND", sql, bindings });
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
havingBetween(column, [min, max]) {
|
|
306
|
+
this.havings.push({ boolean: "AND", sql: `${column} BETWEEN ? AND ?`, bindings: [min, max] });
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
214
309
|
distinct() {
|
|
215
310
|
this._distinct = true;
|
|
216
311
|
return this;
|
|
@@ -223,15 +318,39 @@ export class QueryBuilder {
|
|
|
223
318
|
otherwise?.(this, condition);
|
|
224
319
|
return this;
|
|
225
320
|
}
|
|
321
|
+
/** The inverse of `when` — apply `then` only when `condition` is falsy. */
|
|
322
|
+
unless(condition, then, otherwise) {
|
|
323
|
+
if (!condition)
|
|
324
|
+
then(this, condition);
|
|
325
|
+
else
|
|
326
|
+
otherwise?.(this, condition);
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
226
329
|
orderBy(column, direction = "asc") {
|
|
227
330
|
this.orders.push(`${column} ${direction.toUpperCase()}`);
|
|
228
331
|
return this;
|
|
229
332
|
}
|
|
333
|
+
orderByDesc(column) {
|
|
334
|
+
return this.orderBy(column, "desc");
|
|
335
|
+
}
|
|
230
336
|
/** A raw ORDER BY fragment, e.g. `orderByRaw("LENGTH(name) DESC")`. */
|
|
231
337
|
orderByRaw(sql) {
|
|
232
338
|
this.orders.push(sql);
|
|
233
339
|
return this;
|
|
234
340
|
}
|
|
341
|
+
/** Drop existing ordering, optionally setting a new one. */
|
|
342
|
+
reorder(column, direction = "asc") {
|
|
343
|
+
this.orders = [];
|
|
344
|
+
this._randomOrder = false;
|
|
345
|
+
if (column)
|
|
346
|
+
this.orderBy(column, direction);
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
/** Order rows randomly (dialect-aware `RANDOM()` / `RAND()`). */
|
|
350
|
+
inRandomOrder() {
|
|
351
|
+
this._randomOrder = true;
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
235
354
|
/** Newest-first / oldest-first by a timestamp column (default `created_at`). */
|
|
236
355
|
latest(column = "created_at") {
|
|
237
356
|
return this.orderBy(column, "desc");
|
|
@@ -247,6 +366,28 @@ export class QueryBuilder {
|
|
|
247
366
|
this._offset = n;
|
|
248
367
|
return this;
|
|
249
368
|
}
|
|
369
|
+
/** Alias of `limit`. */
|
|
370
|
+
take(n) {
|
|
371
|
+
return this.limit(n);
|
|
372
|
+
}
|
|
373
|
+
/** Alias of `offset`. */
|
|
374
|
+
skip(n) {
|
|
375
|
+
return this.offset(n);
|
|
376
|
+
}
|
|
377
|
+
/** Limit/offset for a given page (1-based). */
|
|
378
|
+
forPage(page, perPage = 15) {
|
|
379
|
+
return this.offset((Math.max(1, page) - 1) * perPage).limit(perPage);
|
|
380
|
+
}
|
|
381
|
+
/** Add `FOR UPDATE` (write lock) to the SELECT; ignored on sqlite. */
|
|
382
|
+
lockForUpdate() {
|
|
383
|
+
this._lock = "update";
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
/** Add `FOR SHARE` (read lock) to the SELECT; ignored on sqlite. */
|
|
387
|
+
sharedLock() {
|
|
388
|
+
this._lock = "share";
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
250
391
|
whereClause() {
|
|
251
392
|
if (!this.wheres.length)
|
|
252
393
|
return { sql: "", bindings: [] };
|
|
@@ -265,7 +406,8 @@ export class QueryBuilder {
|
|
|
265
406
|
return this.joins.length ? ` ${this.joins.join(" ")}` : "";
|
|
266
407
|
}
|
|
267
408
|
/* ------------------------------- reads ------------------------------- */
|
|
268
|
-
|
|
409
|
+
/** Compile the SELECT into `?`-placeholder SQL plus its bindings. */
|
|
410
|
+
compileSelect(dialect) {
|
|
269
411
|
const where = this.whereClause();
|
|
270
412
|
const having = this.havingClause();
|
|
271
413
|
let sql = `SELECT ${this._distinct ? "DISTINCT " : ""}${this.columns} FROM ${this.table}`;
|
|
@@ -273,19 +415,69 @@ export class QueryBuilder {
|
|
|
273
415
|
if (this.groups.length)
|
|
274
416
|
sql += ` GROUP BY ${this.groups.join(", ")}`;
|
|
275
417
|
sql += having.sql;
|
|
276
|
-
|
|
277
|
-
|
|
418
|
+
const orders = [...this.orders];
|
|
419
|
+
if (this._randomOrder)
|
|
420
|
+
orders.push(dialect === "mysql" ? "RAND()" : "RANDOM()");
|
|
421
|
+
if (orders.length)
|
|
422
|
+
sql += ` ORDER BY ${orders.join(", ")}`;
|
|
278
423
|
if (this._limit != null)
|
|
279
424
|
sql += ` LIMIT ${this._limit}`;
|
|
280
425
|
if (this._offset != null)
|
|
281
426
|
sql += ` OFFSET ${this._offset}`;
|
|
282
|
-
|
|
427
|
+
if (this._lock && dialect && dialect !== "sqlite") {
|
|
428
|
+
sql += this._lock === "update" ? " FOR UPDATE" : " FOR SHARE";
|
|
429
|
+
}
|
|
430
|
+
return { sql, bindings: [...where.bindings, ...having.bindings] };
|
|
431
|
+
}
|
|
432
|
+
async get() {
|
|
433
|
+
const source = this.getSource();
|
|
434
|
+
const { sql, bindings } = this.compileSelect(source.dialect);
|
|
435
|
+
return (await selectOn(source, sql, bindings));
|
|
283
436
|
}
|
|
284
437
|
async first() {
|
|
285
438
|
this._limit = 1;
|
|
286
439
|
const rows = await this.get();
|
|
287
440
|
return rows[0] ?? null;
|
|
288
441
|
}
|
|
442
|
+
/** The first row, or throw `NotFoundException` when there is none. */
|
|
443
|
+
async firstOrFail() {
|
|
444
|
+
const row = await this.first();
|
|
445
|
+
if (!row)
|
|
446
|
+
throw new NotFoundException(`No row found in ${this.table}`);
|
|
447
|
+
return row;
|
|
448
|
+
}
|
|
449
|
+
/** Find by primary key (default `id`), or null. */
|
|
450
|
+
async find(id, key = "id") {
|
|
451
|
+
return this.where(key, id).first();
|
|
452
|
+
}
|
|
453
|
+
/** Exactly one row: throws if none, or if more than one matches. */
|
|
454
|
+
async sole() {
|
|
455
|
+
this._limit = 2;
|
|
456
|
+
const rows = await this.get();
|
|
457
|
+
if (rows.length === 0)
|
|
458
|
+
throw new NotFoundException(`No row found in ${this.table}`);
|
|
459
|
+
if (rows.length > 1)
|
|
460
|
+
throw new Error(`Multiple rows found in ${this.table}`);
|
|
461
|
+
return rows[0];
|
|
462
|
+
}
|
|
463
|
+
/** The SQL this query would run, with `?` placeholders (does not execute). */
|
|
464
|
+
toSql() {
|
|
465
|
+
return this.compileSelect().sql;
|
|
466
|
+
}
|
|
467
|
+
/** The bindings this query would run with. */
|
|
468
|
+
getBindings() {
|
|
469
|
+
return this.compileSelect().bindings;
|
|
470
|
+
}
|
|
471
|
+
/** Log the compiled SQL + bindings, then return the builder for chaining. */
|
|
472
|
+
dump() {
|
|
473
|
+
console.log(this.toSql(), this.getBindings());
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
/** Dump the compiled SQL + bindings and throw, halting execution. */
|
|
477
|
+
dd() {
|
|
478
|
+
console.log(this.toSql(), this.getBindings());
|
|
479
|
+
throw new Error("dd(): query dumped");
|
|
480
|
+
}
|
|
289
481
|
async count() {
|
|
290
482
|
const where = this.whereClause();
|
|
291
483
|
const rows = (await this.runSelect(`SELECT COUNT(*) AS count FROM ${this.table}${this.joinSql()}${where.sql}`, where.bindings));
|
|
@@ -294,6 +486,9 @@ export class QueryBuilder {
|
|
|
294
486
|
async exists() {
|
|
295
487
|
return (await this.count()) > 0;
|
|
296
488
|
}
|
|
489
|
+
async doesntExist() {
|
|
490
|
+
return !(await this.exists());
|
|
491
|
+
}
|
|
297
492
|
async aggregate(fn, column) {
|
|
298
493
|
const where = this.whereClause();
|
|
299
494
|
const rows = (await this.runSelect(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${this.joinSql()}${where.sql}`, where.bindings));
|
|
@@ -323,6 +518,10 @@ export class QueryBuilder {
|
|
|
323
518
|
const rows = await this.get();
|
|
324
519
|
return rows.map((row) => row[column]);
|
|
325
520
|
}
|
|
521
|
+
/** Join a single column's values into a string. */
|
|
522
|
+
async implode(column, glue = "") {
|
|
523
|
+
return (await this.pluck(column)).join(glue);
|
|
524
|
+
}
|
|
326
525
|
/** A page of results plus pagination metadata. */
|
|
327
526
|
async paginate(page = 1, perPage = 15) {
|
|
328
527
|
const total = await this.count();
|
|
@@ -337,6 +536,17 @@ export class QueryBuilder {
|
|
|
337
536
|
lastPage: Math.max(1, Math.ceil(total / perPage)),
|
|
338
537
|
};
|
|
339
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* A lighter page: no `COUNT` query. Fetches one extra row to know whether a
|
|
541
|
+
* next page exists — cheaper for "load more" UIs that don't need a total.
|
|
542
|
+
*/
|
|
543
|
+
async simplePaginate(page = 1, perPage = 15) {
|
|
544
|
+
this._limit = perPage + 1;
|
|
545
|
+
this._offset = (Math.max(1, page) - 1) * perPage;
|
|
546
|
+
const rows = await this.get();
|
|
547
|
+
const hasMore = rows.length > perPage;
|
|
548
|
+
return { data: hasMore ? rows.slice(0, perPage) : rows, perPage, currentPage: page, hasMore };
|
|
549
|
+
}
|
|
340
550
|
/* ------------------------------- writes ------------------------------ */
|
|
341
551
|
async insert(data) {
|
|
342
552
|
const keys = Object.keys(data);
|
|
@@ -361,6 +571,22 @@ export class QueryBuilder {
|
|
|
361
571
|
const where = this.whereClause();
|
|
362
572
|
return this.runWrite(`DELETE FROM ${this.table}${where.sql}`, where.bindings);
|
|
363
573
|
}
|
|
574
|
+
/** Empty the table. */
|
|
575
|
+
async truncate() {
|
|
576
|
+
const dialect = this.getSource().dialect;
|
|
577
|
+
// sqlite has no TRUNCATE; a bare DELETE is the portable equivalent.
|
|
578
|
+
if (dialect === "sqlite")
|
|
579
|
+
return this.runWrite(`DELETE FROM ${this.table}`, []);
|
|
580
|
+
return this.runWrite(`TRUNCATE TABLE ${this.table}`, []);
|
|
581
|
+
}
|
|
582
|
+
/** Update the first matching row, or insert `{ ...match, ...values }` if none. */
|
|
583
|
+
async updateOrInsert(match, values = {}) {
|
|
584
|
+
for (const [column, value] of Object.entries(match))
|
|
585
|
+
this.where(column, value);
|
|
586
|
+
if (await this.exists())
|
|
587
|
+
return this.update(values);
|
|
588
|
+
return this.insert({ ...match, ...values });
|
|
589
|
+
}
|
|
364
590
|
/** Atomically add to a numeric column (optionally setting other columns too). */
|
|
365
591
|
increment(column, amount = 1, extra = {}) {
|
|
366
592
|
const where = this.whereClause();
|
|
@@ -375,6 +601,24 @@ export class QueryBuilder {
|
|
|
375
601
|
const bindings = [amount, ...Object.values(extra), ...where.bindings];
|
|
376
602
|
return this.runWrite(`UPDATE ${this.table} SET ${sets.join(", ")}${where.sql}`, bindings);
|
|
377
603
|
}
|
|
604
|
+
/** Increment several columns by 1 (or per-column amounts) in one statement. */
|
|
605
|
+
incrementEach(columns, extra = {}) {
|
|
606
|
+
return this.stepEach("+", columns, extra);
|
|
607
|
+
}
|
|
608
|
+
/** Decrement several columns by 1 (or per-column amounts) in one statement. */
|
|
609
|
+
decrementEach(columns, extra = {}) {
|
|
610
|
+
return this.stepEach("-", columns, extra);
|
|
611
|
+
}
|
|
612
|
+
stepEach(op, columns, extra) {
|
|
613
|
+
const entries = Array.isArray(columns) ? columns.map((c) => [c, 1]) : Object.entries(columns);
|
|
614
|
+
const where = this.whereClause();
|
|
615
|
+
const sets = [
|
|
616
|
+
...entries.map(([c]) => `${c} = ${c} ${op} ?`),
|
|
617
|
+
...Object.keys(extra).map((k) => `${k} = ?`),
|
|
618
|
+
];
|
|
619
|
+
const bindings = [...entries.map(([, n]) => n), ...Object.values(extra), ...where.bindings];
|
|
620
|
+
return this.runWrite(`UPDATE ${this.table} SET ${sets.join(", ")}${where.sql}`, bindings);
|
|
621
|
+
}
|
|
378
622
|
/** Insert one or more rows, ignoring any that violate a unique constraint. */
|
|
379
623
|
async insertOrIgnore(rows) {
|
|
380
624
|
const list = Array.isArray(rows) ? rows : [rows];
|
package/dist/core/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type { SecurityHeadersOptions, HstsOptions } from "./shield.js";
|
|
|
38
38
|
export { csrf, csrfToken, csrfField } from "./csrf.js";
|
|
39
39
|
export type { CsrfOptions } from "./csrf.js";
|
|
40
40
|
export { db, connection, getConnection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, transaction, inTransaction, QueryBuilder, } from "./database.js";
|
|
41
|
-
export type { Connection, TransactionConnection, TransactionHandle, ConnectionHandle, WriteResult, Row, Dialect, Operator, Paginated, } from "./database.js";
|
|
41
|
+
export type { Connection, TransactionConnection, TransactionHandle, ConnectionHandle, WriteResult, Row, Dialect, Operator, Paginated, SimplePaginated, } from "./database.js";
|
|
42
42
|
export { Model } from "./model.js";
|
|
43
43
|
export type { GlobalScope } from "./model.js";
|
|
44
44
|
export { ModelQuery } from "./model-query.js";
|
package/docs/ai-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaferllc/keel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
|
|
5
5
|
"repo": "https://github.com/shaferllc/keel",
|
|
6
6
|
"generated": "run `npm run build:ai` to regenerate",
|
|
@@ -75,6 +75,13 @@
|
|
|
75
75
|
"path": "docs/cache.md",
|
|
76
76
|
"example": "docs/examples/cache.ts"
|
|
77
77
|
},
|
|
78
|
+
{
|
|
79
|
+
"slug": "changelog",
|
|
80
|
+
"title": "Changelog",
|
|
81
|
+
"summary": "All notable changes to Keel are documented here.",
|
|
82
|
+
"path": "docs/changelog.md",
|
|
83
|
+
"example": null
|
|
84
|
+
},
|
|
78
85
|
{
|
|
79
86
|
"slug": "configuration",
|
|
80
87
|
"title": "Configuration",
|
|
@@ -2804,6 +2811,11 @@
|
|
|
2804
2811
|
"kind": "value",
|
|
2805
2812
|
"module": "storage"
|
|
2806
2813
|
},
|
|
2814
|
+
{
|
|
2815
|
+
"name": "SimplePaginated",
|
|
2816
|
+
"kind": "type",
|
|
2817
|
+
"module": "database"
|
|
2818
|
+
},
|
|
2807
2819
|
{
|
|
2808
2820
|
"name": "singleton",
|
|
2809
2821
|
"kind": "value",
|