arn-db 0.1.0 → 0.2.1
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/GUIDE.md +108 -9
- package/README.md +58 -7
- package/dist/index.cjs +136 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -13
- package/dist/index.d.mts +114 -13
- package/dist/index.mjs +135 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -52,6 +52,47 @@ export declare class Now {
|
|
|
52
52
|
type Column = string | Col;
|
|
53
53
|
export declare const col: (name: string) => Col;
|
|
54
54
|
export declare const now: () => Now;
|
|
55
|
+
/**
|
|
56
|
+
* A raw SQL fragment: one expression inside the query the builder makes, with
|
|
57
|
+
* `?` for each binding (`\\?` is a literal `?`, e.g. the jsonb `?` operator).
|
|
58
|
+
* Bindings are sent as query parameters, never pasted into the SQL. Use it in
|
|
59
|
+
* `where()`, `order()`, `having()`, as a `select()` column with `.as()`, and as
|
|
60
|
+
* an insert/update/upsert value or upsert `merge` value. The API key must be
|
|
61
|
+
* allowed to use raw fragments.
|
|
62
|
+
*
|
|
63
|
+
* .where(db.raw('lower(email) = any(?)', [emails]))
|
|
64
|
+
* .update({ sent_total: db.raw('sent_total + ?', [1]) })
|
|
65
|
+
*/
|
|
66
|
+
export declare class Raw {
|
|
67
|
+
#private;
|
|
68
|
+
readonly sql: string;
|
|
69
|
+
readonly bindings: unknown[];
|
|
70
|
+
constructor(sql: string, bindings?: unknown[], as?: string);
|
|
71
|
+
/** Name of this fragment as a `select()` column: `raw('char_length(title)').as('title_len')`. */
|
|
72
|
+
as(name: string): Raw;
|
|
73
|
+
/** @internal The `.as()` name, if any. */
|
|
74
|
+
alias(): string | undefined;
|
|
75
|
+
/** @internal The fragment without its name, for filters, order and having. */
|
|
76
|
+
fragment(): {
|
|
77
|
+
$raw: string;
|
|
78
|
+
bindings: unknown[];
|
|
79
|
+
};
|
|
80
|
+
/** @internal Wire format. */
|
|
81
|
+
toJSON(): {
|
|
82
|
+
$raw: string;
|
|
83
|
+
bindings: unknown[];
|
|
84
|
+
as?: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A raw SQL fragment with `?` bindings: knex's `whereRaw` / `orderByRaw` /
|
|
89
|
+
* `havingRaw` / `knex.raw` value. Throws if `sql` is not a non-empty string
|
|
90
|
+
* or `bindings` not an array; the server checks the rest.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* db.from('users').select().where(raw('lower(trim(email)) = any(?)', [emails]))
|
|
94
|
+
*/
|
|
95
|
+
export declare const raw: (sql: string, bindings?: unknown[]) => Raw;
|
|
55
96
|
/**
|
|
56
97
|
* Atomic update operator, applied by Postgres to the current column value.
|
|
57
98
|
*
|
|
@@ -150,9 +191,21 @@ interface GroupFilter {
|
|
|
150
191
|
group: 'and' | 'or' | 'not';
|
|
151
192
|
filters: Filter[];
|
|
152
193
|
}
|
|
153
|
-
|
|
194
|
+
/** A raw SQL fragment on the wire: `?` placeholders and their values. */
|
|
195
|
+
interface RawFragment {
|
|
196
|
+
$raw: string;
|
|
197
|
+
bindings: unknown[];
|
|
198
|
+
}
|
|
199
|
+
/** A `where(raw(...))` condition: `AND (<fragment>)`, or one member of an OR group. */
|
|
200
|
+
interface RawFilter {
|
|
201
|
+
op: 'raw';
|
|
202
|
+
value: RawFragment;
|
|
203
|
+
}
|
|
204
|
+
type Filter = ConditionFilter | GroupFilter | RawFilter;
|
|
154
205
|
interface Order {
|
|
155
206
|
column?: string;
|
|
207
|
+
/** `order(raw(...))`: sort by the fragment instead of a column. */
|
|
208
|
+
raw?: RawFragment;
|
|
156
209
|
ascending?: boolean;
|
|
157
210
|
nullsFirst?: boolean;
|
|
158
211
|
random?: boolean;
|
|
@@ -180,8 +233,14 @@ interface QueryRequest {
|
|
|
180
233
|
upsert?: boolean;
|
|
181
234
|
onConflict?: string[];
|
|
182
235
|
ignoreDuplicates?: boolean;
|
|
183
|
-
merge?: Record<string, MergeMode | Op>;
|
|
236
|
+
merge?: Record<string, MergeMode | Op | Raw>;
|
|
184
237
|
groupBy?: string[];
|
|
238
|
+
/** `having(raw(...))` fragments, joined with AND. Only with `groupBy`. */
|
|
239
|
+
having?: RawFragment[];
|
|
240
|
+
/** `select(..., raw(...).as(name))` columns of the main table. */
|
|
241
|
+
rawColumns?: (RawFragment & {
|
|
242
|
+
as: string;
|
|
243
|
+
})[];
|
|
185
244
|
distinct?: boolean;
|
|
186
245
|
aggregate?: {
|
|
187
246
|
fn: AggregateFn;
|
|
@@ -210,14 +269,21 @@ interface UpsertOptions {
|
|
|
210
269
|
/**
|
|
211
270
|
* Change only these columns of an existing row; the rest keep their stored
|
|
212
271
|
* value. Each is a {@link MergeMode} or a `db.ops` value applied to the stored
|
|
213
|
-
* value
|
|
214
|
-
*
|
|
272
|
+
* value, or a `db.raw` fragment (`excluded.col` is the incoming value). A
|
|
273
|
+
* list of names means `'set'` for each. Without `merge`, every column you
|
|
274
|
+
* send (except the conflict key) is replaced.
|
|
215
275
|
*
|
|
216
276
|
* @example
|
|
217
277
|
* merge: { total: 'add', email: 'set', hits: db.ops.increment(1) }
|
|
278
|
+
* merge: { total: db.raw('greatest(accounts.total, excluded.total)') }
|
|
218
279
|
* merge: ['email', 'name']
|
|
219
280
|
*/
|
|
220
|
-
merge?: Record<string, MergeMode | Op> | string[];
|
|
281
|
+
merge?: Record<string, MergeMode | Op | Raw> | string[];
|
|
282
|
+
}
|
|
283
|
+
/** `select()` options: `count: 'exact'` also fills `count`; `head: true` skips the rows. */
|
|
284
|
+
interface SelectOptions {
|
|
285
|
+
count?: 'exact';
|
|
286
|
+
head?: boolean;
|
|
221
287
|
}
|
|
222
288
|
/** Options for `order()`, `random()` and `limit()` that target an embedded table. */
|
|
223
289
|
interface ReferencedTableOption {
|
|
@@ -235,6 +301,8 @@ interface TxStepRequest extends QueryRequest {
|
|
|
235
301
|
/** Filter methods, shared by builders and groups. */
|
|
236
302
|
declare abstract class Filters {
|
|
237
303
|
#private;
|
|
304
|
+
/** @internal A problem found while building, reported instead of sending. */
|
|
305
|
+
protected problem: string | undefined;
|
|
238
306
|
/** @internal Adds one filter or group. */
|
|
239
307
|
protected abstract push(filter: Filter): void;
|
|
240
308
|
/**
|
|
@@ -317,6 +385,17 @@ declare abstract class Filters {
|
|
|
317
385
|
* .match({ active: true, age: 30 })
|
|
318
386
|
*/
|
|
319
387
|
match(values: Row): this;
|
|
388
|
+
/**
|
|
389
|
+
* A raw SQL condition, knex's `whereRaw`: `AND (<fragment>)`, or one member
|
|
390
|
+
* of an `or()` group. Only takes `raw()`; for a column use `eq()` and friends.
|
|
391
|
+
* The fragment sees the main table's columns, not related tables.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* .where(db.raw('lower(trim(email)) = any(?)', [emails]))
|
|
395
|
+
* .where(db.raw('sent_per_hour < limit_per_hour'))
|
|
396
|
+
* .or((q) => q.where(db.raw('meta \\? ?', ['vip'])).eq('age', 30)) // jsonb ? operator
|
|
397
|
+
*/
|
|
398
|
+
where(fragment: Raw): this;
|
|
320
399
|
/**
|
|
321
400
|
* Any of the conditions.
|
|
322
401
|
*
|
|
@@ -364,8 +443,6 @@ declare abstract class Builder<T extends Row, R> extends Filters implements Prom
|
|
|
364
443
|
#private;
|
|
365
444
|
/** @internal */ protected readonly transport: Transport | null;
|
|
366
445
|
/** @internal */ protected readonly req: QueryRequest;
|
|
367
|
-
/** @internal A problem found while building, reported instead of sending. */
|
|
368
|
-
protected problem: string | undefined;
|
|
369
446
|
/** @internal */
|
|
370
447
|
constructor(transport: Transport | null, req: QueryRequest, name?: string, problem?: string);
|
|
371
448
|
/** @internal */
|
|
@@ -416,11 +493,14 @@ export declare class QueryBuilder<T extends Row = Row, R = T[]> extends Builder<
|
|
|
416
493
|
* db.from('users').select('name, posts!user_id!inner(title)') // only users that have posts
|
|
417
494
|
* db.from('users').select('name, posts!user_id(count)') // posts: [{ count: 2 }]
|
|
418
495
|
* db.from('posts').select('*', { count: 'exact' }).range(0, 9) // rows + total
|
|
496
|
+
*
|
|
497
|
+
* Extra `raw(...).as(name)` arguments add SQL expressions of the main table
|
|
498
|
+
* as columns (options, if any, go last).
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* db.from('posts').select('id, title', db.raw('char_length(title)').as('title_len'))
|
|
419
502
|
*/
|
|
420
|
-
select(columns?: string,
|
|
421
|
-
count?: 'exact';
|
|
422
|
-
head?: boolean;
|
|
423
|
-
}): QueryBuilder<T, T[]>;
|
|
503
|
+
select(columns?: string, ...extra: (Raw | SelectOptions)[]): QueryBuilder<T, T[]>;
|
|
424
504
|
/**
|
|
425
505
|
* Number of matching rows: `data` is the number. All filters apply, including
|
|
426
506
|
* jsonb paths and `relation.column` filters on `!inner` relations.
|
|
@@ -455,6 +535,14 @@ export declare class QueryBuilder<T extends Row = Row, R = T[]> extends Builder<
|
|
|
455
535
|
* db.from('posts').select('user_id, n:count(), last:max(created_at)').groupBy('user_id').order('n', { ascending: false })
|
|
456
536
|
*/
|
|
457
537
|
groupBy(columns: string): this;
|
|
538
|
+
/**
|
|
539
|
+
* Keep only the groups where the raw condition holds, knex's `havingRaw`.
|
|
540
|
+
* Use with `groupBy()`; several calls are joined with AND.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* db.from('posts').select('user_id, n:count()').groupBy('user_id').having(db.raw('count(*) >= ?', [2]))
|
|
544
|
+
*/
|
|
545
|
+
having(fragment: Raw): this;
|
|
458
546
|
/**
|
|
459
547
|
* Leave out repeated rows: `SELECT DISTINCT`.
|
|
460
548
|
*
|
|
@@ -540,8 +628,9 @@ export declare class QueryBuilder<T extends Row = Row, R = T[]> extends Builder<
|
|
|
540
628
|
* .order('active', { ascending: false }).order('age', { ascending: false }).order('name')
|
|
541
629
|
* .order('meta->>score::int', { ascending: false, nullsFirst: false })
|
|
542
630
|
* .order('title', { referencedTable: 'posts' }) // posts inside each user
|
|
631
|
+
* .order(db.raw("coalesce(last_read_time, '1970-01-01')")) // raw, main table only
|
|
543
632
|
*/
|
|
544
|
-
order(column: string, options?: {
|
|
633
|
+
order(column: string | Raw, options?: {
|
|
545
634
|
ascending?: boolean;
|
|
546
635
|
nullsFirst?: boolean;
|
|
547
636
|
} & ReferencedTableOption): this;
|
|
@@ -722,6 +811,8 @@ interface Tx {
|
|
|
722
811
|
readonly col: typeof col;
|
|
723
812
|
/** The database server's time, same as `db.now`. */
|
|
724
813
|
readonly now: typeof now;
|
|
814
|
+
/** A raw SQL fragment, same as `db.raw`. */
|
|
815
|
+
readonly raw: typeof raw;
|
|
725
816
|
}
|
|
726
817
|
interface Client {
|
|
727
818
|
from<T extends Row = Row>(table: string): QueryBuilder<T>;
|
|
@@ -756,6 +847,16 @@ interface Client {
|
|
|
756
847
|
* await db.from('accounts').update({ last_read_time: db.now() }).eq('id', id)
|
|
757
848
|
*/
|
|
758
849
|
readonly now: typeof now;
|
|
850
|
+
/**
|
|
851
|
+
* A raw SQL fragment with `?` bindings (same as `import { raw }`), for
|
|
852
|
+
* `where()`, `order()`, `having()`, `select()` columns and write values.
|
|
853
|
+
* The API key must be allowed to use raw fragments.
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* await db.from('accounts').select().where(db.raw('sent_per_hour < limit_per_hour'))
|
|
857
|
+
* await db.from('accounts').update({ sent_total: db.raw('sent_total + ?', [1]) }).eq('id', id)
|
|
858
|
+
*/
|
|
859
|
+
readonly raw: typeof raw;
|
|
759
860
|
/**
|
|
760
861
|
* Run several steps in one database transaction: all succeed or nothing is saved.
|
|
761
862
|
* On failure `error.step` / `error.stepName` say which step failed.
|
|
@@ -794,5 +895,5 @@ export declare function createClient(url: string, apiKey: string, options?: Clie
|
|
|
794
895
|
* const { data, error } = await db.from('users').select().eq('id', id).single()
|
|
795
896
|
*/
|
|
796
897
|
export declare const db: Client;
|
|
797
|
-
export type { AggregateFn, Client, ClientOptions, Column, ConditionFilter, Filter, FilterOp, GroupFilter, Key, MergeMode, Order, PgError, QueryRequest, QueryResult, ReadFrom, Ref, ReferencedTableOption, RelOp, RelOpName, Row, Step, StepData, Tx, TxStepRequest, UpsertOptions };
|
|
898
|
+
export type { AggregateFn, Client, ClientOptions, Column, ConditionFilter, Filter, FilterOp, GroupFilter, Key, MergeMode, Order, PgError, QueryRequest, QueryResult, RawFilter, RawFragment, ReadFrom, Ref, ReferencedTableOption, RelOp, RelOpName, Row, SelectOptions, Step, StepData, Tx, TxStepRequest, UpsertOptions };
|
|
798
899
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -151,10 +151,66 @@ var Now = class Now {
|
|
|
151
151
|
};
|
|
152
152
|
const col = (name) => new Col(name);
|
|
153
153
|
const now = () => new Now();
|
|
154
|
+
/**
|
|
155
|
+
* A raw SQL fragment: one expression inside the query the builder makes, with
|
|
156
|
+
* `?` for each binding (`\\?` is a literal `?`, e.g. the jsonb `?` operator).
|
|
157
|
+
* Bindings are sent as query parameters, never pasted into the SQL. Use it in
|
|
158
|
+
* `where()`, `order()`, `having()`, as a `select()` column with `.as()`, and as
|
|
159
|
+
* an insert/update/upsert value or upsert `merge` value. The API key must be
|
|
160
|
+
* allowed to use raw fragments.
|
|
161
|
+
*
|
|
162
|
+
* .where(db.raw('lower(email) = any(?)', [emails]))
|
|
163
|
+
* .update({ sent_total: db.raw('sent_total + ?', [1]) })
|
|
164
|
+
*/
|
|
165
|
+
var Raw = class Raw {
|
|
166
|
+
sql;
|
|
167
|
+
bindings;
|
|
168
|
+
#as;
|
|
169
|
+
constructor(sql, bindings = [], as) {
|
|
170
|
+
if (typeof sql !== "string" || sql.trim() === "") throw new Error("arn-db: raw(sql) needs a non-empty SQL string");
|
|
171
|
+
if (!Array.isArray(bindings)) throw new Error("arn-db: raw(sql, bindings) takes the bindings as an array");
|
|
172
|
+
this.sql = sql;
|
|
173
|
+
this.bindings = bindings;
|
|
174
|
+
this.#as = as;
|
|
175
|
+
}
|
|
176
|
+
/** Name of this fragment as a `select()` column: `raw('char_length(title)').as('title_len')`. */
|
|
177
|
+
as(name) {
|
|
178
|
+
return new Raw(this.sql, this.bindings, name);
|
|
179
|
+
}
|
|
180
|
+
/** @internal The `.as()` name, if any. */
|
|
181
|
+
alias() {
|
|
182
|
+
return this.#as;
|
|
183
|
+
}
|
|
184
|
+
/** @internal The fragment without its name, for filters, order and having. */
|
|
185
|
+
fragment() {
|
|
186
|
+
return {
|
|
187
|
+
$raw: this.sql,
|
|
188
|
+
bindings: this.bindings
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** @internal Wire format. */
|
|
192
|
+
toJSON() {
|
|
193
|
+
return this.#as === void 0 ? this.fragment() : {
|
|
194
|
+
...this.fragment(),
|
|
195
|
+
as: this.#as
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* A raw SQL fragment with `?` bindings: knex's `whereRaw` / `orderByRaw` /
|
|
201
|
+
* `havingRaw` / `knex.raw` value. Throws if `sql` is not a non-empty string
|
|
202
|
+
* or `bindings` not an array; the server checks the rest.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* db.from('users').select().where(raw('lower(trim(email)) = any(?)', [emails]))
|
|
206
|
+
*/
|
|
207
|
+
const raw = (sql, bindings) => new Raw(sql, bindings);
|
|
154
208
|
// src/builder.ts
|
|
155
209
|
const embedOf = (o) => o.referencedTable ?? o.foreignTable;
|
|
156
210
|
/** Filter methods, shared by builders and groups. */
|
|
157
211
|
var Filters = class {
|
|
212
|
+
/** @internal A problem found while building, reported instead of sending. */
|
|
213
|
+
problem;
|
|
158
214
|
#filter(column, op, value) {
|
|
159
215
|
if (column instanceof Col) {
|
|
160
216
|
const ifNull = column.nullValue();
|
|
@@ -287,6 +343,24 @@ var Filters = class {
|
|
|
287
343
|
return this;
|
|
288
344
|
}
|
|
289
345
|
/**
|
|
346
|
+
* A raw SQL condition, knex's `whereRaw`: `AND (<fragment>)`, or one member
|
|
347
|
+
* of an `or()` group. Only takes `raw()`; for a column use `eq()` and friends.
|
|
348
|
+
* The fragment sees the main table's columns, not related tables.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* .where(db.raw('lower(trim(email)) = any(?)', [emails]))
|
|
352
|
+
* .where(db.raw('sent_per_hour < limit_per_hour'))
|
|
353
|
+
* .or((q) => q.where(db.raw('meta \\? ?', ['vip'])).eq('age', 30)) // jsonb ? operator
|
|
354
|
+
*/
|
|
355
|
+
where(fragment) {
|
|
356
|
+
if (fragment instanceof Raw) this.push({
|
|
357
|
+
op: "raw",
|
|
358
|
+
value: fragment.fragment()
|
|
359
|
+
});
|
|
360
|
+
else this.problem ??= "where() takes a raw() fragment; for a column use eq(), gt(), in() and friends";
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
290
364
|
* Any of the conditions.
|
|
291
365
|
*
|
|
292
366
|
* @example
|
|
@@ -313,6 +387,7 @@ var Filters = class {
|
|
|
313
387
|
#group(group, conditions) {
|
|
314
388
|
const q = new FilterGroup();
|
|
315
389
|
conditions(q);
|
|
390
|
+
if (q.problem) this.problem ??= q.problem;
|
|
316
391
|
this.push({
|
|
317
392
|
group,
|
|
318
393
|
filters: q.filters
|
|
@@ -339,8 +414,6 @@ var FilterGroup = class extends Filters {
|
|
|
339
414
|
var Builder = class extends Filters {
|
|
340
415
|
/** @internal */ transport;
|
|
341
416
|
/** @internal */ req;
|
|
342
|
-
/** @internal A problem found while building, reported instead of sending. */
|
|
343
|
-
problem;
|
|
344
417
|
#name;
|
|
345
418
|
/** @internal */
|
|
346
419
|
constructor(transport, req, name, problem) {
|
|
@@ -444,11 +517,30 @@ var QueryBuilder = class extends Builder {
|
|
|
444
517
|
* db.from('users').select('name, posts!user_id!inner(title)') // only users that have posts
|
|
445
518
|
* db.from('users').select('name, posts!user_id(count)') // posts: [{ count: 2 }]
|
|
446
519
|
* db.from('posts').select('*', { count: 'exact' }).range(0, 9) // rows + total
|
|
520
|
+
*
|
|
521
|
+
* Extra `raw(...).as(name)` arguments add SQL expressions of the main table
|
|
522
|
+
* as columns (options, if any, go last).
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* db.from('posts').select('id, title', db.raw('char_length(title)').as('title_len'))
|
|
447
526
|
*/
|
|
448
|
-
select(columns = "*",
|
|
527
|
+
select(columns = "*", ...extra) {
|
|
449
528
|
this.req.columns = columns;
|
|
450
|
-
|
|
451
|
-
|
|
529
|
+
const rawColumns = [];
|
|
530
|
+
for (const e of extra) if (e instanceof Raw) {
|
|
531
|
+
const as = e.alias();
|
|
532
|
+
if (as === void 0) this.problem ??= `raw("${e.sql}") in select() needs a column name: add .as('name')`;
|
|
533
|
+
else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(as)) this.problem ??= `raw(...).as("${as}"): the name must be letters, digits and _`;
|
|
534
|
+
else rawColumns.push({
|
|
535
|
+
...e.fragment(),
|
|
536
|
+
as
|
|
537
|
+
});
|
|
538
|
+
} else if (e) {
|
|
539
|
+
if (e.count) this.req.count = true;
|
|
540
|
+
if (e.head) this.req.head = true;
|
|
541
|
+
}
|
|
542
|
+
if (rawColumns.length) this.req.rawColumns = rawColumns;
|
|
543
|
+
else delete this.req.rawColumns;
|
|
452
544
|
return this;
|
|
453
545
|
}
|
|
454
546
|
/**
|
|
@@ -510,6 +602,18 @@ var QueryBuilder = class extends Builder {
|
|
|
510
602
|
return this;
|
|
511
603
|
}
|
|
512
604
|
/**
|
|
605
|
+
* Keep only the groups where the raw condition holds, knex's `havingRaw`.
|
|
606
|
+
* Use with `groupBy()`; several calls are joined with AND.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* db.from('posts').select('user_id, n:count()').groupBy('user_id').having(db.raw('count(*) >= ?', [2]))
|
|
610
|
+
*/
|
|
611
|
+
having(fragment) {
|
|
612
|
+
if (fragment instanceof Raw) (this.req.having ??= []).push(fragment.fragment());
|
|
613
|
+
else this.problem ??= "having() takes a raw() fragment: having(db.raw(\"count(*) >= ?\", [2]))";
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
513
617
|
* Leave out repeated rows: `SELECT DISTINCT`.
|
|
514
618
|
*
|
|
515
619
|
* @example
|
|
@@ -636,9 +740,19 @@ var QueryBuilder = class extends Builder {
|
|
|
636
740
|
* .order('active', { ascending: false }).order('age', { ascending: false }).order('name')
|
|
637
741
|
* .order('meta->>score::int', { ascending: false, nullsFirst: false })
|
|
638
742
|
* .order('title', { referencedTable: 'posts' }) // posts inside each user
|
|
743
|
+
* .order(db.raw("coalesce(last_read_time, '1970-01-01')")) // raw, main table only
|
|
639
744
|
*/
|
|
640
745
|
order(column, options = {}) {
|
|
641
746
|
const { ascending = true, nullsFirst } = options;
|
|
747
|
+
if (column instanceof Raw) {
|
|
748
|
+
if (embedOf(options)) this.problem ??= "order(raw(...)) sorts the main table only: drop referencedTable";
|
|
749
|
+
else this.req.order.push({
|
|
750
|
+
raw: column.fragment(),
|
|
751
|
+
ascending,
|
|
752
|
+
nullsFirst
|
|
753
|
+
});
|
|
754
|
+
return this;
|
|
755
|
+
}
|
|
642
756
|
this.req.order.push({
|
|
643
757
|
column,
|
|
644
758
|
ascending,
|
|
@@ -920,6 +1034,7 @@ function createClient(url, apiKey, options = {}) {
|
|
|
920
1034
|
rel,
|
|
921
1035
|
col,
|
|
922
1036
|
now,
|
|
1037
|
+
raw,
|
|
923
1038
|
async transaction(build) {
|
|
924
1039
|
const steps = toSteps(build({
|
|
925
1040
|
from: (table) => new QueryBuilder(null, table),
|
|
@@ -927,7 +1042,8 @@ function createClient(url, apiKey, options = {}) {
|
|
|
927
1042
|
ops,
|
|
928
1043
|
rel,
|
|
929
1044
|
col,
|
|
930
|
-
now
|
|
1045
|
+
now,
|
|
1046
|
+
raw
|
|
931
1047
|
}));
|
|
932
1048
|
if (typeof steps === "string") return fail(steps);
|
|
933
1049
|
const res = await transport.post("/transaction", { steps: steps.wire });
|
|
@@ -943,14 +1059,20 @@ function createClient(url, apiKey, options = {}) {
|
|
|
943
1059
|
}
|
|
944
1060
|
// src/env.ts
|
|
945
1061
|
let client;
|
|
1062
|
+
const proc = globalThis.process;
|
|
1063
|
+
/** Reads ./.env into process.env (Node 20.12+; never overrides set vars). */
|
|
1064
|
+
function loadDotEnv() {
|
|
1065
|
+
if (!proc?.loadEnvFile) return;
|
|
1066
|
+
try {
|
|
1067
|
+
proc.loadEnvFile();
|
|
1068
|
+
} catch {}
|
|
1069
|
+
}
|
|
1070
|
+
loadDotEnv();
|
|
946
1071
|
/** The ARN_DB_URL / ARN_DB_KEY client, made on the first query (so a later dotenv still counts). */
|
|
947
1072
|
function envClient() {
|
|
948
1073
|
if (client) return client;
|
|
949
|
-
const proc = globalThis.process;
|
|
950
1074
|
if (!proc) throw new Error("arn-db: `db` reads ARN_DB_URL / ARN_DB_KEY from process.env; use createClient(url, key) here");
|
|
951
|
-
if (
|
|
952
|
-
proc.loadEnvFile();
|
|
953
|
-
} catch {}
|
|
1075
|
+
if (!proc.env.ARN_DB_URL || !proc.env.ARN_DB_KEY) loadDotEnv();
|
|
954
1076
|
const url = proc.env.ARN_DB_URL;
|
|
955
1077
|
const key = proc.env.ARN_DB_KEY;
|
|
956
1078
|
if (!url) throw new Error("arn-db: set ARN_DB_URL (e.g. https://your-api.example.com/v2/raw) or use createClient(url, key)");
|
|
@@ -974,8 +1096,9 @@ const db = {
|
|
|
974
1096
|
ops,
|
|
975
1097
|
rel,
|
|
976
1098
|
col,
|
|
977
|
-
now
|
|
1099
|
+
now,
|
|
1100
|
+
raw
|
|
978
1101
|
};
|
|
979
|
-
export { Col, FilterGroup, Now, Op, QueryBuilder, WriteBuilder, col, createClient, db, now, ops, rel };
|
|
1102
|
+
export { Col, FilterGroup, Now, Op, QueryBuilder, Raw, WriteBuilder, col, createClient, db, now, ops, raw, rel };
|
|
980
1103
|
|
|
981
1104
|
//# sourceMappingURL=index.mjs.map
|