@reldens/storage
Version:
611 lines (495 loc) • 25.2 kB
Markdown
## ⚠️ CRITICAL: The Library Works - Fix The Tests
**This package is battle-tested in hundreds of production projects handling thousands of API calls successfully.**
**Default Assumption: Tests are failing because TEST CODE is incorrect, NOT because library code is broken.**
**Your Primary Task:**
- Fix test files in `tests/` directory to properly use the library
- Study working examples from reldens-cms and reldens main project
- Make all tests pass by correcting how they call the library APIs
**If You Suspect a Library Bug:**
- Present complete proof with production evidence
- Wait for user confirmation before proposing any library changes
- Do NOT modify `lib/` code without explicit approval
---
## Package Overview
**@reldens/storage** is the database abstraction layer for Reldens. It provides:
- Multi-ORM support (objection-js, mikro-orm, prisma)
- Entity/model generation from database schemas
- Unified API across different ORM drivers
- Database connection management
- Schema introspection and code generation
- Type mapping between database and JavaScript types
## Key Commands
```bash
# Generate entities from database
npx reldens-storage generateEntities --user=<user> --pass=<pass> --host=<host> --database=<db> --driver=<driver> --client=<client>
# Generate entities with override (overwrites existing entities)
npx reldens-storage generateEntities --user=<user> --pass=<pass> --database=<db> --driver=<driver> --override
# Generate Prisma schema
npx reldens-storage-prisma --host=<host> --port=<port> --database=<db> --user=<user> --password=<pass>
# Generate Prisma schema with database parameters
npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --password=<pass> --dbParams="authPlugin=mysql_native_password"
# Alternative: Use environment variable for database parameters
export RELDENS_DB_PARAMS="authPlugin=mysql_native_password&sslmode=require"
npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --password=<pass>
```
## Architecture
### Core Classes
**EntitiesGenerator** (`lib/entities-generator.js`):
- Main orchestrator for entity generation
- Coordinates all generation steps
- Detects existing entities and models
- Determines what needs to be generated or updated
- Key methods:
- `generate()`: Main entry point for generation
- `detectExistingEntities()`: Scans for existing entity files
- `detectExistingModels()`: Scans for existing model files
- `filterTablesToGenerate()`: Determines what needs generation
- `entityNeedsUpdate()`: Checks if entity fields changed
- `extractPrismaRelationsMetadata()`: Extracts Prisma relation info
**BaseDriver** (`lib/base-driver.js`):
- Abstract base class for ORM drivers
- Provides common interface for all database operations
- Stores operator mapping for all drivers (`this.operatorsMap`)
- Key methods (all must be implemented by drivers):
- CRUD: `create()`, `update()`, `delete()`, `upsert()`
- Read: `load()`, `loadById()`, `loadAll()`, `loadOne()`
- Relations: `loadWithRelations()`, `createWithRelations()`
- Count: `count()`, `countWithRelations()`
- Query: `rawQuery()`, `executeCustomQuery()`
- Helpers: `parseRelationsString()`, `isJsonField()`
- **Operator Mapping**: Maps string operators to SQL operators
- `GT` → `>`, `GTE` → `>=`, `LT` → `<`, `LTE` → `<=`, `NE` → `!=`, `EQ` → `=`
- Available to all drivers via `this.operatorsMap`
**BaseDataServer** (`lib/base-data-server.js`):
- Abstract base class for data servers
- Manages database connection and entity management
- Uses EntityManager for entity registry
- Key methods:
- `connect()`: Establishes database connection
- `generateEntities()`: Generates entities from raw models
- `fetchEntitiesFromDatabase()`: Introspects database schema
- `getEntity()`: Retrieves entity by name
- `createConnectionString()`: Builds connection string
**EntityManager** (`lib/entity-manager.js`):
- Registry for managing entities
- Simple key-value store for entity instances
- Methods: `get()`, `add()`, `remove()`, `clear()`, `setEntities()`
**TypeMapper** (`lib/type-mapper.js`):
- Maps database types to JavaScript and Prisma types
- Handles MySQL types: int, varchar, text, json, datetime, enum, blob, etc.
- Methods:
- `mapDbTypeToJsType()`: Returns JS type (number, string, Date, object, Buffer, boolean)
- `mapDbTypeToPrismaType()`: Returns Prisma type (Int, String, DateTime, Json, Bytes, Boolean)
### Driver Implementations
**ObjectionJS Driver** (`lib/objection-js/`):
- `objection-js-driver.js`: Query builder using Objection.js API
- `objection-js-data-server.js`: Data server using Knex for connection
- Features:
- Complex relation support via `relationMappings`
- Uses `withGraphFetched()` for eager loading
- Supports relation modifiers (orderBy, limit)
- JSON field handling with `castText()` for LIKE queries
- **Filter operators** (case-insensitive): OR, IN, NOT, LIKE, GT, GTE, LT, LTE, NE, EQ
- **Operator conversion**: String operators converted to uppercase before processing
- **Nested filtering**: AND/OR operators support nested conditions with proper SQL grouping
- Methods: `appendFilters()`, `appendRelationsToQuery()`
- **Upsert behavior**: Check if record exists → update if found, create if not
**MikroORM Driver** (`lib/mikro-orm/`):
- `mikro-orm-driver.js`: Driver implementation
- `mikro-orm-data-server.js`: Data server for MongoDB/SQL
- Features:
- MongoDB support
- Entity metadata decorators
- Automatic schema synchronization
- **MikroORM v7 breaking changes applied:**
- `prop.joinColumn` renamed to `prop.joinColumns` (now an array) - use `prop.joinColumns[0]`
- `entity` in relation properties must be a function reference, not a string: `entity: () => require('./model-file').ModelClass`
- `orm.driver.connection.options` removed (private in v7) - use `orm.config.get('dbName')` instead
**Prisma Driver** (`lib/prisma/`):
- `prisma-driver.js`: Driver implementation with enhanced validation
- `prisma-data-server.js`: Data server using Prisma Client
- `prisma-schema-generator.js`: Schema generation and introspection
- `prisma-metadata-loader.js`: Loads field metadata including defaults
- `prisma-type-caster.js`: Type casting and normalization
- `prisma-relation-resolver.js`: Relation mapping and transformations
- `prisma-client-loader.js`: Utility for loading Prisma Client instances
- Features:
- Schema-first approach with auto-introspection
- Type-safe queries with Prisma Client
- Custom `ensureRequiredFields()` validation for better error messages
- Database default value support (skips validation for fields with defaults)
- VARCHAR foreign key support using relation connect syntax
- Introspection via `prisma db pull`
- Data proxy support
- Windows permission error handling
- **Prisma.DbNull handling**: PrismaDataServer passes `Prisma.DbNull` to driver, which passes it to type caster
- **Type caster isolation**: PrismaTypeCaster never requires `@prisma/client` directly, receives `prismaDbNull` as prop
- **Prisma v7 breaking changes applied:**
- Requires `@prisma/adapter-mariadb` for MySQL connections (WASM engine mandates a driver adapter)
- `datasource` block in `schema.prisma` no longer accepts `url` - connection URL is provided via `prisma.config.js` generated at project root using `{ datasource: { url: process.env.DATABASE_URL } }`
- `PrismaClient` constructor no longer accepts `datasources` or `datasourceUrl` - use `adapter: new PrismaMariaDb(connectionString)` instead
- `provider = "prisma-client-js"` is kept (deprecated but functional); switching to `prisma-client` would require additional adapter changes
- `_runtimeDataModel` in Prisma 7 is pruned: fields only contain `{ name, kind, type, relationName, dbName }` - `isId`, `isRequired`, `hasDefaultValue` are stripped. ID field detection falls back to `field.name === 'id' && field.kind === 'scalar'`
- `prisma.config.js` at project root is generated automatically by `PrismaSchemaGenerator.generateConfigFile()` and cleaned up after tests
### Generators
**EntitiesGeneration** (`lib/generators/entities-generation.js`):
- Generates entity definition files
- Determines title property (label, title, name, key)
- Detects primary keys and auto-increment fields
- Handles ENUM values with formatted labels
- Generates property configurations with types
- Creates list/show/edit property arrays
- Methods:
- `generateEntityFile()`: Creates entity file
- `generatePropertiesConfig()`: Builds properties object
- `determineTitleProperty()`: Finds display field
- `getPropertyAttributes()`: Builds property metadata
- `parseEnumValues()`: Extracts ENUM options
**ModelsGeneration** (`lib/generators/models-generation.js`):
- Generates ORM-specific model files
- Creates relation mappings for ObjectionJS
- Generates relation types for Prisma
- Handles forward and reverse relations
- Creates registered models file
- Relation key naming:
- Single reference: `related_[table]`
- Multiple references: `related_[table]_[column_suffix]`
- Methods:
- `generateModelFile()`: Creates model file
- `generateObjectionJsRelations()`: Builds relationMappings
- `generatePrismaRelations()`: Builds relationTypes
- `detectObjectionJsRelations()`: Finds forward relations
- `detectReverseObjectionJsRelations()`: Finds reverse relations
- `generateRegisteredModelsFile()`: Creates model registry
- `countReferencesPerTable()`: Determines relation naming
**EntitiesConfigGeneration** (`lib/generators/entities-config-generation.js`):
- Generates `entities-config.js` file
- Contains entity-to-entity relation mappings
- Used by entity loader to resolve relations
**EntitiesTranslationsGeneration** (`lib/generators/entities-translations-generation.js`):
- Generates `entities-translations.js` file
- Creates i18n translation keys for entities and fields
- Generates two types of translations:
- Table labels: Human-readable entity names
- Entity-specific field translations: Field labels per entity
- Methods:
- `getTranslationLabels()`: Generates table name translations
- `getFieldTranslations()`: Generates entity-specific field translations
- `formatFieldName()`: Converts field names to human-readable labels (e.g., `owner_id` → `Owner ID`)
**BaseGenerator** (`lib/generators/base-generator.js`):
- Base class for all generators
- Provides `applyReplacements()` method for template processing
### Database Introspection
**MySQLTablesProvider** (`lib/mysql-tables-provider.js`):
- Queries `information_schema` for table structure
- Fetches columns with types, constraints, defaults
- Retrieves foreign key relationships
- Returns structured table data with:
- Table name
- Columns with type, length, nullable, key, extra, default
- Referenced tables and columns for foreign keys
- Used by ObjectionJS and Prisma drivers
### Entity Templates
Located in `lib/entity-templates/`:
- `entity.template`: Base entity class template
- `objection-js-model.template`: ObjectionJS model template
- `mikro-orm-model.template`: MikroORM model template
- `prisma-model.template`: Prisma model template
- `entities-config.template`: Entities configuration template
- `entities-translations.template`: Translations template
- `registered-models.template`: Model registry template
Templates use placeholder replacement with `{{placeholderName}}` syntax.
## Workflow
1. **Database Connection**: Connect to database using appropriate driver
2. **Schema Introspection**: Read database schema (tables, columns, foreign keys)
3. **Entity Detection**: Scan for existing entities and models
4. **Change Detection**: Compare database schema with existing entities
5. **Entity Generation**:
- Generate entity files with property definitions
- Generate model files with ORM-specific code
- Generate relation mappings
6. **Configuration**:
- Update `entities-config.js` with new/updated entities
- Update `entities-translations.js` with translation keys
- Generate `registered-models-[driver].js` with model imports
7. **File Output**: Write all files to `generated-entities/` directory
## Generated File Structure
All generated files are created in the **generated-entities/** directory:
**Entity Definitions:**
- entities/[table-name]-entity.js
**Driver Models:**
- models/objection-js/[table-name]-model.js
- models/objection-js/registered-models-objection-js.js
- models/mikro-orm/[table-name]-model.js
- models/mikro-orm/registered-models-mikro-orm.js
- models/prisma/[table-name]-model.js
- models/prisma/registered-models-prisma.js
**Configuration Files:**
- entities-config.js (entity relation configuration)
- entities-translations.js (translation keys with two sections):
- `labels`: Table name translations (e.g., `'skills_class_path': 'Class Paths'`)
- `fields`: Entity-specific field translations (e.g., `'skills_class_path': {'id': 'ID', 'key': 'Key'}`)
## Relation Keys Pattern
All generated entity relations follow the `related_*` prefix pattern:
- **Single reference**: `related_[table_name]`
- Example: `related_players`, `related_users`
- **Multiple references to same table**: `related_[table_name]_[column_suffix]`
- Example: `related_skills_skill`, `related_skills_owner`
- The `_id` suffix is removed from column name when multiple references exist
This pattern is consistent across all ORM drivers and is defined in `entities-config.js`.
## Prisma Driver Validation System
### ensureRequiredFields() Method
The Prisma driver includes custom validation that differs from ObjectionJS and provides better error messages:
**Location:** `lib/prisma/prisma-driver.js` lines 201-223
**Purpose:** Validates that all required fields are present before sending data to Prisma Client
**Key Behavior:**
```javascript
ensureRequiredFields(data) {
let missingFields = [];
for(let field of this.requiredFields){
if(sc.hasOwn(data, field)){
continue; // Field is present
}
if(sc.hasOwn(this.foreignKeyMappings, field)){
let relationName = this.foreignKeyMappings[field];
if(sc.hasOwn(data, relationName)){
continue; // FK mapped to relation (connect syntax)
}
}
if(sc.hasOwn(this.fieldDefaults, field)){
continue; // Field has database default - skip validation
}
missingFields.push(field);
}
if(0 < missingFields.length){
Logger.warning('Missing required fields for '+this.tableName()+': '+missingFields.join(', '));
}
return data;
}
```
**Important:** This validation runs BEFORE Prisma Client, allowing it to:
1. Provide clear error messages about missing fields
2. Allow database defaults to work (doesn't validate fields with defaults)
3. Support foreign key relation syntax (checks both FK field and relation)
### Database Default Values
**How It Works:**
- Metadata loader extracts default values from Prisma schema (`@default()` directive)
- Stored in `this.fieldDefaults` map during driver initialization
- Validation skips required fields that have defaults
- Prisma/database applies default when field is missing
**Example:**
```javascript
// Prisma schema
model scores_detail {
kill_time DateTime @default(now()) @db.DateTime(0)
}
// Admin panel sends data without kill_time
{
player_id: 1001,
obtained_score: 150
// kill_time missing but has default
}
// Validation skips kill_time (has default)
// Prisma creates record, database applies DEFAULT CURRENT_TIMESTAMP
```
### Comparison with ObjectionJS
**ObjectionJS Driver:**
```javascript
create(params) {
return this.queryBuilder().insert(params);
}
```
- No validation
- Passes data directly to Knex
- Database handles missing fields and defaults
- Less informative error messages
**Prisma Driver:**
```javascript
async create(params) {
let preparedData = this.prepareDataWithRelations(params, true);
this.ensureRequiredFields(preparedData); // Custom validation
try {
return this.typeCaster.normalizeReturnData(await this.model.create({data: preparedData}));
```
- Custom validation before Prisma
- Better error messages
- Supports database defaults
- Type-safe queries
**Key Difference:** Prisma validates first, so it must be aware of database defaults to allow them to work.
### Foreign Key Handling
Both drivers handle foreign keys, but with different syntax:
**ObjectionJS:**
```javascript
// Direct FK field
{player_id: 1001}
```
**Prisma:**
```javascript
// Relation connect syntax
{players: {connect: {id: 1001}}}
// Also supports VARCHAR FKs
{related_table: {connect: {custom_id: "ABC123"}}}
```
The `prepareDataWithRelations()` method (lines 156-199) automatically converts FK fields to relation syntax.
## Important Notes
### Entity Management
- **DO NOT modify** generated entities directly (files in `generated-entities/`)
- Extend generated entities in custom model files if customization needed
- Custom models should be placed in project-specific directories
- Entity configuration (`entities-config.js`) defines relation keys used throughout codebase
- Relation keys are critical - changing them affects all code referencing relations
### Generation Behavior
- Generation is smart: only creates/updates entities that changed
- Detects new tables, field changes, missing configs, missing models
- Use `--override` flag to force regeneration of all files
- Override flag useful after major schema changes or driver switches
- Each driver has its own model structure and relation syntax
### Schema Changes
- Always regenerate entities after database schema changes
- Adding columns: entities auto-update with new fields
- Removing columns: entities auto-update, remove fields
- Changing relations: models regenerate with new relation mappings
- ENUM changes: entity updates with new available values
### Driver-Specific Notes
- **ObjectionJS**: Recommended driver, mature and stable
- **MikroORM**: Use for MongoDB or NoSQL requirements
- **Prisma**: Requires schema generation first, then entity generation
- Cannot mix drivers - regenerate all when switching drivers
- Each driver has different relation syntax in generated models
### Binary Executables
- `bin/reldens-storage.js`: Main CLI for entity generation
- `bin/generate-prisma-schema.js`: Prisma schema generator CLI
- Both are available via npx after package installation
### Environment Variables
- `RELDENS_DB_PARAMS`: Database connection parameters (used by Prisma)
- Format: `key1=value1&key2=value2`
- Example: `authPlugin=mysql_native_password&sslmode=require`
## PrismaClientLoader Utility
**Location:** `lib/prisma/prisma-client-loader.js`
**Purpose:** Shared utility for loading Prisma Client instances in CLI tools and applications.
**Exported From Package:** Yes, available via `const { PrismaClientLoader } = require('@reldens/storage');`
**Method:**
```javascript
PrismaClientLoader.load(projectPath, customPath, connectionData)
```
**Parameters:**
- `projectPath` (string): Project root directory path
- `customPath` (string|null): Optional custom path to a Prisma client (overrides default)
- `connectionData` (object|null): Optional database connection configuration with properties:
- `client` (string): Database client type (mysql, postgresql, etc.)
- `user` (string): Database username
- `password` (string): Database password
- `host` (string): Database host
- `port` (number): Database port
- `database` (string): Database name
**Returns:** PrismaClient instance or null on error
**Behavior:**
- If `customPath` is provided, uses that path
- Otherwise, uses a default path: `projectPath/prisma/client`
- Validates that Prisma Client exists at the path
- Requires `prismaModule.PrismaClient` export
- If `connectionData` is null: Creates adapter using `process.env.DATABASE_URL`
- If `connectionData` is provided: Builds connection string and creates `PrismaMariaDb` adapter
- Returns initialized PrismaClient instance
- **Prisma v7**: Uses `@prisma/adapter-mariadb` - `PrismaClient` constructor receives `{ adapter }` instead of `{ datasources }`
**Usage Examples:**
Using the default connection from schema:
```javascript
const { PrismaClientLoader } = require('@reldens/storage');
const prismaClient = PrismaClientLoader.load(process.cwd(), null, null);
if(!prismaClient){
console.error('Failed to load Prisma client');
process.exit(1);
}
```
Using custom connection:
```javascript
const { PrismaClientLoader } = require('@reldens/storage');
const prismaClient = PrismaClientLoader.load(
process.cwd(),
null,
{
client: 'mysql',
user: 'dbuser',
password: 'dbpass',
host: 'localhost',
port: 3306,
database: 'mydb'
}
);
if(!prismaClient){
console.error('Failed to load Prisma client');
process.exit(1);
}
```
**Used By:**
- `bin/reldens-storage.js`: CLI entity generator
- External packages: `@reldens/cms` CLI tools (update-password, generate-entities, generate-sitemap)
## Test Suite Architecture
**CRITICAL:** For detailed test suite architecture, execution flow, and lifecycle hooks, see: `.claude/test-architecture.md`
### The Golden Rule: Connect Once, Test Many
**DO NOT** create database connections, tables, or entities in `beforeEach()` hooks. These operations happen **ONCE per driver** in `before()` hooks.
**Correct test lifecycle:**
1. `before()` hook - RUNS ONCE per driver:
- Connect to database
- Create tables via SQL
- Generate entities (introspects database, creates models)
- For Prisma: Generate schema → Generate client → Reconnect
- Get repository references
2. `beforeEach()` hook - RUNS BEFORE EACH TEST:
- DELETE data from tables (fast cleanup)
- Never drop/recreate tables
- Never reconnect to database
3. `after()` hook - RUNS ONCE per driver:
- DROP all tables
- Disconnect from database
### Test Database Operations
- **cleanDatabase()**: Uses DELETE statements to clear data between tests (fast)
- **dropTestTables()**: Uses DROP TABLE statements for final cleanup (slow, runs once)
- **executeRawSQL()**: Creates tables from SQL schema file (slow, runs once)
### DataServer Flow
All three drivers follow this pattern:
```
1. new DataServer({config, rawEntities})
↓
2. await dataServer.connect()
↓
3. await executeRawSQL(dataServer, schemaSql) ← Tables must exist before next step
↓
4. await generateTestEntities(dataServer, driverName)
├─ Introspects database schema
├─ Creates models from tables
├─ [Prisma only]: Generate schema + client + reconnect
└─ Calls dataServer.generateEntities()
↓
5. dataServer.getEntity('entityName') ← Returns repository
```
### Why Tables Must Exist Before Entity Generation
- `generateTestEntities()` calls `fetchEntitiesFromDatabase()`
- This queries MySQL `information_schema` for actual table structures
- Without tables, introspection returns empty/null
- Entity generation fails without table metadata
### Prisma Special Handling
Prisma requires additional subprocess steps during entity generation:
1. Generate `schema.prisma` file from database (subprocess: `npx reldens-storage-prisma`)
2. Generate PrismaClient code (subprocess: `npx prisma generate`)
3. Disconnect old client
4. Clear Node.js module cache
5. Reconnect with newly generated client
6. Now PrismaClient has models for the current database schema
This happens ONCE during the `before()` hook inside `generateTestEntities()`.
**For critical patterns including:**
- Why tests use dynamic models (not generated models)
- Prisma client loading pattern
- Common pitfalls and solutions
**See:** `.claude/test-architecture.md` - Critical Patterns section
### Test Files
**Integration tests:**
- `tests/integration/test-cross-driver-compatibility.js`: Full CRUD cycle for all 3 drivers
- `tests/integration/test-nested-filters.js`: Complex filter syntax (AND/OR/NOT/IN/LIKE)
- `tests/integration/test-relations.js`: Relation loading and nested relations
**Test utilities:**
- `tests/utils/test-helpers.js`: Database setup, entity generation, cleanup utilities
- `tests/utils/test-runner.js`: Test framework with suite/group/test methods, uses Logger for output
**All integration test files use the correct lifecycle pattern as of 2026-01-18.**
For complete details on test architecture, common issues, performance comparison, and troubleshooting, see `.claude/test-architecture.md`.