Chuyển đến nội dung

API multi-tenancy

Đường dẫn import: @sdcorejs/nestjs/core

Tenancy được điều khiển bằng metadata và strategy. Mọi thuộc tính entity được trang trí bằng @Scoped() trở thành boundary query/write bắt buộc do BaseRepository thực thi. Entity không có scope không thay đổi.

Các export

ExportLoạiMục đích
ITenancyStrategyinterfacePhân giải scope và grant đặc quyền tùy chọn
TenancyOperationtyperead, create, import, update, delete, soft-delete, restore
TENANCY_OPERATIONSreadonly tupleAllowlist thao tác runtime đầy đủ
TenancyBypassGrant, TenancyBypassAuditEventinterfaceTruy cập đặc quyền có giới hạn và audit event
TenancyCallbacksinterfaceCallback resolve, bypass đã deprecated và bypassGrant inline
DefaultTenancyStrategyclassMặc định scope rỗng, không bao giờ bypass
CallbackTenancyStrategyclassChuyển callback thành ITenancyStrategy
TenancyModule, TenancyModuleOptionsclass/typeModule đăng ký strategy
TENANCY_STRATEGYvalueDI token
buildScopeFiltersfunctionDựng predicate Filter[]
buildScopeWherefunctionDựng predicate criteria TypeORM
applyScopeToEntityfunctionValidation và áp dụng write scope
RegisteredTenancyinterfaceBinding strategy/context toàn process
registerTenancy, getTenancyfunctionAPI registry toàn process
TenancyErrorclassLỗi tenancy cơ sở có code
MissingTenancyContextErrorclassEntity có scope nhưng không có strategy
MissingTenancyScopeErrorclassThiếu chiều scope bắt buộc
InvalidTenancyScopeErrorclassGiá trị scope không an toàn/không hợp lệ
TenancyScopeMutationErrorclassUpdate cố di chuyển một hàng giữa các scope
UnauthorizedTenancyBypassErrorclassBypass grant vắng mặt, sai dạng hoặc chưa được cấp quyền

Các export Scoped, ScopedOptions, ScopedColumnMetadata, getScopedColumnsgetScopedColumnMetadata được mô tả cùng decorator ORM.

Ví dụ strategy

ts
import type { RequestContext } from '@sdcorejs/nestjs';
import {
  type ITenancyStrategy,
  TenancyModule,
  type TenancyBypassGrant,
} from '@sdcorejs/nestjs/core';

export class AppTenancyStrategy implements ITenancyStrategy {
  getCurrentScope(ctx: RequestContext): Record<string, unknown> {
    return {
      tenantCode: ctx.tenant,
      departmentCode: ctx.custom?.departmentCode,
    };
  }

  shouldBypass(): boolean {
    return false;
  }

  getBypassGrant(ctx: RequestContext): TenancyBypassGrant | undefined {
    if (!ctx.roles?.includes('platform-admin') || !ctx.userId) return undefined;
    return {
      authorized: true,
      actorId: ctx.userId,
      reason: 'approved cross-tenant support request',
      allowedTargets: ['public.product'],
      allowedOperations: ['read'],
      audit: (event) => privilegedAuditSink.writeSync(event),
    };
  }
}

TenancyModule.forRoot({ strategy: AppTenancyStrategy });

Các key của getCurrentScopetên thuộc tính entity, không phải tên cột cơ sở dữ liệu. Scalar trở thành EQUAL; mảng trở thành IN. Scope tùy chọn bị thiếu được bỏ qua. Scope bắt buộc không có giá trị sẽ fail closed. Mảng giá trị cho phép rỗng không khớp hàng nào.

Đối với write, scalar scope ghi đè đầu vào caller. Khi có nhiều giá trị được phép, caller phải chọn một trong số đó trên entity mới; một giá trị duy nhất được chọn tự động.

Cấu hình inline

ts
TenancyModule.forRoot({
  resolve: (ctx) => ({ tenantCode: ctx.tenant }),
  global: true,
  registerGlobally: true,
});

globalregisterGlobally đều mặc định là true. Lớp strategy được ưu tiên hơn callback inline. Registry dùng một slot Symbol.for để repository được tải qua các package subpath khác nhau vẫn quan sát cùng một binding.

Bypass đặc quyền

Bypass grant phải có authorized: true, actor/reason không rỗng, ít nhất một EntityMetadata.tablePath chính xác (hoặc *), ít nhất một thao tác hợp lệ và callback audit đồng bộ. Callback phải trả undefined; promise/thenable sẽ fail closed vì repository không thể bảo đảm audit bất đồng bộ hoàn tất trước khi phát SQL.

Luồng cũ shouldBypass() === true bị chủ ý từ chối. Giữ method để tương thích interface, nhưng trả false và triển khai getBypassGrant cho các luồng đặc quyền đã review.

Mã lỗi

Lỗicode
MissingTenancyContextErrorTENANCY_CONTEXT_MISSING
MissingTenancyScopeErrorTENANCY_SCOPE_MISSING
InvalidTenancyScopeErrorTENANCY_SCOPE_INVALID
TenancyScopeMutationErrorTENANCY_SCOPE_IMMUTABLE
UnauthorizedTenancyBypassErrorTENANCY_BYPASS_UNAUTHORIZED

Đây là lỗi ứng dụng thay vì HTTP envelope tự động. Hãy ánh xạ chúng tại boundary của bạn mà không tiết lộ sự tồn tại của resource cross-tenant.

Phát hành theo giấy phép MIT.