npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@amsom-habitat/nest-generateur

v2.0.0

Published

Générateur de ressources pour les apis NestJS AMSOM : lecture sur vue, actions sur fonctions stockées et CRUD sur table, lus dans Oracle et enregistrés dans le module de l'api.

Readme

@amsom-habitat/nest-generateur

Générateur de ressources des apis NestJS AMSOM : une route en lecture sur une vue Oracle, des routes d'action sur des fonctions stockées, ou un CRUD sur une table, écrites dans src/ de l'api, formatées par son Prettier et enregistrées dans son module.

La première partie est le guide d'utilisation : de quoi générer une route en quelques minutes. La suite explique comment l'outil fonctionne, comment l'étendre, et ce qui casse quand ça casse. Les règles de rangement qu'il applique sont dans docs/organisation-src.md.

Installation dans une api

npm i -D @amsom-habitat/nest-generateur
{
  "scripts": {
    "generate": "amsom-generer"
  }
}

oracledb et prettier sont des peerDependencies : ce sont ceux de l'api qui servent (Prettier lit le .prettierrc de l'api).

Guide d'utilisation

Lancer

Le générateur tourne dans le conteneur de dev de l'api : c'est lui qui a la connexion Oracle et l'Instant Client. Depuis la racine de l'api :

docker compose up -d
docker compose exec --user $(id -u):$(id -g) api-<nom> npm run generate

Le --user évite que les fichiers générés appartiennent à root sur l'hôte.

Le bin travaille sur le dossier courant, qui doit être la racine de l'api (celle qui contient src/app.module.ts) : il y écrit src/, y lit .env, .env.custom, .env.local et .prettierrc. Rien n'est lu ni écrit dans node_modules.

amsom-generer [lecture|action|crud] [--schema S] [--prefixes "…"] [--colonnes "…"] [--dates "…"] [-h|--help]

| Option | Variable équivalente | Défaut | Rôle | |---|---|---|---| | --schema | AMSOM_GENERER_SCHEMA | ORACLE_USER | schéma Oracle où chercher vues, tables et fonctions (utile quand l'utilisateur de connexion n'est pas le propriétaire, ex. SYNEO) | | --prefixes | AMSOM_GENERER_PREFIXES | AHVWS_,AHVW_,AHV_,AHWS_,AH_ | préfixes Oracle retirés pour proposer un nom de ressource, de route ou de clé FN (le plus long qui convient) | | --colonnes | AMSOM_GENERER_COLONNES | — | mode hors base (cf. Sans base) | | --dates | AMSOM_GENERER_DATES | — | colonnes DATE du mode hors base |

Sans argument, il demande d'abord quoi générer :

| Choix | Ce qu'il produit | Il faut avoir sous la main | |---|---|---| | lecture | GET /<ressource> (liste) + GET /<ressource>/:id | une vue AHVWS_* | | action | des routes POST/PATCH/DELETE dans un même contrôleur | les fonctions stockées AH_* à appeler | | crud | un CRUD sur une table (/admin, le plus souvent), verbes au choix : liste, :id, POST, PATCH, DELETE | une table |

À la fin, Générer une autre ressource ? [O/n] : Entrée enchaîne, n arrête. Un échec (fichier déjà existant…) ne fait pas sortir de la boucle ; Ctrl+C, si.

Les touches

| Type de question | Ce qu'on fait | |---|---| | recherche (dossier, vue, table, fonction, préfixe) | taper quelques lettres, la liste se filtre ; ↑↓ puis Entrée | | liste | ↑↓ puis Entrée | | cases à cocher | Espace coche, Entrée valide (Entrée seule = rien de coché) | | texte | la valeur entre parenthèses est le défaut : Entrée l'accepte |

La recherche prend plusieurs mots, dans n'importe quel ordre : cally ged trouve AHVWS_CALLY_CONTRAT_GED. Ce qui contient les mots dans l'ordre tapé remonte en tête. Les vues, tables et fonctions sont lues dans Oracle au premier appel.

Une saisie absente de la liste reste possible là où elle a un sens : un nouveau dossier, un nouveau préfixe, ou un nom d'objet Oracle « tel quel ».

Ctrl+C abandonne sans rien écrire.

Exemple : une route en lecture

? Que générer ?                    lecture
✔ Vue Oracle                       contrat ged → AHVWS_CALLY_CONTRAT_GED
✔ Dossier parent, dans src/        contrat/
? Nom de la ressource              (contrat-ged)        ← déduit de la vue et d'API_NOM
✔ Préfixe d'URL                    cally2/   33 contrôleur(s)
? Nom dans l'URL                   (contrat_geds)       ← déduit de la ressource
? Colonne identifiant              ID
? Route de détail …/:id ?          Yes
? Filtres de la liste              ◉ idContrat
? Vue lourde ? Exiger un filtre    (Entrée si non)
? Cache serveur                    aucun
? Route publique (sans token) ?    No

? Typage proposé                   (d'après le type Oracle, Entrée pour valider)

Filtres, comparaison par défaut :
  ?idContrat=…  (exact)
? Filtres à comparer autrement     (Entrée si aucun)

Résultat (dans api-cally) : src/contrat/contrat-ged/contrat-ged.{controller,service}.ts, enregistrés dans ContratModule (ou AppModule sans module de thème), formatés par Prettier.

Le typage et les opérateurs de filtre ne posent de question que pour les colonnes qu'on coche : la proposition vient du type Oracle déclaré (cf. Le typage des colonnes, Les opérateurs de filtre).

Exemple : des routes d'action

? Que générer ?                    action
? Ajouter les routes à un contrôleur d'actions existant ?
❯ non : nouveau contrôleur
  sollicitation/sollicitation-action/   1 route(s), préfixe solly
✔ Fonction stockée                 sollicitation desc → AHWS_CALLY_SOLLICITATION_DESCRIPTION_MODIFIER
? Nom de la route (snake_case)     modifier_sollicitation
? Méthode HTTP                     POST
? Code de retour                   201 Created
? Paramètre Oracle P_IDLIGNE (NUMBER) — d'où vient sa valeur ?   un champ du body JSON
?     nom du champ dans le body JSON                             idLigne
?     « idLigne » arrive sous quelle forme ?                     une valeur simple
?     si « idLigne » est absent du body                          erreur : champ obligatoire
…                                  (une question par paramètre de la fonction)
? Appeler une autre fonction après celle-ci ? (rare)   No
? Autre route dans ce contrôleur ?                     No

Contrôleur existant : le générateur liste les contrôleurs d'actions de l'api (service avec const FN = { … } qui appelle des fonctions stockées). En en choisissant un, les routes y sont ajoutées : méthodes du contrôleur et du service, entrées de FN (une fonction déjà déclarée garde sa clé), interfaces du DTO, imports manquants. Un nom de route déjà présent est refusé. Sinon, « nouveau contrôleur » demande dossier, nom et préfixe.

Avant d'écrire, le générateur affiche le mapping paramètre Oracle ← valeur : le relire, c'est là que se voit une inversion d'arguments. Un paramètre qui a un défaut Oracle est proposé avec « passer NULL » si absent.

Le code produit a la forme des contrôleurs d'actions écrits à la main : corps brut (@Body() corps: RequestBody), lu champ par champ avec requiredField (clé absente → 400) / optionalField (absent ou null → défaut), PEC exigé (exigerPec(pec), 403 s'il manque), @Param('id') id: string. Pas de DTO class-validator : le DTO est une interface, les règles métier sont contrôlées par Oracle.

@Patch('modifier_composant/:id')
@HttpCode(HttpStatus.OK)
modifierComposant(
  @Pec() pec: PecAgent | null,
  @Param('id') id: string,
  @Body() corps: RequestBody,
) {
  return this.service.modifierComposant(exigerPec(pec), id, {
    libelle: requiredField(corps, 'libelle'),
    urgent: optionalField(corps, 'urgent', false),
  });
}

Sans base

Oracle injoignable, ou table pas encore créée : on décrit les colonnes.

docker compose exec --user $(id -u):$(id -g) api-<nom> \
  npm run generate -- --colonnes "ID,LIBELLE,DATE_MAJ" --dates "DATE_MAJ"

Les variables AMSOM_GENERER_COLONNES et AMSOM_GENERER_DATES font la même chose (docker compose exec -e AMSOM_GENERER_COLONNES=… …). Les noms d'objets Oracle se saisissent alors en entier. Les colonnes décrivent un seul objet : elles ne valent que pour la première ressource, la suivante (« Générer une autre ressource ? ») est lue dans Oracle. Le générateur action lit toujours la signature de la fonction dans Oracle : il lui faut la base.

Après

Le générateur termine par sa checklist. L'essentiel :

  1. confronter filtres, identifiant, cache et typage à l'entité v1 ;
  2. ajouter les requêtes à tests/collection.json (à la main) ;
  3. GET /coverage doit rester à 200.

Pourquoi il existe

La très grande majorité des routes de ces apis se ressemblent : un GET de liste et un GET /:id sur une vue AHVWS_*, ou un CRUD complet sur une table. D'un domaine à l'autre, seuls changent la vue, les filtres et le chemin — le reste est de la plomberie recopiée, avec un piège : l'enregistrement dans le module, qu'on oublie — et la route ne répond pas, sans que rien ne le signale.

Le générateur l'écrit correctement, à partir des colonnes réelles lues dans Oracle.

Fonctionnement

Ce qu'il fait

  1. lit dans le dictionnaire de données Oracle les colonnes et leurs types, la clé primaire (proposée comme identifiant) et les séquences rattachées à la table — colonne identité, trigger, ou <TABLE>_SEQ si elle existe (proposée, ou au choix s'il y en a plusieurs) ;
  2. vérifie qu'aucun des fichiers à créer n'existe (sinon rien n'est écrit), puis les écrit dans src/<domaine>/<ressource>/ ;
  3. passe Prettier dessus (la coupure de ligne dépend de la longueur des identifiants : le même template rend du code conforme pour une ressource et non conforme pour une autre) ;
  4. enregistre la ressource dans son module — imports, controllers, providers : le module du thème choisi s'il y en a un, sinon AppModule (cf. Dans quel module la ressource s'enregistre) ;
  5. écrit le typage des colonnes dans le service (view('…', { schema }), cf. Le typage des colonnes) ;
  6. rappelle en fin d'exécution ce qui reste à faire à la main.

Le code produit importe le socle par ses paquets, en 2.0.0 : @amsom-habitat/nest-oracle (view, formattedDate, unixTimestamp, unaccentSql, OracleService, toJsonParam), nest-rest (listPaginated, orNotFound, CrudService, filtres apply…, RequestBody, requiredField, optionalField), nest-auth et nest-cache (CachedGet, InvalidateCache). Le code généré n'a aucune trace de la v1 : il convient aussi à une api neuve. L'api doit avoir ces paquets en dépendances.

Ce qu'il ne fait pas, volontairement

  • tests/collection.json — c'est la référence des tests d'intégration ;
  • la vérification de la parité v1 (filtres, identifiant, TTL, typage) : il n'écrit aucun commentaire ni TODO dans le code produit ; ce qui reste à vérifier est rappelé dans la checklist affichée en fin de génération.

Comment le bin est lancé

amsom-generer lance plop par son API (Plop.prepare / Plop.execute) avec le plopfile.js du paquet et, comme destination et dossier de travail, le dossier courant. Chaque ressource est générée dans un processus enfant (lib/executer.mjs) : un échec ou un process.exit d'une génération ne coupe pas la boucle. Les options du bin passent à l'enfant par les variables AMSOM_GENERER_*. Tous les chemins de lib/ et toute la configuration passent par lib/config.js (process.cwd(), .env de l'api), et oracledb / prettier sont chargés depuis l'api.

Oracle n'est lu que pendant les questions, par une seule connexion (ouverte au premier besoin, fermée à la fin des questions) ; les lectures sont mémoïsées. Une erreur prévue (objet introuvable, fonction surchargée…) s'affiche en une ligne [ERROR] …, sans trace.

Le paquet exporte aussi son plopfile (require('@amsom-habitat/nest-generateur')) pour qui voudrait le charger avec node-plop.

Les prompts, et ce qu'ils décident

Les questions sont formulées pour être comprises sans connaître le projet. Deux méritent une explication.

Les opérateurs de filtre (générateur lecture)

Après le choix des colonnes filtrables, le générateur affiche la comparaison par défaut de chacune (exact, ou boolean sur une colonne 0/1), puis demande lesquelles comparer autrement. Seules celles-là ouvrent la question détaillée :

? Comparaison pour « nom » (NOM)
  ── comparaison de ?nom= (un seul) ──
  ❯◉ est exactement la valeur                 (exact)
   ◯ est exactement, majuscules ignorées      (iexact)
   ◯ contient                                 (partial)   ⚠ balaie la vue
   ◯ contient, majuscules et accents ignorés  (ipartial)  ⚠ balaie la vue
   ◯ commence par                             (start)     ⚠ balaie la vue
   ◯ finit par                                (end)       ⚠ balaie la vue
   ◯ est dans la liste (a,b,c)                (in)
   ◯ n'est PAS dans la liste (a,b,c)          (not in)
   ◯ est vrai / faux (true, false, 1, 0)      (boolean)
  ── bornes, cumulables (?nom[gte]=…) ──
   ◯ plus grand que (gt)   ◯ plus grand ou égal à (gte)
   ◯ plus petit que (lt)   ◯ plus petit ou égal à (lte)

Le nom entre parenthèses est celui d'API Platform : c'est lui qu'on retrouve dans les #[ApiFilter] de la v1 quand on vérifie une parité.

| Opérateur | Query param | SQL produit | |---|---|---| | exact | ?nom=DUPONT | where('NOM', v) | | iexact | ?nom=dupont | UPPER(NOM) = UPPER(?) | | partial | ?nom=UPO | NOM LIKE '%UPO%' | | ipartial | ?nom=upo | UPPER(unaccent(NOM)) LIKE '%UPO%' | | start / end | ?nom=DUP | LIKE 'DUP%' / LIKE '%DUP' | | in / not in | ?nom=a,b,c | whereIn / whereNotIn | | boolean | ?actif=true | where('ACTIF', 1) | | gt gte lt lte | ?exercice[gte]=2020 | where('EXERCICE', '>=', v) |

Deux familles, deux comportements. Les opérateurs de valeur comparent le paramètre nu (?nom=…) : ils se disputent le même query param, donc un seul par champ — la question le refuse sinon (includes_any, qui lit aussi ?nom=, compte parmi eux). Les bornes ont chacune leur clé (?exercice[gte]=, ?exercice[lte]=), elles se cumulent : les deux ensemble donnent un intervalle.

Le cas booléen

boolean rejoue le BooleanFilter d'API Platform, et ce n'est pas un exact déguisé :

| Requête | boolean | exact sur la même colonne | |---|---|---| | ?actif=1 / ?actif=0 | vrais / faux | vrais / faux (Oracle convertit '0') | | ?actif=true / ?actif=false | vrais / faux | ORA-01722 → 400 | | ?actif=oui, ?actif=2 | collection entière | ORA-01722 → 400 |

Le piège est qu'exact a l'air de marcher : la moitié numérique passe par conversion implicite, et c'est ?actif=true — la valeur qu'un client écrit spontanément, et que la v1 accepte — qui casse en erreur Oracle.

Deux conséquences dans le code généré : la valeur est comparée à 1/0 (la forme du booléen dans ces vues), et la garde n'est plus « la valeur est fournie » mais « la valeur est interprétable » — une valeur inconnue laisse la collection entière, sans erreur, comme chez API Platform :

const actifBooleen = parseBooleanFilter(actif);
if (actifBooleen !== undefined) query.where('ACTIF', actifBooleen ? 1 : 0);

Une colonne typée booléenne (cf. Le typage des colonnes) propose boolean pré-coché à la place d'exact : le typage est demandé juste avant, et c'est la réponse donnée qui compte.

⚠️ Les quatre opérateurs marqués « balaie la vue » interdisent l'usage d'un index (mesuré : ~1,5 s par requête sur une vue de recherche d'individus). À réserver aux champs qu'un humain saisit.

Le piège que tout ceci évite : Oracle compare octet par octet. Sur une colonne stockée en majuscules, ?nom=tahi ne rend rien là où ?nom=TAHI rend la ligne.

include / ninclude (colonne contenant une liste à virgules) encadrent la liste normalisée de virgules ; la négation passe par un NVL pour remonter aussi les colonnes vides. in accepte le CSV comme le JSON (souvent encodé %5B…%5D).

Pour ajouter un opérateur au catalogue, une entrée dans lib/operateurs.js suffit : les questions, le rendu et l'interface générée le reprennent tout seuls.

Le typage des colonnes

Oracle est faiblement typé, et ce que le driver rend ne suffit pas à retrouver le JSON de la v1 :

| Colonne Oracle | Sans déclaration | Ce que rend la v1 | |---|---|---| | NUMBER(5,2) | 20.6 ✅ | 20.6 | | NUMBER sans échelle | 1234.56 ✅ | 1234.56 | | NUMBER(1) booléen | 1 ❌ | true | | VARCHAR2 « 1 234,56 » | "1 234,56" ❌ | 1234.56 |

Les décimaux numériques passent tout seuls : node-oracledb rend un NUMBER en nombre JS, avec le point décimal, quelle que soit la langue de la session.

Le générateur propose un type par colonne, d'après son seul type déclaré dans Oracle (ALL_TAB_COLUMNS), et affiche la liste ; on coche celles à changer, Entrée valide.

| Type Oracle | Proposé | |---|---| | NUMBER(1,0) | booléen (convention de la base) | | tout le reste | tel quel : nombre pour un NUMBER, chaîne sinon, DD/MM/RR pour une DATE |

Aucune lecture de données, aucune déduction sur le nom : la proposition est la même à chaque exécution, et l'entité v1 tranche. Ce qu'Oracle ne dit pas se choisit à la main — un booléen calculé (CASE WHEN … THEN 1 ELSE 0, décrit NUMBER sans précision), un montant stocké en VARCHAR2 (« 1 234,56 »), un JSON en VARCHAR2 ou CLOB, un ?int de la v1 sur un VARCHAR2.

? Typage proposé — cocher les colonnes à changer, Entrée pour valider
❯◯ id                       NUMBER         → tel quel (nombre)
 ◯ travaux                  VARCHAR2(100)  → tel quel (chaîne)
 ◉ pge                      NUMBER(1)      → booléen
 ◯ creele                   DATE           → tel quel (date DD/MM/RR)
…
? pge                      NUMBER(1)      (PGE)
  tel quel (nombre)
❯ booléen          0/1 → true/false  ← proposé
  entier           "28330" → 28330, (int) PHP
  …

Chaque choix correspond à une clé de ViewSchema (booleans, integers, decimals, stringOrNumbers, strings, json) ; « tel quel » n'écrit rien. Le résultat est écrit dans le service généré, dans la déclaration de la vue :

const VIEW = view('AHVWS_GED_EDL', {
  schema: {
    strings: ['ID'],
    integers: ['ID_CONTRAT', 'EXERCICE_EDL'],
  },
});
…
return listPaginated(VIEW, query, filters);

Sans aucune colonne à convertir, la déclaration se réduit à view('AHVWS_X').

Sans base (--colonnes), il n'y a pas de type déclaré : tout est proposé « tel quel », à changer dans la même liste.

⚠️ Sur un CRUD, le typage joue dans les deux sens. La même vue est passée au CrudTableDefinition (table: TABLE), dont le CrudService de @amsom-habitat/nest-rest relit le typage en écriture : sans déclaration, un POST {"pge": true} écrirait le booléen JS tel quel au lieu de 1.

Le CRUD du socle lit, écrit et relit par la colonne ID : le générateur crud refuse une table sans colonne ID, et prévient si sa clé primaire est une autre colonne.

Les dates

Une colonne Oracle DATE porte toujours jour et heure : rien dans le dictionnaire ne dit si l'heure compte. On choisit donc la forme dans la liste de typage :

| Choix | Exemple | Heure | |---|---|---| | DD/MM/RR (défaut, comme la v1) | 21/02/23 | perdue : un PATCH la remet à 00:00:00 | | DD/MM/YYYY, YYYY-MM-DD | 21/02/2023 | perdue | | DD/MM/YYYY HH24:MI:SS, YYYY-MM-DD HH24:MI:SS | 21/02/2023 14:30:00 | conservée | | timestamp Unix | 1676986200 | conservée | | autre format Oracle… | DD/MM/YYYY HH24:MI | selon le format |

Le format libre est vérifié par Oracle à la saisie (TO_DATE(TO_CHAR(SYSDATE, format), format)) : un format refusé l'est avant la génération, avec le message d'Oracle.

Un format s'applique en lecture (TO_CHAR) comme en écriture (TO_DATE). Le timestamp passe par AH_TIMESTAMP / AH_UNTIMESTAMP (Europe/Paris, changement d'heure compris), comme les fonctions stockées ; une valeur non numérique est refusée en 400. Dans le service : dates (liste en DD/MM/RR, ou objet colonne → format) et timestamps du CrudTableDefinition, formattedDate(db, colonne, format?) / unixTimestamp(db, colonne) dans columns().

Les clés JSON (générateur crud)

Chaque colonne est exposée sous sa clé par défaut, en camelCase (DATE_FIN → dateFin). La liste les affiche toutes ; on coche celles à renommer et on saisit le nouveau nom (proposé : la clé par défaut). La clé renommée vaut partout : JSON rendu, corps du POST/PATCH et nom du query param de filtre. Deux colonnes ne peuvent pas porter la même clé.

Le cache

Ne cacher qu'une donnée identique pour tous les utilisateurs. La clé de cache est l'URL seule, sans segmentation par appelant : une réponse nominative mise en cache serait servie à quelqu'un d'autre.

Le contrôleur caché porte @CachedGet(ttlMs). Un CRUD caché qui expose aussi une écriture porte en plus @InvalidateCache('<route>'), sans quoi une lecture resterait périmée jusqu'à la fin du TTL ; ce tag se déclare dans src/cache-tags.ts (rappelé en fin de génération).

Le chemin de la route : rien à déclarer

src/ est l'api entière : pas de question « où », le préfixe d'URL et le nom de la ressource sont découverts à chaque exécution (lib/projet.js, lib/questions.js).

  • préfixes = le premier segment des @Controller(...) de src/, comptés et triés par fréquence ;
  • nom de la ressource = le nom de la vue, de la table ou de la fonction sans son préfixe Oracle (le plus long de --prefixes, par défaut AHVWS_, AHVW_, AHV_, AHWS_, AH_) ni le nom de l'api : API_NOM, lu dans .env < .env.custom < .env.local (le dernier gagne), suffixe numérique toléré. Avec API_NOM=cally, AHVWS_CALLY_CONTRAT_GED et AHVWS_CALLY2_CONTRAT_GED donnent contrat-ged. Le même retrait propose le nom d'une route d'action (AHWS_CALLY_PLIP_MODIFIER → plip_modifier).

Conséquence : le premier contrôleur d'une api neuve n'a aucun préfixe à proposer. Taper le préfixe et choisir « nouveau préfixe », une fois — les suivants le retrouveront dans la liste.

Le générateur action — routes sur fonctions stockées

Le problème qu'il résout

Les deux autres générateurs couvrent les GET sur vue et le CRUD sur table. Reste une troisième famille, ni l'une ni l'autre : les routes qui appellent une fonction stockée.

POST   /patri/creer_composant           → ah_patri_composant_creer
DELETE /patri/delier_logement_composant → ah_composant_module_delier

Elles ne sont pas générables en CRUD, et pour trois raisons cumulées :

  1. les ressources concernées sont exposées par des vues (AHVWS_*), et on n'écrit pas dans une vue ;
  2. la v1 ne les expose pas en #[ApiResource] mais dans des contrôleurs Symfony, avec des chemins d'action en snake_case — un CRUD REST serait un autre contrat ;
  3. surtout, la logique est dans la fonction stockée : ah_patri_composant_creer enchaîne sur ah_composant_maj_caracteristiques. Un INSERT knex écrirait la ligne sans jouer cette logique, et la donnée serait incomplète sans erreur visible.

Ce qu'il apporte

L'ordre des arguments d'un callFunction est positionnel. Deux arguments intervertis compilent, passent, et écrivent la mauvaise valeur dans la bonne colonne — sans que rien ne le signale. C'est ce que le générateur supprime.

Il lit la signature dans Oracle (ALL_ARGUMENTS : nom, position, type, valeurs par défaut) et demande, pour chaque paramètre, d'où vient sa valeur : un champ du corps JSON, le :id de l'URL, ou le PEC de l'agent connecté.

Une seule source, Oracle. Rien à monter, rien à installer, le même parcours pour tout le monde — y compris pour qui n'a pas l'API v1 sur son poste. Ce qu'Oracle ne sait pas dire (quel champ du corps remplit WCCCP) est demandé.

Trouver la fonction sans connaître son nom

Le générateur liste les fonctions du schéma (ALL_OBJECTS, filtré sur --schema, par défaut ORACLE_USER, lu une fois par exécution) et les filtre à la frappe. Les procédures n'y sont pas : callFunction n'appelle que des fonctions. Une fonction de package, surchargée, ou qui a un paramètre OUT / IN OUT est refusée avec un message :

? Fonction stockée (tapez un fragment, ex. patri composant) patri compos cre
❯ AH_PATRI_COMPOSANT_CREER

Les mots se cherchent dans n'importe quel ordre ; ceux qui suivent l'ordre tapé remontent en tête. La liste affiche au plus 100 résultats : taper un mot de plus si la fonction n'y est pas.

Plusieurs routes en une passe

Un contrôleur d'action en regroupe plusieurs. Le générateur boucle donc : après chaque route, il propose d'en ajouter une autre dans le même fichier. Les générer une par une buterait sur « File already exists » dès la deuxième (ou passerait par « contrôleur existant »).

Le mapping est montré avant écriture

mapping × signature Oracle :
  POST /creer_incident → ah_patri_incident_creer
      1 WIDRESIDENCE   ← corps.residenceId
      2 WDESCRIPTION   ← corps.incident (optionnel)
      3 WURGENT        ← corps.urgent
      4 WPEC           ← exigerPec(@Pec()) (agent du token)
  DELETE /supprimer_incident → ah_patri_incident_supprimer
      1 WID            ← paramètre d'URL {id}
      2 WPEC           ← exigerPec(@Pec()) (agent du token)

C'est le point où l'on rattrape une inversion avant qu'elle n'atteigne la base.

Les paramètres à défaut Oracle

Certaines fonctions déclarent des paramètres DEFAULTED='Y', qu'on peut omettre : ah_composant_supprimer en déclare quatre et la v1 n'en passe qu'un. Le générateur propose « ne pas le passer » uniquement quand tous les paramètres restants ont un défaut — un appel est positionnel, on ne saute pas un trou au milieu. Ce qui est omis est rappelé dans le mapping.

Les appels enchaînés

Une route d'écriture de la v1 appelle parfois une seconde fonction stockée, sous condition :

if (isset($json->data)) {
    if (array_key_exists('caracteristiques', $data)) { … maj_caracteristiques … }

Les deux gardes sont demandées, pas devinées : « quand l'appeler ? » (à la présence d'un champ du corps, ou toujours) et, pour chaque paramètre du second appel, la même question d'origine que pour le premier — avec un choix de plus, l'identifiant rendu par l'appel précédent (le $json->data de la v1).

const res = await this.oracle.callFunction<OracleParam>(FN.CREER_COMPOSANT, [ … ]);
const idCree = res.data;
if (idCree !== undefined && dto.caracteristiques !== undefined) {
  await this.oracle.callFunction(FN.COMPOSANT_MAJ_CARACTERISTIQUES, [
    idCree,
    toJsonParam(dto.caracteristiques),
  ]);
}

Le champ déclencheur entre dans le même DTO, lu par optionalField — c'est son absence qui coupe l'appel. Les fonctions enchaînées rejoignent la table FN, dédoublonnées (la même sert la création et la mise à jour). Une clé déjà portée par une autre fonction est suffixée (CREER_2) plutôt qu'écrasée.

La forme des valeurs du corps

Oracle donne le type de chaque paramètre, pas la forme de ce que le front envoie. Deux cas où un bind nu écrirait n'importe quoi, donc demandés :

| Type Oracle | Choix proposé | Code produit | |---|---|---| | NUMBER | un booléen | dto.urgent ? 1 : 0 | | VARCHAR2 | un tableau ou un objet | toJsonParam(dto.caracteristiques) | | CLOB | un tableau ou un objet | bind { type: 'clob', … } |

toJsonParam (@amsom-habitat/nest-oracle) sérialise en JSON ; absent, [] et {} deviennent NULL. La valeur simple est le premier choix : une Entrée suffit dans le cas courant.

« Si absent du body » dépend de la forme : un booléen absent vaut false ou true (jamais NULL : il passerait en 0), un JSON absent vaut NULL, une valeur simple vaut NULL, une chaîne vide ou la valeur saisie. Le défaut choisi est le troisième argument d'optionalField(corps, 'champ', défaut).

Ce qu'il ne fait pas

Le post-traitement de la réponse. La v1 relit l'objet créé et le renvoie sérialisé ($composantRepository->find($json->data)), c'est-à-dire sous la même forme que la route GET correspondante. Le générateur rend le retour brut de la fonction stockée ; la checklist de fin le rappelle.

⚠️ Autre point à confronter à la v1 : l'ordre des erreurs. exigerPec(pec) est le premier argument, donc le 403 passe avant le 500 d'un champ requis absent ; si la v1 lit le corps d'abord, déplacer l'appel. Le corps n'est pas validé (parité v1) : Oracle convertit la valeur ou répond l'erreur.

Dans quel module la ressource s'enregistre

Le module de l'api est src/app.module.ts (AppModule), qui importe aussi le socle (SocleModule.forRoot(…), CoverageModule). Un thème peut avoir son module : contrat/contrat.module.ts, sollicitation/sollicitation.module.ts… importés par AppModule. Le générateur (lib/projet.js) remonte donc du dossier de la ressource jusqu'à src/, et prend le premier *.module.ts rencontré :

| Dossier saisi | Module modifié | |---|---| | (vide) | app.module.ts | | contrat | contrat/contrat.module.ts | | sollicitation/sollicitation-action | sollicitation/sollicitation.module.ts | | nouveau-dossier (sans module) | app.module.ts |

Un dossier qui contient plusieurs modules (ex. referentiel/) est retenu s'il en a un qui porte son nom, sinon on remonte d'un cran. Le module retenu est affiché pendant la génération (module cible : …).

Module de thème. Quand un dossier de premier niveau de src/ atteint deux contrôleurs sans avoir de module (ex. plip/ après plip-plop/ puis plip), le générateur crée plip/plip.module.ts (PlipModule), y déplace les enregistrements faits dans AppModule, exporte les services utilisés ailleurs dans l'api, et ajoute le nouveau module en fin des imports d'AppModule. MailModule et DocumentModule sont globaux (forRoot({ isGlobal: true }) dans AppModule) : un module de thème ne les importe pas. Les ressources suivantes du thème s'y enregistrent directement. La convention complète est dans docs/organisation-src.md.

Si la nouvelle ressource injecte le service d'un autre module de thème, il faut encore l'ajouter aux exports de ce dernier, à la main : sinon Nest refuse de démarrer (Nest can't resolve dependencies…).

Comment le module est modifié

Le générateur ne cherche pas de motif dans le texte : il ouvre le module avec ts-morph (lib/ast.js), trouve le @Module({ … }) et ajoute les imports et les entrées de controllers / providers (en tête de tableau, imports juste après celui de @nestjs/common). La mise en forme du module est donc libre : tableau sur une ligne ou plusieurs, commentaires, propriété absente (elle est créée). Une ressource déjà présente n'est pas réinsérée si l'on relance le générateur. Prettier reformate ensuite le module.

Le même mécanisme sert à l'ajout de routes à un contrôleur existant (imports complétés, méthodes ajoutées en fin de classe, entrées de FN).

Ajouter un filtre à une route existante

Le générateur ne passe qu'une fois : les filtres qui arrivent ensuite s'ajoutent à la main. Il y a trois endroits à toucher, et en oublier un ne se voit pas toujours à la compilation.

1. Le contrôleur — exposer le query param

@Query('idService') idService?: string,   // ← le nom EXPOSÉ, pas la colonne Oracle
// …
return this.service.findAll({ idService, /* … */ });

Le nom du query param est celui du champ JSON (idService), pas celui de la colonne (ID_SERVICE) : c'est ce que rend VIEW.rows, donc ce que le client voit.

2. Le service — déclarer le filtre

export interface CollaborateurFilters extends Pagination {
  /** SearchFilter `exact` — code service ('CLIA', 'EXCO'…). */
  idService?: string;
}

Noter en commentaire l'ApiFilter v1 correspondant : c'est ce qui permet, six mois plus tard, de vérifier une parité sans rouvrir la v1.

3. Le service — appliquer la condition

Le SQL de chaque opérateur est décrit dans lib/operateurs.js — le générateur s'en sert, et un ajout à la main gagne à reprendre la même forme. Les trois plus courantes :

| Cas | Code | |---|---| | code, identifiant (défaut) | query.where('ID_SERVICE', idService) | | valeur saisie, casse ignorée | query.whereRaw('UPPER(ID_SERVICE) = UPPER(?)', [idService]) | | recherche « humaine » | voir ci-dessous |

const motif = `%${removeAccents(nom).toUpperCase()}%`;
query.whereRaw(`UPPER(${unaccentSql('NOM')}) LIKE ?`, [motif]);

removeAccents et unaccentSql viennent de @amsom-habitat/nest-oracle. ⚠️ Cette forme interdit l'usage d'un index : la vue est balayée entièrement.

Deux détails qui comptent :

Une chaîne vide vaut « pas de filtre » — la v1 ignore un query param vide plutôt que de chercher les lignes dont la colonne vaut '' :

if (idService !== undefined && idService !== '') query.where(…);

Un booléen se compare à 1 / 0 : les vues le stockent en NUMBER(1).

Filtre à valeurs multiples

Quand un filtre doit accepter une liste (?idModule=A,B,C) :

if (idModule !== undefined && idModule !== '')
  applyIn(query, 'ID_MODULE', idModule);

applyIn (@amsom-habitat/nest-rest) accepte le CSV comme le JSON, comme la v1.

Si la route a un garde-fou de disponibilité

Sur les routes qui exigent un filtre sélectif (liste sur une vue lourde), il faut décider si le nouveau filtre ouvre la route ou non. La réponse se mesure, elle ne se devine pas :

curl -s -m 30 -H "Authorization: Bearer $T" \
  "http://localhost:<port>/<route>?monFiltre=X&itemsPerPage=1" \
  -o /dev/null -w "%{http_code} en %{time_total}s\n"

Sous ~1 s → l'ajouter aux filtres sélectifs. Au-delà, surtout borné à une ligne, c'est qu'il balaie la vue : le laisser comme filtre d'appoint.

Et la parité

Un filtre qui n'existe pas côté v1 est un écart, pas une amélioration : la règle est la parité stricte. Vérifier les #[ApiFilter] de l'entité v1 avant d'ajouter — et si le filtre est volontairement en plus, l'écrire en commentaire (Parité v1 : …, une ligne).

Organisation du paquet

| Fichier | Rôle | |---|---| | bin/amsom-generer.js | options, vérification de la racine de l'api, boucle « Générer une autre ressource ? » | | lib/executer.mjs | une génération : plop lancé par son API, plopfile du paquet, destination = dossier courant | | plopfile.js | enregistre les trois générateurs | | lib/lecture.js / crud.js / action.js | prompts + actions d'un générateur | | lib/config.js | racine de l'api, .env de l'api, options AMSOM_GENERER_*, chargement d'oracledb / prettier depuis l'api | | lib/erreurs.js | ErreurUtilisateur (message sans trace), Ctrl+C, curseur | | lib/casse.js / noms.js | conversions de casse ; noms proposés d'après les objets Oracle | | lib/fichiers.js / imports.js | parcours de src/ ; imports d'un fichier généré (texte ou ts-morph) | | lib/ast.js | éditions ts-morph (imports, @Module, membres de classe, FN) | | lib/questions-action.js | questions propres au générateur action | | lib/ajout.js | contrôleurs d'actions existants de l'api, ajout de routes dans leurs fichiers | | lib/questions.js | questions communes (dossier, objet Oracle, préfixe, cache), nom de ressource déduit, recherche à la frappe | | lib/oracle.js | dictionnaire de données Oracle (colonnes, types, clé primaire, séquences, fonctions), lecture mémoïsée, mode hors base | | lib/projet.js | ce qui touche src/ : API_NOM, préfixes existants, enregistrement dans le module, module de thème, Prettier | | lib/typage.js | type proposé d'après Oracle, retypage des colonnes cochées, déclaration view(…) du service | | lib/operateurs.js | catalogue des opérateurs de filtre et question associée | | lib/rendu-lecture.js / rendu-crud.js / rendu-action.js (+ rendu-commun.js) | traduction des réponses en fragments de code | | templates/ | les fichiers .hbs rendus | | test/ | tests node:test : fonctions pures, génération de bout en bout hors base |

Le partage des rôles est constant : les templates ne font que de la mise en page, tout ce qui demande une décision (quelles colonnes, quel TO_CHAR, quel import) est résolu dans les fichiers de rendu, où c'est lisible et testable.

Étendre le générateur

Modifier le code produit → les .hbs de templates/, et le fichier de rendu correspondant si la décision est calculée. Aucun commentaire de gabarit dans le code produit.

Ajouter un prompt → dans le générateur concerné. S'il est commun à plusieurs, dans lib/questions.js. Une liste longue passe par chercher() (@inquirer/search), qui filtre à la frappe et peut proposer la saisie libre.

Ajouter un générateur → un fichier dans lib/ exportant () => ({ description, prompts, actions }), plus une ligne dans plopfile.js (et dans GENERATEURS du bin).

Tout chemin et toute option passent par lib/config.js : jamais de __dirname pour atteindre src/ ou un .env (le paquet est installé dans node_modules), jamais de process.env lu ailleurs. Une nouvelle option du bin devient une variable AMSOM_GENERER_* lue par config().

Signaler une erreur → throw new ErreurUtilisateur(…lignes) : plop l'affiche sans trace. Dans un validate d'inquirer, rendre le message (chaîne) plutôt que lever.

Tester → npm test (node:test). Une fonction pure se teste directement (test/unitaires.test.js) ; un rendu se teste de bout en bout hors base (test/generation.test.js : AMSOM_GENERER_COLONNES, node-plop, runActions sur des réponses scriptées dans une api jetable, contrôle de syntaxe TypeScript du code produit et absence de commentaire).

Poser une question dont la liste n'est pas connue d'avance

Un tableau de prompts est figé avant l'exécution. Or deux parcours en dépendent : les paramètres d'une fonction stockée (générateur action) et une question par colonne filtrable (générateur lecture).

La réponse n'est pas de poser la question depuis une action. prompts accepte aussi une fonction (cf. node-plop/src/generator-runner.js) : elle reçoit inquirer, enchaîne autant de questions qu'elle veut, et rend l'objet de réponses.

prompts: async (inquirer) => {
  const reponses = await inquirer.prompt([ /* le tableau habituel */ ]);
  // … questions supplémentaires, calculées d'après ce qui précède
  return reponses;
},

⚠️ Aucune question ne doit être posée depuis une action. Plop ferme l'UI d'inquirer à la fin de la phase prompts et laisse process.stdin en pause : un prompt posé ensuite s'affiche mais ne reçoit jamais rien. Pas d'erreur, pas de timeout — le générateur a juste l'air figé sur sa question, et la frappe ne s'écho pas. Ni process.stdin.resume() ni un inquirer.createPromptModule() neuf ne le débloquent (vérifié sous Node 24 / inquirer 9.3.8).

Pour tester sans lancer de génération, charger le plopfile avec node-plop (destBasePath = une copie d'api, dossier courant compris) et appeler generateur.runActions(reponses) (cf. test/generation.test.js). Les réponses du générateur action portent déjà le croisement avec Oracle (routesCroisees) : toute lecture d'Oracle a lieu pendant les questions.

Dépannage

| Symptôme | Cause | Correctif | |---|---|---| | ✖ pas d'api ici | lancé ailleurs qu'à la racine de l'api | cd à la racine (celle de src/app.module.ts) ; dans le conteneur, /app | | amsom-generer: not found | paquet non installé dans l'image | npm i -D @amsom-habitat/nest-generateur, puis reconstruire l'image (node_modules est celui de l'image) | | Le nom de ressource proposé garde le nom de l'api (cally-contrat-ged) | API_NOM ne correspond pas au préfixe Oracle des vues | corriger API_NOM dans .env, ou saisir le nom | | [ERROR] Table ou vue X introuvable | faute de frappe, objet absent du schéma, ou schéma différent de l'utilisateur de connexion | vérifier le nom, passer --schema ; s'il n'existe pas encore : --colonnes "ID,NOM" --dates "DATE_MAJ" | | [ERROR] Lecture d’Oracle impossible | base injoignable (VPN ?) | même échappatoire --colonnes / --dates | | client Oracle Thick indisponible … : mode Thin | Instant Client absent de ORACLE_CLIENT_LIB_DIR | le mode Thin suffit souvent ; sinon corriger le chemin | | [ERROR] X est une procédure / surchargée / paramètres OUT | la fonction n'est pas appelable par callFunction tel quel | écrire la route à la main | | [ERROR] … n’a pas de colonne ID | le CRUD du socle exige ID | générer une lecture et des actions | | déjà présent(s) : … — rien n’a été écrit | la ressource existe déjà | choisir un autre nom, ou supprimer le dossier (et ses lignes dans le module) | | La route générée répond 404 Cannot GET | ressource non enregistrée, ou compilation en échec | vérifier le module affiché en module cible : … et les logs du conteneur | | Cannot find module './xxx/xxx.service' au démarrage | une ressource a été supprimée sans nettoyer le module | retirer les 4 lignes correspondantes ; npx tsc --noEmit les liste | | Cannot find module '@amsom-habitat/nest-…' | le code généré importe un paquet du socle absent de l'api | l'ajouter aux dépendances de l'api | | Le spinner efface l'écran en boucle | terminal sans largeur (pty sans taille, ex. expect) | donner une taille au terminal (stty columns 120 rows 40) | | Une question s'affiche et le parcours reste figé dessus, sans erreur | la question est posée depuis une action | déplacer la question dans prompts, forme fonction (cf. Poser une question dont la liste n'est pas connue d'avance) |

⚠️ Une erreur de compilation ne se voit pas côté client. Le watch recompile, échoue, et laisse le serveur à terre : Postman affiche socket hang up, ce qui ressemble à un problème réseau. Le réflexe : npx tsc --noEmit dans le conteneur, ses logs (« Found N errors. Watching for file changes. »), puis /health (000 = mort, pas lent).

Après la génération

Le générateur le rappelle lui-même, mais pour mémoire :

  1. confronter filtres, identifiant et TTL à l'entité v1 ;
  2. relire le typage de la view écrite dans le service généré : proposé d'après le type Oracle, il n'a été revu que pour les colonnes cochées — cf. Le typage des colonnes ;
  3. si la route est cachée et qu'une écriture doit la purger, déclarer le tag dans src/cache-tags.ts (un CRUD caché porte déjà @InvalidateCache sur son propre chemin : le tag est à déclarer aussi) ;
  4. ajouter les requêtes à tests/collection.json (édition manuelle) ;
  5. GET /coverage doit rester à 200.

La v1 fait foi y compris quand elle a tort : une bizarrerie reproduite volontairement porte un commentaire Parité v1 : … d'une ligne, au plus près du code, pour qu'elle ne soit pas « corrigée » de bonne foi.

⚠️ Une liste sur une vue lourde doit porter un filtre obligatoire. Sans lui, une seule requête peut saturer le heap et tuer le process — donc couper l'api pour tout le monde. Le générateur lecture le propose (« Vue lourde ? Exiger un de ces filtres ») : un 400 si aucun n'est fourni. itemsPerPage n'est pas une protection : la borne s'applique après le balayage de la vue.