@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.
- package/CHANGELOG.md +31 -0
- package/README.md +14 -4
- package/examples/06-base-repository-crud/.env.example +3 -0
- package/examples/06-base-repository-crud/06-base-repository-crud.sh +11 -0
- package/examples/06-base-repository-crud/README.md +61 -0
- package/examples/06-base-repository-crud/app.ts +104 -0
- package/examples/06-base-repository-crud/package.json +18 -0
- package/examples/06-base-repository-crud/schemas/user.schema.ts +32 -0
- package/examples/07-filter-query-mongodb-like/.env.example +2 -0
- package/examples/07-filter-query-mongodb-like/07-filter-query-mongodb-like.sh +11 -0
- package/examples/07-filter-query-mongodb-like/README.md +63 -0
- package/examples/07-filter-query-mongodb-like/app.ts +75 -0
- package/examples/07-filter-query-mongodb-like/package.json +18 -0
- package/examples/07-filter-query-mongodb-like/schemas/user.schema.ts +30 -0
- package/examples/08-aggregate-pipeline/.env.example +11 -0
- package/examples/08-aggregate-pipeline/08-aggregate-pipeline.sh +11 -0
- package/examples/08-aggregate-pipeline/README.md +74 -0
- package/examples/08-aggregate-pipeline/app.ts +95 -0
- package/examples/08-aggregate-pipeline/package.json +18 -0
- package/examples/08-aggregate-pipeline/schemas/order.schema.ts +28 -0
- package/examples/09-findbyid-polymorphic/.env.example +2 -0
- package/examples/09-findbyid-polymorphic/09-findbyid-polymorphic.sh +11 -0
- package/examples/09-findbyid-polymorphic/README.md +65 -0
- package/examples/09-findbyid-polymorphic/app.ts +97 -0
- package/examples/09-findbyid-polymorphic/package.json +18 -0
- package/examples/09-findbyid-polymorphic/schemas/index.ts +45 -0
- package/llms.txt +9 -4
- 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 (
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,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,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,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,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.
|
|
5
|
-
- Chemin: mostajs/mosta-orm-samples · Statut:
|
|
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
|
|
34
|
-
-
|
|
33
|
+
## SAMPLES LIVRÉS — Lot 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.
|
|
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
|
}
|