@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/DEVELOPER_GUIDE.md +227 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/bun.lock +22 -0
  6. package/example/README.md +200 -0
  7. package/example/create-user.interactor.ts +88 -0
  8. package/example/dto/user.dto.ts +34 -0
  9. package/example/entity/user.entity.ts +54 -0
  10. package/example/index.ts +18 -0
  11. package/example/interface/create-user.usecase.ts +93 -0
  12. package/example/interface/user.repository.ts +23 -0
  13. package/mod.ts +1 -0
  14. package/package.json +32 -0
  15. package/src/common/abstract.actor.ts +59 -0
  16. package/src/common/abstract.entity.ts +59 -0
  17. package/src/common/abstract.interceptor.ts +17 -0
  18. package/src/common/abstract.repository.ts +162 -0
  19. package/src/common/abstract.usecase.ts +113 -0
  20. package/src/common/config/config-registry.ts +190 -0
  21. package/src/common/config/config-section.ts +106 -0
  22. package/src/common/exception/authorization-exception.ts +28 -0
  23. package/src/common/exception/repository-exception.ts +46 -0
  24. package/src/common/interceptor/audit-log.interceptor.ts +181 -0
  25. package/src/common/interceptor/authorization.interceptor.ts +252 -0
  26. package/src/common/interceptor/performance.interceptor.ts +101 -0
  27. package/src/common/llm/api-definition.type.ts +185 -0
  28. package/src/common/pattern/unit-of-work.ts +78 -0
  29. package/src/common/platform/env.ts +38 -0
  30. package/src/common/registry/usecase-registry.ts +80 -0
  31. package/src/common/type/interceptor-context.type.ts +25 -0
  32. package/src/common/type/json-schema.type.ts +80 -0
  33. package/src/common/type/json.type.ts +5 -0
  34. package/src/common/type/lowercase.type.ts +48 -0
  35. package/src/common/type/metadata.type.ts +5 -0
  36. package/src/common/type/money.class.ts +384 -0
  37. package/src/common/type/permission.type.ts +43 -0
  38. package/src/common/validation/validation-result.ts +52 -0
  39. package/src/common/validation/validators.ts +441 -0
  40. package/src/index.ts +95 -0
  41. package/test/unit/abstract-actor.test.ts +608 -0
  42. package/test/unit/actor.test.ts +89 -0
  43. package/test/unit/api-definition.test.ts +628 -0
  44. package/test/unit/authorization.test.ts +101 -0
  45. package/test/unit/entity.test.ts +95 -0
  46. package/test/unit/money.test.ts +480 -0
  47. package/test/unit/validation.test.ts +138 -0
  48. package/tsconfig.json +18 -0
@@ -0,0 +1,441 @@
1
+ import { ValidationResult } from "./validation-result.ts";
2
+ import type {
3
+ JsonSchema,
4
+ JsonSchemaType,
5
+ MutableJsonSchema,
6
+ } from "../type/json-schema.type.ts";
7
+
8
+ export class ValidationError {
9
+ constructor(public readonly message: string) {}
10
+
11
+ toString(): string {
12
+ return this.message;
13
+ }
14
+ }
15
+
16
+ export class ValidatorBuilder {
17
+ private _schema: MutableJsonSchema = { type: "object", properties: {} };
18
+ private _errors: ValidationError[] = [];
19
+
20
+ constructor(private readonly _prefix?: string) {}
21
+
22
+ string(
23
+ field: string,
24
+ value: string | null | undefined,
25
+ options: {
26
+ required?: boolean;
27
+ minLength?: number;
28
+ maxLength?: number;
29
+ pattern?: string;
30
+ } = {},
31
+ ): ValidatorBuilder {
32
+ const { required = false, minLength, maxLength, pattern } = options;
33
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
34
+
35
+ if (!this._schema.properties) {
36
+ this._schema.properties = {};
37
+ }
38
+ this._schema.properties[field] = {
39
+ type: "string",
40
+ ...(required && { required: true }),
41
+ ...(minLength !== undefined && { minLength }),
42
+ ...(maxLength !== undefined && { maxLength }),
43
+ ...(pattern && { pattern }),
44
+ };
45
+
46
+ if (required && (!value || value.trim() === "")) {
47
+ this._errors.push(new ValidationError(`${fullField} is required`));
48
+ } else if (value) {
49
+ if (minLength !== undefined && value.length < minLength) {
50
+ this._errors.push(
51
+ new ValidationError(
52
+ `${fullField} must be at least ${minLength} characters`,
53
+ ),
54
+ );
55
+ }
56
+ if (maxLength !== undefined && value.length > maxLength) {
57
+ this._errors.push(
58
+ new ValidationError(
59
+ `${fullField} must be at most ${maxLength} characters`,
60
+ ),
61
+ );
62
+ }
63
+ if (pattern && !new RegExp(pattern).test(value)) {
64
+ this._errors.push(
65
+ new ValidationError(`${fullField} format is invalid`),
66
+ );
67
+ }
68
+ }
69
+
70
+ return this;
71
+ }
72
+
73
+ email(
74
+ field: string,
75
+ value: string | null | undefined,
76
+ required = false,
77
+ ): ValidatorBuilder {
78
+ return this.string(field, value, {
79
+ required,
80
+ pattern: /^[^@]+@[^@]+\.[^@]+$/.source,
81
+ });
82
+ }
83
+
84
+ uuid(
85
+ field: string,
86
+ value: string | null | undefined,
87
+ required = false,
88
+ ): ValidatorBuilder {
89
+ return this.string(field, value, {
90
+ required,
91
+ pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
92
+ .source,
93
+ });
94
+ }
95
+
96
+ entityId(
97
+ field: string,
98
+ value: string | null | undefined,
99
+ prefix: string,
100
+ required = false,
101
+ ): ValidatorBuilder {
102
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
103
+
104
+ if (!this._schema.properties) {
105
+ this._schema.properties = {};
106
+ }
107
+ this._schema.properties[field] = {
108
+ type: "string",
109
+ ...(required && { required: true }),
110
+ pattern:
111
+ /^[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
112
+ .source,
113
+ };
114
+
115
+ if (required && (!value || value === "")) {
116
+ this._errors.push(new ValidationError(`${fullField} is required`));
117
+ } else if (value) {
118
+ const entityIdRegex =
119
+ /^[0-9a-f]{4}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
120
+ if (!entityIdRegex.test(value)) {
121
+ this._errors.push(
122
+ new ValidationError(
123
+ `${fullField} must be in format xxxx-{uuid} where xxxx is a 4-hex-character prefix`,
124
+ ),
125
+ );
126
+ } else if (!value.toLowerCase().startsWith(`${prefix.toLowerCase()}-`)) {
127
+ this._errors.push(
128
+ new ValidationError(`${fullField} must start with prefix ${prefix}`),
129
+ );
130
+ }
131
+ }
132
+
133
+ return this;
134
+ }
135
+
136
+ number(
137
+ field: string,
138
+ value: number | null | undefined,
139
+ options: {
140
+ required?: boolean;
141
+ min?: number;
142
+ max?: number;
143
+ } = {},
144
+ ): ValidatorBuilder {
145
+ const { required = false, min, max } = options;
146
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
147
+
148
+ if (!this._schema.properties) {
149
+ this._schema.properties = {};
150
+ }
151
+ this._schema.properties[field] = {
152
+ type: "number",
153
+ ...(required && { required: true }),
154
+ ...(min !== undefined && { minimum: min }),
155
+ ...(max !== undefined && { maximum: max }),
156
+ };
157
+
158
+ if (required && value == null) {
159
+ this._errors.push(new ValidationError(`${fullField} is required`));
160
+ } else if (value != null) {
161
+ if (min !== undefined && value < min) {
162
+ this._errors.push(
163
+ new ValidationError(`${fullField} must be at least ${min}`),
164
+ );
165
+ }
166
+ if (max !== undefined && value > max) {
167
+ this._errors.push(
168
+ new ValidationError(`${fullField} must be at most ${max}`),
169
+ );
170
+ }
171
+ }
172
+
173
+ return this;
174
+ }
175
+
176
+ integer(
177
+ field: string,
178
+ value: number | null | undefined,
179
+ options: {
180
+ required?: boolean;
181
+ min?: number;
182
+ max?: number;
183
+ } = {},
184
+ ): ValidatorBuilder {
185
+ const { required = false, min, max } = options;
186
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
187
+
188
+ if (!this._schema.properties) {
189
+ this._schema.properties = {};
190
+ }
191
+ this._schema.properties[field] = {
192
+ type: "integer",
193
+ ...(required && { required: true }),
194
+ ...(min !== undefined && { minimum: min }),
195
+ ...(max !== undefined && { maximum: max }),
196
+ };
197
+
198
+ if (required && value == null) {
199
+ this._errors.push(new ValidationError(`${fullField} is required`));
200
+ } else if (value != null) {
201
+ if (!Number.isInteger(value)) {
202
+ this._errors.push(
203
+ new ValidationError(`${fullField} must be an integer`),
204
+ );
205
+ }
206
+ if (min !== undefined && value < min) {
207
+ this._errors.push(
208
+ new ValidationError(`${fullField} must be at least ${min}`),
209
+ );
210
+ }
211
+ if (max !== undefined && value > max) {
212
+ this._errors.push(
213
+ new ValidationError(`${fullField} must be at most ${max}`),
214
+ );
215
+ }
216
+ }
217
+
218
+ return this;
219
+ }
220
+
221
+ boolean(
222
+ field: string,
223
+ value: boolean | null | undefined,
224
+ options: {
225
+ required?: boolean;
226
+ mustBe?: boolean;
227
+ } = {},
228
+ ): ValidatorBuilder {
229
+ const { required = false, mustBe } = options;
230
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
231
+
232
+ if (!this._schema.properties) {
233
+ this._schema.properties = {};
234
+ }
235
+ this._schema.properties[field] = {
236
+ type: "boolean",
237
+ ...(required && { required: true }),
238
+ ...(mustBe !== undefined && { const: mustBe }),
239
+ };
240
+
241
+ if (required && value == null) {
242
+ this._errors.push(new ValidationError(`${fullField} is required`));
243
+ } else if (mustBe !== undefined && value !== mustBe) {
244
+ this._errors.push(new ValidationError(`${fullField} must be ${mustBe}`));
245
+ }
246
+
247
+ return this;
248
+ }
249
+
250
+ array<T>(
251
+ field: string,
252
+ value: T[] | null | undefined,
253
+ options: {
254
+ required?: boolean;
255
+ minItems?: number;
256
+ maxItems?: number;
257
+ itemType?: string;
258
+ allowEmpty?: boolean;
259
+ } = {},
260
+ ): ValidatorBuilder {
261
+ const {
262
+ required = false,
263
+ minItems,
264
+ maxItems,
265
+ itemType = "string",
266
+ allowEmpty = true,
267
+ } = options;
268
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
269
+
270
+ if (!this._schema.properties) {
271
+ this._schema.properties = {};
272
+ }
273
+ this._schema.properties[field] = {
274
+ type: "array",
275
+ ...(required && { required: true }),
276
+ ...(minItems !== undefined && { minItems }),
277
+ ...(maxItems !== undefined && { maxItems }),
278
+ items: { type: itemType as JsonSchemaType },
279
+ };
280
+
281
+ if (required && value == null) {
282
+ this._errors.push(new ValidationError(`${fullField} is required`));
283
+ } else if (value != null) {
284
+ if (!allowEmpty && value.length === 0) {
285
+ this._errors.push(new ValidationError(`${fullField} cannot be empty`));
286
+ }
287
+ if (minItems !== undefined && value.length < minItems) {
288
+ this._errors.push(
289
+ new ValidationError(
290
+ `${fullField} must have at least ${minItems} items`,
291
+ ),
292
+ );
293
+ }
294
+ if (maxItems !== undefined && value.length > maxItems) {
295
+ this._errors.push(
296
+ new ValidationError(
297
+ `${fullField} must have at most ${maxItems} items`,
298
+ ),
299
+ );
300
+ }
301
+ }
302
+
303
+ return this;
304
+ }
305
+
306
+ enum<T>(
307
+ field: string,
308
+ value: T | null | undefined,
309
+ options: readonly T[],
310
+ required = false,
311
+ ): ValidatorBuilder {
312
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
313
+
314
+ if (!this._schema.properties) {
315
+ this._schema.properties = {};
316
+ }
317
+ this._schema.properties[field] = {
318
+ type: "string",
319
+ enum: options.map(String),
320
+ ...(required && { required: true }),
321
+ };
322
+
323
+ if (required && value == null) {
324
+ this._errors.push(new ValidationError(`${fullField} is required`));
325
+ } else if (value != null && !options.includes(value)) {
326
+ this._errors.push(
327
+ new ValidationError(
328
+ `${fullField} must be one of: ${options.join(", ")}`,
329
+ ),
330
+ );
331
+ }
332
+
333
+ return this;
334
+ }
335
+
336
+ dateTime(
337
+ field: string,
338
+ value: Date | null | undefined,
339
+ options: {
340
+ required?: boolean;
341
+ pastOrPresent?: boolean;
342
+ after?: Date;
343
+ before?: Date;
344
+ } = {},
345
+ ): ValidatorBuilder {
346
+ const { required = false, pastOrPresent = false, after, before } = options;
347
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
348
+
349
+ if (!this._schema.properties) {
350
+ this._schema.properties = {};
351
+ }
352
+ this._schema.properties[field] = {
353
+ type: "string",
354
+ format: "date-time",
355
+ ...(required && { required: true }),
356
+ };
357
+
358
+ if (required && value == null) {
359
+ this._errors.push(new ValidationError(`${fullField} is required`));
360
+ } else if (value != null) {
361
+ if (pastOrPresent && value > new Date()) {
362
+ this._errors.push(
363
+ new ValidationError(`${fullField} cannot be in the future`),
364
+ );
365
+ }
366
+ if (after && value < after) {
367
+ this._errors.push(
368
+ new ValidationError(`${fullField} must be after ${after}`),
369
+ );
370
+ }
371
+ if (before && value > before) {
372
+ this._errors.push(
373
+ new ValidationError(`${fullField} must be before ${before}`),
374
+ );
375
+ }
376
+ }
377
+
378
+ return this;
379
+ }
380
+
381
+ custom(
382
+ field: string,
383
+ validator: () => boolean,
384
+ errorMessage: string,
385
+ ): ValidatorBuilder {
386
+ const fullField = this._prefix ? `${this._prefix}.${field}` : field;
387
+
388
+ if (!validator()) {
389
+ this._errors.push(new ValidationError(`${fullField}: ${errorMessage}`));
390
+ }
391
+
392
+ return this;
393
+ }
394
+
395
+ build(): ValidationResult {
396
+ if (this._errors.length === 0) {
397
+ return ValidationResult.success();
398
+ }
399
+
400
+ const fieldErrors: Record<string, string[]> = {};
401
+ const generalErrors: string[] = [];
402
+
403
+ for (const error of this._errors) {
404
+ const message = error.message;
405
+ generalErrors.push(message);
406
+
407
+ // Extract field name from error message for field-specific grouping
408
+ // Messages are in format: "fieldName is required" or "fieldName: custom message"
409
+ const colonIndex = message.indexOf(":");
410
+ const spaceIndex = message.indexOf(" ");
411
+
412
+ let fieldName: string | null = null;
413
+ let errorMsg = message;
414
+
415
+ if (colonIndex !== -1 && (spaceIndex === -1 || colonIndex < spaceIndex)) {
416
+ // Format: "field: message"
417
+ fieldName = message.substring(0, colonIndex);
418
+ errorMsg = message.substring(colonIndex + 1).trim();
419
+ } else if (spaceIndex !== -1) {
420
+ // Format: "field message"
421
+ fieldName = message.substring(0, spaceIndex);
422
+ }
423
+
424
+ if (fieldName) {
425
+ if (!fieldErrors[fieldName]) {
426
+ fieldErrors[fieldName] = [];
427
+ }
428
+ const errors = fieldErrors[fieldName];
429
+ if (errors) {
430
+ errors.push(errorMsg);
431
+ }
432
+ }
433
+ }
434
+
435
+ return new ValidationResult(false, generalErrors, fieldErrors);
436
+ }
437
+
438
+ get schema(): JsonSchema {
439
+ return { ...this._schema } as JsonSchema;
440
+ }
441
+ }
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ // Core abstractions
2
+ export * from "./common/abstract.entity.ts";
3
+ export { Actor } from "./common/abstract.actor.ts";
4
+ export { Usecase, type UsecaseName } from "./common/abstract.usecase.ts";
5
+ export { type Interceptor } from "./common/abstract.interceptor.ts";
6
+
7
+ // Interceptors
8
+ export { AuthorizationInterceptor } from "./common/interceptor/authorization.interceptor.ts";
9
+ export { PerformanceInterceptor } from "./common/interceptor/performance.interceptor.ts";
10
+ export {
11
+ type AuditLogEntry,
12
+ AuditLogInterceptor,
13
+ FileAuditLogger,
14
+ } from "./common/interceptor/audit-log.interceptor.ts";
15
+
16
+ // Types
17
+ export { type JSON } from "./common/type/json.type.ts";
18
+ export {
19
+ type JsonSchema,
20
+ type JsonSchemaProperties,
21
+ type JsonSchemaType,
22
+ } from "./common/type/json-schema.type.ts";
23
+ export { type Metadata } from "./common/type/metadata.type.ts";
24
+ export {
25
+ type CommonAction,
26
+ isCommonAction,
27
+ type Permission,
28
+ type ResourceConstraints,
29
+ ResourceScope,
30
+ } from "./common/type/permission.type.ts";
31
+ export {
32
+ createInterceptorContext,
33
+ type InterceptorContext,
34
+ } from "./common/type/interceptor-context.type.ts";
35
+ export { type Cents, Money } from "./common/type/money.class.ts";
36
+ export {
37
+ type Lowercase,
38
+ type LowercaseArray,
39
+ type LowercaseArrayUnion,
40
+ } from "./common/type/lowercase.type.ts";
41
+
42
+ // LLM API Definition
43
+ export {
44
+ type ApiDefinition,
45
+ type LlmParameter,
46
+ type LlmReturnValue,
47
+ } from "./common/llm/api-definition.type.ts";
48
+
49
+ // Validation
50
+ export { ValidationResult } from "./common/validation/validation-result.ts";
51
+ export {
52
+ ValidationError,
53
+ ValidatorBuilder,
54
+ } from "./common/validation/validators.ts";
55
+
56
+ // Repository
57
+ export { type BaseFilter, Repository } from "./common/abstract.repository.ts";
58
+
59
+ /**
60
+ * @deprecated Use Repository instead. Will be removed in v1.0.0
61
+ */
62
+ export { Repository as BaseRepository } from "./common/abstract.repository.ts";
63
+
64
+ // Configuration (12-Factor App compliant)
65
+ export {
66
+ BaseConfigSection,
67
+ type ConfigSection,
68
+ } from "./common/config/config-section.ts";
69
+ export { ConfigRegistry } from "./common/config/config-registry.ts";
70
+
71
+ // Patterns
72
+ export {
73
+ type UnitOfWork,
74
+ type UnitOfWorkFactory,
75
+ } from "./common/pattern/unit-of-work.ts";
76
+
77
+ // Exceptions
78
+ export { AuthorizationException } from "./common/exception/authorization-exception.ts";
79
+ export {
80
+ DatabaseException,
81
+ EntityAlreadyExistsException,
82
+ EntityNotFoundException,
83
+ NetworkException,
84
+ RepositoryException,
85
+ ValidationException,
86
+ } from "./common/exception/repository-exception.ts";
87
+
88
+ // Platform abstraction
89
+ export { getEnv, getEnvOrDefault } from "./common/platform/env.ts";
90
+
91
+ // Use Case Registry (for auto-discovery systems)
92
+ export {
93
+ type UseCaseRegistryEntry,
94
+ UseCaseRegistryHelpers,
95
+ } from "./common/registry/usecase-registry.ts";