arn-db 0.0.0-stage → 0.1.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/index.cjs ADDED
@@ -0,0 +1,993 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/rel.ts
3
+ const list = (v) => Array.isArray(v) ? v : [v];
4
+ /** "user_id, title" -> { onConflict: ['user_id', 'title'] } */
5
+ const conflict = (columns) => columns ? { onConflict: columns.split(",").map((c) => c.trim()).filter(Boolean) } : {};
6
+ const rel = {
7
+ /**
8
+ * Insert related rows (they may contain `rel.*` themselves).
9
+ * @example
10
+ * db.from('posts').insert({ title, user_id, comments: rel.create([{ body, author_id }]) })
11
+ */
12
+ create: (rows) => ({
13
+ $rel: "create",
14
+ rows: list(rows)
15
+ }),
16
+ /**
17
+ * Insert one related row, or update it if its `onConflict` key (default: primary key) exists.
18
+ * @example
19
+ * db.from('posts').update({ comments: rel.upsert({ id: commentId, body: 'v2', author_id }) }).eq('id', postId)
20
+ */
21
+ upsert: (row, options = {}) => ({
22
+ $rel: "upsert",
23
+ rows: [row],
24
+ ...conflict(options.onConflict)
25
+ }),
26
+ /**
27
+ * Find one related row by its `onConflict` key (default: primary key), or create it,
28
+ * then link it. An existing row is not changed.
29
+ * @example
30
+ * db.from('posts').insert({ title, user_id, labels: rel.connectOrCreate({ name: 'go' }, { onConflict: 'name' }) })
31
+ */
32
+ connectOrCreate: (row, options = {}) => ({
33
+ $rel: "connectOrCreate",
34
+ rows: [row],
35
+ ...conflict(options.onConflict)
36
+ }),
37
+ /**
38
+ * Link existing rows by key. Fails (and rolls back) if a key does not exist.
39
+ * @example
40
+ * db.from('posts').update({ labels: rel.connect([labelA, labelB]) }).eq('id', postId)
41
+ */
42
+ connect: (keys) => ({
43
+ $rel: "connect",
44
+ keys: list(keys)
45
+ }),
46
+ /**
47
+ * Unlink rows by key (many-to-one: clears the FK; one-to-many: sets the child FK to null).
48
+ * @example
49
+ * db.from('posts').update({ labels: rel.disconnect(labelId) }).eq('id', postId)
50
+ */
51
+ disconnect: (keys = []) => ({
52
+ $rel: "disconnect",
53
+ keys: list(keys)
54
+ }),
55
+ /**
56
+ * Make exactly these rows linked: unlinks the others, links the missing ones.
57
+ * @example
58
+ * db.from('posts').update({ labels: rel.set([labelA]) }).eq('id', postId) // rel.set([]) unlinks all
59
+ */
60
+ set: (keys) => ({
61
+ $rel: "set",
62
+ keys
63
+ }),
64
+ /**
65
+ * Update linked rows that match (equality on every key of `match`).
66
+ * @example
67
+ * db.from('posts').update({ comments: rel.update({ id: commentId }, { body: 'edited' }) }).eq('id', postId)
68
+ */
69
+ update: (match, values) => ({
70
+ $rel: "update",
71
+ match,
72
+ values
73
+ }),
74
+ /**
75
+ * Delete linked rows that match; `rel.delete()` deletes all linked rows.
76
+ * @example
77
+ * db.from('posts').update({ comments: rel.delete({ body: 'spam' }) }).eq('id', postId)
78
+ */
79
+ delete: (match = {}) => ({
80
+ $rel: "delete",
81
+ match
82
+ })
83
+ };
84
+ // src/values.ts
85
+ /**
86
+ * Values the database fills in: another column of the same row (`db.col`)
87
+ * and the database's current time (`db.now`).
88
+ */
89
+ /**
90
+ * Another column of the same row. As a filter value it compares two columns;
91
+ * as the column of a filter it lets `ifNull()` replace NULL first.
92
+ *
93
+ * .lt('sent_per_hour', db.col('limit_per_hour'))
94
+ * .lt(db.col('sent_per_hour').ifNull(0), db.col('limit_per_hour'))
95
+ */
96
+ var Col = class Col {
97
+ name;
98
+ #ifNull;
99
+ constructor(name, ifNull) {
100
+ this.name = name;
101
+ this.#ifNull = ifNull;
102
+ }
103
+ /** Use `value` where the column is NULL: `COALESCE(column, value)`. */
104
+ ifNull(value) {
105
+ return new Col(this.name, { value });
106
+ }
107
+ /** @internal The NULL replacement, if any. */
108
+ nullValue() {
109
+ return this.#ifNull;
110
+ }
111
+ /** @internal Wire format. */
112
+ toJSON() {
113
+ return this.#ifNull ? {
114
+ $col: this.name,
115
+ ifNull: this.#ifNull.value
116
+ } : { $col: this.name };
117
+ }
118
+ };
119
+ /**
120
+ * The database server's `now()`, optionally shifted by one interval like
121
+ * `'1 hour'` or `'30 minutes'` (units: second, minute, hour, day, week,
122
+ * month, year). Works in filters and in insert/update values.
123
+ *
124
+ * .lte('client_sent_time', db.now().minus('1 hour'))
125
+ * .update({ last_read_time: db.now() })
126
+ */
127
+ var Now = class Now {
128
+ #interval;
129
+ constructor(interval) {
130
+ this.#interval = interval;
131
+ }
132
+ /** Later by `interval`: `db.now().plus('1 day')`. */
133
+ plus(interval) {
134
+ return this.#shift(interval.trim());
135
+ }
136
+ /** Earlier by `interval`: `db.now().minus('1 hour')`. */
137
+ minus(interval) {
138
+ const i = interval.trim();
139
+ return this.#shift(i.startsWith("-") ? i.slice(1) : `-${i}`);
140
+ }
141
+ #shift(interval) {
142
+ if (this.#interval !== void 0) throw new Error("arn-db: db.now() takes one plus() or minus(); use a single interval");
143
+ return new Now(interval);
144
+ }
145
+ /** @internal Wire format. */
146
+ toJSON() {
147
+ return this.#interval === void 0 ? { $now: true } : {
148
+ $now: true,
149
+ interval: this.#interval
150
+ };
151
+ }
152
+ };
153
+ const col = (name) => new Col(name);
154
+ const now = () => new Now();
155
+ // src/builder.ts
156
+ const embedOf = (o) => o.referencedTable ?? o.foreignTable;
157
+ /** Filter methods, shared by builders and groups. */
158
+ var Filters = class {
159
+ #filter(column, op, value) {
160
+ if (column instanceof Col) {
161
+ const ifNull = column.nullValue();
162
+ this.push(ifNull ? {
163
+ column: column.name,
164
+ op,
165
+ value,
166
+ ifNull: ifNull.value
167
+ } : {
168
+ column: column.name,
169
+ op,
170
+ value
171
+ });
172
+ } else this.push({
173
+ column,
174
+ op,
175
+ value
176
+ });
177
+ return this;
178
+ }
179
+ /**
180
+ * Equal. Works on columns, jsonb paths and related tables.
181
+ *
182
+ * @example
183
+ * .eq('active', true)
184
+ * .eq('meta->>city', 'Tokyo')
185
+ * .eq('posts.published', true) // filters the related posts
186
+ */
187
+ eq(column, value) {
188
+ return this.#filter(column, "eq", value);
189
+ }
190
+ /** Not equal. */
191
+ neq(column, value) {
192
+ return this.#filter(column, "neq", value);
193
+ }
194
+ /**
195
+ * Greater than. For numbers inside jsonb, cast: `.gt('meta->>score::int', 90)`.
196
+ * `eq` to `lte` also compare with another column (`db.col`) or the
197
+ * database time (`db.now`).
198
+ *
199
+ * @example
200
+ * .gt('age', 30)
201
+ * .lt('sent_per_hour', db.col('limit_per_hour'))
202
+ * .lt(db.col('sent_per_hour').ifNull(0), db.col('limit_per_hour'))
203
+ * .lte('client_sent_time', db.now().minus('1 hour'))
204
+ */
205
+ gt(column, value) {
206
+ return this.#filter(column, "gt", value);
207
+ }
208
+ /** Greater than or equal. @example .gte('meta->>score::int', 90) */
209
+ gte(column, value) {
210
+ return this.#filter(column, "gte", value);
211
+ }
212
+ /** Less than. */
213
+ lt(column, value) {
214
+ return this.#filter(column, "lt", value);
215
+ }
216
+ /** Less than or equal. */
217
+ lte(column, value) {
218
+ return this.#filter(column, "lte", value);
219
+ }
220
+ /**
221
+ * SQL LIKE: `%` is any text, `_` is one character (escape with `\\_`).
222
+ *
223
+ * @example
224
+ * .like('name', 'user\\_1%')
225
+ */
226
+ like(column, pattern) {
227
+ return this.#filter(column, "like", pattern);
228
+ }
229
+ /**
230
+ * Case-insensitive LIKE.
231
+ *
232
+ * @example
233
+ * .ilike('email', '%@example.com')
234
+ * .ilike('posts.comments.body', '%hello%') // search a deeper related table
235
+ */
236
+ ilike(column, pattern) {
237
+ return this.#filter(column, "ilike", pattern);
238
+ }
239
+ /** IS NULL / IS TRUE / IS FALSE. @example .is('deleted_at', null) */
240
+ is(column, value) {
241
+ return this.#filter(column, "is", value);
242
+ }
243
+ /** One of the values. @example .in('name', ['user_3', 'user_5']) */
244
+ in(column, values) {
245
+ return this.#filter(column, "in", values);
246
+ }
247
+ /**
248
+ * Has ALL of the values (arrays), or has these keys and values (jsonb objects).
249
+ *
250
+ * @example
251
+ * .contains('tags', ['one', 'two'])
252
+ * .contains('meta', { city: 'Dhaka', vip: true })
253
+ */
254
+ contains(column, value) {
255
+ return this.#filter(column, "cs", value);
256
+ }
257
+ /**
258
+ * Has nothing outside the values.
259
+ *
260
+ * @example
261
+ * .containedBy('tags', ['one', 'two', 'three'])
262
+ */
263
+ containedBy(column, value) {
264
+ return this.#filter(column, "cd", value);
265
+ }
266
+ /**
267
+ * Has ANY of the values. Works on jsonb/json arrays, jsonb paths and Postgres arrays.
268
+ *
269
+ * @example
270
+ * .overlaps('scores', ['one', 'three'])
271
+ * .overlaps('meta->tags', ['red', 'blue'])
272
+ */
273
+ overlaps(column, values) {
274
+ return this.#filter(column, "ov", values);
275
+ }
276
+ /** Any filter by name: `.filter('age', 'gte', 30)`. */
277
+ filter(column, op, value) {
278
+ return this.#filter(column, op, value);
279
+ }
280
+ /**
281
+ * Equal on every key.
282
+ *
283
+ * @example
284
+ * .match({ active: true, age: 30 })
285
+ */
286
+ match(values) {
287
+ for (const [k, v] of Object.entries(values)) this.eq(k, v);
288
+ return this;
289
+ }
290
+ /**
291
+ * Any of the conditions.
292
+ *
293
+ * @example
294
+ * .or((q) => q.gt('age', 30).eq('meta->>city', 'Dhaka')) // age > 30 OR city = 'Dhaka'
295
+ * .or((q) => q.eq('role', 'admin').and((q) => q.gte('age', 18).is('verified', true)))
296
+ * .or((q) => q.eq('posts.published', true).gt('posts.likes', 10)) // filters the related posts
297
+ */
298
+ or(conditions) {
299
+ return this.#group("or", conditions);
300
+ }
301
+ /**
302
+ * All of the conditions, as one group. Mostly useful inside `or()`.
303
+ *
304
+ * @example
305
+ * .or((q) => q.eq('role', 'admin').and((q) => q.gte('age', 18).eq('active', true)))
306
+ */
307
+ and(conditions) {
308
+ return this.#group("and", conditions);
309
+ }
310
+ not(a, op, value) {
311
+ if (typeof a === "function") return this.#group("not", a);
312
+ return this.#group("not", (q) => q.filter(a, op, value));
313
+ }
314
+ #group(group, conditions) {
315
+ const q = new FilterGroup();
316
+ conditions(q);
317
+ this.push({
318
+ group,
319
+ filters: q.filters
320
+ });
321
+ return this;
322
+ }
323
+ };
324
+ /**
325
+ * The conditions inside `or()`, `and()` or `not()`: every filter method, plus
326
+ * nested groups. Chain them on `q`; the callback's return value is ignored.
327
+ */
328
+ var FilterGroup = class extends Filters {
329
+ /** @internal */ filters = [];
330
+ /** @internal */
331
+ push(filter) {
332
+ this.filters.push(filter);
333
+ }
334
+ };
335
+ /**
336
+ * What reads and writes share: filters, step names and sending. Awaiting a
337
+ * builder sends the request and resolves to `{ data, error, count, status }`;
338
+ * it never throws. `T` is the row type, `R` the type of `data`.
339
+ */
340
+ var Builder = class extends Filters {
341
+ /** @internal */ transport;
342
+ /** @internal */ req;
343
+ /** @internal A problem found while building, reported instead of sending. */
344
+ problem;
345
+ #name;
346
+ /** @internal */
347
+ constructor(transport, req, name, problem) {
348
+ super();
349
+ this.transport = transport;
350
+ this.req = req;
351
+ this.#name = name;
352
+ this.problem = problem;
353
+ }
354
+ /** @internal */
355
+ push(filter) {
356
+ this.req.filters.push(filter);
357
+ }
358
+ /**
359
+ * Name a step in the array form of `transaction()`, so later steps can use
360
+ * its rows with `tx.ref('name.column')`. (In the object form the key is the name.)
361
+ *
362
+ * @example
363
+ * db.transaction((tx) => [
364
+ * tx.from('posts').insert({ title, user_id }).returning('id').as('post'),
365
+ * tx.from('comments').insert({ post_id: tx.ref('post.id'), body, author_id }),
366
+ * ])
367
+ */
368
+ as(name) {
369
+ this.#name = name;
370
+ return this;
371
+ }
372
+ /** @internal The `.as()` name, if any. */
373
+ stepName() {
374
+ return this.#name;
375
+ }
376
+ /** @internal A problem found while building, reported instead of sending. */
377
+ invalid() {
378
+ return this.problem;
379
+ }
380
+ /** @internal The request as sent to POST /query. */
381
+ request() {
382
+ return this.req;
383
+ }
384
+ /** @internal The request as sent to the server. */
385
+ toStep() {
386
+ return this.#name ? {
387
+ name: this.#name,
388
+ ...this.req
389
+ } : { ...this.req };
390
+ }
391
+ then(onfulfilled, onrejected) {
392
+ const run = async () => {
393
+ const invalid = this.invalid();
394
+ if (invalid) return {
395
+ data: null,
396
+ error: { message: invalid },
397
+ count: null,
398
+ status: 0
399
+ };
400
+ if (!this.transport) return {
401
+ data: null,
402
+ error: { message: "this query belongs to a transaction: return it from the transaction callback instead of awaiting it" },
403
+ count: null,
404
+ status: 0
405
+ };
406
+ return this.transport.post("/query", this.request());
407
+ };
408
+ return run().then(onfulfilled, onrejected);
409
+ }
410
+ };
411
+ /**
412
+ * Supabase-style query builder. Reads use `select()` and its modifiers;
413
+ * `insert()` / `upsert()` / `update()` / `delete()` turn it into a
414
+ * {@link WriteBuilder}. See GUIDE.md for recipes.
415
+ *
416
+ * @example
417
+ * const { data, error } = await db.from('users').select('id, name').eq('active', true).random().limit(5)
418
+ */
419
+ var QueryBuilder = class extends Builder {
420
+ #single = false;
421
+ #defaultReadFrom;
422
+ /** @internal Use `client.from()` or `tx.from()`. */
423
+ constructor(transport, table, readFrom) {
424
+ super(transport, {
425
+ table,
426
+ action: "select",
427
+ columns: "*",
428
+ filters: [],
429
+ order: [],
430
+ returning: false,
431
+ count: false
432
+ });
433
+ this.#defaultReadFrom = readFrom;
434
+ }
435
+ /**
436
+ * Columns to return. `'*'` (the default) means all. Supports aliases, jsonb
437
+ * paths, casts, related tables and related counts.
438
+ *
439
+ * `{ count: 'exact' }` also fills `count`; add `head: true` to skip the rows.
440
+ *
441
+ * @example
442
+ * db.from('users').select() // all columns
443
+ * db.from('users').select('id, fullName:name, city:meta->>city, score:meta->>score::int')
444
+ * db.from('posts').select('title, author:users!user_id(name), comments(body), labels(name)')
445
+ * db.from('users').select('name, posts!user_id!inner(title)') // only users that have posts
446
+ * db.from('users').select('name, posts!user_id(count)') // posts: [{ count: 2 }]
447
+ * db.from('posts').select('*', { count: 'exact' }).range(0, 9) // rows + total
448
+ */
449
+ select(columns = "*", options = {}) {
450
+ this.req.columns = columns;
451
+ if (options.count) this.req.count = true;
452
+ if (options.head) this.req.head = true;
453
+ return this;
454
+ }
455
+ /**
456
+ * Number of matching rows: `data` is the number. All filters apply, including
457
+ * jsonb paths and `relation.column` filters on `!inner` relations.
458
+ *
459
+ * @example
460
+ * const { data: total } = await db.from('users').count() // 100
461
+ * const { data: active } = await db.from('users').eq('active', true).count() // 75
462
+ */
463
+ count(options = {}) {
464
+ if (options.distinct) return this.#aggregate("count", options.distinct, true);
465
+ this.req.countOnly = true;
466
+ return this;
467
+ }
468
+ /**
469
+ * Smallest value of a column over the matching rows: `data` is that value,
470
+ * or `null` when no rows match. `max`, `sum` and `avg` work the same way.
471
+ *
472
+ * @example
473
+ * const { data: first } = await db.from('posts').eq('user_id', id).min('created_at')
474
+ */
475
+ min(column) {
476
+ return this.#aggregate("min", column);
477
+ }
478
+ /** Largest value. @example .eq('email', email).max('created_at') */
479
+ max(column) {
480
+ return this.#aggregate("max", column);
481
+ }
482
+ /** Sum of a number column, `null` when no rows match. */
483
+ sum(column) {
484
+ return this.#aggregate("sum", column);
485
+ }
486
+ /** Average of a number column, `null` when no rows match. */
487
+ avg(column) {
488
+ return this.#aggregate("avg", column);
489
+ }
490
+ #aggregate(fn, column, distinct) {
491
+ this.req.aggregate = distinct ? {
492
+ fn,
493
+ column,
494
+ distinct
495
+ } : {
496
+ fn,
497
+ column
498
+ };
499
+ return this;
500
+ }
501
+ /**
502
+ * Group rows for aggregates in `select()`: `count()`, `sum(col)`, `avg(col)`,
503
+ * `min(col)`, `max(col)`, `count(distinct col)`. Every plain column in the
504
+ * select must be grouped. `order()` can use an aggregate's name.
505
+ *
506
+ * @example
507
+ * db.from('posts').select('user_id, n:count(), last:max(created_at)').groupBy('user_id').order('n', { ascending: false })
508
+ */
509
+ groupBy(columns) {
510
+ this.req.groupBy = columns.split(",").map((c) => c.trim()).filter(Boolean);
511
+ return this;
512
+ }
513
+ /**
514
+ * Leave out repeated rows: `SELECT DISTINCT`.
515
+ *
516
+ * @example
517
+ * db.from('posts').select('user_id').distinct()
518
+ */
519
+ distinct() {
520
+ this.req.distinct = true;
521
+ return this;
522
+ }
523
+ /**
524
+ * Insert one or more rows. Values may contain `rel.*` to write related rows
525
+ * in the same transaction. Add `.returning()` to get the new rows back.
526
+ *
527
+ * @example
528
+ * await db.from('users').insert({ name, email }) // data: null, count: 1
529
+ * const { data } = await db.from('users').insert({ name, email }).returning('id') // [{ id }]
530
+ * await db.from('posts').insert({ title, user_id, comments: rel.create([{ body, author_id }]) })
531
+ */
532
+ insert(values) {
533
+ return this.#write("insert", Array.isArray(values) ? values : [values]);
534
+ }
535
+ /**
536
+ * Insert rows, or handle an existing row with the same `onConflict` key
537
+ * (needs a unique constraint; default: primary key): update it, or with
538
+ * `ignoreDuplicates: true` return it unchanged (find-or-create). `merge`
539
+ * picks which columns change and how. Up to 5,000 rows per call, each key
540
+ * at most once.
541
+ *
542
+ * @example
543
+ * // find-or-create
544
+ * const { data } = await db.from('users')
545
+ * .upsert({ email, name }, { onConflict: 'email', ignoreDuplicates: true })
546
+ * .returning('id')
547
+ * data[0].id
548
+ * // insert or update
549
+ * await db.from('users').upsert({ email, name }, { onConflict: 'email' })
550
+ * // many rows: add the counters, set the rest
551
+ * await db.from('posts_stats').upsert(rows, {
552
+ * onConflict: 'post_id',
553
+ * merge: { views: 'add', likes: 'add', last_viewer_id: 'set' },
554
+ * })
555
+ */
556
+ upsert(rows, options = {}) {
557
+ const { onConflict } = conflict(options.onConflict);
558
+ const { merge } = options;
559
+ return this.#write("insert", Array.isArray(rows) ? rows : [rows], {
560
+ upsert: true,
561
+ ...options.ignoreDuplicates && { ignoreDuplicates: true },
562
+ ...onConflict && { onConflict },
563
+ ...merge && { merge: Array.isArray(merge) ? Object.fromEntries(merge.map((c) => [c, "set"])) : merge }
564
+ });
565
+ }
566
+ /**
567
+ * Update every matching row (needs at least one filter). Values may be plain
568
+ * (full replace), `ops.*` (atomic change) or `rel.*` (related rows).
569
+ *
570
+ * @example
571
+ * await db.from('users').update({ name: 'New' }).eq('id', id)
572
+ * await db.from('users').update({ scores: ops.append(4), age: ops.increment() }).eq('id', id).returning('age')
573
+ * await db.from('posts').update({ labels: rel.set([labelId]) }).eq('id', postId)
574
+ */
575
+ update(values) {
576
+ return this.#write("update", [values]);
577
+ }
578
+ /**
579
+ * Delete every matching row (needs at least one filter).
580
+ *
581
+ * @example
582
+ * await db.from('posts').delete().eq('id', postId)
583
+ * await db.from('posts').delete().eq('id', postId).returning('title') // the deleted rows
584
+ */
585
+ delete() {
586
+ return this.#write("delete", void 0);
587
+ }
588
+ #write(action, values, extra = {}, problem) {
589
+ const { readFrom, ...rest } = this.req;
590
+ const req = {
591
+ ...rest,
592
+ ...extra,
593
+ action,
594
+ columns: "*"
595
+ };
596
+ if (values) req.values = values;
597
+ if (readFrom === "replica") problem ??= `replica() only works with select(); ${action} always runs on the primary`;
598
+ return new WriteBuilder(this.transport, req, this.stepName(), problem ?? this.invalid());
599
+ }
600
+ /**
601
+ * Let this read run on a read replica. Replicas can be a moment behind the
602
+ * primary, so use it where slightly stale rows are fine (lists, search,
603
+ * counts), not right after writing the rows you read. Without a healthy
604
+ * replica on the server it reads from the primary. Reads only, not inside
605
+ * a transaction.
606
+ *
607
+ * @example
608
+ * await db.from('posts').select('title').eq('published', true).replica()
609
+ */
610
+ replica() {
611
+ this.req.readFrom = "replica";
612
+ return this;
613
+ }
614
+ /**
615
+ * Read from the primary, overriding `createClient(..., { readFrom: 'replica' })`.
616
+ *
617
+ * @example
618
+ * await db.from('users').select().eq('id', id).primary() // just wrote it: read your own write
619
+ */
620
+ primary() {
621
+ this.req.readFrom = "primary";
622
+ return this;
623
+ }
624
+ /** @internal Applies the client's readFrom default to reads. */
625
+ request() {
626
+ if (this.req.readFrom || !this.#defaultReadFrom) return this.req;
627
+ return {
628
+ ...this.req,
629
+ readFrom: this.#defaultReadFrom
630
+ };
631
+ }
632
+ /**
633
+ * Sort. Chain several: they apply in the order written. Ascending by default.
634
+ * With `referencedTable` it sorts the rows inside a relation.
635
+ *
636
+ * @example
637
+ * .order('active', { ascending: false }).order('age', { ascending: false }).order('name')
638
+ * .order('meta->>score::int', { ascending: false, nullsFirst: false })
639
+ * .order('title', { referencedTable: 'posts' }) // posts inside each user
640
+ */
641
+ order(column, options = {}) {
642
+ const { ascending = true, nullsFirst } = options;
643
+ this.req.order.push({
644
+ column,
645
+ ascending,
646
+ nullsFirst,
647
+ table: embedOf(options)
648
+ });
649
+ return this;
650
+ }
651
+ /**
652
+ * Random order. After other `order()` calls it only shuffles ties. With a
653
+ * `seed` the order is stable (same seed, same order), so `range()` pages never
654
+ * repeat or skip rows.
655
+ *
656
+ * @example
657
+ * .random().limit(5) // 5 random rows
658
+ * .order('active', { ascending: false }).random() // active first, each group shuffled
659
+ * .random({ seed: sessionId }).range(10, 19) // stable page 2
660
+ * .random({ referencedTable: 'posts' }).limit(3, { referencedTable: 'posts' }) // 3 random posts each
661
+ */
662
+ random(options = {}) {
663
+ const seed = options.seed === void 0 ? void 0 : String(options.seed);
664
+ this.req.order.push({
665
+ random: true,
666
+ seed,
667
+ table: embedOf(options)
668
+ });
669
+ return this;
670
+ }
671
+ /**
672
+ * At most `count` rows: always an array, `[]` when none match, even for
673
+ * `limit(1)`. With `referencedTable`, at most `count` related rows per parent.
674
+ *
675
+ * @example
676
+ * .limit(10)
677
+ * .limit(3, { referencedTable: 'posts' })
678
+ */
679
+ limit(count, options = {}) {
680
+ const embed = embedOf(options);
681
+ if (embed) this.req.embedLimits = {
682
+ ...this.req.embedLimits,
683
+ [embed]: count
684
+ };
685
+ else this.req.limit = count;
686
+ return this;
687
+ }
688
+ /**
689
+ * Rows `from` to `to`, inclusive, 0-based.
690
+ *
691
+ * @example
692
+ * .range(0, 9) // page 1
693
+ * .range(10, 19) // page 2
694
+ */
695
+ range(from, to) {
696
+ this.req.offset = from;
697
+ this.req.limit = to - from + 1;
698
+ return this;
699
+ }
700
+ /**
701
+ * One row: `data` is that row as an object, or `null` when nothing matches
702
+ * (not an error). Adds `LIMIT 1`, so don't combine it with `limit()` or
703
+ * `range()`. With several matches it is the first in your `order()`.
704
+ *
705
+ * @example
706
+ * const { data: user } = await db.from('users').select().eq('id', id).single()
707
+ * if (!user) { ... }
708
+ * const { data: email } = await db.from('emails').select().eq('status', 'active').random().single()
709
+ */
710
+ single() {
711
+ this.#single = true;
712
+ this.req.single = "single";
713
+ return this;
714
+ }
715
+ /** @internal */
716
+ invalid() {
717
+ if (this.problem) return this.problem;
718
+ if (this.#single && (this.req.limit !== void 0 || this.req.offset !== void 0)) return "single() already means one row: don't combine it with limit() or range()";
719
+ }
720
+ };
721
+ /**
722
+ * An insert, upsert, update or delete. Like Postgres, it writes every
723
+ * matching row; `data` is `null` and `count` the number of rows written,
724
+ * unless `returning()` asks for the rows.
725
+ */
726
+ var WriteBuilder = class extends Builder {
727
+ /**
728
+ * Return the written rows, like Postgres `RETURNING`: always an array (one
729
+ * row is `[row]`, none is `[]`). Takes `'*'` (the default) or column names.
730
+ *
731
+ * @example
732
+ * await db.from('users').insert({ name, email }).returning() // [{ id, name, email, ... }]
733
+ * await db.from('users').update({ age: ops.increment() }).eq('id', id).returning('id, age')
734
+ * await db.from('posts').delete().eq('user_id', id).returning('id') // every deleted row
735
+ */
736
+ returning(columns = "*") {
737
+ this.req.columns = columns;
738
+ this.req.returning = true;
739
+ return this;
740
+ }
741
+ };
742
+ // src/ops.ts
743
+ /**
744
+ * Atomic update operator, applied by Postgres to the current column value.
745
+ *
746
+ * update({ scores: ops.append(4), meta: ops.merge({ vip: true }), age: ops.increment() })
747
+ */
748
+ var Op = class Op {
749
+ $op;
750
+ values;
751
+ path;
752
+ constructor(op, values, path = []) {
753
+ this.$op = op;
754
+ this.values = values;
755
+ this.path = path;
756
+ }
757
+ /** Apply at a JSON path inside a json/jsonb column: `ops.append('x').at('address', 'lines')`. */
758
+ at(...path) {
759
+ return new Op(this.$op, this.values, path.map(String));
760
+ }
761
+ };
762
+ const ops = {
763
+ /**
764
+ * Add values at the end of a jsonb array or Postgres array. An array argument is added as one element.
765
+ * @example
766
+ * update({ scores: ops.append(4) }) // [1,2,3] -> [1,2,3,4]
767
+ * update({ meta: ops.append('y').at('tags') }) // inside a jsonb object
768
+ */
769
+ append: (...values) => new Op("append", values),
770
+ /**
771
+ * Add values at the start of a jsonb array or Postgres array.
772
+ * @example
773
+ * update({ scores: ops.prepend(0) }) // [1,2,3] -> [0,1,2,3]
774
+ */
775
+ prepend: (...values) => new Op("prepend", values),
776
+ /**
777
+ * Remove every element equal to one of the values.
778
+ * @example
779
+ * update({ scores: ops.remove(2) }) // [1,2,3] -> [1,3]
780
+ */
781
+ remove: (...values) => new Op("remove", values),
782
+ /**
783
+ * Shallow-merge keys into a json/jsonb object.
784
+ * @example
785
+ * update({ meta: ops.merge({ vip: true }) }) // {a:1} -> {a:1,vip:true}
786
+ */
787
+ merge: (object) => new Op("merge", [object]),
788
+ /**
789
+ * Set a value at a JSON path (needs `.at(...)`); a number is an array position.
790
+ * @example
791
+ * update({ meta: ops.set('Lisbon').at('city') })
792
+ * update({ scores: ops.set(9).at(0) }) // [1,2,3] -> [9,2,3]
793
+ */
794
+ set: (value) => new Op("set", [value]),
795
+ /**
796
+ * Remove the key (or array position) at a JSON path (needs `.at(...)`).
797
+ * @example
798
+ * update({ meta: ops.unset().at('vip') })
799
+ * update({ scores: ops.unset().at(0) }) // [1,2,3] -> [2,3]
800
+ */
801
+ unset: () => new Op("unset", []),
802
+ /**
803
+ * Add to a number column, or to a number at a JSON path (negative to subtract).
804
+ * @example
805
+ * update({ age: ops.increment() })
806
+ * update({ meta: ops.increment(5).at('points') })
807
+ */
808
+ increment: (by = 1) => new Op("increment", [by])
809
+ };
810
+ // src/client.ts
811
+ const fail = (message) => ({
812
+ data: null,
813
+ error: { message },
814
+ count: null,
815
+ status: 0
816
+ });
817
+ function stepProblem(s) {
818
+ if (s.toStep().readFrom === "replica") return "transaction steps always run on the primary: drop replica()";
819
+ return s.invalid();
820
+ }
821
+ /** Turn the callback's result into wire steps; null + message when it is not usable. */
822
+ function toSteps(steps) {
823
+ if (Array.isArray(steps)) {
824
+ if (steps.length === 0) return "transaction callback must return a non-empty array or object of steps";
825
+ for (const s of steps) {
826
+ if (!(s instanceof QueryBuilder || s instanceof WriteBuilder)) return "every transaction step must be tx.from(...)";
827
+ const bad = stepProblem(s);
828
+ if (bad) return bad;
829
+ }
830
+ return {
831
+ names: null,
832
+ wire: steps.map((s) => s.toStep())
833
+ };
834
+ }
835
+ if (steps && typeof steps === "object") {
836
+ const entries = Object.entries(steps);
837
+ if (entries.length === 0) return "transaction callback must return a non-empty array or object of steps";
838
+ for (const [name, s] of entries) {
839
+ if (/^(0|[1-9]\d*)$/.test(name)) return `step name "${name}" looks like a number; use a word (JavaScript reorders number keys)`;
840
+ if (name.includes(".")) return `step name "${name}" must not contain a dot (refs use name.column)`;
841
+ if (!(s instanceof QueryBuilder || s instanceof WriteBuilder)) return `step "${name}" must be tx.from(...)`;
842
+ if (s.stepName()) return `step "${name}" already has a name; drop .as("${s.stepName()}") in the object form`;
843
+ const bad = stepProblem(s);
844
+ if (bad) return bad;
845
+ }
846
+ return {
847
+ names: entries.map(([n]) => n),
848
+ wire: entries.map(([name, s]) => ({
849
+ ...s.toStep(),
850
+ name
851
+ }))
852
+ };
853
+ }
854
+ return "transaction callback must return an array or object of steps";
855
+ }
856
+ function createTransport(url, apiKey, options) {
857
+ const base = url.replace(/\/+$/, "");
858
+ const fetchImpl = options.fetch ?? globalThis.fetch;
859
+ if (!fetchImpl) throw new Error("arn-db: no fetch available; pass options.fetch (Node 18+ has a global fetch)");
860
+ if (typeof apiKey !== "string" || apiKey === "") throw new Error("arn-db: createClient(url, apiKey) needs an API key");
861
+ return { async post(path, body) {
862
+ let res;
863
+ try {
864
+ res = await fetchImpl(base + path, {
865
+ method: "POST",
866
+ headers: {
867
+ ...options.headers,
868
+ "Content-Type": "application/json",
869
+ apikey: apiKey
870
+ },
871
+ body: JSON.stringify(body)
872
+ });
873
+ } catch (err) {
874
+ return {
875
+ data: null,
876
+ error: { message: err.message },
877
+ count: null,
878
+ status: 0
879
+ };
880
+ }
881
+ let json;
882
+ try {
883
+ json = await res.json();
884
+ } catch {
885
+ return {
886
+ data: null,
887
+ error: { message: `invalid response (HTTP ${res.status})` },
888
+ count: null,
889
+ status: res.status
890
+ };
891
+ }
892
+ if (json.error) return {
893
+ data: null,
894
+ error: json.error,
895
+ count: null,
896
+ status: res.status
897
+ };
898
+ return {
899
+ data: json.data ?? null,
900
+ error: null,
901
+ count: json.count ?? null,
902
+ status: res.status
903
+ };
904
+ } };
905
+ }
906
+ /**
907
+ * Server-side client for the Go API (it refuses browser requests). Every call
908
+ * resolves to `{ data, error, count, status }` and never throws.
909
+ *
910
+ * @example
911
+ * const db = createClient(process.env.PG_API_URL!, process.env.PG_API_KEY!)
912
+ * const { data, error } = await db.from('users').select().eq('active', true)
913
+ */
914
+ function createClient(url, apiKey, options = {}) {
915
+ const transport = createTransport(url, apiKey, options);
916
+ const { readFrom } = options;
917
+ if (readFrom !== void 0 && readFrom !== "primary" && readFrom !== "replica") throw new Error(`arn-db: readFrom must be 'primary' or 'replica', got ${JSON.stringify(readFrom)}`);
918
+ return {
919
+ from: (table) => new QueryBuilder(transport, table, readFrom),
920
+ ops,
921
+ rel,
922
+ col,
923
+ now,
924
+ async transaction(build) {
925
+ const steps = toSteps(build({
926
+ from: (table) => new QueryBuilder(null, table),
927
+ ref: (path) => ({ $ref: path }),
928
+ ops,
929
+ rel,
930
+ col,
931
+ now
932
+ }));
933
+ if (typeof steps === "string") return fail(steps);
934
+ const res = await transport.post("/transaction", { steps: steps.wire });
935
+ if (res.error || !steps.names) return res;
936
+ const results = res.data;
937
+ const byName = Object.fromEntries(steps.names.map((n, i) => [n, results[i]]));
938
+ return {
939
+ ...res,
940
+ data: byName
941
+ };
942
+ }
943
+ };
944
+ }
945
+ // src/env.ts
946
+ let client;
947
+ /** The ARN_DB_URL / ARN_DB_KEY client, made on the first query (so a later dotenv still counts). */
948
+ function envClient() {
949
+ if (client) return client;
950
+ const proc = globalThis.process;
951
+ if (!proc) throw new Error("arn-db: `db` reads ARN_DB_URL / ARN_DB_KEY from process.env; use createClient(url, key) here");
952
+ if ((!proc.env.ARN_DB_URL || !proc.env.ARN_DB_KEY) && proc.loadEnvFile) try {
953
+ proc.loadEnvFile();
954
+ } catch {}
955
+ const url = proc.env.ARN_DB_URL;
956
+ const key = proc.env.ARN_DB_KEY;
957
+ 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)");
958
+ if (!key) throw new Error("arn-db: set ARN_DB_KEY or use createClient(url, key)");
959
+ client = createClient(url, key);
960
+ return client;
961
+ }
962
+ /**
963
+ * Ready-made client from the environment: `ARN_DB_URL` (e.g.
964
+ * `https://your-api.example.com/v2/raw`) and `ARN_DB_KEY`, read on the first query,
965
+ * with `./.env` loaded if they are not set yet. A missing variable throws on
966
+ * that first query. Use `createClient()` for other keys or options.
967
+ *
968
+ * @example
969
+ * import { db } from 'arn-db'
970
+ * const { data, error } = await db.from('users').select().eq('id', id).single()
971
+ */
972
+ const db = {
973
+ from: (table) => envClient().from(table),
974
+ transaction: ((build) => envClient().transaction(build)),
975
+ ops,
976
+ rel,
977
+ col,
978
+ now
979
+ };
980
+ exports.Col = Col;
981
+ exports.FilterGroup = FilterGroup;
982
+ exports.Now = Now;
983
+ exports.Op = Op;
984
+ exports.QueryBuilder = QueryBuilder;
985
+ exports.WriteBuilder = WriteBuilder;
986
+ exports.col = col;
987
+ exports.createClient = createClient;
988
+ exports.db = db;
989
+ exports.now = now;
990
+ exports.ops = ops;
991
+ exports.rel = rel;
992
+
993
+ //# sourceMappingURL=index.cjs.map