@emkodev/emkore 1.0.3
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/CHANGELOG.md +269 -0
- package/DEVELOPER_GUIDE.md +227 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/bun.lock +22 -0
- package/example/README.md +200 -0
- package/example/create-user.interactor.ts +88 -0
- package/example/dto/user.dto.ts +34 -0
- package/example/entity/user.entity.ts +54 -0
- package/example/index.ts +18 -0
- package/example/interface/create-user.usecase.ts +93 -0
- package/example/interface/user.repository.ts +23 -0
- package/mod.ts +1 -0
- package/package.json +32 -0
- package/src/common/abstract.actor.ts +59 -0
- package/src/common/abstract.entity.ts +59 -0
- package/src/common/abstract.interceptor.ts +17 -0
- package/src/common/abstract.repository.ts +162 -0
- package/src/common/abstract.usecase.ts +113 -0
- package/src/common/config/config-registry.ts +190 -0
- package/src/common/config/config-section.ts +106 -0
- package/src/common/exception/authorization-exception.ts +28 -0
- package/src/common/exception/repository-exception.ts +46 -0
- package/src/common/interceptor/audit-log.interceptor.ts +181 -0
- package/src/common/interceptor/authorization.interceptor.ts +252 -0
- package/src/common/interceptor/performance.interceptor.ts +101 -0
- package/src/common/llm/api-definition.type.ts +185 -0
- package/src/common/pattern/unit-of-work.ts +78 -0
- package/src/common/platform/env.ts +38 -0
- package/src/common/registry/usecase-registry.ts +80 -0
- package/src/common/type/interceptor-context.type.ts +25 -0
- package/src/common/type/json-schema.type.ts +80 -0
- package/src/common/type/json.type.ts +5 -0
- package/src/common/type/lowercase.type.ts +48 -0
- package/src/common/type/metadata.type.ts +5 -0
- package/src/common/type/money.class.ts +384 -0
- package/src/common/type/permission.type.ts +43 -0
- package/src/common/validation/validation-result.ts +52 -0
- package/src/common/validation/validators.ts +441 -0
- package/src/index.ts +95 -0
- package/test/unit/abstract-actor.test.ts +608 -0
- package/test/unit/actor.test.ts +89 -0
- package/test/unit/api-definition.test.ts +628 -0
- package/test/unit/authorization.test.ts +101 -0
- package/test/unit/entity.test.ts +95 -0
- package/test/unit/money.test.ts +480 -0
- package/test/unit/validation.test.ts +138 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Emkore Example - Modern Production Patterns
|
|
2
|
+
|
|
3
|
+
This example demonstrates the recommended patterns for using emkore in
|
|
4
|
+
production, based on real-world usage in the hardkore codebase.
|
|
5
|
+
|
|
6
|
+
## Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
example/
|
|
10
|
+
├── interface/ # Use case interfaces (contracts)
|
|
11
|
+
│ ├── create-user.usecase.ts
|
|
12
|
+
│ └── user.repository.ts
|
|
13
|
+
├── dto/ # Data Transfer Objects
|
|
14
|
+
│ └── user.dto.ts
|
|
15
|
+
├── entity/ # Business entities
|
|
16
|
+
│ └── user.entity.ts
|
|
17
|
+
└── create-user.interactor.ts # Use case implementation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Key Patterns
|
|
21
|
+
|
|
22
|
+
### 1. Import Style
|
|
23
|
+
|
|
24
|
+
When using emkore in your project, import as a namespace from the published
|
|
25
|
+
package:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import * as emkore from "@emkodev/emkore";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Within the emkore package examples, we use relative imports:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import * as emkore from "../mod.ts"; // For interactor files
|
|
35
|
+
import * as emkore from "../../mod.ts"; // For nested files
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Input/Output Types
|
|
39
|
+
|
|
40
|
+
- **Input**: Use `interface` for input types
|
|
41
|
+
- **Output**: Use type alias pointing to the DTO
|
|
42
|
+
- **Validation**: Use separate validation functions
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
export interface CreateUserInput {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly email: string;
|
|
48
|
+
readonly owner: emkore.ResourceScope;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type CreateUserOutput = UserDto;
|
|
52
|
+
|
|
53
|
+
export const validateCreateUserInput = (
|
|
54
|
+
input: CreateUserInput,
|
|
55
|
+
): emkore.ValidationResult =>
|
|
56
|
+
new emkore.ValidatorBuilder()
|
|
57
|
+
.string("name", input.name, { required: true })
|
|
58
|
+
.email("email", input.email, true)
|
|
59
|
+
.build();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Entity Pattern
|
|
63
|
+
|
|
64
|
+
Entities extend `emkore.Entity` and provide:
|
|
65
|
+
|
|
66
|
+
- Business logic methods
|
|
67
|
+
- `toDto()` for persistence
|
|
68
|
+
- `isValid()` for validation
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export class UserEntity extends emkore.Entity {
|
|
72
|
+
readonly type = "user";
|
|
73
|
+
|
|
74
|
+
override toDto(): UserDto {
|
|
75
|
+
return {
|
|
76
|
+
...super.toDto(),
|
|
77
|
+
name: this.name,
|
|
78
|
+
email: this.email,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. DTO Pattern
|
|
85
|
+
|
|
86
|
+
DTOs extend `emkore.Dto` and represent the persistence structure:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export interface UserDto extends emkore.Dto {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly email: string;
|
|
92
|
+
readonly role?: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 5. Use Case Pattern
|
|
97
|
+
|
|
98
|
+
Use cases define the contract with:
|
|
99
|
+
|
|
100
|
+
- `override` keyword for abstract properties
|
|
101
|
+
- Proper API definitions for RPC/documentation
|
|
102
|
+
- Resource scope support
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
export abstract class CreateUserUsecase extends emkore.Usecase<
|
|
106
|
+
CreateUserInput,
|
|
107
|
+
CreateUserOutput
|
|
108
|
+
> {
|
|
109
|
+
override usecaseName: emkore.UsecaseName = ["create", "user"];
|
|
110
|
+
|
|
111
|
+
override apiDefinition: emkore.ApiDefinition = {
|
|
112
|
+
name: this.usecaseName.join("_"),
|
|
113
|
+
description: "Create a new user in the system",
|
|
114
|
+
// ...
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
override get requiredPermissions(): emkore.Permission[] {
|
|
118
|
+
return [{ action: "create", resource: "user" }];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 6. Interactor Pattern
|
|
124
|
+
|
|
125
|
+
Interactors implement the business logic with:
|
|
126
|
+
|
|
127
|
+
- Input validation
|
|
128
|
+
- Entity creation
|
|
129
|
+
- Repository operations within transactions
|
|
130
|
+
- DTO validation
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
export class CreateUserInteractor extends CreateUserUsecase {
|
|
134
|
+
protected async _execute(
|
|
135
|
+
actor: emkore.Actor,
|
|
136
|
+
input: CreateUserInput,
|
|
137
|
+
): Promise<CreateUserOutput> {
|
|
138
|
+
// 1. Validate input
|
|
139
|
+
const validation = validateCreateUserInput(input);
|
|
140
|
+
if (!validation.isValid) {
|
|
141
|
+
throw new emkore.ValidationException(validation.errors.join(", "));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Create entity
|
|
145
|
+
const entity = new UserEntity({
|
|
146
|
+
id: emkore.Entity.generateId("user"),
|
|
147
|
+
ownerId: input.owner === emkore.ResourceScope.BUSINESS
|
|
148
|
+
? actor.businessId
|
|
149
|
+
: actor.id,
|
|
150
|
+
// ...
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 3. Persist with transaction
|
|
154
|
+
return await this._uowFactory.transaction(actor.businessId, async (uow) => {
|
|
155
|
+
const createdDto = await uow.userRepository.create(entity.toDto());
|
|
156
|
+
|
|
157
|
+
// 4. Validate returned DTO
|
|
158
|
+
const dtoValidation = validateUserDto(createdDto);
|
|
159
|
+
if (!dtoValidation.isValid) {
|
|
160
|
+
throw new emkore.ValidationException("Invalid DTO");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return createdDto;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Best Practices
|
|
170
|
+
|
|
171
|
+
1. **Always validate input** before processing
|
|
172
|
+
2. **Use ResourceScope** for proper ownership (USER vs BUSINESS)
|
|
173
|
+
3. **Validate DTOs** returned from repositories
|
|
174
|
+
4. **Use transactions** for all database operations
|
|
175
|
+
5. **Generate IDs** with proper prefixes using `Entity.generateId("prefix")`
|
|
176
|
+
6. **Keep entities pure** - business logic only, no persistence concerns
|
|
177
|
+
7. **Use the override keyword** for all abstract properties
|
|
178
|
+
|
|
179
|
+
## Running the Example
|
|
180
|
+
|
|
181
|
+
This example is meant to demonstrate patterns. To use it in your application:
|
|
182
|
+
|
|
183
|
+
1. Implement a concrete `UnitOfWorkFactory` for your database
|
|
184
|
+
2. Implement repositories for your storage layer
|
|
185
|
+
3. Wire up dependencies with your DI container
|
|
186
|
+
4. Register use cases in your registry for RPC exposure
|
|
187
|
+
|
|
188
|
+
## Migration from Old Patterns
|
|
189
|
+
|
|
190
|
+
If you're using older patterns:
|
|
191
|
+
|
|
192
|
+
| Old Pattern | New Pattern |
|
|
193
|
+
| ------------------------- | ------------------------------------------- |
|
|
194
|
+
| Relative imports | `import * as emkore from "@emkodev/emkore"` |
|
|
195
|
+
| Abstract class for Input | `interface` for Input |
|
|
196
|
+
| Abstract class for Output | Type alias to DTO |
|
|
197
|
+
| Static validation methods | Separate validation functions |
|
|
198
|
+
| No Entity class | Entity extends `emkore.Entity` |
|
|
199
|
+
| Direct DTO manipulation | Entity with `toDto()` method |
|
|
200
|
+
| No `override` keyword | Use `override` for all abstract properties |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as emkore from "../mod.ts";
|
|
2
|
+
import { UserEntity } from "./entity/user.entity.ts";
|
|
3
|
+
import { validateUserDto } from "./dto/user.dto.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CreateUserInput,
|
|
6
|
+
type CreateUserOutput,
|
|
7
|
+
CreateUserUsecase,
|
|
8
|
+
validateCreateUserInput,
|
|
9
|
+
} from "./interface/create-user.usecase.ts";
|
|
10
|
+
import type { UserRepository } from "./interface/user.repository.ts";
|
|
11
|
+
|
|
12
|
+
// Example concrete UnitOfWorkFactory implementation
|
|
13
|
+
interface ExampleUnitOfWork extends emkore.UnitOfWork {
|
|
14
|
+
userRepository: UserRepository;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ExampleUnitOfWorkFactory extends emkore.UnitOfWorkFactory {
|
|
18
|
+
transaction<T>(
|
|
19
|
+
businessId: string,
|
|
20
|
+
callback: (uow: ExampleUnitOfWork) => Promise<T>,
|
|
21
|
+
): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Concrete implementation of CreateUserUsecase.
|
|
26
|
+
*
|
|
27
|
+
* This Interactor:
|
|
28
|
+
* 1. Extends the abstract CreateUserUsecase (inherits all metadata)
|
|
29
|
+
* 2. Accepts dependencies through constructor (UnitOfWorkFactory)
|
|
30
|
+
* 3. Implements only the _execute method with HOW to create a user
|
|
31
|
+
* 4. Uses UoW pattern for transaction management
|
|
32
|
+
* 5. Follows production patterns from hardkore codebase
|
|
33
|
+
*/
|
|
34
|
+
export class CreateUserInteractor extends CreateUserUsecase {
|
|
35
|
+
constructor(private readonly _uowFactory: ExampleUnitOfWorkFactory) {
|
|
36
|
+
super();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected async _execute(
|
|
40
|
+
actor: emkore.Actor,
|
|
41
|
+
input: CreateUserInput,
|
|
42
|
+
): Promise<CreateUserOutput> {
|
|
43
|
+
// Validate input
|
|
44
|
+
const validation = validateCreateUserInput(input);
|
|
45
|
+
if (!validation.isValid) {
|
|
46
|
+
throw new emkore.ValidationException(validation.errors.join(", "));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create entity
|
|
50
|
+
const entity = new UserEntity({
|
|
51
|
+
id: emkore.Entity.generateId("user"),
|
|
52
|
+
ownerId: input.owner === emkore.ResourceScope.BUSINESS
|
|
53
|
+
? actor.businessId
|
|
54
|
+
: actor.id,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
name: input.name,
|
|
58
|
+
email: input.email,
|
|
59
|
+
...(input.role && { role: input.role }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Persist via repository using transaction
|
|
63
|
+
return await this._uowFactory.transaction(actor.businessId, async (uow) => {
|
|
64
|
+
// Check for existing email
|
|
65
|
+
const existingUser = await uow.userRepository.findByEmail(input.email);
|
|
66
|
+
if (existingUser) {
|
|
67
|
+
throw new emkore.ValidationException(
|
|
68
|
+
`Email ${input.email} already exists`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create the user
|
|
73
|
+
const createdDto = await uow.userRepository.create(entity.toDto());
|
|
74
|
+
|
|
75
|
+
// Validate the returned DTO
|
|
76
|
+
const dtoValidation = validateUserDto(createdDto);
|
|
77
|
+
if (!dtoValidation.isValid) {
|
|
78
|
+
throw new emkore.ValidationException(
|
|
79
|
+
`Invalid DTO returned from repository: ${
|
|
80
|
+
dtoValidation.errors.join(", ")
|
|
81
|
+
}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return createdDto;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as emkore from "../../mod.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User Data Transfer Object.
|
|
5
|
+
* This represents the data structure for persistence.
|
|
6
|
+
*/
|
|
7
|
+
export interface UserDto extends emkore.Dto {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly email: string;
|
|
10
|
+
readonly role?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates a User DTO.
|
|
15
|
+
*/
|
|
16
|
+
export const validateUserDto = (dto: UserDto): emkore.ValidationResult =>
|
|
17
|
+
new emkore.ValidatorBuilder()
|
|
18
|
+
.entityId("id", dto.id, "user", true)
|
|
19
|
+
.string("ownerId", dto.ownerId, { required: true })
|
|
20
|
+
.string("name", dto.name, {
|
|
21
|
+
required: true,
|
|
22
|
+
minLength: 1,
|
|
23
|
+
maxLength: 100,
|
|
24
|
+
})
|
|
25
|
+
.email("email", dto.email, true)
|
|
26
|
+
.string("role", dto.role, { required: false })
|
|
27
|
+
.dateTime("createdAt", new Date(dto.createdAt), { required: true })
|
|
28
|
+
.dateTime("updatedAt", new Date(dto.updatedAt), { required: true })
|
|
29
|
+
.dateTime(
|
|
30
|
+
"deletedAt",
|
|
31
|
+
dto.deletedAt ? new Date(dto.deletedAt) : undefined,
|
|
32
|
+
{ required: false },
|
|
33
|
+
)
|
|
34
|
+
.build();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as emkore from "../../mod.ts";
|
|
2
|
+
import { type UserDto, validateUserDto } from "../dto/user.dto.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User Entity class.
|
|
6
|
+
* This represents the business object with behavior.
|
|
7
|
+
*/
|
|
8
|
+
export class UserEntity extends emkore.Entity {
|
|
9
|
+
readonly type = "user";
|
|
10
|
+
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly email: string;
|
|
13
|
+
readonly role?: string;
|
|
14
|
+
|
|
15
|
+
constructor(dto: UserDto) {
|
|
16
|
+
super(dto);
|
|
17
|
+
this.name = dto.name;
|
|
18
|
+
this.email = dto.email;
|
|
19
|
+
if (dto.role) this.role = dto.role;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts the entity back to a DTO for persistence.
|
|
24
|
+
*/
|
|
25
|
+
override toDto(): UserDto {
|
|
26
|
+
return {
|
|
27
|
+
...super.toDto(),
|
|
28
|
+
name: this.name,
|
|
29
|
+
email: this.email,
|
|
30
|
+
...(this.role && { role: this.role }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validates the entity's data.
|
|
36
|
+
*/
|
|
37
|
+
isValid(): boolean {
|
|
38
|
+
return validateUserDto(this.toDto()).isValid;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Business logic example: Check if user has admin role.
|
|
43
|
+
*/
|
|
44
|
+
isAdmin(): boolean {
|
|
45
|
+
return this.role === "admin";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Business logic example: Format display name.
|
|
50
|
+
*/
|
|
51
|
+
getDisplayName(): string {
|
|
52
|
+
return this.name || this.email.split("@")[0];
|
|
53
|
+
}
|
|
54
|
+
}
|
package/example/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example exports demonstrating modern emkore patterns.
|
|
3
|
+
*
|
|
4
|
+
* This example shows production-ready patterns based on real-world usage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// DTOs
|
|
8
|
+
export * from "./dto/user.dto.ts";
|
|
9
|
+
|
|
10
|
+
// Entities
|
|
11
|
+
export * from "./entity/user.entity.ts";
|
|
12
|
+
|
|
13
|
+
// Interfaces
|
|
14
|
+
export * from "./interface/create-user.usecase.ts";
|
|
15
|
+
export * from "./interface/user.repository.ts";
|
|
16
|
+
|
|
17
|
+
// Interactors
|
|
18
|
+
export * from "./create-user.interactor.ts";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as emkore from "../../mod.ts";
|
|
2
|
+
import type { UserDto } from "../dto/user.dto.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input data for creating a new user.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateUserInput {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly email: string;
|
|
10
|
+
readonly role?: string;
|
|
11
|
+
readonly owner: emkore.ResourceScope;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Output data for create operation.
|
|
16
|
+
* Returns plain DTO object, not Entity class.
|
|
17
|
+
*/
|
|
18
|
+
export type CreateUserOutput = UserDto;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates the create user input.
|
|
22
|
+
*/
|
|
23
|
+
export const validateCreateUserInput = (
|
|
24
|
+
input: CreateUserInput,
|
|
25
|
+
): emkore.ValidationResult =>
|
|
26
|
+
new emkore.ValidatorBuilder()
|
|
27
|
+
.string("name", input.name, {
|
|
28
|
+
required: true,
|
|
29
|
+
minLength: 1,
|
|
30
|
+
maxLength: 100,
|
|
31
|
+
})
|
|
32
|
+
.email("email", input.email, true)
|
|
33
|
+
.string("role", input.role, { required: false })
|
|
34
|
+
.enum("owner", input.owner, Object.values(emkore.ResourceScope))
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Abstract Usecase defining the contract for creating a user.
|
|
39
|
+
* This defines WHAT the usecase does, not HOW it does it.
|
|
40
|
+
*/
|
|
41
|
+
export abstract class CreateUserUsecase extends emkore.Usecase<
|
|
42
|
+
CreateUserInput,
|
|
43
|
+
CreateUserOutput
|
|
44
|
+
> {
|
|
45
|
+
override usecaseName: emkore.UsecaseName = ["create", "user"];
|
|
46
|
+
|
|
47
|
+
override apiDefinition: emkore.ApiDefinition = {
|
|
48
|
+
name: this.usecaseName.join("_"),
|
|
49
|
+
description: "Create a new user in the system",
|
|
50
|
+
parameters: [
|
|
51
|
+
{
|
|
52
|
+
name: "name",
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "User full name",
|
|
55
|
+
isRequired: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "email",
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "User email address",
|
|
61
|
+
isRequired: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "role",
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Optional user role",
|
|
67
|
+
isRequired: false,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "owner",
|
|
71
|
+
type: "string",
|
|
72
|
+
description: `Resource scope owner (${
|
|
73
|
+
Object.values(emkore.ResourceScope).join(", ")
|
|
74
|
+
})`,
|
|
75
|
+
isRequired: true,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
returns: {
|
|
79
|
+
type: "object",
|
|
80
|
+
description:
|
|
81
|
+
"Result containing the created user with id, name, email, and timestamps",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
override get requiredPermissions(): emkore.Permission[] {
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
action: "create",
|
|
89
|
+
resource: "user",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as emkore from "../../mod.ts";
|
|
2
|
+
import type { UserDto } from "../dto/user.dto.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User Repository interface.
|
|
6
|
+
* Extends the base repository with user-specific methods.
|
|
7
|
+
*/
|
|
8
|
+
export interface UserRepository extends emkore.Repository<UserDto> {
|
|
9
|
+
/**
|
|
10
|
+
* Find a user by email address.
|
|
11
|
+
*/
|
|
12
|
+
findByEmail(email: string): Promise<UserDto | null>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find users by role.
|
|
16
|
+
*/
|
|
17
|
+
findByRole(role: string): Promise<UserDto[]>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if email exists in the system.
|
|
21
|
+
*/
|
|
22
|
+
emailExists(email: string): Promise<boolean>;
|
|
23
|
+
}
|
package/mod.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emkodev/emkore",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "A TypeScript foundation framework for building robust, type-safe business applications with clean architecture patterns",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "emko.dev",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"typescript",
|
|
10
|
+
"clean-architecture",
|
|
11
|
+
"business-logic",
|
|
12
|
+
"framework",
|
|
13
|
+
"patterns"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/emkore/emkore.git"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./mod.ts"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"check": "bun run tsc --noEmit",
|
|
25
|
+
"lint": "biome lint ./src ./test",
|
|
26
|
+
"fmt": "biome format --write ./src ./test"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"bun-types": "^1.3.9",
|
|
30
|
+
"typescript": "^5.9.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Permission } from "./type/permission.type.ts";
|
|
2
|
+
|
|
3
|
+
export abstract class Actor {
|
|
4
|
+
private _permissions = new Set<Permission>();
|
|
5
|
+
private _cachedTeamIds?: string[] | undefined;
|
|
6
|
+
|
|
7
|
+
abstract get id(): string;
|
|
8
|
+
abstract get businessId(): string;
|
|
9
|
+
abstract get token(): string;
|
|
10
|
+
|
|
11
|
+
get permissions(): Permission[] {
|
|
12
|
+
return Array.from(this._permissions);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
set permissions(permissions: Permission[]) {
|
|
16
|
+
this._permissions = new Set(permissions);
|
|
17
|
+
this._cachedTeamIds = undefined; // Invalidate cache when permissions change
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
addPermission(permission: Permission): void {
|
|
21
|
+
this._permissions.add(permission);
|
|
22
|
+
this._cachedTeamIds = undefined; // Invalidate cache
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addPermissions(permissions: Permission[]): void {
|
|
26
|
+
permissions.forEach((permission) => this._permissions.add(permission));
|
|
27
|
+
this._cachedTeamIds = undefined; // Invalidate cache
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract unique team IDs from user permissions.
|
|
32
|
+
*
|
|
33
|
+
* Team membership is derived from permissions with `constraints.teamId`.
|
|
34
|
+
* A user is a member of a team if they have ANY permission with that teamId.
|
|
35
|
+
*
|
|
36
|
+
* Result is cached for performance within the actor's lifetime (request scope).
|
|
37
|
+
*
|
|
38
|
+
* @returns Array of team IDs the actor belongs to
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const teamIds = actor.getTeamIds(); // ["1100-legal", "1100-sales"]
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
getTeamIds(): string[] {
|
|
45
|
+
if (this._cachedTeamIds) {
|
|
46
|
+
return this._cachedTeamIds;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const teamIds = new Set<string>();
|
|
50
|
+
for (const permission of this._permissions) {
|
|
51
|
+
if (permission.constraints?.teamId) {
|
|
52
|
+
teamIds.add(permission.constraints.teamId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._cachedTeamIds = Array.from(teamIds);
|
|
57
|
+
return this._cachedTeamIds;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ValidationResult } from "./validation/validation-result.ts";
|
|
2
|
+
import { ValidatorBuilder } from "./validation/validators.ts";
|
|
3
|
+
|
|
4
|
+
export interface Dto {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly ownerId: string;
|
|
7
|
+
readonly createdAt: string;
|
|
8
|
+
readonly updatedAt: string;
|
|
9
|
+
readonly deletedAt?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const validateDto = (): ValidationResult =>
|
|
13
|
+
new ValidatorBuilder()
|
|
14
|
+
.string("id", "id")
|
|
15
|
+
.string("ownerId", "")
|
|
16
|
+
.dateTime("createdAt", new Date())
|
|
17
|
+
.build();
|
|
18
|
+
|
|
19
|
+
export abstract class Entity {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly ownerId: string;
|
|
22
|
+
readonly createdAt: Date;
|
|
23
|
+
readonly updatedAt: Date;
|
|
24
|
+
readonly deletedAt?: Date;
|
|
25
|
+
|
|
26
|
+
constructor(dto: Dto) {
|
|
27
|
+
this.id = dto.id;
|
|
28
|
+
this.ownerId = dto.ownerId;
|
|
29
|
+
this.createdAt = new Date(dto.createdAt);
|
|
30
|
+
this.updatedAt = new Date(dto.updatedAt);
|
|
31
|
+
if (dto.deletedAt) {
|
|
32
|
+
this.deletedAt = new Date(dto.deletedAt);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isDeleted(): boolean {
|
|
37
|
+
return this.deletedAt !== undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toDto(): Dto {
|
|
41
|
+
return {
|
|
42
|
+
id: this.id,
|
|
43
|
+
ownerId: this.ownerId,
|
|
44
|
+
createdAt: this.createdAt.toISOString(),
|
|
45
|
+
updatedAt: this.updatedAt.toISOString(),
|
|
46
|
+
...(this.deletedAt && { deletedAt: this.deletedAt.toISOString() }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static generateId(prefix: string): string {
|
|
51
|
+
const validSymbols = /^[0-9a-fA-F]{4}$/;
|
|
52
|
+
if (!validSymbols.test(prefix)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Prefix must be exactly four hexadecimal characters (0-9, a-f, A-F), no hyphens.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return `${prefix}-${crypto.randomUUID()}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InterceptorContext } from "./type/interceptor-context.type.ts";
|
|
2
|
+
|
|
3
|
+
export interface Interceptor<Input, Output> {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
|
|
6
|
+
beforeExecute?(
|
|
7
|
+
input: Input,
|
|
8
|
+
context: InterceptorContext,
|
|
9
|
+
): Input | Promise<Input>;
|
|
10
|
+
|
|
11
|
+
afterExecute?(
|
|
12
|
+
result: Output,
|
|
13
|
+
context: InterceptorContext,
|
|
14
|
+
): Output | Promise<Output>;
|
|
15
|
+
|
|
16
|
+
onError?(error: Error, context: InterceptorContext): void | Promise<void>;
|
|
17
|
+
}
|