@shaferllc/keel 0.81.2 → 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 +6 -1
- package/docs/database.md +233 -59
- package/llms-full.txt +233 -59
- 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",
|
|
@@ -2811,6 +2811,11 @@
|
|
|
2811
2811
|
"kind": "value",
|
|
2812
2812
|
"module": "storage"
|
|
2813
2813
|
},
|
|
2814
|
+
{
|
|
2815
|
+
"name": "SimplePaginated",
|
|
2816
|
+
"kind": "type",
|
|
2817
|
+
"module": "database"
|
|
2818
|
+
},
|
|
2814
2819
|
{
|
|
2815
2820
|
"name": "singleton",
|
|
2816
2821
|
"kind": "value",
|
package/docs/database.md
CHANGED
|
@@ -110,106 +110,218 @@ An unregistered connection name doesn't fail when you *build* a query, only when
|
|
|
110
110
|
it runs — so a misconfigured name surfaces as a rejected read/write, not a
|
|
111
111
|
construction-time throw.
|
|
112
112
|
|
|
113
|
-
##
|
|
113
|
+
## Query builder
|
|
114
114
|
|
|
115
|
-
Start a query with `db(table)`, chain constraints
|
|
116
|
-
method
|
|
115
|
+
Start a query with `db(table)`, chain constraints (they return the builder, so
|
|
116
|
+
order doesn't matter), and finish with a terminal method. Nothing hits the
|
|
117
|
+
database until a terminal runs. Every value becomes a **binding**, never
|
|
118
|
+
string-interpolated SQL — the builder is injection-safe by construction. It's
|
|
119
|
+
driver-agnostic and edge-safe: the same chain compiles for sqlite, MySQL, and
|
|
120
|
+
Postgres.
|
|
117
121
|
|
|
118
122
|
```ts
|
|
119
123
|
import { db } from "@shaferllc/keel/core";
|
|
120
124
|
|
|
121
|
-
await db("users")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await db("orders")
|
|
128
|
-
.select("id", "total")
|
|
129
|
-
.where("status", "paid")
|
|
130
|
-
.orWhere("status", "shipped")
|
|
125
|
+
const active = await db("users")
|
|
126
|
+
.where("active", true)
|
|
127
|
+
.where("age", ">", 18)
|
|
128
|
+
.orderBy("name")
|
|
129
|
+
.limit(20)
|
|
131
130
|
.get();
|
|
132
131
|
```
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
### Retrieving results
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
await db("users").get(); // Row[]
|
|
137
|
+
await db("users").where("id", 1).first(); // Row | null
|
|
138
|
+
await db("users").where("id", 1).firstOrFail(); // Row, or throws NotFoundException
|
|
139
|
+
await db("users").find(1); // by primary key (default "id")
|
|
140
|
+
await db("users").where("email", e).sole(); // exactly one, else throws
|
|
141
|
+
await db("users").where("id", 1).value("email"); // one column of the first row
|
|
142
|
+
await db("posts").pluck("title"); // string[] of one column
|
|
143
|
+
await db("tags").orderBy("name").implode("name", ", "); // "a, b, c"
|
|
144
|
+
```
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
For large sets, `chunk` pages through without loading everything (return `false`
|
|
147
|
+
to stop early):
|
|
139
148
|
|
|
140
149
|
```ts
|
|
141
|
-
await db("
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await db("posts").latest().get(); // ORDER BY created_at DESC
|
|
145
|
-
await db("posts").oldest("published_at").get();
|
|
150
|
+
await db("users").orderBy("id").chunk(500, async (rows) => {
|
|
151
|
+
for (const row of rows) await process(row);
|
|
152
|
+
});
|
|
146
153
|
```
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
### Aggregates
|
|
149
156
|
|
|
150
157
|
```ts
|
|
151
|
-
await db("
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
await db("orders").count();
|
|
159
|
+
await db("orders").where("paid", true).sum("total");
|
|
160
|
+
await db("orders").avg("total"); // also min(col), max(col)
|
|
161
|
+
await db("users").where("email", e).exists(); // boolean
|
|
162
|
+
await db("users").where("banned", true).doesntExist();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Selects
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
db("users").select("id", "email");
|
|
169
|
+
db("users").select("id").addSelect("email"); // append, don't replace
|
|
170
|
+
db("orders").selectRaw("SUM(total) AS revenue");
|
|
171
|
+
db("users").distinct().select("country");
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Where clauses
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
db("users").where("votes", 100); // = is the default operator
|
|
178
|
+
db("users").where("votes", ">=", 100);
|
|
179
|
+
db("users").where("name", "like", "T%");
|
|
180
|
+
|
|
181
|
+
db("users").where("votes", 100).orWhere("name", "John");
|
|
182
|
+
db("users").whereNot("status", "cancelled");
|
|
183
|
+
|
|
184
|
+
db("users").whereIn("id", [1, 2, 3]).whereNotIn("id", [4]);
|
|
185
|
+
db("users").whereNull("deleted_at").whereNotNull("email_verified_at");
|
|
186
|
+
db("products").whereBetween("price", [10, 100]).whereNotBetween("stock", [0, 5]);
|
|
187
|
+
db("posts").whereLike("title", "%keel%");
|
|
188
|
+
db("events").whereColumn("updated_at", ">", "created_at"); // column vs column
|
|
189
|
+
db("users").whereRaw("score >= ? AND score <= ?", [10, 90]);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Every clause has an `orWhere…` twin — `orWhereIn`, `orWhereNull`,
|
|
193
|
+
`orWhereNotNull`, `orWhereBetween`, `orWhereColumn`, `orWhereLike`, `orWhereRaw`,
|
|
194
|
+
`orWhereNotIn`.
|
|
195
|
+
|
|
196
|
+
**Grouped clauses.** Pass a callback to `where`/`orWhere` to parenthesize a set
|
|
197
|
+
of conditions — the way to express `A AND (B OR C)`:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
await db("users")
|
|
201
|
+
.where("active", true)
|
|
202
|
+
.where((q) => q.where("role", "admin").orWhere("role", "owner"))
|
|
154
203
|
.get();
|
|
204
|
+
// … WHERE active = ? AND (role = ? OR role = ?)
|
|
205
|
+
```
|
|
155
206
|
|
|
156
|
-
|
|
157
|
-
await db("users").distinct().select("country").pluck("country");
|
|
207
|
+
### Ordering, grouping, limit & offset
|
|
158
208
|
|
|
159
|
-
|
|
160
|
-
|
|
209
|
+
```ts
|
|
210
|
+
db("users").orderBy("name").orderByDesc("created_at");
|
|
211
|
+
db("posts").latest(); // ORDER BY created_at DESC (oldest() for ASC)
|
|
212
|
+
db("posts").orderByRaw("LENGTH(title) DESC");
|
|
213
|
+
db("users").inRandomOrder(); // dialect-aware RANDOM()/RAND()
|
|
214
|
+
db("users").reorder("name"); // clear existing ordering, then set
|
|
215
|
+
|
|
216
|
+
db("orders")
|
|
217
|
+
.select("user_id")
|
|
218
|
+
.selectRaw("SUM(total) AS spent")
|
|
219
|
+
.groupBy("user_id")
|
|
220
|
+
.having("spent", ">", 1000) // also havingRaw(...), havingBetween(...)
|
|
221
|
+
.get();
|
|
161
222
|
|
|
162
|
-
|
|
223
|
+
db("users").limit(10).offset(20); // take(10)/skip(20) are aliases
|
|
224
|
+
db("users").forPage(3, 15); // page 3, 15 per page
|
|
163
225
|
```
|
|
164
226
|
|
|
165
|
-
|
|
227
|
+
### Joins
|
|
166
228
|
|
|
167
229
|
```ts
|
|
168
|
-
await db("
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
230
|
+
await db("posts")
|
|
231
|
+
.join("users", "posts.user_id", "users.id") // INNER JOIN on equality
|
|
232
|
+
.leftJoin("images", "images.post_id", "posts.id")
|
|
233
|
+
.select("posts.title", "users.name")
|
|
234
|
+
.get();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`rightJoin` and `crossJoin` round out the set. Joins with several `ON`
|
|
238
|
+
conditions aren't modelled — use `whereRaw` or a view.
|
|
172
239
|
|
|
173
|
-
|
|
174
|
-
await db("posts").pluck("title"); // string[] of one column
|
|
240
|
+
### Conditional clauses
|
|
175
241
|
|
|
176
|
-
|
|
242
|
+
`when` / `unless` apply a callback based on a runtime value, so you build a query
|
|
243
|
+
without breaking the chain into `if`s. The callback receives the value:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
await db("users")
|
|
247
|
+
.when(search, (q, term) => q.whereLike("name", `%${term}%`))
|
|
248
|
+
.unless(includeArchived, (q) => q.whereNull("archived_at"))
|
|
249
|
+
.get();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Inserts
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await db("users").insert({ email, name });
|
|
256
|
+
const id = await db("users").insertGetId({ email, name }); // new primary key
|
|
257
|
+
await db("logs").insertOrIgnore({ key, value }); // skip unique conflicts
|
|
258
|
+
await db("users").upsert([{ id: 1, name: "Ada" }], ["id"], ["name"]); // insert/update
|
|
177
259
|
```
|
|
178
260
|
|
|
179
|
-
`
|
|
180
|
-
|
|
261
|
+
`upsert(rows, uniqueBy, update?)` inserts, updating the `update` columns (default:
|
|
262
|
+
everything not in `uniqueBy`) on a conflict — dialect-aware (`ON CONFLICT` /
|
|
263
|
+
`ON DUPLICATE KEY UPDATE`).
|
|
181
264
|
|
|
182
|
-
|
|
265
|
+
### Updates
|
|
183
266
|
|
|
184
267
|
```ts
|
|
185
|
-
const id = await db("users").insertGetId({ email, name });
|
|
186
268
|
await db("users").where("id", id).update({ name: "Grace" });
|
|
187
|
-
await db("users").
|
|
269
|
+
await db("users").updateOrInsert({ email }, { name }); // update match, else insert
|
|
270
|
+
await db("posts").where("id", id).increment("views"); // += 1
|
|
271
|
+
await db("posts").where("id", id).decrement("stock", 3, { updated_at: now });
|
|
272
|
+
await db("counters").incrementEach({ hits: 1, misses: 2 }); // several columns at once
|
|
188
273
|
```
|
|
189
274
|
|
|
190
|
-
|
|
191
|
-
SQL — so it's injection-safe by construction. Writes return a `WriteResult`;
|
|
192
|
-
`insertGetId` unwraps it to just the new id.
|
|
275
|
+
### Deletes
|
|
193
276
|
|
|
194
|
-
|
|
277
|
+
```ts
|
|
278
|
+
await db("sessions").where("expires_at", "<", now).delete();
|
|
279
|
+
await db("cache").truncate(); // empty the table (DELETE on sqlite)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
> **Guard your writes.** `update()`, `delete()`, and the increments apply to
|
|
283
|
+
> every row matching the current `where` clause — with none, that's the whole
|
|
284
|
+
> table. Scope every write unless you truly mean to touch every row.
|
|
285
|
+
|
|
286
|
+
### Pagination
|
|
195
287
|
|
|
196
288
|
```ts
|
|
197
|
-
await db("posts").
|
|
198
|
-
|
|
289
|
+
const page = await db("posts").latest().paginate(2, 15);
|
|
290
|
+
// { data, total, perPage, currentPage, lastPage } — a COUNT plus a page query
|
|
199
291
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
292
|
+
const feed = await db("posts").latest().simplePaginate(2, 15);
|
|
293
|
+
// { data, perPage, currentPage, hasMore } — no COUNT; one extra row tells hasMore
|
|
294
|
+
```
|
|
203
295
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
296
|
+
### Pessimistic locking
|
|
297
|
+
|
|
298
|
+
Inside a [transaction](#transactions), lock the selected rows against concurrent
|
|
299
|
+
writes. No-ops on sqlite (which locks the whole database anyway):
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
await transaction(async () => {
|
|
303
|
+
const row = await db("accounts").where("id", id).lockForUpdate().first(); // FOR UPDATE
|
|
304
|
+
await db("accounts").where("id", id).update({ balance: row.balance - 10 });
|
|
207
305
|
});
|
|
306
|
+
// sharedLock() takes a read lock (FOR SHARE) instead.
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Debugging
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
db("users").where("active", true).toSql(); // "SELECT * FROM users WHERE active = ?"
|
|
313
|
+
db("users").where("active", true).getBindings(); // [true]
|
|
314
|
+
db("users").where("active", true).dump(); // logs SQL + bindings, returns the builder
|
|
315
|
+
db("users").where("active", true).dd(); // logs and throws (dump-and-die)
|
|
208
316
|
```
|
|
209
317
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
318
|
+
### Not (yet) modelled
|
|
319
|
+
|
|
320
|
+
Kept out on purpose, to stay driver-agnostic and honest about what compiles
|
|
321
|
+
everywhere: unions, subquery `where`/join builders (`whereExists`, `joinSub`),
|
|
322
|
+
the `whereDate`/`whereMonth`/… date-function family (no portable form across
|
|
323
|
+
dialects), and `cursor`/`lazy` streaming. Reach for `whereRaw`, a raw
|
|
324
|
+
`connection().select(sql)`, or a database view when you need them.
|
|
213
325
|
|
|
214
326
|
## Transactions
|
|
215
327
|
|
|
@@ -656,6 +768,68 @@ Insert one or more rows, skipping any that violate a unique constraint
|
|
|
656
768
|
Process results a page at a time so a large table never loads at once. Return
|
|
657
769
|
`false` from the callback to stop early. Pair with `orderBy` for a stable order.
|
|
658
770
|
|
|
771
|
+
#### `addSelect(...columns)` · `selectRaw(sql)`
|
|
772
|
+
|
|
773
|
+
Append columns to the SELECT list without replacing it; `selectRaw` appends a raw
|
|
774
|
+
expression (`selectRaw("SUM(total) AS revenue")`).
|
|
775
|
+
|
|
776
|
+
#### `orWhere` family · `whereNot(...)` · `whereNotBetween(column, [min, max])`
|
|
777
|
+
|
|
778
|
+
Every `where…` clause has an `orWhere…` twin joined with `OR` — `orWhereIn`,
|
|
779
|
+
`orWhereNotIn`, `orWhereNull`, `orWhereNotNull`, `orWhereBetween`,
|
|
780
|
+
`orWhereColumn`, `orWhereLike`, `orWhereRaw`. `whereNot` negates a comparison;
|
|
781
|
+
`whereNotBetween` is the inverse of `whereBetween`. Passing a **callback** to
|
|
782
|
+
`where`/`orWhere` groups its conditions in parentheses.
|
|
783
|
+
|
|
784
|
+
#### `orderByDesc(column)` · `reorder(column?, direction?)` · `inRandomOrder()`
|
|
785
|
+
|
|
786
|
+
Descending order; clear existing ordering (optionally setting a new one); random
|
|
787
|
+
order (dialect-aware `RANDOM()`/`RAND()`).
|
|
788
|
+
|
|
789
|
+
#### `groupByRaw(sql)` · `havingRaw(sql, bindings?)` · `havingBetween(column, [min, max])`
|
|
790
|
+
|
|
791
|
+
Raw `GROUP BY`, a raw/bound `HAVING`, and a `HAVING … BETWEEN`.
|
|
792
|
+
|
|
793
|
+
#### `take(n)` · `skip(n)` · `forPage(page, perPage?)`
|
|
794
|
+
|
|
795
|
+
Aliases for `limit`/`offset`, and limit+offset for a 1-based page.
|
|
796
|
+
|
|
797
|
+
#### `rightJoin(...)` · `crossJoin(table)`
|
|
798
|
+
|
|
799
|
+
`RIGHT JOIN` on an equality; `CROSS JOIN`.
|
|
800
|
+
|
|
801
|
+
#### `unless(condition, then, otherwise?)`
|
|
802
|
+
|
|
803
|
+
The inverse of `when` — runs `then` only when `condition` is falsy.
|
|
804
|
+
|
|
805
|
+
#### `find(id, key?)` · `firstOrFail()` · `sole()` · `doesntExist()` · `implode(column, glue?)`
|
|
806
|
+
|
|
807
|
+
Find by key (default `"id"`); first-or-throw; exactly-one-or-throw; the negation
|
|
808
|
+
of `exists`; and join one column's values into a string.
|
|
809
|
+
|
|
810
|
+
#### `simplePaginate(page?, perPage?)`
|
|
811
|
+
|
|
812
|
+
`simplePaginate(page = 1, perPage = 15): Promise<SimplePaginated<T>>`
|
|
813
|
+
|
|
814
|
+
A page without a `COUNT` — fetches one extra row to set `hasMore`. Cheaper than
|
|
815
|
+
`paginate` for "load more" UIs.
|
|
816
|
+
|
|
817
|
+
#### `lockForUpdate()` · `sharedLock()`
|
|
818
|
+
|
|
819
|
+
Add `FOR UPDATE` / `FOR SHARE` to the SELECT (inside a transaction). Ignored on
|
|
820
|
+
sqlite.
|
|
821
|
+
|
|
822
|
+
#### `updateOrInsert(match, values?)` · `truncate()` · `incrementEach(cols, extra?)` · `decrementEach(cols, extra?)`
|
|
823
|
+
|
|
824
|
+
Update the first match or insert `{ ...match, ...values }`; empty the table
|
|
825
|
+
(`DELETE` on sqlite); and step several numeric columns in one statement (`cols` is
|
|
826
|
+
an array — each by 1 — or a `{ column: amount }` map).
|
|
827
|
+
|
|
828
|
+
#### `toSql()` · `getBindings()` · `dump()` · `dd()`
|
|
829
|
+
|
|
830
|
+
The compiled `?`-placeholder SQL and its bindings, without executing; `dump` logs
|
|
831
|
+
them and returns the builder; `dd` logs and throws.
|
|
832
|
+
|
|
659
833
|
### Interfaces & types
|
|
660
834
|
|
|
661
835
|
#### `Connection`
|
package/llms-full.txt
CHANGED
|
@@ -7759,106 +7759,218 @@ An unregistered connection name doesn't fail when you *build* a query, only when
|
|
|
7759
7759
|
it runs — so a misconfigured name surfaces as a rejected read/write, not a
|
|
7760
7760
|
construction-time throw.
|
|
7761
7761
|
|
|
7762
|
-
##
|
|
7762
|
+
## Query builder
|
|
7763
7763
|
|
|
7764
|
-
Start a query with `db(table)`, chain constraints
|
|
7765
|
-
method
|
|
7764
|
+
Start a query with `db(table)`, chain constraints (they return the builder, so
|
|
7765
|
+
order doesn't matter), and finish with a terminal method. Nothing hits the
|
|
7766
|
+
database until a terminal runs. Every value becomes a **binding**, never
|
|
7767
|
+
string-interpolated SQL — the builder is injection-safe by construction. It's
|
|
7768
|
+
driver-agnostic and edge-safe: the same chain compiles for sqlite, MySQL, and
|
|
7769
|
+
Postgres.
|
|
7766
7770
|
|
|
7767
7771
|
```ts
|
|
7768
7772
|
import { db } from "@shaferllc/keel/core";
|
|
7769
7773
|
|
|
7770
|
-
await db("users")
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
await db("orders")
|
|
7777
|
-
.select("id", "total")
|
|
7778
|
-
.where("status", "paid")
|
|
7779
|
-
.orWhere("status", "shipped")
|
|
7774
|
+
const active = await db("users")
|
|
7775
|
+
.where("active", true)
|
|
7776
|
+
.where("age", ">", 18)
|
|
7777
|
+
.orderBy("name")
|
|
7778
|
+
.limit(20)
|
|
7780
7779
|
.get();
|
|
7781
7780
|
```
|
|
7782
7781
|
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7782
|
+
### Retrieving results
|
|
7783
|
+
|
|
7784
|
+
```ts
|
|
7785
|
+
await db("users").get(); // Row[]
|
|
7786
|
+
await db("users").where("id", 1).first(); // Row | null
|
|
7787
|
+
await db("users").where("id", 1).firstOrFail(); // Row, or throws NotFoundException
|
|
7788
|
+
await db("users").find(1); // by primary key (default "id")
|
|
7789
|
+
await db("users").where("email", e).sole(); // exactly one, else throws
|
|
7790
|
+
await db("users").where("id", 1).value("email"); // one column of the first row
|
|
7791
|
+
await db("posts").pluck("title"); // string[] of one column
|
|
7792
|
+
await db("tags").orderBy("name").implode("name", ", "); // "a, b, c"
|
|
7793
|
+
```
|
|
7786
7794
|
|
|
7787
|
-
|
|
7795
|
+
For large sets, `chunk` pages through without loading everything (return `false`
|
|
7796
|
+
to stop early):
|
|
7788
7797
|
|
|
7789
7798
|
```ts
|
|
7790
|
-
await db("
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
await db("posts").latest().get(); // ORDER BY created_at DESC
|
|
7794
|
-
await db("posts").oldest("published_at").get();
|
|
7799
|
+
await db("users").orderBy("id").chunk(500, async (rows) => {
|
|
7800
|
+
for (const row of rows) await process(row);
|
|
7801
|
+
});
|
|
7795
7802
|
```
|
|
7796
7803
|
|
|
7797
|
-
|
|
7804
|
+
### Aggregates
|
|
7798
7805
|
|
|
7799
7806
|
```ts
|
|
7800
|
-
await db("
|
|
7801
|
-
|
|
7802
|
-
|
|
7807
|
+
await db("orders").count();
|
|
7808
|
+
await db("orders").where("paid", true).sum("total");
|
|
7809
|
+
await db("orders").avg("total"); // also min(col), max(col)
|
|
7810
|
+
await db("users").where("email", e).exists(); // boolean
|
|
7811
|
+
await db("users").where("banned", true).doesntExist();
|
|
7812
|
+
```
|
|
7813
|
+
|
|
7814
|
+
### Selects
|
|
7815
|
+
|
|
7816
|
+
```ts
|
|
7817
|
+
db("users").select("id", "email");
|
|
7818
|
+
db("users").select("id").addSelect("email"); // append, don't replace
|
|
7819
|
+
db("orders").selectRaw("SUM(total) AS revenue");
|
|
7820
|
+
db("users").distinct().select("country");
|
|
7821
|
+
```
|
|
7822
|
+
|
|
7823
|
+
### Where clauses
|
|
7824
|
+
|
|
7825
|
+
```ts
|
|
7826
|
+
db("users").where("votes", 100); // = is the default operator
|
|
7827
|
+
db("users").where("votes", ">=", 100);
|
|
7828
|
+
db("users").where("name", "like", "T%");
|
|
7829
|
+
|
|
7830
|
+
db("users").where("votes", 100).orWhere("name", "John");
|
|
7831
|
+
db("users").whereNot("status", "cancelled");
|
|
7832
|
+
|
|
7833
|
+
db("users").whereIn("id", [1, 2, 3]).whereNotIn("id", [4]);
|
|
7834
|
+
db("users").whereNull("deleted_at").whereNotNull("email_verified_at");
|
|
7835
|
+
db("products").whereBetween("price", [10, 100]).whereNotBetween("stock", [0, 5]);
|
|
7836
|
+
db("posts").whereLike("title", "%keel%");
|
|
7837
|
+
db("events").whereColumn("updated_at", ">", "created_at"); // column vs column
|
|
7838
|
+
db("users").whereRaw("score >= ? AND score <= ?", [10, 90]);
|
|
7839
|
+
```
|
|
7840
|
+
|
|
7841
|
+
Every clause has an `orWhere…` twin — `orWhereIn`, `orWhereNull`,
|
|
7842
|
+
`orWhereNotNull`, `orWhereBetween`, `orWhereColumn`, `orWhereLike`, `orWhereRaw`,
|
|
7843
|
+
`orWhereNotIn`.
|
|
7844
|
+
|
|
7845
|
+
**Grouped clauses.** Pass a callback to `where`/`orWhere` to parenthesize a set
|
|
7846
|
+
of conditions — the way to express `A AND (B OR C)`:
|
|
7847
|
+
|
|
7848
|
+
```ts
|
|
7849
|
+
await db("users")
|
|
7850
|
+
.where("active", true)
|
|
7851
|
+
.where((q) => q.where("role", "admin").orWhere("role", "owner"))
|
|
7803
7852
|
.get();
|
|
7853
|
+
// … WHERE active = ? AND (role = ? OR role = ?)
|
|
7854
|
+
```
|
|
7855
|
+
|
|
7856
|
+
### Ordering, grouping, limit & offset
|
|
7804
7857
|
|
|
7805
|
-
|
|
7806
|
-
|
|
7858
|
+
```ts
|
|
7859
|
+
db("users").orderBy("name").orderByDesc("created_at");
|
|
7860
|
+
db("posts").latest(); // ORDER BY created_at DESC (oldest() for ASC)
|
|
7861
|
+
db("posts").orderByRaw("LENGTH(title) DESC");
|
|
7862
|
+
db("users").inRandomOrder(); // dialect-aware RANDOM()/RAND()
|
|
7863
|
+
db("users").reorder("name"); // clear existing ordering, then set
|
|
7864
|
+
|
|
7865
|
+
db("orders")
|
|
7866
|
+
.select("user_id")
|
|
7867
|
+
.selectRaw("SUM(total) AS spent")
|
|
7868
|
+
.groupBy("user_id")
|
|
7869
|
+
.having("spent", ">", 1000) // also havingRaw(...), havingBetween(...)
|
|
7870
|
+
.get();
|
|
7871
|
+
|
|
7872
|
+
db("users").limit(10).offset(20); // take(10)/skip(20) are aliases
|
|
7873
|
+
db("users").forPage(3, 15); // page 3, 15 per page
|
|
7874
|
+
```
|
|
7807
7875
|
|
|
7808
|
-
|
|
7809
|
-
await db("users").whereRaw("score >= ?", [10]).orderByRaw("LENGTH(name) DESC").get();
|
|
7876
|
+
### Joins
|
|
7810
7877
|
|
|
7811
|
-
|
|
7878
|
+
```ts
|
|
7879
|
+
await db("posts")
|
|
7880
|
+
.join("users", "posts.user_id", "users.id") // INNER JOIN on equality
|
|
7881
|
+
.leftJoin("images", "images.post_id", "posts.id")
|
|
7882
|
+
.select("posts.title", "users.name")
|
|
7883
|
+
.get();
|
|
7812
7884
|
```
|
|
7813
7885
|
|
|
7814
|
-
|
|
7886
|
+
`rightJoin` and `crossJoin` round out the set. Joins with several `ON`
|
|
7887
|
+
conditions aren't modelled — use `whereRaw` or a view.
|
|
7888
|
+
|
|
7889
|
+
### Conditional clauses
|
|
7890
|
+
|
|
7891
|
+
`when` / `unless` apply a callback based on a runtime value, so you build a query
|
|
7892
|
+
without breaking the chain into `if`s. The callback receives the value:
|
|
7815
7893
|
|
|
7816
7894
|
```ts
|
|
7817
|
-
await db("
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7895
|
+
await db("users")
|
|
7896
|
+
.when(search, (q, term) => q.whereLike("name", `%${term}%`))
|
|
7897
|
+
.unless(includeArchived, (q) => q.whereNull("archived_at"))
|
|
7898
|
+
.get();
|
|
7899
|
+
```
|
|
7821
7900
|
|
|
7822
|
-
|
|
7823
|
-
await db("posts").pluck("title"); // string[] of one column
|
|
7901
|
+
### Inserts
|
|
7824
7902
|
|
|
7825
|
-
|
|
7903
|
+
```ts
|
|
7904
|
+
await db("users").insert({ email, name });
|
|
7905
|
+
const id = await db("users").insertGetId({ email, name }); // new primary key
|
|
7906
|
+
await db("logs").insertOrIgnore({ key, value }); // skip unique conflicts
|
|
7907
|
+
await db("users").upsert([{ id: 1, name: "Ada" }], ["id"], ["name"]); // insert/update
|
|
7826
7908
|
```
|
|
7827
7909
|
|
|
7828
|
-
`
|
|
7829
|
-
|
|
7910
|
+
`upsert(rows, uniqueBy, update?)` inserts, updating the `update` columns (default:
|
|
7911
|
+
everything not in `uniqueBy`) on a conflict — dialect-aware (`ON CONFLICT` /
|
|
7912
|
+
`ON DUPLICATE KEY UPDATE`).
|
|
7830
7913
|
|
|
7831
|
-
|
|
7914
|
+
### Updates
|
|
7832
7915
|
|
|
7833
7916
|
```ts
|
|
7834
|
-
const id = await db("users").insertGetId({ email, name });
|
|
7835
7917
|
await db("users").where("id", id).update({ name: "Grace" });
|
|
7836
|
-
await db("users").
|
|
7918
|
+
await db("users").updateOrInsert({ email }, { name }); // update match, else insert
|
|
7919
|
+
await db("posts").where("id", id).increment("views"); // += 1
|
|
7920
|
+
await db("posts").where("id", id).decrement("stock", 3, { updated_at: now });
|
|
7921
|
+
await db("counters").incrementEach({ hits: 1, misses: 2 }); // several columns at once
|
|
7922
|
+
```
|
|
7923
|
+
|
|
7924
|
+
### Deletes
|
|
7925
|
+
|
|
7926
|
+
```ts
|
|
7927
|
+
await db("sessions").where("expires_at", "<", now).delete();
|
|
7928
|
+
await db("cache").truncate(); // empty the table (DELETE on sqlite)
|
|
7837
7929
|
```
|
|
7838
7930
|
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7931
|
+
> **Guard your writes.** `update()`, `delete()`, and the increments apply to
|
|
7932
|
+
> every row matching the current `where` clause — with none, that's the whole
|
|
7933
|
+
> table. Scope every write unless you truly mean to touch every row.
|
|
7842
7934
|
|
|
7843
|
-
|
|
7935
|
+
### Pagination
|
|
7844
7936
|
|
|
7845
7937
|
```ts
|
|
7846
|
-
await db("posts").
|
|
7847
|
-
|
|
7938
|
+
const page = await db("posts").latest().paginate(2, 15);
|
|
7939
|
+
// { data, total, perPage, currentPage, lastPage } — a COUNT plus a page query
|
|
7848
7940
|
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7941
|
+
const feed = await db("posts").latest().simplePaginate(2, 15);
|
|
7942
|
+
// { data, perPage, currentPage, hasMore } — no COUNT; one extra row tells hasMore
|
|
7943
|
+
```
|
|
7852
7944
|
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7945
|
+
### Pessimistic locking
|
|
7946
|
+
|
|
7947
|
+
Inside a [transaction](#transactions), lock the selected rows against concurrent
|
|
7948
|
+
writes. No-ops on sqlite (which locks the whole database anyway):
|
|
7949
|
+
|
|
7950
|
+
```ts
|
|
7951
|
+
await transaction(async () => {
|
|
7952
|
+
const row = await db("accounts").where("id", id).lockForUpdate().first(); // FOR UPDATE
|
|
7953
|
+
await db("accounts").where("id", id).update({ balance: row.balance - 10 });
|
|
7856
7954
|
});
|
|
7955
|
+
// sharedLock() takes a read lock (FOR SHARE) instead.
|
|
7857
7956
|
```
|
|
7858
7957
|
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7958
|
+
### Debugging
|
|
7959
|
+
|
|
7960
|
+
```ts
|
|
7961
|
+
db("users").where("active", true).toSql(); // "SELECT * FROM users WHERE active = ?"
|
|
7962
|
+
db("users").where("active", true).getBindings(); // [true]
|
|
7963
|
+
db("users").where("active", true).dump(); // logs SQL + bindings, returns the builder
|
|
7964
|
+
db("users").where("active", true).dd(); // logs and throws (dump-and-die)
|
|
7965
|
+
```
|
|
7966
|
+
|
|
7967
|
+
### Not (yet) modelled
|
|
7968
|
+
|
|
7969
|
+
Kept out on purpose, to stay driver-agnostic and honest about what compiles
|
|
7970
|
+
everywhere: unions, subquery `where`/join builders (`whereExists`, `joinSub`),
|
|
7971
|
+
the `whereDate`/`whereMonth`/… date-function family (no portable form across
|
|
7972
|
+
dialects), and `cursor`/`lazy` streaming. Reach for `whereRaw`, a raw
|
|
7973
|
+
`connection().select(sql)`, or a database view when you need them.
|
|
7862
7974
|
|
|
7863
7975
|
## Transactions
|
|
7864
7976
|
|
|
@@ -8305,6 +8417,68 @@ Insert one or more rows, skipping any that violate a unique constraint
|
|
|
8305
8417
|
Process results a page at a time so a large table never loads at once. Return
|
|
8306
8418
|
`false` from the callback to stop early. Pair with `orderBy` for a stable order.
|
|
8307
8419
|
|
|
8420
|
+
#### `addSelect(...columns)` · `selectRaw(sql)`
|
|
8421
|
+
|
|
8422
|
+
Append columns to the SELECT list without replacing it; `selectRaw` appends a raw
|
|
8423
|
+
expression (`selectRaw("SUM(total) AS revenue")`).
|
|
8424
|
+
|
|
8425
|
+
#### `orWhere` family · `whereNot(...)` · `whereNotBetween(column, [min, max])`
|
|
8426
|
+
|
|
8427
|
+
Every `where…` clause has an `orWhere…` twin joined with `OR` — `orWhereIn`,
|
|
8428
|
+
`orWhereNotIn`, `orWhereNull`, `orWhereNotNull`, `orWhereBetween`,
|
|
8429
|
+
`orWhereColumn`, `orWhereLike`, `orWhereRaw`. `whereNot` negates a comparison;
|
|
8430
|
+
`whereNotBetween` is the inverse of `whereBetween`. Passing a **callback** to
|
|
8431
|
+
`where`/`orWhere` groups its conditions in parentheses.
|
|
8432
|
+
|
|
8433
|
+
#### `orderByDesc(column)` · `reorder(column?, direction?)` · `inRandomOrder()`
|
|
8434
|
+
|
|
8435
|
+
Descending order; clear existing ordering (optionally setting a new one); random
|
|
8436
|
+
order (dialect-aware `RANDOM()`/`RAND()`).
|
|
8437
|
+
|
|
8438
|
+
#### `groupByRaw(sql)` · `havingRaw(sql, bindings?)` · `havingBetween(column, [min, max])`
|
|
8439
|
+
|
|
8440
|
+
Raw `GROUP BY`, a raw/bound `HAVING`, and a `HAVING … BETWEEN`.
|
|
8441
|
+
|
|
8442
|
+
#### `take(n)` · `skip(n)` · `forPage(page, perPage?)`
|
|
8443
|
+
|
|
8444
|
+
Aliases for `limit`/`offset`, and limit+offset for a 1-based page.
|
|
8445
|
+
|
|
8446
|
+
#### `rightJoin(...)` · `crossJoin(table)`
|
|
8447
|
+
|
|
8448
|
+
`RIGHT JOIN` on an equality; `CROSS JOIN`.
|
|
8449
|
+
|
|
8450
|
+
#### `unless(condition, then, otherwise?)`
|
|
8451
|
+
|
|
8452
|
+
The inverse of `when` — runs `then` only when `condition` is falsy.
|
|
8453
|
+
|
|
8454
|
+
#### `find(id, key?)` · `firstOrFail()` · `sole()` · `doesntExist()` · `implode(column, glue?)`
|
|
8455
|
+
|
|
8456
|
+
Find by key (default `"id"`); first-or-throw; exactly-one-or-throw; the negation
|
|
8457
|
+
of `exists`; and join one column's values into a string.
|
|
8458
|
+
|
|
8459
|
+
#### `simplePaginate(page?, perPage?)`
|
|
8460
|
+
|
|
8461
|
+
`simplePaginate(page = 1, perPage = 15): Promise<SimplePaginated<T>>`
|
|
8462
|
+
|
|
8463
|
+
A page without a `COUNT` — fetches one extra row to set `hasMore`. Cheaper than
|
|
8464
|
+
`paginate` for "load more" UIs.
|
|
8465
|
+
|
|
8466
|
+
#### `lockForUpdate()` · `sharedLock()`
|
|
8467
|
+
|
|
8468
|
+
Add `FOR UPDATE` / `FOR SHARE` to the SELECT (inside a transaction). Ignored on
|
|
8469
|
+
sqlite.
|
|
8470
|
+
|
|
8471
|
+
#### `updateOrInsert(match, values?)` · `truncate()` · `incrementEach(cols, extra?)` · `decrementEach(cols, extra?)`
|
|
8472
|
+
|
|
8473
|
+
Update the first match or insert `{ ...match, ...values }`; empty the table
|
|
8474
|
+
(`DELETE` on sqlite); and step several numeric columns in one statement (`cols` is
|
|
8475
|
+
an array — each by 1 — or a `{ column: amount }` map).
|
|
8476
|
+
|
|
8477
|
+
#### `toSql()` · `getBindings()` · `dump()` · `dd()`
|
|
8478
|
+
|
|
8479
|
+
The compiled `?`-placeholder SQL and its bindings, without executing; `dump` logs
|
|
8480
|
+
them and returns the builder; `dd` logs and throws.
|
|
8481
|
+
|
|
8308
8482
|
### Interfaces & types
|
|
8309
8483
|
|
|
8310
8484
|
#### `Connection`
|
package/package.json
CHANGED