@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,162 @@
|
|
|
1
|
+
import type { Dto } from "./abstract.entity.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base filter type for repository queries.
|
|
5
|
+
*
|
|
6
|
+
* Extracts business fields from DTO (excludes id, ownerId, createdAt, updatedAt, deletedAt)
|
|
7
|
+
* and adds back the base fields with appropriate query types:
|
|
8
|
+
* - ownerId: exact match filtering
|
|
9
|
+
* - createdAt/updatedAt/deletedAt: range-based date filtering
|
|
10
|
+
*
|
|
11
|
+
* @template TDto - The DTO type to create a filter for
|
|
12
|
+
*/
|
|
13
|
+
export type BaseFilter<TDto extends Dto> = Omit<TDto, keyof Dto> & {
|
|
14
|
+
readonly ownerId?: string;
|
|
15
|
+
readonly createdAt?: { from?: Date; to?: Date };
|
|
16
|
+
readonly updatedAt?: { from?: Date; to?: Date };
|
|
17
|
+
readonly deletedAt?: { from?: Date; to?: Date };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base repository interface defining common CRUD operations for DTOs.
|
|
22
|
+
*
|
|
23
|
+
* @template T - The DTO type this repository works with, must extend Dto
|
|
24
|
+
* @template F - The filter type for queries, defaults to Partial<T>
|
|
25
|
+
*/
|
|
26
|
+
export abstract class Repository<T extends Dto, F = Partial<T>> {
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new DTO in the repository.
|
|
29
|
+
* @param dto - The DTO to create
|
|
30
|
+
* @returns Promise resolving to the created DTO
|
|
31
|
+
* @throws ValidationException if DTO data is invalid
|
|
32
|
+
* @throws EntityAlreadyExistsException if DTO with same ID already exists
|
|
33
|
+
* @throws DatabaseException on database operation failure
|
|
34
|
+
*/
|
|
35
|
+
abstract create(dto: T): Promise<T>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves a DTO by ID.
|
|
39
|
+
* @param id - The DTO ID
|
|
40
|
+
* @returns Promise resolving to the found DTO
|
|
41
|
+
* @throws ValidationException if ID is invalid
|
|
42
|
+
* @throws EntityNotFoundException if DTO not found
|
|
43
|
+
* @throws DatabaseException on database operation failure
|
|
44
|
+
*/
|
|
45
|
+
abstract retrieve(id: string): Promise<T>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Updates an existing DTO.
|
|
49
|
+
* @param dto - The DTO to update
|
|
50
|
+
* @returns Promise resolving to the updated DTO
|
|
51
|
+
* @throws ValidationException if DTO data is invalid
|
|
52
|
+
* @throws EntityNotFoundException if DTO not found
|
|
53
|
+
* @throws DatabaseException on database operation failure
|
|
54
|
+
*/
|
|
55
|
+
abstract update(dto: T): Promise<T>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deletes a DTO by ID.
|
|
59
|
+
* @param id - The DTO ID
|
|
60
|
+
* @returns Promise resolving when deletion is complete
|
|
61
|
+
* @throws ValidationException if ID is invalid
|
|
62
|
+
* @throws EntityNotFoundException if DTO not found
|
|
63
|
+
* @throws DatabaseException on database operation failure
|
|
64
|
+
*/
|
|
65
|
+
abstract delete(id: string): Promise<void>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lists DTOs with pagination and filtering.
|
|
69
|
+
* @param options - Query options including filter, pagination
|
|
70
|
+
* @returns Promise resolving to list of DTOs
|
|
71
|
+
* @throws ValidationException if query parameters are invalid
|
|
72
|
+
* @throws DatabaseException on database operation failure
|
|
73
|
+
*/
|
|
74
|
+
abstract list(options?: {
|
|
75
|
+
filter?: Partial<F>;
|
|
76
|
+
limit?: number;
|
|
77
|
+
offset?: number;
|
|
78
|
+
sortBy?: string;
|
|
79
|
+
sortOrder?: "asc" | "desc";
|
|
80
|
+
}): Promise<T[]>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Counts DTOs with filtering.
|
|
84
|
+
* @param filter - Optional filter criteria
|
|
85
|
+
* @returns Promise resolving to DTO count
|
|
86
|
+
* @throws ValidationException if filter parameters are invalid
|
|
87
|
+
* @throws DatabaseException on database operation failure
|
|
88
|
+
*/
|
|
89
|
+
abstract count(filter?: Partial<F>): Promise<number>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks if a DTO exists by ID.
|
|
93
|
+
* Default implementation uses retrieve and catches EntityNotFoundException.
|
|
94
|
+
* Can be overridden for more efficient implementation.
|
|
95
|
+
* @param id - The DTO ID
|
|
96
|
+
* @returns Promise resolving to boolean existence check
|
|
97
|
+
*/
|
|
98
|
+
async exists(id: string): Promise<boolean> {
|
|
99
|
+
try {
|
|
100
|
+
await this.retrieve(id);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof Error && error.name === "EntityNotFoundException") {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Deletes multiple DTOs by IDs in a single transaction.
|
|
112
|
+
* Default implementation calls delete for each ID.
|
|
113
|
+
* Should be overridden for batch operations in production repositories.
|
|
114
|
+
* @param ids - Array of DTO IDs to delete
|
|
115
|
+
* @returns Promise resolving to number of DTOs actually deleted
|
|
116
|
+
*/
|
|
117
|
+
async bulkDelete(ids: string[]): Promise<number> {
|
|
118
|
+
let deletedCount = 0;
|
|
119
|
+
for (const id of ids) {
|
|
120
|
+
try {
|
|
121
|
+
await this.delete(id);
|
|
122
|
+
deletedCount++;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Log error but continue with other deletions
|
|
125
|
+
console.error(`Failed to delete DTO ${id}:`, error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return deletedCount;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Creates multiple DTOs in a single transaction.
|
|
133
|
+
* Default implementation calls create for each DTO.
|
|
134
|
+
* Should be overridden for batch operations in production repositories.
|
|
135
|
+
* @param dtos - Array of DTOs to create
|
|
136
|
+
* @returns Promise resolving to array of created DTOs
|
|
137
|
+
*/
|
|
138
|
+
async bulkCreate(dtos: T[]): Promise<T[]> {
|
|
139
|
+
const created: T[] = [];
|
|
140
|
+
for (const dto of dtos) {
|
|
141
|
+
const result = await this.create(dto);
|
|
142
|
+
created.push(result);
|
|
143
|
+
}
|
|
144
|
+
return created;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Updates multiple DTOs in a single transaction.
|
|
149
|
+
* Default implementation calls update for each DTO.
|
|
150
|
+
* Should be overridden for batch operations in production repositories.
|
|
151
|
+
* @param dtos - Array of DTOs to update
|
|
152
|
+
* @returns Promise resolving to array of updated DTOs
|
|
153
|
+
*/
|
|
154
|
+
async bulkUpdate(dtos: T[]): Promise<T[]> {
|
|
155
|
+
const updated: T[] = [];
|
|
156
|
+
for (const dto of dtos) {
|
|
157
|
+
const result = await this.update(dto);
|
|
158
|
+
updated.push(result);
|
|
159
|
+
}
|
|
160
|
+
return updated;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Actor } from "./abstract.actor.ts";
|
|
2
|
+
import type { Interceptor } from "./abstract.interceptor.ts";
|
|
3
|
+
import { createInterceptorContext } from "./type/interceptor-context.type.ts";
|
|
4
|
+
import type { ApiDefinition } from "./llm/api-definition.type.ts";
|
|
5
|
+
import type { Permission } from "./type/permission.type.ts";
|
|
6
|
+
|
|
7
|
+
export type UsecaseName = readonly [Lowercase<string>, Lowercase<string>];
|
|
8
|
+
|
|
9
|
+
export abstract class Usecase<Input, Output> {
|
|
10
|
+
/* This is necessary for Permissions management and code generation. */
|
|
11
|
+
abstract readonly usecaseName: UsecaseName;
|
|
12
|
+
|
|
13
|
+
/* API definition schema makes it possible to generate API docs, MCP tools, JSON-RPC definitions. It also make it possible for business people to describe the Usecase in more or less plain text. */
|
|
14
|
+
abstract readonly apiDefinition: ApiDefinition;
|
|
15
|
+
|
|
16
|
+
/* Global interceptors are applied to each Interactor (Usecases implementation). Could be used for logging, sanitization, auth, etc. */
|
|
17
|
+
private static readonly _globalInterceptors = new Set<
|
|
18
|
+
// deno-lint-ignore no-explicit-any
|
|
19
|
+
Interceptor<any, any>
|
|
20
|
+
>();
|
|
21
|
+
|
|
22
|
+
/* Local interceptors are applied ONLY to the interactors instances you explicitly called .use() for. */
|
|
23
|
+
private readonly _localInterceptors = new Set<Interceptor<Input, Output>>();
|
|
24
|
+
|
|
25
|
+
/* Global interceptors registry. call `Usecase.use(...)` to add one or more interceptors. */
|
|
26
|
+
// deno-lint-ignore no-explicit-any
|
|
27
|
+
static use(interceptors: Interceptor<any, any>[]): void {
|
|
28
|
+
interceptors.forEach((interceptor) =>
|
|
29
|
+
this._globalInterceptors.add(interceptor)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Global interceptors registry. Instantiate the desired interactor and call `interactor.use(...)` to add one or more interceptors. */
|
|
34
|
+
use(interceptors: Interceptor<Input, Output>[]): void {
|
|
35
|
+
interceptors.forEach((interceptor) =>
|
|
36
|
+
this._localInterceptors.add(interceptor)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Call this method in your code. It will call `_execute` internally, which in turn must be implemented by your Interactors. */
|
|
41
|
+
async execute(actor: Actor, input: Input): Promise<Output> {
|
|
42
|
+
const context = createInterceptorContext(
|
|
43
|
+
actor,
|
|
44
|
+
this.usecaseName,
|
|
45
|
+
this.requiredPermissions,
|
|
46
|
+
{},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
let processedInput = input;
|
|
51
|
+
|
|
52
|
+
// Global interceptors beforeExecute
|
|
53
|
+
for (const interceptor of Usecase._globalInterceptors) {
|
|
54
|
+
if (interceptor.beforeExecute) {
|
|
55
|
+
processedInput = await interceptor.beforeExecute(
|
|
56
|
+
processedInput,
|
|
57
|
+
context,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Local interceptors beforeExecute
|
|
63
|
+
for (const interceptor of this._localInterceptors) {
|
|
64
|
+
if (interceptor.beforeExecute) {
|
|
65
|
+
processedInput = await interceptor.beforeExecute(
|
|
66
|
+
processedInput,
|
|
67
|
+
context,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let output = await this._execute(actor, processedInput);
|
|
73
|
+
|
|
74
|
+
// Local interceptors afterExecute (reversed)
|
|
75
|
+
for (const interceptor of [...this._localInterceptors].reverse()) {
|
|
76
|
+
if (interceptor.afterExecute) {
|
|
77
|
+
output = await interceptor.afterExecute(output, context);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Global interceptors afterExecute (reversed)
|
|
82
|
+
for (const interceptor of [...Usecase._globalInterceptors].reverse()) {
|
|
83
|
+
if (interceptor.afterExecute) {
|
|
84
|
+
output = await interceptor.afterExecute(output, context);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return output;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Local interceptors onError (reversed)
|
|
91
|
+
for (const interceptor of [...this._localInterceptors].reverse()) {
|
|
92
|
+
if (interceptor.onError) {
|
|
93
|
+
await interceptor.onError(error as Error, context);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Global interceptors onError (reversed)
|
|
98
|
+
for (const interceptor of [...Usecase._globalInterceptors].reverse()) {
|
|
99
|
+
if (interceptor.onError) {
|
|
100
|
+
await interceptor.onError(error as Error, context);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* DO NOT call this method directly. DO implement it in your Interactors. */
|
|
109
|
+
protected abstract _execute(actor: Actor, input: Input): Promise<Output>;
|
|
110
|
+
|
|
111
|
+
/* Define the permissions required for this usecase. Return empty array if no permissions needed. */
|
|
112
|
+
abstract get requiredPermissions(): Permission[];
|
|
113
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { ValidationError } from "../validation/validators.ts";
|
|
2
|
+
import { ValidationResult } from "../validation/validation-result.ts";
|
|
3
|
+
import type { ConfigSection } from "./config-section.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 12-Factor App compliant configuration registry.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Modular: Register only the config sections you need
|
|
10
|
+
* - Environment-based: All values from environment variables
|
|
11
|
+
* - No hardcoded environments: No dev/staging/prod classes
|
|
12
|
+
* - Composable: Build your configuration dynamically
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const config = new ConfigRegistry();
|
|
17
|
+
*
|
|
18
|
+
* // Register only what you need
|
|
19
|
+
* config.register("database", new DatabaseConfig());
|
|
20
|
+
* config.register("logging", new LoggingConfig());
|
|
21
|
+
*
|
|
22
|
+
* // Validate all sections
|
|
23
|
+
* const result = config.validate();
|
|
24
|
+
* if (!result.isValid) {
|
|
25
|
+
* throw new Error(`Config validation failed: ${result.errors.join(", ")}`);
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Access config sections
|
|
29
|
+
* const db = config.get<DatabaseConfig>("database");
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class ConfigRegistry {
|
|
33
|
+
private readonly sections = new Map<string, ConfigSection>();
|
|
34
|
+
private _validated = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a configuration section
|
|
38
|
+
* @param name Unique name for this section
|
|
39
|
+
* @param section The configuration section to register
|
|
40
|
+
*/
|
|
41
|
+
register(name: string, section: ConfigSection): void {
|
|
42
|
+
if (this.sections.has(name)) {
|
|
43
|
+
throw new Error(`Configuration section '${name}' is already registered`);
|
|
44
|
+
}
|
|
45
|
+
this.sections.set(name, section);
|
|
46
|
+
this._validated = false; // Reset validation state
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a configuration section by name
|
|
51
|
+
* @param name The name of the section to retrieve
|
|
52
|
+
* @returns The config section or undefined if not found
|
|
53
|
+
*/
|
|
54
|
+
get<T extends ConfigSection>(name: string): T | undefined {
|
|
55
|
+
return this.sections.get(name) as T | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a configuration section by name, throwing if not found
|
|
60
|
+
* @param name The name of the section to retrieve
|
|
61
|
+
* @returns The config section
|
|
62
|
+
* @throws Error if section not found
|
|
63
|
+
*/
|
|
64
|
+
require<T extends ConfigSection>(name: string): T {
|
|
65
|
+
const section = this.get<T>(name);
|
|
66
|
+
if (!section) {
|
|
67
|
+
throw new Error(`Required configuration section '${name}' not found`);
|
|
68
|
+
}
|
|
69
|
+
return section;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a configuration section is registered
|
|
74
|
+
* @param name The name of the section to check
|
|
75
|
+
*/
|
|
76
|
+
has(name: string): boolean {
|
|
77
|
+
return this.sections.has(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove a configuration section
|
|
82
|
+
* @param name The name of the section to remove
|
|
83
|
+
*/
|
|
84
|
+
unregister(name: string): boolean {
|
|
85
|
+
const result = this.sections.delete(name);
|
|
86
|
+
if (result) {
|
|
87
|
+
this._validated = false;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all registered section names
|
|
94
|
+
*/
|
|
95
|
+
getSectionNames(): string[] {
|
|
96
|
+
return Array.from(this.sections.keys());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate all registered configuration sections
|
|
101
|
+
* @returns ValidationResult with any errors found
|
|
102
|
+
*/
|
|
103
|
+
validate(): ValidationResult {
|
|
104
|
+
const errors: ValidationError[] = [];
|
|
105
|
+
|
|
106
|
+
for (const [name, section] of this.sections) {
|
|
107
|
+
const sectionErrors = section.validate();
|
|
108
|
+
// Add section name context to errors
|
|
109
|
+
for (const error of sectionErrors) {
|
|
110
|
+
errors.push(
|
|
111
|
+
new ValidationError(`[${name}] ${error.message}`),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._validated = errors.length === 0;
|
|
117
|
+
return new ValidationResult(
|
|
118
|
+
errors.length === 0,
|
|
119
|
+
errors.map((e) => e.message),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if configuration has been validated successfully
|
|
125
|
+
*/
|
|
126
|
+
get isValidated(): boolean {
|
|
127
|
+
return this._validated;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize the configuration (validate and throw if invalid)
|
|
132
|
+
* @throws Error if validation fails
|
|
133
|
+
*/
|
|
134
|
+
initialize(): void {
|
|
135
|
+
const result = this.validate();
|
|
136
|
+
if (!result.isValid) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Configuration validation failed:\n${result.errors.join("\n")}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convert all sections to a plain object for serialization/debugging
|
|
145
|
+
*/
|
|
146
|
+
toMap(): Record<string, unknown> {
|
|
147
|
+
const result: Record<string, unknown> = {};
|
|
148
|
+
for (const [name, section] of this.sections) {
|
|
149
|
+
result[name] = section.toMap();
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clear all registered sections
|
|
156
|
+
*/
|
|
157
|
+
clear(): void {
|
|
158
|
+
this.sections.clear();
|
|
159
|
+
this._validated = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a ConfigRegistry from environment variables with auto-detection.
|
|
164
|
+
* Detects which configs to load based on presence of marker env vars.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* // Auto-detect and load configs based on environment
|
|
169
|
+
* const config = ConfigRegistry.fromEnvironment({
|
|
170
|
+
* database: () => Deno.env.get("DATABASE_HOST") ? new DatabaseConfig() : undefined,
|
|
171
|
+
* cache: () => Deno.env.get("REDIS_HOST") ? new RedisConfig() : undefined,
|
|
172
|
+
* logging: () => new LoggingConfig(), // Always include
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
static fromEnvironment(
|
|
177
|
+
detectors: Record<string, () => ConfigSection | undefined>,
|
|
178
|
+
): ConfigRegistry {
|
|
179
|
+
const registry = new ConfigRegistry();
|
|
180
|
+
|
|
181
|
+
for (const [name, detector] of Object.entries(detectors)) {
|
|
182
|
+
const section = detector();
|
|
183
|
+
if (section) {
|
|
184
|
+
registry.register(name, section);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return registry;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ValidationError } from "../validation/validators.ts";
|
|
2
|
+
import { getEnv, getEnvOrDefault } from "../platform/env.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base interface for configuration sections.
|
|
6
|
+
* Each section is responsible for:
|
|
7
|
+
* - Loading its values from environment variables
|
|
8
|
+
* - Validating its configuration
|
|
9
|
+
* - Providing a serializable representation
|
|
10
|
+
*/
|
|
11
|
+
export interface ConfigSection {
|
|
12
|
+
/**
|
|
13
|
+
* Validate this configuration section
|
|
14
|
+
* @returns Array of validation errors, empty if valid
|
|
15
|
+
*/
|
|
16
|
+
validate(): ValidationError[];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert to a plain object for serialization/debugging
|
|
20
|
+
*/
|
|
21
|
+
toMap(): Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Base class providing common functionality for config sections
|
|
26
|
+
*/
|
|
27
|
+
export abstract class BaseConfigSection implements ConfigSection {
|
|
28
|
+
abstract validate(): ValidationError[];
|
|
29
|
+
abstract toMap(): Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Helper to get required environment variable
|
|
33
|
+
*/
|
|
34
|
+
protected getRequiredEnv(key: string): string {
|
|
35
|
+
const value = getEnv(key);
|
|
36
|
+
if (!value) {
|
|
37
|
+
throw new Error(`Required environment variable ${key} is not set`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Helper to get optional environment variable with default
|
|
44
|
+
*/
|
|
45
|
+
protected getEnvString(key: string, defaultValue: string): string {
|
|
46
|
+
return getEnvOrDefault(key, defaultValue);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper to parse integer from environment variable
|
|
51
|
+
*/
|
|
52
|
+
protected getEnvInt(key: string, defaultValue: number): number {
|
|
53
|
+
const value = getEnv(key);
|
|
54
|
+
if (!value) return defaultValue;
|
|
55
|
+
const parsed = parseInt(value, 10);
|
|
56
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper to parse boolean from environment variable
|
|
61
|
+
*/
|
|
62
|
+
protected getEnvBool(key: string, defaultValue: boolean): boolean {
|
|
63
|
+
const value = getEnv(key);
|
|
64
|
+
if (!value) return defaultValue;
|
|
65
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Helper to parse duration from environment variable
|
|
70
|
+
* Supports: 30s, 5m, 2h, 1d
|
|
71
|
+
*/
|
|
72
|
+
protected getEnvDuration(key: string, defaultValue: number): number {
|
|
73
|
+
const value = getEnv(key);
|
|
74
|
+
if (!value) return defaultValue;
|
|
75
|
+
|
|
76
|
+
const match = /^(\d+)(ms|s|m|h|d)$/.exec(value.toLowerCase());
|
|
77
|
+
if (!match) return defaultValue;
|
|
78
|
+
|
|
79
|
+
const amount = parseInt(match[1]!, 10);
|
|
80
|
+
const unit = match[2];
|
|
81
|
+
|
|
82
|
+
switch (unit) {
|
|
83
|
+
case "ms":
|
|
84
|
+
return amount;
|
|
85
|
+
case "s":
|
|
86
|
+
return amount * 1000;
|
|
87
|
+
case "m":
|
|
88
|
+
return amount * 60 * 1000;
|
|
89
|
+
case "h":
|
|
90
|
+
return amount * 60 * 60 * 1000;
|
|
91
|
+
case "d":
|
|
92
|
+
return amount * 24 * 60 * 60 * 1000;
|
|
93
|
+
default:
|
|
94
|
+
return defaultValue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Helper to parse comma-separated list from environment variable
|
|
100
|
+
*/
|
|
101
|
+
protected getEnvList(key: string, defaultValue: string[] = []): string[] {
|
|
102
|
+
const value = getEnv(key);
|
|
103
|
+
if (!value) return defaultValue;
|
|
104
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Metadata } from "../type/metadata.type.ts";
|
|
2
|
+
|
|
3
|
+
export class AuthorizationException extends Error {
|
|
4
|
+
public readonly details: Metadata;
|
|
5
|
+
|
|
6
|
+
constructor(message: string, details: Metadata = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "AuthorizationException";
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static insufficientPermissions(options: {
|
|
13
|
+
actorId: string;
|
|
14
|
+
resource: string;
|
|
15
|
+
action: string;
|
|
16
|
+
missingPermissions: string[];
|
|
17
|
+
}): AuthorizationException {
|
|
18
|
+
return new AuthorizationException(
|
|
19
|
+
`Actor ${options.actorId} lacks required permissions for ${options.action} on ${options.resource}`,
|
|
20
|
+
{
|
|
21
|
+
actorId: options.actorId,
|
|
22
|
+
resource: options.resource,
|
|
23
|
+
action: options.action,
|
|
24
|
+
missingPermissions: options.missingPermissions,
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export abstract class RepositoryException extends Error {
|
|
2
|
+
constructor(message: string, override readonly cause?: Error) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "RepositoryException";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class NetworkException extends RepositoryException {
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
public readonly statusCode?: number,
|
|
12
|
+
public readonly responseBody?: string,
|
|
13
|
+
cause?: Error,
|
|
14
|
+
) {
|
|
15
|
+
super(message, cause);
|
|
16
|
+
this.name = "NetworkException";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ValidationException extends RepositoryException {
|
|
21
|
+
constructor(message: string, cause?: Error) {
|
|
22
|
+
super(message, cause);
|
|
23
|
+
this.name = "ValidationException";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class EntityNotFoundException extends RepositoryException {
|
|
28
|
+
constructor(message: string, cause?: Error) {
|
|
29
|
+
super(message, cause);
|
|
30
|
+
this.name = "EntityNotFoundException";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class EntityAlreadyExistsException extends RepositoryException {
|
|
35
|
+
constructor(message: string, cause?: Error) {
|
|
36
|
+
super(message, cause);
|
|
37
|
+
this.name = "EntityAlreadyExistsException";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class DatabaseException extends RepositoryException {
|
|
42
|
+
constructor(message: string, cause?: Error) {
|
|
43
|
+
super(message, cause);
|
|
44
|
+
this.name = "DatabaseException";
|
|
45
|
+
}
|
|
46
|
+
}
|