nestjs-tenant-shield 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +612 -0
- package/dist/cache/cache.registry.d.ts +19 -0
- package/dist/cache/cache.registry.d.ts.map +1 -0
- package/dist/cache/cache.registry.js +51 -0
- package/dist/cache/cache.registry.js.map +1 -0
- package/dist/cache/cache.service.d.ts +44 -0
- package/dist/cache/cache.service.d.ts.map +1 -0
- package/dist/cache/cache.service.js +64 -0
- package/dist/cache/cache.service.js.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +19 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/constants/index.d.ts +64 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +67 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/context/get-current-tenant-id.d.ts +30 -0
- package/dist/context/get-current-tenant-id.d.ts.map +1 -0
- package/dist/context/get-current-tenant-id.js +40 -0
- package/dist/context/get-current-tenant-id.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +23 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/run-with-tenant.d.ts +84 -0
- package/dist/context/run-with-tenant.d.ts.map +1 -0
- package/dist/context/run-with-tenant.js +95 -0
- package/dist/context/run-with-tenant.js.map +1 -0
- package/dist/context/tenant-context.storage.d.ts +43 -0
- package/dist/context/tenant-context.storage.d.ts.map +1 -0
- package/dist/context/tenant-context.storage.js +45 -0
- package/dist/context/tenant-context.storage.js.map +1 -0
- package/dist/decorators/cacheable.decorator.d.ts +27 -0
- package/dist/decorators/cacheable.decorator.d.ts.map +1 -0
- package/dist/decorators/cacheable.decorator.js +108 -0
- package/dist/decorators/cacheable.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +13 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +29 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/require-tenant.decorator.d.ts +41 -0
- package/dist/decorators/require-tenant.decorator.d.ts.map +1 -0
- package/dist/decorators/require-tenant.decorator.js +125 -0
- package/dist/decorators/require-tenant.decorator.js.map +1 -0
- package/dist/decorators/system-action.decorator.d.ts +39 -0
- package/dist/decorators/system-action.decorator.d.ts.map +1 -0
- package/dist/decorators/system-action.decorator.js +50 -0
- package/dist/decorators/system-action.decorator.js.map +1 -0
- package/dist/decorators/tenant-context.decorator.d.ts +33 -0
- package/dist/decorators/tenant-context.decorator.d.ts.map +1 -0
- package/dist/decorators/tenant-context.decorator.js +54 -0
- package/dist/decorators/tenant-context.decorator.js.map +1 -0
- package/dist/errors/cross-tenant-access.error.d.ts +29 -0
- package/dist/errors/cross-tenant-access.error.d.ts.map +1 -0
- package/dist/errors/cross-tenant-access.error.js +37 -0
- package/dist/errors/cross-tenant-access.error.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +26 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/invalid-tenant-source.error.d.ts +20 -0
- package/dist/errors/invalid-tenant-source.error.d.ts.map +1 -0
- package/dist/errors/invalid-tenant-source.error.js +28 -0
- package/dist/errors/invalid-tenant-source.error.js.map +1 -0
- package/dist/errors/missing-tenant-context.error.d.ts +22 -0
- package/dist/errors/missing-tenant-context.error.d.ts.map +1 -0
- package/dist/errors/missing-tenant-context.error.js +32 -0
- package/dist/errors/missing-tenant-context.error.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/cacheable-options.interface.d.ts +37 -0
- package/dist/interfaces/cacheable-options.interface.d.ts.map +1 -0
- package/dist/interfaces/cacheable-options.interface.js +3 -0
- package/dist/interfaces/cacheable-options.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +13 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +30 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/require-tenant-options.interface.d.ts +23 -0
- package/dist/interfaces/require-tenant-options.interface.d.ts.map +1 -0
- package/dist/interfaces/require-tenant-options.interface.js +3 -0
- package/dist/interfaces/require-tenant-options.interface.js.map +1 -0
- package/dist/interfaces/tenant-context-options.interface.d.ts +19 -0
- package/dist/interfaces/tenant-context-options.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-context-options.interface.js +3 -0
- package/dist/interfaces/tenant-context-options.interface.js.map +1 -0
- package/dist/interfaces/tenant-context.interface.d.ts +26 -0
- package/dist/interfaces/tenant-context.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-context.interface.js +3 -0
- package/dist/interfaces/tenant-context.interface.js.map +1 -0
- package/dist/interfaces/tenant-shield-options.interface.d.ts +141 -0
- package/dist/interfaces/tenant-shield-options.interface.d.ts.map +1 -0
- package/dist/interfaces/tenant-shield-options.interface.js +11 -0
- package/dist/interfaces/tenant-shield-options.interface.js.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/tenant-context.middleware.d.ts +30 -0
- package/dist/middleware/tenant-context.middleware.d.ts.map +1 -0
- package/dist/middleware/tenant-context.middleware.js +68 -0
- package/dist/middleware/tenant-context.middleware.js.map +1 -0
- package/dist/options/index.d.ts +2 -0
- package/dist/options/index.d.ts.map +1 -0
- package/dist/options/index.js +18 -0
- package/dist/options/index.js.map +1 -0
- package/dist/options/options.registry.d.ts +8 -0
- package/dist/options/options.registry.d.ts.map +1 -0
- package/dist/options/options.registry.js +36 -0
- package/dist/options/options.registry.js.map +1 -0
- package/dist/resolvers/custom.resolver.d.ts +29 -0
- package/dist/resolvers/custom.resolver.d.ts.map +1 -0
- package/dist/resolvers/custom.resolver.js +47 -0
- package/dist/resolvers/custom.resolver.js.map +1 -0
- package/dist/resolvers/header.resolver.d.ts +22 -0
- package/dist/resolvers/header.resolver.d.ts.map +1 -0
- package/dist/resolvers/header.resolver.js +39 -0
- package/dist/resolvers/header.resolver.js.map +1 -0
- package/dist/resolvers/index.d.ts +13 -0
- package/dist/resolvers/index.d.ts.map +1 -0
- package/dist/resolvers/index.js +29 -0
- package/dist/resolvers/index.js.map +1 -0
- package/dist/resolvers/jwt.resolver.d.ts +35 -0
- package/dist/resolvers/jwt.resolver.d.ts.map +1 -0
- package/dist/resolvers/jwt.resolver.js +51 -0
- package/dist/resolvers/jwt.resolver.js.map +1 -0
- package/dist/resolvers/resolver.factory.d.ts +12 -0
- package/dist/resolvers/resolver.factory.d.ts.map +1 -0
- package/dist/resolvers/resolver.factory.js +43 -0
- package/dist/resolvers/resolver.factory.js.map +1 -0
- package/dist/resolvers/subdomain.resolver.d.ts +37 -0
- package/dist/resolvers/subdomain.resolver.d.ts.map +1 -0
- package/dist/resolvers/subdomain.resolver.js +57 -0
- package/dist/resolvers/subdomain.resolver.js.map +1 -0
- package/dist/resolvers/tenant-resolver.interface.d.ts +22 -0
- package/dist/resolvers/tenant-resolver.interface.d.ts.map +1 -0
- package/dist/resolvers/tenant-resolver.interface.js +3 -0
- package/dist/resolvers/tenant-resolver.interface.js.map +1 -0
- package/dist/tenant-shield.module.d.ts +88 -0
- package/dist/tenant-shield.module.d.ts.map +1 -0
- package/dist/tenant-shield.module.js +263 -0
- package/dist/tenant-shield.module.js.map +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/test-helpers.d.ts +52 -0
- package/dist/testing/test-helpers.d.ts.map +1 -0
- package/dist/testing/test-helpers.js +72 -0
- package/dist/testing/test-helpers.js.map +1 -0
- package/dist/typeorm/index.d.ts +10 -0
- package/dist/typeorm/index.d.ts.map +1 -0
- package/dist/typeorm/index.js +26 -0
- package/dist/typeorm/index.js.map +1 -0
- package/dist/typeorm/raw-sql.helper.d.ts +35 -0
- package/dist/typeorm/raw-sql.helper.d.ts.map +1 -0
- package/dist/typeorm/raw-sql.helper.js +24 -0
- package/dist/typeorm/raw-sql.helper.js.map +1 -0
- package/dist/typeorm/tenant.subscriber.d.ts +61 -0
- package/dist/typeorm/tenant.subscriber.d.ts.map +1 -0
- package/dist/typeorm/tenant.subscriber.js +487 -0
- package/dist/typeorm/tenant.subscriber.js.map +1 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jinyeong Jung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
# nestjs-tenant-shield
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://nestjs.com/)
|
|
5
|
+
|
|
6
|
+
> NestJS B2B SaaS 백엔드의 데이터 격리를 데코레이터 한 줄로 자동화하는 라이브러리. Cross-tenant 데이터 누출 사고를 사전 차단하고, 코드를 깨끗하게 유지합니다.
|
|
7
|
+
|
|
8
|
+
## ⚠️ 현재 상태 (솔직한 안내)
|
|
9
|
+
|
|
10
|
+
- v0.1이 배포되었습니다.
|
|
11
|
+
- v0.1은 TypeORM + discriminator 패턴 중심이며, v0.2 이후 기능(Bull/BullMQ, Prisma, RLS)은 계획 단계입니다.
|
|
12
|
+
- README 예시는 v0.1 기준이며, v0.2 기능은 별도 표기합니다.
|
|
13
|
+
|
|
14
|
+
## 🎯 왜 만들었나요?
|
|
15
|
+
|
|
16
|
+
### B2B SaaS의 가장 무서운 사고: 데이터 누출
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
하나의 학원 관리 SaaS = 여러 학원이 입주한 빌딩
|
|
20
|
+
- A학원: 강남 지점
|
|
21
|
+
- B학원: 종로 지점
|
|
22
|
+
- C학원: 부산 지점
|
|
23
|
+
|
|
24
|
+
❌ 사고: A학원 사용자가 B학원 학생 명단을 봄 → 회사 망함
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 현재의 일반적 NestJS 코드 (지저분하고 위험)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// 호출자가 tenantId를 직접 넘겨야 해서 누락 위험이 큼
|
|
31
|
+
async findAll(tenantId: string) {
|
|
32
|
+
// where에 tenantId를 직접 넣어야만 같은 테넌트 데이터만 조회됨
|
|
33
|
+
return this.repo.find({ where: { tenantId } });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 단건 조회도 id + tenantId를 매번 함께 조건으로 넣어야 안전함
|
|
37
|
+
async findOne(tenantId: string, id: string) {
|
|
38
|
+
// tenantId를 빼먹으면 다른 테넌트의 같은 id를 조회할 수 있음
|
|
39
|
+
return this.repo.findOne({ where: { id, tenantId } });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 캐시 조회 시에도 tenantId를 키 prefix에 수동 반영해야 함
|
|
43
|
+
async getCachedRoster(tenantId: string) {
|
|
44
|
+
// prefix 누락 시 캐시가 테넌트 간 섞일 수 있음
|
|
45
|
+
return this.cache.get(`roster:${tenantId}`);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
문제:
|
|
50
|
+
- 모든 메서드에 `tenantId` 수동 명시 → 빠뜨리면 데이터 누출
|
|
51
|
+
- 캐시 키 prefix 빠뜨리면 다른 tenant 데이터가 보임
|
|
52
|
+
- 백그라운드 작업에서 컨텍스트 잃기 쉬움
|
|
53
|
+
- 코드 리뷰에서 100% 잡아내기 어려움
|
|
54
|
+
|
|
55
|
+
### nestjs-tenant-shield의 솔루션
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// Nest DI에 서비스로 등록
|
|
59
|
+
@Injectable()
|
|
60
|
+
// 이 클래스의 메서드 실행 시 tenant 컨텍스트가 필수
|
|
61
|
+
@RequireTenant()
|
|
62
|
+
export class StudentsService {
|
|
63
|
+
|
|
64
|
+
// 호출부에서 tenantId를 전달하지 않아도 자동 격리됨
|
|
65
|
+
async findAll() {
|
|
66
|
+
// 내부적으로 현재 tenant 기준 WHERE가 주입됨
|
|
67
|
+
return this.repo.find();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// id만 받아도 tenant 범위 안에서만 조회됨
|
|
71
|
+
async findOne(id: string) {
|
|
72
|
+
// where에 tenantId를 직접 쓰지 않아도 안전하게 동작
|
|
73
|
+
return this.repo.findOne({ where: { id } });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 캐시 사용 시에도 tenant 단위로 키 분리
|
|
77
|
+
@Cacheable({ ttl: 300, tenantScoped: true })
|
|
78
|
+
async getRoster() {
|
|
79
|
+
// 같은 key라도 tenant마다 별도 캐시 엔트리로 저장됨
|
|
80
|
+
return this.repo.find();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
개선:
|
|
86
|
+
- tenant_id 명시 0번
|
|
87
|
+
- 빠뜨릴 가능성 0
|
|
88
|
+
- 캐시 자동 분리
|
|
89
|
+
- 실수로 다른 tenant 접근 시 즉시 throw
|
|
90
|
+
|
|
91
|
+
## ✨ 핵심 차별화
|
|
92
|
+
|
|
93
|
+
| 기능 | nestjs-cls | nestjs-tenancy류 | nestjs-tenant-shield |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| AsyncLocalStorage | ✅ | 부분 | ✅ |
|
|
96
|
+
| Auto WHERE 주입 | ❌ | ❌ | ✅ 핵심 |
|
|
97
|
+
| Database/Schema 분리 | ❌ | ✅ | ✅ (v0.2) |
|
|
98
|
+
| 캐시 키 자동 분리 | ❌ | ❌ | ✅ |
|
|
99
|
+
| 백그라운드 작업 컨텍스트 | 부분 | ❌ | ✅ |
|
|
100
|
+
| 테스트 시 cross-tenant 자동 차단 | ❌ | ❌ | ✅ 핵심 |
|
|
101
|
+
| Postgres RLS 통합 | ❌ | ❌ | ✅ (v0.3) |
|
|
102
|
+
|
|
103
|
+
카테고리: 기존은 "도구". nestjs-tenant-shield는 **"멀티테넌시 안전망"**.
|
|
104
|
+
|
|
105
|
+
## ✅ 차별성 판단 (현재 기준)
|
|
106
|
+
|
|
107
|
+
- 아이디어 차별성: **있음**
|
|
108
|
+
- 구현 차별성: **아직 없음**
|
|
109
|
+
|
|
110
|
+
왜냐하면 시장에서 평가하는 것은 문서가 아니라 아래 증거이기 때문입니다.
|
|
111
|
+
|
|
112
|
+
- 동작하는 패키지
|
|
113
|
+
- 신뢰 가능한 통합 테스트
|
|
114
|
+
- 실제 누출 시나리오 재현/차단 데모
|
|
115
|
+
- 외부 도입 사례 또는 파일럿
|
|
116
|
+
|
|
117
|
+
이 프로젝트는 위 증거를 만드는 단계로 진입해야 본격적인 시장 경쟁력을 갖습니다.
|
|
118
|
+
|
|
119
|
+
## 📦 설치
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm add nestjs-tenant-shield # 라이브러리 본체 설치
|
|
123
|
+
|
|
124
|
+
# TypeORM 사용 시 추가 설치
|
|
125
|
+
pnpm add typeorm # TypeORM 연동 기능 사용
|
|
126
|
+
|
|
127
|
+
# Prisma 사용 시 추가 설치 (v0.2)
|
|
128
|
+
pnpm add @prisma/client # Prisma Client 연동 준비
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
필수 환경:
|
|
132
|
+
- NestJS 10.0+
|
|
133
|
+
- TypeScript 4.7+
|
|
134
|
+
- Node.js 18+
|
|
135
|
+
- TypeORM 0.3+ (v0.1)
|
|
136
|
+
|
|
137
|
+
현재는 설치보다 아래 문서를 우선 확인하세요.
|
|
138
|
+
|
|
139
|
+
- `docs/tenant-shield-PRD.md`
|
|
140
|
+
- `docs/tenant-shield-api-spec.md`
|
|
141
|
+
- `claude/IMPLEMENTATION_PLAN.md`
|
|
142
|
+
|
|
143
|
+
## 🚀 빠른 시작
|
|
144
|
+
|
|
145
|
+
아래는 v0.1 기준 API 예시입니다. v0.2 이상 기능은 별도 표기합니다.
|
|
146
|
+
|
|
147
|
+
### 1단계: 모듈 설정
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Nest의 모듈 데코레이터
|
|
151
|
+
import { Module } from '@nestjs/common';
|
|
152
|
+
// 멀티테넌시 보호 모듈
|
|
153
|
+
import { TenantShieldModule } from 'nestjs-tenant-shield';
|
|
154
|
+
|
|
155
|
+
// 루트 모듈 정의
|
|
156
|
+
@Module({
|
|
157
|
+
imports: [
|
|
158
|
+
// 전역 설정 등록
|
|
159
|
+
TenantShieldModule.forRoot({
|
|
160
|
+
strategy: 'discriminator', // 단일 테이블 + tenantId 컬럼 전략
|
|
161
|
+
tenantIdField: 'tenantId', // 테넌트 컬럼명 (서비스에 맞게 변경 가능)
|
|
162
|
+
tenantSource: 'header', // 요청 헤더에서 tenant 식별
|
|
163
|
+
headerName: 'x-tenant-id', // 사용할 헤더 이름
|
|
164
|
+
strictMode: true, // tenant 없으면 즉시 예외 발생
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
// 앱 시작 모듈
|
|
169
|
+
export class AppModule {}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2단계: Entity에 tenant_id 추가
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
|
176
|
+
|
|
177
|
+
@Entity()
|
|
178
|
+
export class Student {
|
|
179
|
+
@PrimaryGeneratedColumn()
|
|
180
|
+
id: number;
|
|
181
|
+
|
|
182
|
+
@Column()
|
|
183
|
+
tenantId: string;
|
|
184
|
+
|
|
185
|
+
@Column()
|
|
186
|
+
name: string;
|
|
187
|
+
|
|
188
|
+
@Column()
|
|
189
|
+
grade: number;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 3단계: 서비스에 데코레이터
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// 서비스 데코레이터 import
|
|
197
|
+
import { Injectable } from '@nestjs/common';
|
|
198
|
+
// tenant 강제 + 캐시 데코레이터 import
|
|
199
|
+
import { RequireTenant, Cacheable } from 'nestjs-tenant-shield';
|
|
200
|
+
|
|
201
|
+
// DI 등록
|
|
202
|
+
@Injectable()
|
|
203
|
+
// 이 서비스의 메서드는 tenant 컨텍스트 없으면 실행 불가
|
|
204
|
+
@RequireTenant()
|
|
205
|
+
export class StudentsService {
|
|
206
|
+
// 저장소 주입
|
|
207
|
+
constructor(private readonly repo: StudentsRepository) {}
|
|
208
|
+
|
|
209
|
+
// 현재 tenant 범위의 학생 목록 조회
|
|
210
|
+
async findAll() {
|
|
211
|
+
return this.repo.find();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 현재 tenant 범위에서 id로 단건 조회
|
|
215
|
+
async findOne(id: string) {
|
|
216
|
+
return this.repo.findOne({ where: { id } });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 현재 tenant 범위에서 학생 정보 수정
|
|
220
|
+
async update(id: string, dto: UpdateStudentDto) {
|
|
221
|
+
return this.repo.update(id, dto);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// tenant별 캐시 분리 + 300초 TTL
|
|
225
|
+
@Cacheable({ ttl: 300, tenantScoped: true })
|
|
226
|
+
async getStatistics() {
|
|
227
|
+
// count도 tenant 범위로 자동 제한됨
|
|
228
|
+
return { total: await this.repo.count() };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 4단계: 백그라운드 작업 (v0.2)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Bull 큐 프로세서 데코레이터 import
|
|
237
|
+
import { Processor, Process } from '@nestjs/bull';
|
|
238
|
+
// 잡 실행 시 tenant 컨텍스트 복원 데코레이터
|
|
239
|
+
import { TenantContext } from 'nestjs-tenant-shield';
|
|
240
|
+
|
|
241
|
+
// reports 큐를 처리하는 워커
|
|
242
|
+
@Processor('reports')
|
|
243
|
+
export class ReportProcessor {
|
|
244
|
+
|
|
245
|
+
// monthly 잡 타입 핸들러
|
|
246
|
+
@Process('monthly')
|
|
247
|
+
// 잡 payload의 tenant 정보로 컨텍스트를 설정
|
|
248
|
+
@TenantContext()
|
|
249
|
+
async generate(job: Job<{ tenantId: string }>) {
|
|
250
|
+
// 백그라운드에서도 tenant 격리 유지
|
|
251
|
+
return this.studentsService.findAll();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 5단계: 테스트
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// 테스트 헬퍼 및 에러 타입 import
|
|
260
|
+
import { runWithTenant, CrossTenantAccessError } from 'nestjs-tenant-shield';
|
|
261
|
+
|
|
262
|
+
// StudentsService 동작 검증
|
|
263
|
+
describe('StudentsService', () => {
|
|
264
|
+
// 같은 tenant 데이터만 보이는지 확인
|
|
265
|
+
it('A 학원 컨텍스트에서는 A 학생만 조회', async () => {
|
|
266
|
+
// 테스트 실행 컨텍스트를 academy-A로 고정
|
|
267
|
+
await runWithTenant('academy-A', async () => {
|
|
268
|
+
// 서비스 호출
|
|
269
|
+
const students = await service.findAll();
|
|
270
|
+
// 결과 전체가 academy-A 소속인지 검증
|
|
271
|
+
expect(students.every(s => s.tenantId === 'academy-A')).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// 다른 tenant 데이터 접근 차단 검증
|
|
276
|
+
it('cross-tenant 시도 시 자동 throw', async () => {
|
|
277
|
+
// 실행 컨텍스트는 academy-A
|
|
278
|
+
await runWithTenant('academy-A', async () => {
|
|
279
|
+
// academy-B 학생을 조회하면 예외가 발생해야 함
|
|
280
|
+
await expect(
|
|
281
|
+
service.findOne('student-from-academy-B')
|
|
282
|
+
).rejects.toThrow(CrossTenantAccessError);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 📖 핵심 개념
|
|
289
|
+
|
|
290
|
+
### AsyncLocalStorage
|
|
291
|
+
|
|
292
|
+
비유: 사무실에 여러 손님이 동시에 와도 각 손님 요청을 안 섞이게 처리.
|
|
293
|
+
|
|
294
|
+
요청 진입 시 미들웨어가 "이 요청은 학원 A" 표를 붙이고, 비동기 호출 체인 끝까지 그 표가 따라옴.
|
|
295
|
+
|
|
296
|
+
### Discriminator 패턴
|
|
297
|
+
|
|
298
|
+
같은 테이블에 `tenant_id` 컬럼:
|
|
299
|
+
|
|
300
|
+
```sql
|
|
301
|
+
| id | tenantId | name | grade |
|
|
302
|
+
|----|------------|---------|-------|
|
|
303
|
+
| 1 | academy-A | 김민수 | 90 |
|
|
304
|
+
| 2 | academy-A | 이지연 | 85 |
|
|
305
|
+
| 3 | academy-B | 박철수 | 88 | ← 다른 학원
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
쿼리 시 `WHERE tenantId = 'academy-A'` 빠뜨리면 사고. 라이브러리가 자동 주입.
|
|
309
|
+
|
|
310
|
+
```sql
|
|
311
|
+
-- tenant_id 조건을 누락한 위험한 조회 (금지)
|
|
312
|
+
SELECT id, tenantId, name, grade
|
|
313
|
+
FROM student;
|
|
314
|
+
|
|
315
|
+
-- tenant_id 조건을 포함한 안전한 조회 (권장)
|
|
316
|
+
SELECT id, tenantId, name, grade
|
|
317
|
+
FROM student
|
|
318
|
+
WHERE tenantId = 'academy-A';
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### 다층 격리 (Defense in Depth)
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
1층: ORM 쿼리 (자동 WHERE) ← v0.1
|
|
325
|
+
2층: 캐시 키 (자동 prefix) ← v0.1
|
|
326
|
+
3층: 백그라운드 큐 (컨텍스트 전파) ← v0.2
|
|
327
|
+
4층: DB Row-Level Security ← v0.3
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
한 층 뚫려도 다음 층에서 차단.
|
|
331
|
+
|
|
332
|
+
## 🔧 API 레퍼런스
|
|
333
|
+
|
|
334
|
+
상세 API 명세는 [`docs/tenant-shield-api-spec.md`](./docs/tenant-shield-api-spec.md) 참조. 핵심만 요약:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// 모듈 설정: 앱 시작 시 tenant-shield 전역 옵션 등록
|
|
338
|
+
TenantShieldModule.forRoot({
|
|
339
|
+
strategy: 'discriminator', // 단일 DB/테이블 + tenant 컬럼 전략
|
|
340
|
+
tenantIdField: 'tenantId', // tenant 식별 컬럼명
|
|
341
|
+
tenantSource: 'header' | 'jwt' | 'subdomain' | 'custom', // tenant 추출 소스 타입
|
|
342
|
+
// ... source별 옵션 // 소스마다 추가 설정 가능
|
|
343
|
+
strictMode: true, // tenant 누락 요청 즉시 차단
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// 데코레이터: 메서드/클래스에 보안 정책 부착
|
|
347
|
+
@RequireTenant({ allowSystem?: boolean }) // tenant 컨텍스트 강제
|
|
348
|
+
@SystemAction() // 시스템 작업(예외 허용 경로) 표시
|
|
349
|
+
@Cacheable({ ttl: number, tenantScoped?: boolean }) // 캐시 TTL + tenant 분리 여부
|
|
350
|
+
@TenantContext({ extractFrom?: (job) => string }) // v0.2, 큐 잡에서 tenant 추출
|
|
351
|
+
|
|
352
|
+
// 헬퍼: 코드에서 컨텍스트를 직접 제어할 때 사용
|
|
353
|
+
const tenantId = getCurrentTenantId(); // 현재 요청 tenant 읽기
|
|
354
|
+
await runWithTenant('A', async () => {}); // 특정 tenant 컨텍스트로 실행
|
|
355
|
+
await runWithoutTenant(async () => {}); // tenant 없이 시스템 작업 실행
|
|
356
|
+
|
|
357
|
+
// 에러: tenant 관련 예외 타입
|
|
358
|
+
class MissingTenantContextError extends Error {} // tenant가 없을 때
|
|
359
|
+
class CrossTenantAccessError extends Error {} // 타 tenant 접근 감지 시
|
|
360
|
+
class InvalidTenantSourceError extends Error {} // tenant 추출 소스 설정 오류
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 🛡️ 보안 가이드
|
|
364
|
+
|
|
365
|
+
### nestjs-tenant-shield가 자동 보호하는 것
|
|
366
|
+
|
|
367
|
+
- ✅ 표준 Repository 메서드 (find, findOne, save, update, delete)
|
|
368
|
+
- ✅ QueryBuilder의 단일 entity 쿼리
|
|
369
|
+
- ✅ 캐시 키 분리
|
|
370
|
+
- ✅ 백그라운드 작업 컨텍스트 (v0.2)
|
|
371
|
+
|
|
372
|
+
### nestjs-tenant-shield가 자동 보호 **못하는** 것 ⚠️
|
|
373
|
+
|
|
374
|
+
- ❌ Raw SQL (`repo.query`, `dataSource.query`)
|
|
375
|
+
- ❌ JOIN의 다른 테이블 (ON 절 명시 필요)
|
|
376
|
+
- ❌ Subquery 내부 (명시 필요)
|
|
377
|
+
- ❌ TypeORM CLI/Migration
|
|
378
|
+
- ❌ 멀티 DataSource 트랜잭션 (v0.1)
|
|
379
|
+
- ❌ 인증 미들웨어 외 tenant_id 변조
|
|
380
|
+
|
|
381
|
+
상세는 [docs/SECURITY.md](./docs/SECURITY.md) 참조.
|
|
382
|
+
|
|
383
|
+
현재 `docs/SECURITY.md`는 아직 생성 전이며, v0.1.0 공개 전 필수 산출물로 작성 예정입니다.
|
|
384
|
+
|
|
385
|
+
### 진실의 한 문장
|
|
386
|
+
|
|
387
|
+
> **nestjs-tenant-shield는 "흔한 사고의 90%를 자동 차단"하는 도구입니다.**
|
|
388
|
+
> **나머지 10%는 위 한계를 인지하고 수동 처리해야 합니다.**
|
|
389
|
+
> **만능 보안이 아니며, 다층 방어(defense in depth)의 한 층입니다.**
|
|
390
|
+
|
|
391
|
+
### 🔬 v0.1 자동 보호 정밀 범위
|
|
392
|
+
|
|
393
|
+
라이브러리가 "어디까지" 자동으로 보호하는지 정확히 알아두면 사고를 막을 수 있습니다.
|
|
394
|
+
|
|
395
|
+
#### ✅ 자동 보호되는 TypeORM API
|
|
396
|
+
|
|
397
|
+
| API | 동작 | 비고 |
|
|
398
|
+
|---|---|---|
|
|
399
|
+
| `Repository.find(options)` | `options.where`에 `tenantId` 자동 머지 | 배열(OR)도 각 절에 머지 |
|
|
400
|
+
| `Repository.findOne(options)` | 동일 | |
|
|
401
|
+
| `Repository.findBy(where)` | `where`에 `tenantId` 머지 | |
|
|
402
|
+
| `Repository.findOneBy(where)` | 동일 | |
|
|
403
|
+
| `Repository.count(options)` | 동일 | |
|
|
404
|
+
| `Repository.save(entity)` | INSERT 시 `tenantId` 자동 주입 / cross-tenant insert 차단 | 배열도 처리 |
|
|
405
|
+
| `SelectQueryBuilder.getMany/One/ManyAndCount/Count/RawMany/RawOne` | 실행 직전 `andWhere('alias.tenantId = :id')` 자동 추가 | 이미 있으면 중복 추가 X |
|
|
406
|
+
| `UpdateQueryBuilder.execute()` | 동일하게 `andWhere` 자동 추가 | |
|
|
407
|
+
| `DeleteQueryBuilder.execute()` | 동일 | |
|
|
408
|
+
| Subscriber `beforeInsert` | `tenantId` 비어 있으면 자동 주입, 다른 tenant면 즉시 throw | |
|
|
409
|
+
| Subscriber `afterLoad` | 로드된 row의 `tenantId`가 컨텍스트와 다르면 즉시 throw | 최후의 안전망 |
|
|
410
|
+
|
|
411
|
+
#### ⚠️ 자동 보호 안 되는 API (수동 보호 필요)
|
|
412
|
+
|
|
413
|
+
| API | 권장 대응 |
|
|
414
|
+
|---|---|
|
|
415
|
+
| `Repository.query(rawSql)` | `withTenantWhere(sql, 'tenant_id')` 헬퍼로 SQL 변환 후 실행 |
|
|
416
|
+
| `dataSource.createQueryRunner().query()` | 동일하게 수동 헬퍼 사용 |
|
|
417
|
+
| `Repository.update(criteria, partial)` (옵션 없는 단순 형태) | QueryBuilder 사용 또는 `criteria`에 `tenantId` 명시 |
|
|
418
|
+
| `Repository.delete(criteria)` (옵션 없는 단순 형태) | 동일 |
|
|
419
|
+
| `Repository.insert(values)` | `save()` 사용 권장 (subscriber hook이 더 잘 동작) |
|
|
420
|
+
| Entity Listener (`@AfterLoad` 등 사용자 정의) | 사용자가 직접 `getCurrentTenantId()`로 검증 |
|
|
421
|
+
|
|
422
|
+
#### 🚪 의도적인 우회 경로
|
|
423
|
+
|
|
424
|
+
다음은 의도된 cross-tenant 접근을 위한 "안전한 우회 경로"입니다:
|
|
425
|
+
|
|
426
|
+
1. **`runWithoutTenant(fn)`** — 시스템 작업용. 컨텍스트에 `isSystemAction: true` 플래그가 박혀, Subscriber와 데코레이터가 자동 검사를 건너뜁니다.
|
|
427
|
+
2. **`@SystemAction()` 데코레이터** — `@RequireTenant()` 클래스 안의 특정 메서드만 우회 표시. `forRoot.allowSystemActions: true` 필요.
|
|
428
|
+
3. **`runWithTenant(otherTenantId, fn)`** — 명시적으로 다른 tenant 컨텍스트에 진입. cron이 모든 tenant를 순회할 때 사용.
|
|
429
|
+
|
|
430
|
+
⚠️ 이 우회 경로들은 강력한 만큼 위험합니다. 보안 로그에 호출 위치를 남기고, 가능하면 `@SystemAction()` + `runWithTenant()` 조합으로 명확하게 표시하세요.
|
|
431
|
+
|
|
432
|
+
### 권장 다층 보안 (Defense in Depth)
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
1. nestjs-tenant-shield (애플리케이션 자동, 90% 케이스) ← 본 라이브러리
|
|
436
|
+
2. + 인증 미들웨어 (tenant_id 검증)
|
|
437
|
+
3. + 코드 리뷰 가이드 (raw SQL/JOIN/subquery 점검)
|
|
438
|
+
4. + Postgres RLS (DB 레이어, v0.3 통합)
|
|
439
|
+
5. + DB 접근 권한 최소화
|
|
440
|
+
6. + 정기 침투 테스트 (분기별)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## 🔄 기존 코드에서 마이그레이션
|
|
444
|
+
|
|
445
|
+
기존에 수동으로 `WHERE tenantId` 적던 코드를 라이브러리로 전환하는 권장 절차.
|
|
446
|
+
|
|
447
|
+
### Step 1: 한 모듈만 먼저
|
|
448
|
+
StudentsModule 같은 작은 모듈 하나만 먼저 적용. 나머지 모듈은 그대로.
|
|
449
|
+
|
|
450
|
+
### Step 2: @RequireTenant 부착
|
|
451
|
+
서비스 클래스에 `@RequireTenant()` 추가.
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// Before
|
|
455
|
+
@Injectable()
|
|
456
|
+
export class StudentsService { /* ... */ }
|
|
457
|
+
|
|
458
|
+
// After
|
|
459
|
+
@Injectable()
|
|
460
|
+
@RequireTenant()
|
|
461
|
+
export class StudentsService { /* ... */ }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Step 3: 수동 WHERE 제거
|
|
465
|
+
`where: { tenantId, ... }` → `where: { ... }`
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// Before
|
|
469
|
+
return this.repo.find({ where: { tenantId, classId } });
|
|
470
|
+
|
|
471
|
+
// After
|
|
472
|
+
return this.repo.find({ where: { classId } }); // tenant 자동 주입
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Step 4: 통합 테스트로 회귀 확인
|
|
476
|
+
`runWithTenant`로 테스트 작성. 누출 자동 감지(`CrossTenantAccessError`).
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
await runWithTenant('A', async () => {
|
|
480
|
+
const list = await service.findAll();
|
|
481
|
+
expect(list.every(s => s.tenantId === 'A')).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Step 5: 다음 모듈로 확장
|
|
486
|
+
한 모듈씩 점진적으로. 일괄 변경 금지 — 한 번에 너무 많이 바꾸면 회귀 추적이 어려움.
|
|
487
|
+
|
|
488
|
+
## 🗺️ 로드맵
|
|
489
|
+
|
|
490
|
+
실행 판단 기준(Go/No-Go):
|
|
491
|
+
|
|
492
|
+
- 4주 내: 동작하는 최소 버전 + 통합 테스트 확보
|
|
493
|
+
- 6~8주 내: 외부 사용자 피드백 3건 이상 또는 파일럿 1건 이상
|
|
494
|
+
- 기준 미달 시: 범위 축소 또는 중단
|
|
495
|
+
|
|
496
|
+
### v0.1 (개발 중)
|
|
497
|
+
- [ ] AsyncLocalStorage 컨텍스트 매니저
|
|
498
|
+
- [ ] @RequireTenant() 데코레이터
|
|
499
|
+
- [ ] TypeORM Subscriber (auto WHERE)
|
|
500
|
+
- [ ] @Cacheable({ tenantScoped })
|
|
501
|
+
- [ ] runWithTenant() 테스트 헬퍼
|
|
502
|
+
|
|
503
|
+
### v0.2
|
|
504
|
+
- [ ] Prisma 어댑터
|
|
505
|
+
- [ ] BullMQ/Bull 통합 (@TenantContext)
|
|
506
|
+
- [ ] Schema/Database 격리 패턴
|
|
507
|
+
- [ ] Tenant purge 헬퍼 (GDPR)
|
|
508
|
+
|
|
509
|
+
### v0.3
|
|
510
|
+
- [ ] Mongoose 어댑터
|
|
511
|
+
- [ ] Postgres RLS 자동 설정
|
|
512
|
+
- [ ] GraphQL DataLoader 통합
|
|
513
|
+
|
|
514
|
+
### v1.0
|
|
515
|
+
- [ ] 성능 벤치마크 (오버헤드 < 1ms)
|
|
516
|
+
- [ ] 마이그레이션 도구 (수동 → 자동)
|
|
517
|
+
- [ ] 보안 감사
|
|
518
|
+
|
|
519
|
+
## 💡 어떤 컬럼명이든 OK — 다양한 도메인 예시
|
|
520
|
+
|
|
521
|
+
`tenantIdField`는 회사/도메인의 관습에 맞게 자유롭게 설정 가능.
|
|
522
|
+
|
|
523
|
+
### 학원 관리 SaaS
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
TenantShieldModule.forRoot({
|
|
527
|
+
strategy: 'discriminator',
|
|
528
|
+
tenantIdField: 'spaceId', // ← 학원 ID
|
|
529
|
+
tenantSource: 'jwt',
|
|
530
|
+
jwtClaim: 'spaceId',
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
@Entity()
|
|
534
|
+
export class Student {
|
|
535
|
+
@Column()
|
|
536
|
+
spaceId: string; // 학원 ID
|
|
537
|
+
|
|
538
|
+
@Column()
|
|
539
|
+
name: string;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@Injectable()
|
|
543
|
+
@RequireTenant()
|
|
544
|
+
export class StudentsService {
|
|
545
|
+
async findAll() {
|
|
546
|
+
return this.repo.find(); // 자동 WHERE space_id = ?
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 병원 관리 SaaS
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// 병원 도메인에 맞게 tenant 컬럼명을 hospitalId로 지정
|
|
555
|
+
TenantShieldModule.forRoot({
|
|
556
|
+
tenantIdField: 'hospitalId', // 테넌트 식별 필드명 (병원 ID)
|
|
557
|
+
tenantSource: 'jwt', // JWT에서 tenant 추출
|
|
558
|
+
jwtClaim: 'hospital_id', // JWT claim 키 이름
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
@Entity()
|
|
562
|
+
export class Patient {
|
|
563
|
+
@Column()
|
|
564
|
+
hospitalId: string;
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Slack 같은 팀 협업
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// 워크스페이스 단위 SaaS 구성 예시
|
|
572
|
+
TenantShieldModule.forRoot({
|
|
573
|
+
tenantIdField: 'workspaceId', // 워크스페이스 식별 필드
|
|
574
|
+
tenantSource: 'subdomain', // 서브도메인에서 tenant 추출
|
|
575
|
+
subdomainPattern: '*.yourapp.com', // tenant 추출 대상 도메인 패턴
|
|
576
|
+
})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### CRM (Salesforce식)
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
TenantShieldModule.forRoot({
|
|
583
|
+
tenantIdField: 'orgId', // ← 조직 ID
|
|
584
|
+
tenantSource: 'header',
|
|
585
|
+
headerName: 'x-org-id',
|
|
586
|
+
})
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### 일반 B2B SaaS
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
TenantShieldModule.forRoot({
|
|
593
|
+
tenantIdField: 'tenantId', // ← 가장 일반적
|
|
594
|
+
})
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
핵심: **회사 도메인 관습 그대로 사용 가능.** `spaceId`, `hospitalId`, `workspaceId`, `orgId`, `accountId`, `companyId`, `clientId`, `customerId` 등 어떤 이름이든 OK.
|
|
598
|
+
|
|
599
|
+
## 🤝 기여
|
|
600
|
+
|
|
601
|
+
이슈와 PR 환영. v0.x 단계에서 적극적인 피드백 수용.
|
|
602
|
+
|
|
603
|
+
## 📜 라이선스
|
|
604
|
+
|
|
605
|
+
MIT © Jinyeong Jung
|
|
606
|
+
|
|
607
|
+
## 📚 문서 안내
|
|
608
|
+
|
|
609
|
+
- README: 사용법/예제/빠른 시작 (이 문서)
|
|
610
|
+
- docs/tenant-shield-api-spec.md: API 상세 명세
|
|
611
|
+
- docs/tenant-shield-PRD.md: 설계/로드맵/배경
|
|
612
|
+
- multi-tenant-explainer.md: 멀티테넌시 개념 요약
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TenantAwareCacheService } from './cache.service';
|
|
2
|
+
/**
|
|
3
|
+
* 모듈 bootstrap 단계에서 1회 호출.
|
|
4
|
+
* 라이브러리 외부에서 직접 호출할 일은 없습니다.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setGlobalCache(cache: TenantAwareCacheService): void;
|
|
7
|
+
/**
|
|
8
|
+
* @Cacheable 데코레이터가 fallback으로 사용하는 lookup 함수.
|
|
9
|
+
*
|
|
10
|
+
* 반환값:
|
|
11
|
+
* - TenantAwareCacheService 인스턴스 (모듈 bootstrap 후 정상 상태)
|
|
12
|
+
* - undefined (모듈이 아직 init되지 않았거나, 라이브러리 미사용 환경)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getGlobalCache(): TenantAwareCacheService | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* 테스트 격리용. 각 테스트 케이스 후 호출해서 다음 테스트에 영향을 안 미치게.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resetGlobalCache(): void;
|
|
19
|
+
//# sourceMappingURL=cache.registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.registry.d.ts","sourceRoot":"","sources":["../../src/cache/cache.registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AA0B1D;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAEnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,uBAAuB,GAAG,SAAS,CAEpE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|