@neogroup/neorm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1019 -0
- package/dist/connection.d.ts +10 -0
- package/dist/connection.js +3 -0
- package/dist/connection.js.map +1 -0
- package/dist/data-connection.d.ts +24 -0
- package/dist/data-connection.js +99 -0
- package/dist/data-connection.js.map +1 -0
- package/dist/data-set.d.ts +3 -0
- package/dist/data-set.js +3 -0
- package/dist/data-set.js.map +1 -0
- package/dist/data-source.d.ts +23 -0
- package/dist/data-source.js +56 -0
- package/dist/data-source.js.map +1 -0
- package/dist/data-table.d.ts +19 -0
- package/dist/data-table.js +75 -0
- package/dist/data-table.js.map +1 -0
- package/dist/db.d.ts +57 -0
- package/dist/db.js +213 -0
- package/dist/db.js.map +1 -0
- package/dist/entities/decorators.d.ts +50 -0
- package/dist/entities/decorators.js +305 -0
- package/dist/entities/decorators.js.map +1 -0
- package/dist/entities/entity-query.d.ts +60 -0
- package/dist/entities/entity-query.js +305 -0
- package/dist/entities/entity-query.js.map +1 -0
- package/dist/entities/entity.d.ts +1 -0
- package/dist/entities/entity.js +3 -0
- package/dist/entities/entity.js.map +1 -0
- package/dist/entities/index.d.ts +4 -0
- package/dist/entities/index.js +20 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/relationship.d.ts +15 -0
- package/dist/entities/relationship.js +24 -0
- package/dist/entities/relationship.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/query/builders/default-query-builder.d.ts +87 -0
- package/dist/query/builders/default-query-builder.js +524 -0
- package/dist/query/builders/default-query-builder.js.map +1 -0
- package/dist/query/builders/index.d.ts +2 -0
- package/dist/query/builders/index.js +19 -0
- package/dist/query/builders/index.js.map +1 -0
- package/dist/query/builders/query-builder.d.ts +5 -0
- package/dist/query/builders/query-builder.js +7 -0
- package/dist/query/builders/query-builder.js.map +1 -0
- package/dist/query/conditions.d.ts +60 -0
- package/dist/query/conditions.js +142 -0
- package/dist/query/conditions.js.map +1 -0
- package/dist/query/delete-query.d.ts +8 -0
- package/dist/query/delete-query.js +17 -0
- package/dist/query/delete-query.js.map +1 -0
- package/dist/query/features/has-alias.d.ts +6 -0
- package/dist/query/features/has-alias.js +17 -0
- package/dist/query/features/has-alias.js.map +1 -0
- package/dist/query/features/has-distinct.d.ts +6 -0
- package/dist/query/features/has-distinct.js +20 -0
- package/dist/query/features/has-distinct.js.map +1 -0
- package/dist/query/features/has-field-values.d.ts +9 -0
- package/dist/query/features/has-field-values.js +27 -0
- package/dist/query/features/has-field-values.js.map +1 -0
- package/dist/query/features/has-group-by-fields.d.ts +7 -0
- package/dist/query/features/has-group-by-fields.js +21 -0
- package/dist/query/features/has-group-by-fields.js.map +1 -0
- package/dist/query/features/has-having-conditions.d.ts +13 -0
- package/dist/query/features/has-having-conditions.js +28 -0
- package/dist/query/features/has-having-conditions.js.map +1 -0
- package/dist/query/features/has-joins.d.ts +41 -0
- package/dist/query/features/has-joins.js +67 -0
- package/dist/query/features/has-joins.js.map +1 -0
- package/dist/query/features/has-limit.d.ts +6 -0
- package/dist/query/features/has-limit.js +20 -0
- package/dist/query/features/has-limit.js.map +1 -0
- package/dist/query/features/has-offset.d.ts +6 -0
- package/dist/query/features/has-offset.js +20 -0
- package/dist/query/features/has-offset.js.map +1 -0
- package/dist/query/features/has-order-by-fields.d.ts +15 -0
- package/dist/query/features/has-order-by-fields.js +38 -0
- package/dist/query/features/has-order-by-fields.js.map +1 -0
- package/dist/query/features/has-select-fields.d.ts +11 -0
- package/dist/query/features/has-select-fields.js +21 -0
- package/dist/query/features/has-select-fields.js.map +1 -0
- package/dist/query/features/has-table.d.ts +7 -0
- package/dist/query/features/has-table.js +20 -0
- package/dist/query/features/has-table.js.map +1 -0
- package/dist/query/features/has-unions.d.ts +13 -0
- package/dist/query/features/has-unions.js +21 -0
- package/dist/query/features/has-unions.js.map +1 -0
- package/dist/query/features/has-when.d.ts +3 -0
- package/dist/query/features/has-when.js +13 -0
- package/dist/query/features/has-when.js.map +1 -0
- package/dist/query/features/has-where-conditions.d.ts +35 -0
- package/dist/query/features/has-where-conditions.js +92 -0
- package/dist/query/features/has-where-conditions.js.map +1 -0
- package/dist/query/features/index.d.ts +14 -0
- package/dist/query/features/index.js +31 -0
- package/dist/query/features/index.js.map +1 -0
- package/dist/query/fields.d.ts +12 -0
- package/dist/query/fields.js +18 -0
- package/dist/query/fields.js.map +1 -0
- package/dist/query/index.d.ts +11 -0
- package/dist/query/index.js +28 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/insert-query.d.ts +8 -0
- package/dist/query/insert-query.js +17 -0
- package/dist/query/insert-query.js.map +1 -0
- package/dist/query/query.d.ts +2 -0
- package/dist/query/query.js +7 -0
- package/dist/query/query.js.map +1 -0
- package/dist/query/select-query.d.ts +9 -0
- package/dist/query/select-query.js +33 -0
- package/dist/query/select-query.js.map +1 -0
- package/dist/query/statement.d.ts +4 -0
- package/dist/query/statement.js +3 -0
- package/dist/query/statement.js.map +1 -0
- package/dist/query/table.d.ts +6 -0
- package/dist/query/table.js +3 -0
- package/dist/query/table.js.map +1 -0
- package/dist/query/update-query.d.ts +8 -0
- package/dist/query/update-query.js +17 -0
- package/dist/query/update-query.js.map +1 -0
- package/dist/sources/mysql/index.d.ts +3 -0
- package/dist/sources/mysql/index.js +20 -0
- package/dist/sources/mysql/index.js.map +1 -0
- package/dist/sources/mysql/mysql-connection.d.ts +14 -0
- package/dist/sources/mysql/mysql-connection.js +36 -0
- package/dist/sources/mysql/mysql-connection.js.map +1 -0
- package/dist/sources/mysql/mysql-data-source.d.ts +24 -0
- package/dist/sources/mysql/mysql-data-source.js +72 -0
- package/dist/sources/mysql/mysql-data-source.js.map +1 -0
- package/dist/sources/mysql/mysql-query-builder.d.ts +8 -0
- package/dist/sources/mysql/mysql-query-builder.js +28 -0
- package/dist/sources/mysql/mysql-query-builder.js.map +1 -0
- package/dist/sources/postgres/index.d.ts +2 -0
- package/dist/sources/postgres/index.js +19 -0
- package/dist/sources/postgres/index.js.map +1 -0
- package/dist/sources/postgres/postgres-connection.d.ts +13 -0
- package/dist/sources/postgres/postgres-connection.js +40 -0
- package/dist/sources/postgres/postgres-connection.js.map +1 -0
- package/dist/sources/postgres/postgres-data-source.d.ts +24 -0
- package/dist/sources/postgres/postgres-data-source.js +73 -0
- package/dist/sources/postgres/postgres-data-source.js.map +1 -0
- package/dist/sources/postgres/postgres-query-builder.d.ts +5 -0
- package/dist/sources/postgres/postgres-query-builder.js +13 -0
- package/dist/sources/postgres/postgres-query-builder.js.map +1 -0
- package/dist/sources/sqlite/index.d.ts +3 -0
- package/dist/sources/sqlite/index.js +20 -0
- package/dist/sources/sqlite/index.js.map +1 -0
- package/dist/sources/sqlite/sqlite-connection.d.ts +14 -0
- package/dist/sources/sqlite/sqlite-connection.js +37 -0
- package/dist/sources/sqlite/sqlite-connection.js.map +1 -0
- package/dist/sources/sqlite/sqlite-data-source.d.ts +11 -0
- package/dist/sources/sqlite/sqlite-data-source.js +34 -0
- package/dist/sources/sqlite/sqlite-data-source.js.map +1 -0
- package/dist/sources/sqlite/sqlite-query-builder.d.ts +6 -0
- package/dist/sources/sqlite/sqlite-query-builder.js +23 -0
- package/dist/sources/sqlite/sqlite-query-builder.js.map +1 -0
- package/dist/utilities.d.ts +2 -0
- package/dist/utilities.js +17 -0
- package/dist/utilities.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
[](https://badge.fury.io/js/@neogroup%2Fneorm)
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
# NeORM
|
|
8
|
+
|
|
9
|
+
A lightweight, fluent TypeScript library for interacting with relational databases. It provides a chainable query builder, connection/transaction management, and pluggable data sources for **PostgreSQL**, **MySQL**, and **SQLite** — with a clean architecture that makes it easy to add new engines.
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Data sources](#data-sources)
|
|
15
|
+
- [PostgreSQL](#postgresql)
|
|
16
|
+
- [MySQL](#mysql)
|
|
17
|
+
- [SQLite](#sqlite)
|
|
18
|
+
- [Multiple sources](#multiple-sources)
|
|
19
|
+
- [Configuration via environment variables](#configuration-via-environment-variables)
|
|
20
|
+
- [Querying with DataTable](#querying-with-datatable)
|
|
21
|
+
- [SELECT — basic](#select--basic)
|
|
22
|
+
- [Filtering — WHERE](#filtering--where)
|
|
23
|
+
- [Conditional clauses — when](#conditional-clauses--when)
|
|
24
|
+
- [Sorting — ORDER BY](#sorting--order-by)
|
|
25
|
+
- [Pagination — LIMIT & OFFSET](#pagination--limit--offset)
|
|
26
|
+
- [Grouping — GROUP BY & HAVING](#grouping--group-by--having)
|
|
27
|
+
- [Joins](#joins)
|
|
28
|
+
- [Distinct](#distinct)
|
|
29
|
+
- [Field aliases & functions](#field-aliases--functions)
|
|
30
|
+
- [Table alias](#table-alias)
|
|
31
|
+
- [INSERT, UPDATE, DELETE](#insert-update-delete)
|
|
32
|
+
- [Raw queries](#raw-queries)
|
|
33
|
+
- [Advanced queries with SelectQuery](#advanced-queries-with-selectquery)
|
|
34
|
+
- [UNION / UNION ALL](#union--union-all)
|
|
35
|
+
- [Subqueries](#subqueries)
|
|
36
|
+
- [Connections & transactions](#connections--transactions)
|
|
37
|
+
- [Explicit connection](#explicit-connection)
|
|
38
|
+
- [DB shorthand methods](#db-shorthand-methods)
|
|
39
|
+
- [Entities (Active Record)](#entities-active-record)
|
|
40
|
+
- [Defining an Entity](#defining-an-entity)
|
|
41
|
+
- [Querying](#querying)
|
|
42
|
+
- [Saving & deleting](#saving--deleting)
|
|
43
|
+
- [Casts](#casts)
|
|
44
|
+
- [Relationships](#relationships)
|
|
45
|
+
- [Eager loading — with()](#eager-loading--with)
|
|
46
|
+
- [Joining via relationships](#joining-via-relationships)
|
|
47
|
+
- [Debug mode](#debug-mode)
|
|
48
|
+
- [Extending the library](#extending-the-library)
|
|
49
|
+
- [Contact](#contact)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @neogroup/neorm
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Depending on the database engine you use, install the corresponding driver:
|
|
60
|
+
|
|
61
|
+
| Engine | Driver |
|
|
62
|
+
|------------|-------------------------|
|
|
63
|
+
| PostgreSQL | `npm install pg` |
|
|
64
|
+
| MySQL | `npm install mysql2` |
|
|
65
|
+
| SQLite | built-in (Node ≥ 22.5) |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Data sources
|
|
70
|
+
|
|
71
|
+
A **DataSource** represents a configured connection to a database engine. Register it once at startup with `DB.register()` and use `DB` anywhere in your code from that point on.
|
|
72
|
+
|
|
73
|
+
### PostgreSQL
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { DB, PostgresDataSource } from '@neogroup/neorm'
|
|
77
|
+
|
|
78
|
+
const source = new PostgresDataSource()
|
|
79
|
+
source.setHost('localhost')
|
|
80
|
+
source.setPort(5432) // default: 5432
|
|
81
|
+
source.setDatabaseName('mydb')
|
|
82
|
+
source.setUsername('admin')
|
|
83
|
+
source.setPassword('secret')
|
|
84
|
+
|
|
85
|
+
DB.register(source)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### MySQL
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { DB, MysqlDataSource } from '@neogroup/neorm'
|
|
92
|
+
|
|
93
|
+
const source = new MysqlDataSource()
|
|
94
|
+
source.setHost('localhost')
|
|
95
|
+
source.setPort(3306) // default: 3306
|
|
96
|
+
source.setDatabaseName('mydb')
|
|
97
|
+
source.setUsername('admin')
|
|
98
|
+
source.setPassword('secret')
|
|
99
|
+
|
|
100
|
+
DB.register(source)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
MySQL identifiers (table names, column names) are automatically quoted with backticks to avoid conflicts with reserved words.
|
|
104
|
+
|
|
105
|
+
### SQLite
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { DB, SqliteDataSource } from '@neogroup/neorm'
|
|
109
|
+
|
|
110
|
+
const source = new SqliteDataSource() // in-memory database
|
|
111
|
+
// source.setFilename('./data.db') // or a file path
|
|
112
|
+
|
|
113
|
+
DB.register(source)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
SQLite uses Node's built-in `node:sqlite` module — no native compilation required.
|
|
117
|
+
|
|
118
|
+
### Multiple sources
|
|
119
|
+
|
|
120
|
+
You can register multiple sources and switch between them by name:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
DB.register('primary', primarySource)
|
|
124
|
+
DB.register('reporting', reportingSource)
|
|
125
|
+
|
|
126
|
+
// Use a specific source explicitly
|
|
127
|
+
const rows = await DB.source('reporting').table('analytics').get()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The first registered source becomes the **active source** automatically. `DB.table(...)`, `DB.query(...)`, `DB.execute(...)` and the transaction helpers all target it. You can change it at any time with `setActiveSource()`:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
DB.setActiveSource('reporting')
|
|
134
|
+
|
|
135
|
+
// Now all DB.* calls target reportingSource
|
|
136
|
+
const rows = await DB.table('analytics').get()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Configuration via environment variables
|
|
140
|
+
|
|
141
|
+
Instead of calling `DB.register()` in code, you can configure data sources entirely through environment variables. The library auto-detects them the first time a source is needed — no bootstrap code required.
|
|
142
|
+
|
|
143
|
+
**Default source** — used by all `DB.*` calls:
|
|
144
|
+
|
|
145
|
+
| Variable | Description |
|
|
146
|
+
|---------------|----------------------------------------------------|
|
|
147
|
+
| `DB_DRIVER` | `sqlite` \| `postgres` \| `mysql` **(required)** |
|
|
148
|
+
| `DB_FILE` | SQLite file path (default: `:memory:`) |
|
|
149
|
+
| `DB_HOST` | Database host (postgres / mysql) |
|
|
150
|
+
| `DB_PORT` | Database port (postgres / mysql) |
|
|
151
|
+
| `DB_NAME` | Database name (postgres / mysql) |
|
|
152
|
+
| `DB_USERNAME` | Login username (postgres / mysql) |
|
|
153
|
+
| `DB_PASSWORD` | Login password (postgres / mysql) |
|
|
154
|
+
|
|
155
|
+
**Named sources** — replace `<NAME>` with the source name in upper-case:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
DB_<NAME>_DRIVER, DB_<NAME>_HOST, DB_<NAME>_PORT, DB_<NAME>_NAME,
|
|
159
|
+
DB_<NAME>_USERNAME, DB_<NAME>_PASSWORD, DB_<NAME>_FILE
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Single SQLite source — no code needed
|
|
166
|
+
DB_DRIVER=sqlite
|
|
167
|
+
DB_FILE=./data.db
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# PostgreSQL default + named SQLite for reporting
|
|
172
|
+
DB_DRIVER=postgres
|
|
173
|
+
DB_HOST=localhost
|
|
174
|
+
DB_NAME=myapp
|
|
175
|
+
DB_USERNAME=admin
|
|
176
|
+
DB_PASSWORD=secret
|
|
177
|
+
|
|
178
|
+
DB_REPORTING_DRIVER=sqlite
|
|
179
|
+
DB_REPORTING_FILE=./reporting.db
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// No DB.register() anywhere — sources are resolved from the environment
|
|
184
|
+
const users = await DB.table('users').get()
|
|
185
|
+
const report = await DB.source('reporting').table('stats').get()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
You can also call `DB.configure()` explicitly at startup if you want fail-fast behaviour (e.g. crash early instead of on the first query if a required variable is missing):
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
DB.configure() // throws immediately if DB_DRIVER is not set or invalid
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`DB.configure()` is a no-op if sources have already been registered manually, so it is safe to mix both styles in the same application.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Querying with DataTable
|
|
199
|
+
|
|
200
|
+
`DB.table('name')` returns a `DataTable` — a chainable query builder scoped to a single table. All methods return the same `DataTable` instance so you can chain them freely.
|
|
201
|
+
|
|
202
|
+
### SELECT — basic
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// SELECT * FROM users
|
|
206
|
+
const users = await DB.table('users').get()
|
|
207
|
+
|
|
208
|
+
// SELECT * FROM users LIMIT 1
|
|
209
|
+
const user = await DB.table('users').first() // returns null if no rows match
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Select specific columns:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// SELECT id, name, email FROM users
|
|
216
|
+
const users = await DB.table('users')
|
|
217
|
+
.select('id', 'name', 'email')
|
|
218
|
+
.get()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### Filtering — WHERE
|
|
224
|
+
|
|
225
|
+
**Simple equality:**
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// WHERE name = 'Alice'
|
|
229
|
+
await DB.table('users').where('name', 'Alice').get()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Comparison operators** (`=`, `<>`, `<`, `>`, `<=`, `>=`):
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
await DB.table('users').where('age', '>', 18).get()
|
|
236
|
+
await DB.table('users').where('age', '<>', 30).get()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Multiple conditions (AND by default):**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// WHERE active = 1 AND age > 18
|
|
243
|
+
await DB.table('users').where('active', 1).where('age', '>', 18).get()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**OR conditions:**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// WHERE name = 'Alice' OR name = 'Bob'
|
|
250
|
+
await DB.table('users').where('name', 'Alice').orWhere('name', 'Bob').get()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**IN / NOT IN:**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// WHERE role IN ('admin', 'editor')
|
|
257
|
+
await DB.table('users').whereIn('role', ['admin', 'editor']).get()
|
|
258
|
+
|
|
259
|
+
// WHERE id NOT IN (1, 2, 3)
|
|
260
|
+
await DB.table('users').whereNotIn('id', [1, 2, 3]).get()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Also available as `orWhereIn` / `orWhereNotIn`.
|
|
264
|
+
|
|
265
|
+
**BETWEEN / NOT BETWEEN:**
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// WHERE age BETWEEN 18 AND 65
|
|
269
|
+
await DB.table('users').whereBetween('age', [18, 65]).get()
|
|
270
|
+
|
|
271
|
+
// WHERE created_at NOT BETWEEN '2024-01-01' AND '2024-12-31'
|
|
272
|
+
await DB.table('orders').whereNotBetween('created_at', ['2024-01-01', '2024-12-31']).get()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Also available as `orWhereBetween` / `orWhereNotBetween`.
|
|
276
|
+
|
|
277
|
+
**LIKE / NOT LIKE:**
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
await DB.table('users').whereLike('email', '%@gmail.com').get()
|
|
281
|
+
await DB.table('users').whereNotLike('name', 'A%').get()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Also available as `orWhereLike` / `orWhereNotLike`.
|
|
285
|
+
|
|
286
|
+
**NULL checks:**
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// WHERE deleted_at IS NULL
|
|
290
|
+
await DB.table('users').whereNull('deleted_at').get()
|
|
291
|
+
|
|
292
|
+
// WHERE deleted_at IS NOT NULL
|
|
293
|
+
await DB.table('users').whereNotNull('deleted_at').get()
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Also available as `orWhereNull` / `orWhereNotNull`.
|
|
297
|
+
|
|
298
|
+
**Grouped conditions (parentheses):**
|
|
299
|
+
|
|
300
|
+
Pass a callback to `where()` to create a parenthesized group. Inside the callback the full `where*` / `orWhere*` API is available:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// WHERE (name = 'Alice' OR name = 'Bob') AND active = 1
|
|
304
|
+
await DB.table('users')
|
|
305
|
+
.where((group) => group.where('name', 'Alice').orWhere('name', 'Bob'))
|
|
306
|
+
.where('active', 1)
|
|
307
|
+
.get()
|
|
308
|
+
|
|
309
|
+
// WHERE (age IN (25, 30)) AND active = 1
|
|
310
|
+
await DB.table('users')
|
|
311
|
+
.where((q) => q.whereIn('age', [25, 30]))
|
|
312
|
+
.where('active', 1)
|
|
313
|
+
.get()
|
|
314
|
+
|
|
315
|
+
// WHERE (age BETWEEN 18 AND 40 OR nickname IS NULL)
|
|
316
|
+
await DB.table('users')
|
|
317
|
+
.where((q) => q.whereBetween('age', [18, 40]).orWhereNull('nickname'))
|
|
318
|
+
.get()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Nest groups as deep as needed:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// WHERE (role = 'admin' OR (role = 'editor' AND verified = 1))
|
|
325
|
+
await DB.table('users')
|
|
326
|
+
.where((group) =>
|
|
327
|
+
group
|
|
328
|
+
.where('role', 'admin')
|
|
329
|
+
.orWhere((inner) => inner.where('role', 'editor').where('verified', 1))
|
|
330
|
+
)
|
|
331
|
+
.get()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`orWhere()` also accepts a callback, producing an OR-connected group.
|
|
335
|
+
|
|
336
|
+
**Column comparisons — `whereColumn` / `orWhereColumn`:**
|
|
337
|
+
|
|
338
|
+
Compare two columns against each other instead of against a scalar value:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// WHERE updated_at > created_at
|
|
342
|
+
await DB.table('users').whereColumn('updated_at', '>', 'created_at').get()
|
|
343
|
+
|
|
344
|
+
// WHERE col_a = col_b (operator defaults to =)
|
|
345
|
+
await DB.table('items').whereColumn('col_a', 'col_b').get()
|
|
346
|
+
|
|
347
|
+
// OR variant
|
|
348
|
+
await DB.table('items').whereColumn('a', 'b').orWhereColumn('a', '>', 'b').get()
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`whereColumn` / `orWhereColumn` are also available inside grouped-condition callbacks:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
await DB.table('items')
|
|
355
|
+
.where((q) => q.whereColumn('a', 'b').orWhereColumn('a', '>', 'b'))
|
|
356
|
+
.get()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### Conditional clauses — `when`
|
|
362
|
+
|
|
363
|
+
`when(condition, callback)` applies query modifications only when `condition` is truthy. This keeps dynamic query building readable without `if` statements scattered through the chain:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
const onlyActive = true
|
|
367
|
+
const minAge = 18
|
|
368
|
+
|
|
369
|
+
const users = await DB.table('users')
|
|
370
|
+
.when(onlyActive, (q) => q.where('active', 1))
|
|
371
|
+
.when(minAge != null, (q) => q.where('age', '>=', minAge))
|
|
372
|
+
.get()
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
When `condition` is falsy the callback is skipped and the chain continues unchanged. `when` works on both `DataTable` and `SelectQuery`.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### Sorting — ORDER BY
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// ORDER BY name ASC
|
|
383
|
+
await DB.table('users').orderBy('name').get()
|
|
384
|
+
|
|
385
|
+
// ORDER BY age DESC
|
|
386
|
+
import { OrderByDirection } from '@neogroup/neorm'
|
|
387
|
+
await DB.table('users').orderBy('age', OrderByDirection.DESC).get()
|
|
388
|
+
|
|
389
|
+
// Multiple sort fields
|
|
390
|
+
await DB.table('users')
|
|
391
|
+
.orderBy('country', OrderByDirection.ASC)
|
|
392
|
+
.orderBy('age', OrderByDirection.DESC)
|
|
393
|
+
.get()
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### Pagination — LIMIT & OFFSET
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// LIMIT 10
|
|
402
|
+
await DB.table('users').limit(10).get()
|
|
403
|
+
|
|
404
|
+
// LIMIT 10 OFFSET 20 (page 3)
|
|
405
|
+
await DB.table('users').limit(10).offset(20).get()
|
|
406
|
+
|
|
407
|
+
// OFFSET only (SQLite emits LIMIT -1 automatically)
|
|
408
|
+
await DB.table('users').offset(5).get()
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
### Grouping — GROUP BY & HAVING
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// SELECT country, COUNT(*) AS total FROM users GROUP BY country
|
|
417
|
+
await DB.table('users')
|
|
418
|
+
.select('country', 'COUNT(*) AS total')
|
|
419
|
+
.groupBy('country')
|
|
420
|
+
.get()
|
|
421
|
+
|
|
422
|
+
// GROUP BY + HAVING
|
|
423
|
+
// HAVING COUNT(*) > 5
|
|
424
|
+
await DB.table('users')
|
|
425
|
+
.select('country', 'COUNT(*) AS total')
|
|
426
|
+
.groupBy('country')
|
|
427
|
+
.having('COUNT(*)', '>', 5)
|
|
428
|
+
.get()
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`having()` and `orHaving()` accept the same signatures as `where()`.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
### Joins
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// INNER JOIN orders ON users.id = orders.user_id
|
|
439
|
+
await DB.table('users')
|
|
440
|
+
.innerJoin('orders', 'users.id', 'orders.user_id')
|
|
441
|
+
.get()
|
|
442
|
+
|
|
443
|
+
// LEFT JOIN
|
|
444
|
+
await DB.table('users')
|
|
445
|
+
.leftJoin('orders', 'users.id', 'orders.user_id')
|
|
446
|
+
.get()
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Fields follow the `'table.field'` notation — the builder parses and quotes each component according to the engine (e.g. backticks in MySQL). Object form is also accepted for finer control:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
.innerJoin('orders', { name: 'id', table: 'users' }, { name: 'user_id', table: 'orders' })
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Available join methods: `join()`, `innerJoin()`, `leftJoin()`, `rightJoin()`, `outerJoin()`, `crossJoin()`.
|
|
456
|
+
|
|
457
|
+
Join with table alias:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
await DB.table('users')
|
|
461
|
+
.innerJoin('orders', 'users.id', 'orders.user_id', 'o')
|
|
462
|
+
.select('users.name', 'o.total')
|
|
463
|
+
.get()
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
### Distinct
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// SELECT DISTINCT country FROM users
|
|
472
|
+
await DB.table('users').distinct().select('country').get()
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
### Field aliases & functions
|
|
478
|
+
|
|
479
|
+
Raw string fields support `'table.field'` notation and `'FUNC(table.field)'` — the builder parses and quotes each component using the engine's rules:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// SELECT users.name, COUNT(orders.id) AS order_count FROM ...
|
|
483
|
+
await DB.table('users')
|
|
484
|
+
.select('users.name', 'COUNT(orders.id) AS order_count')
|
|
485
|
+
.innerJoin('orders', 'users.id', 'orders.user_id')
|
|
486
|
+
.groupBy('users.id')
|
|
487
|
+
.get()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Object form is also available for precise control:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// SELECT COUNT(*) AS total FROM users
|
|
494
|
+
await DB.table('users')
|
|
495
|
+
.select({ name: '*', function: 'COUNT', alias: 'total' })
|
|
496
|
+
.get()
|
|
497
|
+
|
|
498
|
+
// SELECT MAX(age) AS max_age FROM users
|
|
499
|
+
await DB.table('users')
|
|
500
|
+
.select({ name: 'age', function: 'MAX', alias: 'max_age' })
|
|
501
|
+
.get()
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### Table alias
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// FROM users AS u
|
|
510
|
+
await DB.table('users').alias('u').get()
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## INSERT, UPDATE, DELETE
|
|
516
|
+
|
|
517
|
+
**INSERT:**
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// Using set() — chainable
|
|
521
|
+
const rowsAffected = await DB.table('users')
|
|
522
|
+
.set('name', 'Alice')
|
|
523
|
+
.set('email', 'alice@example.com')
|
|
524
|
+
.set('age', 30)
|
|
525
|
+
.insert()
|
|
526
|
+
|
|
527
|
+
// Passing a fields object directly (Laravel-style)
|
|
528
|
+
const rowsAffected = await DB.table('users')
|
|
529
|
+
.insert({ name: 'Alice', email: 'alice@example.com', age: 30 })
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**UPDATE:**
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// Using set() — chainable
|
|
536
|
+
const rowsAffected = await DB.table('users')
|
|
537
|
+
.where('id', 7)
|
|
538
|
+
.set('active', 0)
|
|
539
|
+
.update()
|
|
540
|
+
|
|
541
|
+
// Passing a fields object directly
|
|
542
|
+
const rowsAffected = await DB.table('users')
|
|
543
|
+
.where('id', 7)
|
|
544
|
+
.update({ active: 0, name: 'Bob' })
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**DELETE:**
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// DELETE FROM users WHERE active = 0
|
|
551
|
+
const rowsAffected = await DB.table('users').where('active', 0).delete()
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
> ⚠️ Omitting `where()` on `update()` and `delete()` will affect **all rows** in the table.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Raw queries
|
|
559
|
+
|
|
560
|
+
When you need full control over the SQL string, use `DB.query()` and `DB.execute()` directly on the active source:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// Raw SELECT
|
|
564
|
+
const rows = await DB.query(
|
|
565
|
+
'SELECT * FROM users WHERE age > ? AND active = ?',
|
|
566
|
+
[18, 1]
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
// Raw execute (INSERT / UPDATE / DELETE / DDL)
|
|
570
|
+
const affected = await DB.execute(
|
|
571
|
+
'UPDATE users SET active = ? WHERE last_login < ?',
|
|
572
|
+
[0, '2023-01-01']
|
|
573
|
+
)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
To target a specific source, use `DB.source()`:
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
const rows = await DB.source('reporting').query('SELECT * FROM analytics')
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Advanced queries with SelectQuery
|
|
585
|
+
|
|
586
|
+
For queries that go beyond a single table — unions, subqueries, or complex joins — build a `SelectQuery` directly and pass it to `source.query()`.
|
|
587
|
+
|
|
588
|
+
### UNION / UNION ALL
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { DB } from '@neogroup/neorm'
|
|
592
|
+
|
|
593
|
+
// SELECT name FROM users WHERE active = 1
|
|
594
|
+
// UNION
|
|
595
|
+
// SELECT name FROM users WHERE age > 50
|
|
596
|
+
const query = DB.selectQuery('users')
|
|
597
|
+
.select('name')
|
|
598
|
+
.where('active', 1)
|
|
599
|
+
.union(
|
|
600
|
+
DB.selectQuery('users').select('name').where('age', '>', 50)
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
const rows = await DB.source('primary').query(query)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
`union()` removes duplicates (standard SQL `UNION`). Use `unionAll()` to keep them:
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
const query = DB.selectQuery('orders')
|
|
610
|
+
.select('user_id')
|
|
611
|
+
.where('status', 'paid')
|
|
612
|
+
.unionAll(
|
|
613
|
+
DB.selectQuery('orders').select('user_id').where('status', 'refunded')
|
|
614
|
+
)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Chain multiple unions:
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
const query = DB.selectQuery('table_a').select('id')
|
|
621
|
+
.union(DB.selectQuery('table_b').select('id'))
|
|
622
|
+
.union(DB.selectQuery('table_c').select('id'))
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Subqueries
|
|
626
|
+
|
|
627
|
+
A `SelectQuery` can be used as a value in a `where()` condition:
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
const activeUserIds = DB.selectQuery('users').select('id').where('active', 1)
|
|
631
|
+
|
|
632
|
+
const orders = await DB.source('primary').query(
|
|
633
|
+
DB.selectQuery('orders').where('user_id', 'IN', activeUserIds)
|
|
634
|
+
)
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Connections & transactions
|
|
640
|
+
|
|
641
|
+
### Explicit connection
|
|
642
|
+
|
|
643
|
+
Obtain a connection explicitly when you need full control over its lifecycle:
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
const conn = await DB.connection()
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await conn.beginTransaction()
|
|
650
|
+
|
|
651
|
+
await conn.execute('INSERT INTO accounts (user_id, balance) VALUES (?, ?)', [1, 1000])
|
|
652
|
+
await conn.execute('UPDATE accounts SET balance = balance - ? WHERE user_id = ?', [200, 2])
|
|
653
|
+
|
|
654
|
+
await conn.commitTransaction()
|
|
655
|
+
} catch (err) {
|
|
656
|
+
await conn.rollbackTransaction()
|
|
657
|
+
} finally {
|
|
658
|
+
await conn.close()
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
`executeTransaction()` on a connection handles begin/commit/rollback automatically:
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
const conn = await DB.connection()
|
|
666
|
+
|
|
667
|
+
await conn.executeTransaction(async (tx) => {
|
|
668
|
+
await tx.execute('INSERT INTO logs (event) VALUES (?)', ['login'])
|
|
669
|
+
await tx.execute('UPDATE users SET last_login = NOW() WHERE id = ?', [userId])
|
|
670
|
+
// throws → automatic rollback; resolves → automatic commit
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
await conn.close()
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### DB shorthand methods
|
|
677
|
+
|
|
678
|
+
`DB` exposes convenience wrappers that delegate directly to the active source's connection, so you don't need to manage a connection object for simple cases:
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
// One-shot query / execute
|
|
682
|
+
const rows = await DB.query('SELECT * FROM users WHERE active = ?', [1])
|
|
683
|
+
const affected = await DB.execute('DELETE FROM logs WHERE created_at < ?', ['2024-01-01'])
|
|
684
|
+
|
|
685
|
+
// Auto-managed transaction (recommended)
|
|
686
|
+
await DB.executeTransaction(async (tx) => {
|
|
687
|
+
await tx.execute('INSERT INTO logs (event) VALUES (?)', ['login'])
|
|
688
|
+
await tx.execute('UPDATE users SET last_login = NOW() WHERE id = ?', [userId])
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// Manual transaction control
|
|
692
|
+
await DB.beginTransaction()
|
|
693
|
+
try {
|
|
694
|
+
await DB.execute('INSERT INTO ...')
|
|
695
|
+
await DB.commitTransaction()
|
|
696
|
+
} catch (err) {
|
|
697
|
+
await DB.rollbackTransaction()
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## Entities (Active Record)
|
|
704
|
+
|
|
705
|
+
Entities are Eloquent-style Active Record models that map a plain class to a database table using TypeScript decorators. They sit on top of `DataTable` and add hydration, casts, relationships, and standalone persistence functions — no base class required.
|
|
706
|
+
|
|
707
|
+
> Enable `"experimentalDecorators": true` and `"useDefineForClassFields": false` in your `tsconfig.json`.
|
|
708
|
+
|
|
709
|
+
### Defining an Entity
|
|
710
|
+
|
|
711
|
+
Annotate any class with `@Entity` and mark its columns with `@Column`. All static query methods are injected at runtime; use `saveEntity()` and `deleteEntity()` for persistence.
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import {
|
|
715
|
+
Entity, Column, Entities,
|
|
716
|
+
HasOne, HasMany, BelongsTo, HasOneThrough, HasManyThrough
|
|
717
|
+
} from '@neogroup/neorm'
|
|
718
|
+
|
|
719
|
+
@Entity() // table name defaults to lowercase class name + 's' → 'users'
|
|
720
|
+
class User {
|
|
721
|
+
@Column({ primaryKey: true, autoGenerated: true })
|
|
722
|
+
id!: number
|
|
723
|
+
|
|
724
|
+
@Column()
|
|
725
|
+
name!: string
|
|
726
|
+
|
|
727
|
+
@Column()
|
|
728
|
+
email!: string
|
|
729
|
+
|
|
730
|
+
@Column({ cast: 'number' })
|
|
731
|
+
age!: number
|
|
732
|
+
|
|
733
|
+
@Column({ cast: 'boolean' })
|
|
734
|
+
active!: boolean
|
|
735
|
+
|
|
736
|
+
@HasMany(() => Order, 'userId')
|
|
737
|
+
orders?: Order[]
|
|
738
|
+
|
|
739
|
+
@HasOne(() => Profile, 'userId')
|
|
740
|
+
profile?: Profile | null
|
|
741
|
+
|
|
742
|
+
// Computed attributes work as plain JS getters — no extra decorator needed
|
|
743
|
+
get displayName(): string {
|
|
744
|
+
return `${this.name} (${this.email})`
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@Entity()
|
|
749
|
+
class Order {
|
|
750
|
+
@Column({ primaryKey: true, autoGenerated: true })
|
|
751
|
+
id!: number
|
|
752
|
+
|
|
753
|
+
@Column()
|
|
754
|
+
userId!: number
|
|
755
|
+
|
|
756
|
+
@Column({ cast: 'number' })
|
|
757
|
+
total!: number
|
|
758
|
+
|
|
759
|
+
@BelongsTo(() => User, 'userId')
|
|
760
|
+
user?: User
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**`@Column` options:**
|
|
765
|
+
|
|
766
|
+
| Option | Type | Description |
|
|
767
|
+
|-----------------|------------|-------------|
|
|
768
|
+
| `cast` | `CastType` | Coerce the value when reading from the DB (see [Casts](#casts)). |
|
|
769
|
+
| `primaryKey` | `boolean` | Marks this column as the primary key. |
|
|
770
|
+
| `autoGenerated` | `boolean` | Column is DB-managed (e.g. `AUTOINCREMENT`). Excluded from INSERT/UPDATE; the generated value is written back to the instance after INSERT. |
|
|
771
|
+
|
|
772
|
+
When a custom table name or primary key column name is needed, pass options to `@Entity`:
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
@Entity({ table: 'shipping_addresses' })
|
|
776
|
+
class ShippingAddress {
|
|
777
|
+
@Column({ primaryKey: true, autoGenerated: true })
|
|
778
|
+
id!: number
|
|
779
|
+
// ...
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
To target a specific data source instead of the active one:
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
@Entity({ source: DB.source('archive') })
|
|
787
|
+
class ArchiveUser {
|
|
788
|
+
// ...
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
### Querying
|
|
795
|
+
|
|
796
|
+
All `DataTable` query methods are available as static methods on every Entity, returning a chainable `EntityQuery<T>`:
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
const UserModel = User as any // cast needed — TypeScript doesn't see injected statics
|
|
800
|
+
|
|
801
|
+
// Fetch all
|
|
802
|
+
const users = await UserModel.get()
|
|
803
|
+
|
|
804
|
+
// Find by primary key
|
|
805
|
+
const user = await UserModel.find(1) // User | null
|
|
806
|
+
|
|
807
|
+
// First match
|
|
808
|
+
const admin = await UserModel.where('role', 'admin').first()
|
|
809
|
+
|
|
810
|
+
// Chained conditions
|
|
811
|
+
const adults = await UserModel.where('active', 1)
|
|
812
|
+
.whereNotNull('email')
|
|
813
|
+
.orderBy('name')
|
|
814
|
+
.limit(20)
|
|
815
|
+
.get()
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
The full `where*` / `orWhere*` API, `select()`, `orderBy()`, `limit()`, `offset()`, `groupBy()`, `distinct()`, and `whereColumn()` are all supported.
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
### Saving & deleting
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
// INSERT — autoGenerated PK is written back after insert
|
|
826
|
+
const user = new User() as any
|
|
827
|
+
user.name = 'Alice'
|
|
828
|
+
user.email = 'alice@example.com'
|
|
829
|
+
user.age = 30
|
|
830
|
+
user.active = 1
|
|
831
|
+
await Entities.save(user)
|
|
832
|
+
console.log(user.id) // populated from lastInsertId
|
|
833
|
+
|
|
834
|
+
// UPDATE — detected by the presence of a primary key value
|
|
835
|
+
user.age = 31
|
|
836
|
+
await Entities.save(user)
|
|
837
|
+
|
|
838
|
+
// DELETE
|
|
839
|
+
await Entities.delete(user)
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
`Entities.save` and `Entities.delete` read the entity metadata from its constructor at runtime — no coupling between the call site and the `@Entity` definition.
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
### Casts
|
|
847
|
+
|
|
848
|
+
Declare `cast` on `@Column` to automatically convert raw DB values when hydrating a row:
|
|
849
|
+
|
|
850
|
+
| Cast type | Description |
|
|
851
|
+
|-------------|--------------------------------------------------------|
|
|
852
|
+
| `'number'` | `Number(value)` |
|
|
853
|
+
| `'boolean'` | `true` for `1`, `'1'`, `'true'`; `false` otherwise |
|
|
854
|
+
| `'string'` | `String(value)` |
|
|
855
|
+
| `'json'` | `JSON.parse(value)` on read, `JSON.stringify` on write |
|
|
856
|
+
| `'date'` | `new Date(value)` |
|
|
857
|
+
|
|
858
|
+
Booleans stored as integers (SQLite, MySQL) are handled transparently.
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
### Relationships
|
|
863
|
+
|
|
864
|
+
Use relationship decorators to declare associations between entities:
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
// HasOne(related, foreignKey, localKey = 'id')
|
|
868
|
+
// HasMany(related, foreignKey, localKey = 'id')
|
|
869
|
+
// BelongsTo(related, foreignKey, localKey = 'id')
|
|
870
|
+
// HasOneThrough(related, through, foreignKey, throughForeignKey, localKey, throughLocalKey)
|
|
871
|
+
// HasManyThrough(related, through, foreignKey, throughForeignKey, localKey, throughLocalKey)
|
|
872
|
+
|
|
873
|
+
@Entity({ table: 'countries' })
|
|
874
|
+
class Country {
|
|
875
|
+
@Column({ primaryKey: true, autoGenerated: true })
|
|
876
|
+
id!: number
|
|
877
|
+
|
|
878
|
+
@HasMany(() => User, 'countryId')
|
|
879
|
+
users?: User[]
|
|
880
|
+
|
|
881
|
+
// Reach Users' Orders through the Users table
|
|
882
|
+
@HasManyThrough(() => Order, () => User, 'userId', 'countryId')
|
|
883
|
+
orders?: Order[]
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
The `related` and `through` arguments are lazy callbacks (`() => ClassName`) to avoid circular-dependency issues when models reference each other.
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
### Eager loading — `with()`
|
|
892
|
+
|
|
893
|
+
Pass relation names to `with()` to load related entities in a single extra query per relation (avoids N+1):
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
// Preload orders for each user
|
|
897
|
+
const users = await UserModel.with('orders').get()
|
|
898
|
+
users.forEach(u => console.log(u.orders)) // Order[] attached
|
|
899
|
+
|
|
900
|
+
// Multiple relations
|
|
901
|
+
const users = await UserModel.with('orders', 'profile').get()
|
|
902
|
+
|
|
903
|
+
// Dot-notation for nested eager loading
|
|
904
|
+
// Loads orders → users → countries in 3 total queries
|
|
905
|
+
const orders = await OrderModel.with('user.country').get()
|
|
906
|
+
orders.forEach(o => console.log(o.user.country))
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
`with()` is chainable after any `where*` or ordering method:
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
const users = await UserModel.where('active', 1).with('orders').orderBy('name').get()
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
### Joining via relationships
|
|
918
|
+
|
|
919
|
+
Use `joinRelationship` / `innerJoinRelationship` / `leftJoinRelationship` to add SQL JOINs derived from relationship definitions:
|
|
920
|
+
|
|
921
|
+
```typescript
|
|
922
|
+
// INNER JOIN orders ON users.id = orders.userId
|
|
923
|
+
const users = await UserModel.innerJoinRelationship('orders')
|
|
924
|
+
.select('users.*', 'COUNT(orders.id) AS orderCount')
|
|
925
|
+
.groupBy('users.id')
|
|
926
|
+
.get()
|
|
927
|
+
|
|
928
|
+
// LEFT JOIN
|
|
929
|
+
const users = await UserModel.leftJoinRelationship('profile').get()
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
These are available as static methods on every Entity and also on `EntityQuery`:
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
const query = UserModel.where('active', 1).leftJoinRelationship('profile')
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
## Debug mode
|
|
941
|
+
|
|
942
|
+
Enable SQL logging on a data source to print every statement and its bindings to the console:
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
source.setDebugEnabled(true)
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Example output:
|
|
949
|
+
|
|
950
|
+
```
|
|
951
|
+
SQL: SELECT * FROM users WHERE active = ? AND age > ?; ["1", 18]
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Read-only mode prevents any write operations from reaching the database — useful for testing:
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
source.setReadonly(true)
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Extending the library
|
|
963
|
+
|
|
964
|
+
### Custom data source
|
|
965
|
+
|
|
966
|
+
Implement `DataSource` to connect any database engine:
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
import { Connection, DataSource } from '@neogroup/neorm'
|
|
970
|
+
|
|
971
|
+
class MyConnection implements Connection {
|
|
972
|
+
async query(sql: string, bindings?: any[]): Promise<any[]> { /* ... */ }
|
|
973
|
+
async execute(sql: string, bindings?: any[]): Promise<number> { /* ... */ }
|
|
974
|
+
async lastInsertId(): Promise<number> { /* ... */ }
|
|
975
|
+
async beginTransaction(): Promise<void> { /* ... */ }
|
|
976
|
+
async commitTransaction(): Promise<void> { /* ... */ }
|
|
977
|
+
async rollbackTransaction(): Promise<void> { /* ... */ }
|
|
978
|
+
async close(): Promise<void> { /* ... */ }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
class MyDataSource extends DataSource {
|
|
982
|
+
protected async requestConnection(): Promise<Connection> {
|
|
983
|
+
return new MyConnection(/* ... */)
|
|
984
|
+
}
|
|
985
|
+
async close(): Promise<void> { /* ... */ }
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Custom query builder
|
|
990
|
+
|
|
991
|
+
Override `DefaultQueryBuilder` to adapt SQL generation for your engine:
|
|
992
|
+
|
|
993
|
+
```typescript
|
|
994
|
+
import { DefaultQueryBuilder, Statement, Table } from '@neogroup/neorm'
|
|
995
|
+
|
|
996
|
+
class MyQueryBuilder extends DefaultQueryBuilder {
|
|
997
|
+
protected buildTable(table: Table, statement: Statement) {
|
|
998
|
+
// e.g. wrap table names in double-brackets for SQL Server
|
|
999
|
+
statement.sql += typeof table === 'string' ? `[${table}]` : `[${table.name}]`
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
Pass your custom builder to the data source constructor:
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
class MyDataSource extends DataSource {
|
|
1008
|
+
constructor() {
|
|
1009
|
+
super(new MyQueryBuilder())
|
|
1010
|
+
}
|
|
1011
|
+
// ...
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Contact
|
|
1018
|
+
|
|
1019
|
+
For bugs or feature requests open an issue on [GitHub](https://github.com/luismanuelamengual/NeORM/issues) or contact the author at luismanuelamengual@gmail.com.
|