@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.
Files changed (161) hide show
  1. package/README.md +1019 -0
  2. package/dist/connection.d.ts +10 -0
  3. package/dist/connection.js +3 -0
  4. package/dist/connection.js.map +1 -0
  5. package/dist/data-connection.d.ts +24 -0
  6. package/dist/data-connection.js +99 -0
  7. package/dist/data-connection.js.map +1 -0
  8. package/dist/data-set.d.ts +3 -0
  9. package/dist/data-set.js +3 -0
  10. package/dist/data-set.js.map +1 -0
  11. package/dist/data-source.d.ts +23 -0
  12. package/dist/data-source.js +56 -0
  13. package/dist/data-source.js.map +1 -0
  14. package/dist/data-table.d.ts +19 -0
  15. package/dist/data-table.js +75 -0
  16. package/dist/data-table.js.map +1 -0
  17. package/dist/db.d.ts +57 -0
  18. package/dist/db.js +213 -0
  19. package/dist/db.js.map +1 -0
  20. package/dist/entities/decorators.d.ts +50 -0
  21. package/dist/entities/decorators.js +305 -0
  22. package/dist/entities/decorators.js.map +1 -0
  23. package/dist/entities/entity-query.d.ts +60 -0
  24. package/dist/entities/entity-query.js +305 -0
  25. package/dist/entities/entity-query.js.map +1 -0
  26. package/dist/entities/entity.d.ts +1 -0
  27. package/dist/entities/entity.js +3 -0
  28. package/dist/entities/entity.js.map +1 -0
  29. package/dist/entities/index.d.ts +4 -0
  30. package/dist/entities/index.js +20 -0
  31. package/dist/entities/index.js.map +1 -0
  32. package/dist/entities/relationship.d.ts +15 -0
  33. package/dist/entities/relationship.js +24 -0
  34. package/dist/entities/relationship.js.map +1 -0
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.js +32 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/query/builders/default-query-builder.d.ts +87 -0
  39. package/dist/query/builders/default-query-builder.js +524 -0
  40. package/dist/query/builders/default-query-builder.js.map +1 -0
  41. package/dist/query/builders/index.d.ts +2 -0
  42. package/dist/query/builders/index.js +19 -0
  43. package/dist/query/builders/index.js.map +1 -0
  44. package/dist/query/builders/query-builder.d.ts +5 -0
  45. package/dist/query/builders/query-builder.js +7 -0
  46. package/dist/query/builders/query-builder.js.map +1 -0
  47. package/dist/query/conditions.d.ts +60 -0
  48. package/dist/query/conditions.js +142 -0
  49. package/dist/query/conditions.js.map +1 -0
  50. package/dist/query/delete-query.d.ts +8 -0
  51. package/dist/query/delete-query.js +17 -0
  52. package/dist/query/delete-query.js.map +1 -0
  53. package/dist/query/features/has-alias.d.ts +6 -0
  54. package/dist/query/features/has-alias.js +17 -0
  55. package/dist/query/features/has-alias.js.map +1 -0
  56. package/dist/query/features/has-distinct.d.ts +6 -0
  57. package/dist/query/features/has-distinct.js +20 -0
  58. package/dist/query/features/has-distinct.js.map +1 -0
  59. package/dist/query/features/has-field-values.d.ts +9 -0
  60. package/dist/query/features/has-field-values.js +27 -0
  61. package/dist/query/features/has-field-values.js.map +1 -0
  62. package/dist/query/features/has-group-by-fields.d.ts +7 -0
  63. package/dist/query/features/has-group-by-fields.js +21 -0
  64. package/dist/query/features/has-group-by-fields.js.map +1 -0
  65. package/dist/query/features/has-having-conditions.d.ts +13 -0
  66. package/dist/query/features/has-having-conditions.js +28 -0
  67. package/dist/query/features/has-having-conditions.js.map +1 -0
  68. package/dist/query/features/has-joins.d.ts +41 -0
  69. package/dist/query/features/has-joins.js +67 -0
  70. package/dist/query/features/has-joins.js.map +1 -0
  71. package/dist/query/features/has-limit.d.ts +6 -0
  72. package/dist/query/features/has-limit.js +20 -0
  73. package/dist/query/features/has-limit.js.map +1 -0
  74. package/dist/query/features/has-offset.d.ts +6 -0
  75. package/dist/query/features/has-offset.js +20 -0
  76. package/dist/query/features/has-offset.js.map +1 -0
  77. package/dist/query/features/has-order-by-fields.d.ts +15 -0
  78. package/dist/query/features/has-order-by-fields.js +38 -0
  79. package/dist/query/features/has-order-by-fields.js.map +1 -0
  80. package/dist/query/features/has-select-fields.d.ts +11 -0
  81. package/dist/query/features/has-select-fields.js +21 -0
  82. package/dist/query/features/has-select-fields.js.map +1 -0
  83. package/dist/query/features/has-table.d.ts +7 -0
  84. package/dist/query/features/has-table.js +20 -0
  85. package/dist/query/features/has-table.js.map +1 -0
  86. package/dist/query/features/has-unions.d.ts +13 -0
  87. package/dist/query/features/has-unions.js +21 -0
  88. package/dist/query/features/has-unions.js.map +1 -0
  89. package/dist/query/features/has-when.d.ts +3 -0
  90. package/dist/query/features/has-when.js +13 -0
  91. package/dist/query/features/has-when.js.map +1 -0
  92. package/dist/query/features/has-where-conditions.d.ts +35 -0
  93. package/dist/query/features/has-where-conditions.js +92 -0
  94. package/dist/query/features/has-where-conditions.js.map +1 -0
  95. package/dist/query/features/index.d.ts +14 -0
  96. package/dist/query/features/index.js +31 -0
  97. package/dist/query/features/index.js.map +1 -0
  98. package/dist/query/fields.d.ts +12 -0
  99. package/dist/query/fields.js +18 -0
  100. package/dist/query/fields.js.map +1 -0
  101. package/dist/query/index.d.ts +11 -0
  102. package/dist/query/index.js +28 -0
  103. package/dist/query/index.js.map +1 -0
  104. package/dist/query/insert-query.d.ts +8 -0
  105. package/dist/query/insert-query.js +17 -0
  106. package/dist/query/insert-query.js.map +1 -0
  107. package/dist/query/query.d.ts +2 -0
  108. package/dist/query/query.js +7 -0
  109. package/dist/query/query.js.map +1 -0
  110. package/dist/query/select-query.d.ts +9 -0
  111. package/dist/query/select-query.js +33 -0
  112. package/dist/query/select-query.js.map +1 -0
  113. package/dist/query/statement.d.ts +4 -0
  114. package/dist/query/statement.js +3 -0
  115. package/dist/query/statement.js.map +1 -0
  116. package/dist/query/table.d.ts +6 -0
  117. package/dist/query/table.js +3 -0
  118. package/dist/query/table.js.map +1 -0
  119. package/dist/query/update-query.d.ts +8 -0
  120. package/dist/query/update-query.js +17 -0
  121. package/dist/query/update-query.js.map +1 -0
  122. package/dist/sources/mysql/index.d.ts +3 -0
  123. package/dist/sources/mysql/index.js +20 -0
  124. package/dist/sources/mysql/index.js.map +1 -0
  125. package/dist/sources/mysql/mysql-connection.d.ts +14 -0
  126. package/dist/sources/mysql/mysql-connection.js +36 -0
  127. package/dist/sources/mysql/mysql-connection.js.map +1 -0
  128. package/dist/sources/mysql/mysql-data-source.d.ts +24 -0
  129. package/dist/sources/mysql/mysql-data-source.js +72 -0
  130. package/dist/sources/mysql/mysql-data-source.js.map +1 -0
  131. package/dist/sources/mysql/mysql-query-builder.d.ts +8 -0
  132. package/dist/sources/mysql/mysql-query-builder.js +28 -0
  133. package/dist/sources/mysql/mysql-query-builder.js.map +1 -0
  134. package/dist/sources/postgres/index.d.ts +2 -0
  135. package/dist/sources/postgres/index.js +19 -0
  136. package/dist/sources/postgres/index.js.map +1 -0
  137. package/dist/sources/postgres/postgres-connection.d.ts +13 -0
  138. package/dist/sources/postgres/postgres-connection.js +40 -0
  139. package/dist/sources/postgres/postgres-connection.js.map +1 -0
  140. package/dist/sources/postgres/postgres-data-source.d.ts +24 -0
  141. package/dist/sources/postgres/postgres-data-source.js +73 -0
  142. package/dist/sources/postgres/postgres-data-source.js.map +1 -0
  143. package/dist/sources/postgres/postgres-query-builder.d.ts +5 -0
  144. package/dist/sources/postgres/postgres-query-builder.js +13 -0
  145. package/dist/sources/postgres/postgres-query-builder.js.map +1 -0
  146. package/dist/sources/sqlite/index.d.ts +3 -0
  147. package/dist/sources/sqlite/index.js +20 -0
  148. package/dist/sources/sqlite/index.js.map +1 -0
  149. package/dist/sources/sqlite/sqlite-connection.d.ts +14 -0
  150. package/dist/sources/sqlite/sqlite-connection.js +37 -0
  151. package/dist/sources/sqlite/sqlite-connection.js.map +1 -0
  152. package/dist/sources/sqlite/sqlite-data-source.d.ts +11 -0
  153. package/dist/sources/sqlite/sqlite-data-source.js +34 -0
  154. package/dist/sources/sqlite/sqlite-data-source.js.map +1 -0
  155. package/dist/sources/sqlite/sqlite-query-builder.d.ts +6 -0
  156. package/dist/sources/sqlite/sqlite-query-builder.js +23 -0
  157. package/dist/sources/sqlite/sqlite-query-builder.js.map +1 -0
  158. package/dist/utilities.d.ts +2 -0
  159. package/dist/utilities.js +17 -0
  160. package/dist/utilities.js.map +1 -0
  161. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,1019 @@
1
+ [![npm version](https://badge.fury.io/js/@neogroup%2Fneorm.svg)](https://badge.fury.io/js/@neogroup%2Fneorm)
2
+ ![](https://img.shields.io/github/forks/luismanuelamengual/NeORM.svg?style=social&label=Fork)
3
+ ![](https://img.shields.io/github/stars/luismanuelamengual/NeORM.svg?style=social&label=Star)
4
+ ![](https://img.shields.io/github/watchers/luismanuelamengual/NeORM.svg?style=social&label=Watch)
5
+ ![](https://img.shields.io/github/followers/luismanuelamengual.svg?style=social&label=Follow)
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.