@nodefony/orm-core 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +131 -0
- package/dist/index.js +22 -0
- package/dist/nodefony/interfaces/IEntity.js +1 -0
- package/dist/nodefony/interfaces/IOrm.js +1 -0
- package/dist/nodefony/interfaces/IOrmFlow.js +1 -0
- package/dist/nodefony/interfaces/IOrmGraph.js +1 -0
- package/dist/nodefony/interfaces/IOrmMigrations.js +12 -0
- package/dist/nodefony/interfaces/IOrmProbe.js +1 -0
- package/dist/nodefony/interfaces/IPage.js +1 -0
- package/dist/nodefony/interfaces/IRepository.js +1 -0
- package/dist/nodefony/interfaces/ITransaction.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/AbstractCrudService.js +199 -0
- package/dist/nodefony/src/ConnectionMonitor.js +181 -0
- package/dist/nodefony/src/Entity.js +42 -0
- package/dist/nodefony/src/EntityRegistry.js +109 -0
- package/dist/nodefony/src/Orm.js +297 -0
- package/dist/nodefony/src/OrmAdminApi.js +491 -0
- package/dist/nodefony/src/OrmRegistry.js +75 -0
- package/dist/nodefony/src/QueryFlowMonitor.js +128 -0
- package/dist/nodefony/src/buildOrmLeanHealth.js +54 -0
- package/dist/nodefony/src/criteria.js +176 -0
- package/dist/nodefony/src/decorators/entitiesDecorator.js +67 -0
- package/dist/nodefony/src/decorators/entityDecorator.js +51 -0
- package/dist/nodefony/src/decorators/index.js +5 -0
- package/dist/nodefony/src/decorators/metadataStore.js +38 -0
- package/dist/nodefony/src/decorators/repositoryDecorator.js +35 -0
- package/dist/nodefony/src/defineEntity.js +27 -0
- package/dist/nodefony/src/errors.js +76 -0
- package/dist/nodefony/src/ormWiring.js +78 -0
- package/dist/nodefony/src/paginate.js +55 -0
- package/dist/nodefony/src/readOptions.js +52 -0
- package/dist/nodefony/src/serviceWiring.js +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IEntity.d.ts +65 -0
- package/dist/types/nodefony/interfaces/IOrm.d.ts +136 -0
- package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +71 -0
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +165 -0
- package/dist/types/nodefony/interfaces/IOrmMigrations.d.ts +145 -0
- package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +70 -0
- package/dist/types/nodefony/interfaces/IPage.d.ts +24 -0
- package/dist/types/nodefony/interfaces/IRepository.d.ts +369 -0
- package/dist/types/nodefony/interfaces/ITransaction.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/src/AbstractCrudService.d.ts +158 -0
- package/dist/types/nodefony/src/ConnectionMonitor.d.ts +86 -0
- package/dist/types/nodefony/src/Entity.d.ts +44 -0
- package/dist/types/nodefony/src/EntityRegistry.d.ts +60 -0
- package/dist/types/nodefony/src/Orm.d.ts +197 -0
- package/dist/types/nodefony/src/OrmAdminApi.d.ts +84 -0
- package/dist/types/nodefony/src/OrmRegistry.d.ts +58 -0
- package/dist/types/nodefony/src/QueryFlowMonitor.d.ts +53 -0
- package/dist/types/nodefony/src/buildOrmLeanHealth.d.ts +15 -0
- package/dist/types/nodefony/src/criteria.d.ts +131 -0
- package/dist/types/nodefony/src/decorators/entitiesDecorator.d.ts +48 -0
- package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +49 -0
- package/dist/types/nodefony/src/decorators/index.d.ts +10 -0
- package/dist/types/nodefony/src/decorators/metadataStore.d.ts +54 -0
- package/dist/types/nodefony/src/decorators/repositoryDecorator.d.ts +30 -0
- package/dist/types/nodefony/src/defineEntity.d.ts +43 -0
- package/dist/types/nodefony/src/errors.d.ts +62 -0
- package/dist/types/nodefony/src/ormWiring.d.ts +48 -0
- package/dist/types/nodefony/src/paginate.d.ts +43 -0
- package/dist/types/nodefony/src/readOptions.d.ts +21 -0
- package/dist/types/nodefony/src/serviceWiring.d.ts +24 -0
- package/docs/index.md +791 -0
- package/docs/tutorial-entity.md +577 -0
- package/package.json +73 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ormRegistry } from "./OrmRegistry.js";
|
|
2
|
+
import { connectionMonitor } from "./ConnectionMonitor.js";
|
|
3
|
+
import { queryFlowMonitor } from "./QueryFlowMonitor.js";
|
|
4
|
+
//#region nodefony/src/buildOrmLeanHealth.ts
|
|
5
|
+
/**
|
|
6
|
+
* **Santé ORM lean per-instance** — somme process-wide de tous les connecteurs enregistrés,
|
|
7
|
+
* destinée à voyager dans le report de sonde cluster (« ORM par worker »). C'est la fonction
|
|
8
|
+
* branchée par le driver via `setOrmHealthProvider` (core) ; le framework la lit dans le report.
|
|
9
|
+
*
|
|
10
|
+
* Perf (règle ABSOLUE) : lecture PURE des singletons déjà alimentés (`queryFlowMonitor` +
|
|
11
|
+
* `connectionMonitor`) → **0 ping**, **0 `toSQL()`**, O(N connecteurs, N petit). `isConnected()`
|
|
12
|
+
* est un simple test d'état (pas une requête). `queryTotal` reste à 0 si le flux est OFF
|
|
13
|
+
* (prod) — c'est voulu : la sonde ne crée aucun coût, elle agrège ce qui existe déjà.
|
|
14
|
+
*
|
|
15
|
+
* @returns la santé ORM agrégée du process ({@link IOrmLeanHealth}).
|
|
16
|
+
*/
|
|
17
|
+
function buildOrmLeanHealth() {
|
|
18
|
+
const names = ormRegistry.list();
|
|
19
|
+
let connected = 0;
|
|
20
|
+
let assumed = 0;
|
|
21
|
+
let queryTotal = 0;
|
|
22
|
+
let slowTotal = 0;
|
|
23
|
+
let errorTotal = 0;
|
|
24
|
+
let reconnectTotal = 0;
|
|
25
|
+
let maxEwmaMs = null;
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
try {
|
|
28
|
+
const orm = ormRegistry.get(name);
|
|
29
|
+
if (orm.isConnected()) {
|
|
30
|
+
connected += 1;
|
|
31
|
+
if (orm.liveness !== "events") assumed += 1;
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
const conn = connectionMonitor.snapshot(name);
|
|
35
|
+
errorTotal += conn.errorCount;
|
|
36
|
+
reconnectTotal += conn.reconnectCount;
|
|
37
|
+
const flow = queryFlowMonitor.snapshot(name, "");
|
|
38
|
+
queryTotal += flow.total;
|
|
39
|
+
slowTotal += flow.slowTotal;
|
|
40
|
+
if (flow.ewmaMs !== null && (maxEwmaMs === null || flow.ewmaMs > maxEwmaMs)) maxEwmaMs = flow.ewmaMs;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
connectors: names.length,
|
|
44
|
+
connected,
|
|
45
|
+
assumed,
|
|
46
|
+
queryTotal,
|
|
47
|
+
slowTotal,
|
|
48
|
+
errorTotal,
|
|
49
|
+
reconnectTotal,
|
|
50
|
+
maxEwmaMs
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { buildOrmLeanHealth, buildOrmLeanHealth as default };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//#region nodefony/src/criteria.ts
|
|
2
|
+
/**
|
|
3
|
+
* Liste figée des opérateurs riches reconnus dans un critère portable.
|
|
4
|
+
*
|
|
5
|
+
* Source de vérité unique partagée par tous les adapters : un objet de valeur de
|
|
6
|
+
* champ n'est traité comme {@link FieldOperators} que si **toutes** ses clés
|
|
7
|
+
* appartiennent à cette liste (cf {@link isFieldOperators}).
|
|
8
|
+
*/
|
|
9
|
+
const OPERATOR_KEYS = [
|
|
10
|
+
"$eq",
|
|
11
|
+
"$ne",
|
|
12
|
+
"$gt",
|
|
13
|
+
"$gte",
|
|
14
|
+
"$lt",
|
|
15
|
+
"$lte",
|
|
16
|
+
"$in",
|
|
17
|
+
"$nin",
|
|
18
|
+
"$like",
|
|
19
|
+
"$null"
|
|
20
|
+
];
|
|
21
|
+
const OPERATOR_SET = new Set(OPERATOR_KEYS);
|
|
22
|
+
/**
|
|
23
|
+
* Indique si une valeur de champ est un objet d'{@link FieldOperators}.
|
|
24
|
+
*
|
|
25
|
+
* Heuristique : objet simple, non-`null`, non-tableau, dont **toutes** les clés
|
|
26
|
+
* propres sont des opérateurs reconnus (et au moins une). Une valeur objet
|
|
27
|
+
* « ordinaire » (colonne JSON, sous-document) n'a pas que des clés `$`-préfixées
|
|
28
|
+
* reconnues → elle est traitée comme une égalité, jamais comme un filtre riche.
|
|
29
|
+
*
|
|
30
|
+
* @param value - valeur de critère associée à un champ.
|
|
31
|
+
* @returns `true` si `value` doit être interprétée comme des opérateurs.
|
|
32
|
+
*/
|
|
33
|
+
function isFieldOperators(value) {
|
|
34
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
35
|
+
const keys = Object.keys(value);
|
|
36
|
+
if (keys.length === 0) return false;
|
|
37
|
+
for (const key of keys) if (!OPERATOR_SET.has(key)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Liste figée des opérateurs d'**écriture** reconnus dans le `update` d'un
|
|
42
|
+
* `upsert` (cf {@link UpdateOperators}).
|
|
43
|
+
*
|
|
44
|
+
* Source de vérité unique partagée par tous les adapters — pendant, côté
|
|
45
|
+
* écriture, de {@link OPERATOR_KEYS}.
|
|
46
|
+
*/
|
|
47
|
+
const UPDATE_OPERATOR_KEYS = ["$max", "$min"];
|
|
48
|
+
const UPDATE_OPERATOR_SET = new Set(UPDATE_OPERATOR_KEYS);
|
|
49
|
+
/**
|
|
50
|
+
* Indique si une valeur d'écriture est un objet d'{@link UpdateOperators}.
|
|
51
|
+
*
|
|
52
|
+
* Même heuristique que {@link isFieldOperators} : objet simple, non-`null`,
|
|
53
|
+
* non-tableau, dont **toutes** les clés propres sont des opérateurs d'écriture
|
|
54
|
+
* reconnus (et au moins une). Une valeur objet « ordinaire » (colonne JSON,
|
|
55
|
+
* sous-document) est donc écrite telle quelle, jamais interprétée.
|
|
56
|
+
*
|
|
57
|
+
* @param value - valeur d'écriture associée à un champ.
|
|
58
|
+
* @returns `true` si `value` doit être interprétée comme des opérateurs.
|
|
59
|
+
*/
|
|
60
|
+
function isUpdateOperators(value) {
|
|
61
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
62
|
+
const keys = Object.keys(value);
|
|
63
|
+
if (keys.length === 0) return false;
|
|
64
|
+
for (const key of keys) if (!UPDATE_OPERATOR_SET.has(key)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Le caractère d'échappement des motifs `$like` du contrat portable.
|
|
69
|
+
*
|
|
70
|
+
* Il ne se choisit pas librement : **PostgreSQL et MySQL appliquent déjà `\`**
|
|
71
|
+
* quand aucune clause `ESCAPE` n'est écrite, si bien qu'un motif portant un
|
|
72
|
+
* antislash se comportait DÉJÀ différemment selon le moteur (SQLite, lui, n'a
|
|
73
|
+
* aucun échappement par défaut : il cherchait l'antislash littéral). Fixer `\`
|
|
74
|
+
* et l'émettre explicitement ne change donc pas la sémantique du contrat — cela
|
|
75
|
+
* la fait exister, en alignant les trois moteurs sur celui des deux
|
|
76
|
+
* comportements qui était déjà majoritaire.
|
|
77
|
+
*
|
|
78
|
+
* @see {@link escapeLikeTerm} pour construire un motif, {@link likePatternToRegExp}
|
|
79
|
+
* pour l'interpréter là où il n'y a pas de SQL (Mongo, mémoire).
|
|
80
|
+
*/
|
|
81
|
+
const LIKE_ESCAPE_CHAR = "\\";
|
|
82
|
+
/**
|
|
83
|
+
* Neutralise les métacaractères d'un **texte** pour l'insérer dans un motif
|
|
84
|
+
* `$like` — `%`, `_` et l'antislash lui-même.
|
|
85
|
+
*
|
|
86
|
+
* À utiliser dès qu'un fragment de motif vient d'un humain ou d'une donnée :
|
|
87
|
+
* sans elle, chercher `50%` demande « 50 suivi de n'importe quoi », et chercher
|
|
88
|
+
* `a_b` ramène `axb`. L'utilisateur ne lit pas ça comme une imprécision, il le
|
|
89
|
+
* lit comme un résultat.
|
|
90
|
+
*
|
|
91
|
+
* L'échappement n'a de valeur que si la clause `ESCAPE` correspondante est
|
|
92
|
+
* ÉMISE : un motif échappé sans elle est cherché littéralement, antislash
|
|
93
|
+
* compris, et ne rend plus rien — en silence. C'est pourquoi les deux vont
|
|
94
|
+
* ensemble et vivent ici, et non chez chaque appelant.
|
|
95
|
+
*
|
|
96
|
+
* @param text - le fragment littéral à insérer dans un motif.
|
|
97
|
+
* @returns le même texte, ses métacaractères neutralisés.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* { discount: { $like: `${escapeLikeTerm("50%")}%` } } // → "50\%%"
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
function escapeLikeTerm(text) {
|
|
105
|
+
return text.replace(/[\\%_]/g, (c) => "\\" + c);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Traduit un motif `$like` en expression régulière **ancrée** — pour les stores
|
|
109
|
+
* qui n'ont pas de `LIKE` (MongoDB, implémentations en mémoire).
|
|
110
|
+
*
|
|
111
|
+
* C'est la contrepartie exacte de ce qu'un moteur SQL fait avec
|
|
112
|
+
* `LIKE … ESCAPE '\'` : `%` vaut « n'importe quelle suite », `_` « un
|
|
113
|
+
* caractère », et un caractère précédé de {@link LIKE_ESCAPE_CHAR} vaut
|
|
114
|
+
* lui-même. Sans cette lecture, un adapter documentaire rendrait des résultats
|
|
115
|
+
* différents d'un adapter SQL pour le même critère portable — la divergence la
|
|
116
|
+
* plus coûteuse qui soit, puisqu'elle ne se voit qu'en changeant de backend.
|
|
117
|
+
*
|
|
118
|
+
* Un antislash final sans caractère à échapper est traité comme un antislash
|
|
119
|
+
* littéral, comme le font les moteurs SQL.
|
|
120
|
+
*
|
|
121
|
+
* @param pattern - le motif du contrat (`préfixe%`, `a\_b`…).
|
|
122
|
+
* @returns une `RegExp` ancrée aux deux bouts.
|
|
123
|
+
*/
|
|
124
|
+
function likePatternToRegExp(pattern) {
|
|
125
|
+
let source = "";
|
|
126
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
127
|
+
const c = pattern[i];
|
|
128
|
+
if (c === "\\" && i + 1 < pattern.length) source += escapeRegExpChar(pattern[++i]);
|
|
129
|
+
else if (c === "%") source += ".*";
|
|
130
|
+
else if (c === "_") source += ".";
|
|
131
|
+
else source += escapeRegExpChar(c);
|
|
132
|
+
}
|
|
133
|
+
return new RegExp(`^${source}$`);
|
|
134
|
+
}
|
|
135
|
+
/** Neutralise UN caractère pour l'insérer dans une expression régulière. */
|
|
136
|
+
function escapeRegExpChar(c) {
|
|
137
|
+
return /[.*+?^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Traduit un terme de recherche `?q=` en critère portable — **la** règle de
|
|
141
|
+
* recherche d'orm-core, en un seul exemplaire.
|
|
142
|
+
*
|
|
143
|
+
* Elle existe parce qu'elle était écrite plusieurs fois, presque à l'identique,
|
|
144
|
+
* dans des stores qui n'avaient aucun test dessus.
|
|
145
|
+
*
|
|
146
|
+
* **Le motif est ANCRÉ À GAUCHE** (`préfixe%`), donc **indexable**. Une
|
|
147
|
+
* recherche `%terme%` interdit tout usage d'index et impose un balayage
|
|
148
|
+
* complet : plus « pratique » sur dix lignes, intenable sur un million. C'est un
|
|
149
|
+
* choix de conception — un besoin de sous-chaîne relève d'un index plein-texte,
|
|
150
|
+
* pas de `LIKE`.
|
|
151
|
+
*
|
|
152
|
+
* Plusieurs champs deviennent un `$or` ; un seul reste un critère plat (les
|
|
153
|
+
* deux formes sont équivalentes pour les adapters, la seconde est plus lisible
|
|
154
|
+
* dans les journaux de requêtes).
|
|
155
|
+
*
|
|
156
|
+
* Le terme est **échappé** ({@link escapeLikeTerm}) : chercher `50%` cherche
|
|
157
|
+
* « 50% », et `a_b` ne ramène pas `axb`. Ça n'a été possible qu'une fois la
|
|
158
|
+
* clause `ESCAPE` émise par la traduction de `$like` — auparavant un terme
|
|
159
|
+
* échappé était cherché littéralement, antislash compris, et ne rendait plus
|
|
160
|
+
* rien du tout, en silence. Les deux gestes sont indissociables : c'est pourquoi
|
|
161
|
+
* ils vivent dans le même fichier.
|
|
162
|
+
*
|
|
163
|
+
* @param q - le terme saisi, déjà trimé par `parsePageQuery`.
|
|
164
|
+
* @param fields - les champs sur lesquels chercher, en noms de propriétés.
|
|
165
|
+
* @returns le critère à fusionner, ou `null` si le terme est vide ou qu'aucun
|
|
166
|
+
* champ n'est déclaré — l'appelant décide alors quoi faire de `q`.
|
|
167
|
+
*/
|
|
168
|
+
function searchCriteria(q, fields) {
|
|
169
|
+
const term = q?.trim();
|
|
170
|
+
if (!term || fields.length === 0) return null;
|
|
171
|
+
const likePattern = `${escapeLikeTerm(term)}%`;
|
|
172
|
+
if (fields.length === 1) return { [fields[0]]: { $like: likePattern } };
|
|
173
|
+
return { $or: fields.map((f) => ({ [f]: { $like: likePattern } })) };
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
export { LIKE_ESCAPE_CHAR, OPERATOR_KEYS, UPDATE_OPERATOR_KEYS, escapeLikeTerm, isFieldOperators, isUpdateOperators, likePatternToRegExp, searchCriteria };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { entityRegistry } from "../EntityRegistry.js";
|
|
2
|
+
//#region nodefony/src/decorators/entitiesDecorator.ts
|
|
3
|
+
/** Connecteur retenu quand ni l'entité ni le décorateur n'en nomment un. */
|
|
4
|
+
const DEFAULT_CONNECTOR = "default";
|
|
5
|
+
/**
|
|
6
|
+
* Déclare les entités qu'un module apporte — l'équivalent de `@controllers([...])`
|
|
7
|
+
* pour la couche données.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* @entities([PostEntity, CommentEntity])
|
|
11
|
+
* @controllers([PostController])
|
|
12
|
+
* class Blog extends Module { … }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* **Pourquoi ce décorateur existe** : l'inscription d'une entité est impérative
|
|
16
|
+
* (`entityRegistry.register()`), et il n'y a aucune découverte automatique. Sans lui,
|
|
17
|
+
* une application n'a **aucun endroit** où déclarer ses entités : elle dépend d'un
|
|
18
|
+
* import à effet de bord, où un fichier simplement oublié donne une entité
|
|
19
|
+
* silencieusement absente — l'erreur n'apparaît qu'au premier `getRepository()`, loin
|
|
20
|
+
* de sa cause. Une liste, elle, se lit.
|
|
21
|
+
*
|
|
22
|
+
* **Phase `onRegister`, jamais `onBoot`** (piège) : les connecteurs se branchent à
|
|
23
|
+
* `onBoot` et créent les tables à ce moment-là. Enregistrer les entités à `onBoot`,
|
|
24
|
+
* comme le fait `@controllers`, en ferait une **course** avec le `connect()` : selon
|
|
25
|
+
* l'ordre des écouteurs, la table n'existerait pas. `onRegister` est strictement
|
|
26
|
+
* antérieur — sûr par construction.
|
|
27
|
+
*
|
|
28
|
+
* **Idempotent** : une entité déjà inscrite pour le même connecteur est ignorée (un
|
|
29
|
+
* module peut être instancié deux fois dans un même processus — tests, rechargement).
|
|
30
|
+
* Une **collision réelle** (deux entités différentes, même nom, même connecteur) reste
|
|
31
|
+
* une erreur levée par le registre : c'est un conflit de modèle, pas un doublon bénin.
|
|
32
|
+
*
|
|
33
|
+
* @param list - descripteurs produits par `defineEntity()` (un seul ou un tableau).
|
|
34
|
+
* @param options - connecteur cible commun.
|
|
35
|
+
* @returns le décorateur de classe `Module`.
|
|
36
|
+
*/
|
|
37
|
+
function entities(list, options = {}) {
|
|
38
|
+
const definitions = Array.isArray(list) ? list : [list];
|
|
39
|
+
return function(constructor) {
|
|
40
|
+
class NewConstructorEntities extends constructor {
|
|
41
|
+
constructor(...args) {
|
|
42
|
+
super(...args);
|
|
43
|
+
this.kernel?.once("onRegister", () => {
|
|
44
|
+
this.initDecoratorEntities();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Inscrit les entités déclarées dans le registre, avant toute connexion ORM. */
|
|
48
|
+
initDecoratorEntities() {
|
|
49
|
+
for (const definition of definitions) {
|
|
50
|
+
const connector = definition.connector ?? options.connector ?? "default";
|
|
51
|
+
if (entityRegistry.has(definition.name, connector)) {
|
|
52
|
+
this.log(`ENTITY ${definition.name} (${connector}) déjà enregistrée — ignorée`, "DEBUG");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
entityRegistry.register({
|
|
56
|
+
...definition,
|
|
57
|
+
connector
|
|
58
|
+
});
|
|
59
|
+
this.log(`ADD ENTITY : ${definition.name} (connector ${connector})`, "DEBUG");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return NewConstructorEntities;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { DEFAULT_CONNECTOR, entities };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { entityRegistry } from "../EntityRegistry.js";
|
|
2
|
+
import { setEntityMeta } from "./metadataStore.js";
|
|
3
|
+
//#region nodefony/src/decorators/entityDecorator.ts
|
|
4
|
+
/**
|
|
5
|
+
* Décore une classe comme entité multi-ORM et l'enregistre au chargement.
|
|
6
|
+
*
|
|
7
|
+
* Résout le piège d'ordre d'initialisation TS (le constructeur de base
|
|
8
|
+
* s'exécute avant les initialiseurs de champs de la sous-classe) : le décorateur
|
|
9
|
+
* s'exécute sur la **classe** au chargement du module, donc `name`/`connector`
|
|
10
|
+
* sont connus sans instance. Il construit un **descripteur {@link IEntity} depuis
|
|
11
|
+
* les options** (aucune instanciation au boot), l'enregistre dans le
|
|
12
|
+
* `entityRegistry` process-wide, et stocke la métadonnée via un `WeakMap`
|
|
13
|
+
* (cf. `metadataStore`, sans `reflect-metadata`).
|
|
14
|
+
*
|
|
15
|
+
* @param options - connecteur cible + nom/schéma/relations optionnels.
|
|
16
|
+
* @returns le décorateur de classe (renvoie la classe inchangée).
|
|
17
|
+
* @throws si une entité de même `name` est déjà enregistrée sur le même `connector`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* \@entity({ connector: "db_principale", schema: { id: { type: "uuid" } } })
|
|
22
|
+
* class User extends Entity { ... }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function entity(options) {
|
|
26
|
+
return (target) => {
|
|
27
|
+
const name = options.name ?? target.name;
|
|
28
|
+
const meta = {
|
|
29
|
+
name,
|
|
30
|
+
connector: options.connector,
|
|
31
|
+
module: options.module,
|
|
32
|
+
domain: options.domain,
|
|
33
|
+
schema: options.schema,
|
|
34
|
+
relations: options.relations,
|
|
35
|
+
target
|
|
36
|
+
};
|
|
37
|
+
setEntityMeta(target, meta);
|
|
38
|
+
const descriptor = {
|
|
39
|
+
name,
|
|
40
|
+
connector: options.connector,
|
|
41
|
+
module: options.module,
|
|
42
|
+
domain: options.domain,
|
|
43
|
+
schema: options.schema,
|
|
44
|
+
relations: options.relations
|
|
45
|
+
};
|
|
46
|
+
entityRegistry.register(descriptor);
|
|
47
|
+
return target;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { entity };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta } from "./metadataStore.js";
|
|
2
|
+
import { entity } from "./entityDecorator.js";
|
|
3
|
+
import { DEFAULT_CONNECTOR, entities } from "./entitiesDecorator.js";
|
|
4
|
+
import { repository } from "./repositoryDecorator.js";
|
|
5
|
+
export { DEFAULT_CONNECTOR, entities, entity, getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, repository };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region nodefony/src/decorators/metadataStore.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stockage des métadonnées de décorateurs SANS `reflect-metadata`.
|
|
4
|
+
*
|
|
5
|
+
* orm-core est une lib pure : aucune dépendance runtime hors `nodefony`. Les
|
|
6
|
+
* décorateurs `@entity`/`@repository` ne lisent jamais les types émis par TS
|
|
7
|
+
* (`design:paramtypes`), donc un `WeakMap<classe, métadonnée>` process-wide
|
|
8
|
+
* suffit — clé = constructeur, GC-friendly (pas de fuite si la classe est
|
|
9
|
+
* déréférencée). Aucun polyfill Reflect requis.
|
|
10
|
+
*/
|
|
11
|
+
const ENTITY_META = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
const REPOSITORY_META = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
/** Enregistre la métadonnée `@entity` d'une classe. */
|
|
14
|
+
function setEntityMeta(target, meta) {
|
|
15
|
+
ENTITY_META.set(target, meta);
|
|
16
|
+
}
|
|
17
|
+
/** Récupère la métadonnée `@entity` d'une classe, ou `undefined`. */
|
|
18
|
+
function getEntityMeta(target) {
|
|
19
|
+
return ENTITY_META.get(target);
|
|
20
|
+
}
|
|
21
|
+
/** Indique si une classe porte une métadonnée `@entity`. */
|
|
22
|
+
function hasEntityMeta(target) {
|
|
23
|
+
return ENTITY_META.has(target);
|
|
24
|
+
}
|
|
25
|
+
/** Enregistre la métadonnée `@repository` d'une classe. */
|
|
26
|
+
function setRepositoryMeta(target, meta) {
|
|
27
|
+
REPOSITORY_META.set(target, meta);
|
|
28
|
+
}
|
|
29
|
+
/** Récupère la métadonnée `@repository` d'une classe, ou `undefined`. */
|
|
30
|
+
function getRepositoryMeta(target) {
|
|
31
|
+
return REPOSITORY_META.get(target);
|
|
32
|
+
}
|
|
33
|
+
/** Indique si une classe porte une métadonnée `@repository`. */
|
|
34
|
+
function hasRepositoryMeta(target) {
|
|
35
|
+
return REPOSITORY_META.has(target);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, setEntityMeta, setRepositoryMeta };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { setRepositoryMeta } from "./metadataStore.js";
|
|
2
|
+
//#region nodefony/src/decorators/repositoryDecorator.ts
|
|
3
|
+
/**
|
|
4
|
+
* Décore une classe comme repository d'une entité (tag métadonnée pur).
|
|
5
|
+
*
|
|
6
|
+
* Stocke le lien repo↔entity dans un `WeakMap` (cf. `metadataStore`, sans
|
|
7
|
+
* `reflect-metadata`). N'enregistre RIEN dans un registre en P5.3 : le binding
|
|
8
|
+
* DI (`@Inject('repository.user.db_principale')`) est câblé par l'adapter ORM
|
|
9
|
+
* concret (P5.4+), qui scanne ces métadonnées. Coût uniquement au chargement
|
|
10
|
+
* du module — jamais par requête.
|
|
11
|
+
*
|
|
12
|
+
* @param name - nom logique du repository (clé DI, ex. `"repository.user"`).
|
|
13
|
+
* @param options - entité gérée + connecteur cible optionnel.
|
|
14
|
+
* @returns le décorateur de classe (renvoie la classe inchangée).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* \@repository("repository.user", { entity: "User", connector: "db_principale" })
|
|
19
|
+
* class UserRepository implements IRepository<User> { ... }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function repository(name, options) {
|
|
23
|
+
return (target) => {
|
|
24
|
+
const meta = {
|
|
25
|
+
name,
|
|
26
|
+
entity: options.entity,
|
|
27
|
+
connector: options.connector,
|
|
28
|
+
target
|
|
29
|
+
};
|
|
30
|
+
setRepositoryMeta(target, meta);
|
|
31
|
+
return target;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { repository };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region nodefony/src/defineEntity.ts
|
|
2
|
+
/**
|
|
3
|
+
* Déclare une entité applicative. Fonction d'**identité typée** : elle ne fait
|
|
4
|
+
* qu'attacher le type — aucun effet de bord, aucun enregistrement.
|
|
5
|
+
*
|
|
6
|
+
* L'enregistrement est le rôle du décorateur `entities([...])` posé sur le Module,
|
|
7
|
+
* qui s'exécute à la phase `onRegister` (avant que l'ORM ne se connecte). Séparer
|
|
8
|
+
* les deux permet d'importer une entité (dans un test, un script, un autre module)
|
|
9
|
+
* sans déclencher son inscription dans un registre global.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const PostEntity = defineEntity({
|
|
14
|
+
* name: "Post",
|
|
15
|
+
* module: "blog",
|
|
16
|
+
* schema: postTable, // table Drizzle native (ou SchemaDefinition Mongoose)
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @param definition - descripteur (nom logique, schéma natif, module, relations…).
|
|
21
|
+
* @returns le descripteur, tel quel.
|
|
22
|
+
*/
|
|
23
|
+
function defineEntity(definition) {
|
|
24
|
+
return definition;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { defineEntity };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region nodefony/src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Erreurs typées du socle ORM — **data-level uniquement** (aucun couplage à une
|
|
4
|
+
* couche API/transport : ces erreurs décrivent des fautes sur les données, pas
|
|
5
|
+
* sur une requête HTTP/GraphQL). Une surface API qui les rencontre décide
|
|
6
|
+
* elle-même comment les projeter (400, erreur GraphQL…), l'ORM n'en sait rien.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Levée quand un critère de repository référence un **champ inconnu** de l'entité
|
|
10
|
+
* ciblée.
|
|
11
|
+
*
|
|
12
|
+
* Garde-fou « ultra solide » + **portabilité** : sans elle, un champ inconnu est
|
|
13
|
+
* traité différemment selon l'adapter — Drizzle l'**ignore** (la condition
|
|
14
|
+
* disparaît → la requête peut renvoyer **toute** la table), Mongoose le **garde**
|
|
15
|
+
* (→ **0 résultat**). Le même critère mal typé donnerait donc des résultats
|
|
16
|
+
* opposés selon l'ORM (rupture de la promesse « swap d'ORM ») et une faute de
|
|
17
|
+
* frappe (`emial`) passerait silencieusement. On échoue **tôt et pareil** sur les
|
|
18
|
+
* deux drivers.
|
|
19
|
+
*
|
|
20
|
+
* Les requêtes natives/calculées (opérateurs logiques `$or`, sous-requêtes…) ne
|
|
21
|
+
* relèvent **pas** du critère portable : passer par `IOrm.getNativeConnection()`.
|
|
22
|
+
*/
|
|
23
|
+
var UnknownCriteriaField = class extends Error {
|
|
24
|
+
/** Champ fautif (clé du critère non résolue sur l'entité). */
|
|
25
|
+
field;
|
|
26
|
+
/** Nom logique de l'entité ciblée. */
|
|
27
|
+
entity;
|
|
28
|
+
/** Champs connus de l'entité (aide au diagnostic / faute de frappe). */
|
|
29
|
+
known;
|
|
30
|
+
/**
|
|
31
|
+
* @param field - champ inconnu rencontré dans le critère.
|
|
32
|
+
* @param entity - entité ciblée (nom logique).
|
|
33
|
+
* @param known - champs connus de l'entité.
|
|
34
|
+
*/
|
|
35
|
+
constructor(field, entity, known) {
|
|
36
|
+
super(`Unknown criteria field "${field}" on entity "${entity}". Known fields: ${known.join(", ")}. For native/computed queries (logical $or, sub-queries…), use getNativeConnection().`);
|
|
37
|
+
this.name = "UnknownCriteriaField";
|
|
38
|
+
this.field = field;
|
|
39
|
+
this.entity = entity;
|
|
40
|
+
this.known = known;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Levée quand l'option de lecture `order` n'est pas un tableau de couples
|
|
45
|
+
* `[champ, "ASC" | "DESC"]`.
|
|
46
|
+
*
|
|
47
|
+
* Symétrique de {@link UnknownCriteriaField}, sous la même règle « jamais un skip
|
|
48
|
+
* silencieux » : avant cette garde, une forme voisine mais fausse — `{ age: "asc" }`
|
|
49
|
+
* au lieu de `[["age", "ASC"]]` — faisait partir la requête **sans `ORDER BY`**, et
|
|
50
|
+
* l'appelant recevait des lignes non triées qu'il croyait triées. Le sens est vérifié
|
|
51
|
+
* en casse exacte : un `"desc"` minuscule aurait produit un tri **ASC**, soit
|
|
52
|
+
* l'inverse de l'intention.
|
|
53
|
+
*
|
|
54
|
+
* TypeScript attrape déjà le cas au typage ; la surface réelle est l'appel non typé
|
|
55
|
+
* ou construit dynamiquement — exactement là où un tri faux ne se voit pas. La
|
|
56
|
+
* normalisation d'une entrée utilisateur (casse, `champ:sens`) appartient à la
|
|
57
|
+
* frontière qui la reçoit (`parsePageQuery`), jamais au repository.
|
|
58
|
+
*/
|
|
59
|
+
var InvalidOrderOption = class extends Error {
|
|
60
|
+
/** Nom logique de l'entité ciblée. */
|
|
61
|
+
entity;
|
|
62
|
+
/** Ce qui a été reçu, décrit par sa FORME (jamais la valeur : pas de fuite de données). */
|
|
63
|
+
received;
|
|
64
|
+
/**
|
|
65
|
+
* @param entity - entité ciblée (nom logique).
|
|
66
|
+
* @param received - description de la forme reçue (ex. `an object`, `pair #0 is not an array`).
|
|
67
|
+
*/
|
|
68
|
+
constructor(entity, received) {
|
|
69
|
+
super(`Invalid "order" option on entity "${entity}": expected an array of [field, "ASC" | "DESC"] pairs, received ${received}. Example: { order: [["createdAt", "DESC"]] }.`);
|
|
70
|
+
this.name = "InvalidOrderOption";
|
|
71
|
+
this.entity = entity;
|
|
72
|
+
this.received = received;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { InvalidOrderOption, UnknownCriteriaField };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ormRegistry } from "./OrmRegistry.js";
|
|
2
|
+
import { buildConnectionHealth, buildOrmFlow, registerOrmAdminApi } from "./OrmAdminApi.js";
|
|
3
|
+
import { buildOrmLeanHealth } from "./buildOrmLeanHealth.js";
|
|
4
|
+
import { setOrmHealthProvider, setOrmRichProvider } from "nodefony";
|
|
5
|
+
//#region nodefony/src/ormWiring.ts
|
|
6
|
+
/**
|
|
7
|
+
* Branche le « plan d'administration » ORM dans le kernel — montage **idempotent**
|
|
8
|
+
* appelé par CHAQUE driver à son `onKernelBoot`. Factorise la dette C5 : le bloc
|
|
9
|
+
* était jusqu'ici recopié à l'identique dans chaque module driver (Drizzle,
|
|
10
|
+
* Mongoose), donc voué à diverger ; il vit désormais à un seul endroit.
|
|
11
|
+
*
|
|
12
|
+
* Trois branchements, tous **GLOBAUX** (ils itèrent `ormRegistry`) → couvrent
|
|
13
|
+
* TOUS les ORM enregistrés, peu importe le driver appelant ; idempotents
|
|
14
|
+
* (« dernier gagne ») → sûr d'être invoqué par N drivers :
|
|
15
|
+
* 1. `registerOrmAdminApi` (si un broker admin est présent) — monte les routes
|
|
16
|
+
* data plane `/nodefony/orm/api/*` ;
|
|
17
|
+
* 2. `setOrmHealthProvider` — santé ORM lean dans la sonde cluster (par worker) ;
|
|
18
|
+
* 3. `setOrmRichProvider` — diagnostic riche (connexion + flux) pour le drill Studio.
|
|
19
|
+
*
|
|
20
|
+
* Les seams `setOrm*Provider` matérialisent l'inversion de dépendance : le core
|
|
21
|
+
* expose la prise, orm-core fournit l'implémentation agnostique
|
|
22
|
+
* (0 dépendance `framework` → `orm-core`).
|
|
23
|
+
*
|
|
24
|
+
* @param kernel - kernel courant (`this.kernel` du module driver), ou nullish.
|
|
25
|
+
*/
|
|
26
|
+
function wireOrmAdminPlane(kernel) {
|
|
27
|
+
const broker = kernel?.container?.get("adminBroker");
|
|
28
|
+
if (broker) registerOrmAdminApi(broker);
|
|
29
|
+
setOrmHealthProvider(buildOrmLeanHealth);
|
|
30
|
+
setOrmRichProvider(async () => ({
|
|
31
|
+
health: await buildConnectionHealth(),
|
|
32
|
+
flow: buildOrmFlow()
|
|
33
|
+
}));
|
|
34
|
+
kernel?.once?.("onServersReady", () => reportOrmBootLines(kernel));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pousse dans le BootReporter une ligne par ORM enregistré (« nom → driver
|
|
38
|
+
* (cible) ») pour que la phase de boot « Services & ORM » RACONTE les connexions
|
|
39
|
+
* mises en place, au lieu d'une phase muette. Idempotent : reconstruit la liste
|
|
40
|
+
* complète depuis le registre et REMPLACE (sûr d'être appelé par N drivers).
|
|
41
|
+
*
|
|
42
|
+
* `describeConnection()` ne révèle JAMAIS de credential (redaction côté adapter).
|
|
43
|
+
* Le libellé « Services & ORM » doit matcher une phase du `BootReporter` (core).
|
|
44
|
+
*
|
|
45
|
+
* @param kernel - kernel courant (`this.kernel` du module driver), ou nullish.
|
|
46
|
+
*/
|
|
47
|
+
function reportOrmBootLines(kernel) {
|
|
48
|
+
if (!kernel) return;
|
|
49
|
+
const lines = [];
|
|
50
|
+
for (const name of ormRegistry.list()) try {
|
|
51
|
+
const orm = ormRegistry.get(name);
|
|
52
|
+
const info = orm.describeConnection?.();
|
|
53
|
+
if (!info) continue;
|
|
54
|
+
const target = info.target ? ` ${info.target}` : "";
|
|
55
|
+
const state = orm.isConnected() ? "" : " (non connecté)";
|
|
56
|
+
lines.push(`${name} → ${info.driver}${target}${state}`);
|
|
57
|
+
} catch {}
|
|
58
|
+
kernel.setBootLines("Services & ORM", lines);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Détermine si la sonde de flux ORM (`queryFlowMonitor`) doit être active :
|
|
62
|
+
* **OFF en production** (coût nul sur le hot path des requêtes), **ON sinon**
|
|
63
|
+
* (observabilité dev / Supervision). Override explicite par la variable
|
|
64
|
+
* d'environnement `NF_ORM_FLOW` (`1`/`true` = forcer ON).
|
|
65
|
+
*
|
|
66
|
+
* Factorise le calcul recopié à l'identique dans chaque `*Service.onBoot` (le
|
|
67
|
+
* pendant « flux » de {@link wireOrmAdminPlane}, gardé distinct car il s'exécute
|
|
68
|
+
* dans le Service, pas le Module, et porte une responsabilité différente).
|
|
69
|
+
*
|
|
70
|
+
* @param kernel - kernel courant (pour lire l'environnement d'exécution).
|
|
71
|
+
* @returns `true` si la sonde de flux doit être activée.
|
|
72
|
+
*/
|
|
73
|
+
function resolveOrmFlowEnabled(kernel) {
|
|
74
|
+
const flag = process.env.NF_ORM_FLOW;
|
|
75
|
+
return flag !== void 0 ? flag === "1" || flag === "true" : kernel?.environment !== "production";
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { reportOrmBootLines, resolveOrmFlowEnabled, wireOrmAdminPlane };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { searchCriteria } from "./criteria.js";
|
|
2
|
+
import { PageQueryError } from "nodefony";
|
|
3
|
+
//#region nodefony/src/paginate.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pagine un {@link IRepository} de façon **portable** — au-dessus des primitives
|
|
6
|
+
* natives `find(criteria, { limit, offset, order })` et `count(criteria)` que tout
|
|
7
|
+
* adapter implémente déjà (SQL `LIMIT/OFFSET`+`COUNT`, Mongo `skip/limit`+
|
|
8
|
+
* `countDocuments`…). Aucun adapter n'a à changer : un seul aller vers la page,
|
|
9
|
+
* jamais de matérialisation de toute la collection.
|
|
10
|
+
*
|
|
11
|
+
* `hasNext` est obtenu **sans `COUNT`** par l'astuce `limit + 1` : on demande une
|
|
12
|
+
* ligne de plus que la page ; si elle arrive, il y a une suite (on la retire du
|
|
13
|
+
* résultat). Le `COUNT(*)` — coûteux sur les grosses tables — n'est payé que si
|
|
14
|
+
* `withTotal` n'est pas `false` (mode « Page » vs « Slice », cf Spring Data).
|
|
15
|
+
*
|
|
16
|
+
* La **recherche** `?q=` n'est honorée que si l'appelant déclare où chercher
|
|
17
|
+
* ({@link IPaginateOptions.searchable}) ; sans cela elle est refusée en `400`.
|
|
18
|
+
*
|
|
19
|
+
* @typeParam T - type de l'entité paginée.
|
|
20
|
+
* @param repo - le repository à paginer.
|
|
21
|
+
* @param page - la requête de page ({@link PageQuery}).
|
|
22
|
+
* @param options - capacités de ce point d'entrée (recherche).
|
|
23
|
+
* @returns une {@link Page} : au plus `limit` items, `hasNext`, et `total` si demandé.
|
|
24
|
+
* @throws {@link PageQueryError} (`400`) si un `q` est reçu sans champ
|
|
25
|
+
* cherchable déclaré, ou si le critère porte déjà un `$or`.
|
|
26
|
+
*/
|
|
27
|
+
async function paginate(repo, page, options = {}) {
|
|
28
|
+
const limit = Math.max(1, Math.floor(page.limit));
|
|
29
|
+
const offset = Math.max(0, Math.floor(page.offset ?? 0));
|
|
30
|
+
const withTotal = page.withTotal ?? true;
|
|
31
|
+
let criteria = page.criteria;
|
|
32
|
+
const search = searchCriteria(page.q, options.searchable ?? []);
|
|
33
|
+
if (search) {
|
|
34
|
+
if (criteria && "$or" in criteria) throw new PageQueryError("Cannot combine full-text search with a criteria that already uses \"$or\" (the grammar has no \"$and\" to nest them). Search on a single field, or build the criteria in the caller.");
|
|
35
|
+
criteria = {
|
|
36
|
+
...criteria,
|
|
37
|
+
...search
|
|
38
|
+
};
|
|
39
|
+
} else if (page.q?.trim()) throw new PageQueryError("This endpoint does not support full-text search (\"q\"): no searchable field is declared. Pass { searchable: [...] } to paginate(), or drop \"q\".");
|
|
40
|
+
const rows = await repo.find(criteria, {
|
|
41
|
+
limit: limit + 1,
|
|
42
|
+
offset,
|
|
43
|
+
order: page.order
|
|
44
|
+
});
|
|
45
|
+
const hasNext = rows.length > limit;
|
|
46
|
+
return {
|
|
47
|
+
items: hasNext ? rows.slice(0, limit) : rows,
|
|
48
|
+
limit,
|
|
49
|
+
offset,
|
|
50
|
+
hasNext,
|
|
51
|
+
total: withTotal ? await repo.count(criteria) : void 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { paginate };
|