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.
- package/LICENSE +21 -0
- package/README.md +1995 -0
- package/dist/prd.tsconfig.tsbuildinfo +1 -0
- package/dist/src/Table.d.ts +180 -0
- package/dist/src/Table.d.ts.map +1 -0
- package/dist/src/Table.js +8 -0
- package/dist/src/Table.js.map +1 -0
- package/dist/src/context.d.ts +90 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +84 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/createModelParser.d.ts +47 -0
- package/dist/src/createModelParser.d.ts.map +1 -0
- package/dist/src/createModelParser.js +339 -0
- package/dist/src/createModelParser.js.map +1 -0
- package/dist/src/ddl.d.ts +22 -0
- package/dist/src/ddl.d.ts.map +1 -0
- package/dist/src/ddl.js +91 -0
- package/dist/src/ddl.js.map +1 -0
- package/dist/src/entity.d.ts +114 -0
- package/dist/src/entity.d.ts.map +1 -0
- package/dist/src/entity.js +539 -0
- package/dist/src/entity.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +71 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keywords.d.ts +158 -0
- package/dist/src/keywords.d.ts.map +1 -0
- package/dist/src/keywords.js +187 -0
- package/dist/src/keywords.js.map +1 -0
- package/dist/src/resolve.d.ts +15 -0
- package/dist/src/resolve.d.ts.map +1 -0
- package/dist/src/resolve.js +600 -0
- package/dist/src/resolve.js.map +1 -0
- package/dist/src/testTransaction.d.ts +23 -0
- package/dist/src/testTransaction.d.ts.map +1 -0
- package/dist/src/testTransaction.js +113 -0
- package/dist/src/testTransaction.js.map +1 -0
- package/dist/src/transaction.d.ts +6 -0
- package/dist/src/transaction.d.ts.map +1 -0
- package/dist/src/transaction.js +28 -0
- package/dist/src/transaction.js.map +1 -0
- package/dist/src/utils.d.ts +62 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +86 -0
- package/dist/src/utils.js.map +1 -0
- 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
|
+
|