Pattern Framework IGNIS
Tất cả các dịch vụ backend đều được xây dựng trên IGNIS Framework (@venizia/ignis + @venizia/ignis-helpers). Trang này tài liệu hóa các pattern mà mọi dịch vụ đều tuân theo.
Phân cấp Ứng dụng
IssuerApplication— chỉ@nx/identityextends lớp này (nó ký JWT và phục vụ endpoint JWKS)VerifierApplication— tất cả các dịch vụ khác extends lớp này (chúng xác minh JWT bằng cách lấy/jw-certscủa identity)
Pattern Lớp Application
src/application.ts của mỗi dịch vụ tuân theo cấu trúc chính xác này:
import { createAppConfig, VerifierApplication } from '@nx/core';
export const appConfig = createAppConfig();
export class Application extends VerifierApplication {
override async boot() { return {}; }
override getAppInfo() { return packageJson; }
override getProjectRoot() { /* bind and return __dirname */ }
override configureDatasources(): void {
super.configureDatasources();
this.dataSource(PostgresCoreDataSource);
}
override configureRepositories(): void {
super.configureRepositories();
this.repository(MyRepository);
}
override configureServices(): void {
super.configureServices();
this.service(MyService);
}
override configureControllers(): void {
super.configureControllers();
this.controller(MyController);
}
override configureComponents(): void {
super.configureComponents();
this.useCacheRedis({ bindingKey: BindingKeys.APPLICATION_REDIS_CACHE });
}
}Cấu trúc Component
Component nhóm các đăng ký DI có liên quan (repository, service, controller) có thể được thêm như một đơn vị:
export class MyComponent extends BaseComponent {
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
protected application: BaseApplication,
) {
super({
scope: MyComponent.name,
initDefault: { enable: true, container: application },
bindings: {},
});
}
override async binding(): Promise<void> {
this.application.repository(MyRepository);
this.application.service(MyService);
this.application.controller(MyController);
}
}Đăng ký trong Application.configureComponents():
this.component(MyComponent);Dependency Injection
IGNIS dùng DI dựa trên decorator. Inject các phụ thuộc trong constructor của service/controller:
import { inject, BaseService, BindingKeys, BindingNamespaces } from '@venizia/ignis';
export class MyService extends BaseService {
constructor(
@inject({
key: BindingKeys.build({
namespace: BindingNamespaces.REPOSITORY,
key: MyRepository.name,
}),
})
private myRepository: MyRepository,
@inject({ key: 'custom/binding/key' })
private redis: DefaultRedisHelper,
) {
super({ scope: MyService.name });
}
}Pattern Binding Key
| Pattern | Ví dụ |
|---|---|
| Repository | BindingKeys.build({ namespace: BindingNamespaces.REPOSITORY, key: 'MyRepository' }) |
| Service | BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: 'MyService' }) |
| Custom | Chuỗi literal, ví dụ '@nx/licensing/redis/cache' |
BaseService cung cấp this.logger với this.logger.for(methodName) trả về { info, error, warn, debug }.
Controller Factory (Auto CRUD)
ControllerFactory.defineCrudController() tạo ra một controller REST đầy đủ với các route được chuẩn hóa:
const _Controller = ControllerFactory.defineCrudController({
repository: { name: MyRepository.name },
authenticate: { strategies: [AuthenticateStrategy.JWT, AuthenticateStrategy.BASIC] },
authorize: { action: AuthorizationActions.READ, resource: MyPermissions.FIND.code },
controller: {
name: 'MyController',
basePath: '/my-entities',
isStrict: { path: true, requestSchema: true },
},
entity: () => MyEntity,
routes: {
find: { authorize: { action: AuthorizationActions.READ, resource: '...' } },
findById: { authorize: { ... } },
count: { authorize: { ... } },
findOne: { authorize: { ... } },
create: { request: { body: MyInsertSchema }, authorize: { ... } },
updateById: { request: { body: MyUpdateSchema }, authorize: { ... } },
deleteById: { authorize: { ... } },
deleteBy: { authorize: { ... } },
},
});
@controller({ path: '/my-entities', transport: ControllerTransports.REST })
export class MyController extends _Controller {
constructor(
@inject({ key: 'repositories.MyRepository' })
protected readonly repository: MyRepository,
) {
super(repository);
}
}Nó tạo ra 10 route CRUD: find, findById, findOne, count, create, updateById, updateBy, deleteById, deleteBy. Các route tùy chỉnh được thêm bằng decorator @get(), @post(), v.v. trên các phương thức bổ sung.
Mã Quyền
crudPermissions(subject, labels) tạo các object quyền với mã:
| Mã | Hành động |
|---|---|
Subject.find | List |
Subject.findById | Get by ID |
Subject.findOne | Find one |
Subject.count | Count |
Subject.create | Create |
Subject.updateById | Update by ID |
Subject.updateBy | Batch update |
Subject.deleteById | Delete by ID |
Subject.deleteBy | Batch delete |
Các quyền hành động tùy chỉnh được định nghĩa thủ công (xem License.issue, License.suspend của licensing, v.v.).
Bootstrap Helper
Điểm vào Application
// packages/*/src/index.ts
import { bootstrapApplication } from '@nx/core';
import { Application, appConfig } from './application';
bootstrapApplication({
ApplicationClass: Application,
config: appConfig,
options: { bannerPath: resolve(__dirname, '../resources/banner.txt') },
});Điểm vào Migration
// packages/*/src/migrate.ts
import { bootstrapMigration } from '@nx/core';
import { Application } from './application';
import { getMigrationProcesses } from './migrations/processes/migration-process';
bootstrapMigration({ ApplicationClass: Application, getMigrationProcesses });Loader Process Migration
// packages/*/src/migrations/processes/migration-process.ts
import { createMigrationProcessLoader } from '@nx/core';
export const getMigrationProcesses = createMigrationProcessLoader({
seedPaths: ['my-package-0001-seed-permissions'],
importFn: path => import(`../processes/${path}.js`),
});Mỗi file seed export một TMigrationProcess với options: { alwaysRun?: boolean }, một name, và một migrateFn.
Pattern Cơ sở Dữ liệu
Drizzle ORM với PostgreSQL
Mỗi dịch vụ dùng chung một PostgresCoreDataSource:
@datasource({ driver: 'node-postgres' })
export class PostgresCoreDataSource extends BaseDataSource {
override configure(): void {
const schema = this.getSchema();
this.pool = new Pool(this.settings);
this.connector = drizzle({ client: this.pool, schema });
}
}Repository Soft-Deletable
SoftDeletableRepository từ @nx/core tự động lọc các hàng có deleted_at IS NOT NULL:
@repository({ dataSource: PostgresCoreDataSource, model: MyEntity })
export class MyRepository extends SoftDeletableRepository<TSchema, TEntity, TPersist> {
// Kế thừa: deleteById đặt deleted_at thay vì xóa cứng
// findById, find, findOne đều loại trừ các hàng soft-deleted
}Transaction
const tx = await this.repository.beginTransaction();
try {
await this.repository.create({ data, options: { transaction: tx } });
await this.otherRepository.updateById({ id, data, options: { transaction: tx } });
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}Khóa mức hàng để đảm bảo an toàn đồng thời:
const row = await this.repository.findOne({
filter: { where: { id } },
options: { transaction: tx, lock: { strength: LockStrengths.UPDATE } },
});Snowflake ID
Tất cả khóa chính là Snowflake ID (số nguyên 64-bit dưới dạng chuỗi) được tạo bởi IdGenerator.getInstance().nextId(). Mỗi dịch vụ có một APP_ENV_SNOWFLAKE_WORKER_ID duy nhất để tránh trùng lặp.
Pattern Model
Drizzle schema + validation Zod:
// Định nghĩa schema (trong @nx/core)
export const MyEntitySchema = licensingSchema.table('MyEntity', {
...generateCommonColumnDefs(), // id, createdAt, modifiedAt, deletedAt
name: text('name').notNull(),
status: text('status').notNull().default('activated'),
data: jsonb('data').$type<MyJsonType>(),
}, def => [
index('IDX_MyEntity_status').on(def.status),
]);
// Export kiểu
export type TMyEntity = TTableObject<typeof MyEntitySchema>;
export type TMyEntityPersist = TTableInsert<typeof MyEntitySchema>;
// Schema Zod (tự động sinh từ Drizzle)
export const MyEntitySelectSchema = createSelectSchema(MyEntitySchema);
export const MyEntityInsertSchema = createInsertSchema(MyEntitySchema);
export const MyEntityUpdateSchema = MyEntityInsertSchema.partial();generateCommonColumnDefs() thêm: id (Snowflake, text PK), createdAt, modifiedAt, deletedAt (soft-delete).
Đối với entity không có soft-delete:
{
...generateIdColumnDefs({ id: { dataType: 'string' } }),
...generateTzColumnDefs({ deleted: { enable: false } }),
}Quan hệ Entity
@model({
type: 'entity',
settings: { hiddenProperties: ['createdAt', 'modifiedAt', 'deletedAt'] },
})
export class MyEntity extends BaseEntity<typeof MyEntitySchema> {
static override TABLE_NAME = 'MyEntity';
static override schema = MyEntitySchema;
static override relations = (): TRelationConfig[] => [
{ type: RelationTypes.ONE, name: 'parent', schema: ParentSchema, ... },
{ type: RelationTypes.MANY, name: 'children', schema: ChildSchema, ... },
];
}Quan hệ được sử dụng bởi controller CRUD cho các query include — ví dụ ?filter[include][0][relation]=children.
Cấu hình Môi trường
Mỗi package dùng dotenv-flow để quản lý môi trường:
| File | Mục đích | Bị gitignore |
|---|---|---|
.env.development | Cấu hình dev server | Không |
.env.test | Cấu hình test | Thường là có |
.env.local | Override cục bộ | Có |
Giá trị NODE_ENV quyết định file nào được nạp. Xem Tham khảo Biến Môi trường để có danh mục biến đầy đủ.
Tạo Key JWKS (ES256, PKCS#8)
Chỉ dành cho dịch vụ identity:
# Tạo cặp key
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
| tee private.pem | openssl pkey -pubout > public.pem
# Định dạng cho .env (single-line với \n)
awk 'NF {printf "%s\\n", $0}' private.pem
awk 'NF {printf "%s\\n", $0}' public.pemPrivate key bắt buộc phải là định dạng PKCS#8 (-----BEGIN PRIVATE KEY-----), không phải SEC1.
File Mã nguồn Chính
| Pattern | Đường dẫn file |
|---|---|
| Phân cấp Application | packages/core/src/application/{base,default,issuer,verifier}.ts |
| Repository soft-delete | packages/core/src/repositories/soft-deletable.repository.ts |
| Generator Snowflake ID | packages/core/src/utilities/id-generator.utility.ts |
| Hằng số & vai trò | packages/core/src/common/constants.ts |
| Key môi trường | packages/core/src/common/environments.ts |
| Factory app config | packages/core/src/common/app-config.ts |
| Bootstrap helper | packages/core/src/helpers/bootstraps/application.ts |
| Migration helper | packages/core/src/helpers/migration/ |
| Quyền CRUD | packages/core/src/common/constants.ts → crudPermissions() |
| Application của dịch vụ | packages/*/src/application.ts |
Trang Liên quan
| Trang | Mô tả |
|---|---|
| Bắt đầu | Hướng dẫn cài đặt cục bộ |
| Hệ thống Build | Target Makefile và đồ thị phụ thuộc |
| Tham khảo Biến Môi trường | Danh mục biến môi trường đầy đủ |
| Package Core | Tham khảo chi tiết @nx/core |