@mostajs/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 +548 -0
- package/dist/core/base-repository.d.ts +26 -0
- package/dist/core/base-repository.js +82 -0
- package/dist/core/config.d.ts +62 -0
- package/dist/core/config.js +116 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +49 -0
- package/dist/core/factory.d.ts +41 -0
- package/dist/core/factory.js +142 -0
- package/dist/core/normalizer.d.ts +9 -0
- package/dist/core/normalizer.js +19 -0
- package/dist/core/registry.d.ts +43 -0
- package/dist/core/registry.js +78 -0
- package/dist/core/types.d.ts +228 -0
- package/dist/core/types.js +5 -0
- package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
- package/dist/dialects/abstract-sql.dialect.js +1071 -0
- package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
- package/dist/dialects/cockroachdb.dialect.js +23 -0
- package/dist/dialects/db2.dialect.d.ts +2 -0
- package/dist/dialects/db2.dialect.js +190 -0
- package/dist/dialects/hana.dialect.d.ts +2 -0
- package/dist/dialects/hana.dialect.js +199 -0
- package/dist/dialects/hsqldb.dialect.d.ts +2 -0
- package/dist/dialects/hsqldb.dialect.js +114 -0
- package/dist/dialects/mariadb.dialect.d.ts +2 -0
- package/dist/dialects/mariadb.dialect.js +87 -0
- package/dist/dialects/mongo.dialect.d.ts +2 -0
- package/dist/dialects/mongo.dialect.js +480 -0
- package/dist/dialects/mssql.dialect.d.ts +27 -0
- package/dist/dialects/mssql.dialect.js +127 -0
- package/dist/dialects/mysql.dialect.d.ts +24 -0
- package/dist/dialects/mysql.dialect.js +101 -0
- package/dist/dialects/oracle.dialect.d.ts +2 -0
- package/dist/dialects/oracle.dialect.js +206 -0
- package/dist/dialects/postgres.dialect.d.ts +26 -0
- package/dist/dialects/postgres.dialect.js +105 -0
- package/dist/dialects/spanner.dialect.d.ts +2 -0
- package/dist/dialects/spanner.dialect.js +259 -0
- package/dist/dialects/sqlite.dialect.d.ts +2 -0
- package/dist/dialects/sqlite.dialect.js +1027 -0
- package/dist/dialects/sybase.dialect.d.ts +2 -0
- package/dist/dialects/sybase.dialect.js +119 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/docs/api-reference.md +1009 -0
- package/docs/dialects.md +673 -0
- package/docs/tutorial.md +846 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# MostaORM
|
|
2
|
+
|
|
3
|
+
> **Multi-dialect ORM for Node.js/TypeScript** — inspired by Hibernate.
|
|
4
|
+
> One API. 13 databases. Zero lock-in.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@mostajs/orm)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://www.typescriptlang.org)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What is MostaORM?
|
|
14
|
+
|
|
15
|
+
MostaORM brings the **Hibernate philosophy** to Node.js: define your entities once as schemas, then switch databases without touching your application code. It provides a clean, type-safe **Repository pattern** on top of 13 database backends.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Your app code → BaseRepository<T> → IDialect → MongoDB / SQLite / PostgreSQL / ...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No code change required when switching from SQLite (development) to PostgreSQL (production) to MongoDB (cloud).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **13 database dialects** — MongoDB, SQLite, PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, CockroachDB, IBM DB2, SAP HANA, HyperSQL, Google Spanner, Sybase ASE
|
|
28
|
+
- **Single unified API** — `findAll()`, `findById()`, `create()`, `update()`, `delete()`, `aggregate()`, and more
|
|
29
|
+
- **Repository pattern** — extend `BaseRepository<T>` to add custom methods
|
|
30
|
+
- **Hibernate-style schema definition** — declare fields, relations, indexes in one `EntitySchema`
|
|
31
|
+
- **Relations support** — one-to-one, many-to-one, one-to-many, many-to-many with `populate()`
|
|
32
|
+
- **Aggregation pipeline** — `$match`, `$group`, `$sort`, `$limit` translated per dialect
|
|
33
|
+
- **Schema strategies** — `validate`, `update`, `create`, `create-drop`
|
|
34
|
+
- **Lazy dialect loading** — only the driver for your active database is loaded
|
|
35
|
+
- **Full TypeScript** — generics, strict types, complete `.d.ts` declarations
|
|
36
|
+
- **Zero boilerplate** — one `createConnection()` call to configure everything
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Supported Databases
|
|
41
|
+
|
|
42
|
+
| Dialect | Package | Status |
|
|
43
|
+
|---------|---------|--------|
|
|
44
|
+
| **MongoDB** | `mongoose` | ✅ Production |
|
|
45
|
+
| **SQLite** | `better-sqlite3` | ✅ Production |
|
|
46
|
+
| **PostgreSQL** | `pg` | ✅ Production |
|
|
47
|
+
| **MySQL** | `mysql2` | ✅ Production |
|
|
48
|
+
| **MariaDB** | `mariadb` | ✅ Production |
|
|
49
|
+
| **Oracle Database** | `oracledb` | ✅ Production |
|
|
50
|
+
| **SQL Server** | `mssql` | ✅ Production |
|
|
51
|
+
| **CockroachDB** | `pg` | ✅ Production |
|
|
52
|
+
| **IBM DB2** | `ibm_db` | ✅ Production |
|
|
53
|
+
| **SAP HANA** | `@sap/hana-client` | ✅ Production |
|
|
54
|
+
| **HyperSQL (HSQLDB)** | HTTP bridge | ✅ Production |
|
|
55
|
+
| **Google Cloud Spanner** | `@google-cloud/spanner` | ✅ Production |
|
|
56
|
+
| **Sybase ASE** | `mssql` | ✅ Production |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Install the core package:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install @mostajs/orm
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Install **only the driver(s)** you need:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# SQLite
|
|
73
|
+
npm install better-sqlite3
|
|
74
|
+
|
|
75
|
+
# PostgreSQL
|
|
76
|
+
npm install pg
|
|
77
|
+
|
|
78
|
+
# MongoDB
|
|
79
|
+
npm install mongoose
|
|
80
|
+
|
|
81
|
+
# MySQL / MariaDB
|
|
82
|
+
npm install mysql2
|
|
83
|
+
npm install mariadb
|
|
84
|
+
|
|
85
|
+
# Others
|
|
86
|
+
npm install oracledb # Oracle
|
|
87
|
+
npm install mssql # SQL Server, Sybase
|
|
88
|
+
npm install ibm_db # IBM DB2
|
|
89
|
+
npm install @sap/hana-client # SAP HANA
|
|
90
|
+
npm install @google-cloud/spanner # Google Spanner
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick Start — 5 minutes
|
|
96
|
+
|
|
97
|
+
### 1. Define your Entity Schema
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// src/schemas/user.schema.ts
|
|
101
|
+
import type { EntitySchema } from '@mostajs/orm'
|
|
102
|
+
|
|
103
|
+
export const UserSchema: EntitySchema = {
|
|
104
|
+
name: 'User',
|
|
105
|
+
collection: 'users', // table name in SQL, collection name in MongoDB
|
|
106
|
+
timestamps: true, // auto createdAt / updatedAt
|
|
107
|
+
fields: {
|
|
108
|
+
email: { type: 'string', required: true, unique: true, lowercase: true },
|
|
109
|
+
username: { type: 'string', required: true, unique: true },
|
|
110
|
+
password: { type: 'string', required: true },
|
|
111
|
+
role: { type: 'string', enum: ['user', 'admin'], default: 'user' },
|
|
112
|
+
status: { type: 'string', enum: ['active', 'banned'], default: 'active' },
|
|
113
|
+
lastLogin: { type: 'date' },
|
|
114
|
+
score: { type: 'number', default: 0 },
|
|
115
|
+
},
|
|
116
|
+
relations: {},
|
|
117
|
+
indexes: [
|
|
118
|
+
{ fields: { email: 'asc' }, unique: true },
|
|
119
|
+
{ fields: { role: 'asc' } },
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2. Create a Repository
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// src/repositories/user.repository.ts
|
|
128
|
+
import { BaseRepository } from '@mostajs/orm'
|
|
129
|
+
import type { IDialect } from '@mostajs/orm'
|
|
130
|
+
import { UserSchema } from '../schemas/user.schema.js'
|
|
131
|
+
|
|
132
|
+
export interface UserDTO {
|
|
133
|
+
id: string
|
|
134
|
+
email: string
|
|
135
|
+
username: string
|
|
136
|
+
role: string
|
|
137
|
+
status: string
|
|
138
|
+
createdAt: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class UserRepository extends BaseRepository<UserDTO> {
|
|
142
|
+
constructor(dialect: IDialect) {
|
|
143
|
+
super(UserSchema, dialect)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Custom method — uses built-in findOne()
|
|
147
|
+
async findByEmail(email: string): Promise<UserDTO | null> {
|
|
148
|
+
return this.findOne({ email: email.toLowerCase() })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async findAdmins(): Promise<UserDTO[]> {
|
|
152
|
+
return this.findAll({ role: 'admin' }, { sort: { createdAt: -1 } })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async countActive(): Promise<number> {
|
|
156
|
+
return this.count({ status: 'active' })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. Connect and Use
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// src/index.ts
|
|
165
|
+
import { createConnection, registerSchema } from '@mostajs/orm'
|
|
166
|
+
import { UserSchema } from './schemas/user.schema.js'
|
|
167
|
+
import { UserRepository } from './repositories/user.repository.js'
|
|
168
|
+
|
|
169
|
+
// Register all schemas
|
|
170
|
+
registerSchema(UserSchema)
|
|
171
|
+
|
|
172
|
+
// Connect — reads DB_DIALECT and SGBD_URI from environment
|
|
173
|
+
const dialect = await createConnection()
|
|
174
|
+
|
|
175
|
+
// Create a repository instance
|
|
176
|
+
const userRepo = new UserRepository(dialect)
|
|
177
|
+
|
|
178
|
+
// --- CRUD Operations ---
|
|
179
|
+
|
|
180
|
+
// Create
|
|
181
|
+
const user = await userRepo.create({
|
|
182
|
+
email: 'alice@example.com',
|
|
183
|
+
username: 'alice',
|
|
184
|
+
password: 'hashed_password',
|
|
185
|
+
})
|
|
186
|
+
console.log(user.id) // auto-generated ID
|
|
187
|
+
|
|
188
|
+
// Find
|
|
189
|
+
const found = await userRepo.findByEmail('alice@example.com')
|
|
190
|
+
const allAdmins = await userRepo.findAdmins()
|
|
191
|
+
const activeCount = await userRepo.countActive()
|
|
192
|
+
|
|
193
|
+
// Update
|
|
194
|
+
const updated = await userRepo.update(user.id, { role: 'admin' })
|
|
195
|
+
|
|
196
|
+
// Delete
|
|
197
|
+
const deleted = await userRepo.delete(user.id)
|
|
198
|
+
|
|
199
|
+
console.log('Done!')
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 4. Configure your database (env vars)
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# .env
|
|
206
|
+
|
|
207
|
+
# SQLite (development)
|
|
208
|
+
DB_DIALECT=sqlite
|
|
209
|
+
SGBD_URI=./myapp.db
|
|
210
|
+
|
|
211
|
+
# PostgreSQL (production)
|
|
212
|
+
DB_DIALECT=postgres
|
|
213
|
+
SGBD_URI=postgresql://user:password@localhost:5432/mydb
|
|
214
|
+
|
|
215
|
+
# MongoDB (cloud)
|
|
216
|
+
DB_DIALECT=mongodb
|
|
217
|
+
SGBD_URI=mongodb+srv://user:password@cluster.mongodb.net/mydb
|
|
218
|
+
|
|
219
|
+
# MySQL
|
|
220
|
+
DB_DIALECT=mysql
|
|
221
|
+
SGBD_URI=mysql://user:password@localhost:3306/mydb
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Core API Reference
|
|
227
|
+
|
|
228
|
+
### Connection
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { createConnection, registerSchema, registerSchemas } from '@mostajs/orm'
|
|
232
|
+
|
|
233
|
+
// Register schemas before connecting
|
|
234
|
+
registerSchema(UserSchema)
|
|
235
|
+
registerSchemas([UserSchema, PostSchema, CommentSchema])
|
|
236
|
+
|
|
237
|
+
// Connect (reads from environment)
|
|
238
|
+
const dialect = await createConnection()
|
|
239
|
+
|
|
240
|
+
// Or pass config directly
|
|
241
|
+
const dialect = await createConnection({
|
|
242
|
+
dialect: 'postgres',
|
|
243
|
+
uri: 'postgresql://localhost/mydb',
|
|
244
|
+
schemaStrategy: 'update', // validate | update | create | create-drop
|
|
245
|
+
showSQL: false,
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### BaseRepository\<T\> — All Methods
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ── READ ────────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
// Get all records (with optional filter & options)
|
|
255
|
+
findAll(filter?: FilterQuery, options?: QueryOptions): Promise<T[]>
|
|
256
|
+
|
|
257
|
+
// Get one record by filter
|
|
258
|
+
findOne(filter: FilterQuery, options?: QueryOptions): Promise<T | null>
|
|
259
|
+
|
|
260
|
+
// Get by ID
|
|
261
|
+
findById(id: string, options?: QueryOptions): Promise<T | null>
|
|
262
|
+
|
|
263
|
+
// Get by ID with related entities populated
|
|
264
|
+
findByIdWithRelations(id: string, relations?: string[], options?): Promise<T | null>
|
|
265
|
+
|
|
266
|
+
// Get all with relations
|
|
267
|
+
findWithRelations(filter?, relations?, options?): Promise<T[]>
|
|
268
|
+
|
|
269
|
+
// ── WRITE ────────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
// Create a new record
|
|
272
|
+
create(data: Partial<T>): Promise<T>
|
|
273
|
+
|
|
274
|
+
// Update one record by ID (partial)
|
|
275
|
+
update(id: string, data: Partial<T>): Promise<T | null>
|
|
276
|
+
|
|
277
|
+
// Update many records matching filter
|
|
278
|
+
updateMany(filter: FilterQuery, data: Partial<T>): Promise<number>
|
|
279
|
+
|
|
280
|
+
// Delete one record by ID
|
|
281
|
+
delete(id: string): Promise<boolean>
|
|
282
|
+
|
|
283
|
+
// Delete many records matching filter
|
|
284
|
+
deleteMany(filter: FilterQuery): Promise<number>
|
|
285
|
+
|
|
286
|
+
// Create or update (upsert)
|
|
287
|
+
upsert(filter: FilterQuery, data: Partial<T>): Promise<T>
|
|
288
|
+
|
|
289
|
+
// ── QUERY ────────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
// Count matching records
|
|
292
|
+
count(filter?: FilterQuery): Promise<number>
|
|
293
|
+
|
|
294
|
+
// Get distinct values of a field
|
|
295
|
+
distinct(field: string, filter?: FilterQuery): Promise<unknown[]>
|
|
296
|
+
|
|
297
|
+
// Full-text search across string fields
|
|
298
|
+
search(query: string, options?: QueryOptions): Promise<T[]>
|
|
299
|
+
|
|
300
|
+
// ── ATOMIC ───────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
// Increment a numeric field
|
|
303
|
+
increment(id: string, field: string, amount?: number): Promise<T | null>
|
|
304
|
+
|
|
305
|
+
// Add value to array field (no duplicates)
|
|
306
|
+
addToSet(id: string, field: string, value: unknown): Promise<T | null>
|
|
307
|
+
|
|
308
|
+
// Remove value from array field
|
|
309
|
+
pull(id: string, field: string, value: unknown): Promise<T | null>
|
|
310
|
+
|
|
311
|
+
// ── AGGREGATE ────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
// Aggregation pipeline
|
|
314
|
+
aggregate<R>(stages: AggregateStage[]): Promise<R[]>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### FilterQuery — Operators
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Equality (shorthand)
|
|
321
|
+
findAll({ status: 'active' })
|
|
322
|
+
findAll({ role: 'admin', status: 'active' })
|
|
323
|
+
|
|
324
|
+
// Comparison operators
|
|
325
|
+
findAll({ score: { $gt: 100 } })
|
|
326
|
+
findAll({ score: { $gte: 0, $lte: 1000 } })
|
|
327
|
+
findAll({ age: { $lt: 18 } })
|
|
328
|
+
findAll({ name: { $ne: 'anonymous' } })
|
|
329
|
+
|
|
330
|
+
// Array membership
|
|
331
|
+
findAll({ status: { $in: ['active', 'pending'] } })
|
|
332
|
+
findAll({ role: { $nin: ['banned', 'deleted'] } })
|
|
333
|
+
|
|
334
|
+
// Existence
|
|
335
|
+
findAll({ photo: { $exists: true } })
|
|
336
|
+
findAll({ deletedAt: { $exists: false } })
|
|
337
|
+
|
|
338
|
+
// Regex
|
|
339
|
+
findAll({ email: { $regex: '@gmail\\.com$' } })
|
|
340
|
+
findAll({ name: { $regex: 'alice', $options: 'i' } }) // case-insensitive
|
|
341
|
+
|
|
342
|
+
// Logical
|
|
343
|
+
findAll({ $or: [{ role: 'admin' }, { score: { $gt: 9000 } }] })
|
|
344
|
+
findAll({ $and: [{ status: 'active' }, { score: { $gte: 100 } }] })
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### QueryOptions
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
findAll(filter, {
|
|
351
|
+
sort: { createdAt: -1, name: 1 }, // -1 = DESC, 1 = ASC
|
|
352
|
+
skip: 0,
|
|
353
|
+
limit: 20,
|
|
354
|
+
select: ['id', 'email', 'role'], // include only these fields
|
|
355
|
+
exclude: ['password', '__v'], // exclude these fields
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### EntitySchema Definition
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
const PostSchema: EntitySchema = {
|
|
363
|
+
name: 'Post',
|
|
364
|
+
collection: 'posts',
|
|
365
|
+
timestamps: true,
|
|
366
|
+
|
|
367
|
+
fields: {
|
|
368
|
+
title: { type: 'string', required: true },
|
|
369
|
+
slug: { type: 'string', required: true, unique: true },
|
|
370
|
+
body: { type: 'string', required: true },
|
|
371
|
+
status: { type: 'string', enum: ['draft', 'published'], default: 'draft' },
|
|
372
|
+
views: { type: 'number', default: 0 },
|
|
373
|
+
published: { type: 'boolean', default: false },
|
|
374
|
+
tags: { type: 'array' },
|
|
375
|
+
metadata: { type: 'json' },
|
|
376
|
+
publishedAt: { type: 'date' },
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
relations: {
|
|
380
|
+
author: { target: 'User', type: 'many-to-one', required: true },
|
|
381
|
+
comments: { target: 'Comment', type: 'one-to-many' },
|
|
382
|
+
likes: { target: 'User', type: 'many-to-many', through: 'post_likes' },
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
indexes: [
|
|
386
|
+
{ fields: { slug: 'asc' }, unique: true },
|
|
387
|
+
{ fields: { status: 'asc', publishedAt: -1 } },
|
|
388
|
+
{ fields: { author: 'asc' } },
|
|
389
|
+
],
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Field Types
|
|
394
|
+
|
|
395
|
+
| Type | SQL | MongoDB | Description |
|
|
396
|
+
|------|-----|---------|-------------|
|
|
397
|
+
| `string` | TEXT / VARCHAR | String | Text values |
|
|
398
|
+
| `number` | REAL / DOUBLE | Number | Integer or float |
|
|
399
|
+
| `boolean` | INTEGER(0/1) | Boolean | True/false |
|
|
400
|
+
| `date` | TEXT (ISO) | Date | Datetime values |
|
|
401
|
+
| `json` | TEXT (JSON) | Mixed | Arbitrary object |
|
|
402
|
+
| `array` | TEXT (JSON) | Array | List of values |
|
|
403
|
+
|
|
404
|
+
### Aggregation Pipeline
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// Count users by role
|
|
408
|
+
const stats = await userRepo.aggregate<{ role: string; count: number }>([
|
|
409
|
+
{ $match: { status: 'active' } },
|
|
410
|
+
{ $group: { _by: 'role', count: { $sum: 1 } } },
|
|
411
|
+
{ $sort: { count: -1 } },
|
|
412
|
+
])
|
|
413
|
+
// → [{ role: 'admin', count: 5 }, { role: 'user', count: 142 }]
|
|
414
|
+
|
|
415
|
+
// Sum revenue by month
|
|
416
|
+
const revenue = await orderRepo.aggregate([
|
|
417
|
+
{ $match: { status: 'paid' } },
|
|
418
|
+
{ $group: { _by: 'month', total: { $sum: 'amount' } } },
|
|
419
|
+
])
|
|
420
|
+
|
|
421
|
+
// Top 10 most viewed posts
|
|
422
|
+
const top = await postRepo.aggregate([
|
|
423
|
+
{ $match: { status: 'published' } },
|
|
424
|
+
{ $sort: { views: -1 } },
|
|
425
|
+
{ $limit: 10 },
|
|
426
|
+
])
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Relations
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Schema with relations
|
|
435
|
+
const OrderSchema: EntitySchema = {
|
|
436
|
+
name: 'Order',
|
|
437
|
+
collection: 'orders',
|
|
438
|
+
timestamps: true,
|
|
439
|
+
fields: {
|
|
440
|
+
total: { type: 'number', required: true },
|
|
441
|
+
status: { type: 'string', default: 'pending' },
|
|
442
|
+
},
|
|
443
|
+
relations: {
|
|
444
|
+
customer: { target: 'User', type: 'many-to-one', required: true },
|
|
445
|
+
items: { target: 'Product', type: 'many-to-many', through: 'order_items' },
|
|
446
|
+
},
|
|
447
|
+
indexes: [],
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Populate relations
|
|
451
|
+
const order = await orderRepo.findByIdWithRelations(orderId, ['customer', 'items'])
|
|
452
|
+
// → { id, total, status, customer: { id, email, ... }, items: [{ id, name, price }] }
|
|
453
|
+
|
|
454
|
+
// Filter with relations
|
|
455
|
+
const orders = await orderRepo.findWithRelations(
|
|
456
|
+
{ status: 'pending' },
|
|
457
|
+
['customer'],
|
|
458
|
+
{ sort: { createdAt: -1 }, limit: 50 }
|
|
459
|
+
)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Schema Strategies
|
|
465
|
+
|
|
466
|
+
| Strategy | Behavior | Use Case |
|
|
467
|
+
|----------|----------|----------|
|
|
468
|
+
| `validate` | Checks tables/collections exist, throws if missing | Production safety |
|
|
469
|
+
| `update` | Creates missing tables/indexes, preserves data | Recommended for dev |
|
|
470
|
+
| `create` | Creates tables if not exist | First run |
|
|
471
|
+
| `create-drop` | Drops and recreates all tables | Testing only |
|
|
472
|
+
| `none` | No schema management | External migrations |
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
DB_SCHEMA_STRATEGY=update # in .env
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Environment Variables
|
|
481
|
+
|
|
482
|
+
| Variable | Default | Description |
|
|
483
|
+
|----------|---------|-------------|
|
|
484
|
+
| `DB_DIALECT` | — | Required: `mongodb`, `sqlite`, `postgres`, `mysql`, etc. |
|
|
485
|
+
| `SGBD_URI` | — | Required: connection string |
|
|
486
|
+
| `DB_SCHEMA_STRATEGY` | `update` | Schema management strategy |
|
|
487
|
+
| `DB_SHOW_SQL` | `false` | Log all SQL queries |
|
|
488
|
+
| `DB_FORMAT_SQL` | `false` | Pretty-print SQL logs |
|
|
489
|
+
| `DB_POOL_SIZE` | `10` | Connection pool size (SQL dialects) |
|
|
490
|
+
| `DB_CACHE_ENABLED` | `false` | Query result cache |
|
|
491
|
+
| `DB_CACHE_TTL` | `300` | Cache TTL in seconds |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Complete Example — Blog API
|
|
496
|
+
|
|
497
|
+
See the [full tutorial](docs/tutorial.md) for a step-by-step walkthrough building a complete blog REST API with authentication, pagination, and relations.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Architecture
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
mosta-orm/
|
|
505
|
+
├── src/
|
|
506
|
+
│ ├── index.ts ← Main export
|
|
507
|
+
│ ├── core/
|
|
508
|
+
│ │ ├── types.ts ← Interfaces & types
|
|
509
|
+
│ │ ├── base-repository.ts ← Generic repository
|
|
510
|
+
│ │ ├── factory.ts ← Connection factory
|
|
511
|
+
│ │ ├── registry.ts ← Schema registry
|
|
512
|
+
│ │ ├── normalizer.ts ← _id → id normalization
|
|
513
|
+
│ │ ├── errors.ts ← Custom error classes
|
|
514
|
+
│ │ └── config.ts ← Dialect metadata
|
|
515
|
+
│ └── dialects/
|
|
516
|
+
│ ├── abstract-sql.dialect.ts ← Shared SQL logic
|
|
517
|
+
│ ├── mongo.dialect.ts
|
|
518
|
+
│ ├── sqlite.dialect.ts
|
|
519
|
+
│ ├── postgres.dialect.ts
|
|
520
|
+
│ ├── mysql.dialect.ts
|
|
521
|
+
│ └── ...
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Why MostaORM?
|
|
527
|
+
|
|
528
|
+
| | Prisma | TypeORM | Sequelize | **MostaORM** |
|
|
529
|
+
|---|---|---|---|---|
|
|
530
|
+
| Databases | 6 | 9 | 6 | **13** |
|
|
531
|
+
| MongoDB | ✅ | ✅ | ❌ | ✅ |
|
|
532
|
+
| Oracle | ❌ | ✅ | ✅ | ✅ |
|
|
533
|
+
| SAP HANA | ❌ | ❌ | ❌ | ✅ |
|
|
534
|
+
| Google Spanner | ❌ | ❌ | ❌ | ✅ |
|
|
535
|
+
| Repository pattern | ❌ | ✅ | ❌ | ✅ |
|
|
536
|
+
| No code-gen needed | ❌ | ✅ | ✅ | ✅ |
|
|
537
|
+
| Dialect switching | ❌ | ⚠️ | ⚠️ | ✅ |
|
|
538
|
+
| Lazy driver loading | ❌ | ❌ | ❌ | ✅ |
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## License
|
|
543
|
+
|
|
544
|
+
MIT — © 2025 Dr Hamid MADANI <drmdh@msn.com>
|
|
545
|
+
|
|
546
|
+
## Contributing
|
|
547
|
+
|
|
548
|
+
Issues and PRs are welcome at [github.com/apolocine/mosta-orm](https://github.com/apolocine/mosta-orm).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IRepository, IDialect, EntitySchema, FilterQuery, QueryOptions, AggregateStage } from './types.js';
|
|
2
|
+
export declare class BaseRepository<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}> implements IRepository<T> {
|
|
5
|
+
protected readonly schema: EntitySchema;
|
|
6
|
+
protected readonly dialect: IDialect;
|
|
7
|
+
constructor(schema: EntitySchema, dialect: IDialect);
|
|
8
|
+
findAll(filter?: FilterQuery, options?: QueryOptions): Promise<T[]>;
|
|
9
|
+
findOne(filter: FilterQuery, options?: QueryOptions): Promise<T | null>;
|
|
10
|
+
findById(id: string, options?: QueryOptions): Promise<T | null>;
|
|
11
|
+
findByIdWithRelations(id: string, relations?: string[], options?: QueryOptions): Promise<T | null>;
|
|
12
|
+
create(data: Partial<T>): Promise<T>;
|
|
13
|
+
update(id: string, data: Partial<T>): Promise<T | null>;
|
|
14
|
+
updateMany(filter: FilterQuery, data: Partial<T>): Promise<number>;
|
|
15
|
+
delete(id: string): Promise<boolean>;
|
|
16
|
+
deleteMany(filter: FilterQuery): Promise<number>;
|
|
17
|
+
count(filter?: FilterQuery): Promise<number>;
|
|
18
|
+
search(query: string, options?: QueryOptions): Promise<T[]>;
|
|
19
|
+
distinct(field: string, filter?: FilterQuery): Promise<unknown[]>;
|
|
20
|
+
aggregate<R = Record<string, unknown>>(stages: AggregateStage[]): Promise<R[]>;
|
|
21
|
+
upsert(filter: FilterQuery, data: Partial<T>): Promise<T>;
|
|
22
|
+
increment(id: string, field: string, amount: number): Promise<T | null>;
|
|
23
|
+
addToSet(id: string, field: string, value: unknown): Promise<T | null>;
|
|
24
|
+
pull(id: string, field: string, value: unknown): Promise<T | null>;
|
|
25
|
+
findWithRelations(filter: FilterQuery, relations: string[], options?: QueryOptions): Promise<T[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizeDoc, normalizeDocs } from './normalizer.js';
|
|
2
|
+
export class BaseRepository {
|
|
3
|
+
schema;
|
|
4
|
+
dialect;
|
|
5
|
+
constructor(schema, dialect) {
|
|
6
|
+
this.schema = schema;
|
|
7
|
+
this.dialect = dialect;
|
|
8
|
+
}
|
|
9
|
+
async findAll(filter = {}, options) {
|
|
10
|
+
const docs = await this.dialect.find(this.schema, filter, options);
|
|
11
|
+
return normalizeDocs(docs);
|
|
12
|
+
}
|
|
13
|
+
async findOne(filter, options) {
|
|
14
|
+
const doc = await this.dialect.findOne(this.schema, filter, options);
|
|
15
|
+
return doc ? normalizeDoc(doc) : null;
|
|
16
|
+
}
|
|
17
|
+
async findById(id, options) {
|
|
18
|
+
const doc = await this.dialect.findById(this.schema, id, options);
|
|
19
|
+
return doc ? normalizeDoc(doc) : null;
|
|
20
|
+
}
|
|
21
|
+
async findByIdWithRelations(id, relations, options) {
|
|
22
|
+
if (!relations || relations.length === 0) {
|
|
23
|
+
return this.findById(id, options);
|
|
24
|
+
}
|
|
25
|
+
const doc = await this.dialect.findByIdWithRelations(this.schema, id, relations, options);
|
|
26
|
+
return doc ? normalizeDoc(doc) : null;
|
|
27
|
+
}
|
|
28
|
+
async create(data) {
|
|
29
|
+
const doc = await this.dialect.create(this.schema, data);
|
|
30
|
+
return normalizeDoc(doc);
|
|
31
|
+
}
|
|
32
|
+
async update(id, data) {
|
|
33
|
+
const doc = await this.dialect.update(this.schema, id, data);
|
|
34
|
+
return doc ? normalizeDoc(doc) : null;
|
|
35
|
+
}
|
|
36
|
+
async updateMany(filter, data) {
|
|
37
|
+
return this.dialect.updateMany(this.schema, filter, data);
|
|
38
|
+
}
|
|
39
|
+
async delete(id) {
|
|
40
|
+
return this.dialect.delete(this.schema, id);
|
|
41
|
+
}
|
|
42
|
+
async deleteMany(filter) {
|
|
43
|
+
return this.dialect.deleteMany(this.schema, filter);
|
|
44
|
+
}
|
|
45
|
+
async count(filter = {}) {
|
|
46
|
+
return this.dialect.count(this.schema, filter);
|
|
47
|
+
}
|
|
48
|
+
async search(query, options) {
|
|
49
|
+
// Default: search all string fields — subclasses override with specific fields
|
|
50
|
+
const fields = Object.entries(this.schema.fields)
|
|
51
|
+
.filter(([, f]) => f.type === 'string')
|
|
52
|
+
.map(([name]) => name);
|
|
53
|
+
const docs = await this.dialect.search(this.schema, query, fields, options);
|
|
54
|
+
return normalizeDocs(docs);
|
|
55
|
+
}
|
|
56
|
+
async distinct(field, filter = {}) {
|
|
57
|
+
return this.dialect.distinct(this.schema, field, filter);
|
|
58
|
+
}
|
|
59
|
+
async aggregate(stages) {
|
|
60
|
+
return this.dialect.aggregate(this.schema, stages);
|
|
61
|
+
}
|
|
62
|
+
async upsert(filter, data) {
|
|
63
|
+
const doc = await this.dialect.upsert(this.schema, filter, data);
|
|
64
|
+
return normalizeDoc(doc);
|
|
65
|
+
}
|
|
66
|
+
async increment(id, field, amount) {
|
|
67
|
+
const doc = await this.dialect.increment(this.schema, id, field, amount);
|
|
68
|
+
return doc ? normalizeDoc(doc) : null;
|
|
69
|
+
}
|
|
70
|
+
async addToSet(id, field, value) {
|
|
71
|
+
const doc = await this.dialect.addToSet(this.schema, id, field, value);
|
|
72
|
+
return doc ? normalizeDoc(doc) : null;
|
|
73
|
+
}
|
|
74
|
+
async pull(id, field, value) {
|
|
75
|
+
const doc = await this.dialect.pull(this.schema, id, field, value);
|
|
76
|
+
return doc ? normalizeDoc(doc) : null;
|
|
77
|
+
}
|
|
78
|
+
async findWithRelations(filter, relations, options) {
|
|
79
|
+
const docs = await this.dialect.findWithRelations(this.schema, filter, relations, options);
|
|
80
|
+
return normalizeDocs(docs);
|
|
81
|
+
}
|
|
82
|
+
}
|