@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +548 -0
  3. package/dist/core/base-repository.d.ts +26 -0
  4. package/dist/core/base-repository.js +82 -0
  5. package/dist/core/config.d.ts +62 -0
  6. package/dist/core/config.js +116 -0
  7. package/dist/core/errors.d.ts +30 -0
  8. package/dist/core/errors.js +49 -0
  9. package/dist/core/factory.d.ts +41 -0
  10. package/dist/core/factory.js +142 -0
  11. package/dist/core/normalizer.d.ts +9 -0
  12. package/dist/core/normalizer.js +19 -0
  13. package/dist/core/registry.d.ts +43 -0
  14. package/dist/core/registry.js +78 -0
  15. package/dist/core/types.d.ts +228 -0
  16. package/dist/core/types.js +5 -0
  17. package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
  18. package/dist/dialects/abstract-sql.dialect.js +1071 -0
  19. package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
  20. package/dist/dialects/cockroachdb.dialect.js +23 -0
  21. package/dist/dialects/db2.dialect.d.ts +2 -0
  22. package/dist/dialects/db2.dialect.js +190 -0
  23. package/dist/dialects/hana.dialect.d.ts +2 -0
  24. package/dist/dialects/hana.dialect.js +199 -0
  25. package/dist/dialects/hsqldb.dialect.d.ts +2 -0
  26. package/dist/dialects/hsqldb.dialect.js +114 -0
  27. package/dist/dialects/mariadb.dialect.d.ts +2 -0
  28. package/dist/dialects/mariadb.dialect.js +87 -0
  29. package/dist/dialects/mongo.dialect.d.ts +2 -0
  30. package/dist/dialects/mongo.dialect.js +480 -0
  31. package/dist/dialects/mssql.dialect.d.ts +27 -0
  32. package/dist/dialects/mssql.dialect.js +127 -0
  33. package/dist/dialects/mysql.dialect.d.ts +24 -0
  34. package/dist/dialects/mysql.dialect.js +101 -0
  35. package/dist/dialects/oracle.dialect.d.ts +2 -0
  36. package/dist/dialects/oracle.dialect.js +206 -0
  37. package/dist/dialects/postgres.dialect.d.ts +26 -0
  38. package/dist/dialects/postgres.dialect.js +105 -0
  39. package/dist/dialects/spanner.dialect.d.ts +2 -0
  40. package/dist/dialects/spanner.dialect.js +259 -0
  41. package/dist/dialects/sqlite.dialect.d.ts +2 -0
  42. package/dist/dialects/sqlite.dialect.js +1027 -0
  43. package/dist/dialects/sybase.dialect.d.ts +2 -0
  44. package/dist/dialects/sybase.dialect.js +119 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +26 -0
  47. package/docs/api-reference.md +1009 -0
  48. package/docs/dialects.md +673 -0
  49. package/docs/tutorial.md +846 -0
  50. 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
+ [![npm version](https://img.shields.io/npm/v/@mostajs/orm.svg)](https://www.npmjs.com/package/@mostajs/orm)
7
+ [![license](https://img.shields.io/npm/l/@mostajs/orm.svg)](LICENSE)
8
+ [![node](https://img.shields.io/node/v/@mostajs/orm.svg)](https://nodejs.org)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](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
+ }