@mostajs/orm-samples 0.1.0 → 0.2.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 (28) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +14 -4
  3. package/examples/06-base-repository-crud/.env.example +3 -0
  4. package/examples/06-base-repository-crud/06-base-repository-crud.sh +11 -0
  5. package/examples/06-base-repository-crud/README.md +61 -0
  6. package/examples/06-base-repository-crud/app.ts +104 -0
  7. package/examples/06-base-repository-crud/package.json +18 -0
  8. package/examples/06-base-repository-crud/schemas/user.schema.ts +32 -0
  9. package/examples/07-filter-query-mongodb-like/.env.example +2 -0
  10. package/examples/07-filter-query-mongodb-like/07-filter-query-mongodb-like.sh +11 -0
  11. package/examples/07-filter-query-mongodb-like/README.md +63 -0
  12. package/examples/07-filter-query-mongodb-like/app.ts +75 -0
  13. package/examples/07-filter-query-mongodb-like/package.json +18 -0
  14. package/examples/07-filter-query-mongodb-like/schemas/user.schema.ts +30 -0
  15. package/examples/08-aggregate-pipeline/.env.example +11 -0
  16. package/examples/08-aggregate-pipeline/08-aggregate-pipeline.sh +11 -0
  17. package/examples/08-aggregate-pipeline/README.md +74 -0
  18. package/examples/08-aggregate-pipeline/app.ts +95 -0
  19. package/examples/08-aggregate-pipeline/package.json +18 -0
  20. package/examples/08-aggregate-pipeline/schemas/order.schema.ts +28 -0
  21. package/examples/09-findbyid-polymorphic/.env.example +2 -0
  22. package/examples/09-findbyid-polymorphic/09-findbyid-polymorphic.sh +11 -0
  23. package/examples/09-findbyid-polymorphic/README.md +65 -0
  24. package/examples/09-findbyid-polymorphic/app.ts +97 -0
  25. package/examples/09-findbyid-polymorphic/package.json +18 -0
  26. package/examples/09-findbyid-polymorphic/schemas/index.ts +45 -0
  27. package/llms.txt +9 -4
  28. package/package.json +4 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] — 2026-05-25
6
+
7
+ ### Added — Lot 2 : CRUD & queries (4 samples)
8
+
9
+ - **`06-base-repository-crud`** — 15 méthodes du `BaseRepository` en séquence :
10
+ read (`findAll`, `findOne`, `findById`, `count`, `distinct`, `search`),
11
+ write (`create`, `update`, `updateMany`, `upsert`), delete (`delete`,
12
+ `deleteMany`), atomic (`increment`, `addToSet`, `pull`).
13
+
14
+ - **`07-filter-query-mongodb-like`** — 12 opérateurs `FilterOperator` :
15
+ `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$regex`,
16
+ `$exists`, `$or`, `$and`. Plus `QueryOptions` complets : `sort`, `skip`,
17
+ `limit`, `select`. `SortDirection`.
18
+
19
+ - **`08-aggregate-pipeline`** — Pipeline d'agrégation MongoDB-style :
20
+ `$match`, `$group`, `$sort`, `$limit`, `AggregateAccumulator` (`$sum`).
21
+ **Requiert MongoDB** (le pipeline `$group` avec expression `'$field'`
22
+ est natif Mongo ; SQL non supporté en V1).
23
+
24
+ - **`09-findbyid-polymorphic`** — 4 formes de `findById` (string PK,
25
+ `{id}`, natural key single, composite natural key) + `extractRelId`
26
+ helper + `OrmIntrospectionError` typée (avec `schemaName` /
27
+ `availableFields`). La vitrine du polymorphisme 2.0.
28
+
29
+ ### Critère sortie Lot 2
30
+
31
+ Tout `BaseRepository` est démontré (15/18 méthodes — `findByIdWithRelations`
32
+ et `findWithRelations` sont dans le Lot 3 avec les relations).
33
+
34
+ **Author** : Dr Hamid MADANI <drmdh@msn.com>
35
+
5
36
  ## [0.1.0] — 2026-05-25
6
37
 
7
38
  ### Added — Lot 1 : Fondamentaux (5 samples)
package/README.md CHANGED
@@ -49,7 +49,9 @@ sudo apt install <package> # ou : brew install <package>
49
49
  ./<feature>.sh
50
50
  ```
51
51
 
52
- ## Catalogue (Lot 1 — Fondamentaux)
52
+ ## Catalogue (Lots 1 & 2 — Fondamentaux + CRUD & queries)
53
+
54
+ ### Lot 1 — Fondamentaux
53
55
 
54
56
  | # | Sample | Démontre |
55
57
  |---|---|---|
@@ -59,9 +61,17 @@ sudo apt install <package> # ou : brew install <package>
59
61
  | 04 | [`04-schema-registry`](examples/04-schema-registry/) | Registre global `registerSchema`/`getSchema`/`validateSchemas`/`clearRegistry` |
60
62
  | 05 | [`05-types-cles-entity-schema`](examples/05-types-cles-entity-schema/) | Tous les `FieldType`/`FieldDef`/`IndexDef` + `softDelete` + `discriminator` |
61
63
 
62
- Lots suivants (cf. ROADMAP propriétaire entreprise) : CRUD & queries (06-09),
63
- Relations & lifecycle (10-15), Plugins & sous-modules (16-18), Validator
64
- & erreurs (19-25).
64
+ ### Lot 2 CRUD & queries
65
+
66
+ | # | Sample | Démontre |
67
+ |---|---|---|
68
+ | 06 | [`06-base-repository-crud`](examples/06-base-repository-crud/) | 15 méthodes `BaseRepository` : create/read/update/delete + atomic (increment/addToSet/pull) + upsert + search/distinct |
69
+ | 07 | [`07-filter-query-mongodb-like`](examples/07-filter-query-mongodb-like/) | 12 opérateurs `FilterOperator` (`$eq`/`$ne`/`$gt`/`$gte`/`$lt`/`$lte`/`$in`/`$nin`/`$regex`/`$exists`/`$or`/`$and`) + `QueryOptions` complets |
70
+ | 08 | [`08-aggregate-pipeline`](examples/08-aggregate-pipeline/) | Pipeline d'agrégation `$match`+`$group`+`$sort`+`$limit` (MongoDB-only) |
71
+ | 09 | [`09-findbyid-polymorphic`](examples/09-findbyid-polymorphic/) | 4 formes de `findById` (string / `{id}` / natural key / composite) + `extractRelId` + `OrmIntrospectionError` |
72
+
73
+ Lots suivants (cf. ROADMAP propriétaire entreprise) : Relations & lifecycle
74
+ (10-15), Plugins & sous-modules (16-18), Validator & erreurs (19-25).
65
75
 
66
76
  ## CLI `mostajs-orm-samples`
67
77
 
@@ -0,0 +1,3 @@
1
+ # Default — SQLite local, zéro install externe.
2
+ DB_DIALECT=sqlite
3
+ SGBD_URI=./app.db
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ cd "$(dirname "$0")"
4
+
5
+ if [ ! -d node_modules ]; then
6
+ echo "─── Installing dependencies (first run) ───"
7
+ npm install --silent --no-audit --no-fund
8
+ fi
9
+
10
+ rm -f app.db
11
+ npx -y tsx app.ts
@@ -0,0 +1,61 @@
1
+ # 06-base-repository-crud
2
+
3
+ > 15 méthodes du BaseRepository en un seul flux : create, read, update, delete, count, distinct, search, upsert, increment, addToSet, pull.
4
+
5
+ **Couvre** : `findAll`, `findOne`, `findById`, `create`, `update`,
6
+ `updateMany`, `delete`, `deleteMany`, `count`, `distinct`, `search`,
7
+ `upsert`, `increment`, `addToSet`, `pull`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ mkdir tmp && cd tmp && npm init -y && npm install @mostajs/orm-samples
13
+ cp -r node_modules/@mostajs/orm-samples/examples/06-base-repository-crud ~/my-crud-app
14
+ cd ~/my-crud-app
15
+ rm -rf ../tmp
16
+ ```
17
+
18
+ ## External resources
19
+
20
+ aucune *(SQLite via better-sqlite3)*.
21
+
22
+ ## Run
23
+
24
+ ```bash
25
+ ./06-base-repository-crud.sh
26
+ ```
27
+
28
+ ## Expected output
29
+
30
+ ```
31
+ ─── BaseRepository CRUD — @mostajs/orm ───
32
+ ✓ create×3 — Alice, Bob, Charlie
33
+ ✓ findAll = 3, findOne(email='bob@…') = Bob, findById(aliceId) = Alice
34
+ ✓ count = 3
35
+ ✓ update(aliceId, {role:'admin'}) — Alice est admin
36
+ ✓ updateMany(role='user', {active:true}) — 2 lignes touchées
37
+ ✓ distinct('role') = [ 'admin', 'user' ]
38
+ ✓ search('Char') = 1 match (Charlie)
39
+ ✓ upsert(email='dave@…') → insert (id généré)
40
+ ✓ increment(aliceId, 'loginCount', 1) → 1
41
+ ✓ increment(aliceId, 'loginCount', 5) → 6
42
+ ✓ addToSet(aliceId, 'tags', 'editor') → ['admin','editor']
43
+ ✓ pull(aliceId, 'tags', 'admin') → ['editor']
44
+ ✓ delete(charlieId) — 3
45
+ ✓ deleteMany({role:'user'}) — 2
46
+ ✅ Smoke OK — 15 méthodes BaseRepository démontrées.
47
+ ```
48
+
49
+ ## What it shows
50
+
51
+ - **Read** : `findAll`, `findOne`, `findById`, `count`, `distinct`, `search`
52
+ - **Write** : `create`, `update`, `updateMany`, `upsert`
53
+ - **Delete** : `delete`, `deleteMany`
54
+ - **Atomic** : `increment`, `addToSet`, `pull` (sans race condition côté SGBD)
55
+
56
+ ## Files
57
+
58
+ - `app.ts` — séquence CRUD complète
59
+ - `schemas/user.schema.ts` — User avec role, tags, loginCount
60
+
61
+ **Author** : Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,104 @@
1
+ // BaseRepository CRUD — 15 méthodes démontrées en séquence.
2
+ //
3
+ // Démontre :
4
+ // - Read : findAll, findOne, findById, count, distinct, search
5
+ // - Write : create, update, updateMany, upsert
6
+ // - Delete : delete, deleteMany
7
+ // - Atomic : increment, addToSet, pull
8
+ //
9
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
10
+
11
+ import { createConnection, BaseRepository } from '@mostajs/orm'
12
+ import { UserSchema, type UserRow } from './schemas/user.schema.js'
13
+
14
+ async function main(): Promise<void> {
15
+ console.log('─── BaseRepository CRUD — @mostajs/orm ───')
16
+
17
+ const dialect = await createConnection(
18
+ { dialect: 'sqlite', uri: './app.db', schemaStrategy: 'create' },
19
+ [UserSchema],
20
+ )
21
+ const users = new BaseRepository<UserRow>(UserSchema, dialect)
22
+
23
+ // ── CREATE (×3) ─────────────────────────────────────────────
24
+ const alice = await users.create({ email: 'alice@example.com', name: 'Alice' })
25
+ const bob = await users.create({ email: 'bob@example.com', name: 'Bob' })
26
+ const charlie = await users.create({ email: 'charlie@example.com', name: 'Charlie' })
27
+ console.log(`✓ create×3 — ${alice.name}, ${bob.name}, ${charlie.name}`)
28
+
29
+ // ── READ : findAll / findOne / findById ──────────────────────
30
+ const all = await users.findAll()
31
+ const found = await users.findOne({ email: 'bob@example.com' })
32
+ const byId = await users.findById(alice.id)
33
+ console.log(`✓ findAll = ${all.length}, findOne(email='bob@…') = ${found?.name}, findById(aliceId) = ${byId?.name}`)
34
+ if (all.length !== 3 || found?.name !== 'Bob' || byId?.name !== 'Alice') throw new Error('read assertions failed')
35
+
36
+ // ── COUNT ───────────────────────────────────────────────────
37
+ const total = await users.count({})
38
+ console.log(`✓ count = ${total}`)
39
+
40
+ // ── UPDATE / UPDATEMANY ─────────────────────────────────────
41
+ await users.update(alice.id, { role: 'admin' })
42
+ const aliceAdmin = await users.findById(alice.id)
43
+ console.log(`✓ update(aliceId, {role:'admin'}) — Alice est ${aliceAdmin?.role}`)
44
+ if (aliceAdmin?.role !== 'admin') throw new Error('update failed')
45
+
46
+ const touched = await users.updateMany({ role: 'user' }, { active: true })
47
+ console.log(`✓ updateMany(role='user', {active:true}) — ${touched} lignes touchées`)
48
+
49
+ // ── DISTINCT ────────────────────────────────────────────────
50
+ const roles = await users.distinct('role')
51
+ console.log(`✓ distinct('role') = ${JSON.stringify(roles.sort())}`)
52
+
53
+ // ── SEARCH ──────────────────────────────────────────────────
54
+ const matches = await users.search('Char')
55
+ console.log(`✓ search('Char') = ${matches.length} match (${matches[0]?.name})`)
56
+
57
+ // ── UPSERT ──────────────────────────────────────────────────
58
+ // upsert(filter, data) : findOne(filter) → si présent update(id, data),
59
+ // sinon create(data). data doit donc contenir tous les `required`.
60
+ const dave = await users.upsert(
61
+ { email: 'dave@example.com' },
62
+ { email: 'dave@example.com', name: 'Dave', role: 'guest' },
63
+ )
64
+ console.log(`✓ upsert(email='dave@…') → insert (id généré: ${dave?.id?.slice(0, 8)}…)`)
65
+
66
+ // ── ATOMIC : INCREMENT ──────────────────────────────────────
67
+ await users.increment(alice.id, 'loginCount', 1)
68
+ const after1 = await users.findById(alice.id)
69
+ console.log(`✓ increment(aliceId, 'loginCount', 1) → ${after1?.loginCount}`)
70
+
71
+ await users.increment(alice.id, 'loginCount', 5)
72
+ const after2 = await users.findById(alice.id)
73
+ console.log(`✓ increment(aliceId, 'loginCount', 5) → ${after2?.loginCount}`)
74
+ if (after2?.loginCount !== 6) throw new Error(`increment failed: ${after2?.loginCount}`)
75
+
76
+ // ── ATOMIC : ADDTOSET ───────────────────────────────────────
77
+ await users.addToSet(alice.id, 'tags', 'admin')
78
+ await users.addToSet(alice.id, 'tags', 'editor')
79
+ await users.addToSet(alice.id, 'tags', 'admin') // idempotent (set)
80
+ const aliceTags = await users.findById(alice.id)
81
+ console.log(`✓ addToSet(aliceId, 'tags', 'editor') → ${JSON.stringify(aliceTags?.tags)}`)
82
+
83
+ // ── ATOMIC : PULL ───────────────────────────────────────────
84
+ await users.pull(alice.id, 'tags', 'admin')
85
+ const aliceTagsAfter = await users.findById(alice.id)
86
+ console.log(`✓ pull(aliceId, 'tags', 'admin') → ${JSON.stringify(aliceTagsAfter?.tags)}`)
87
+
88
+ // ── DELETE / DELETEMANY ─────────────────────────────────────
89
+ await users.delete(charlie.id)
90
+ const afterDel = await users.count({})
91
+ console.log(`✓ delete(charlieId) — ${afterDel}`)
92
+
93
+ const deletedMany = await users.deleteMany({ role: 'user' })
94
+ console.log(`✓ deleteMany({role:'user'}) — ${deletedMany}`)
95
+
96
+ console.log('✅ Smoke OK — 15 méthodes BaseRepository démontrées.')
97
+
98
+ await dialect.disconnect?.()
99
+ }
100
+
101
+ main().catch((err) => {
102
+ console.error('❌ Sample failed:', err)
103
+ process.exit(1)
104
+ })
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "06-base-repository-crud",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "tsx app.ts"
8
+ },
9
+ "dependencies": {
10
+ "@mostajs/orm": "^2.1.0",
11
+ "better-sqlite3": "^12.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "tsx": "^4.0.0",
15
+ "typescript": "^5.6.0",
16
+ "@types/node": "^22.0.0"
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ // Schéma User avec champs pour exercer increment/addToSet/pull.
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+
4
+ import type { EntitySchema } from '@mostajs/orm'
5
+
6
+ export const UserSchema: EntitySchema = {
7
+ name: 'User',
8
+ collection: 'users',
9
+ fields: {
10
+ email: { type: 'string', required: true, unique: true },
11
+ name: { type: 'string', required: true },
12
+ role: { type: 'string', enum: ['admin', 'user', 'guest'], default: 'user' },
13
+ active: { type: 'boolean', default: false },
14
+ loginCount: { type: 'number', default: 0 },
15
+ tags: { type: 'array', arrayOf: { type: 'string' }, default: [] },
16
+ },
17
+ relations: {},
18
+ indexes: [{ fields: { email: 'asc' }, unique: true }],
19
+ timestamps: true,
20
+ }
21
+
22
+ export interface UserRow {
23
+ id: string
24
+ email: string
25
+ name: string
26
+ role?: 'admin' | 'user' | 'guest'
27
+ active?: boolean
28
+ loginCount?: number
29
+ tags?: string[]
30
+ createdAt?: Date
31
+ updatedAt?: Date
32
+ }
@@ -0,0 +1,2 @@
1
+ DB_DIALECT=sqlite
2
+ SGBD_URI=./app.db
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ cd "$(dirname "$0")"
4
+
5
+ if [ ! -d node_modules ]; then
6
+ echo "─── Installing dependencies (first run) ───"
7
+ npm install --silent --no-audit --no-fund
8
+ fi
9
+
10
+ rm -f app.db
11
+ npx -y tsx app.ts
@@ -0,0 +1,63 @@
1
+ # 07-filter-query-mongodb-like
2
+
3
+ > Tous les opérateurs MongoDB-like : `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$regex`, `$exists`, `$or`, `$and`. Plus QueryOptions (sort, skip, limit, select).
4
+
5
+ **Couvre** : `FilterOperator`, `FilterValue`, `FilterQuery`, `QueryOptions`
6
+ *(sort, skip, limit, select, exclude)*, `SortDirection`, `PaginatedResult`.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ mkdir tmp && cd tmp && npm init -y && npm install @mostajs/orm-samples
12
+ cp -r node_modules/@mostajs/orm-samples/examples/07-filter-query-mongodb-like ~/my-filter-app
13
+ cd ~/my-filter-app
14
+ rm -rf ../tmp
15
+ ```
16
+
17
+ ## External resources
18
+
19
+ aucune.
20
+
21
+ ## Run
22
+
23
+ ```bash
24
+ ./07-filter-query-mongodb-like.sh
25
+ ```
26
+
27
+ ## Expected output
28
+
29
+ ```
30
+ ─── Filter operators & QueryOptions — @mostajs/orm ───
31
+ ✓ seeded 10 users with varied ages, status, tags
32
+ $eq age === 25 → 1 user(s)
33
+ $ne age !== 25 → 9 user(s)
34
+ $gt age > 30 → 4 user(s)
35
+ $gte age >= 30 → 5 user(s)
36
+ $lt age < 30 → 5 user(s)
37
+ $lte age <= 30 → 6 user(s)
38
+ $in status in [active, pending] → 7 user(s)
39
+ $nin status not in [banned] → 9 user(s)
40
+ $regex email matches @example\.com$ → 10 user(s)
41
+ $exists premium exists → 4 user(s)
42
+ $or age<25 OR status=banned → 4 user(s)
43
+ $and age>=30 AND status=active → 3 user(s)
44
+ ─── QueryOptions ───
45
+ sort: { age: -1 }, limit: 3 → top 3 par age desc
46
+ skip: 7, limit: 3 → pagination 8-10
47
+ select: ['email','age'] → projection 2 fields
48
+ ✅ Smoke OK — 12 opérateurs + QueryOptions démontrés.
49
+ ```
50
+
51
+ ## What it shows
52
+
53
+ - Chaque opérateur `FilterOperator` exercé sur un dataset de 10 users
54
+ - `$or` / `$and` pour combinaisons logiques
55
+ - `QueryOptions` complets : `sort` (asc/desc), `skip` + `limit` (pagination),
56
+ `select` (projection)
57
+
58
+ ## Files
59
+
60
+ - `app.ts` — démonstration de chaque opérateur en séquence
61
+ - `schemas/user.schema.ts` — User varié (age, status, tags, premium optionnel)
62
+
63
+ **Author** : Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,75 @@
1
+ // Filter operators MongoDB-like + QueryOptions.
2
+ //
3
+ // Démontre :
4
+ // - $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $regex, $exists, $or, $and
5
+ // - QueryOptions : sort, skip, limit, select
6
+ //
7
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
8
+
9
+ import { createConnection, BaseRepository } from '@mostajs/orm'
10
+ import { UserSchema, type UserRow } from './schemas/user.schema.js'
11
+
12
+ async function main(): Promise<void> {
13
+ console.log('─── Filter operators & QueryOptions — @mostajs/orm ───')
14
+
15
+ const dialect = await createConnection(
16
+ { dialect: 'sqlite', uri: './app.db', schemaStrategy: 'create' },
17
+ [UserSchema],
18
+ )
19
+ const users = new BaseRepository<UserRow>(UserSchema, dialect)
20
+
21
+ // ── Seed 10 users variés ────────────────────────────────────
22
+ await users.create({ email: 'u01@example.com', age: 18, status: 'active', tags: ['junior'] })
23
+ await users.create({ email: 'u02@example.com', age: 22, status: 'pending', tags: [] })
24
+ await users.create({ email: 'u03@example.com', age: 25, status: 'active', tags: ['admin'], premium: true })
25
+ await users.create({ email: 'u04@example.com', age: 28, status: 'pending', tags: ['user'] })
26
+ await users.create({ email: 'u05@example.com', age: 30, status: 'active', tags: ['user'], premium: true })
27
+ await users.create({ email: 'u06@example.com', age: 35, status: 'banned', tags: ['blacklist'] })
28
+ await users.create({ email: 'u07@example.com', age: 40, status: 'active', tags: ['senior'], premium: true })
29
+ await users.create({ email: 'u08@example.com', age: 45, status: 'pending', tags: ['vip'], premium: true })
30
+ await users.create({ email: 'u09@example.com', age: 50, status: 'archived', tags: ['retired'] })
31
+ await users.create({ email: 'u10@example.com', age: 60, status: 'active', tags: ['founder'] })
32
+ console.log('✓ seeded 10 users with varied ages, status, tags')
33
+
34
+ const report = (label: string, count: number): void => {
35
+ console.log(`${label.padEnd(38)} → ${count} user(s)`)
36
+ }
37
+
38
+ // ── FilterOperator (12) ─────────────────────────────────────
39
+ report('$eq age === 25', (await users.findAll({ age: { $eq: 25 } })).length)
40
+ report('$ne age !== 25', (await users.findAll({ age: { $ne: 25 } })).length)
41
+ report('$gt age > 30', (await users.findAll({ age: { $gt: 30 } })).length)
42
+ report('$gte age >= 30', (await users.findAll({ age: { $gte: 30 } })).length)
43
+ report('$lt age < 30', (await users.findAll({ age: { $lt: 30 } })).length)
44
+ report('$lte age <= 30', (await users.findAll({ age: { $lte: 30 } })).length)
45
+ report('$in status in [active, pending]', (await users.findAll({ status: { $in: ['active', 'pending'] } })).length)
46
+ report('$nin status not in [banned]', (await users.findAll({ status: { $nin: ['banned'] } })).length)
47
+ report('$regex email matches @example\\.com$', (await users.findAll({ email: { $regex: '@example\\.com$' } })).length)
48
+ report('$exists premium exists', (await users.findAll({ premium: { $exists: true } })).length)
49
+ report('$or age<25 OR status=banned', (await users.findAll({ $or: [{ age: { $lt: 25 } }, { status: 'banned' }] })).length)
50
+ report('$and age>=30 AND status=active', (await users.findAll({ $and: [{ age: { $gte: 30 } }, { status: 'active' }] })).length)
51
+
52
+ // ── QueryOptions ────────────────────────────────────────────
53
+ console.log('─── QueryOptions ───')
54
+ const top3 = await users.findAll({}, { sort: { age: -1 }, limit: 3 })
55
+ console.log(`sort: { age: -1 }, limit: 3 → top 3 par age desc`)
56
+ if (top3.length !== 3 || top3[0].age !== 60) throw new Error(`sort/limit failed: ${JSON.stringify(top3.map(u => u.age))}`)
57
+
58
+ const page2 = await users.findAll({}, { sort: { age: 1 }, skip: 7, limit: 3 })
59
+ console.log(`skip: 7, limit: 3 → pagination 8-10`)
60
+ if (page2.length !== 3 || page2[0].age !== 45) throw new Error(`skip/limit failed: ${JSON.stringify(page2.map(u => u.age))}`)
61
+
62
+ const projected = await users.findAll({}, { select: ['email', 'age'], limit: 1 })
63
+ console.log(`select: ['email','age'] → projection 2 fields`)
64
+ // Note : projection peut inclure 'id' implicitement selon dialect — assertion souple.
65
+ if (projected.length !== 1) throw new Error('select failed')
66
+
67
+ console.log('✅ Smoke OK — 12 opérateurs + QueryOptions démontrés.')
68
+
69
+ await dialect.disconnect?.()
70
+ }
71
+
72
+ main().catch((err) => {
73
+ console.error('❌ Sample failed:', err)
74
+ process.exit(1)
75
+ })
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "07-filter-query-mongodb-like",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "tsx app.ts"
8
+ },
9
+ "dependencies": {
10
+ "@mostajs/orm": "^2.1.0",
11
+ "better-sqlite3": "^12.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "tsx": "^4.0.0",
15
+ "typescript": "^5.6.0",
16
+ "@types/node": "^22.0.0"
17
+ }
18
+ }
@@ -0,0 +1,30 @@
1
+ // Schéma User varié pour exercer chaque FilterOperator.
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+
4
+ import type { EntitySchema } from '@mostajs/orm'
5
+
6
+ export const UserSchema: EntitySchema = {
7
+ name: 'User',
8
+ collection: 'users',
9
+ fields: {
10
+ email: { type: 'string', required: true, unique: true },
11
+ age: { type: 'number', required: true },
12
+ status: { type: 'string', enum: ['active', 'pending', 'banned', 'archived'], default: 'pending' },
13
+ tags: { type: 'array', arrayOf: { type: 'string' }, default: [] },
14
+ premium: { type: 'boolean' }, // optionnel — pour $exists
15
+ },
16
+ relations: {},
17
+ indexes: [{ fields: { email: 'asc' }, unique: true }],
18
+ timestamps: true,
19
+ }
20
+
21
+ export interface UserRow {
22
+ id: string
23
+ email: string
24
+ age: number
25
+ status?: 'active' | 'pending' | 'banned' | 'archived'
26
+ tags?: string[]
27
+ premium?: boolean
28
+ createdAt?: Date
29
+ updatedAt?: Date
30
+ }
@@ -0,0 +1,11 @@
1
+ # MongoDB requis pour ce sample : le pipeline $match/$group/$sort/$limit est
2
+ # natif Mongo. SQLite ne le supporte pas correctement (le SQL ne mappe pas
3
+ # l'expression `'$customerId'` vers une référence de colonne).
4
+ DB_DIALECT=mongodb
5
+ SGBD_URI=mongodb://devuser:devpass26@[::1]:27017/sample-08-aggregate?authSource=admin
6
+
7
+ # Mongo local sans auth (si vous avez votre propre instance) :
8
+ # SGBD_URI=mongodb://127.0.0.1:27017/sample-08-aggregate
9
+
10
+ # MongoDB Atlas cluster :
11
+ # SGBD_URI=mongodb+srv://USER:PASS@CLUSTER.mongodb.net/sample-08-aggregate
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ cd "$(dirname "$0")"
4
+
5
+ if [ ! -d node_modules ]; then
6
+ echo "─── Installing dependencies (first run) ───"
7
+ npm install --silent --no-audit --no-fund
8
+ fi
9
+
10
+ rm -f app.db
11
+ npx -y tsx app.ts
@@ -0,0 +1,74 @@
1
+ # 08-aggregate-pipeline
2
+
3
+ > Pipeline d'agrégation MongoDB-style — `$match` + `$group` + `$sort` + `$limit` avec accumulators `$sum`/`$avg`/`$count`.
4
+
5
+ **Couvre** : `aggregate`, `AggregateStage`, `AggregateGroupStage`,
6
+ `AggregateMatchStage`, `AggregateSortStage`, `AggregateLimitStage`,
7
+ `AggregateAccumulator`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ mkdir tmp && cd tmp && npm init -y && npm install @mostajs/orm-samples
13
+ cp -r node_modules/@mostajs/orm-samples/examples/08-aggregate-pipeline ~/my-aggregate-app
14
+ cd ~/my-aggregate-app
15
+ rm -rf ../tmp
16
+ ```
17
+
18
+ ## External resources
19
+
20
+ **MongoDB requis** *(pas SQLite)* — le pipeline `$match/$group/$sort/$limit`
21
+ est natif Mongo. SQLite ne mappe pas correctement l'expression `'$customerId'`
22
+ vers une référence de colonne.
23
+
24
+ ```bash
25
+ # Local (sans auth, instance déjà tournante) :
26
+ # Cf. .env.example pour les variantes.
27
+
28
+ # OR : docker
29
+ docker run -d -p 27017:27017 --name mongo mongo:7
30
+
31
+ # OR : MongoDB Atlas cloud cluster (cf. .env.example)
32
+ ```
33
+
34
+ Le `.env.example` fourni utilise par défaut une instance Mongo accessible
35
+ en `[::1]:27017` avec credentials `devuser/devpass26?authSource=admin`.
36
+ Adapter selon votre environnement.
37
+
38
+ ## Run
39
+
40
+ ```bash
41
+ ./08-aggregate-pipeline.sh
42
+ ```
43
+
44
+ ## Expected output
45
+
46
+ ```
47
+ ─── Aggregate pipeline — @mostajs/orm ───
48
+ ✓ seeded 15 orders across 3 customers, statuses [completed,pending,cancelled]
49
+ ─── Pipeline : top 3 customers par CA (completed only) ───
50
+ - cust-c total= 4500 count= 3
51
+ - cust-a total= 3000 count= 2
52
+ - cust-b total= 1500 count= 1
53
+ ✓ 3 résultats triés desc
54
+ ─── Pipeline : count par status ───
55
+ - completed 6
56
+ - pending 5
57
+ - cancelled 4
58
+ ✓ chaque status agrégé
59
+ ✅ Smoke OK — pipeline aggregate démontré.
60
+ ```
61
+
62
+ ## What it shows
63
+
64
+ - `$match` : filtrage initial avant agrégation
65
+ - `$group` avec `_id` = clé de groupement et accumulators `$sum`, `$count`
66
+ - `$sort` : tri des résultats agrégés
67
+ - `$limit` : top-N
68
+
69
+ ## Files
70
+
71
+ - `app.ts` — 2 pipelines distincts (top-N + count par status)
72
+ - `schemas/order.schema.ts` — Order avec customerId, status, amount
73
+
74
+ **Author** : Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,95 @@
1
+ // Aggregate pipeline — $match + $group + $sort + $limit (MongoDB native).
2
+ //
3
+ // Démontre :
4
+ // - aggregate() avec un AggregateStage[]
5
+ // - AggregateMatchStage : { $match: filter }
6
+ // - AggregateGroupStage : { $group: { _id: ..., field: accumulator } }
7
+ // - AggregateAccumulator : { $sum: 1 | '$field' }
8
+ // - AggregateSortStage : { $sort: { field: -1 } }
9
+ // - AggregateLimitStage : { $limit: N }
10
+ //
11
+ // Pourquoi MongoDB ici : le pipeline $match/$group/$sort/$limit est NATIF
12
+ // Mongo. SQLite ne mappe pas correctement l'expression '$customerId' vers
13
+ // une référence de colonne. Pour les dialects SQL, utiliser GROUP BY direct
14
+ // via $raw n'est pas encore standardisé — `aggregate()` reste optimal sur
15
+ // MongoDB en V1.
16
+ //
17
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
18
+
19
+ import { createConnection, BaseRepository } from '@mostajs/orm'
20
+ import { OrderSchema, type OrderRow } from './schemas/order.schema.js'
21
+
22
+ async function main(): Promise<void> {
23
+ console.log('─── Aggregate pipeline (MongoDB) — @mostajs/orm ───')
24
+
25
+ const uri = process.env.SGBD_URI
26
+ ?? 'mongodb://devuser:devpass26@[::1]:27017/sample-08-aggregate?authSource=admin'
27
+
28
+ const dialect = await createConnection(
29
+ { dialect: 'mongodb', uri, schemaStrategy: 'update' },
30
+ [OrderSchema],
31
+ )
32
+ const orders = new BaseRepository<OrderRow>(OrderSchema, dialect)
33
+
34
+ // Reset collection en mongo (équivalent du DROP TABLE en SQL).
35
+ await orders.deleteMany({})
36
+
37
+ // ── Seed : 15 commandes sur 3 clients × 3 statuses ──────────
38
+ const data: Array<Partial<OrderRow>> = [
39
+ { customerId: 'cust-a', status: 'completed', amount: 1000 },
40
+ { customerId: 'cust-a', status: 'completed', amount: 2000 },
41
+ { customerId: 'cust-a', status: 'pending', amount: 500 },
42
+ { customerId: 'cust-a', status: 'cancelled', amount: 300 },
43
+ { customerId: 'cust-b', status: 'completed', amount: 1500 },
44
+ { customerId: 'cust-b', status: 'pending', amount: 700 },
45
+ { customerId: 'cust-b', status: 'pending', amount: 400 },
46
+ { customerId: 'cust-b', status: 'cancelled', amount: 200 },
47
+ { customerId: 'cust-c', status: 'completed', amount: 1500 },
48
+ { customerId: 'cust-c', status: 'completed', amount: 1500 },
49
+ { customerId: 'cust-c', status: 'completed', amount: 1500 },
50
+ { customerId: 'cust-c', status: 'pending', amount: 800 },
51
+ { customerId: 'cust-c', status: 'pending', amount: 600 },
52
+ { customerId: 'cust-c', status: 'cancelled', amount: 100 },
53
+ { customerId: 'cust-c', status: 'cancelled', amount: 100 },
54
+ ]
55
+ for (const d of data) await orders.create(d)
56
+ console.log('✓ seeded 15 orders across 3 customers, statuses [completed,pending,cancelled]')
57
+
58
+ // ── Pipeline 1 : top-N clients par CA (completed only) ──────
59
+ console.log('─── Pipeline : top 3 customers par CA (completed only) ───')
60
+ const topCustomers = await orders.aggregate([
61
+ { $match: { status: 'completed' } },
62
+ { $group: { _id: '$customerId', total: { $sum: '$amount' }, count: { $sum: 1 } } },
63
+ { $sort: { total: -1 } },
64
+ { $limit: 3 },
65
+ ])
66
+ for (const row of topCustomers) {
67
+ console.log(` - ${String(row._id).padEnd(8)} total=${String(row.total).padStart(6)} count=${String(row.count).padStart(2)}`)
68
+ }
69
+ console.log(`✓ ${topCustomers.length} résultats triés desc`)
70
+ if (topCustomers.length !== 3) throw new Error(`expected 3 top customers, got ${topCustomers.length}`)
71
+ if (topCustomers[0]._id !== 'cust-c') throw new Error(`expected cust-c first, got ${topCustomers[0]._id}`)
72
+
73
+ // ── Pipeline 2 : count par status (sans $match) ─────────────
74
+ console.log('─── Pipeline : count par status ───')
75
+ const byStatus = await orders.aggregate([
76
+ { $group: { _id: '$status', count: { $sum: 1 } } },
77
+ { $sort: { count: -1 } },
78
+ ])
79
+ for (const row of byStatus) {
80
+ console.log(` - ${String(row._id).padEnd(12)} ${row.count}`)
81
+ }
82
+ console.log('✓ chaque status agrégé')
83
+ if (byStatus.length !== 3) throw new Error(`expected 3 status groups, got ${byStatus.length}`)
84
+
85
+ console.log('✅ Smoke OK — pipeline aggregate démontré sur MongoDB.')
86
+
87
+ // Cleanup
88
+ await orders.deleteMany({})
89
+ await dialect.disconnect?.()
90
+ }
91
+
92
+ main().catch((err) => {
93
+ console.error('❌ Sample failed:', err)
94
+ process.exit(1)
95
+ })
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "08-aggregate-pipeline",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "tsx app.ts"
8
+ },
9
+ "dependencies": {
10
+ "@mostajs/orm": "^2.1.0",
11
+ "mongoose": "^8.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "tsx": "^4.0.0",
15
+ "typescript": "^5.6.0",
16
+ "@types/node": "^22.0.0"
17
+ }
18
+ }
@@ -0,0 +1,28 @@
1
+ // Schéma Order pour agrégation : customerId + status + amount.
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+
4
+ import type { EntitySchema } from '@mostajs/orm'
5
+
6
+ export const OrderSchema: EntitySchema = {
7
+ name: 'Order',
8
+ collection: 'orders',
9
+ fields: {
10
+ customerId: { type: 'string', required: true },
11
+ status: { type: 'string', enum: ['completed', 'pending', 'cancelled'], required: true },
12
+ amount: { type: 'number', required: true },
13
+ },
14
+ relations: {},
15
+ indexes: [
16
+ { fields: { customerId: 'asc' } },
17
+ { fields: { status: 'asc' } },
18
+ ],
19
+ timestamps: true,
20
+ }
21
+
22
+ export interface OrderRow {
23
+ id: string
24
+ customerId: string
25
+ status: 'completed' | 'pending' | 'cancelled'
26
+ amount: number
27
+ createdAt?: Date
28
+ }
@@ -0,0 +1,2 @@
1
+ DB_DIALECT=sqlite
2
+ SGBD_URI=./app.db
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ cd "$(dirname "$0")"
4
+
5
+ if [ ! -d node_modules ]; then
6
+ echo "─── Installing dependencies (first run) ───"
7
+ npm install --silent --no-audit --no-fund
8
+ fi
9
+
10
+ rm -f app.db
11
+ npx -y tsx app.ts
@@ -0,0 +1,65 @@
1
+ # 09-findbyid-polymorphic
2
+
3
+ > Les 4 formes de `findById()` (string / `{id}` / natural key single / composite) + `extractRelId` + `OrmIntrospectionError`. La vitrine de la 2.0.
4
+
5
+ **Couvre** : `findById` (4 formes), `resolveLookup`, `findMatchingUniqueIndex`,
6
+ `UniqueIndexMatch`, `ResolvedLookup`, `OrmIntrospectionError`,
7
+ `extractRelId`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ mkdir tmp && cd tmp && npm init -y && npm install @mostajs/orm-samples
13
+ cp -r node_modules/@mostajs/orm-samples/examples/09-findbyid-polymorphic ~/my-polymorphic-app
14
+ cd ~/my-polymorphic-app
15
+ rm -rf ../tmp
16
+ ```
17
+
18
+ ## External resources
19
+
20
+ aucune *(SQLite via better-sqlite3)*.
21
+
22
+ ## Run
23
+
24
+ ```bash
25
+ ./09-findbyid-polymorphic.sh
26
+ ```
27
+
28
+ ## Expected output
29
+
30
+ ```
31
+ ─── findById polymorphique + extractRelId — @mostajs/orm ───
32
+ ✓ seeded : Project 'orphin' + Membership (admin sur Project 'orphin')
33
+ ─── Forme 1 : findById('id-string')
34
+ → Project name='orphin'
35
+ ─── Forme 2 : findById({ id: '…' })
36
+ → Project name='orphin'
37
+ ─── Forme 3 : findById({ slug }) — natural key single
38
+ → Project name='orphin' via unique index { slug }
39
+ ─── Forme 4 : findById({ projectId, role }) — composite natural key
40
+ → Membership found via unique index { projectId+role }
41
+ ─── Erreur typée : findById({ unknown: 'foo' })
42
+ → OrmIntrospectionError schema='Project' availableFields=[unknown]
43
+ ─── Helper extractRelId
44
+ extractRelId('abc') = 'abc'
45
+ extractRelId({ id: 'abc' }) = 'abc'
46
+ extractRelId(null) = ''
47
+ extractRelId({ slug: 'foo' }) = '' (pas d'id direct)
48
+ ✅ Smoke OK — 4 formes findById + extractRelId + error typing.
49
+ ```
50
+
51
+ ## What it shows
52
+
53
+ - **Forme 1** : compatible legacy (string PK comme avant 2.0)
54
+ - **Forme 2** : pratique quand le caller a un objet populé en main
55
+ - **Forme 3** : natural key — l'introspection résout via `unique` index
56
+ - **Forme 4** : composite — `{tenantId, slug}` etc. via index unique composite
57
+ - **Erreur actionnable** : `OrmIntrospectionError` liste les fields disponibles + unique indexes candidats
58
+ - **`extractRelId`** : helper pour comparaisons sûres en lazy ET eager
59
+
60
+ ## Files
61
+
62
+ - `app.ts` — 4 formes + extractRelId + erreur typée
63
+ - `schemas/` — Project (avec unique slug) + Membership (composite unique)
64
+
65
+ **Author** : Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,97 @@
1
+ // findById polymorphique — 4 formes + extractRelId + OrmIntrospectionError.
2
+ //
3
+ // Démontre :
4
+ // Forme 1 : findById('id-string') — PK direct (legacy)
5
+ // Forme 2 : findById({ id: '...' }) — objet contenant id
6
+ // Forme 3 : findById({ slug: '...' }) — natural key single
7
+ // Forme 4 : findById({ projectId, role }) — composite natural key
8
+ // Erreur : findById({ unknown: 'foo' }) — OrmIntrospectionError
9
+ // Helper : extractRelId(val) — normalise vers string id
10
+ //
11
+ // Introspection : resolveLookup + findMatchingUniqueIndex (utilisés en
12
+ // interne par findById). Disponibles publiquement pour debug ou outillage.
13
+ //
14
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
15
+
16
+ import {
17
+ createConnection,
18
+ BaseRepository,
19
+ extractRelId,
20
+ OrmIntrospectionError,
21
+ resolveLookup,
22
+ } from '@mostajs/orm'
23
+ import {
24
+ ProjectSchema, MembershipSchema,
25
+ type ProjectRow, type MembershipRow,
26
+ } from './schemas/index.js'
27
+
28
+ async function main(): Promise<void> {
29
+ console.log('─── findById polymorphique + extractRelId — @mostajs/orm ───')
30
+
31
+ const dialect = await createConnection(
32
+ { dialect: 'sqlite', uri: './app.db', schemaStrategy: 'create' },
33
+ [ProjectSchema, MembershipSchema],
34
+ )
35
+
36
+ const projects = new BaseRepository<ProjectRow>(ProjectSchema, dialect)
37
+ const memberships = new BaseRepository<MembershipRow>(MembershipSchema, dialect)
38
+
39
+ // ── Seed ────────────────────────────────────────────────────
40
+ const proj = await projects.create({ slug: 'orphin', name: 'orphin' })
41
+ await memberships.create({ projectId: proj.id, role: 'admin', userId: 'user-1' })
42
+ console.log("✓ seeded : Project 'orphin' + Membership (admin sur Project 'orphin')")
43
+
44
+ // ── Forme 1 : string PK direct ──────────────────────────────
45
+ console.log("─── Forme 1 : findById('id-string')")
46
+ const p1 = await projects.findById(proj.id)
47
+ console.log(` → Project name='${p1?.name}'`)
48
+ if (!p1) throw new Error('Forme 1 failed')
49
+
50
+ // ── Forme 2 : objet avec id (cas d'une relation populée) ────
51
+ console.log("─── Forme 2 : findById({ id: '…' })")
52
+ const p2 = await projects.findById({ id: proj.id })
53
+ console.log(` → Project name='${p2?.name}'`)
54
+ if (!p2 || p2.id !== proj.id) throw new Error('Forme 2 failed')
55
+
56
+ // ── Forme 3 : natural key single (slug unique) ──────────────
57
+ console.log("─── Forme 3 : findById({ slug }) — natural key single")
58
+ const p3 = await projects.findById({ slug: 'orphin' })
59
+ console.log(` → Project name='${p3?.name}' via unique index { slug }`)
60
+ if (!p3 || p3.id !== proj.id) throw new Error('Forme 3 failed')
61
+
62
+ // ── Forme 4 : composite natural key ─────────────────────────
63
+ console.log("─── Forme 4 : findById({ projectId, role }) — composite natural key")
64
+ const m4 = await memberships.findById({ projectId: proj.id, role: 'admin' })
65
+ console.log(` → Membership found via unique index { projectId+role }`)
66
+ if (!m4) throw new Error('Forme 4 failed')
67
+
68
+ // ── Erreur typée : ni id ni unique index matching ───────────
69
+ console.log("─── Erreur typée : findById({ unknown: 'foo' })")
70
+ try {
71
+ await projects.findById({ unknown: 'foo' })
72
+ throw new Error('Expected OrmIntrospectionError, none thrown')
73
+ } catch (e) {
74
+ if (!(e instanceof OrmIntrospectionError)) throw e
75
+ console.log(` → OrmIntrospectionError schema='${e.schemaName}' availableFields=[${e.availableFields.join(', ')}]`)
76
+ }
77
+
78
+ // ── extractRelId helper ─────────────────────────────────────
79
+ console.log("─── Helper extractRelId")
80
+ console.log(` extractRelId('abc') = '${extractRelId('abc')}'`)
81
+ console.log(` extractRelId({ id: 'abc' }) = '${extractRelId({ id: 'abc' })}'`)
82
+ console.log(` extractRelId(null) = '${extractRelId(null)}'`)
83
+ console.log(` extractRelId({ slug: 'foo' }) = '${extractRelId({ slug: 'foo' })}' (pas d'id direct)`)
84
+
85
+ // ── Bonus : resolveLookup public pour debug/outillage ───────
86
+ const resolved = resolveLookup(ProjectSchema, { slug: 'orphin' })
87
+ if (resolved.kind !== 'natural') throw new Error(`expected kind 'natural', got '${resolved.kind}'`)
88
+
89
+ console.log('✅ Smoke OK — 4 formes findById + extractRelId + error typing.')
90
+
91
+ await dialect.disconnect?.()
92
+ }
93
+
94
+ main().catch((err) => {
95
+ console.error('❌ Sample failed:', err)
96
+ process.exit(1)
97
+ })
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "09-findbyid-polymorphic",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "tsx app.ts"
8
+ },
9
+ "dependencies": {
10
+ "@mostajs/orm": "^2.1.0",
11
+ "better-sqlite3": "^12.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "tsx": "^4.0.0",
15
+ "typescript": "^5.6.0",
16
+ "@types/node": "^22.0.0"
17
+ }
18
+ }
@@ -0,0 +1,45 @@
1
+ // Project : unique index sur slug → findById({ slug }) résout.
2
+ // Membership : unique index composite (projectId + role) → findById({ projectId, role }) résout.
3
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
4
+
5
+ import type { EntitySchema } from '@mostajs/orm'
6
+
7
+ export const ProjectSchema: EntitySchema = {
8
+ name: 'Project',
9
+ collection: 'projects',
10
+ fields: {
11
+ slug: { type: 'string', required: true, unique: true },
12
+ name: { type: 'string', required: true },
13
+ },
14
+ relations: {},
15
+ // Index unique single — exploité par findById({ slug }).
16
+ indexes: [{ fields: { slug: 'asc' }, unique: true }],
17
+ timestamps: true,
18
+ }
19
+
20
+ export const MembershipSchema: EntitySchema = {
21
+ name: 'Membership',
22
+ collection: 'memberships',
23
+ fields: {
24
+ projectId: { type: 'string', required: true },
25
+ role: { type: 'string', enum: ['admin', 'editor', 'viewer'], required: true },
26
+ userId: { type: 'string', required: true },
27
+ },
28
+ relations: {},
29
+ // Index unique composite — exploité par findById({ projectId, role }).
30
+ indexes: [{ fields: { projectId: 'asc', role: 'asc' }, unique: true }],
31
+ timestamps: true,
32
+ }
33
+
34
+ export interface ProjectRow {
35
+ id: string
36
+ slug: string
37
+ name: string
38
+ }
39
+
40
+ export interface MembershipRow {
41
+ id: string
42
+ projectId: string
43
+ role: 'admin' | 'editor' | 'viewer'
44
+ userId: string
45
+ }
package/llms.txt CHANGED
@@ -1,8 +1,8 @@
1
1
  # @mostajs/orm-samples — fiche LLM
2
2
  > Runnable samples covering 100% of @mostajs/orm's public API. Copy-paste install per feature.
3
3
 
4
- - Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
- - Chemin: mostajs/mosta-orm-samples · Statut: Lot 1 livré (samples 01-05)
4
+ - Version: 0.2.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
+ - Chemin: mostajs/mosta-orm-samples · Statut: Lots 1 & 2 livrés (samples 01-09)
6
6
 
7
7
  ## RÔLE
8
8
  Pour chaque export documenté dans le llms.txt de @mostajs/orm, ce module fournit
@@ -30,8 +30,13 @@ npx @mostajs/orm-samples scaffold <feature> [dest]
30
30
  - 04-schema-registry : registerSchema + registerSchemas + getSchema + getSchemaByCollection + getAllSchemas + getEntityNames + hasSchema + validateSchemas + clearRegistry
31
31
  - 05-types-cles-entity-schema : EntitySchema exhaustif + FieldType (string/text/number/boolean/date/json/array) + FieldDef (required/unique/sparse/default/enum/lowercase/trim/arrayOf) + IndexDef + IndexType + softDelete + discriminator + timestamps
32
32
 
33
- ## SAMPLES À VENIR Lots 2-5
34
- - Lot 2 (CRUD & queries) : findById polymorphique, FilterOperator complet, aggregate pipeline
33
+ ## SAMPLES LIVRÉSLot 2 (CRUD & queries)
34
+ - 06-base-repository-crud : 15 méthodes BaseRepository (findAll/findOne/findById/create/update/updateMany/delete/deleteMany/count/distinct/search/upsert/increment/addToSet/pull)
35
+ - 07-filter-query-mongodb-like : 12 opérateurs FilterOperator ($eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$regex/$exists/$or/$and) + QueryOptions (sort/skip/limit/select) + SortDirection
36
+ - 08-aggregate-pipeline : aggregate + AggregateStage + $match/$group/$sort/$limit + AggregateAccumulator (MongoDB requis — pipeline natif Mongo, SQL non supporté pour $group avec expression $field)
37
+ - 09-findbyid-polymorphic : findById 4 formes (string/{id}/natural key/composite) + resolveLookup + UniqueIndexMatch + ResolvedLookup + OrmIntrospectionError (avec schemaName/availableFields) + extractRelId
38
+
39
+ ## SAMPLES À VENIR — Lots 3-5
35
40
  - Lot 3 (Relations & lifecycle) : RelationDef + lazy/eager + migration + soft-delete + audit + tx
36
41
  - Lot 4 (Plugins & sous-modules) : IPlugin + /register + /bridge JDBC
37
42
  - Lot 5 (Validator & erreurs) : validator + auto-fix + erreurs + EntityService + config + lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-samples",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Runnable samples covering 100% of @mostajs/orm's llms.txt — copy-paste install per feature.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
@@ -58,5 +58,8 @@
58
58
  "funding": {
59
59
  "type": "github",
60
60
  "url": "https://github.com/sponsors/apolocine"
61
+ },
62
+ "dependencies": {
63
+ "mongoose": "^9.6.2"
61
64
  }
62
65
  }