# ladocβ„’ by MYRE ## docs - [Engine](/docs/engine.md): Repository to shared typescript stuff for all others repositories - [API Decorators Reference](/docs/engine/api/decorators.md): Comprehensive reference of all API decorators available in the MYRE framework - [API Framework Fundamentals](/docs/engine/api/fundamentals.md): Core concepts and principles of the MYRE API framework - [API Patterns](/docs/engine/api/patterns.md): Common patterns and best practices for MYRE API development - [Validation Integration](/docs/engine/api/validation-integration.md): How the API framework integrates with the validation system for request/response validation and SDK generation - [Validator Decorators Reference](/docs/engine/validators/decorators.md): Complete reference table for all validation decorators available in MYRE - [Real-World Examples](/docs/engine/validators/examples.md): Real validation patterns from best practices - [Validation Fundamentals](/docs/engine/validators/fundamentals.md): Core concepts and principles of the MYRE validation system - [Validation Entity Patterns](/docs/engine/validators/patterns.md): Pure validation entity design patterns and best practices - [SEQ](/docs/seq.md): Repository for shared typescript stuff related to Sequelize - [Counter increment](/docs/seq/counter-incrementer.md): Description - [Table integrities](/docs/seq/table-integrities.md): Integrity check - [Updating references](/docs/seq/update-reference.md): In some cases, we need to update references to an object in the database by a new value. - [Services](/docs/services.md): Repository of shared services for all other repositories - [Encrypted Records](/docs/services/encrypted-record-dao.md): The purpose of this DAO is to store both the encrypted value and the encryption information used in the same place. This allows the data to --- # Full Documentation Content ## Engine > Repository to shared typescript stuff for all others repositories # Engine Repository to shared typescript stuff for all others repositories [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MYRE-CORP_ENGINE\&metric=alert_status\&token=888134517ecbe51b8297583059c2469d9266a132)](https://sonarcloud.io/summary/new_code?id=MYRE-CORP_ENGINE) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=MYRE-CORP_ENGINE\&metric=ncloc\&token=888134517ecbe51b8297583059c2469d9266a132)](https://sonarcloud.io/summary/new_code?id=MYRE-CORP_ENGINE) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=MYRE-CORP_ENGINE\&metric=coverage\&token=888134517ecbe51b8297583059c2469d9266a132)](https://sonarcloud.io/summary/new_code?id=MYRE-CORP_ENGINE) ## [πŸ—ƒοΈ API Framework](/docs/api/) [4 items](/docs/api/) ## [πŸ—ƒοΈ Business Plan Module](/docs/bp/) [2 items](/docs/bp/) ## [πŸ—ƒοΈ Cashflow Module](/docs/cashflow/) [3 items](/docs/cashflow/) ## [πŸ“„οΈ CLI](/docs/engine/cli/) [GitHub](/docs/engine/cli/) ## [πŸ“„οΈ Configuration Management](/docs/conf/) [Configuration management utilities for loading environment variables](/docs/conf/) ## [πŸ“„οΈ Crypto Module](/docs/crypto/) [Encryption and decryption utilities using AES-256-GCM algorithm](/docs/crypto/) ## [πŸ“„οΈ Excel Module](/docs/excel/) [Excel file building and parsing utilities](/docs/excel/) ## [πŸ—ƒοΈ MYRE PDF Generation System](/docs/pdf/) [4 items](/docs/pdf/) ## [πŸ—ƒοΈ Validation System](/docs/validators/) [4 items](/docs/validators/) ## Testing[​](#testing "Direct link to Testing") ### Scope restriction[​](#scope-restriction "Direct link to Scope restriction") If you want to run tests only for a specific module, you can use `-m` or `--module` flag. ``` npm run test:dev -- -m pdf ``` It will run tests only for spec files located in `src/pdf` module. If you want to run tests for several modules, you can type: ``` npm run test:dev -- -m pdf -m excel ``` If you want to exclude some modules, you can use `-e` or `--exclude` flag. For more information about the test command: `npm run test -- -h`. This implementation is located in `src/cli/test/jasmine.runner.ts` --- ## API Decorators Reference > Comprehensive reference of all API decorators available in the MYRE framework # API Decorators Reference This page provides a comprehensive reference of all API decorators available in the MYRE framework for building REST APIs. ## Import Guidelines[​](#import-guidelines "Direct link to Import Guidelines") Most API decorators are imported from `@myre/engine/api`: ``` import { Controller, Get, Post, Put, Delete, Patch, Body, ReturnType, Auth, Rights, Version, ApiDescription, ApiOperation, Query, QueryRequired, Param, File, inject, Service, Scope, SCOPES, PrimitiveReferences } from '@myre/engine/api'; import { Transactional } from '@myre/seq/core'; import { PermissionRight } from '@myre/engine/models'; import { PermissionResource } from '@myre/engine/models/am'; ``` ## Controller Decorators[​](#controller-decorators "Direct link to Controller Decorators") ### @Controller()[​](#controller "Direct link to @Controller()") Defines a class as an API controller. Can specify route prefix and options: ``` import { Controller } from '@myre/engine/api'; // Basic controller @Controller() export class UserApi { // endpoints } // Controller with route prefix @Controller('/users') export class UserApi { // endpoints will be prefixed with /users } // Controller with middlewares @Controller('/users', [authMiddleware, loggingMiddleware]) export class UserApi { // endpoints } ``` ### @Service()[​](#service "Direct link to @Service()") Marks a class as a service for dependency injection: ``` import { Service, inject } from '@myre/engine/api'; @Service({ summary: 'User management service' }) export class UserService { private readonly _userDao = inject(UserDao); } ``` ## HTTP Method Decorators[​](#http-method-decorators "Direct link to HTTP Method Decorators") ### @Get()[​](#get "Direct link to @Get()") Defines a GET endpoint: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) public getUsers(req: Request): Promise { return this._userService.getUsers(req.customer); } } @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User) public getUser(req: Request): Promise { // req.user populated by Auth class return this._userService.getUser(req.user); } } ``` ### @Post()[​](#post "Direct link to @Post()") Defines a POST endpoint: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @ReturnType(User) @Version('1.0.0') public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { return this._userService.createUser(body); } } ``` ### @Put()[​](#put "Direct link to @Put()") Defines a PUT endpoint: ``` @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Put('/') @ReturnType(User) @Version('1.0.0') public updateUser( req: Request, @Body(UpdateUser) body: UpdateUser ): Promise { // req.user populated by Auth class return this._userService.updateUser(req.user, body); } } ``` ### @Delete()[​](#delete "Direct link to @Delete()") Defines a DELETE endpoint: ``` @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Delete('/') @ReturnType(PrimitiveReferences.Boolean) @Version('1.0.0') public async deleteUser(req: Request): Promise { // req.user populated by Auth class await this._userService.deleteUser(req.user); return true; } } ``` ### @Patch()[​](#patch "Direct link to @Patch()") Defines a PATCH endpoint: ``` @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Patch('/status') @ReturnType(User) public updateUserStatus( req: Request, @Body(UpdateUserStatus) body: UpdateUserStatus ): Promise { // req.user populated by Auth class return this._userService.updateUserStatus(req.user, body); } } ``` ## Parameter Decorators[​](#parameter-decorators "Direct link to Parameter Decorators") ### @Body()[​](#body "Direct link to @Body()") Validates and injects request body. **Mandatory for all endpoints with request bodies:** ``` // Simple body validation @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { return this._userService.createUser(body); } } // Body validation with picker function for dynamic types @Controller('/:entityId') @Auth(EntityAuth) export class EntityApi { private readonly _entityService = inject(EntityService); @Put('/') @ReturnType(EntityUnion) public updateEntity( req: Request, @Body([PersonUpdate, CompanyUpdate], entityUpdatePicker) body: PersonUpdate | CompanyUpdate ): Promise { // req.entity populated by EntityAuth class return this._entityService.updateEntity(req.entity, body); } } ``` ### @ReturnType()[​](#returntype "Direct link to @ReturnType()") Specifies the return type for SDK generation. **Mandatory for all endpoints:** ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); // Array return @Get('/') @ReturnType(User, { isArray: true }) public getUsers(req: Request): Promise { // implementation } } @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); // Single entity return @Get('/') @ReturnType(User) public getUser(req: Request): Promise { // implementation } // Primitive return @Delete('/') @ReturnType(PrimitiveReferences.Boolean) public deleteUser(req: Request): Promise { // implementation } // String return @Get('/avatar-url') @ReturnType(PrimitiveReferences.String) public getUserAvatarUrl(req: Request): Promise { // implementation } } ``` #### Type Handling: Classes vs Interfaces[​](#type-handling-classes-vs-interfaces "Direct link to Type Handling: Classes vs Interfaces") **Important**: When using entity types with the `@ReturnType()` decorator, you must use **classes** - you obviously can't pass interfaces since they don't exist at runtime. This creates a challenge in passthrough services where you need to return entity types from other services. **The Problem**: Generated types in MYRE are interfaces, not entity classes: ``` // ❌ This won't work - Person is an interface, not a class import { type Person } from '@myre/entity/core'; @Controller('/customers/:customerId/people/:personId') @Auth(PersonAuth) export class PersonApi { private readonly _entityService = inject(EntityService); @Get('/') @ReturnType(Person) // ❌ TypeScript error: Person is not a class public getPerson(req: Request): Promise { // Passthrough to Entity service return this._entityService.getPerson(req.person.id); } } ``` **The Solution**: Use `@myre/entity/core/internal` to import actual classes: ``` // βœ… Correct approach - import the class from internal import { Person } from '@myre/entity/core/internal'; @Controller('/customers/:customerId/people/:personId') @Auth(PersonAuth) export class PersonApi { private readonly _entityService = inject(EntityService); @Get('/') @ReturnType(Person) // βœ… Works - Person is now a class public getPerson(req: Request): Promise { // Passthrough to Entity service return this._entityService.getPerson(req.person.id); } } ``` **Handling Naming Conflicts**: When you need both the interface and the class, use aliases: ``` import { type Person } from '@myre/entity/core'; import { Person as PersonCV } from '@myre/entity/core/internal'; @Controller('/customers/:customerId/people/:personId') @Auth(PersonAuth) export class PersonApi { private readonly _entityService = inject(EntityService); @Get('/') @ReturnType(PersonCV) // Use class for decorator public getPerson(req: Request): Promise { // Use interface for typing return this._entityService.getPerson(req.person.id); } } ``` **Key Points**: * Generated types from `@myre/entity/core` are interfaces for type safety * `@ReturnType()` decorator requires classes for runtime metadata * Use `@myre/entity/core/internal` to access the actual entity classes * **Classes must be manually exported** in the `/internal` module to be available for `@ReturnType()` usage * This pattern is essential for passthrough services that proxy calls to other services * Classes in `/internal` are the same entities used to generate the interface types ```` ### @Param() Extracts and validates route parameters. **Note: @Param() should only be used in Auth classes, not in controllers.** ```typescript // ❌ Avoid using @Param in controllers @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Get('/:userId') public getUser( req: Request, @Param('userId', Number) userId: number ): Promise { return this._userService.getUserById(userId); } } // βœ… Use Auth classes to populate request objects instead @Controller('/:userId') @Auth(UserAuth) // UserAuth extracts userId and populates req.user export class UserApi { private readonly _userService = inject(UserService); @Get('/') public getUser(req: Request): Promise { return this._userService.getUser(req.user); } } ```` **@Param() usage in Auth classes:** For complete Auth class implementation patterns and examples, see [Authentication Patterns](/docs/engine/api/patterns.md#authentication-patterns). ### @Query()[​](#query "Direct link to @Query()") Extracts optional individual query parameters: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) public getUsers( req: Request, @Query('search', String) search?: string, @Query('limit', Number) limit?: number, @Query('status', String) status?: UserStatus ): Promise { return this._userService.getUsers(req.customer, { search, limit, status }); } @Get('/filtered') @ReturnType(User, { isArray: true }) public getFilteredUsers( req: Request, @Query('roleIds', String) roleIdsString?: string, // "1,2,3" @Query('active', Boolean) active?: boolean ): Promise { const roleIds = roleIdsString ? roleIdsString.split(',').map(Number) : undefined; return this._userService.getFilteredUsers(req.customer, { roleIds, active }); } } ``` ### @QueryRequired()[​](#queryrequired "Direct link to @QueryRequired()") Extracts required query parameters: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Get('/export') @ReturnType(PrimitiveReferences.String) public exportUsers( req: Request, @QueryRequired('format', String) format: string ): Promise { return this._userService.exportUsers(format); } } ``` ## Authentication & Authorization[​](#authentication--authorization "Direct link to Authentication & Authorization") ### @Auth()[​](#auth "Direct link to @Auth()") Specifies authentication requirements: ``` import { UserAuth } from '../auth/user.auth'; @Controller('/users') @Auth(UserAuth) // Applied to all endpoints in controller export class UserApi { @Get('/') @Auth(AdminAuth) // Override for specific endpoint public getUsers(req: Request): Promise { // implementation } } ``` ### @Rights()[​](#rights "Direct link to @Rights()") Specifies authorization requirements: ``` import { PermissionRight } from '@myre/engine/models'; import { PermissionResource } from '@myre/engine/models/am'; @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @Rights([ { resource: PermissionResource.USER, right: PermissionRight.READ } ]) public getUsers(req: Request): Promise { // implementation } @Post('/') @Rights([ { resource: PermissionResource.USER, right: PermissionRight.WRITE } ]) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { // implementation } } ``` ### @Protect()[​](#protect "Direct link to @Protect()") Specifies protection strategy: ``` import { PROTECT_STRATEGY } from '@myre/engine/api'; @Controller('/users') @Protect(PROTECT_STRATEGY.AUTH) export class UserApi { // All endpoints require authentication } ``` ## Documentation Decorators[​](#documentation-decorators "Direct link to Documentation Decorators") ### @ApiDescription()[​](#apidescription "Direct link to @ApiDescription()") Describes the controller for Swagger documentation: ``` @Controller('/users') @ApiDescription({ description: 'User management operations' }) export class UserApi { // endpoints } ``` ### @ApiOperation()[​](#apioperation "Direct link to @ApiOperation()") Describes individual operations for Swagger: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @ApiOperation({ summary: 'Create a new user', description: 'Creates a new user account with the provided information. The user will be created with default permissions.' }) @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { // implementation } } ``` ## Versioning[​](#versioning "Direct link to Versioning") ### @Version()[​](#version "Direct link to @Version()") Specifies API version: ``` @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @Version('1.0.0') @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { // implementation } } @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Put('/') @Version('1.1.0') // Updated version with new features @ReturnType(User) public updateUser( req: Request, @Body(UpdateUserV2) body: UpdateUserV2 ): Promise { // req.user populated by Auth class return this._userService.updateUser(req.user, body); } } ``` ## Advanced Decorators[​](#advanced-decorators "Direct link to Advanced Decorators") ### @Scope()[​](#scope "Direct link to @Scope()") Defines service scope: ``` import { SCOPES } from '@myre/engine/api'; @Controller('/users') @Scope(SCOPES.SINGLETON) export class UserApi { // Controller will be singleton } ``` ### @OnSend()[​](#onsend "Direct link to @OnSend()") Triggers actions after response is sent: ``` import { FeedAction } from '@myre/engine/feed'; @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @OnSend(UserFeedService, { action: FeedAction.Create }) @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { // Feed service will be called after response return this._userService.createUser(body); } } ``` ### @File()[​](#file "Direct link to @File()") Handles file uploads: ``` import { FileAndMetadata } from '@myre/engine/api'; import { AbstractError } from '@myre/engine/core'; export class MissingFileError extends AbstractError { constructor() { super(50007, { en: 'File is required for this operation', fr: 'Un fichier est requis pour cette opΓ©ration', }); } } @Controller('/:userId') @Auth(UserAuth) export class UserApi { private readonly _userService = inject(UserService); @Post('/upload-avatar') @ReturnType(User) public uploadAvatar( req: Request, @File() file: FileAndMetadata ): Promise { if (!file) { throw new MissingFileError(); } return this._userService.uploadAvatar(req.user, file); } } ``` ### @Transactional()[​](#transactional "Direct link to @Transactional()") Provides automatic transaction management for service methods. For complete documentation and examples, see [Transaction Management](/docs/engine/api/fundamentals.md#transaction-management). **Key Points:** * Only use in service methods, not in controllers or DAOs * Transaction parameter is automatically injected by the framework * DAO methods should always have a non-optional transaction as the last parameter ## Dependency Injection[​](#dependency-injection "Direct link to Dependency Injection") ### inject()[​](#inject "Direct link to inject()") Injects dependencies into services: ``` @Service() export class UserService { private readonly _userDao = inject(UserDao); private readonly _emailService = inject(EmailService); public async createUser(data: CreateUser): Promise { const user = await this._userDao.createUser(data); await this._emailService.sendWelcomeEmail(user.email); return user; } } ``` ## Complete Controller Example[​](#complete-controller-example "Direct link to Complete Controller Example") ``` import { Controller, Get, Post, Put, Delete, Body, ReturnType, Auth, Rights, Version, ApiDescription, ApiOperation, Query, inject, Scope, SCOPES } from '@myre/engine/api'; import { PermissionRight } from '@myre/engine/models'; import { PermissionResource } from '@myre/engine/models/am'; import { PrimitiveReferences } from '@myre/engine/api'; // Customer-scoped root controller @Controller('/customers/:customerId') @Scope(SCOPES.SINGLETON) @Auth(CustomerAuth) // CustomerAuth extracts customerId and populates req.customer @ApiDescription({ description: 'Customer management operations' }) export class CustomerApi { private readonly _customerService = inject(CustomerService); @Get('/') @Rights([ { resource: PermissionResource.CUSTOMER, right: PermissionRight.READ } ]) @Version('1.0.0') @ApiOperation({ summary: 'Get customer details' }) @ReturnType(Customer) public getCustomer(req: Request): Promise { // req.customer populated by CustomerAuth return this._customerService.getCustomer(req.customer); } } // Nested collection controller under customer scope @Controller('/users') @Scope(SCOPES.SINGLETON) @ApiDescription({ description: 'User management operations' }) export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @Rights([ { resource: PermissionResource.USER, right: PermissionRight.READ } ]) @Version('1.0.0') @ApiOperation({ summary: 'Get all users', description: 'Retrieves a list of all users with optional filtering' }) @ReturnType(User, { isArray: true }) public getUsers( req: Request, @Query('search', String) search?: string, @Query('limit', Number) limit?: number, @Query('active', Boolean) active?: boolean ): Promise { return this._userService.getUsers({ loggedUser: req.user, customer: req.customer }, { search, limit, active }); } @Post('/') @Rights([ { resource: PermissionResource.USER, right: PermissionRight.WRITE } ]) @Version('1.0.0') @ApiOperation({ summary: 'Create a new user' }) @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { return this._userService.createUser({ loggedUser: req.user, customer: req.customer }, body); } } // Individual user controller @Controller('/') @Auth(UserAuth) // Populates req.user @ApiDescription({ description: 'Individual user operations' }) export class UserApi { private readonly _userService = inject(UserService); @Delete('/') @Rights([ { resource: PermissionResource.USER, right: PermissionRight.DELETE } ]) @Version('1.0.0') @ReturnType(PrimitiveReferences.Boolean) public deleteUser(req: Request): Promise { // req.user populated by UserAuth await this._userService.deleteUser(req.user); return true; } } ``` ## Automatic Generation Features[​](#automatic-generation-features "Direct link to Automatic Generation Features") The framework automatically generates TypeScript SDKs and seed functions based on your `@ReturnType()` and `@Body()` decorators. For complete information, see the [main API documentation](/docs/api/#sdk-generation). ### Seed Generation[​](#seed-generation "Direct link to Seed Generation") ``` import { seedUserFromApi, seedCreateUserFromApi } from '@myre/your-service/core/testing'; // Generate plain JS objects with valid data const userData = seedUserFromApi(); const createData = seedCreateUserFromApi({ name: 'Custom Name' }); ``` **Note:** Seed functions return plain JavaScript objects, not entity instances. ## Best Practices[​](#best-practices "Direct link to Best Practices") 1. **Always use @ReturnType()** - Required for SDK generation and seed function creation 2. **Always use @Body()** - Required for endpoints with request bodies and seed generation 3. **Use Auth classes for parameter validation** - Avoid direct @Param() usage in controllers 4. **Use @Version()** - For API versioning and tracking 5. **Add @ApiOperation()** - For better Swagger documentation 6. **Specify @Rights()** - For proper authorization 7. **Use individual @Query()/@QueryRequired() parameters** - Not entity objects for query validation 8. **Use specific parameter types** - Number, String, Boolean for @Param and @Query 9. **Combine decorators logically** - Auth, Rights, Version, ReturnType together --- ## API Framework Fundamentals > Core concepts and principles of the MYRE API framework # API Framework Fundamentals ## Core Decorators[​](#core-decorators "Direct link to Core Decorators") ### Class Decorators[​](#class-decorators "Direct link to Class Decorators") #### @Controller()[​](#controller "Direct link to @Controller()") Defines a class as an API controller. The `@Controller()` decorator provides metadata that the framework uses to organize the application structure. ``` import { Controller } from '@myre/engine/api'; // Basic controller @Controller() export class UserApi { // endpoints } // Controller with route prefix @Controller('/users') export class UserApi { // All endpoints will be prefixed with /users } // Controller with middlewares @Controller('/users', [authMiddleware, loggingMiddleware]) export class UserApi { // endpoints with middleware applied } ``` For complete `@Controller()` options and signatures, see [Decorators](/docs/engine/api/decorators.md#controller). #### @Service()[​](#service "Direct link to @Service()") Registers a class for dependency injection: ``` import { Service, inject } from '@myre/engine/api'; @Service() export class UserService { private readonly _userDao = inject(UserDao); // Scope pattern: consolidate related entities into single parameter public async getUsers(scope: { loggedUser: User; customer: Customer; }): Promise { return this._userDao.findAll(scope); } } // Service with metadata @Service({ summary: 'User management service' }) export class UserService { private readonly _userDao = inject(UserDao); // Service implementation } ``` ### HTTP Method Decorators[​](#http-method-decorators "Direct link to HTTP Method Decorators") HTTP method decorators make controller methods into API endpoints and can add routing. Available decorators include `@Get()`, `@Post()`, `@Put()`, `@Patch()`, and `@Delete()`. For complete controller examples including routing patterns, see [Controller Patterns](/docs/engine/api/patterns.md#controller-patterns) and [Complete Controller Example](/docs/engine/api/decorators.md#complete-controller-example). The method decorators combine with the controller route to create the full endpoint path. Each method decorator can optionally specify additional routing that gets appended to the controller's base route. ### Parameter Decorators[​](#parameter-decorators "Direct link to Parameter Decorators") #### @Body() - Mandatory for Request Bodies[​](#body---mandatory-for-request-bodies "Direct link to @Body() - Mandatory for Request Bodies") ``` import { Controller, Post, Body, ReturnType, inject } from '@myre/engine/api'; @Controller('/users') export class UserApi { private readonly _userService = inject(UserService); @Post('/') @ReturnType(User) public createUser( @Body(CreateUser) body: CreateUser ): Promise { return this._userService.createUser(body); } } ``` #### @Query() - Optional Parameters[​](#query---optional-parameters "Direct link to @Query() - Optional Parameters") ``` import { Controller, Get, Query, ReturnType, inject } from '@myre/engine/api'; import { Request } from 'express'; @Controller('/users') export class UserApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) public getUsers( req: Request, @Query('search', String) search?: string, @Query('limit', Number) limit?: number ): Promise { return this._userService.getUsers(req.customer, { search, limit }); } } ``` #### @QueryRequired() - Required Parameters[​](#queryrequired---required-parameters "Direct link to @QueryRequired() - Required Parameters") ``` import { Controller, Get, QueryRequired, ReturnType, inject, PrimitiveReferences } from '@myre/engine/api'; import { Request } from 'express'; @Controller('/users') export class UserApi { private readonly _userService = inject(UserService); @Get('/export') @ReturnType(PrimitiveReferences.String) public exportUsers( req: Request, @QueryRequired('format', String) format: string ): Promise { return this._userService.exportUsers(format); } } ``` ### @ReturnType() - Mandatory for All Endpoints[​](#returntype---mandatory-for-all-endpoints "Direct link to @ReturnType() - Mandatory for All Endpoints") Specifies return type for SDK generation. See [Decorators](/docs/engine/api/decorators.md#returntype) for complete reference and [Validation Integration](/docs/engine/api/validation-integration.md#response-typing-with-returntype) for integration patterns. ``` import { Controller, Get, ReturnType, inject } from '@myre/engine/api'; @Controller('/users') export class UserApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) public getUsers(): Promise { return this._userService.getUsers(); } } ``` ## Dependency Injection[​](#dependency-injection "Direct link to Dependency Injection") The framework provides a full dependency injection system. Anything that can be injected is called a `provider`. ### Basic Injection[​](#basic-injection "Direct link to Basic Injection") ``` import { Service, inject } from '@myre/engine/api'; @Service() export class UserService { private readonly _userDao = inject(UserDao); public async getUsers(): Promise { return this._userDao.findAll(); } } ``` ### Advanced Provider Patterns[​](#advanced-provider-patterns "Direct link to Advanced Provider Patterns") Providers can be registered at bootstrap level with various strategies: ``` import { Service, inject, InjectionToken } from '@myre/engine/api'; export const envToken = new InjectionToken('env'); export const configToken = new InjectionToken('config'); @Service() export class MyService1 { private _config = inject(configToken); } @Service() export class MyService2 { constructor( private _myService1: MyService1 ) { } } // Provider registration in bootstrap providers: [ MyService1, MyService2, { provide: envToken, useValue: process.env.NODE_ENV, }, { provide: configToken, useFactory: (env: string) => generateConfFromEnv(env), deps: [envToken] }, ] ``` ### Multiple Dependencies[​](#multiple-dependencies "Direct link to Multiple Dependencies") ``` import { Service, inject } from '@myre/engine/api'; @Service() export class UserService { private readonly _userDao = inject(UserDao); private readonly _emailService = inject(EmailService); private readonly _logService = inject(LogService); public async createUser(data: CreateUser): Promise { const user = await this._userDao.createUser(data); await this._emailService.sendWelcomeEmail(user.email); this._logService.logUserCreation(user.id); return user; } } ``` ## Authentication & Authorization[​](#authentication--authorization "Direct link to Authentication & Authorization") The framework provides `@Auth()` and `@Rights()` decorators for securing endpoints. **Key Concepts:** * **Auth Classes**: Validate parameters and populate request objects (e.g., `req.user`, `req.customer`) * **Rights-Based Authorization**: Use `@Rights()` for fine-grained permission control * **Security-First Design**: Avoid direct parameter access in controllers For complete authentication patterns, Auth class implementation, and examples, see [Authentication Patterns](/docs/engine/api/patterns.md#authentication-patterns). ## Application Bootstrap[​](#application-bootstrap "Direct link to Application Bootstrap") An application is bootstrapped via the `bootstrap` method that you call with the application configuration and returns a `Promise` of the `Server`. ``` import { bootstrap } from '@myre/engine/api'; import { type Bootstrap } from '@myre/engine/api'; // Bootstrap configuration example with customer-scoped structure const config: Bootstrap = { port: 4242, controllers: [ { route: '/customers/:customerId', controller: CustomerApi, // Has @Auth(CustomerAuth) children: [ UsersApi, // Nested under customer scope OrdersApi, // Nested under customer scope { route: '/users/:userId', controller: UserApi, // Individual user operations }, ], }, PublicApi, // Public endpoints without customer scope ], providers: [ UserService, CustomerService, { provide: Token, useValue: 'VALUE', }, ], }; // Start the application const server = await bootstrap(config); ``` The bootstrap configuration is mainly used to declare the port, controllers, and providers for your application. ## Transaction Management[​](#transaction-management "Direct link to Transaction Management") ### @Transactional()[​](#transactional "Direct link to @Transactional()") Provides automatic transaction management for service methods. **Important**: The `@Transactional()` decorator automatically adds a transaction parameter at the end of the method signature. ``` import { Service, inject } from '@myre/engine/api'; import { Transactional } from '@myre/seq/core'; import { Transaction } from 'sequelize'; @Service() export class UserService { private readonly _userDao = inject(UserDao); @Transactional() public async createUser( scope: { loggedUser: User; customer: Customer; organization: Organization; }, data: CreateUser, transaction?: Transaction // automatically added by @Transactional() ): Promise { // Transaction is automatically created and passed to DAO methods return this._userDao.createUser(scope, data, transaction!); } } ``` **Key Points:** * The transaction parameter is automatically injected by the framework * DAO methods should always have a non-optional transaction as the last parameter * Use `@Transactional()` only in service methods, not in DAOs or controllers ## API Documentation with Swagger[​](#api-documentation-with-swagger "Direct link to API Documentation with Swagger") A Swagger specification is automatically generated on bootstrap and served via `swagger-ui-express`. While the application is running, open your browser and navigate to `http://localhost:port/api-doc` to see the Swagger UI. ### @ApiDescription()[​](#apidescription "Direct link to @ApiDescription()") Describes controllers for Swagger documentation: ``` import { Controller, ApiDescription } from '@myre/engine/api'; @Controller('/users') @ApiDescription({ description: 'User management operations' }) export class UserApi { // endpoints } ``` ### @ApiOperation()[​](#apioperation "Direct link to @ApiOperation()") Describes individual operations for Swagger: ``` import { Controller, Post, Body, ReturnType, inject, ApiDescription, ApiOperation } from '@myre/engine/api'; import { Request } from 'express'; @Controller('/users') @ApiDescription({ description: 'User management operations' }) export class UserApi { private readonly _userService = inject(UserService); @Post('/') @ApiOperation({ summary: 'Create a new user', description: 'Creates a new user account with the provided information' }) @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { return this._userService.createUser(body); } } ``` The Swagger documentation is automatically generated based on your decorators, especially `@ReturnType()` and `@Body()` decorators which provide type information for request and response schemas. ## Framework Features[​](#framework-features "Direct link to Framework Features") ### Request/Response Integration[​](#requestresponse-integration "Direct link to Request/Response Integration") * **Automatic request validation** through validation entities * **Type-safe parameter extraction** with automatic type conversion * **Consistent error responses** for validation failures and business logic errors ### Error Handling[​](#error-handling "Direct link to Error Handling") * **Validation errors**: Automatically return 400 Bad Request * **Authentication errors**: Appropriate HTTP status codes * **Business logic errors**: Custom error handling in services * **Unexpected errors**: Global error handler returns 500 Internal Server Error --- ## API Patterns > Common patterns and best practices for MYRE API development # API Patterns This page covers common development patterns and best practices for the MYRE API framework. ## Controller Patterns[​](#controller-patterns "Direct link to Controller Patterns") ### Two Controllers Pattern[​](#two-controllers-pattern "Direct link to Two Controllers Pattern") Use plural names for collections and singular for individual resources. For complete decorator reference, see [Decorators](/docs/engine/api/decorators.md). **Customer-Scoped Architecture**: `@Auth(CustomerAuth)` should only be applied at the customer-scoped root level ( `/customers/:customerId`). Nested controllers inherit the customer context from the parent route without needing the decorator. #### Collection Controllers (Plural)[​](#collection-controllers-plural "Direct link to Collection Controllers (Plural)") ``` // Customer-scoped root controller @Controller('/customers/:customerId') @Auth(CustomerAuth) // CustomerAuth extracts customerId and populates req.customer export class CustomerApi { @Get('/') @ReturnType(Customer) public getCustomer(req: Request): Promise { // req.customer populated by CustomerAuth return req.customer; } } // Nested collection controller under customer scope @Controller('/users') export class UsersApi { private readonly _userService = inject(UserService); // GET /customers/:customerId/users - Get all users for customer @Get('/') @ReturnType(User, { isArray: true }) public getUsers(req: Request): Promise { // Controller extracts entities from request and passes them as scope object return this._userService.getUsers({ loggedUser: req.user, customer: req.customer }); } // POST /customers/:customerId/users - Create a new user @Post('/') @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser ): Promise { // Scope object consolidates related entities for service layer return this._userService.createUser({ loggedUser: req.user, customer: req.customer }, body); } } ``` #### Individual Resource Controllers (Singular)[​](#individual-resource-controllers-singular "Direct link to Individual Resource Controllers (Singular)") ``` // Individual user controller nested under customer scope @Controller('/:userId') @Auth(UserAuth) // UserAuth extracts userId and populates req.user export class UserApi { private readonly _userService = inject(UserService); // GET /customers/:customerId/users/:userId - Get specific user @Get('/') @ReturnType(User) public getUser(req: Request): Promise { // Controller consolidates entities from request into scope object return this._userService.getUser({ loggedUser: req.user, targetUser: req.user, // In this case, same user customer: req.customer }); } // PUT /customers/:customerId/users/:userId - Update specific user @Put('/') @ReturnType(User) public updateUser( req: Request, @Body(UpdateUser) body: UpdateUser ): Promise { // Scope object groups all entities needed for the operation return this._userService.updateUser({ loggedUser: req.user, targetUser: req.user, customer: req.customer }, body); } // DELETE /customers/:customerId/users/:userId - Delete specific user @Delete('/') @ReturnType(PrimitiveReferences.Boolean) public deleteUser(req: Request): Promise { // Even single entity operations benefit from scope pattern consistency await this._userService.deleteUser({ loggedUser: req.user, targetUser: req.user, customer: req.customer }); return true; } } ``` ## Service Patterns[​](#service-patterns "Direct link to Service Patterns") ### Passthrough Service Pattern[​](#passthrough-service-pattern "Direct link to Passthrough Service Pattern") When implementing API endpoints that proxy calls to other services, since APIs only automatically generate interfaces, you will need to export entity class from the proxied service's `/core/internal` module. For complete implementation details, see [Type Handling: Classes vs Interfaces](/docs/engine/api/decorators.md#type-handling-classes-vs-interfaces). ### Scope Pattern (Parameter Consolidation)[​](#scope-pattern-parameter-consolidation "Direct link to Scope Pattern (Parameter Consolidation)") Use scope objects to consolidate related entities into a single parameter, improving method signature readability and reducing parameter count: ``` import { Service, inject } from '@myre/engine/api'; import { Transactional } from '@myre/seq/core'; @Service() export class UserService { private readonly _userDao = inject(UserDao); // ❌ Verbose - passing entities individually public async createUserOldWay( loggedUser: User, customer: Customer, userData: CreateUserEntity, transaction?: Transaction ): Promise { // Multiple individual parameters make method signatures verbose } // βœ… Clean - consolidating related entities in scope object @Transactional() public async createUser( scope: { loggedUser: User; customer: Customer; }, userData: CreateUserEntity, transaction?: Transaction // automatically added by @Transactional() ): Promise { return this._userDao.createUser(scope, userData, transaction!); } // Scope composition varies based on method requirements @Transactional() public async updateUserProfile( scope: { loggedUser: User; targetUser: User; organization: Organization; }, profileData: UpdateProfileEntity, transaction?: Transaction ): Promise { // Scope contains all entities needed for this operation return this._userDao.updateProfile(scope, profileData, transaction!); } } ``` ## DAO Patterns[​](#dao-patterns "Direct link to DAO Patterns") ### Transaction Parameter Convention[​](#transaction-parameter-convention "Direct link to Transaction Parameter Convention") DAO methods should always have a non-optional transaction as the last parameter. Use scope objects to consolidate related entities: ``` @Service() export class UserDao { // Simple scope with single entity public async createUser( scope: { customer: Customer; }, data: CreateUserEntity, transaction: Transaction // Non-optional, last parameter ): Promise { const user = await UserModel.create({ ...data, customerId: scope.customer.id }, { transaction }); return new UserEntity().init(user.toJSON(), { trust: true }); } // Complex scope with multiple entities public async updateProfile( scope: { loggedUser: User; targetUser: User; organization: Organization; }, profileData: UpdateProfileEntity, transaction: Transaction ): Promise { // Validate permissions using scope entities if (scope.loggedUser.id !== scope.targetUser.id && !scope.loggedUser.hasAdminRoleIn(scope.organization)) { throw new UnauthorizedProfileUpdateError({ loggedUserId: scope.loggedUser.id, targetUserId: scope.targetUser.id, }); } const updatedUser = await UserModel.update(profileData, { where: { id: scope.targetUser.id, organizationId: scope.organization.id }, transaction }); return new UserEntity().init(updatedUser.toJSON(), { trust: true }); } } ``` ## Transaction Patterns[​](#transaction-patterns "Direct link to Transaction Patterns") For complete transaction management documentation, see [Transaction Management](/docs/engine/api/fundamentals.md#transaction-management). ### Multiple DAO Operations[​](#multiple-dao-operations "Direct link to Multiple DAO Operations") ``` import { Service, inject } from '@myre/engine/api'; import { Transactional } from '@myre/seq/core'; @Service() export class UserService { private readonly _userDao = inject(UserDao); private readonly _profileDao = inject(ProfileDao); @Transactional() public async createUserWithProfile( scope: { loggedUser: User; customer: Customer; organization: Organization; }, userData: CreateUserEntity, profileData: CreateProfileEntity, transaction?: Transaction // automatically added by @Transactional() ): Promise { // Both operations use the same transaction and scope object const user = await this._userDao.createUser(scope, userData, transaction!); // Extend scope with newly created user for profile creation const profileScope = { ...scope, targetUser: user }; await this._profileDao.createProfile(profileScope, profileData, transaction!); return user; } } ``` ## Error Handling Patterns[​](#error-handling-patterns "Direct link to Error Handling Patterns") ### Service Layer Error Handling[​](#service-layer-error-handling "Direct link to Service Layer Error Handling") ``` import { AbstractError } from '@myre/engine/core'; // Define specific error classes extending AbstractError export class UserEmailAlreadyExistsError extends AbstractError { constructor(email: string) { super(50001, { en: 'A user with this email already exists', fr: 'Un utilisateur avec cet email existe dΓ©jΓ ', }, { email }); } } export class InsufficientPermissionsError extends AbstractError { constructor(userId: number, organizationId: number) { super(50002, { en: 'Insufficient permissions to create users in this organization', fr: 'Permissions insuffisantes pour crΓ©er des utilisateurs dans cette organisation', }, { userId, organizationId }); } } @Service() export class UserService { private readonly _userDao = inject(UserDao); public async createUser( scope: { loggedUser: User; customer: Customer; organization: Organization; }, data: CreateUserEntity ): Promise { // Use scope object for all DAO operations const existingUser = await this._userDao.findByEmail(scope, data.email); if (existingUser) { throw new UserEmailAlreadyExistsError(data.email); } // Validate permissions using scope entities if (!scope.loggedUser.canCreateUsersIn(scope.organization)) { throw new InsufficientPermissionsError( scope.loggedUser.id, scope.organization.id ); } return await this._userDao.createUser(scope, data); } } ``` ## Authentication Patterns[​](#authentication-patterns "Direct link to Authentication Patterns") ### Auth Class Implementation[​](#auth-class-implementation "Direct link to Auth Class Implementation") An Auth is a class annotated with the `@Service()` decorator. Auths should extend the `BaseApi` and implement the `Authorizer` interface. It acts like a guard middleware with a single responsibility: determining whether a given request will be handled by the route handler or not. **Auth method return values:** * `true` if the access is granted * `false` if the access is forbidden (403 response automatically sent) * `null` if Auth responded manually (the framework will not call the next handler just stop the chain) **Error Classes for Auth:** ``` export class OperationNotFoundError extends AbstractError { constructor(operationId: number) { super(50003, { en: 'Operation not found', fr: 'OpΓ©ration introuvable', }, { operationId }); } } export class UserNotFoundError extends AbstractError { constructor(userId: number) { super(50004, { en: 'User not found', fr: 'Utilisateur introuvable', }, { userId }); } } ``` ### Complete Auth Class Example[​](#complete-auth-class-example "Direct link to Complete Auth Class Example") ``` import { Service, Param } from '@myre/engine/api'; import { BaseApi, Authorizer } from '@myre/engine/api'; @Service() export class OperationAuth implements Authorizer { private readonly _operationService = inject(OperationService); public async authorize( req: Request, res: Response, @Param('operationId', Number) operationId: number, ): Promise { const operation = await this._operationService.getOperation(operationId); if (!operation) { this._handleError(req, res, new OperationNotFoundError(operationId)); return null; } req.operation = operation; return true; } } ``` ### Parameter Validation in Auth Classes[​](#parameter-validation-in-auth-classes "Direct link to Parameter Validation in Auth Classes") Auth classes should use `@Param()` for parameter extraction and populate request objects: ``` export class UserAuth implements Authorizer { public async authorize( req: Request, @Param('userId', Number) userId: number ): Promise { const user = await this._userService.getUserById(userId); if (!user) { throw new UserNotFoundError(userId); } req.user = user; // Populate request with validated entity return true; } } ``` ### Auth Decorator Usage[​](#auth-decorator-usage "Direct link to Auth Decorator Usage") `@Auth` decorators can be used with controllers or methods to protect routes. Auths can also be chained: ``` @Controller('/operations/:operationId') @Auth(OperationAuth) // Additional auth for specific endpoint export class OperationApi { private readonly _operationService = inject(OperationService); @Delete() @Auth(CanUpdateOperationAuth) // Multiple auths can be chained public deleteOperation(req: Request): Promise { // req.operation is populated by auth classes return this._operationService.deleteOperation(req.operation); } } ``` ### Request Interface Declaration[​](#request-interface-declaration "Direct link to Request Interface Declaration") Declare Express request extensions for type safety: ``` declare global { namespace Express { export interface Request { user?: User; operation?: Operation; customer?: Customer; // Add other auth-populated properties } } } ``` ## Best Practices[​](#best-practices "Direct link to Best Practices") ### Layer Separation[​](#layer-separation "Direct link to Layer Separation") * **Controllers**: Only handle HTTP concerns, delegate to services * **Services**: Contain business logic, orchestrate DAOs * **DAOs**: Only handle data access, no business logic ### Naming Conventions[​](#naming-conventions "Direct link to Naming Conventions") * Use `Api` suffix for controllers: `UserApi`, `OrderApi` * Use descriptive service names: `UserService`, `EmailService` * Use `Dao` suffix for data access: `UserDao`, `OrderDao` ### Transaction Management[​](#transaction-management "Direct link to Transaction Management") * Use `@Transactional()` for automatic transaction management in services only * DAO methods should always have a non-optional transaction as the last parameter * Services orchestrate transactions, DAOs execute within them * Remember: `@Transactional()` automatically adds transaction parameter to method arguments ### Code Organization[​](#code-organization "Direct link to Code Organization") * Follow the **Scope Pattern** for parameter consolidation in service and DAO layers * Use scope objects to group related entities instead of passing them individually * Controllers extract entities from requests and pass them as consolidated scope objects * Use Auth classes for parameter validation and request population * Separate collection controllers (plural) from individual resource controllers (singular) * Always include `@ReturnType()` and `@Body()` decorators where required ### Scope Pattern Guidelines[​](#scope-pattern-guidelines "Direct link to Scope Pattern Guidelines") * **Purpose**: Consolidate related entities to improve method signature readability * **Composition**: Include all entities needed for the operation, not just customer * **Consistency**: Use scope objects even for single entity operations to maintain pattern consistency * **Layer Application**: Apply to service and DAO layers, not API controllers * **Benefits**: Reduces parameter count, improves code readability, enables easier refactoring --- ## Validation Integration > How the API framework integrates with the validation system for request/response validation and SDK generation # Validation Integration This page explains how the API framework integrates with the [validation system](/docs/validators/) for automatic request validation, response typing, and SDK generation. Prerequisites For detailed information about creating validation entities, decorators, and patterns, see the [Validation System documentation](/docs/validators/). For API decorator reference, see [Decorators](/docs/engine/api/decorators.md). ## Overview[​](#overview "Direct link to Overview") The MYRE API framework integrates seamlessly with the validation system through two key decorators: * **`@Body()`** - Validates request bodies using validation entities * **`@ReturnType()`** - Specifies response types for SDK generation and documentation ## Request Validation with @Body()[​](#request-validation-with-body "Direct link to Request Validation with @Body()") The `@Body()` decorator automatically validates incoming request data against validation entities: ``` import { Controller, Post, Body, ReturnType, Auth, inject } from '@myre/engine/api'; @Controller('/users') @Auth(CustomerAuth) export class UsersApi { private readonly _userService = inject(UserService); @Post('/') @ReturnType(User) public createUser( req: Request, @Body(CreateUser) body: CreateUser // Automatic validation ): Promise { // body is guaranteed to be valid CreateUser return this._userService.createUser({ loggedUser: req.user, customer: req.customer, organization: req.organization }, body); } } ``` **Key Features:** * Automatic validation against the specified entity class * Returns 400 Bad Request with detailed errors if validation fails * Type-safe parameter injection in controller methods ## Response Typing with @ReturnType()[​](#response-typing-with-returntype "Direct link to Response Typing with @ReturnType()") The `@ReturnType()` decorator specifies response types for SDK generation and documentation: ``` // Collection controller @Controller('/users') @Auth(CustomerAuth) export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) // Array of entities public getUsers(req: Request): Promise { return this._userService.getUsers({ loggedUser: req.user, customer: req.customer }); } } // Individual user controller @Controller('/:userId') @Auth(UserAuth) // UserAuth populates req.user export class UserApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User) // Single entity public getUser(req: Request): Promise { // req.user populated by UserAuth class return this._userService.getUser(req.user); } @Delete('/') @ReturnType(PrimitiveReferences.Boolean) // Primitive types public deleteUser(req: Request): Promise { // req.user populated by UserAuth class await this._userService.deleteUser(req.user); return true; } } ``` ## Advanced Integration Patterns[​](#advanced-integration-patterns "Direct link to Advanced Integration Patterns") ### Dynamic Type Validation[​](#dynamic-type-validation "Direct link to Dynamic Type Validation") For endpoints that accept multiple entity types, use picker functions: ``` @Controller('/entities') @Auth(EntityAuth) // EntityAuth populates req.entity export class EntityApi { private readonly _entityService = inject(EntityService); @Put('/') @ReturnType(EntityUnion) public updateEntity( req: Request, @Body([PersonUpdate, CompanyUpdate], entityUpdatePicker) body: PersonUpdate | CompanyUpdate ): Promise { // req.entity populated by EntityAuth class return this._entityService.updateEntity(req.entity.id, body); } } ``` ### Query Parameter Integration[​](#query-parameter-integration "Direct link to Query Parameter Integration") Query parameters use individual `@Query()` decorators, not validation entities: ``` @Controller('/users') @Auth(CustomerAuth) export class UsersApi { private readonly _userService = inject(UserService); @Get('/') @ReturnType(User, { isArray: true }) public getUsers( req: Request, @Query('search', String) search?: string, @Query('limit', Number) limit?: number, @Query('status', String) status?: UserStatus ): Promise { return this._userService.getUsers({ loggedUser: req.user, customer: req.customer }, { search, limit, status }); } } ``` ## SDK Generation[​](#sdk-generation "Direct link to SDK Generation") The framework automatically generates TypeScript SDKs based on `@ReturnType()` and `@Body()` decorators. For complete SDK information, see the [main API documentation](/docs/api/#sdk-generation). ## Error Handling Integration[​](#error-handling-integration "Direct link to Error Handling Integration") ### Automatic Validation Errors[​](#automatic-validation-errors "Direct link to Automatic Validation Errors") The framework automatically handles validation errors: * Returns 400 Bad Request for validation failures * Includes detailed error messages from validation decorators * Maintains consistent error response format ### Business Logic Validation[​](#business-logic-validation "Direct link to Business Logic Validation") For business logic validation, handle errors in services: ``` import { Service, inject } from '@myre/engine/api'; import { Transactional } from '@myre/seq/core'; import { AbstractError } from '@myre/engine/core'; // Define specific validation error classes export class InvalidEmailDomainError extends AbstractError { constructor(email: string) { super(50005, { en: 'Email domain not allowed', fr: 'Domaine email non autorisΓ©', }, { email }); } } export class AdminUsersNotAllowedError extends AbstractError { constructor(customerId: number) { super(50006, { en: 'Admin users not allowed for this customer', fr: 'Utilisateurs admin non autorisΓ©s pour ce client', }, { customerId }); } } @Service() export class UserService { private readonly _userDao = inject(UserDao); @Transactional() public async createUser( scope: { loggedUser: User; customer: Customer; organization: Organization; }, data: CreateUser, transaction?: Transaction // automatically added by @Transactional() ): Promise { await this._checkBusinessRules(scope, data); return this._userDao.createUser(scope, data, transaction!); } private async _checkBusinessRules( scope: { loggedUser: User; customer: Customer; organization: Organization; }, data: CreateUser ): Promise { // Custom business validation using scope entities if (data.email.endsWith('@competitor.com')) { throw new InvalidEmailDomainError(data.email); } if (data.name.toLowerCase().includes('admin') && !scope.customer.allowAdminUsers) { throw new AdminUsersNotAllowedError(scope.customer.id); } // Additional validation using other scope entities if (!scope.loggedUser.canCreateUsersIn(scope.organization)) { throw new InsufficientPermissionsError( scope.loggedUser.id, scope.organization.id ); } } } ``` ## Integration Best Practices[​](#integration-best-practices "Direct link to Integration Best Practices") For comprehensive best practices, see [Decorators Best Practices](/docs/engine/api/decorators.md#best-practices) and [API Patterns Best Practices](/docs/engine/api/patterns.md#best-practices). **Key Integration Rules:** 1. **Always use `@ReturnType()`** - Mandatory for all endpoints 2. **Always use `@Body()`** - Mandatory for endpoints with request bodies 3. **Use individual `@Query()` parameters** - Not validation entities for query parameters ## Testing Integration[​](#testing-integration "Direct link to Testing Integration") The framework automatically generates seed functions for testing: ``` import { seedUserFromApi, seedCustomerFromApi } from '@myre/your-service/core/testing'; // Generate plain JS objects with valid data const userData = seedUserFromApi(); const customerData = seedCustomerFromApi({ name: 'Custom Name' }); ``` note Seed functions return plain JavaScript objects, not entity instances. --- ## Validator Decorators Reference > Complete reference table for all validation decorators available in MYRE # Validator Decorators Reference Complete reference for all validation decorators available in the MYRE validation system. ## Complete Decorator Reference[​](#complete-decorator-reference "Direct link to Complete Decorator Reference") | Decorator | Source | Signature | Purpose | Example | | ---------------------------- | ----------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------- | | `@IsString()` | @myre/engine/validators | `(maxLength?: number, validationOptions?)` | String validation with optional max length (default: 255) | `@IsString(50)` | | `@IsText()` | @myre/engine/validators | `(validationOptions?)` | Unlimited text validation | `@IsText()` | | `@IsInteger()` | @myre/engine/validators | `(validationOptions?)` | Integer validation | `@IsInteger()` | | `@IsSmallInt()` | @myre/engine/validators | `(validationOptions?)` | Small integer validation (more efficient) | `@IsSmallInt()` | | `@IsBigInt()` | @myre/engine/validators | `(validationOptions?)` | Big integer validation | `@IsBigInt()` | | `@IsDecimal()` | @myre/engine/validators | `([precision, scale]?, validationOptions?)` | Decimal validation with optional precision/scale | `@IsDecimal([10, 2])` | | `@IsNumeric()` | @myre/engine/validators | `(validationOptions?)` | Numeric validation (less precise than IsDecimal) | `@IsNumeric()` | | `@IsReal()` | @myre/engine/validators | `(validationOptions?)` | Real number validation | `@IsReal()` | | `@IsDoublePrecision()` | @myre/engine/validators | `(validationOptions?)` | Double precision floating point validation | `@IsDoublePrecision()` | | `@IsDateOnly()` | @myre/engine/validators | `(validationOptions?)` | Date validation (must be start of day) | `@IsDateOnly()` | | `@IsDateOnlyUnrestricted()` | @myre/engine/validators | `(validationOptions?)` | Date validation (start of day) without DWH date range restrictions | `@IsDateOnlyUnrestricted()` | | `@ParseDate()` | @myre/engine/validators | `(validationOptions?)` | Parse and validate date values | `@ParseDate()` | | `@ParseDateOnly()` | @myre/engine/validators | `(validationOptions?)` | Parse and validate date-only values | `@ParseDateOnly()` | | `@IsInstance()` | @myre/engine/validators | `(targetType, validationOptions?)` or `(types[], picker, validationOptions?)` | Nested entity validation | `@IsInstance(AddressEntity)` | | `@IsInstanceArray()` | @myre/engine/validators | `(targetType, validationOptions?)` or `(types[], picker, validationOptions?)` | Array of nested entities validation | `@IsInstanceArray(ContactEntity)` | | `@IsOptional()` | @myre/engine/validators | `(options?: {nullable: boolean}, validationOptions?: ValidationOptions)` | Makes field optional (allows undefined/null) | `@IsOptional()` | | `@IsBIC()` | @myre/engine/validators | `(validationOptions?)` | BIC (Bank Identifier Code) validation | `@IsBIC()` | | `@IsJsonPrimitive()` | @myre/engine/validators | `(validationOptions?)` | JSON primitive validation (string, number, boolean, null) | `@IsJsonPrimitive()` | | `@SwaggerExtraInfo()` | @myre/engine/validators | `(swaggerExtraInfo?)` | Adds Swagger/OpenAPI metadata | `@SwaggerExtraInfo({'x-enumName': 'Status'})` | | `@HasAdditionalProperties()` | @myre/engine/validators | `()` | Class decorator for Swagger additionalProperties | `@HasAdditionalProperties()` | | `@Min()` | class-validator | `(value, validationOptions?)` | Minimum value constraint | `@Min(1)` | | `@Max()` | class-validator | `(value, validationOptions?)` | Maximum value constraint | `@Max(100)` | | `@MinLength()` | class-validator | `(length, validationOptions?)` | Minimum string length | `@MinLength(3)` | | `@MaxLength()` | class-validator | `(length, validationOptions?)` | Maximum string length | `@MaxLength(50)` | | `@IsEmail()` | class-validator | `(validationOptions?)` | Email format validation | `@IsEmail()` | | `@IsEnum()` | class-validator | `(enumObject, validationOptions?)` | Enum value validation | `@IsEnum(UserStatus)` | | `@IsUUID()` | class-validator | `(version?, validationOptions?)` | UUID format validation | `@IsUUID()` | | `@IsBoolean()` | class-validator | `(validationOptions?)` | Boolean value validation | `@IsBoolean()` | | `@IsDate()` | class-validator | `(validationOptions?)` | Date instance validation | `@IsDate()` | | `@Equals()` | class-validator | `(comparison, validationOptions?)` | Exact value match | `@Equals('ACTIVE')` | | `@IsIn()` | class-validator | `(values[], validationOptions?)` | Value in array validation | `@IsIn(['red', 'blue', 'green'])` | | `@Contains()` | class-validator | `(seed, validationOptions?)` | String contains substring | `@Contains('hello')` | | `@IsHexColor()` | class-validator | `(validationOptions?)` | Hex color format validation | `@IsHexColor()` | | `@IsSemVer()` | class-validator | `(validationOptions?)` | Semantic version format validation | `@IsSemVer()` | | `@ArrayMinSize()` | class-validator | `(min, validationOptions?)` | Minimum array size | `@ArrayMinSize(1)` | | `@ArrayMaxSize()` | class-validator | `(max, validationOptions?)` | Maximum array size | `@ArrayMaxSize(10)` | | `@MinDate()` | class-validator | `(date, validationOptions?)` | Minimum date constraint | `@MinDate(new Date())` | | `@MaxDate()` | class-validator | `(date, validationOptions?)` | Maximum date constraint | `@MaxDate(new Date())` | | `@IsNotEmpty()` | class-validator | `(validationOptions?)` | Non-empty value validation | `@IsNotEmpty()` | ## Import Guidelines[​](#import-guidelines "Direct link to Import Guidelines") ### From @myre/engine/validators[​](#from-myreenginevalidators "Direct link to From @myre/engine/validators") ``` import { BaseValidatorClass, BasePeriod, IsString, IsText, IsInteger, IsSmallInt, IsBigInt, IsDecimal, IsNumeric, IsReal, IsDoublePrecision, IsDateOnly, IsDateOnlyUnrestricted, ParseDate, ParseDateOnly, IsInstance, IsInstanceArray, IsOptional, IsBIC, IsJsonPrimitive, SwaggerExtraInfo, HasAdditionalProperties } from '@myre/engine/validators'; ``` ### From class-validator[​](#from-class-validator "Direct link to From class-validator") ``` import { Min, Max, MinLength, MaxLength, IsEmail, IsEnum, IsUUID, IsBoolean, IsDate, Equals, IsIn, Contains, IsHexColor, IsSemVer, ArrayMinSize, ArrayMaxSize, MinDate, MaxDate, IsNotEmpty } from 'class-validator'; ``` ## Usage Notes[​](#usage-notes "Direct link to Usage Notes") * **@IsOptional()**: Must be combined with other validators. Use `{nullable: false}` to disallow null values while allowing undefined. * **@IsDecimal()**: Use `[precision, scale]` format for financial amounts: `@IsDecimal([10, 2])`. * **@IsInstance()**: For single nested entities. Use picker function for dynamic type selection: `@IsInstance([TypeA, TypeB], picker)`. * **@IsInstanceArray()**: For arrays of nested entities. Supports same picker pattern as `@IsInstance()`. * **@SwaggerExtraInfo()**: Add `'x-enumName'` for enums, `examples` for sample values. * **@HasAdditionalProperties()**: Class decorator that allows additional properties in Swagger schema. Apply to the class, not individual properties. * **@IsString()**: Default maximum length is 255 characters. Use `@IsText()` for unlimited text. * **@IsDateOnlyUnrestricted()**: Allows dates outside the DWH date range (1900-2199), unlike `@IsDateOnly()` which enforces DWH date range restrictions. * **BasePeriod**: Base class for entities needing optional `startDate` and `endDate` fields with built-in validation. --- ## Real-World Examples > Real validation patterns from best practices # Real-World Examples This page showcases real validation patterns extracted from the codebase, demonstrating how validation entities are used in production code. For complete decorator documentation, see [Decorators Reference](/docs/engine/validators/decorators.md). ## Basic Entity Examples[​](#basic-entity-examples "Direct link to Basic Entity Examples") ### Currency Entity[​](#currency-entity "Direct link to Currency Entity") ``` import { BaseValidatorClass, IsInteger, IsString, IsText, SwaggerExtraInfo, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class CurrencyCreateEntity extends BaseValidatorClass { @IsText() public code: string; @IsString(50) public name: string; @IsString(3) @SwaggerExtraInfo({ examples: ['eur'], }) public iso4217: string; } export class CurrencyEntity extends CurrencyCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ### Broker Entity[​](#broker-entity "Direct link to Broker Entity") ``` import { BaseValidatorClass, IsInteger, IsOptional, IsText, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class BrokerCreateEntity extends BaseValidatorClass { @IsText() @IsOptional() public name?: string; } export class BrokerEntity extends BrokerCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ## Complex Nested Entities[​](#complex-nested-entities "Direct link to Complex Nested Entities") ### Covenant Computation Element[​](#covenant-computation-element "Direct link to Covenant Computation Element") ``` import { TranslationEntity } from '@myre/engine/models'; import { CovenantComputationType } from '@myre/engine/models/am'; import { BaseValidatorClass, IsInstance, IsInteger, IsOptional, SwaggerExtraInfo, } from '@myre/engine/validators'; import { IsEnum, Min } from 'class-validator'; export class CovenantComputationElementCreateEntity extends BaseValidatorClass { @IsInstance(TranslationEntity) public name: TranslationEntity; @IsInteger() @Min(1) @IsOptional() public parentId?: number; @IsEnum(CovenantComputationType) @SwaggerExtraInfo({ 'x-enumName': 'CovenantComputationType' }) @IsOptional() public elementType?: CovenantComputationType; @IsInteger() public priority: number; } export class CovenantComputationElementEntity extends CovenantComputationElementCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ### Typology Category with Color[​](#typology-category-with-color "Direct link to Typology Category with Color") ``` import { type HexColor } from '@myre/engine/core'; import { TranslationEntity } from '@myre/engine/models'; import { BaseValidatorClass, IsInstance, IsInteger, IsOptional, } from '@myre/engine/validators'; import { IsBoolean, IsHexColor, Min } from 'class-validator'; export class TypologyCategoryCreateEntity extends BaseValidatorClass { @IsInstance(TranslationEntity) public name: TranslationEntity; @IsInteger() @Min(1) @IsOptional() public customerId?: number; @IsHexColor() public color: HexColor; @IsBoolean() public autoMerge: boolean; } export class TypologyCategoryEntity extends TypologyCategoryCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ## API Request/Response Types[​](#api-requestresponse-types "Direct link to API Request/Response Types") ### Invoice Status Update[​](#invoice-status-update "Direct link to Invoice Status Update") ``` import { BaseValidatorClass, IsInteger, SwaggerExtraInfo, } from '@myre/engine/validators'; import { InvoiceStatusType } from '@myre/invoicing/core'; import { IsEnum } from 'class-validator'; export class UpdateInvoiceStatusBody extends BaseValidatorClass { @IsEnum(InvoiceStatusType) @SwaggerExtraInfo({ 'x-enumName': 'InvoiceStatusType' }) public status: InvoiceStatusType; } export class GetContractsForInvoiceContextRequest extends BaseValidatorClass { @IsInteger({ each: true }) public contractIds: readonly number[]; } ``` ### Invoice Payment Creation[​](#invoice-payment-creation "Direct link to Invoice Payment Creation") ``` import { PaymentTarget, PaymentType, } from '@@db/sql/models/invoice-payment/invoice-payment.entity'; import { BaseValidatorClass, IsDateOnly, IsInstanceArray, IsInteger, IsOptional, SwaggerExtraInfo, } from '@myre/engine/validators'; import { ArrayMinSize, IsBoolean, IsEnum, Min } from 'class-validator'; export class InvoicePaymentsCreateRequest extends BaseValidatorClass { @IsInstanceArray(InvoicePaymentSummaryCreate) public invoicePaymentSummaries: readonly InvoicePaymentSummaryCreate[]; } export class InvoicePaymentSummaryCreate extends BaseValidatorClass { @IsDateOnly() public paymentDate: Date; @IsEnum(PaymentType) @SwaggerExtraInfo({ 'x-enumName': 'PaymentType' }) public paymentType: PaymentType; @IsEnum(PaymentTarget) @SwaggerExtraInfo({ 'x-enumName': 'PaymentTarget' }) public paymentTarget: PaymentTarget; @IsBoolean() @IsOptional() public isPartialPayment?: boolean; } ``` ## DAO Entity Examples[​](#dao-entity-examples "Direct link to DAO Entity Examples") ### Invoice Parameter DAO[​](#invoice-parameter-dao "Direct link to Invoice Parameter DAO") ``` import { InvoiceParameterContractEntity } from '@@db/sql/models/invoice-parameter-contract/invoice-parameter-contract.entity'; import { InvoiceParameterEntity } from '@@db/sql/models/invoice-parameter/invoice-parameter.entity'; import { BaseValidatorClass, IsInstanceArray, IsInteger, IsOptional, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class InvoiceParameterEntityDaoCv extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInteger() @Min(1) public entityId: number; @IsInteger() @Min(1) public propertyId: number; } export class InvoiceParameterDaoCv extends InvoiceParameterEntityDaoCv { @IsInstanceArray(InvoiceParameterContractEntity) @IsOptional() public invoiceParameterContracts?: readonly InvoiceParameterContractEntity[]; } ``` ### Budget Item DAO[​](#budget-item-dao "Direct link to Budget Item DAO") ``` import { TranslationInput } from '@@external/translation.types'; import { Translation } from '@myre/engine/models'; import { BaseValidatorClass, IsInstance, IsInteger, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class BudgetItemDaoCv extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInteger() @Min(1) public customerId: number; @IsInstance(Translation) public name: Translation; } export class BudgetItemCreateDaoCv extends BaseValidatorClass { @IsInstance(TranslationInput) public name: TranslationInput; } export class BudgetItemUpdateDaoCv extends BaseValidatorClass { @IsInstance(TranslationInput) public name: TranslationInput; } ``` ## Service Layer Types[​](#service-layer-types "Direct link to Service Layer Types") ### Budget Plan Service Types[​](#budget-plan-service-types "Direct link to Budget Plan Service Types") ``` import { BaseValidatorClass, IsInstanceArray, IsInteger, IsString, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class BudgetPlanCreate extends BaseValidatorClass { @IsString() public name: string; } export class BudgetPlanUpdate extends BaseValidatorClass { @IsString() public name: string; } export class PropertyName extends BaseValidatorClass { @IsString() public name: string; } export class CorporateName extends BaseValidatorClass { @IsString() public name: string; } export class BudgetPlan extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInteger() @Min(1) public customerId: number; @IsString() public name: string; @IsInteger() @Min(0) public numberOfCharges: number; @IsInstanceArray(PropertyName) public propertyNames: readonly PropertyName[]; @IsInstanceArray(CorporateName) public corporateNames: readonly CorporateName[]; } ``` ## Advanced Patterns[​](#advanced-patterns "Direct link to Advanced Patterns") ### Entity Status with Inheritance[​](#entity-status-with-inheritance "Direct link to Entity Status with Inheritance") ``` import { TranslationInput } from '@@external/translation.types'; import { EntityStatus } from '@myre/engine/core'; import { DeleteEntity, Translation } from '@myre/engine/models'; import { BaseValidatorClass, IsInstance, IsInteger, IsOptional, IsString, SwaggerExtraInfo, } from '@myre/engine/validators'; import { Equals, IsEnum, Min } from 'class-validator'; export class BudgetSubItemCreateDaoCv extends BaseValidatorClass { @IsInstance(TranslationInput) public name: TranslationInput; } export class BudgetSubItemEntityStatusCreate extends BudgetSubItemCreateDaoCv { @IsEnum(EntityStatus) @Equals(EntityStatus.NEW) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) public entityStatus: EntityStatus.NEW; } export class BudgetSubItemEntityStatusUpdate extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInstance(TranslationInput) @IsOptional() public name?: TranslationInput; @IsEnum(EntityStatus) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) public entityStatus: EntityStatus; } ``` ### Query Parameters with Filters[​](#query-parameters-with-filters "Direct link to Query Parameters with Filters") ``` import { InvoiceType } from '@@db/sql/models/invoice/invoice.entity'; import { BaseValidatorClass, IsInteger, IsOptional, SwaggerExtraInfo, } from '@myre/engine/validators'; import { IsEnum, Min } from 'class-validator'; export class InvoiceeInvoicerCoupleIdsQueryCv extends BaseValidatorClass { @IsEnum(InvoiceType) @SwaggerExtraInfo({ 'x-enumName': 'InvoiceType' }) public invoiceType: InvoiceType; @IsInteger({ each: true }) @Min(1, { each: true }) @IsOptional() public readonly invoicerIds?: number[]; @IsInteger({ each: true }) @Min(1, { each: true }) @IsOptional() public readonly invoiceeIds?: number[]; } ``` ## Document Entity Examples[​](#document-entity-examples "Direct link to Document Entity Examples") ### Capex Package Document[​](#capex-package-document "Direct link to Capex Package Document") ``` import { BaseValidatorClass, IsDateOnly, IsInteger, IsOptional, IsString, } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class CapexPackageDocumentCreateEntity extends BaseValidatorClass { @IsInteger() @Min(1) public capexPackageId: number; @IsInteger() @Min(1) @IsOptional() public capexPackageTimeTableId?: number; @IsString() public name: string; @IsString() public path: string; @IsDateOnly() public date: Date; } export class CapexPackageDocumentEntity extends CapexPackageDocumentCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ### Valuation Document[​](#valuation-document "Direct link to Valuation Document") ``` import { BaseValidatorClass, IsInteger, IsText } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class ValuationDocumentCreateEntity extends BaseValidatorClass { @IsInteger() @Min(1) public valuationId: number; @IsText() public path: string; @IsText() public name: string; } export class ValuationDocumentEntity extends ValuationDocumentCreateEntity { @IsInteger() @Min(1) public id: number; } ``` ## Key Takeaways from These Examples[​](#key-takeaways-from-these-examples "Direct link to Key Takeaways from These Examples") 1. **Consistent Naming**: `CreateEntity` for creation, `Entity` for full objects, `DaoCv` for DAO types 2. **Proper Imports**: Always prefer `@myre/engine/validators` over `class-validator` when available 3. **Nested Entities**: Use `@IsInstance` and `@IsInstanceArray` for nested objects 4. **Optional Fields**: Always combine `@IsOptional` with other validators 5. **Swagger Documentation**: Use `@SwaggerExtraInfo` for enums and additional metadata 6. **Type Safety**: Use specific numeric types (`@IsInteger`, `@IsSmallInt`) based on data requirements 7. **String Handling**: Use `@IsText` for unlimited strings, `@IsString(length)` for limited ones These examples demonstrate the real-world application of validation patterns in a production MYRE service. --- ## Validation Fundamentals > Core concepts and principles of the MYRE validation system # Validation Fundamentals ## BaseValidatorClass[​](#basevalidatorclass "Direct link to BaseValidatorClass") All validation entities must extend `BaseValidatorClass`, which provides initialization and type safety features. Validation is performed separately by the MYRE framework when needed. ### Basic Usage[​](#basic-usage "Direct link to Basic Usage") ``` import { BaseValidatorClass, IsString, IsInteger } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class UserEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; } ``` ### Initialization[​](#initialization "Direct link to Initialization") ``` // Standard initialization (no validation during init) const user = new UserEntity().init({ id: 1, name: 'John Doe' }); // Trust mode (marks object as validated for later validation checks) const trustedUser = new UserEntity().init(data, { trust: true }); ``` ## Validation Decorators[​](#validation-decorators "Direct link to Validation Decorators") The MYRE validation system provides custom decorators that extend class-validator functionality. For a complete reference of all available decorators, see the [Decorators Reference](/docs/engine/validators/decorators.md). ## Trust Mode[​](#trust-mode "Direct link to Trust Mode") Trust mode affects how validation is performed when the MYRE framework validates objects, not during initialization. ### How Trust Mode Works[​](#how-trust-mode-works "Direct link to How Trust Mode Works") * The `init()` method **never performs validation** regardless of trust mode setting * Validation occurs separately using `@Validator()` decorator or automatically by the framework * `{ trust: true }` marks the instance as "already validated" for later validation checks * Objects marked with trust mode will skip validation when the framework validates them ### When to Use Trust Mode[​](#when-to-use-trust-mode "Direct link to When to Use Trust Mode") **βœ… Use trust mode for:** * Database query results (already validated data) * Validated external API responses * Internal system data that's already been validated **❌ Never use trust mode for:** * User input (needs validation) * Unvalidated external data * Untrusted sources ### Examples[​](#examples "Direct link to Examples") ``` // βœ… Good - Database data (already validated) const user = new UserEntity().init(dbResult, { trust: true }); // βœ… Good - Validated API response const user = new UserEntity().init(validatedApiData, { trust: true }); // ❌ Bad - User input (needs validation) const user = new UserEntity().init(userInput, { trust: true }); // Never do this ``` ## Import Guidelines[​](#import-guidelines "Direct link to Import Guidelines") Import decorators from the appropriate packages. See [Decorators Reference](/docs/engine/validators/decorators.md) for complete import statements. ## BasePeriod Base Class[​](#baseperiod-base-class "Direct link to BasePeriod Base Class") For entities that need optional date range fields, extend `BasePeriod` instead of `BaseValidatorClass`: ``` import { BasePeriod, IsInteger } from '@myre/engine/validators'; import { Min } from 'class-validator'; export class ReportRequestEntity extends BasePeriod { @IsInteger() @Min(1) public customerId: number; // Inherits optional startDate and endDate fields with @IsDateOnly() validation } ``` The `BasePeriod` class automatically provides: * `startDate?: Date` with `@IsDateOnly()` and `@IsOptional()` validation * `endDate?: Date` with `@IsDateOnly()` and `@IsOptional()` validation ## Entity Design Principles[​](#entity-design-principles "Direct link to Entity Design Principles") ### 1. Always Extend BaseValidatorClass[​](#1-always-extend-basevalidatorclass "Direct link to 1. Always Extend BaseValidatorClass") ``` // βœ… Good export class UserEntity extends BaseValidatorClass { @IsString() public name: string; } // ❌ Bad export class UserEntity { public name: string; } ``` ### 2. Use Specific Validation Types[​](#2-use-specific-validation-types "Direct link to 2. Use Specific Validation Types") ``` // βœ… Good - specific types @IsSmallInt() // For small numbers @IsDecimal([10, 2]) // For financial amounts @IsString(50) // When length is known // ❌ Less optimal - generic types @IsInteger() // When small int would suffice @IsText() // When length limit could be set ``` ### 3. Combine Decorators Properly[​](#3-combine-decorators-properly "Direct link to 3. Combine Decorators Properly") ``` // βœ… Good @IsString() @IsOptional() public name?: string; // ❌ Bad - missing main validator @IsOptional() public name?: string; ``` ## Common Anti-Patterns[​](#common-anti-patterns "Direct link to Common Anti-Patterns") ### ❌ Don't Do This[​](#-dont-do-this "Direct link to ❌ Don't Do This") ``` // Don't use @IsObject for nested entities @IsObject() public address: any; // Don't use @IsOptional alone @IsOptional() public name?: string; // Don't use @IsString for unlimited text @IsString() public longDescription: string; // 255 char limit ``` ### βœ… Do This Instead[​](#-do-this-instead "Direct link to βœ… Do This Instead") ``` // Use @IsInstance for nested entities @IsInstance(AddressEntity) public address: AddressEntity; // Combine @IsOptional with other validators @IsString() @IsOptional() public name?: string; // Use @IsText for unlimited text @IsText() public longDescription: string; ``` --- ## Validation Entity Patterns > Pure validation entity design patterns and best practices # Validation Entity Patterns This page covers common validation entity design patterns, decorator usage, and best practices for creating maintainable validation entities. ## Entity Design Patterns[​](#entity-design-patterns "Direct link to Entity Design Patterns") ### Standard Entity Types[​](#standard-entity-types "Direct link to Standard Entity Types") ``` import { BaseValidatorClass, IsString, IsInteger, IsOptional } from '@myre/engine/validators'; import { Min } from 'class-validator'; // Base entity with ID export class UserEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; } // Create entity (no ID, for new records) export class CreateUserEntity extends BaseValidatorClass { @IsString() public name: string; } // Update entity (optional fields, for partial updates) export class UpdateUserEntity extends BaseValidatorClass { @IsString() @IsOptional() public name?: string; } ``` ### Data Access Entity Types[​](#data-access-entity-types "Direct link to Data Access Entity Types") For data layer operations: ``` // Data access entity for database operations export class UserDataEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; } // Data creation entity export class CreateUserDataEntity extends BaseValidatorClass { @IsString() public name: string; } ``` ## Advanced Implementation Patterns[​](#advanced-implementation-patterns "Direct link to Advanced Implementation Patterns") ## Entity Status Patterns[​](#entity-status-patterns "Direct link to Entity Status Patterns") ### Basic Status Entity[​](#basic-status-entity "Direct link to Basic Status Entity") ``` import { BaseValidatorClass, SwaggerExtraInfo } from '@myre/engine/validators'; import { EntityStatus } from '@myre/engine/core'; import { IsEnum, Equals } from 'class-validator'; export class EntityStatusBase extends BaseValidatorClass { @IsEnum(EntityStatus) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) public entityStatus: EntityStatus; } // Specific status entities export class EntityStatusNew extends BaseValidatorClass { @IsEnum(EntityStatus) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) @Equals(EntityStatus.NEW) public entityStatus: EntityStatus.NEW; } export class EntityStatusEdit extends BaseValidatorClass { @IsEnum(EntityStatus) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) @Equals(EntityStatus.EDIT) public entityStatus: EntityStatus.EDIT; } ``` ### Using Status Entities[​](#using-status-entities "Direct link to Using Status Entities") ``` export class UserEntityStatusCreate extends CreateUserDataEntity { @IsEnum(EntityStatus) @Equals(EntityStatus.NEW) @SwaggerExtraInfo({ 'x-enumName': 'EntityStatus' }) public entityStatus: EntityStatus.NEW; } ``` ## Nested Entity Patterns[​](#nested-entity-patterns "Direct link to Nested Entity Patterns") ### Simple Nested Entities[​](#simple-nested-entities "Direct link to Simple Nested Entities") Use `@IsInstance()` for single nested entities: ``` export class AddressEntity extends BaseValidatorClass { @IsString() public street: string; @IsString() public city: string; } export class UserEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; @IsInstance(AddressEntity) public address: AddressEntity; } ``` ### Array of Nested Entities[​](#array-of-nested-entities "Direct link to Array of Nested Entities") Use `@IsInstanceArray()` for arrays of entities: ``` export class ContactEntity extends BaseValidatorClass { @IsString() public type: string; @IsString() public value: string; } export class UserEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInstanceArray(ContactEntity) public contacts: readonly ContactEntity[]; } ``` ### Complex Nested Structures[​](#complex-nested-structures "Direct link to Complex Nested Structures") ``` export class PropertyNameEntity extends BaseValidatorClass { @IsString() public name: string; } export class CorporateNameEntity extends BaseValidatorClass { @IsString() public name: string; } export class BudgetPlanEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; @IsInstanceArray(PropertyNameEntity) public propertyNames: readonly PropertyNameEntity[]; @IsInstanceArray(CorporateNameEntity) public corporateNames: readonly CorporateNameEntity[]; } ``` ## Translation Entity Patterns[​](#translation-entity-patterns "Direct link to Translation Entity Patterns") ### Using Translation Entities[​](#using-translation-entities "Direct link to Using Translation Entities") ``` import { TranslationEntity } from '@myre/engine/models'; export class CategoryEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInstance(TranslationEntity) public name: TranslationEntity; @IsInstance(TranslationEntity) @IsOptional() public description?: TranslationEntity; } ``` ### Translation Input Patterns[​](#translation-input-patterns "Direct link to Translation Input Patterns") ``` import { TranslationInput } from '@@external/translation.types'; export class CreateCategoryEntity extends BaseValidatorClass { @IsInstance(TranslationInput) public name: TranslationInput; } ``` ## Financial Entity Patterns[​](#financial-entity-patterns "Direct link to Financial Entity Patterns") ### Currency and Amount Patterns[​](#currency-and-amount-patterns "Direct link to Currency and Amount Patterns") ``` export class AmountEntity extends BaseValidatorClass { @IsDecimal([10, 2]) public amount: number; @IsString(3) @SwaggerExtraInfo({ examples: ['EUR', 'USD'] }) public currency: string; } export class InvoiceEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsInstance(AmountEntity) public totalAmount: AmountEntity; } ``` ### Date Range Patterns[​](#date-range-patterns "Direct link to Date Range Patterns") For date range validation, use the `BasePeriod` base class: ``` import { BasePeriod } from '@myre/engine/validators'; export class ReportRequestEntity extends BasePeriod { @IsInteger() @Min(1) public customerId: number; // Inherits startDate and endDate from BasePeriod } ``` The `BasePeriod` class provides optional `startDate` and `endDate` fields with `@IsDateOnly()` validation: ``` // BasePeriod class definition export class BasePeriod extends BaseValidatorClass { @IsDateOnly() @IsOptional() public startDate?: Date; @IsDateOnly() @IsOptional() public endDate?: Date; } ``` ## Query Parameter Patterns[​](#query-parameter-patterns "Direct link to Query Parameter Patterns") ### Filter Entity Patterns[​](#filter-entity-patterns "Direct link to Filter Entity Patterns") For complex filtering scenarios, create dedicated filter entities: ``` export class UserFilterEntity extends BaseValidatorClass { @IsString() @IsOptional() public search?: string; @IsInteger() @Min(1) @IsOptional() public limit?: number; @IsEnum(UserStatus) @SwaggerExtraInfo({ 'x-enumName': 'UserStatus' }) @IsOptional() public status?: UserStatus; } export class UserRoleFilterEntity extends BaseValidatorClass { @IsInteger({ each: true }) @Min(1, { each: true }) public roleIds: number[]; } ``` ## Dynamic Type Selection Patterns[​](#dynamic-type-selection-patterns "Direct link to Dynamic Type Selection Patterns") ### Picker Function Patterns[​](#picker-function-patterns "Direct link to Picker Function Patterns") For entities that can be one of multiple types, use picker functions: ``` import { BaseValidatorClass } from '@myre/engine/validators'; // Define the entity types export class PersonUpdateEntity extends BaseValidatorClass { @IsString() public firstName: string; @IsString() public lastName: string; } export class CompanyUpdateEntity extends BaseValidatorClass { @IsString() public companyName: string; @IsString() public registrationNumber: string; } // Picker function for dynamic type selection function entityUpdatePicker( data: any ): typeof PersonUpdateEntity | typeof CompanyUpdateEntity { return data.firstName ? PersonUpdateEntity : CompanyUpdateEntity; } // Usage in entity initialization const entityData = { firstName: 'John', lastName: 'Doe' }; const EntityClass = entityUpdatePicker(entityData); const entity = new EntityClass().init(entityData); ``` ## Error Handling Patterns[​](#error-handling-patterns "Direct link to Error Handling Patterns") ### Custom Validation Errors[​](#custom-validation-errors "Direct link to Custom Validation Errors") ``` export class CreateUserEntity extends BaseValidatorClass { @IsString() @IsEmail() public email: string; @IsString() @MinLength(8) @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { message: 'Password must contain at least one lowercase letter, one uppercase letter, and one number' }) public password: string; } ``` ## Trust Mode Patterns[​](#trust-mode-patterns "Direct link to Trust Mode Patterns") For comprehensive information about trust mode, including when to use it and detailed examples, see the [Trust Mode section in Fundamentals](/docs/engine/validators/fundamentals.md#trust-mode). ### Quick Trust Mode Reference[​](#quick-trust-mode-reference "Direct link to Quick Trust Mode Reference") ``` // βœ… Use trust mode for database data (already validated) const user = new UserEntity().init(dbResult, { trust: true }); // βœ… Use trust mode for validated external data const user = new UserEntity().init(validatedApiData, { trust: true }); // ❌ Never use trust mode for user input const user = new UserEntity().init(userInput); // No trust mode ``` ## Common Anti-Patterns[​](#common-anti-patterns "Direct link to Common Anti-Patterns") ### Entity Design Anti-Patterns[​](#entity-design-anti-patterns "Direct link to Entity Design Anti-Patterns") ``` // ❌ Don't use @IsObject for nested entities export class BadUserEntity extends BaseValidatorClass { @IsObject() public address: any; // Loses type safety } // βœ… Use @IsInstance for nested entities export class GoodUserEntity extends BaseValidatorClass { @IsInstance(AddressEntity) public address: AddressEntity; // Type-safe } // ❌ Don't use @IsOptional alone export class BadOptionalEntity extends BaseValidatorClass { @IsOptional() public name?: string; // Missing main validator } // βœ… Combine @IsOptional with other validators export class GoodOptionalEntity extends BaseValidatorClass { @IsString() @IsOptional() public name?: string; // Properly validated } // ❌ Don't use generic types when specific ones exist export class InEfficientEntity extends BaseValidatorClass { @IsInteger() // Too broad for small numbers public priority: number; @IsText() // No length limit when one could be set public code: string; @IsNumeric() // Less precise than @IsDecimal public amount: number; } // βœ… Use specific validation types export class EfficientEntity extends BaseValidatorClass { @IsSmallInt() // Specific for small numbers public priority: number; @IsString(50) // Appropriate length limit public code: string; @IsDecimal([10, 2]) // Precise decimal specification public amount: number; } ``` ## Entity Composition Patterns[​](#entity-composition-patterns "Direct link to Entity Composition Patterns") ### Inheritance vs Composition[​](#inheritance-vs-composition "Direct link to Inheritance vs Composition") ``` // βœ… Use inheritance for "is-a" relationships export class BaseAuditEntity extends BaseValidatorClass { @IsDateOnly() public createdAt: Date; @IsInteger() @Min(1) public createdBy: number; } export class UserEntity extends BaseAuditEntity { @IsInteger() @Min(1) public id: number; @IsString() public name: string; // Inherits: createdAt, createdBy } // βœ… Use composition for "has-a" relationships export class AddressEntity extends BaseValidatorClass { @IsString() public street: string; @IsString() public city: string; } export class CompanyEntity extends BaseValidatorClass { @IsInteger() @Min(1) public id: number; @IsString() public name: string; @IsInstance(AddressEntity) public address: AddressEntity; // Composition } ``` --- ## SEQ > Repository for shared typescript stuff related to Sequelize # SEQ Repository for shared typescript stuff related to Sequelize ## [πŸ“„οΈ Counter increment](/docs/seq/counter-incrementer.md) [Description](/docs/seq/counter-incrementer.md) ## [πŸ“„οΈ Table integrities](/docs/seq/table-integrities.md) [Integrity check](/docs/seq/table-integrities.md) ## [πŸ“„οΈ Updating references](/docs/seq/update-reference.md) [In some cases, we need to update references to an object in the database by a new value.](/docs/seq/update-reference.md) --- ## Counter increment > Description # Counter increment ## Description[​](#description "Direct link to Description") When generating number for an entity, for example when creating an invoice number, it's important to have numbers that follow each others. Counter table helps maintain sequence for an entity, with concurrency guarantee. ![](/assets/images/invoicing_counter_table-0339da54f6c51c9862e684686acd7d55.png) The table above represents the `invoicing_counter` table, that holds counters for all generated invoice numbers, collections, expense etc. This table cannot be updated with classical sequelize operations, because of concurrency issues. The incrementer function handle creation, update and concurrent access. ``` const incrementer = this._invoicingCounterDao.getIncrementer(transaction); const counter = await incrementer(invoiceeEntityId, 'invoice_counter'); ``` The code above returns the next value for the `invoice_counter` for the `invoiceeEntityId` provided. ## How to use[​](#how-to-use "Direct link to How to use") The SEQ project provide a `IncrementerDao` abstract class, that handle the code responsible to increment columns. To use it, it's necessary to extend the abstract class: ``` class InstanceIncrementDao extends IncrementerDao< IncrementModel, CounterKeys > { public getIncrementer( _transaction: Transaction, ): IncrementerFunction { [...] } } ``` `IncrementModel` is the sequelize model that represent the counter table. It needs to have a column for the linked entity, all counter columns, and a version for optimistic lock. `CounterKeys` that represents all keys that can be incremented. The `getIncrementer` method will return an increment function. This function is created by the abstract class: ``` public getIncrementer( _transaction: Transaction, ): IncrementerFunction { const findFn = (identifierId: number, transaction: Transaction) => [...]; const createFn = (identifierId: number, transaction: Transaction) => [...]; return this._getIncrementer(findFn, createFn, _transaction); } ``` `findFn` is the function that query the table. `createFn` is the function that create the row, for the `identifierId`, if it's not found. Each function needs to implement the `ModelFunction` interface. ## Concurency[​](#concurency "Direct link to Concurency") The main goal of the incrementer function is to avoid two calls to update the same field at the same time, leading to the same number returned, or to create a hole in the sequence if one of the counter value is rollbacked. ### Inside a transaction[​](#inside-a-transaction "Direct link to Inside a transaction") The incrementer function guarantee that, inside the same transaction, the request are handled one at the time, and until the commit or the rollback actions, the table is not altered. ### With optimistic lock[​](#with-optimistic-lock "Direct link to With optimistic lock") Between two transactions, if the same counter for the same entity is updated, the `version` column will detect inconsitency, and the function throws an `OptimisticLockError` when commiting the transaction. This mean that in this scenario, one transaction will succeed, and the other one will not be able to commit. info This mean that concurrent access to the table is detected afterward. ## Implementation note[​](#implementation-note "Direct link to Implementation note") The `incrementer` function does not use the `increment` method from `sequelize`, as it does not update the model correctly inside the transaction. The increment is done by updating the model, and calling the `save` method on each model at the commit of the transaction. --- ## Table integrities > Integrity check # Table integrities ## Integrity check[​](#integrity-check "Direct link to Integrity check") At the end of migration scripts, we run a check to ensure that the table is in a consistent state. We ensure that the table match the object definition in the backend. i.e. in the model definition of an object, the field `addressId` is defined as non nullable. ``` this.init( [...] addressId: { type: DataTypes.INTEGER, allowNull: false, field: 'address_id', }, [...] ); } ``` But the table in the database has a nullable column. ![The column is nullable](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZ8AAABDCAYAAABDaQh0AAAPKElEQVR4Xu3d/VMT56IH8N5zz9w/4M7cmTtz5owz9wd6z2nnVKsCCUJAebNS8IUXQSCAFClaUUpVWikF9QjyYq0VS3nTVi0WRF5DAkRISGAB55w5f9H37AaCm91NskGIQb8/fEbyPLv7PPuEPN88mzW899FHH4F2pq6Jf6nK3rTdu3erykIViedFRP4dycjyOJZ70odUptzW6z1lAe0c7Y/nkJ6ZoyrfyaTzaX/8QlVORJGL4fOOycwt9kzU0krhbSGdj3ReynMlosi1LeETFRWF2HgTEdGGDz/8UFVG765tCZ9du3apGiKidxvDh+QYPkQUFgwfkmP4EFFYMHxIjuFDRGHB8CE5hg8RhQXDh+QYPkQUFgwfkmP4EFFYMHxIjuFDRGHB8CG5bQmf999/X9XQjmDKhPmbFlytyIBRWReM6SjOtHTjTm02DMq67fYm2ybSieGzTZSvf+XjCLWp8JFWNoHs2G84SDSjdVrAs+828aSZCvH3SQHjN0+FHlyv6022TaTTOxM+Cak4UlQJc05a6PPIZihf/8rHEWpT4ROXeBCB7I81qBqKTTiC2scCXM9uIj8l0bcuqQqdLgFP6jLf7GAxfIi2TcSGz1bPTUnluG0XMFh/3P8+W9mm8vWvfByhtiV8JMqGvIP9cnUV9r5aZJhkdaEO9nZh+BBtm0gPny2bm0IIny1pU/n6Vz6OUGEPH2HkNwzMuzF8vQgmb53WYCdkwtzUg2czLgiCC9PD3fi27NNXoWDKRGXHbxixzsMlrGBFcMLyqA2XLjWja8iK+UUB7hdj6G4owcEE9YmvtZuNqrZHGJ9bhLDkgGVAPN6CInwSj6L8ei8GrQ64l5yw/d6Jr05pLKdVT3giMi/1YVI8V2FlGe65cfTfPIN0f31JyEBxQxcGpxfE813EvPV3NBYdWq8LNhbKtvXs4x2/OSx4xm8B00NdqCtKX79uHKReonds6K1iOpSC/gcPPaSflfX+RHr4bNnc5AmfFbx8+XKdgJ7zinEKpU2NMDNm1eOpYEdH6SH161/5OEKFPXzc3V/gcMVdWJZsuFt5ZO0JUw52fDJO3hzDknsMP9ZVobi0Cpc7x+BeHMG1Au+EfBKNo4L4rqEeZSVlMJ9tQM+0OEkuDONO3TmUllXgfNsQFlZsaDWv7+PTnzSU3rFBcI3gh6/PwXz6c1Rf7xP7JQ+fFJxqncCi/RGazpUgt6gK3z6YhWC/h7IUxfFUT3giknIrUHHajLxTZlQ0PIBNcKDrrGzy3pCM3BsjcLvH0Vn/Bcwl5Sg/X42CT5L0jYWqbR37eMevvwHlp0/DXHkZrYNOrEjnlqyjPpSxobdGyicZGH7+HHfv3fOQfpbKlNtpifTw2bK5aT0sRlvP4EROHo6JDisvrYXSJsNn68LngDhx5V0fFlcDD1CdlagabEPaBXSLK5Ch73Jk79Tz0DAswCnun+Q53trkaGkpfLXSaHgGwdGJsqT1fZIq8MO8GCaNsuOsM6R/iX63gKdXZUtjxWU3Q1oNel0u9F38ZGN/46ffYEAMkbsVikAL9oQniP1/LmCqtRhxyrrUavzsXDtf5b66xkLRtq59VOMnnltBM8aFGbSXHAxaH9LY0Fsh4+hxWKamcLPl1kZZ/XeNsNpsyDyRrdpeaSeEz5bMTRphoRJCm1rHY/gEoGzId7DFx4k5uPzECeeTr5GZ7DvYcUWtsAhWNBfJj3MQBe3iSsVyC7nS9VGNydFU3S8ugx/ifOr6PqZ8NI2J29wqUj0JcUVtmBKm0FworS7WyxXh49lmVbrkJC6PNyxjdVXAL7Xr71K8VE/4IWR83ozuIQtmnS447dOwu1cw832ZKny0zzdQnWIsFG3r2kdj/AxpX+GhsCiuzg4FrQ9pbGjHyy0ohP3FC3x56Yqq7sKXtZibn0dBsVlVJ7czwsf0+nOTRliohNCm1vEYPgEoG1INtsiQ9RUezLsw0NiILp8nWAqGIE+wxuR44GwPloRfcSFtfYmbkLu+2jBrTPgabWiFz7IdnRfykHUiRyYb6cpltHL1kXMNz5ZcGLpdC/Mpceld8AU6JgQ/4aPRl4B1wcJHxz4a4xebUoN+YQk/+wkfeX1IY0M73ujoGMrPVKrKvaS6kdFRVbncjgmf+Necm6SwmA0xfAK0GZtYhg7xeMOy1RbDJwBlQ1qDHRufhPSL/Zhzu+BekS9tq9Hjkpa2sg/+xaXtt2KQLPSc93vZKJTw8bbx/Hr+q/4oL7t5Ls0tY0S+jT+KJ3xtFSbri+korjzVDh9Deg363P4uu+kYC2Xw6dlHY/xCCZ+QxoYofmeFz2vNTeI80mINMvmH0KZnHhsRMHf/842bEhg+ASgb0h5skSkLFx46sPJS/qFeCvKlD/VcY7hbV4Ui6UO9e9KHeqO4dip5/XjqyTGU8PF+cLi4ZEXv9RqUlZahuLJRfOchv+EgDcUdFgiLU+huqsHp0lKYK2twpcaMROVda6ZjqBsUz2/oFsqPpyHu5N8xJjgx2P4lSgpP4URBFVpHtcMnNj4Vhe2TYjsT6PrugtgXsZ2qWlSclO6Q0TEWirYNevbRGL9QwieksSGK32nhY3qNuekwzvW5sGzrRd3n0s06tThbdNj3UnRIbSYh+/qo+FqbROfVcygtPY3S2k5MboSP4vWvfKw83wjx5sMnXlpuXsGvC/LBlrbPRElTN4Zn125nnBnuwbenZbczakyOoYXPWp8Kr3ZiYMqBpZVVCO55WEefoKUy41U7iVkoE/sxaHOK26xgyTmNobvVSJffl++RiJTPbuHJtBMTt6T2UnHi0j0MWJ0QVlexvOTC3PQY+q/ma78bEdspberBkHSrtfiuR7pN/Mdq7zufIGOhalvHPhrjF1r4rPVZ39gQ7cDwid/k3CQ6cLwGd4ZmPP8NZMlhQe+VPH3h46/NpGyc7fgNFscSVlbF/RwzGP+9CzU50lynfP0rH/seP1KEL3yI6J0WseFDbwTDh4jCguFDcgwfIgoLhg/JMXyIKCwYPiTH8CGisGD4kBzDh4jCguFDcgwfIgoLhg/JMXyIKCwYPiTH8CGisGD4kBzDh4jCguFDcpsKn127diGQqKgoVUNE9G5j+JDcpsJHucpR+stfP/D8ohEREWnZVPh8HB2LQKTVz579sURERJoYPkREFHYMHyIiCjuGDxERhR3Dh4hoqxg/w4/OJThmrfil7hg+VtbLRWei9qEVM04B7p8qEa2sf8uFJXzeu/1PD+/j/0/N3iiTSI/l2/1Hxz/wh1uCzz6hiUP2TQvGbuRir6pOT32YxObj+sQEbuQY1XWR0kci0k8Kn7kBfJWu9ZrWYkTK5QE4GT5bHz5SgPz3+fs+QSL9/Le4JM/P3iCSd2p3tBF/MyapyvULNnEHqw+TmBRkV51D9kGtX9QI6SMR6cfw0W3bw+eDxCOef/0FiRRCyrrdMXGqstAEm7iD1EcbAi+XwyJIH4ko8jB8dNv28PHyFyZS+f+WXfd5/MdmF/5U9LXvttHJKGx8gGHbPFxuN16MP0BjSdr6xGyA4cQl3B+Zg8tpx1hfE678IJ+4g9THFuKWdRx36pvROziCiZFWFBhisS+tHI19Y7AvuOGaG8PP9YVIEM9vT0w6im/8iom5BSw4nXhh+Q3f5Mb5L9c4bw+fy25B+khEkY/ho9sbCx/viud/KttV23q396kTwyensgJZhxLwcYwJh7/ogvXFfZQliHXGQjRb5vDwygkYYowwFTZj0C1gwjtxB6uXwmd6Gfbui0g1Su0ZxLJsXH02h6fXzEgyGhGdXoG20Rncr0zG/pOtmJy4jSLP5TIjDOl5+DTFiH1+ypXntkEePsH6SESRj+Gj2xsJH+/nPP937IxqO3/7qMTk4tq4BTfz4rCvoB1WWwfyPcEhSUJlz8LGxB2s3hM+thm0FR7YOP6+vBZMzt5BocG7jwGJNY8wL/6SGLNvYnz2Aa4UZeJA7Ks+7fVT7pcsfIL2kYgiH8NHtzcSPtLP3jvcvJSf/ahuRIhOxNGLt/HwuRUzszOw2exwCHZ0FB/A/oouLAxeRXq093hxyGmZ2rhkFax+7bLbCBqOvfqFkfZxL83DOmnBhJfY5mzPBZhiknDsYgf6hmewsDCDwZ8aUJAs7hvtp1x2nj5k4RO0j0QU+Rg+um17+EiXzqQQ8VLeZu0lbeu9K06+rfc4e4824flsPy5mJa1NxjE5aBqb9YSPZ9Uw2YwTG6uNBJR1zvuufALUr4XPqE/47MtvxZSlFbkbKx9t+5KyUdNrx0y7Gft1lPtQrnwC9ZGIIh/DR7dtD59QSXfH/bngkqrccxnM0oGCeOmxEQnm25gQXnjCZ09cIVosU/i+LHktbD6pRb9j+dXEHaxeI3z2GHLQMGzHk6YSHIoziKuaeJgyzSjOTsb+T8vwWd4RGGKk7dJRfs8Ga1shYvyU79M4Tw/5Zz7B+khEkY/ho1vEhY9f0Ydxum1ADKBRDDx5jJ62a7gzurby8dwpllOHn4YtGPl9AI97W3HtJ/klqyD1WuEj2ptahobuYcw43XA552AZ7EGDOQXRuVfR93waDpcLC047RnqbUHDIiP1+ylXn4qW82y1QH4ko8hlK0Wq1w2YZR9/l4N9wUNM7jkmbHdPflzN81kVe+BAR0VuD4UNERGHH8Nlme9Nr0W2Zhs2mYOlFte7rwkREb5dNhY/yz2bLxcTFM3yIiCigTYWPFC6BREVFqRoiIiLy2lT4KFc7Sn/56weqhoiIiLw2FT7Kz3iUpNWPsiEiIiIvhg8REYUdw4eIiMKO4UNERGHH8CEi2irSd7s5l+CYteKXuuBfr1P70IoZpwA3v9vtLQwfn+9P06gnItoq/GJR3cISPn9oW/H8iYQ/trg2ypR/TsHrP28tesqkfZSd3RSGDxGFC8NHt20PH2/wKMNGT5kymDZls+ETbQi8ZCYiUmL46Lbt4aMVLt6f5dso67XsT/8MjX0jmF1ww+WYxtAP55Ai/e0csS426wI6BmxwuNxw2J6io/o4YqX9FOHjfzvpzyqM4059M3oHRzAx0oqCIH9IjojIB8NHt7CEj7xBedD81w27h67wMeSiYcSBoZZypMQZsNeQgoy844gX29tjPIWbk/N4XJ+PAzFGHMhtwKM5C5rz433DJ+B2YvhML8PefRGpRqlNg7oPRESBMHx020z4/Bu6135yb8Di1wAAAABJRU5ErkJggg==) This will produce a warning in the backend logs: ``` [integrity.db.mismatch] βž₯ Details: {"integrity":{"tableName":"corporate","columnName":"address_id","seqIsNullable":false,"dbIsNullable":true}} ``` ## How to fix[​](#how-to-fix "Direct link to How to fix") The integrity check generate a list of errors that you can fix automatically with the `fixTable` helper. This file is named `db-fixes.xxx.json` and is located in the `private/` folder of the backend. ``` [ { "table": "corporate", "columns": [ { "name": "address_id", "currentNullable": true, "nullable": false } ] } ] ``` You can run the fix in a migration script: ``` export const migration: SqlMigration = { up: async ({ context: queryInterface }): Promise => { const transaction = await queryInterface.sequelize.transaction(); await fixTables( queryInterface, [ { "table": "corporate", "columns": [ { "name": "address_id", "currentNullable": true, "nullable": false } ] } ], transaction, ); await transaction.commit(); }, }; ``` --- ## Updating references > In some cases, we need to update references to an object in the database by a new value. # Updating references In some cases, we need to update references to an object in the database by a new value. For example, when we merge entities, we replace the merged entity id with the new one, in all the tables that reference it. This can be done, in two different ways depending on the nature of the link: * **Foreign key**: If the reference is a foreign key, we can use the method `updatePhysicalReferences` * **Other**: If the reference is not a foreign key, we can use the method `updateLogicalReferences`. To be able to get this kind of reference, the column needs to have a specific comment, made with `getExternalReferenceComment` info For logical references, we update reference for other schemas. Example: ``` await updateLogicalReferences( Schema.Public, Schema.Entity, 'entity', targetEntity.id, [sourceEntity.id], ) ``` This call will replace all the references to the source entity by the target entity, in all columns that have the "entity.entity.id" comment, in all tables in the schema "public". info All columns that reference another one in another schema must have an external comment added, as shown here. --- ## Services > Repository of shared services for all other repositories # Services Repository of shared services for all other repositories --- ## Encrypted Records > The purpose of this DAO is to store both the encrypted value and the encryption information used in the same place. This allows the data to # Encrypted Records The purpose of this DAO is to store both the encrypted value and the encryption information used in the same place. This allows the data to be decrypted even if the encryption algorithm or encoding changes in the future, or to use multiple encryption methods depending on the user's choice. ## Description[​](#description "Direct link to Description") Encrypted Records is a DAO for storing and retrieving encrypted data. Encryption is performed using the [CryptoService](/docs/crypto/), which provides methods for encrypting and decrypting data. The encryption process may vary depending on the parameters provided, such as encoding and the algorithm used. To track encryption parameters, they are stored in the database along with the values. info Currently, only the `AES-256-GCM` algorithm and `Base64` encoding are supported. These details are not yet stored in the database. In addition to the DAO, a model (`EncryptedRecordModel`) is provided to represent the encrypted records table across all backends. ## Configuration[​](#configuration "Direct link to Configuration") Each backend must create its own table for storing encrypted data, following the model definition. info Each update to the model must be reflected in the database. The `EncryptedRecordModel` needs to be imported to backends, using the `getServiceModels` method: ``` const models = [ ...getServiceModels(), ]; ``` The DAO is provided via the `getServicesProviders` method. The [CryptoService](/docs/crypto/) must also be provided so the DAO can use it to encrypt and decrypt data. ## Usage[​](#usage "Direct link to Usage") The DAO can be used as follows: ``` const encryptedRecordDao = inject(EncryptedRecordDao); // Encrypted data is stored in the database, // and the link is established via `encryptedValueId`. const encryptedValueId = await encryptedRecordDao.saveEncryptedValue( 'secret_data', transaction, ); // To retrieve the decrypted value, use `encryptedValueId`. const decryptedValue = await encryptedRecordDao.getDecryptedValue( { encryptedValueId }, transaction, ); ``` ---