zero-orm 1.0.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1995 -0
  3. package/dist/prd.tsconfig.tsbuildinfo +1 -0
  4. package/dist/src/Table.d.ts +180 -0
  5. package/dist/src/Table.d.ts.map +1 -0
  6. package/dist/src/Table.js +8 -0
  7. package/dist/src/Table.js.map +1 -0
  8. package/dist/src/context.d.ts +90 -0
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +84 -0
  11. package/dist/src/context.js.map +1 -0
  12. package/dist/src/createModelParser.d.ts +47 -0
  13. package/dist/src/createModelParser.d.ts.map +1 -0
  14. package/dist/src/createModelParser.js +339 -0
  15. package/dist/src/createModelParser.js.map +1 -0
  16. package/dist/src/ddl.d.ts +22 -0
  17. package/dist/src/ddl.d.ts.map +1 -0
  18. package/dist/src/ddl.js +91 -0
  19. package/dist/src/ddl.js.map +1 -0
  20. package/dist/src/entity.d.ts +114 -0
  21. package/dist/src/entity.d.ts.map +1 -0
  22. package/dist/src/entity.js +539 -0
  23. package/dist/src/entity.js.map +1 -0
  24. package/dist/src/index.d.ts +10 -0
  25. package/dist/src/index.d.ts.map +1 -0
  26. package/dist/src/index.js +71 -0
  27. package/dist/src/index.js.map +1 -0
  28. package/dist/src/keywords.d.ts +158 -0
  29. package/dist/src/keywords.d.ts.map +1 -0
  30. package/dist/src/keywords.js +187 -0
  31. package/dist/src/keywords.js.map +1 -0
  32. package/dist/src/resolve.d.ts +15 -0
  33. package/dist/src/resolve.d.ts.map +1 -0
  34. package/dist/src/resolve.js +600 -0
  35. package/dist/src/resolve.js.map +1 -0
  36. package/dist/src/testTransaction.d.ts +23 -0
  37. package/dist/src/testTransaction.d.ts.map +1 -0
  38. package/dist/src/testTransaction.js +113 -0
  39. package/dist/src/testTransaction.js.map +1 -0
  40. package/dist/src/transaction.d.ts +6 -0
  41. package/dist/src/transaction.d.ts.map +1 -0
  42. package/dist/src/transaction.js +28 -0
  43. package/dist/src/transaction.js.map +1 -0
  44. package/dist/src/utils.d.ts +62 -0
  45. package/dist/src/utils.d.ts.map +1 -0
  46. package/dist/src/utils.js +86 -0
  47. package/dist/src/utils.js.map +1 -0
  48. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,1995 @@
1
+ # Documentation
2
+
3
+ # Table of Contents
4
+
5
+ - [A Note on History](#a-note-on-history)
6
+ - [Introduction](#introduction)
7
+ - [Installation](#installation)
8
+ - [Getting Started](#getting-started)
9
+ - [Connecting to the Database](#connecting-to-the-database)
10
+ - [Defining a Table](#defining-a-table)
11
+ - [Defining an Entity](#defining-an-entity)
12
+ - [Performing a SELECT Query](#performing-a-select-query)
13
+ - [Performing an INSERT Query](#performing-an-insert-query)
14
+ - [Performing an UPDATE Query](#performing-an-update-query)
15
+ - [Performing a DELETE Query](#performing-a-delete-query)
16
+ - [Using JOINs](#using-joins)
17
+ - [Defining a Schema](#defining-a-schema)
18
+ - [Defining a Model](#defining-a-model)
19
+ - [Defining a Model Parser](#defining-a-model-parser)
20
+ - [API Reference](#api-reference)
21
+ - [Advanced Database Connection](#advanced-database-connection)
22
+ - [Advanced Table Definitions](#advanced-table-definitions)
23
+ - [Column Options Reference](#column-options-reference)
24
+ - [`type` Options Reference](#type-options-reference)
25
+ - [`defaultValue` Options Reference](#defaultvalue-options-reference)
26
+ - [`createReference` Reference](#createreference-reference)
27
+ - [Advanced Table Definitions Example](#advanced-table-definitions-example)
28
+ - [Generating Tables and Sequences SQL](#generating-tables-and-sequences-sql)
29
+ - [Setup and Teardown Database Sequences and Tables](#setup-and-teardown-database-sequences-and-tables)
30
+ - [`execute()` and `getData()`](#execute-and-getdata)
31
+ - [Expression System](#expression-system)
32
+ - [U.compare](#ucompare)
33
+ - [U.arithmetic](#uarithmetic)
34
+ - [U.json](#ujson)
35
+ - [U.fun](#ufun)
36
+ - [U.cons](#ucons)
37
+ - [U.switchCase](#uswitchcase)
38
+ - [U.concat](#uconcat)
39
+ - [U.not](#unot)
40
+ - [U.and](#uand)
41
+ - [U.or](#uor)
42
+ - [U.subQuery](#usubquery)
43
+ - [U.subQueryExist](#usubqueryexist)
44
+ - [U.raw](#uraw)
45
+ - [U.ignore](#uignore)
46
+ - [U.column](#ucolumn)
47
+ - [U.value](#uvalue)
48
+ - [Context](#context)
49
+ - [Execution Mode](#execution-mode)
50
+ - [SELECT Operations](#select-operations)
51
+ - [SELECT Parameter: `returning`](#select-parameter-returning)
52
+ - [SELECT Parameter: `where`](#select-parameter-where)
53
+ - [SELECT Parameter: `selectOptions`](#select-parameter-selectoptions)
54
+ - [SelectOptions.distinct?](#selectoptionsdistinct)
55
+ - [SelectOptions.groupBy?](#selectoptionsgroupby)
56
+ - [SelectOptions.orders?](#selectoptionsorders)
57
+ - [SelectOptions.start?](#selectoptionsstart)
58
+ - [SelectOptions.step?](#selectoptionsstep)
59
+ - [SelectOptions.customQueryBuilder?](#selectoptionscustomquerybuilder)
60
+ - [Example of SELECT Operations](#example-of-select-operations)
61
+ - [INSERT Operations](#insert-operations)
62
+ - [INSERT Parameter: `rows`](#insert-parameter-rows)
63
+ - [INSERT Parameter: `returning`](#insert-parameter-returning)
64
+ - [Example of INSERT Operations](#example-of-insert-operations)
65
+ - [UPDATE Operations](#update-operations)
66
+ - [UPDATE Parameter: `sets`](#update-parameter-sets)
67
+ - [UPDATE Parameter: `where`](#update-parameter-where)
68
+ - [UPDATE Parameter: `returning`](#update-parameter-returning)
69
+ - [Example of UPDATE Operations](#example-of-update-operations)
70
+ - [DELETE Operations](#delete-operations)
71
+ - [DELETE Parameter: `where`](#delete-parameter-where)
72
+ - [DELETE Parameter: `returning`](#delete-parameter-returning)
73
+ - [Example of DELETE Operations](#example-of-delete-operations)
74
+ - [JOIN Operations](#join-operations)
75
+ - [JOIN Parameter: `mainAlias`](#join-parameter-mainalias)
76
+ - [JOIN Parameter: `joinType`](#join-parameter-jointype)
77
+ - [JOIN Parameter: `joinTable`](#join-parameter-jointable)
78
+ - [JOIN Parameter: `joinAlias`](#join-parameter-joinalias)
79
+ - [JOIN Parameter: `on`](#join-parameter-on)
80
+ - [Example of JOIN Operations](#example-of-join-operations)
81
+ - [Transaction Management](#transaction-management)
82
+ - [Transaction Parameter: `pool`](#transaction-parameter-pool)
83
+ - [Transaction Parameter: `callback`](#transaction-parameter-callback)
84
+ - [Transaction Parameter: `isolationLevel?`](#transaction-parameter-isolationlevel)
85
+ - [Transaction Parameter: `readOnly?`](#transaction-parameter-readonly)
86
+ - [Example of Transaction](#example-of-transaction)
87
+ - [Test Transaction Utility (for Unit and E2E Tests)](#test-transaction-utility-for-unit-and-e2e-tests)
88
+ - [Test Transaction Parameter: `tablesWithData`](#test-transaction-parameter-tableswithdata)
89
+ - [Test Transaction Parameter: `callback`](#test-transaction-parameter-callback)
90
+ - [Test Transaction Parameter: `pool`](#test-transaction-parameter-pool)
91
+ - [Test Transaction Parameter: `isolationLevel?`](#test-transaction-parameter-isolationlevel)
92
+ - [Test Transaction Parameter: `rollback?`](#test-transaction-parameter-rollback)
93
+ - [Example of Test Transaction](#example-of-test-transaction)
94
+ - [Json Type](#json-type)
95
+
96
+ # A Note on History
97
+
98
+ This repository is the official successor to the original [
99
+ `@mrnafisia/type-query`](https://github.com/MRNafisiA/type-query). The project was moved here for a fresh start with a
100
+ **clean commit history**, proper **semantic versioning**, and the new package name `zero-orm`. The old repository is
101
+ archived and will no longer receive updates.
102
+
103
+ # Introduction
104
+
105
+ Welcome to the documentation for **Zero-ORM**, a powerful, type-safe ORM (Object-Relational Mapper) library written in
106
+ TypeScript. This library bridges the gap between your TypeScript application and your SQL database with a strong
107
+ emphasis on **type safety**, **validation**, and **developer experience**.
108
+
109
+ Unlike traditional ORMs that rely on reflection or decorators, Zero-ORM uses a **schema-first** approach. You define
110
+ your table structure once, and the library infers TypeScript types, generates validation rules, and provides a fluent
111
+ query builder with full IntelliSense support.
112
+
113
+ **Key Features:**
114
+
115
+ - **100% Type-Safe:** Enjoy full autocompletion for column names, operators, and return types.
116
+ - **Schema Validation:** Define `minLength`, `maxLength`, `regex`, and `nullable` constraints directly within your
117
+ schema.
118
+ - **Model Parsing:** Parse and validate incoming API requests before they hit your database with a powerful,
119
+ error-accumulating parser.
120
+ - **Fluent Query Builder:** Use intuitive methods for `select`, `insert`, `update`, `delete`, and `join` operations.
121
+ - **Custom Operators:** Leverage an advanced expression system for complex `where` clauses.
122
+
123
+ # Installation
124
+
125
+ **Peer Dependencies:**
126
+
127
+ - Install the [pg](https://node-postgres.com) client, as this library is built for PostgreSQL-compatible databases.
128
+ - If you intend to use the Postgres `Number` type, install [Decimal.js](https://mikemcl.github.io/decimal.js/) as well.
129
+
130
+ ```bash
131
+ bun add zero-orm pg decimal.js
132
+ bun add --dev @types/pg
133
+ ```
134
+
135
+ ```bash
136
+ npm install zero-orm pg decimal.js
137
+ npm install --save-dev @types/pg
138
+ ```
139
+
140
+ ```bash
141
+ yarn add zero-orm pg decimal.js
142
+ yarn add --dev @types/pg
143
+ ```
144
+
145
+ # Getting Started
146
+
147
+ ## Connecting to the Database
148
+
149
+ Zero-ORM works directly with the [pg](https://node-postgres.com) library.
150
+
151
+ `db.ts`
152
+
153
+ ```typescript
154
+ import { Pool } from 'pg';
155
+
156
+ const pool = new Pool({
157
+ connectionString: 'postgres://postgres:12345678@localhost:5432/app'
158
+ });
159
+
160
+ export { pool };
161
+ ```
162
+
163
+ ## Defining a Table
164
+
165
+ Start by defining your table structure using the `createTable` function. This defines the schema, column types, and
166
+ validation rules for the model parser.
167
+
168
+ `User.ts`
169
+
170
+ ```typescript
171
+ import { createTable } from 'zero-orm';
172
+
173
+ const UserTable = createTable({
174
+ schemaName: 'public', // Database schema
175
+ tableName: 'user', // Table name
176
+ columns: {
177
+ id: {
178
+ type: 'int4', // PostgreSQL type
179
+ nullable: false,
180
+ default: true,
181
+ primary: true,
182
+ defaultValue: ['auto-increment'] // Special syntax for SERIAL/IDENTITY
183
+ },
184
+ username: {
185
+ type: 'varchar',
186
+ nullable: false,
187
+ default: false,
188
+ minLength: 1,
189
+ maxLength: 24,
190
+ regex: /^[\w-]*$/ // Alphanumeric, underscore, or dash
191
+ },
192
+ name: {
193
+ type: 'varchar',
194
+ nullable: true, // This column can be NULL
195
+ default: false,
196
+ minLength: 6,
197
+ maxLength: 100
198
+ },
199
+ isAdmin: {
200
+ type: 'boolean',
201
+ nullable: false,
202
+ default: true,
203
+ defaultValue: ['js', false]
204
+ },
205
+ isActive: {
206
+ type: 'boolean',
207
+ nullable: false,
208
+ default: false
209
+ },
210
+ roles: {
211
+ type: 'jsonb',
212
+ nullable: false,
213
+ default: false,
214
+ // 'narrowType' helps TypeScript narrow the default pg-to-ts type map (e.g., string[])
215
+ narrowType: undefined as unknown as string[]
216
+ }
217
+ }
218
+ });
219
+ ```
220
+
221
+ ## Defining an Entity
222
+
223
+ After defining a table, create an **Entity** to perform database operations (SELECT, INSERT, UPDATE, DELETE, and JOIN).
224
+
225
+ `User.ts`
226
+
227
+ ```typescript
228
+ import { createEntity } from 'zero-orm';
229
+
230
+ // UserTable definition ...
231
+
232
+ const User = createEntity(UserTable);
233
+
234
+ export { User };
235
+ ```
236
+
237
+ The `User` object now provides methods like `.select()`, `.insert()`, `.update()`, `.delete()`, and `.join()`.
238
+
239
+ ## Performing a SELECT Query
240
+
241
+ Select specific columns without any filters.
242
+
243
+ ```typescript
244
+ import { pool } from './db';
245
+ import { User } from './User';
246
+ import { PoolClient } from 'pg';
247
+
248
+ const selectUser = async (client: PoolClient) => {
249
+ const result = await User.select(
250
+ ['id', 'username'], // selecting columns
251
+ true // WHERE condition (true means no filter)
252
+ ).execute(client, []);
253
+ if (!result.ok) {
254
+ throw new Error(`Query failed with error ${result.error}`);
255
+ }
256
+ console.log(result.value); // Array of rows: { id: number, username: string }[]
257
+ };
258
+
259
+ pool.connect().then(async client => {
260
+ await selectUser(client);
261
+ client.release();
262
+ await pool.end();
263
+ });
264
+ ```
265
+
266
+ ## Performing an INSERT Query
267
+
268
+ Insert a single row or multiple rows.
269
+
270
+ ```typescript
271
+ import { pool } from './db';
272
+ import { User } from './User';
273
+ import { PoolClient } from 'pg';
274
+
275
+ const insertUser = async (client: PoolClient) => {
276
+ const insertingRow = {
277
+ username: 'john_doe',
278
+ name: 'john',
279
+ isActive: true,
280
+ roles: ['reporter', 'writer']
281
+ };
282
+
283
+ const result = await User.insert([insertingRow], ['id', 'name']).execute(
284
+ client,
285
+ []
286
+ );
287
+ if (!result.ok) {
288
+ throw new Error(`Query failed with error ${result.error}`);
289
+ }
290
+ console.log(result.value); // Array of rows: { id: number }[]
291
+ };
292
+
293
+ pool.connect().then(async client => {
294
+ await insertUser(client);
295
+ client.release();
296
+ await pool.end();
297
+ });
298
+ ```
299
+
300
+ ## Performing an UPDATE Query
301
+
302
+ Update records that match a condition.
303
+
304
+ ```typescript
305
+ import { pool } from './db';
306
+ import { User } from './User';
307
+ import { PoolClient } from 'pg';
308
+
309
+ const updateUser = async (client: PoolClient) => {
310
+ const result = await User.update(
311
+ { isActive: true }, // Set column values
312
+ context => context.compare('id', '=', 1), // Condition: WHERE id = 1
313
+ ['id'] // Return the 'id' column of updated rows
314
+ ).execute(client, []);
315
+ if (!result.ok) {
316
+ throw new Error(`Query failed with error ${result.error}`);
317
+ }
318
+ console.log(result.value); // Array of rows: { id: number }[]
319
+ };
320
+
321
+ pool.connect().then(async client => {
322
+ await updateUser(client);
323
+ client.release();
324
+ await pool.end();
325
+ });
326
+ ```
327
+
328
+ ## Performing a DELETE Query
329
+
330
+ Delete records matching a condition.
331
+
332
+ ```typescript
333
+ import { pool } from './db';
334
+ import { User } from './User';
335
+ import { PoolClient } from 'pg';
336
+
337
+ const deleteUser = async (client: PoolClient) => {
338
+ const result = await User.delete(
339
+ context => context.compare('username', '=', 'john_doe'),
340
+ ['username']
341
+ ).execute(client, []);
342
+ if (!result.ok) {
343
+ throw new Error(`Query failed with error ${result.error}`);
344
+ }
345
+ console.log(result.value); // Array of rows: { id: number }[]
346
+ };
347
+
348
+ pool.connect().then(async client => {
349
+ await deleteUser(client);
350
+ client.release();
351
+ await pool.end();
352
+ });
353
+ ```
354
+
355
+ ## Using JOINs
356
+
357
+ The library provides a powerful join system that allows you to join multiple tables with type safety.
358
+
359
+ `Product.ts`
360
+
361
+ ```typescript
362
+ import { createTable, createEntity } from 'zero-orm';
363
+
364
+ const Product = createEntity(
365
+ createTable({
366
+ schemaName: 'public',
367
+ tableName: 'product',
368
+ columns: {
369
+ id: {
370
+ type: 'int4',
371
+ nullable: false,
372
+ default: true,
373
+ primary: true,
374
+ defaultValue: ['auto-increment']
375
+ },
376
+ title: {
377
+ type: 'varchar',
378
+ nullable: false,
379
+ default: false
380
+ },
381
+ isDisabled: {
382
+ type: 'boolean',
383
+ nullable: false,
384
+ default: false
385
+ },
386
+ userID: {
387
+ type: 'int4',
388
+ nullable: false,
389
+ default: false
390
+ }
391
+ }
392
+ })
393
+ );
394
+
395
+ export { Product };
396
+ ```
397
+
398
+ ```typescript
399
+ import { pool } from './db';
400
+ import { User } from './User';
401
+ import { PoolClient } from 'pg';
402
+ import { Product } from './Product';
403
+
404
+ const joinSelect = async (client: PoolClient) => {
405
+ const result = await Product.join(
406
+ 'p', // Alias for the Product table
407
+ 'inner', // Join type: 'inner', 'left', 'right', 'full'
408
+ User.table, // The table to join with
409
+ 'u', // Alias for the User table
410
+ (
411
+ { pContext, uContext } // ON clause
412
+ ) => pContext.compare('userID', '=', uContext.column('id'))
413
+ )
414
+ .select(
415
+ [
416
+ 'u_id', // 'u_id' means: from the User table (alias 'u'), column 'id'
417
+ 'u_username',
418
+ 'p_id',
419
+ 'p_title'
420
+ ],
421
+ ({ pContext }) => pContext.compare('title', '=', 'chair') // WHERE clause
422
+ )
423
+ .execute(client, []);
424
+ if (!result.ok) {
425
+ throw new Error(`Query failed with error ${result.error}`);
426
+ }
427
+ console.log(result.value);
428
+ /* Array of rows: {
429
+ * u_id: number,
430
+ * u_username: string,
431
+ * p_id: number,
432
+ * p_title: string
433
+ * }[]
434
+ */
435
+ };
436
+
437
+ pool.connect().then(async client => {
438
+ await joinSelect(client);
439
+ client.release();
440
+ await pool.end();
441
+ });
442
+ ```
443
+
444
+ ## Defining a Schema
445
+
446
+ **Schema** is a handy type for creating models, complex conditions, expressions, and more.
447
+
448
+ `User.ts`
449
+
450
+ ```typescript
451
+ // UserTable definition ...
452
+ // User definition ...
453
+ type UserSchema = typeof User.table.columns;
454
+
455
+ export { type UserSchema };
456
+ ```
457
+
458
+ ## Defining a Model
459
+
460
+ A **Model** simplifies working with typed data structures by allowing you to specify required and optional fields.
461
+
462
+ `User.ts`
463
+
464
+ ```typescript
465
+ import { ModelWithPrefix } from 'zero-orm';
466
+
467
+ // UserTable definition ...
468
+ // User definition ...
469
+ // UserSchema definition ...
470
+
471
+ type UserModel<
472
+ Required extends keyof UserSchema = keyof UserSchema,
473
+ Optional extends keyof UserSchema = never,
474
+ NotNull extends Required | Optional = never,
475
+ Prefix extends string = ''
476
+ > = ModelWithPrefix<UserSchema, Required, Optional, NotNull, Prefix>;
477
+
478
+ export { type UserModel };
479
+ ```
480
+
481
+ Example:
482
+
483
+ ```typescript
484
+ import { UserModel } from './User';
485
+
486
+ type AddUser = UserModel<
487
+ 'username' | 'name', // required
488
+ 'roles' // optional
489
+ >; // { username: string, name: string | null, roles?: string[] }
490
+
491
+ type EditUser = UserModel<
492
+ 'id', // required
493
+ 'username' | 'name' | 'roles', // optional
494
+ 'name' // null not allowed
495
+ >; // { id: number, username?: string, name?: string, roles?: string[] }
496
+
497
+ type GetUser = UserModel<
498
+ 'id', // required
499
+ 'username' | 'name' | 'roles', // optional
500
+ never,
501
+ 'u_' // alias when selecting using join
502
+ >; // { u_id: number, u_username?: string, u_name?: string, u_roles?: string[] }
503
+ ```
504
+
505
+ ## Defining a Model Parser
506
+
507
+ A **Model Parser** validates and transforms raw input data into typed application models.
508
+
509
+ `User.ts`
510
+
511
+ ```typescript
512
+ import { createModelParser } from 'zero-orm';
513
+
514
+ // UserTable definition ...
515
+ // User definition ...
516
+ // UserSchema definition ...
517
+ // UserModel definition ...
518
+
519
+ const UserModelParser = createModelParser(User.table, {
520
+ errorsMap: {
521
+ // Map database columns to user-friendly error messages
522
+ id: 'Invalid ID format.',
523
+ username:
524
+ 'Username must be 1-24 characters and contain only letters, numbers, underscores, or dashes.',
525
+ name: 'Please provide a valid name.',
526
+ isAdmin: 'invalid isAdmin',
527
+ isActive: 'invalid isActive',
528
+ roles: 'At most 5 roles are allowed.'
529
+ },
530
+ parsers: {
531
+ // Custom parsing logic for specific columns
532
+ roles: v => (v.length < 5 ? v : undefined)
533
+ }
534
+ });
535
+
536
+ export { UserModelParser };
537
+ ```
538
+
539
+ Parsing an entire object:
540
+
541
+ ```typescript
542
+ import { UserModelParser } from './User';
543
+
544
+ const data = { id: 1 };
545
+
546
+ const result = UserModelParser.Parse(
547
+ data, // data must be at least in the form of Record<string, unknown>
548
+ ['id', 'name'], // required
549
+ ['username'], // optional
550
+ ['name'] // null not allowed
551
+ );
552
+ if (!result.ok) {
553
+ throw new Error(result.error); // 'Please provide a valid name.'
554
+ }
555
+ console.log(result.value); // { id: number, name: string, username?: string }
556
+ ```
557
+
558
+ Parsing a single field:
559
+
560
+ ```typescript
561
+ import { UserModelParser } from './User';
562
+
563
+ const username = 'admin';
564
+
565
+ const parsedUsername = UserModelParser.username(username);
566
+ if (parsedUsername === undefined) {
567
+ throw new Error('username is invalid.');
568
+ }
569
+ console.log(parsedUsername); // 'admin'
570
+ ```
571
+
572
+ # API Reference
573
+
574
+ ## Advanced Database Connection
575
+
576
+ `db.ts`
577
+
578
+ ```typescript
579
+ import Decimal from 'decimal.js';
580
+ import { Pool, Query, types } from 'pg';
581
+
582
+ // (REQUIRED) Improve the pg parser (refer to the pg documentation for more details)
583
+ types.setTypeParser(types.builtins.INT8, v => BigInt(v));
584
+ types.setTypeParser(types.builtins.NUMERIC, v => new Decimal(v));
585
+ types.setTypeParser(types.builtins.DATE, v => new Date(`${v}T00:00:00Z`));
586
+
587
+ // (OPTIONAL) Log all executed queries to the console
588
+ if (process.env.NODE_ENV === 'development') {
589
+ const submit = Query.prototype.submit;
590
+ Query.prototype.submit = function(
591
+ this: Record<'text' | 'values', string>
592
+ ) {
593
+ console.info(`\x1b[36mQuery: ${this.text}`);
594
+ console.info(`Parameters: ${JSON.stringify(this.values)}\x1b[0m\n`);
595
+ submit.apply(this, arguments as unknown as Parameters<typeof submit>);
596
+ };
597
+ }
598
+
599
+ // Create a pg Pool
600
+ const pool = new Pool({
601
+ connectionString: 'postgres://postgres:12345678@localhost:5432/app'
602
+ });
603
+
604
+ export { pool };
605
+ ```
606
+
607
+ ## Advanced Table Definitions
608
+
609
+ The `createTable` function supports a wide range of column options for rigorous data integrity.
610
+
611
+ ### Column Options Reference
612
+
613
+ | Option | Type | Description |
614
+ |------------------|-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
615
+ | `type` | `string` | See the [type Options Reference](#type-options-reference). |
616
+ | `nullable` | `boolean` | If `true`, the column can store `NULL` values. |
617
+ | `primary?` | `true` | Marks the column as a primary key. Applicable only if the column is not null. |
618
+ | `default` | `boolean` | Indicates whether a default value exists. |
619
+ | `defaultValue?` | | Applicable only if `default: true`. See the [`defaultValue` Options Reference](#defaultvalue-options-reference). |
620
+ | `min?` | `number` \| `Decimal` (depends on type) | (For Integer, Float and Decimal kinds only) Minimum allowed value. |
621
+ | `max?` | `number` \| `Decimal` (depends on type) | (For Integer, Float and Decimal kinds only) Maximum allowed value. |
622
+ | `minLength?` | `number` | (For String kind only) Minimum string length. |
623
+ | `maxLength?` | `number` | (For String kind only) Maximum string length. |
624
+ | `regex?` | `RegExp` | (For String kind only) Regular expression pattern the value must match. |
625
+ | `sequenceTitle?` | `string` | A custom title for the related sequence. Applicable only if `defaultValue` is `['auto-increment']`. |
626
+ | `precision` | `number` | Refer to the [pg documentation](https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL). Applicable only if `type` is `decimal`. |
627
+ | `scale` | `number` | Refer to the [pg documentation](https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL). Applicable only if `type` is `decimal`. |
628
+ | `length` | `number` | Refer to the [pg documentation](https://www.postgresql.org/docs/current/datatype-datetime.html). Applicable only if `type` is `'date' \| 'timestamp' \| 'timestamptz'`. |
629
+ | `narrowType?` | | Helps narrow the base type. (e.g. `1 \| 2 \| 3` for an `int2` type or `'allow' \| 'deny'` for a `string` type) |
630
+ | `reference?` | | Defines a foreign key. See the [`createReference` Reference](#createreference-reference). |
631
+
632
+ ### `type` Options Reference
633
+
634
+ | Kind | PG Type | JS Type | Description |
635
+ |---------------|---------------------|-----------------------------------------------|--------------------------------------------------|
636
+ | **Boolean** | `boolean` | `boolean` | |
637
+ | **Integer** | `int2` | `number` | |
638
+ | **Integer** | `int4` | `number` | |
639
+ | **Integer** | `int8` | `number` | |
640
+ | **Float** | `float4` | `number` | |
641
+ | **Float** | `float8` | `number` | |
642
+ | **Decimal** | `decimal` | `Decimal` | |
643
+ | **String** | `char` | `string` | |
644
+ | **String** | `varchar` | `string` | |
645
+ | **String** | `text` | `string` | |
646
+ | **String** | `uuid` | `string` | |
647
+ | **Date** | `date` | `Date` | |
648
+ | **Date/Time** | `timestamp` | `Date` | |
649
+ | **Date/Time** | `timestamptz` | `Date` | |
650
+ | **JSON** | `json` | `Json` | See [Json Type](#json-type) |
651
+ | **JSON** | `jsonb` | `Json` | See [Json Type](#json-type) |
652
+ | **Custom** | `custom(something)` | Use the `narrowType` definition in the table. | For example, `custom(Circle)` or any custom type |
653
+
654
+ ### `defaultValue` Options Reference
655
+
656
+ | Type | defaultValue Type |
657
+ |-----------------|--------------------------------------------------------------------------------|
658
+ | **boolean** | `['sql', string]` \| `['js', boolean]` |
659
+ | **int2** | `['sql', string]` \| `['js', number]` \| `['auto-increment']` |
660
+ | **int4** | `['sql', string]` \| `['js', number]` \| `['auto-increment']` |
661
+ | **int8** | `['sql', string]` \| `['js', number]` \| `['auto-increment']` |
662
+ | **float4** | `['sql', string]` \| `['js', number]` |
663
+ | **float8** | `['sql', string]` \| `['js', number]` |
664
+ | **decimal** | `['sql', string]` \| `['js', Decimal]` |
665
+ | **char** | `['sql', string]` \| `['js', string]` |
666
+ | **varchar** | `['sql', string]` \| `['js', string]` |
667
+ | **text** | `['sql', string]` \| `['js', string]` |
668
+ | **uuid** | `['sql', string]` \| `['js', string]` |
669
+ | **date** | `['sql', string]` \| `['js', Date]` \| `['created-at']` \| `['updated-at']` |
670
+ | **timestamp** | `['sql', string]` \| `['js', Date]` \| `['created-at']` \| `['updated-at']` |
671
+ | **timestamptz** | `['sql', string]` \| `['js', Date]` \| `['created-at']` \| `['updated-at']` |
672
+ | **json** | `['sql', string]` \| `['js', Json]` (See [Json Type](#json-type)) |
673
+ | **jsonb** | `['sql', string]` \| `['js', Json]` (See [Json Type](#json-type)) |
674
+ | **Custom** | `['sql', string]` \| `['js', inherits the narrowType definition in the table]` |
675
+
676
+ ### `createReference` Reference
677
+
678
+ References are used to define foreign keys.
679
+
680
+ ```typescript
681
+ import { User } from './User';
682
+ import { createTable, createEntity, createReference } from 'zero-orm';
683
+
684
+ const Token = createEntity(
685
+ createTable({
686
+ schemaName: 'public',
687
+ tableName: 'token',
688
+ columns: {
689
+ uuid: {
690
+ type: 'uuid',
691
+ nullable: false,
692
+ default: false,
693
+ primary: true
694
+ },
695
+ userID: {
696
+ type: 'int4',
697
+ nullable: false,
698
+ default: false,
699
+ reference: createReference({
700
+ table: User.table,
701
+ column: 'id',
702
+ onDelete: 'no-action',
703
+ onUpdate: 'restrict'
704
+ })
705
+ }
706
+ }
707
+ })
708
+ );
709
+ ```
710
+
711
+ ### Advanced Table Definitions Example
712
+
713
+ ```typescript
714
+ import Decimal from 'decimal.js';
715
+ import { createEntity, createTable } from 'zero-orm';
716
+
717
+ const Price = createEntity(
718
+ createTable({
719
+ schemaName: 'public',
720
+ tableName: 'token',
721
+ columns: {
722
+ id: {
723
+ type: 'int4',
724
+ nullable: false,
725
+ default: true,
726
+ primary: true,
727
+ min: 1,
728
+ defaultValue: ['auto-increment']
729
+ },
730
+ title: {
731
+ type: 'varchar',
732
+ nullable: false,
733
+ default: false,
734
+ minLength: 3,
735
+ maxLength: 255
736
+ },
737
+ price: {
738
+ type: 'decimal',
739
+ nullable: false,
740
+ default: true,
741
+ precision: 25,
742
+ scale: 15,
743
+ defaultValue: ['js', new Decimal(0)]
744
+ },
745
+ type: {
746
+ type: 'varchar',
747
+ nullable: false,
748
+ default: false,
749
+ narrowType: undefined as unknown as
750
+ | 'retail'
751
+ | 'wholesale'
752
+ | 'mass-production'
753
+ },
754
+ createdAt: {
755
+ type: 'timestamptz',
756
+ nullable: false,
757
+ default: true,
758
+ defaultValue: ['sql', 'now()']
759
+ }
760
+ }
761
+ })
762
+ );
763
+ ```
764
+
765
+ ## Generating Tables and Sequences SQL
766
+
767
+ ```typescript
768
+ import { User } from './User';
769
+ import {
770
+ generateDropTableSQL,
771
+ generateCreateTableSQL,
772
+ generateDropSequencesSQL,
773
+ generateCreateSequencesSQL
774
+ } from 'zero-orm';
775
+
776
+ const createSequencesSQL: string[] = generateCreateSequencesSQL(User.table, {
777
+ applyIfNotExist: true, // Optional: apply CREATE SEQUENCE IF NOT EXISTS
778
+ owner: 'app_admin' // Optional: change owner to: app_admin
779
+ });
780
+
781
+ const dropSequencesSQL: string[] = generateDropSequencesSQL(User.table, {
782
+ applyIfExist: true // Optional: apply DROP SEQUENCE IF EXISTS
783
+ });
784
+
785
+ const createTableSQL: string = generateCreateTableSQL(User.table, {
786
+ applyIfNotExist: true, // Optional: apply CREATE TABLE IF NOT EXISTS
787
+ isTemp: true, // Optional: apply CREATE TEMPORARY TABLE
788
+ owner: 'app_admin' // Optional: change owner to: app_admin
789
+ });
790
+
791
+ const dropTableSQL: string = generateDropTableSQL(User.table, {
792
+ applyIfExist: true // Optional: apply DROP TABLE IF EXISTS
793
+ });
794
+ ```
795
+
796
+ ### Setup and Teardown Database Sequences and Tables
797
+
798
+ `ddl.ts`
799
+
800
+ ```typescript
801
+ import { pool } from './db';
802
+ import { User } from './User';
803
+ import {
804
+ generateDropTableSQL,
805
+ generateCreateTableSQL,
806
+ generateDropSequencesSQL,
807
+ generateCreateSequencesSQL
808
+ } from 'zero-orm';
809
+
810
+ const Tables = [User.table];
811
+
812
+ const setupDatabaseSequencesAndTables = () =>
813
+ pool
814
+ .connect()
815
+ .then(client =>
816
+ client
817
+ .query(
818
+ Tables.flatMap(table => [
819
+ ...generateCreateSequencesSQL(table),
820
+ generateCreateTableSQL(table)
821
+ ]).join(';\n') + ';'
822
+ )
823
+ .finally(() => client.release())
824
+ );
825
+
826
+ const teardownDatabaseSequencesAndTables = () =>
827
+ pool
828
+ .connect()
829
+ .then(client =>
830
+ client
831
+ .query(
832
+ Tables.flatMap(table => [
833
+ ...generateDropSequencesSQL(table),
834
+ generateDropTableSQL(table)
835
+ ]).join(';\n') + ';'
836
+ )
837
+ .finally(() => client.release())
838
+ );
839
+
840
+ export {
841
+ Tables,
842
+ setupDatabaseSequencesAndTables,
843
+ teardownDatabaseSequencesAndTables
844
+ };
845
+ ```
846
+
847
+ ## `execute()` and `getData()`
848
+
849
+ A complete query object exposes two methods: **`getData()`** and **`execute()`**. Both methods prepare the query and its
850
+ associated parameters.
851
+
852
+ - **`getData()`** returns the prepared query in the form `Result<{ sql: string, params: string[] }, unknown>`. This is
853
+ useful for debugging, logging, or inspecting the generated SQL before execution.
854
+ For more details, check out [never-catch](https://github.com/MRNafisiA/never-catch) (inspired by
855
+ the [Rust](https://rust-lang.org/) `Result` enum).
856
+ - **`execute()`** runs the query against the database using the provided `PoolClient`.
857
+
858
+ Both methods may return an error if a problem is detected in the query (e.g., a neutral expression or an invalid
859
+ configuration).
860
+
861
+ ## Expression System
862
+
863
+ Zero-ORM provides a rich set of utilities to create complex expressions and `where` clauses. The `U` object must be
864
+ used exclusively within Zero-ORM functions and contexts. **Do not** mix it with other JavaScript code.
865
+
866
+ ### U.compare
867
+
868
+ Use `U.compare` to compare two values using a variety of operators.
869
+
870
+ | Operators | Expression Type | Example |
871
+ |-------------------------------------------------------|---------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
872
+ | `'= null'` \| `'!= null'` | `null` | `U.compare(null, '!= null')` |
873
+ | `'= true'` \| `'= false'` | `boolean` | `U.compare(false, '= true')` |
874
+ | `'='` \| `'!='` \| `'>'` \| `'>='` \| `'<'` \| `'<='` | `number` \| `bigint` \| `Decimal` \| `string` \| `Date` | `U.compare(1, '=', 2)`, `U.compare(BigInt(1), '!=', BigInt(2))`, `U.compare(new Decimal(1.1), '>', new Decimal(2.2))` |
875
+ | `'in'` \| `'not in'` | `number` \| `bigint` \| `Decimal` \| `string` \| `Date` | `U.compare('a', 'in', ['b', 'c'])`, `U.compare(new Date('2000-01-01T00:00:00.000Z'), 'not in', [new Date('2000-01-02T00:00:00.000Z'), new Date('2000-01-03T00:00:00.000Z')])` |
876
+ | `'in sub-query'` \| `'not in sub-query'` | `number` \| `bigint` \| `Decimal` \| `string` \| `Date` | `U.compare(1, 'in sub-query', User.select(['id'], true))` |
877
+ | `'like'` \| `'like all'` \| `'like some'` | `string` | `U.compare('a', 'like', '%b%')`, `U.compare('a', 'like all', ['%b%', 'c'])`, `U.compare('a', 'like some', ['%b%', 'c'])` |
878
+ | `'between'` | `number` \| `bigint` \| `Decimal` \| `string` \| `Date` | `U.between(1, 'between', 2, 3)`, `U.between(new Date('2000-01-01T00:00:00.000Z'), 'between', new Date('2000-01-02T00:00:00.000Z'), new Date('2000-01-03T00:00:00.000Z'))` |
879
+ | `'='` \| `'!='` \| `'@>'` \| `'<@'` | `Json` (See [Json Type](#json-type)) | `U.compare('{ "name": "john" }', '=', '{ "age": 12 }')`, `U.compare('["blue", "red", "yellow"]', '@>', '["red"]')` |
880
+ | `'?'` \| `'?\|'` \| `'?&'` \| `'@@'` | `Json` (See [Json Type](#json-type)) | `U.compare('["blue", "green", "red"]', '?', "yellow")`, `U.compare('["blue", "green", "red"]', '?&', '["yellow"]')` |
881
+
882
+ Example:
883
+
884
+ ```typescript
885
+ import { U } from 'zero-orm';
886
+ import { User } from './User';
887
+
888
+ console.log(User.select(['id'], U.compare(1, '=', 2)).getData());
889
+ ```
890
+
891
+ ### U.arithmetic
892
+
893
+ Use `U.arithmetic` to apply an arithmetic operator to two values.
894
+
895
+ | Operators | Expression Type | Example |
896
+ |--------------------------------------------|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
897
+ | `'+'` \| `'-'` \| `'*'` \| `'/'` \| `'**'` | `number` \| `bigint` \| `Decimal` | `U.arithmetic(1, '+', 2)` (= 3), `U.arithmetic(1, '+', [2, 3])` (= 6), `U.arithmetic(2, '*', 3)` (= 6), `U.arithmetic(2, '*', [3, 4])` (= 24) |
898
+
899
+ Example:
900
+
901
+ ```typescript
902
+ import { U } from 'zero-orm';
903
+ import { User } from './User';
904
+
905
+ console.log(
906
+ User.select(['id'], U.compare(1, '=', U.arithmetic(2, '+', 3))).getData()
907
+ );
908
+ ```
909
+
910
+ ### U.json
911
+
912
+ Use `U.json` to apply `jsonb`/`json`-specific operators.
913
+ Refer
914
+ to [Processing and Creating JSON Data](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-PROCESSING)
915
+ for details.
916
+
917
+ | Operators | Expression Type | Example |
918
+ |-------------------------------------------------------------|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
919
+ | `'j-'` \| `'j- Array'` \| `'->'` \| `'-> Array'` \| `'->>'` | `Json` (See [Json Type](#json-type)) | `U.json('{ "name": "john" }', 'j-', 'name')`, `U.json('{ "name": "john" }', '->', 'name')`, `U.json('{ "name": { "first": "John", "last": "Diggle" } }', '-> Array', ['name', 'diggle'])` |
920
+
921
+ Example:
922
+
923
+ ```typescript
924
+ import { U } from 'zero-orm';
925
+ import { User } from './User';
926
+
927
+ console.log(
928
+ User.select(
929
+ ['id'],
930
+ U.compare('John', '=', U.json({ name: 'John' }, '->>', 'name'))
931
+ ).getData()
932
+ );
933
+ ```
934
+
935
+ ### U.fun
936
+
937
+ Use `U.fun` to call an SQL function.
938
+
939
+ ```typescript
940
+ import { U } from 'zero-orm';
941
+ import { User } from './User';
942
+
943
+ console.log(
944
+ User.select(
945
+ ['id'],
946
+ U.compare(
947
+ 12,
948
+ '=',
949
+ U.fun(
950
+ 'SUBSTRING', // Function name
951
+ ['AB123', 3, 4], // Function parameters
952
+ '::INTEGER' // SQL cast
953
+ ) as number // Explicit casting is needed as Zero-ORM doesn't know the SQL function signature
954
+ )
955
+ ).getData()
956
+ );
957
+ ```
958
+
959
+ ### U.cons
960
+
961
+ Use `U.cons` to call an SQL constructor.
962
+
963
+ ```typescript
964
+ import { U } from 'zero-orm';
965
+ import { User } from './User';
966
+
967
+ console.log(
968
+ User.select(
969
+ ['id'],
970
+ U.compare(
971
+ 1,
972
+ '=',
973
+ U.fun('ANY', [
974
+ U.cons(
975
+ 'ARRAY', // Constructor name
976
+ [1, 2, 3] // Elements
977
+ ) as number[] // Explicit casting is needed as Zero-ORM doesn't know the SQL constructor signature
978
+ ]) as number
979
+ )
980
+ ).getData()
981
+ );
982
+ ```
983
+
984
+ ### U.switchCase
985
+
986
+ Use `U.switchCase` to define an SQL `CASE` expression.
987
+
988
+ ```typescript
989
+ import { U } from 'zero-orm';
990
+ import { User } from './User';
991
+
992
+ console.log(
993
+ User.select(
994
+ ['id'],
995
+ U.compare(
996
+ 1,
997
+ '=',
998
+ U.switchCase(
999
+ [
1000
+ {
1001
+ // Case 1
1002
+ when: U.compare(2, '=', 3),
1003
+ then: 4
1004
+ },
1005
+ {
1006
+ // Case 2
1007
+ when: U.compare(5, '=', 6),
1008
+ then: 7
1009
+ }
1010
+ ],
1011
+ 8 // Default value
1012
+ )
1013
+ )
1014
+ ).getData()
1015
+ );
1016
+ ```
1017
+
1018
+ ### U.concat
1019
+
1020
+ Use `U.concat` to concatenate strings and JSON values.
1021
+
1022
+ | Expression Type | Example |
1023
+ |--------------------------------------|---------------------------------------------------------------------------|
1024
+ | `string` | `U.concat('hello', ' ', 'world!')` |
1025
+ | `Json` (See [Json Type](#json-type)) | `U.concat(["John"], ["Sam"])`, `U.concat({"name": "John" }, {"age": 10})` |
1026
+
1027
+ Example:
1028
+
1029
+ ```typescript
1030
+ import { U } from 'zero-orm';
1031
+ import { User } from './User';
1032
+
1033
+ console.log(
1034
+ User.select(['id'], U.compare('John', '=', U.concat('Jo', 'hn'))).getData()
1035
+ );
1036
+ ```
1037
+
1038
+ ### U.not
1039
+
1040
+ Use `U.not` to negate a boolean expression.
1041
+
1042
+ ```typescript
1043
+ import { U } from 'zero-orm';
1044
+ import { User } from './User';
1045
+
1046
+ console.log(User.select(['id'], U.not(U.compare(1, '=', 1))).getData());
1047
+ ```
1048
+
1049
+ ### U.and
1050
+
1051
+ Use `U.and` to apply a logical `AND` to multiple boolean expressions.
1052
+
1053
+ ```typescript
1054
+ import { U } from 'zero-orm';
1055
+ import { User } from './User';
1056
+
1057
+ console.log(
1058
+ User.select(
1059
+ ['id'],
1060
+ U.and(U.compare(1, '=', 1), false, U.compare('A', '!=', 'A'))
1061
+ ).getData()
1062
+ );
1063
+ ```
1064
+
1065
+ ### U.or
1066
+
1067
+ Use `U.or` to apply a logical `OR` to multiple boolean expressions.
1068
+
1069
+ ```typescript
1070
+ import { U } from 'zero-orm';
1071
+ import { User } from './User';
1072
+
1073
+ console.log(
1074
+ User.select(
1075
+ ['id'],
1076
+ U.or(U.compare(1, '=', 1), false, U.compare('A', '!=', 'A'))
1077
+ ).getData()
1078
+ );
1079
+ ```
1080
+
1081
+ ### U.subQuery
1082
+
1083
+ Use `U.subQuery` to create a sub-query.
1084
+ **Hint:** Prefer using [U.compare](#ucompare) with the `in sub-query` operator instead.
1085
+
1086
+ ```typescript
1087
+ import { U } from 'zero-orm';
1088
+ import { User } from './User';
1089
+
1090
+ console.log(
1091
+ User.select(
1092
+ ['id'],
1093
+ U.compare(1, 'in', U.subQuery<number[]>(User.select(['id'], true)))
1094
+ ).getData()
1095
+ );
1096
+ ```
1097
+
1098
+ ### U.subQueryExist
1099
+
1100
+ Use `U.subQueryExist` to create an `EXISTS` sub-query.
1101
+
1102
+ ```typescript
1103
+ import { U } from 'zero-orm';
1104
+ import { User } from './User';
1105
+
1106
+ console.log(
1107
+ User.select(['id'], U.subQueryExist(User.select(['id'], true))).getData()
1108
+ );
1109
+ ```
1110
+
1111
+ ### U.raw
1112
+
1113
+ Use `U.raw` to bypass Zero-ORM and write raw SQL directly into the query text. Use the function form if you need to
1114
+ use a parameterized query. Ensure you follow the `paramsStart` number to avoid mixing up parameters with others in the
1115
+ query.
1116
+
1117
+ ```typescript
1118
+ import { U } from 'zero-orm';
1119
+ import { User } from './User';
1120
+
1121
+ console.log(
1122
+ User.select(['id'], context =>
1123
+ U.compare(
1124
+ U.raw<Date>(paramsStart => ({
1125
+ expression: `($${paramsStart++} + $${paramsStart++})`,
1126
+ params: ['2000-01-01T00:00:00.000Z', '2000-01-02T00:00:00.000Z']
1127
+ })),
1128
+ '>',
1129
+ U.raw<Date>('(SELECT NOW())')
1130
+ )
1131
+ ).getData()
1132
+ );
1133
+ ```
1134
+
1135
+ ### U.ignore
1136
+
1137
+ `U.ignore` is one of the magical utilities in Zero-ORM that provides absolute safety and saves you a ton of
1138
+ boilerplate code.
1139
+
1140
+ There are certain scenarios that create **neutral** expressions, which can silently affect your queries:
1141
+
1142
+ - `context.compare('id', 'in', ids)` - What happens if `ids` is an empty array?
1143
+ - `context.columnsAnd({ username: ['like some', targets] })` - What happens if `targets` is an empty array?
1144
+ - `U.and(...rules)` - What happens if `rules` is an empty array?
1145
+
1146
+ We call these **neutral** expressions, and there is no way to catch them at compile time.
1147
+
1148
+ By default, Zero-ORM will create a **run-time error** when it receives a neutral expression instead of simply ignoring
1149
+ it. This is the safe behavior. While it ensures confidence that an entire table's rows will not be deleted if you pass a
1150
+ neutral condition to a `delete` query, it can become a nightmare in `select` queries that involve many optional filters.
1151
+
1152
+ Use `U.ignore` to wrap an expression and safely ignore any neutral expressions within it.
1153
+
1154
+ ```typescript
1155
+ import { U } from 'zero-orm';
1156
+ import { User } from './User';
1157
+
1158
+ const ids: number[] = [];
1159
+ const usernames: string[] = [];
1160
+ const names: string[] = [];
1161
+
1162
+ console.log(
1163
+ User.select(['id'], context =>
1164
+ U.ignore(
1165
+ U.and(
1166
+ context.compare('id', 'in', ids),
1167
+ context.compare('username', 'like all', usernames),
1168
+ context.compare('name', 'like all', names)
1169
+ ),
1170
+ true // Fallback expression if the entire expression is neutral (e.g., no filters provided).
1171
+ )
1172
+ ).getData()
1173
+ );
1174
+ ```
1175
+
1176
+ ### U.column
1177
+
1178
+ Use `U.column` to access the columns of a table.
1179
+ **Hint:** This is a low-level API and is used internally. Prefer using `context.column` instead.
1180
+
1181
+ | Parameters | Type | Description |
1182
+ |------------|-----------|-----------------------------------------------------------------------------------------------------------------|
1183
+ | `table` | `Table` | The table definition. |
1184
+ | `column` | `string` | The column name. |
1185
+ | `full?` | `boolean` | The full form, which includes the schema and table name as a prefix. |
1186
+ | `alias?` | `string` | An alias to replace the schema and table name. Useful when using aliases for tables in JOIN and SELECT queries. |
1187
+
1188
+ Example:
1189
+
1190
+ ```typescript
1191
+ import { U } from 'zero-orm';
1192
+ import { User } from './User';
1193
+
1194
+ console.log(
1195
+ User.select(
1196
+ ['id'],
1197
+ U.compare(U.column(User.table, 'id', false), '=', 1)
1198
+ ).getData()
1199
+ );
1200
+ ```
1201
+
1202
+ ### U.value
1203
+
1204
+ Use `U.value` to wrap a value explicitly. Generally, Zero-ORM detects types properly, and there is usually no need to
1205
+ wrap your values.
1206
+ **Attention:** USING `U.value` IS **REQUIRED ONLY WHEN PASSING A JSON ARRAY**. (Why? It interferes with Zero-ORM's
1207
+ internal type system.)
1208
+
1209
+ ```typescript
1210
+ import { U } from 'zero-orm';
1211
+ import { User } from './User';
1212
+
1213
+ console.log(
1214
+ User.select(
1215
+ ['id'],
1216
+ U.and(
1217
+ U.compare(
1218
+ U.value(1), // Unnecessary wrapping
1219
+ '=',
1220
+ 2
1221
+ ),
1222
+ U.compare(
1223
+ U.value(['green']), // Correct and required wrapping
1224
+ '=',
1225
+ ['blue', 'red'] // Unwrapped value when wrapping is necessary. This causes unexpected behavior!
1226
+ )
1227
+ )
1228
+ ).getData()
1229
+ );
1230
+ ```
1231
+
1232
+ ## Context
1233
+
1234
+ The `context` object provides access to table columns and allows you to create advanced `where` clauses and expressions.
1235
+ Methods like `context.compare`, `context.columnsAnd` and `context.columnsOr` follow the same signature as the `U`
1236
+ functions.
1237
+
1238
+ ```typescript
1239
+ import { U } from 'zero-orm';
1240
+ import { User } from './User';
1241
+
1242
+ console.log(
1243
+ User.select(['id'], context =>
1244
+ U.and(
1245
+ U.compare(context.column('id'), '=', 1),
1246
+ context.compare('id', '=', 1),
1247
+ context.columnsAnd({
1248
+ id: ['=', 1],
1249
+ name: ['like', 'John%']
1250
+ }),
1251
+ context.columnsOr({
1252
+ id: ['=', 1],
1253
+ isAdmin: ['= true']
1254
+ })
1255
+ )
1256
+ ).getData()
1257
+ );
1258
+ ```
1259
+
1260
+ For reusability, you can define a `where` clause separately.
1261
+
1262
+ ```typescript
1263
+ import { U, type Context } from 'zero-orm';
1264
+ import { type UserSchema, User } from './User';
1265
+
1266
+ const where = (context: Context<UserSchema>) =>
1267
+ U.and(
1268
+ U.compare(context.column('id'), '=', 1),
1269
+ context.compare('id', '=', 1),
1270
+ context.columnsAnd({
1271
+ id: ['=', 1],
1272
+ name: ['like', 'John%']
1273
+ }),
1274
+ context.columnsOr({
1275
+ id: ['=', 1],
1276
+ isAdmin: ['= true']
1277
+ })
1278
+ );
1279
+
1280
+ console.log(User.select(['id'], where).getData());
1281
+ ```
1282
+
1283
+ ## Execution Mode
1284
+
1285
+ Zero-ORM returns a `Result<V, E>` as output for all queries (select, insert, update, delete). For more details, check
1286
+ out [never-catch](https://github.com/MRNafisiA/never-catch) (inspired by the [Rust](https://rust-lang.org/) `Result`
1287
+ enum).
1288
+
1289
+ Execution mode is a post-query operation that controls the output type and helps you remove boilerplate code.
1290
+
1291
+ Example: `['get', 'one']`
1292
+
1293
+ ```typescript
1294
+ import { pool } from './db';
1295
+ import { User } from './User';
1296
+ import { PoolClient } from 'pg';
1297
+
1298
+ const selectUser = async (client: PoolClient) => {
1299
+ const result = await User.select(['id'], true).execute(client, [
1300
+ 'get',
1301
+ 'one'
1302
+ ]);
1303
+ if (!result.ok) {
1304
+ console.log('Query failed!');
1305
+ if (result.error === false) {
1306
+ console.log(
1307
+ 'The query was successful on the database, but the number of fetched rows was not 1.'
1308
+ );
1309
+ } else {
1310
+ console.log(`Database failed with error: ${result.error}`);
1311
+ }
1312
+ } else {
1313
+ console.log('Query succeeded and the number of fetched rows is 1.');
1314
+ console.log(result.value); // { "id" }
1315
+ }
1316
+ };
1317
+
1318
+ pool.connect().then(async client => {
1319
+ await selectUser(client);
1320
+ client.release();
1321
+ await pool.end();
1322
+ });
1323
+ ```
1324
+
1325
+ Example: `['get', number]`
1326
+
1327
+ ```typescript
1328
+ import { pool } from './db';
1329
+ import { User } from './User';
1330
+ import { PoolClient } from 'pg';
1331
+
1332
+ const selectUser = async (client: PoolClient) => {
1333
+ const result = await User.select(['id'], true).execute(client, ['get', 5]);
1334
+ if (!result.ok) {
1335
+ console.log('Query failed!');
1336
+ if (result.error === false) {
1337
+ console.log(
1338
+ 'The query was successful on the database, but the number of fetched rows was not 5.'
1339
+ );
1340
+ } else {
1341
+ console.log(`Database failed with error: ${result.error}`);
1342
+ }
1343
+ } else {
1344
+ console.log('Query succeeded and the number of fetched rows is 5.');
1345
+ console.log(result.value[0]); // First element: { "id" }
1346
+ console.log(result.value[3]); // Fourth element: { "id" }
1347
+ }
1348
+ };
1349
+
1350
+ pool.connect().then(async client => {
1351
+ await selectUser(client);
1352
+ client.release();
1353
+ await pool.end();
1354
+ });
1355
+ ```
1356
+
1357
+ Example: `['count', 2]`
1358
+
1359
+ ```typescript
1360
+ import { pool } from './db';
1361
+ import { User } from './User';
1362
+ import { PoolClient } from 'pg';
1363
+
1364
+ const selectUser = async (client: PoolClient) => {
1365
+ const result = await User.select(['id'], true).execute(client, [
1366
+ 'count',
1367
+ 2
1368
+ ]);
1369
+ if (!result.ok) {
1370
+ console.log('Query failed!');
1371
+ if (result.error === false) {
1372
+ console.log(
1373
+ 'The query was successful on the database, but the number of fetched rows was not 2.'
1374
+ );
1375
+ } else {
1376
+ console.log(`Database failed with error: ${result.error}`);
1377
+ }
1378
+ } else {
1379
+ console.log('Query succeeded and the number of fetched rows is 2.');
1380
+ console.log(result.value); // undefined
1381
+ }
1382
+ };
1383
+
1384
+ pool.connect().then(async client => {
1385
+ await selectUser(client);
1386
+ client.release();
1387
+ await pool.end();
1388
+ });
1389
+ ```
1390
+
1391
+ Example: `[]`
1392
+
1393
+ ```typescript
1394
+ import { pool } from './db';
1395
+ import { User } from './User';
1396
+ import { PoolClient } from 'pg';
1397
+
1398
+ const selectUser = async (client: PoolClient) => {
1399
+ const result = await User.select(['id'], true).execute(client, []);
1400
+ if (!result.ok) {
1401
+ console.log(
1402
+ `Query failed! Database failed with error: ${result.error}`
1403
+ );
1404
+ } else {
1405
+ console.log('Query succeeded.');
1406
+ console.log(result.value[0]); // First element: { "id" }
1407
+ console.log(result.value[3]); // Fourth element: { "id" }
1408
+ }
1409
+ };
1410
+
1411
+ pool.connect().then(async client => {
1412
+ await selectUser(client);
1413
+ client.release();
1414
+ await pool.end();
1415
+ });
1416
+ ```
1417
+
1418
+ ## SELECT Operations
1419
+
1420
+ ### SELECT Parameter: `returning`
1421
+
1422
+ `returning` is an array of columns to return. You can pass the array directly (`R`) or use a generator function to
1423
+ access the context (`context => R[]`). The returning parameter can be columns directly (e.g., `'id'`) or virtual columns
1424
+ like `{ name: 'avg', expression: U.fun('AVERAGE', [context.column('age')]) }`.
1425
+
1426
+ ### SELECT Parameter: `where`
1427
+
1428
+ `where` is a boolean expression that is checked for every row. You can pass the condition directly (e.g., `true`) or use
1429
+ a generator function to access the context (e.g., `context => boolean`).
1430
+
1431
+ ### SELECT Parameter: `selectOptions`
1432
+
1433
+ #### SelectOptions.distinct?
1434
+
1435
+ If `distinct` is `true`, Zero-ORM uses `DISTINCT` in the final query. If you pass an array of columns, Zero-ORM uses
1436
+ `DISTINCT ON ()` in the final query. You can pass a custom expression instead of direct columns in the form of
1437
+ `{ expression: value }`. You can also use a generator function to access the context.
1438
+
1439
+ #### SelectOptions.groupBy?
1440
+
1441
+ You can pass an array of columns or a custom expression directly, or use a generator function to access the context.
1442
+
1443
+ #### SelectOptions.orders?
1444
+
1445
+ You can pass an array of `Order` objects to specify your column or expression, the sort direction, and the null position
1446
+ directly, or use a generator function to access the context.
1447
+
1448
+ #### SelectOptions.start?
1449
+
1450
+ `start` specifies how many of the first rows to skip. The default is `0`.
1451
+
1452
+ #### SelectOptions.step?
1453
+
1454
+ `step` specifies how many rows to fetch. The default is all rows.
1455
+
1456
+ #### SelectOptions.customQueryBuilder?
1457
+
1458
+ `customQueryBuilder` gives you full access to the query-building mechanism. If using `U.raw` does not meet your needs,
1459
+ you can use `customQueryBuilder` for writing CTEs, complex sub-queries, recursive queries, and more. Zero-ORM provides
1460
+ a default `customQueryBuilder` that you can call with your changes or rewrite entirely from scratch.
1461
+
1462
+ ```typescript
1463
+ type CustomQueryBuilder = (
1464
+ parts: Record<
1465
+ `${'distinct' | 'returning' | 'from' | 'where' | 'groupBy' | 'orders' | 'pagination'}Part`,
1466
+ string
1467
+ >,
1468
+ params: string[]
1469
+ ) => { sql: string; params: string[] };
1470
+
1471
+ const defaultCustomQueryBuilder: CustomQueryBuilder = (parts, params) => {
1472
+ const tokens = ['SELECT'];
1473
+ if (parts.distinctPart !== '') {
1474
+ tokens.push(parts.distinctPart);
1475
+ }
1476
+ tokens.push(
1477
+ parts.returningPart,
1478
+ 'FROM',
1479
+ parts.fromPart,
1480
+ 'WHERE',
1481
+ parts.wherePart
1482
+ );
1483
+ if (parts.groupByPart !== '') {
1484
+ tokens.push('GROUP BY', parts.groupByPart);
1485
+ }
1486
+ if (parts.ordersPart !== '') {
1487
+ tokens.push('ORDER BY', parts.ordersPart);
1488
+ }
1489
+ if (parts.paginationPart !== '') {
1490
+ tokens.push(parts.paginationPart);
1491
+ }
1492
+
1493
+ return {
1494
+ sql: tokens.join(' '),
1495
+ params
1496
+ };
1497
+ };
1498
+ ```
1499
+
1500
+ ### Example of SELECT Operations
1501
+
1502
+ Example 1:
1503
+
1504
+ ```typescript
1505
+ import { U } from 'zero-orm';
1506
+ import { pool } from './db';
1507
+ import { User } from './User';
1508
+ import { PoolClient } from 'pg';
1509
+
1510
+ const selectUser = async (client: PoolClient) => {
1511
+ const result = await User.select(
1512
+ context => [
1513
+ 'id',
1514
+ 'name',
1515
+ {
1516
+ name: 'isActiveAndIsAdmin',
1517
+ expression: U.and(
1518
+ context.column('isActive'),
1519
+ context.column('isAdmin')
1520
+ )
1521
+ }
1522
+ ],
1523
+ context => context.compare('username', 'like', 'john%'),
1524
+ {
1525
+ distinct: true,
1526
+ orders: context => [
1527
+ {
1528
+ by: 'name',
1529
+ direction: 'desc',
1530
+ nullPosition: 'last'
1531
+ },
1532
+ {
1533
+ by: {
1534
+ expression: U.and(
1535
+ context.column('isActive'),
1536
+ context.column('isAdmin')
1537
+ )
1538
+ },
1539
+ direction: 'desc'
1540
+ }
1541
+ ],
1542
+ start: BigInt(30),
1543
+ step: 25
1544
+ }
1545
+ ).execute(client, []);
1546
+ if (!result.ok) {
1547
+ throw new Error(`Query failed with error ${result.error}`);
1548
+ }
1549
+ console.log(result.value);
1550
+ /* Array of rows: {
1551
+ * id: number,
1552
+ * name: string | null,
1553
+ * isActiveAndIsAdmin: boolean
1554
+ * }[]
1555
+ */
1556
+ };
1557
+
1558
+ pool.connect().then(async client => {
1559
+ await selectUser(client);
1560
+ client.release();
1561
+ await pool.end();
1562
+ });
1563
+ ```
1564
+
1565
+ Example 2:
1566
+
1567
+ ```typescript
1568
+ import { U } from 'zero-orm';
1569
+ import { pool } from './db';
1570
+ import { User } from './User';
1571
+ import { PoolClient } from 'pg';
1572
+
1573
+ const selectUser = async (client: PoolClient) => {
1574
+ const result = await User.select(
1575
+ context => [
1576
+ 'name',
1577
+ {
1578
+ name: 'isActiveAndIsAdmin',
1579
+ expression: U.and(
1580
+ context.column('isActive'),
1581
+ context.column('isAdmin')
1582
+ )
1583
+ }
1584
+ ],
1585
+ true,
1586
+ {
1587
+ distinct: ['name'],
1588
+ groupBy: context => [
1589
+ 'name',
1590
+ {
1591
+ expression: U.and(
1592
+ context.column('isActive'),
1593
+ context.column('isAdmin')
1594
+ )
1595
+ }
1596
+ ]
1597
+ }
1598
+ ).execute(client, []);
1599
+ if (!result.ok) {
1600
+ throw new Error(`Query failed with error ${result.error}`);
1601
+ }
1602
+ console.log(result.value);
1603
+ /* Array of rows: {
1604
+ * name: string | null,
1605
+ * isActiveAndIsAdmin: boolean
1606
+ * }[]
1607
+ */
1608
+ };
1609
+
1610
+ pool.connect().then(async client => {
1611
+ await selectUser(client);
1612
+ client.release();
1613
+ await pool.end();
1614
+ });
1615
+ ```
1616
+
1617
+ ## INSERT Operations
1618
+
1619
+ ### INSERT Parameter: `rows`
1620
+
1621
+ Default and nullable columns are optional in `rows`. The priority is: the value specified in `rows`, then the default
1622
+ value, and then `null`. You can pass the `rows` directly or use a generator function to access the context.
1623
+
1624
+ ### INSERT Parameter: `returning`
1625
+
1626
+ See [SELECT Parameter: `returning`](#select-parameter-returning).
1627
+
1628
+ ### Example of INSERT Operations
1629
+
1630
+ ```typescript
1631
+ import { pool } from './db';
1632
+ import { User } from './User';
1633
+ import { PoolClient } from 'pg';
1634
+
1635
+ const insertUser = async (client: PoolClient) => {
1636
+ const result = await User.insert(
1637
+ [
1638
+ {
1639
+ username: 'root',
1640
+ isActive: true,
1641
+ roles: ['reporter']
1642
+ }
1643
+ ],
1644
+ ['id', 'username']
1645
+ ).execute(client, []);
1646
+ if (!result.ok) {
1647
+ throw new Error(`Query failed with error ${result.error}`);
1648
+ }
1649
+ console.log(result.value); // { id: number }[]
1650
+ };
1651
+
1652
+ pool.connect().then(async client => {
1653
+ await insertUser(client);
1654
+ client.release();
1655
+ await pool.end();
1656
+ });
1657
+ ```
1658
+
1659
+ ## UPDATE Operations
1660
+
1661
+ ### UPDATE Parameter: `sets`
1662
+
1663
+ You can pass the `sets` object directly or use a generator function to access the context.
1664
+
1665
+ ### UPDATE Parameter: `where`
1666
+
1667
+ See [SELECT Parameter: `where`](#select-parameter-where).
1668
+
1669
+ ### UPDATE Parameter: `returning`
1670
+
1671
+ See [SELECT Parameter: `returning`](#select-parameter-returning).
1672
+
1673
+ ### Example of UPDATE Operations
1674
+
1675
+ ```typescript
1676
+ import { U } from 'zero-orm';
1677
+ import { pool } from './db';
1678
+ import { User } from './User';
1679
+ import { PoolClient } from 'pg';
1680
+
1681
+ const updateUser = async (client: PoolClient) => {
1682
+ const result = await User.update(
1683
+ context => ({
1684
+ isActive: U.not(context.column('isActive')),
1685
+ isAdmin: false
1686
+ }),
1687
+ context => context.compare('roles', '?', 'reporter'),
1688
+ ['isActive']
1689
+ ).execute(client, []);
1690
+ if (!result.ok) {
1691
+ throw new Error(`Query failed with error ${result.error}`);
1692
+ }
1693
+ console.log(result.value); // { id: number }[]
1694
+ };
1695
+
1696
+ pool.connect().then(async client => {
1697
+ await updateUser(client);
1698
+ client.release();
1699
+ await pool.end();
1700
+ });
1701
+ ```
1702
+
1703
+ ## DELETE Operations
1704
+
1705
+ ### DELETE Parameter: `where`
1706
+
1707
+ See [SELECT Parameter: `where`](#select-parameter-where).
1708
+
1709
+ ### DELETE Parameter: `returning`
1710
+
1711
+ See [SELECT Parameter: `returning`](#select-parameter-returning).
1712
+
1713
+ ### Example of DELETE Operations
1714
+
1715
+ ```typescript
1716
+ import { pool } from './db';
1717
+ import { User } from './User';
1718
+ import { PoolClient } from 'pg';
1719
+
1720
+ const deleteUser = async (client: PoolClient) => {
1721
+ const result = await User.delete(
1722
+ context => context.compare('name', '=', 'john'),
1723
+ ['id']
1724
+ ).execute(client, []);
1725
+ if (!result.ok) {
1726
+ throw new Error(`Query failed with error ${result.error}`);
1727
+ }
1728
+ console.log(result.value); // { id: number }[]
1729
+ };
1730
+
1731
+ pool.connect().then(async client => {
1732
+ await deleteUser(client);
1733
+ client.release();
1734
+ await pool.end();
1735
+ });
1736
+ ```
1737
+
1738
+ ## JOIN Operations
1739
+
1740
+ Using the `join` function alone will not result in a full query. The output is an object with `select` and `join`
1741
+ functions, allowing you to join another table or perform a `select` on the joined tables.
1742
+
1743
+ ### JOIN Parameter: `mainAlias`
1744
+
1745
+ This is the alias for the main table. It affects the returning column names and the context name whenever a context is
1746
+ provided. A prefix of `${mainAlias}_` is added, so with `'u'` as the main alias, column names will be like `'u_id'`.
1747
+
1748
+ ### JOIN Parameter: `joinType`
1749
+
1750
+ This specifies one of the four standard SQL join types.
1751
+
1752
+ ```typescript
1753
+ type JoinType = 'inner' | 'left' | 'right' | 'full';
1754
+ ```
1755
+
1756
+ ### JOIN Parameter: `joinTable`
1757
+
1758
+ The table to join with.
1759
+
1760
+ ### JOIN Parameter: `joinAlias`
1761
+
1762
+ This behaves like `mainAlias` and affects the `joinTable` in the same way.
1763
+
1764
+ ### JOIN Parameter: `on`
1765
+
1766
+ `on` is a condition, similar to `where`, that determines which rows are included in the join. You can pass the `on`
1767
+ condition directly or use a generator function to access the contexts. When a `join` function is called, the joining
1768
+ table's context is added to the contexts object, prefixed with `${alias}Context`. For example, a main table with alias
1769
+ `u` and a joined table with alias `p` will create `{ uContext, pContext }` instead of a single `context`.
1770
+
1771
+ ### Example of JOIN Operations
1772
+
1773
+ ```typescript
1774
+ import { U } from 'zero-orm';
1775
+ import { pool } from './db';
1776
+ import { User } from './User';
1777
+ import { PoolClient } from 'pg';
1778
+ import { Product } from './Product';
1779
+
1780
+ const joinSelect = async (client: PoolClient) => {
1781
+ const result = await User.join(
1782
+ 'u',
1783
+ 'inner',
1784
+ Product.table,
1785
+ 'p',
1786
+ ({ uContext, pContext }) =>
1787
+ uContext.compare('id', '=', pContext.column('userID'))
1788
+ )
1789
+ .select(['u_id', 'p_title'], ({ uContext, pContext }) =>
1790
+ U.and(
1791
+ uContext.compare('name', 'like', 'john%'),
1792
+ pContext.columnsOr({
1793
+ title: ['like some', ['table%', 'chair%']],
1794
+ isDisabled: ['= false']
1795
+ })
1796
+ )
1797
+ )
1798
+ .execute(client, []);
1799
+ if (!result.ok) {
1800
+ throw new Error(`Query failed with error ${result.error}`);
1801
+ }
1802
+ console.log(result.value); // { u_id: number, p_title: string }[]
1803
+ };
1804
+
1805
+ pool.connect().then(async client => {
1806
+ await joinSelect(client);
1807
+ client.release();
1808
+ await pool.end();
1809
+ });
1810
+ ```
1811
+
1812
+ ## Transaction Management
1813
+
1814
+ You can use `transaction` to create a database transaction. The transaction will be **committed** if you return an `ok`
1815
+ result in the callback, and **rolled back** if you return an `err` result.
1816
+ **Hint:** Refer to [never-catch](https://github.com/MRNafisiA/never-catch) for details on `ok` and `err`.
1817
+
1818
+ ### Transaction Parameter: `pool`
1819
+
1820
+ A created `pg` Pool instance.
1821
+
1822
+ ### Transaction Parameter: `callback`
1823
+
1824
+ An asynchronous callback function that receives a connected `PoolClient` and returns a `Result`. The outcome of this
1825
+ result determines whether the transaction is committed or rolled back.
1826
+
1827
+ ### Transaction Parameter: `isolationLevel?`
1828
+
1829
+ Specifies one of the four standard SQL isolation levels. The default value is `serializable`.
1830
+
1831
+ ```typescript
1832
+ type TransactionIsolationLevel =
1833
+ | 'read-uncommitted'
1834
+ | 'read-committed'
1835
+ | 'repeatable-read'
1836
+ | 'serializable';
1837
+ ```
1838
+
1839
+ ### Transaction Parameter: `readOnly?`
1840
+
1841
+ Indicates whether the transaction is read-only. The default value is `false`.
1842
+
1843
+ ### Example of Transaction
1844
+
1845
+ ```typescript
1846
+ import { pool } from './db';
1847
+ import { User } from './User';
1848
+ import { ok } from 'never-catch';
1849
+ import { transaction } from 'zero-orm';
1850
+
1851
+ transaction(
1852
+ pool,
1853
+ async client => {
1854
+ const result = await User.insert(
1855
+ [{ username: 'admin', isActive: false, roles: ['writer'] }],
1856
+ ['id']
1857
+ ).execute(client, ['get', 1]);
1858
+ if (!result.ok) {
1859
+ return result; // The transaction will be rolled back.
1860
+ }
1861
+
1862
+ return ok(result.value.id); // The transaction will be committed.
1863
+ },
1864
+ 'read-committed',
1865
+ false
1866
+ ).then(async result => {
1867
+ console.log(result);
1868
+ await pool.end();
1869
+ });
1870
+ ```
1871
+
1872
+ ## Test Transaction Utility (for Unit and E2E Tests)
1873
+
1874
+ `testTransaction` is a utility for testing an action in isolation. It creates the necessary tables and sequences, fills
1875
+ them with your provided initial data, performs the action, checks the tables against expected final data, and then
1876
+ destroys everything in preparation for the next test.
1877
+
1878
+ ### Test Transaction Parameter: `tablesWithData`
1879
+
1880
+ An array of tables with their initial and expected final data.
1881
+
1882
+ ### Test Transaction Parameter: `callback`
1883
+
1884
+ An asynchronous callback function that receives a connected `PoolClient` with all your defined tables and data present.
1885
+ Perform your action and assertions for the test here. At the end of the callback, the tables' data will be automatically
1886
+ checked against the defined final data.
1887
+
1888
+ ### Test Transaction Parameter: `pool`
1889
+
1890
+ See [Transaction Parameter: `pool`](#transaction-parameter-pool).
1891
+ `testTransaction` expects an empty database, as it creates and destroys everything itself.
1892
+
1893
+ ### Test Transaction Parameter: `isolationLevel?`
1894
+
1895
+ See [Transaction Parameter: `isolationLevel?`](#transaction-parameter-isolationlevel).
1896
+ The default value in `testTransaction` is `'read-committed'`.
1897
+
1898
+ ### Test Transaction Parameter: `rollback?`
1899
+
1900
+ Controls whether the transaction is rolled back after the test.
1901
+ The default value is `true`. Pass `false` when you need the data to remain for debugging purposes.
1902
+
1903
+ ### Example of Test Transaction
1904
+
1905
+ **Hint:** Do not forget to put `.test.ts` at the end of the file name so the `test` and `expect` functions work.
1906
+
1907
+ ```typescript
1908
+ import { Pool } from 'pg';
1909
+ import { User, type UserModel } from './User';
1910
+ import { testTransaction, createTestTableData } from 'zero-orm';
1911
+
1912
+ const testPool = new Pool({
1913
+ connectionString: 'postgres://postgres:12345678@localhost:5432/test'
1914
+ });
1915
+
1916
+ afterAll(async () => {
1917
+ await testPool.end();
1918
+ });
1919
+
1920
+ test('update user', () => {
1921
+ const user: UserModel = {
1922
+ id: 1,
1923
+ username: 'john',
1924
+ name: 'john doe',
1925
+ isActive: true,
1926
+ isAdmin: false,
1927
+ roles: []
1928
+ };
1929
+
1930
+ return testTransaction(
1931
+ [
1932
+ createTestTableData(
1933
+ User.table,
1934
+ [user], // Nullable and default columns are optional
1935
+ [
1936
+ {
1937
+ ...user,
1938
+ name: 'JOHN DOE', // A plain value to check directly
1939
+ roles: (
1940
+ // Or a function that can check the value dynamically.
1941
+ // A boolean or Promise<boolean> for asynchronous checks (e.g., hash password verification).
1942
+ cell,
1943
+ rows,
1944
+ index
1945
+ ) => cell.length >= 3
1946
+ }
1947
+ ]
1948
+ )
1949
+ ],
1950
+ async client => {
1951
+ const result = await User.update(
1952
+ { name: 'JOHN DOE', roles: ['reporter', 'writer', 'manager'] },
1953
+ context => context.compare('id', '=', 1),
1954
+ ['id', 'name', 'roles']
1955
+ ).execute(client, ['get', 1]);
1956
+ if (!result.ok) {
1957
+ throw new Error('Query failed.');
1958
+ }
1959
+
1960
+ expect(result.value).toStrictEqual({
1961
+ id: 1,
1962
+ name: 'JOHN DOE',
1963
+ roles: ['reporter', 'writer', 'manager']
1964
+ });
1965
+ },
1966
+ testPool,
1967
+ 'read-committed',
1968
+ true
1969
+ );
1970
+ });
1971
+ ```
1972
+
1973
+ ## Json Type
1974
+
1975
+ Zero-ORM only allows serializable values for JSON columns, as data must be stringified when entering the database and
1976
+ parsed when it is fetched.
1977
+
1978
+ ```typescript
1979
+ type Json = JsonObject | JsonArray;
1980
+
1981
+ type JsonObject = {
1982
+ [key: number | string]: BaseJsonValue;
1983
+ };
1984
+ type JsonArray = BaseJsonValue[];
1985
+
1986
+ type BaseJsonValue =
1987
+ | undefined
1988
+ | null
1989
+ | boolean
1990
+ | number
1991
+ | string
1992
+ | JsonObject
1993
+ | JsonArray;
1994
+ ```
1995
+