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")
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.
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".
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
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
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}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)