API Conventions¶
Nommage (verbes par couche, suffixes de type, booléens, lexique de statuts, vocabulaire de domaine, modules) : voir naming-conventions.mdc.
Regle absolue¶
Ne JAMAIS refactoriser du code existant sauf demande explicite. Les conventions ci-dessous s'appliquent au nouveau code uniquement. Si un refactoring semble benefique, demander confirmation d'abord.
DO NOT¶
- DO NOT utiliser les patterns legacy (tout hors
__new/) nidtos/→ utilisermodel/ - DO NOT utiliser de dependency injection ou factory pattern → import direct des service objects (
export const X = {}) - DO NOT assigner un service object a une variable intermediaire (
const x = SomeService,this.x = SomeService) → appelerSomeService.method()directement depuis l'import - DO NOT passer un service object migre en parametre de fonction, propriete d'objet (
services: { ... }) ou champ de controller → import statique et appel direct. Exceptions : services NestJS@Injectable()non migres, instances runtime (ex.lemonwayApicree avec options par factory), state watchers / factories avec deps explicites documentees - DO NOT utiliser de decorateurs hors controllers et entites NestJS
- DO NOT utiliser
moment→dayjs - DO NOT utiliser
var→constpar defaut,letsi reassignation - DO NOT utiliser de primitifs nus pour montants, IDs, durees → types forts (voir section "Types forts")
- DO NOT utiliser
Omit,Pick,Partial, types conditionnels - DO NOT muter les objets →
produced'immer ou redeclarer tous les champs - DO NOT utiliser
produce()d'immer sur des instances de classes (entites TypeORM/ORM) → immer ne peut pas proxifier les class instances sans[immerable] = true. Utiliser l'assignation directe pour muter des entites ORM dans une transaction - DO NOT utiliser le spread
...→ redeclarer chaque champ - DO NOT
try/catch→.catch()apres promesse ouResult<T, E> - DO NOT
throwpour erreurs metier →Err('error-code' as const) - DO NOT throw d'exceptions NestJS depuis les services pour des erreurs metier → retourner
Err(...)et les mapper dans les controllers - DO NOT ajouter d'annotations de type explicites quand TypeScript infere deja le type voulu
- DO NOT construire des helpers metadata-driven autour de
prepareParameterizedQuerypour les casts PG simples → declarercast: 'jsonb'/cast: 'uuid'explicitement dans le mapping - DO NOT
return await→ retourner directement la promesse - DO NOT
switchouif/elseenchaines →match().exhaustive()de ts-pattern - DO NOT mettre de logique metier dans
service/→ seulement dansbusiness/ - DO NOT mettre de validators dans
business/→ seulement dansmodel/ - DO NOT extraire un validator utilise une seule fois → l'inliner au callsite
- DO NOT nommer avec mots generiques (
data,info,value,details) - DO NOT logger de secrets, tokens, cookies de session ou PII (email, telephone, adresse) dans les payloads → logger l'evenement, pas la valeur. Utiliser des summaries sanitisees pour les payloads d'API externes. Ajouter les headers sensibles (
cookie,set-cookie,authorization,x-api-key) auxredact.pathsde pino. Les secrets passes en query param (webhookapiKey) sont masques par le serializerreq(src/__new/lib/log/redact-req-secrets.ts) — un nouveau secret en query = ajouter sa cle aSENSITIVE_QUERY_KEYS, pas un nouveau mecanisme. Si tu logges une URL brute hors du champreq(interceptor, filter, etc.), passe-la parredactUrlSecrets— le serializer etredact.pathsne couvrent que les logs clésreq - DO NOT swallow des erreurs dans les cron tasks batch (graphile-worker) → accumuler les IDs en echec pendant la boucle, puis
throwa la fin si des items ont echoue. Graphile-worker ne retry que si le job throw. Les items deja commites sont naturellement idempotents (filtres par check de statut au prochain run) - DO NOT laisser passer une mutation d'achat quand
funding.blockedest set → fige PG pour la réconciliation Redis ; détail : domain-map §Investissement primaire + doc module - DO NOT mettre de
DEFAULTen DB sur les colonnes applicatives (migrations) → comportement explicite dans le code, l'INSERT fournit toujours la valeur. Exceptions : (1) colonnes techniques (createdAt DEFAULT now(), generated columns) ; (2)DEFAULTtemporaire de backfillADD COLUMN NOT NULL DEFAULT … ; ALTER … DROP DEFAULT→ rétro-remplit les lignes existantes en metadata-only (pas de réécriture de table) puis le code reprend la main sur les nouvelles lignes (cf. V038, V202606181612)
DO¶
- DO poser
CreateAdminActionInterceptorsur toute mutation admin (POST/PUT/PATCH/DELETE) qui change un état — exemptés : simulations compute-only et uploads S3 bruts. M2M (routesx-api-keysansreq.user) :source+ApiKeyAuthGuardsur la route,adminIdNULL. Routes BO session :adminIdviareq.user+sourceoptionnel,AdminAuthGuard+ même interceptor. Invariant DB : au moins l'un des deux non NULL. Body sensible (PII identité/bancaire) :pickBodyKeyspour ne persister que les clés utiles à l'audit. Service :__new/modules/admin-action/service/admin-action.service.ts - DO prefer
function name() {}for module-level helpers and named pure functions — reserveconst fn = () =>for inline callbacks passed as arguments - DO factoriser un validator reutilise plusieurs fois dans le meme fichier
- DO colocate un validator factorise au plus pres de la fonction qui l'utilise
Architecture modulaire¶
Modules par feature dans src/__new/modules/, flat (pas de sous-modules). Reference : project-financing-request/. Le pourquoi des choix : doc MkDocs projects/api/docs/architecture/.
| Couche | Role |
|---|---|
controllers/ |
Couche HTTP : auth (guards) + validation payload, rien d'autre. Appel service, throwApiError. Service reduit a un proxy → appel direct du repository autorise |
services/ |
Plomberie : ordre des appels, transactions, assemblage d'entites, checks de flow. Les side effects vivent ici (ou repository). Integration externe propre au module = service du module (ex. XxxS3Service), pas un provider dans __new/lib/ |
repositories/ |
Seul acces DB. ~1 table par repository. Types du domaine valides a la lecture. Pas d'interpretation metier de l'absence (get* vs find*, cf naming) |
business/ |
Contraintes logiques/metier pures uniquement : zero side effect, pas d'horloge (now en param). Un check d'une ligne reste inline dans le service ; remapping/parsing utilitaire → lib/. Tests 100% couverture — seule couche où les unit tests sont exigés (cf. §Tests) |
mappers/ |
Projection pure vers la shape Response. Recoit ses sources en parametres ; side effects dans service/repository |
guards/ |
Authn/authz NestJS, enrichit request typee. Seule couche qui throw, avec les controllers |
lib/ |
Utilitaires techniques purs specifiques au module. Ni metier ni plomberie |
tasks/, state-watcher/ |
Entry points additionnels au meme niveau que controllers/ (pas de wrapper entrypoints/) |
Modules de l'ere idtlt : dossiers au singulier (repository/, service/) + model/ (validators idtlt) — ne pas renommer.
CRUD simple = controller + repository suffit, pas besoin de service.
__new/lib/ = utilitaires generiques cross-modules (postgres, cache, kysely, providers partages, etc.)
Nommage fichiers : *.repository.ts, *.service.ts, *.model.ts, *.controller.ts, *.mapper.ts, *.guard.ts
Types forts¶
Pas de primitifs nus → utiliser les branded types de @bricks-common/api-communication.
Consulter projects/common/both/api-communication/src/common/ pour la liste complete (nombres, dates, UUIDs, etc.).
IDs : Brand<string, 'EntityId'> avec baseUUID.tagged<Id>() (jamais string).
Discriminated unions OK pour narrowing.
defined() de space-lift/commonjs/is pour verifier null/undefined.
array.push() acceptable dans scope locale.
Patterns de reference¶
Service object / Repository¶
Nommage : PascalCase sans suffixe Factory (CustomerioEmailB2bApi, pas customerioEmailB2bApiFactory).
Methodes publiques definies directement dans l'objet exporte (LSP + rename).
Helpers prives et deps module-level (logger, axios client) en dessous de l'objet exporte.
Ne JAMAIS declarer les methodes publiques comme fonctions separees puis les referencer dans l'objet.
Exporter le type : export type MonService = typeof MonService.
Usage : toujours via l'import statique — jamais d'alias, jamais de re-passage en param.
const logger = new Logger('MonService')
export const MonService = {
async maMethode(id: UUID) {
return helperPrivee(id)
},
}
function helperPrivee(id: UUID) {
logger.log(id)
}
export type MonService = typeof MonService
// appelant (controller, cron, script, helper du meme fichier)
await MonService.maMethode(id)
// MAUVAIS — alias inutile
const monService = MonService
await monService.maMethode(id)
// MAUVAIS — DI d'un service object
async function syncSomething(params: { monService: MonService }) {
await params.monService.maMethode(id)
}
await syncSomething({ monService: MonService })
// MAUVAIS — champ controller
private readonly monService = MonService
await this.monService.maMethode(id)
// BON
await MonService.maMethode(id)
Model (namespace pattern)¶
export namespace MonEntite {
export type Id = Brand<string, 'MonEntiteId'>
export const id = baseUUID.tagged<Id>()
export function Id(): Id { return uuidv4() as Id }
export const validator = object({ id, /* ... */ })
export type T = typeof validator.T
}
Toujours deriver le type du validator. IDs forts obligatoires pour toutes les entites.
Pas de .default() dans le schema Kysely : un default Zod est invisible au callsite et masque en base une valeur qu'on croit fournie. Le champ reste requis (z.boolean(), pas z.boolean().default(false)) et chaque construction pose la valeur explicitement — le compilateur force alors a ne l'oublier nulle part.
La couche DB duplique les brands/value-objects du contrat, jamais la logique metier. Un schema Kysely ne doit pas dependre d'@bricks-common/api-communication-* pour ses types : un id/value-object qui traverse un contrat se redeclare localement (export const id = z.uuid().brand<'XxxId'>(), jamais import { xxxIdSchema } puis = xxxIdSchema ; idem cle S3, coordonnees, enum de MIME). Deux brands/objets de meme forme sont structurellement compatibles (Zod brande sur la string du tag), donc la copie DB matche la copie contrat aux frontieres view/controller sans aucun cast ; as string as UUID reste interdit. Le contrat garde sa propre copie pour le front — independante par design. Ne brander que les ids/VO au format fiable (UUID persiste ou client-generated via crypto.randomUUID(), cle S3, enum) ; un id au format douteux (rep-${Date.now()}) se fiabilise d'abord. Un contrat ne porte que des schemas de forme, jamais de comportement. Une fonction (defaut, construction, regle metier) n'a rien a faire dans api-communication-* : le contrat decrit la forme des donnees echangees, point. Une valeur par defaut de colonne appartient au back, declaree DANS le schema Kysely a cote de la colonne qu'elle initialise (pendant de Id() : export function createDefaultDocumentsStep(): …).
Controller¶
@Post('endpoint')
@UserRoles(CustomerRole.CUSTOMER)
@UseGuards(JwtAuthGuard, UserRoleGuard)
async monEndpoint(@IAM() customer: Customer, @Body() body: unknown) {
const payload = httpValidate(body, validator)
const result = await MonService.methode(payload)
return match(result)
.with({ ok: true }, ({ value }) => value)
.with({ ok: false, error: 'not-found' }, () => { throw new NotFoundException() })
.exhaustive()
}
Logger : controllers → private readonly logger = new Logger(this.constructor.name) ; services, scripts et bootstrap pre-app → module-level const logger = new Logger('Name').
Error handling¶
Critere de decision : l'echec fait-il partie de la reponse qu'on veut exposer au client ?
- Oui → erreur contractuelle :
apiErr({ error, message, type })dans le service, remontee enResult, traduite parthrowApiError(result)au controller (seul point HTTP qui throw). Types exposes :not-found(404),validation-body/validation-params(400),conflict(409) — le payload part dans la reponse. - DO :
if (!row) return apiErr({ error: 'project-not-found' as const, type: 'not-found' })→ controller :if (!result.ok) return throwApiError(result) - DON'T :
throw new NotFoundException()depuis un service - Non → erreur interne (« ne devrait pas arriver » : invariant casse, S3, provider, driver PG) :
throwa la source → 500 opaque (detail loggue, jamais expose). Ne pas la transporter enResult/apiErr:validation-pg/provider-errorfinissent enInternalServerErrorException()vide, leResultn'a aucun consommateur. Variante : un echec interne degradable peut etre absorbe (champ absent, le reste servi) — decision produit explicite. - DO : lib frontiere qui retourne deja un
ResultapiErr (s3-client) →return throwApiError(result)des le premier consommateur ; sinonthrowa la source - DON'T : re-emballer un
Resultenthrow new Error(...)ni propager l'apiErrsur N couches - Routes admin/BO (contrat bricksoffice) : les toasts BO traduisent le code kebab lu dans
message→ erreur contractuelle = exception Nest portant le code (throw new NotFoundException(result.error.error)), pasthrowApiError(message humain). Erreur interne : regle ci-dessus. business/(fonctions pures) :Err('error-code' as const)nu — le service remappe enapiErrsi ca remonte au controller.- DO :
return Err('insufficient-balance' as const) - DON'T :
apiErr(...)dansbusiness/(couple le calcul pur au format HTTP + logger) - Validation runtime DB (Kysely + Zod) :
ky_parseRows/ky_parseFirstRow/ky_parseOneRow+pgParse_zodthrow sur echec schema → 500 (detail loggue, jamais expose au client).ky_parseFirstRow=firstRowValidator: 0 ligne →undefined, le service mappe enapiErrmetier si besoin.ky_parseOneRow=oneRowValidator: exactement 1 ligne sinon throw (0 ou >1) .catch()apres promesse pour convertir en ResultjsonFromStringValidatorau lieu de try/catch pour JSON
Validation¶
Tout ce qui entre dans le systeme = valide avec idonttrustlikethat :
- HTTP : httpValidate(body, validator) dans controllers
- DB : queryPgAndValidate avec validator + transactionHandler
- APIs externes : valider la reponse
- Nullable : .nullable().map(mapNullableToUndefined)
- Regles metier complexes : .and() sur le validator
- Validators communs depuis @bricks-common/api-communication
- Payloads des endpoints back-office (domaine Projects) : importer le validator depuis @bricks-common/api-communication-bricksoffice, ne pas le re-declarer dans le controller (BRI-782, source unique partagee avec bricksoffice-projects). IDs branded : minter au bord via le creator de l'entite (Model.Id(payload.x), meme idiome que Cents(n)/Percentage(n)), jamais as unknown as au callsite. Si le validator partage doit etre plus strict (ex. positiveInteger.then(cents)), renforcer le validator partage (meme type de sortie), pas une copie locale. Etat API↔package : projects/common/both/api-communication-bricksoffice/src/VALIDATOR-MUTUALIZATION.md.
Transactions¶
Mutations = safePgTransaction obligatoire.
Locking = FOR UPDATE avant tout UPDATE (sauf increments/decrements atomiques).
Err(...) dans transaction = rollback automatique, Ok(...) = commit.
Transactions imbriquees via { parent }.
setOnRollback pour cleanup en cas de rollback.
FOR UPDATE SKIP LOCKED uniquement pour workers/queues.
Un FOR UPDATE nu sur un SELECT dont le FROM est un CTE ne verrouille rien : Postgres ignore silencieusement les RTE non verrouillables (FOR UPDATE OF <cte> erreur, mais personne ne l'ecrit). Relire la ligne par id dans une requete dediee sur la vraie table — FOR UPDATE est aussi interdit dans un bras d'UNION (erreur de syntaxe).
Colonne json/jsonb ecrite par plusieurs flux : ne jamais ecrire depuis un snapshot lu avant l'acquisition du verrou — sinon lost update sur les cles des autres flux. Soit la lecture est deja sous verrou dans la meme transaction, soit on relit la ligne apres l'avoir verrouillee. Le document ecrit reste typé (GiftCard.T, InvestorFinancialDocument…) : pas de patch jsonb construit a la main.
Business¶
Fonctions pures : params strictement necessaires, pas de DB/Logger/API,
memes inputs = memes outputs. Tests 100% couverture dans *.test.ts a cote —
seule couche où les unit tests sont exigés. Un unit test qui a besoin de mocks
internes (DB, services, providers) = mauvaise cible (cf. §Tests).
Tests¶
- Ne JAMAIS créer de tests spontanément pendant une implémentation — le dev est l'initiateur, pas de bruit dans les diffs
- En fin d'implem touchant un controller / cron task / worker / webhook, poser la question : proposer d'ajouter les tests d'intégration via le skill
upsert-integration-tests - Unit tests exigés uniquement dans
business/(100% couverture) — skillupsert-unit-tests; besoin de mocks internes → test d'intégration ou rien - Review du diff de tests : skill
review-integration-tests
Stack cible Kysely + Zod¶
Stack par defaut de tout nouveau module (reference : project-financing-request), a privilegier des que le perimetre est raisonnablement independant — l'interop avec TypeORM/idtlt n'est pas optimale. Nee sur l'Espace Financement :
| Outil | Remplace | Usage |
|---|---|---|
| Kysely | queryPgAndValidate |
Query builder type-safe. getKysely() pour l'instance, ky_safePgTransaction pour les transactions |
| Zod | idonttrustlikethat |
Validation + source unique des types. Schemas dans __new/lib/kysely/schemas/ |
| apiErr | Err('code') nu |
Erreurs metier structurees : apiErr({ error, message, type }) |
| throwApiError | match(result) dans controller |
Map type metier -> exception NestJS (seul point HTTP) |
| space-lift | - | Ok/Err/Result (partage avec modules existants) |
Conventions de nommage¶
- Modules : nommage par feature, cf naming-conventions.mdc §8. Le nommage
espace-fiest abandonne — ne jamais creer de nouveau nom espace-fi. Tables EF :project_financing_request_*+project_owner_company(ex-espace_fi_spv) - Utilitaires Kysely dans
__new/lib/kysely/: prefixky_(ex:ky_parseRows,ky_safePgTransaction) - Utilitaires generiques dans
__new/lib/: pas de prefix module
Isolation des donnees¶
Les modules du domaine financement (project-financing-request...) ont leurs propres tables (project_financing_request_*, project_owner_company). Ne JAMAIS reutiliser les tables ou entites de la partie investisseur. Si une donnee existe deja cote investisseur (ex: project_owner, properties, special_purpose_vehicule), creer une table dediee au domaine. Les deux domaines sont completement isoles.
Migrations¶
Restent sur Flyway (fichiers SQL dans migration/flyway/). Pas d'utilisation des outils de migration Kysely.
- DO NOT merger une migration Flyway dont le timestamp de version est ≤ au max deja present dans
flyway_schema_historysur develop/prod — merge out-of-order → Flyway rejette le deploy (Detected resolved migration not applied) ou marque la versionIgnored(prod saute migrate/validate et deploie le code sans le schema). Re-stamper strictement apres le max courant (rename seul, contenu inchange) ; verifier que l'ancienne version n'a jamais ete appliquee avant de renommer. - DO NOT dual-read / dual-write des clés JSON legacy « pour la race de deploy » après un rename/migrate Flyway → les migrations passent avant le traffic ; détail : migrations-flyway.md §Où et quand Flyway s'exécute
Patterns¶
- Zod = source unique des types :
z.infer<typeof schema>pour deriver les types, pas de duplication - Branded types : definis dans Zod (
.brand<'TypeId'>()), propages dans Kysely automatiquement - Validation runtime DB :
ky_parseFirstRow→T | undefined(firstRowValidator) ;ky_parseOneRow→T, throw si ≠1 ligne (oneRowValidator, viaz.tuple([schema])) ;ky_parseRows→T[]; schema invalide → throw - Lectures repository : retour direct
T | undefined/T[]— pas deOk/Err/apiErrau repository - Mutations = toujours dans une transaction. Les methodes de mutation du repository prennent
trx: Transaction<Database>obligatoire (pas optionnel). Le service appelleky_safePgTransactionet passetrxau repository.Err(...)= rollback,Ok(...)= commit. Les lectures n'ont pas besoin de transaction - Erreurs :
apiErr({ error: 'code', message: '...', type: 'not-found' })dans service,throwApiError(err)dans controller. Types exposes :not-found(404),validation-body/validation-params(400),conflict(409).validation-pg/provider-error(500, payload jete) = erreurs internes → prefererthrowa la source (cf. Error handling)
DO NOT (stack Kysely + Zod)¶
- DO NOT retourner
Ok/Err/apiErrdepuis un repository Kysely — lectures =T | undefined/T[]uniquement - DO NOT utiliser
idonttrustlikethatdans les modules sur cette stack - DO NOT utiliser
queryPgAndValidatedans les modules sur cette stack - DO NOT utiliser
safePgTransaction(existant) -> utiliserky_safePgTransaction - DO NOT reutiliser des tables/entites de la partie investisseur
- DO NOT creer de dossier par module dans
__new/lib/(ex./lib/espace-fi/) — les utilitaires generiques y vivent par theme
Stack Kysely investisseur¶
Migration progressive des entites TypeORM legacy vers Kysely + Zod. Tables investisseur plates (referral-link, admin_action, …), une entite par PR.
| Outil | Usage |
|---|---|
| Kysely | getKysely(), ky_safePgTransaction pour les mutations |
| Zod | Schemas dans __new/lib/kysely/schemas/ — namespace XxxPgSchema, validateurs explicites (row, insertRow, …), z.infer — pas de Pick/Omit TS |
| queryPgAndValidate | Ecritures SQL legacy dans une safePgTransaction TypeORM existante (EntityManager) |
Enregistrement des tables¶
Chaque entite migree ajoute une entree dans Database (database.ts), section commentee investisseur. Si KyselyDb (Kysely<Database> | Transaction<Database>) n'est pas encore exporte depuis database.ts, l'ajouter une fois — les repos l'importent, pas d'alias local par fichier.
Repos (__new/modules/<module>/repository/)¶
- Import
KyselyDbdepuisdatabase.ts— pas detype KyselyDblocal par repo - Lectures hors tx :
getKysely()—params: { … }pour les filtres - Mutations dans
ky_safePgTransaction:insertOneWithKysely(params: { trx: KyselyDb; row })—insertInto+ky_parseOneRow. Updates :updateOneWithKysely(params: { trx, … }) - Appelant dans
safePgTransactionTypeORM :insertOneWithTypeorm(params: { transac: EntityManager; row })—INSERT … RETURNINGviatransac.query. Lectures/updates en tx :…WithTypeorm(params: { transac, … }) - Nommage tx :
trxcote Kysely,transaccote TypeORM (EntityManager) - Le service appelle directement la methode correspondant a sa transaction parente — pas de wrapper de dispatch
- Ajouter la branche TypeORM seulement si un grep des call sites montre un appelant encore dans
safePgTransaction - DO NOT wrapper Kysely autour d'un
EntityManager— deux stacks, une transaction a la fois
Validation runtime¶
ky_parseFirstRow / ky_parseOneRow / pgParse_zod : firstRowValidator vs oneRowValidator (cf. pg-return.ts). Schema invalide ou oneRow avec ≠1 ligne → throw (500 + log). find* / get* nullable → ky_parseFirstRow (T | undefined). Reference : project-financing-request/.
Tables legacy partagees — lock idtlt ↔ zod¶
Regle simple : create/update via idtlt, read via Kysely. Sur une table partagee, les ecritures restent sur le write-side typeorm+idtlt ; un XxxPgSchema est par defaut une slice de lecture.
La DB est la seule source de verite ; idtlt et zod n'en sont que des projections, et la shape interne des colonnes jsonb echappe a toute validation SQL. Chaque XxxPgSchema d'une table ayant un modele idtlt write-side porte donc un lock compile-time (__new/lib/kysely/ky-schema-lock.ts) : la divergence casse tsc au lieu d'attendre la prod.
| Le code zod/Kysely… | Schema zod | Lock |
|---|---|---|
| lit la table (le defaut) | slice minimale (champs lus uniquement) | Assert<Extends<IdtltModel, Slice>> |
| ecrit encore une colonne aussi ecrite par idtlt (exception transitoire) | miroir complet | Assert<Exact<IdtltModel, Mirror>> |
Brands d'ids d'entite : un seul brand canonique cote zod (package contrat s'il existe, sinon le PgSchema de la table), qui raffine le brand UUID partage (uuid_zod.brand<'XxxId'>() — reste assignable la ou les contrats attendent uuid_zod) ; le modele idtlt l'aliasse (pattern Cents). Jamais deux brands homonymes : les encodages Brand<> helpers et z.$brand sont inassignables entre eux. Reference : property-construction-budget-request.schema.ts (miroir), project-payment-schedule.schema.ts (slice).
Debug d'un lock casse — tsc echoue avec Type 'false' does not satisfy the constraint 'true', sans nommer le champ : decouper l'assertion champ par champ avec le meme helper (Assert<Exact<L['champ'], Z['champ']>> par ligne) dans un fichier temporaire — le champ fautif ressort immediatement.
DO NOT (investisseur Kysely)¶
- DO NOT migrer plusieurs entites dans une seule PR infra — poser la foundation, puis une entite par PR
- DO NOT ajouter une methode TypeORM dans un repo si tous les appelants sont deja dans
ky_safePgTransaction - DO NOT ajouter un
XxxPgSchemasur une table qui a une entite TypeORM/idtlt sans poser son lock (Extendsen lecture,Exacten ecriture) - DO NOT introduire de nouvelle ecriture Kysely sur une table legacy partagee — create/update passent par idtlt
Routes internes (modules internal-*)¶
Routes server-to-server pour les apps internes hors monorepo (ticketing care, ia-whatsapp, gestion-des-defauts, ai-agent) qui remplacent leurs requetes SQL directes sur la DB Bricks.
- Modules :
src/__new/modules/internal-*/, routes prefixees/internal/<domain> - Contrats : descriptors zod dans
@bricks-common/api-communication-internal(meme stack queapi-communication-project-owner), bodies viabodySafeParse_zod. Consommateurs hors monorepo → la doc d'endpoints auto-generee (MkDocs) est la reference du contrat - Auth : 1 cle API dediee par app consommatrice via
ApiAuthService; routes partagees →assertApiKeyInHeadersAnyOf. Exception : une cle par surface quand plusieurs clients du meme chantier consomment les memes routes (project-owner-invoicing= middleware invoice-axonaut + Bubble). Cles requises en production (refinement env) car cle absente = auth desactivee - Donnees : lecture seule sur les tables legacy via
queryPgAndValidate+ idtlt — PAS Kysely, pas d'isolation de domaine : ces routes lisent les tables investisseur existantes - Repos : la query vit dans le repo prod du domaine s'il existe (methode ajoutee au besoin — les tests
/internalcouvrent alors du code prod) ; repointernal-*seulement pour les projections sans foyer naturel. Lecture leniente via methode dediee quand le validateur strict du domaine peut echouer sur du legacy - Reponses : valeurs brutes (cents, dates ISO), formatage FR cote consommateur ; parite 1:1 avec la requete SQL remplacee, pas de pagination. Shaping row → contrat dans
views/des que ce n'est plus un passe-plat (nullable DB → optional contrat, renommage) : le controleur ne construit pas d'objet de reponse - Tests : happy path uniquement ; l'auth est couverte une fois par le guard test de
/internal/ping, pas re-testee par route - SQL operateur (
/internal/segments) : execute derriere le role PG read-only dedie (DATABASE_USER_RO/DATABASE_PASS_RO, premierDATABASE_SLAVESsinon master) viaqueryPgAndValidate({ readOnly: true })(et{ includeColumns: true }quand on a besoin deresult.fields[]) — la frontiere secu est le role PG (grants DevOps), pas de parsing SQL en code ; erreurs pg (DatabaseError) → 400invalid-segment-sqlavec le message pg (debug operateur), jamais 500 ; SQL operateur toujours encapsule en derived table, parenthese fermante sur sa propre ligne (un-- commentairefinal la commenterait)
Domain documentation maintenance¶
When modifying business logic in __new/modules/*/business/ or */service/ :
- If domain-map.mdc needs updating (new bounded context, changed business rules, new workflow), update it
- If an AGENTS.md exists in the module, update it if business rules change
- If no AGENTS.md exists and the change is significant, propose creating one