Demo

Database

Quark ERP uses TypeORM on PostgreSQL. Prefer the shared default DataSource for most tables. A plugin can also open one or more custom connections with @Database — and any other plugin can inject those connections by owner plugin name (and optional connection name).

Stack

  • PostgreSQL — primary persistent store (env via PluginEnv / base .env)
  • TypeORM — entities, repositories, QueryRunner, migrations
  • Default datasource — shared core connection; most plugins merge into it with plugin: "default"
  • Custom datasource — @Database on the root @Module; one plugin may declare multiple named connections
  • Cross-plugin reuse — inject another plugin’s connection with @InjectDatabaseSource("other-plugin", "connection-name?")

@InjectDatabaseSource

Inject a TypeORM DataSource on a service property (constructor injection is not supported). The first argument is the plugin that owns the connection; the second is the optional connection name when that plugin opened more than one DB.

  • DataSourceManager.DEFAULT_PLUGIN — primary core datasource
  • metadata.name — this plugin’s own default custom connection (if it declared @Database)
  • "other-plugin" — a connection created by another plugin (same process / DI registry)
  • Second arg name — named connection when the owner plugin declares multiple @Database entries (e.g. "analytics")
TSXinject
1import { DataSource } from "typeorm"; 2import { 3 InjectDatabaseSource, 4 DataSourceManager, 5 Service, 6} from "@quan-erp/shared-backend-core"; 7import metadata from "../../module.metadata.json" with { type: "json" }; 8import { OrderEntity } from "./order.entity.js"; 9 10@Service() 11export class OrderService { 12 // Shared core Postgres 13 @InjectDatabaseSource(DataSourceManager.DEFAULT_PLUGIN) 14 source: DataSource; 15 16 // This plugin’s own connection (default name) 17 @InjectDatabaseSource(metadata.name) 18 pluginSource: DataSource; 19 20 // This plugin’s named connection 21 @InjectDatabaseSource(metadata.name, "analytics") 22 analyticsSource: DataSource; 23 24 // Connection owned by another plugin 25 @InjectDatabaseSource("warehouse", "replica") 26 warehouseReplica: DataSource; 27 28 private get repo() { 29 return this.source.getRepository(OrderEntity); 30 } 31}

Define an entity

Table names MUST start with the plugin name. Use metadata.name in a template literal. Always extend BaseEntity for create/update/soft-delete columns.

TSXentity
1import { BaseEntity } from "@quan-erp/shared-backend-core"; 2import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 3import metadata from "../../../../module.metadata.json" with { type: "json" }; 4 5@Entity(`${metadata.name}_driver_document`) 6export class DriverDocumentEntity extends BaseEntity { 7 @PrimaryGeneratedColumn("increment") 8 id: number; 9 10 @Column() 11 documentName: string; 12}

Register on the module

Group entities by datasource. Use plugin: "default" so tables join the primary shared connection. For a custom @Database, set plugin (and optional name) to match that connection — not "default".

TSXdefault datasource
1import { Module } from "@quan-erp/shared-backend-core"; 2import metadata from "../../module.metadata.json" with { type: "json" }; 3import { OrderController } from "./order.controller.js"; 4import { OrderService } from "./order.service.js"; 5import { DriverDocumentEntity } from "./driver-document.entity.js"; 6 7@Module({ 8 name: metadata.name, 9 providers: [OrderService], 10 controllers: [OrderController], 11 entities: [ 12 { 13 plugin: "default", 14 entities: [DriverDocumentEntity], 15 }, 16 ], 17}) 18export class MyPluginModule {}

Custom datasource (@Database)

Declare one or more TypeORM connections on the root module with @Database. Prefer the shared default unless you need isolation (separate host/schema, reporting DB, replica, …). Connection options come from TypeORM DataSourceOptions — load host/credentials from env (PluginEnv / base .env), not hard-coded secrets.

  • @Database({ name?, options }) on the root @Module class — plugin is filled automatically from the loading plugin
  • name — optional connection id (default DataSourceManager.DEFAULT_NAME); required when one plugin opens multiple DBs
  • options — TypeORM DataSourceOptions (type, host, port, username, password, database, synchronize, …)
  • Entities — register via @Module entities with matching plugin + name; do not rely on options.entities (runtime overwrites with the entity registry)
  • Inject in the same plugin — @InjectDatabaseSource(metadata.name) or @InjectDatabaseSource(metadata.name, "analytics")
  • Multiple @Database decorators are allowed on the same module class — each becomes a separate connection
TSXmultiple connections in one plugin
1import { 2 Database, 3 Module, 4} from "@quan-erp/shared-backend-core"; 5import metadata from "../../module.metadata.json" with { type: "json" }; 6import { AnalyticsEntity } from "./analytics.entity.js"; 7import { WarehouseEntity } from "./warehouse.entity.js"; 8import { AnalyticsService } from "./analytics.service.js"; 9 10@Database({ 11 name: "analytics", 12 options: { 13 type: "postgres", 14 host: process.env.ANALYTICS_DB_HOST, 15 port: Number(process.env.ANALYTICS_DB_PORT ?? 5432), 16 username: process.env.ANALYTICS_DB_USERNAME, 17 password: process.env.ANALYTICS_DB_PASSWORD, 18 database: process.env.ANALYTICS_DB_SCHEMA, 19 synchronize: process.env.ANALYTICS_DB_SYNC === "true", 20 }, 21}) 22@Database({ 23 name: "replica", 24 options: { 25 type: "postgres", 26 host: process.env.REPLICA_DB_HOST, 27 port: Number(process.env.REPLICA_DB_PORT ?? 5432), 28 username: process.env.REPLICA_DB_USERNAME, 29 password: process.env.REPLICA_DB_PASSWORD, 30 database: process.env.REPLICA_DB_SCHEMA, 31 synchronize: false, 32 }, 33}) 34@Module({ 35 name: metadata.name, 36 providers: [AnalyticsService], 37 controllers: [], 38 entities: [ 39 { 40 plugin: metadata.name, 41 name: "analytics", 42 entities: [AnalyticsEntity], 43 }, 44 { 45 plugin: metadata.name, 46 name: "replica", 47 entities: [WarehouseEntity], 48 }, 49 ], 50}) 51export class MyPluginModule {}

Use another plugin’s connection

Connections live in the process-wide DataSource registry keyed by owner plugin (+ optional name). A consumer plugin does not re-declare @Database for that DB — it injects the owner’s connection and registers its own entities against that same plugin + name scope when needed.

  • Owner plugin — declares @Database({ name, options }) and boots first (list it in pluginDependencies)
  • Consumer plugin — @InjectDatabaseSource("owner-plugin", "connection-name")
  • Entities on a foreign connection — @Module entities entry uses plugin: "owner-plugin" and name: "connection-name" (not "default")
  • Do not hard-code a second identical @Database in every consumer — reuse the owner’s connection
  • Ensure the owner plugin is installed and loaded before consumers that inject its DataSource
TSXconsumer plugin service
1import { DataSource } from "typeorm"; 2import { InjectDatabaseSource, Service } from "@quan-erp/shared-backend-core"; 3 4@Service() 5export class ReportService { 6 // "warehouse" plugin declared @Database({ name: "replica", ... }) 7 @InjectDatabaseSource("warehouse", "replica") 8 warehouseReplica: DataSource; 9}
TSXconsumer module entities on foreign connection
1@Module({ 2 name: metadata.name, 3 providers: [ReportService], 4 controllers: [], 5 entities: [ 6 { 7 plugin: "warehouse", 8 name: "replica", 9 entities: [ReportSnapshotEntity], 10 }, 11 ], 12}) 13export class ReportPluginModule {}

CRUD patterns

  • Create: repo.insert(...) → return { id } from identifiers (no post-insert findOne)
  • Update: repo.update({ id }, { ...data, updateDate }) and check affected
  • Delete: prefer repo.softDelete + affected check (BaseEntity soft-delete)