Key Design Decisions
Key Design Decisions
Relevant source files
The following files were used as context for generating this wiki page:
This page documents the core architectural choices and philosophical foundations of the @primebrick/dal-pg library. These decisions prioritize performance, safety, and consistency across the Primebrick v3 ecosystem.
Architectural Philosophy
The DAL is designed as a type-driven, metadata-heavy layer that bridges TypeScript classes to PostgreSQL tables without the overhead of a full ORM. It favors explicit configuration via decorators over convention-based magic, while enforcing strict anti-throttling measures to protect the database under high-concurrency workloads.
1. Snake_Case Everywhere
To eliminate the "transformation tax" common in Node.js applications, the DAL enforces snake_case for database columns, TypeScript entity properties, and JSON payloads.
- Implementation: The
@Columndecorator assumes the property name matches the database column name unless explicitly overridden src/meta/entity-meta.ts:333-338. - Data Flow: Data fetched from the wire via
pgis hydrated directly into entity instances. Since the property names are already insnake_case, no key-remapping logic is required during serialization to JSON README.md:42-42.
2. RETURNING * on All Writes
Every write operation (add, update, upsert, delete, restore) appends a RETURNING * clause to the SQL statement README.md:43-43.
- Benefit: This ensures that the application always has the most up-to-date state of the record, including database-generated defaults (e.g., serial IDs), triggers, and audit timestamps, without requiring a secondary
SELECTquery docs/ai/dal-usage-guide.md:231-235. - Hydration: The resulting row is passed through
pgValueToJsValueto ensure proper type coercion (e.g., convertingtimestamptzstrings to JSDateobjects) src/meta/column-pg-io.ts:108-142.
3. "Throw If Not Found" by Default
Finders (findById, findByUUID, find) default to throwing a NotFoundError if zero rows match the criteria docs/ai/dal-usage-guide.md:143-144.
- Rationale: In most business logic, requesting a specific resource that does not exist is an exceptional state. This reduces "if (null)" checks in consumer code.
- Override: Consumers can opt-out by passing
{ throwIfNotFound: false }in the options, which causes the method to returnnullinstead docs/ai/dal-usage-guide.md:146-147.
4. Soft-Delete Visibility (EXCLUDED by Default)
The DAL natively supports soft-deletion via the @DeletableField decorator src/meta/entity-meta.ts:251-267.
- Default Behavior: All read operations automatically append a
WHERE deleted_at IS NULLclause (or equivalent) unless specified otherwise README.md:45-45. - Visibility Control: The
deletedRecordsoption allows three modes:"EXCLUDED"(default),"ONLY", and"INCLUDED"docs/ai/dal-usage-guide.md:155-155.
Sources:
- src/meta/entity-meta.ts:333-338
- src/meta/column-pg-io.ts:108-142
- docs/ai/dal-usage-guide.md:143-155
- README.md:42-45
High-Volume Write Strategy
For bulk operations, the DAL avoids the overhead of individual INSERT or UPDATE statements by using a temporary table strategy.
TEMP TABLE Strategy for Bulk Ops
When performing updateMany or upsertMany, the DAL follows a specific sequence to ensure atomicity and performance README.md:46-46.
Data Flow: Bulk Update
- Create: A temporary table is created with
ON COMMIT DROPto ensure it is cleaned up automatically after the transaction. - Load: Data is inserted into the temporary table using batched
INSERTstatements (respecting the 65,535 parameter limit) docs/ai/dal-usage-guide.md:315-320. - Execute: A single
UPDATE ... FROMorINSERT ... SELECT ON CONFLICTstatement is executed to merge the data from the temporary table into the target table.
Audit-Aware Upserts
The upsertMany operation is designed to preserve the integrity of audit trails during conflicts README.md:47-47.
- It preserves the original
created_atandcreated_byvalues. - It updates the
updated_at,updated_by, and increments theversionfield docs/ai/dal-usage-guide.md:330-335.
Entity Space to Code Entity Space: Bulk Writes
The following diagram maps the high-level bulk strategy to the internal functions that execute it.
| System Name | Code Entity | File Path |
|---|---|---|
| Bulk Gateway | Repository.updateMany | src/repository/repository.ts |
| Temp Table Logic | createTempTable | src/repository/repository.ts |
| Batch Manager | autoBatchSize | src/repository/repository.ts |
Diagram: Bulk Operation Lifecycle
Code
Sources:
Connection and Security
Port-Based Audit
Audit logging is decoupled from the main database transaction via a "Port" pattern src/index.ts:112-115.
- Implementation: The
AuditPortinterface allows consumers to provide their own implementation (e.g., logging to a separate audit database or a message bus) src/types/types.ts:200-210. - Non-Blocking: Audit writes are typically "fire-and-forget" from the perspective of the DAL, ensuring that audit failures do not roll back business transactions unless explicitly configured to do so docs/ai/dal-usage-guide.md:435-440.
Anti-Throttling: statement_timeout
To prevent slow queries from starving the connection pool, the DAL enforces a mandatory statement_timeout README.md:51-51.
- Default: 30 seconds docs/ai/dal-usage-guide.md:53-53.
- Mechanism: The
Dalgateway setsSET statement_timeout = 30000on every new connection via theonConnecthook src/dal/dal.ts:80-95. - Overrides: For long-running bulk operations, the
withClientmethod allows a per-call override usingSET LOCAL statement_timeout, which automatically resets when the client returns to the pool src/dal/dal.ts:180-200.
No Schema Migrations
The DAL intentionally excludes schema migration tools README.md:9-9.
- Decision: Schema management is considered a separate lifecycle concern (handled by tools like
db-migrateorliquibase). - Boundary: The DAL provides
dal.getPool()to allow external migration tools to share the connection configuration, but it does not execute DDL itself outside ofTEMP TABLEcreation docs/ai/dal-usage-guide.md:120-123.
Diagram: Connection Initialization
Code
Sources: