@devstroupe/devkit-nest 1.1.16 → 1.2.0-beta.1
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/README.md +35 -55
- package/dist/application/crud-use-cases.d.ts +35 -0
- package/dist/application/crud-use-cases.js +44 -0
- package/dist/application/crud-use-cases.test.d.ts +1 -0
- package/dist/application/crud-use-cases.test.js +19 -0
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +18 -0
- package/dist/application/read-model.d.ts +5 -0
- package/dist/application/read-model.js +2 -0
- package/dist/context/execution-context.d.ts +19 -0
- package/dist/context/execution-context.interceptor.d.ts +5 -0
- package/dist/context/execution-context.interceptor.js +35 -0
- package/dist/context/execution-context.interceptor.test.d.ts +1 -0
- package/dist/context/execution-context.interceptor.test.js +26 -0
- package/dist/context/execution-context.js +38 -0
- package/dist/context/execution-context.test.d.ts +1 -0
- package/dist/context/execution-context.test.js +18 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +18 -0
- package/dist/database/base.repository.d.ts +25 -13
- package/dist/database/base.repository.js +63 -29
- package/dist/database/base.repository.test.d.ts +1 -0
- package/dist/database/base.repository.test.js +46 -0
- package/dist/database/typeorm-explain.d.ts +3 -0
- package/dist/database/typeorm-explain.js +8 -0
- package/dist/database/typeorm-explain.test.d.ts +1 -0
- package/dist/database/typeorm-explain.test.js +15 -0
- package/dist/database/typeorm-query.builder.d.ts +36 -8
- package/dist/database/typeorm-query.builder.js +150 -165
- package/dist/database/typeorm-query.builder.test.js +32 -1
- package/dist/database/typeorm-reader.d.ts +9 -0
- package/dist/database/typeorm-reader.js +49 -0
- package/dist/database/typeorm-reader.test.d.ts +1 -0
- package/dist/database/typeorm-reader.test.js +55 -0
- package/dist/database/typeorm-sql.d.ts +3 -0
- package/dist/database/typeorm-sql.js +7 -0
- package/dist/database/typeorm-sql.test.d.ts +1 -0
- package/dist/database/typeorm-sql.test.js +21 -0
- package/dist/database/typeorm-unit-of-work.d.ts +13 -0
- package/dist/database/typeorm-unit-of-work.js +16 -0
- package/dist/database/typeorm-unit-of-work.test.d.ts +1 -0
- package/dist/database/typeorm-unit-of-work.test.js +34 -0
- package/dist/http/base-crud.controller.d.ts +26 -10
- package/dist/http/base-crud.controller.js +46 -36
- package/dist/http/base-crud.controller.test.d.ts +1 -0
- package/dist/http/base-crud.controller.test.js +18 -0
- package/dist/http/module-http-proxy.d.ts +16 -0
- package/dist/http/module-http-proxy.js +160 -0
- package/dist/http/module-http-proxy.test.d.ts +1 -0
- package/dist/http/module-http-proxy.test.js +37 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +19 -0
- package/dist/platform/integrations/company-registry.d.ts +16 -0
- package/dist/platform/integrations/company-registry.js +2 -0
- package/dist/platform/integrations/external-http.d.ts +38 -0
- package/dist/platform/integrations/external-http.js +78 -0
- package/dist/platform/integrations/external-http.test.d.ts +1 -0
- package/dist/platform/integrations/external-http.test.js +42 -0
- package/dist/platform/integrations/index.d.ts +3 -0
- package/dist/platform/integrations/index.js +19 -0
- package/dist/platform/integrations/webhook.d.ts +45 -0
- package/dist/platform/integrations/webhook.js +74 -0
- package/dist/platform/integrations/webhook.test.d.ts +1 -0
- package/dist/platform/integrations/webhook.test.js +66 -0
- package/dist/platform/messaging/index.d.ts +1 -0
- package/dist/platform/messaging/index.js +17 -0
- package/dist/platform/messaging/outbox.d.ts +56 -0
- package/dist/platform/messaging/outbox.js +90 -0
- package/dist/platform/messaging/outbox.test.d.ts +1 -0
- package/dist/platform/messaging/outbox.test.js +65 -0
- package/dist/platform/security/index.d.ts +2 -0
- package/dist/platform/security/index.js +18 -0
- package/dist/platform/security/password-hasher.d.ts +14 -0
- package/dist/platform/security/password-hasher.js +20 -0
- package/dist/platform/security/security.test.d.ts +1 -0
- package/dist/platform/security/security.test.js +24 -0
- package/dist/platform/security/token.d.ts +22 -0
- package/dist/platform/security/token.js +44 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +1 -0
- package/dist/realtime/realtime.gateway.d.ts +4 -1
- package/dist/realtime/realtime.gateway.js +10 -38
- package/dist/realtime/realtime.gateway.test.d.ts +1 -0
- package/dist/realtime/realtime.gateway.test.js +34 -0
- package/dist/realtime/realtime.module.d.ts +7 -0
- package/dist/realtime/realtime.module.js +17 -6
- package/dist/realtime/realtime.tokens.d.ts +1 -0
- package/dist/realtime/realtime.tokens.js +4 -0
- package/dist/storage/drivers/local.storage.js +8 -1
- package/package.json +18 -14
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DataSource, EntityManager, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
|
|
2
|
+
export interface UnitOfWorkContext {
|
|
3
|
+
readonly manager: EntityManager;
|
|
4
|
+
repository<Entity extends ObjectLiteral>(target: EntityTarget<Entity>): Repository<Entity>;
|
|
5
|
+
}
|
|
6
|
+
export interface UnitOfWork {
|
|
7
|
+
run<Result>(work: (context: UnitOfWorkContext) => Promise<Result>): Promise<Result>;
|
|
8
|
+
}
|
|
9
|
+
export declare class TypeOrmUnitOfWork implements UnitOfWork {
|
|
10
|
+
private readonly dataSource;
|
|
11
|
+
constructor(dataSource: Pick<DataSource, 'transaction'>);
|
|
12
|
+
run<Result>(work: (context: UnitOfWorkContext) => Promise<Result>): Promise<Result>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TypeOrmUnitOfWork = void 0;
|
|
4
|
+
class TypeOrmUnitOfWork {
|
|
5
|
+
dataSource;
|
|
6
|
+
constructor(dataSource) {
|
|
7
|
+
this.dataSource = dataSource;
|
|
8
|
+
}
|
|
9
|
+
run(work) {
|
|
10
|
+
return this.dataSource.transaction((manager) => work({
|
|
11
|
+
manager,
|
|
12
|
+
repository: (target) => manager.getRepository(target),
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.TypeOrmUnitOfWork = TypeOrmUnitOfWork;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const typeorm_unit_of_work_1 = require("./typeorm-unit-of-work");
|
|
9
|
+
(0, node_test_1.default)('TypeOrmUnitOfWork commits the callback result and uses its manager', async () => {
|
|
10
|
+
const manager = { getRepository: (target) => ({ target }) };
|
|
11
|
+
const dataSource = { transaction: async (work) => work(manager) };
|
|
12
|
+
const result = await new typeorm_unit_of_work_1.TypeOrmUnitOfWork(dataSource).run(async (context) => {
|
|
13
|
+
strict_1.default.equal(context.manager, manager);
|
|
14
|
+
strict_1.default.deepEqual(context.repository('invoice'), { target: 'invoice' });
|
|
15
|
+
return 'committed';
|
|
16
|
+
});
|
|
17
|
+
strict_1.default.equal(result, 'committed');
|
|
18
|
+
});
|
|
19
|
+
(0, node_test_1.default)('TypeOrmUnitOfWork propagates an intermediate failure for rollback', async () => {
|
|
20
|
+
let rolledBack = false;
|
|
21
|
+
const dataSource = {
|
|
22
|
+
async transaction(work) {
|
|
23
|
+
try {
|
|
24
|
+
return await work({ getRepository() { } });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
rolledBack = true;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
await strict_1.default.rejects(() => new typeorm_unit_of_work_1.TypeOrmUnitOfWork(dataSource).run(async () => { throw new Error('second write failed'); }), /second write failed/);
|
|
33
|
+
strict_1.default.equal(rolledBack, true);
|
|
34
|
+
});
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import type { IFilterPagination, IPaginator } from '@devstroupe/devkit-core';
|
|
2
|
+
export interface IdCodec<ID> {
|
|
3
|
+
decode(raw: string): ID;
|
|
4
|
+
}
|
|
5
|
+
export declare const numberIdCodec: IdCodec<number>;
|
|
6
|
+
export declare const stringIdCodec: IdCodec<string>;
|
|
7
|
+
export declare const uuidIdCodec: IdCodec<string>;
|
|
8
|
+
export interface CrudApplication<Entity, FilterDto, ID, CreateInput, UpdateInput, ListResult> {
|
|
9
|
+
create(input: CreateInput): Promise<Entity>;
|
|
10
|
+
update(id: ID, input: UpdateInput): Promise<Entity>;
|
|
11
|
+
findOne?(id: ID): Promise<Entity | null>;
|
|
12
|
+
/** Compatibility alias for existing services during migration. */
|
|
13
|
+
findById?(id: ID): Promise<Entity | null>;
|
|
14
|
+
findMany(filter: FilterDto): Promise<ListResult>;
|
|
15
|
+
delete(id: ID): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Thin typed HTTP adapter. Events, auditing and realtime belong outside HTTP. */
|
|
18
|
+
export declare abstract class BaseCrudController<Entity, FilterDto extends IFilterPagination = IFilterPagination, ID = number, CreateInput = Partial<Entity>, UpdateInput = Partial<Entity>, ListResult = IPaginator<Entity>> {
|
|
19
|
+
protected readonly application: CrudApplication<Entity, FilterDto, ID, CreateInput, UpdateInput, ListResult>;
|
|
20
|
+
protected readonly idCodec: IdCodec<ID>;
|
|
21
|
+
constructor(application: CrudApplication<Entity, FilterDto, ID, CreateInput, UpdateInput, ListResult>, idCodec?: IdCodec<ID>);
|
|
22
|
+
create(body: CreateInput): Promise<Entity>;
|
|
23
|
+
update(rawId: string, body: UpdateInput): Promise<Entity>;
|
|
24
|
+
findMany(filter: FilterDto): Promise<ListResult>;
|
|
25
|
+
findById(rawId: string): Promise<Entity>;
|
|
26
|
+
delete(rawId: string): Promise<void>;
|
|
11
27
|
}
|
|
@@ -12,52 +12,62 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.BaseCrudController = void 0;
|
|
15
|
+
exports.BaseCrudController = exports.uuidIdCodec = exports.stringIdCodec = exports.numberIdCodec = void 0;
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const swagger_1 = require("@nestjs/swagger");
|
|
18
|
-
|
|
18
|
+
exports.numberIdCodec = {
|
|
19
|
+
decode(raw) {
|
|
20
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
21
|
+
throw new Error(`Invalid numeric ID: ${raw}`);
|
|
22
|
+
return Number(raw);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
exports.stringIdCodec = {
|
|
26
|
+
decode(raw) {
|
|
27
|
+
if (!raw.trim())
|
|
28
|
+
throw new Error('ID cannot be empty.');
|
|
29
|
+
return raw;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
exports.uuidIdCodec = {
|
|
33
|
+
decode(raw) {
|
|
34
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(raw)) {
|
|
35
|
+
throw new Error(`Invalid UUID: ${raw}`);
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** Thin typed HTTP adapter. Events, auditing and realtime belong outside HTTP. */
|
|
19
41
|
class BaseCrudController {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
application;
|
|
43
|
+
idCodec;
|
|
44
|
+
constructor(application, idCodec = exports.numberIdCodec) {
|
|
45
|
+
this.application = application;
|
|
46
|
+
this.idCodec = idCodec;
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const result = await this.service.create(body);
|
|
27
|
-
if (this.realtimeService) {
|
|
28
|
-
const entityName = this.constructor.name.replace('Controller', '').replace('MsController', '').toLowerCase();
|
|
29
|
-
this.realtimeService.publishEntityChange(entityName, 'create', result, result.tenantId);
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
48
|
+
create(body) {
|
|
49
|
+
return this.application.create(body);
|
|
32
50
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (this.realtimeService) {
|
|
36
|
-
const entityName = this.constructor.name.replace('Controller', '').replace('MsController', '').toLowerCase();
|
|
37
|
-
this.realtimeService.publishEntityChange(entityName, 'update', result, result.tenantId);
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
51
|
+
update(rawId, body) {
|
|
52
|
+
return this.application.update(this.idCodec.decode(rawId), body);
|
|
40
53
|
}
|
|
41
|
-
|
|
42
|
-
return
|
|
54
|
+
findMany(filter) {
|
|
55
|
+
return this.application.findMany(filter);
|
|
43
56
|
}
|
|
44
|
-
async findById(
|
|
45
|
-
|
|
57
|
+
async findById(rawId) {
|
|
58
|
+
const id = this.idCodec.decode(rawId);
|
|
59
|
+
const result = this.application.findOne
|
|
60
|
+
? await this.application.findOne(id)
|
|
61
|
+
: await this.application.findById?.(id);
|
|
62
|
+
if (!result)
|
|
63
|
+
throw new common_1.NotFoundException(`Entity ${rawId} not found.`);
|
|
64
|
+
return result;
|
|
46
65
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (this.realtimeService) {
|
|
50
|
-
const entityName = this.constructor.name.replace('Controller', '').replace('MsController', '').toLowerCase();
|
|
51
|
-
this.realtimeService.publishEntityChange(entityName, 'delete', { id: +id });
|
|
52
|
-
}
|
|
66
|
+
delete(rawId) {
|
|
67
|
+
return this.application.delete(this.idCodec.decode(rawId));
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
exports.BaseCrudController = BaseCrudController;
|
|
56
|
-
__decorate([
|
|
57
|
-
(0, common_1.Inject)(realtime_service_1.RealtimeService),
|
|
58
|
-
(0, common_1.Optional)(),
|
|
59
|
-
__metadata("design:type", realtime_service_1.RealtimeService)
|
|
60
|
-
], BaseCrudController.prototype, "realtimeService", void 0);
|
|
61
71
|
__decorate([
|
|
62
72
|
(0, common_1.Post)(),
|
|
63
73
|
(0, swagger_1.ApiOperation)({ summary: 'Create entity' }),
|
|
@@ -95,7 +105,7 @@ __decorate([
|
|
|
95
105
|
], BaseCrudController.prototype, "findById", null);
|
|
96
106
|
__decorate([
|
|
97
107
|
(0, common_1.Delete)(':id'),
|
|
98
|
-
(0, swagger_1.ApiOperation)({ summary: '
|
|
108
|
+
(0, swagger_1.ApiOperation)({ summary: 'Soft delete entity' }),
|
|
99
109
|
__param(0, (0, common_1.Param)('id')),
|
|
100
110
|
__metadata("design:type", Function),
|
|
101
111
|
__metadata("design:paramtypes", [String]),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const base_crud_controller_1 = require("./base-crud.controller");
|
|
9
|
+
(0, node_test_1.default)('ID codecs preserve string and UUID identifiers', () => {
|
|
10
|
+
strict_1.default.equal(base_crud_controller_1.stringIdCodec.decode('external-001'), 'external-001');
|
|
11
|
+
strict_1.default.equal(base_crud_controller_1.uuidIdCodec.decode('550e8400-e29b-41d4-a716-446655440000'), '550e8400-e29b-41d4-a716-446655440000');
|
|
12
|
+
strict_1.default.equal(base_crud_controller_1.numberIdCodec.decode('42'), 42);
|
|
13
|
+
});
|
|
14
|
+
(0, node_test_1.default)('ID codecs reject malformed values', () => {
|
|
15
|
+
strict_1.default.throws(() => base_crud_controller_1.numberIdCodec.decode('12x'));
|
|
16
|
+
strict_1.default.throws(() => base_crud_controller_1.numberIdCodec.decode('0'));
|
|
17
|
+
strict_1.default.throws(() => base_crud_controller_1.uuidIdCodec.decode('not-a-uuid'));
|
|
18
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { TokenVerifier } from '../platform/security/token';
|
|
3
|
+
export interface GatewayProxyRequest extends IncomingMessage {
|
|
4
|
+
originalUrl?: string;
|
|
5
|
+
protocol?: string;
|
|
6
|
+
user?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface GatewayAuthenticationOptions {
|
|
9
|
+
publicPaths?: readonly string[];
|
|
10
|
+
requireActor?: boolean;
|
|
11
|
+
requireTenant?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function authenticateGatewayRequest(request: GatewayProxyRequest, verifier: TokenVerifier, options?: GatewayAuthenticationOptions): Promise<void>;
|
|
14
|
+
export declare class ModuleHttpProxy {
|
|
15
|
+
forward(target: string, request: GatewayProxyRequest, response: ServerResponse): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.ModuleHttpProxy = void 0;
|
|
43
|
+
exports.authenticateGatewayRequest = authenticateGatewayRequest;
|
|
44
|
+
const common_1 = require("@nestjs/common");
|
|
45
|
+
const http = __importStar(require("node:http"));
|
|
46
|
+
const https = __importStar(require("node:https"));
|
|
47
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
48
|
+
'connection',
|
|
49
|
+
'keep-alive',
|
|
50
|
+
'proxy-authenticate',
|
|
51
|
+
'proxy-authorization',
|
|
52
|
+
'te',
|
|
53
|
+
'trailer',
|
|
54
|
+
'transfer-encoding',
|
|
55
|
+
'upgrade',
|
|
56
|
+
]);
|
|
57
|
+
const UNTRUSTED_IDENTITY_HEADERS = new Set([
|
|
58
|
+
'x-actor-id',
|
|
59
|
+
'x-tenant-id',
|
|
60
|
+
'x-user-id',
|
|
61
|
+
]);
|
|
62
|
+
function requestPath(request) {
|
|
63
|
+
return request.originalUrl || request.url || '/';
|
|
64
|
+
}
|
|
65
|
+
function pathname(request) {
|
|
66
|
+
return new URL(requestPath(request), 'http://gateway.local').pathname;
|
|
67
|
+
}
|
|
68
|
+
function bearerToken(request) {
|
|
69
|
+
const authorization = request.headers.authorization;
|
|
70
|
+
if (!authorization)
|
|
71
|
+
return undefined;
|
|
72
|
+
const [scheme, token] = authorization.split(' ');
|
|
73
|
+
return scheme?.toLowerCase() === 'bearer' && token ? token : undefined;
|
|
74
|
+
}
|
|
75
|
+
async function authenticateGatewayRequest(request, verifier, options = {}) {
|
|
76
|
+
if (request.method?.toUpperCase() === 'OPTIONS')
|
|
77
|
+
return;
|
|
78
|
+
if (options.publicPaths?.includes(pathname(request)))
|
|
79
|
+
return;
|
|
80
|
+
const token = bearerToken(request);
|
|
81
|
+
if (!token)
|
|
82
|
+
throw new common_1.UnauthorizedException('Token not found');
|
|
83
|
+
try {
|
|
84
|
+
const claims = await verifier.verify(token);
|
|
85
|
+
const actor = claims.sub ?? claims['id'];
|
|
86
|
+
const tenant = claims['tenantId'];
|
|
87
|
+
if (options.requireActor !== false && actor === undefined) {
|
|
88
|
+
throw new Error('Token actor is required.');
|
|
89
|
+
}
|
|
90
|
+
if (options.requireTenant !== false && tenant === undefined) {
|
|
91
|
+
throw new Error('Token tenant is required.');
|
|
92
|
+
}
|
|
93
|
+
request.user = claims;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new common_1.UnauthorizedException('Invalid or expired token');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function forwardedHeaders(request, target) {
|
|
100
|
+
const headers = {};
|
|
101
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
102
|
+
const normalizedName = name.toLowerCase();
|
|
103
|
+
if (!HOP_BY_HOP_HEADERS.has(normalizedName) && !UNTRUSTED_IDENTITY_HEADERS.has(normalizedName)) {
|
|
104
|
+
headers[name] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const remoteAddress = request.socket.remoteAddress;
|
|
108
|
+
const existingForwardedFor = request.headers['x-forwarded-for'];
|
|
109
|
+
const forwardedFor = Array.isArray(existingForwardedFor)
|
|
110
|
+
? existingForwardedFor.join(', ')
|
|
111
|
+
: existingForwardedFor;
|
|
112
|
+
if (remoteAddress)
|
|
113
|
+
headers['x-forwarded-for'] = forwardedFor ? `${forwardedFor}, ${remoteAddress}` : remoteAddress;
|
|
114
|
+
headers['x-forwarded-host'] = request.headers.host;
|
|
115
|
+
headers['x-forwarded-proto'] = request.protocol || 'http';
|
|
116
|
+
headers.host = target.host;
|
|
117
|
+
return headers;
|
|
118
|
+
}
|
|
119
|
+
let ModuleHttpProxy = class ModuleHttpProxy {
|
|
120
|
+
forward(target, request, response) {
|
|
121
|
+
const targetUrl = new URL(target);
|
|
122
|
+
const transport = targetUrl.protocol === 'https:' ? https : http;
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const proxyRequest = transport.request({
|
|
125
|
+
protocol: targetUrl.protocol,
|
|
126
|
+
hostname: targetUrl.hostname,
|
|
127
|
+
port: targetUrl.port,
|
|
128
|
+
method: request.method,
|
|
129
|
+
path: requestPath(request),
|
|
130
|
+
headers: forwardedHeaders(request, targetUrl),
|
|
131
|
+
}, (proxyResponse) => {
|
|
132
|
+
response.statusCode = proxyResponse.statusCode ?? 502;
|
|
133
|
+
for (const [name, value] of Object.entries(proxyResponse.headers)) {
|
|
134
|
+
if (value !== undefined && !HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
|
|
135
|
+
response.setHeader(name, value);
|
|
136
|
+
}
|
|
137
|
+
proxyResponse.once('end', resolve);
|
|
138
|
+
proxyResponse.once('error', reject);
|
|
139
|
+
proxyResponse.pipe(response);
|
|
140
|
+
});
|
|
141
|
+
proxyRequest.once('error', () => {
|
|
142
|
+
if (!response.headersSent) {
|
|
143
|
+
response.statusCode = 502;
|
|
144
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
145
|
+
response.end(JSON.stringify({ statusCode: 502, message: 'Upstream service unavailable' }));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
response.end();
|
|
149
|
+
}
|
|
150
|
+
resolve();
|
|
151
|
+
});
|
|
152
|
+
request.once('aborted', () => proxyRequest.destroy());
|
|
153
|
+
request.pipe(proxyRequest);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
exports.ModuleHttpProxy = ModuleHttpProxy;
|
|
158
|
+
exports.ModuleHttpProxy = ModuleHttpProxy = __decorate([
|
|
159
|
+
(0, common_1.Injectable)()
|
|
160
|
+
], ModuleHttpProxy);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const common_1 = require("@nestjs/common");
|
|
9
|
+
const module_http_proxy_1 = require("./module-http-proxy");
|
|
10
|
+
function request(path, authorization) {
|
|
11
|
+
return {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
url: path,
|
|
14
|
+
originalUrl: path,
|
|
15
|
+
headers: authorization ? { authorization } : {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
(0, node_test_1.default)('gateway authentication skips only explicitly public paths', async () => {
|
|
19
|
+
let calls = 0;
|
|
20
|
+
const verifier = { verify: async () => { calls += 1; return { sub: '1', tenantId: 'tenant-a' }; } };
|
|
21
|
+
await (0, module_http_proxy_1.authenticateGatewayRequest)(request('/api/core/auth/login'), verifier, {
|
|
22
|
+
publicPaths: ['/api/core/auth/login'],
|
|
23
|
+
});
|
|
24
|
+
strict_1.default.equal(calls, 0);
|
|
25
|
+
});
|
|
26
|
+
(0, node_test_1.default)('gateway authentication validates stateless actor and tenant claims', async () => {
|
|
27
|
+
const verifier = { verify: async () => ({ sub: '7', tenantId: 'tenant-a' }) };
|
|
28
|
+
const incoming = request('/api/catalog/product', 'Bearer valid-token');
|
|
29
|
+
await (0, module_http_proxy_1.authenticateGatewayRequest)(incoming, verifier);
|
|
30
|
+
strict_1.default.equal(incoming.user?.sub, '7');
|
|
31
|
+
strict_1.default.equal(incoming.user?.tenantId, 'tenant-a');
|
|
32
|
+
});
|
|
33
|
+
(0, node_test_1.default)('gateway authentication rejects missing or incomplete tokens', async () => {
|
|
34
|
+
const verifier = { verify: async () => ({ sub: '7' }) };
|
|
35
|
+
await strict_1.default.rejects(() => (0, module_http_proxy_1.authenticateGatewayRequest)(request('/api/orders/order'), verifier), common_1.UnauthorizedException);
|
|
36
|
+
await strict_1.default.rejects(() => (0, module_http_proxy_1.authenticateGatewayRequest)(request('/api/orders/order', 'Bearer incomplete'), verifier), common_1.UnauthorizedException);
|
|
37
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export * from './database/factory-where';
|
|
2
2
|
export * from './database/base.repository';
|
|
3
3
|
export * from './database/typeorm-query.builder';
|
|
4
|
+
export * from './database/typeorm-unit-of-work';
|
|
5
|
+
export * from './database/typeorm-sql';
|
|
6
|
+
export * from './database/typeorm-reader';
|
|
7
|
+
export * from './database/typeorm-explain';
|
|
4
8
|
export * from './http/base-crud.controller';
|
|
9
|
+
export * from './http/module-http-proxy';
|
|
5
10
|
export * from './storage';
|
|
6
11
|
export * from './auth';
|
|
7
12
|
export * from './realtime';
|
|
8
13
|
export * from './audit';
|
|
14
|
+
export * from './context';
|
|
15
|
+
export * from './application';
|
|
16
|
+
export * from './platform';
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./database/factory-where"), exports);
|
|
18
18
|
__exportStar(require("./database/base.repository"), exports);
|
|
19
19
|
__exportStar(require("./database/typeorm-query.builder"), exports);
|
|
20
|
+
__exportStar(require("./database/typeorm-unit-of-work"), exports);
|
|
21
|
+
__exportStar(require("./database/typeorm-sql"), exports);
|
|
22
|
+
__exportStar(require("./database/typeorm-reader"), exports);
|
|
23
|
+
__exportStar(require("./database/typeorm-explain"), exports);
|
|
20
24
|
__exportStar(require("./http/base-crud.controller"), exports);
|
|
25
|
+
__exportStar(require("./http/module-http-proxy"), exports);
|
|
21
26
|
__exportStar(require("./storage"), exports);
|
|
22
27
|
__exportStar(require("./auth"), exports);
|
|
23
28
|
__exportStar(require("./realtime"), exports);
|
|
24
29
|
__exportStar(require("./audit"), exports);
|
|
30
|
+
__exportStar(require("./context"), exports);
|
|
31
|
+
__exportStar(require("./application"), exports);
|
|
32
|
+
__exportStar(require("./platform"), exports);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./security"), exports);
|
|
18
|
+
__exportStar(require("./messaging"), exports);
|
|
19
|
+
__exportStar(require("./integrations"), exports);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExternalRequestContext } from './external-http';
|
|
2
|
+
export interface RegisteredCompany {
|
|
3
|
+
registrationNumber: string;
|
|
4
|
+
legalName: string;
|
|
5
|
+
tradeName?: string;
|
|
6
|
+
status: 'active' | 'inactive' | 'unknown';
|
|
7
|
+
address?: {
|
|
8
|
+
city?: string;
|
|
9
|
+
region?: string;
|
|
10
|
+
postalCode?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Capability contract: vendor DTOs remain inside their concrete adapter. */
|
|
14
|
+
export interface CompanyRegistryGateway {
|
|
15
|
+
findByRegistrationNumber(registrationNumber: string, context: ExternalRequestContext): Promise<RegisteredCompany | null>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ExternalRequestContext {
|
|
2
|
+
correlationId: string;
|
|
3
|
+
tenantId?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ExternalHttpPolicy {
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
maxAttempts: number;
|
|
8
|
+
retryDelayMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ExternalMetricSink {
|
|
11
|
+
record(capability: string, outcome: 'success' | 'failure', durationMs: number): void;
|
|
12
|
+
}
|
|
13
|
+
export interface ExternalLogEntry {
|
|
14
|
+
capability: string;
|
|
15
|
+
correlationId: string;
|
|
16
|
+
attempt: number;
|
|
17
|
+
outcome: 'success' | 'retry' | 'failure';
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ExternalLogSink {
|
|
21
|
+
/** URL, headers, body, credentials and provider DTOs are deliberately absent. */
|
|
22
|
+
record(entry: ExternalLogEntry): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class ExternalCapabilityError extends Error {
|
|
25
|
+
readonly capability: string;
|
|
26
|
+
readonly cause?: unknown | undefined;
|
|
27
|
+
constructor(capability: string, cause?: unknown | undefined);
|
|
28
|
+
}
|
|
29
|
+
export declare class ExternalHttpClient {
|
|
30
|
+
private readonly policy;
|
|
31
|
+
private readonly metrics?;
|
|
32
|
+
private readonly fetcher;
|
|
33
|
+
private readonly logs?;
|
|
34
|
+
constructor(policy?: ExternalHttpPolicy, metrics?: ExternalMetricSink | undefined, fetcher?: typeof fetch, logs?: ExternalLogSink | undefined);
|
|
35
|
+
json<Response>(capability: string, url: string, init: RequestInit, context: ExternalRequestContext): Promise<Response>;
|
|
36
|
+
private recordMetric;
|
|
37
|
+
private recordLog;
|
|
38
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExternalHttpClient = exports.ExternalCapabilityError = void 0;
|
|
4
|
+
class ExternalCapabilityError extends Error {
|
|
5
|
+
capability;
|
|
6
|
+
cause;
|
|
7
|
+
constructor(capability, cause) {
|
|
8
|
+
super(`${capability} is temporarily unavailable.`);
|
|
9
|
+
this.capability = capability;
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
this.name = 'ExternalCapabilityError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.ExternalCapabilityError = ExternalCapabilityError;
|
|
15
|
+
class ExternalHttpClient {
|
|
16
|
+
policy;
|
|
17
|
+
metrics;
|
|
18
|
+
fetcher;
|
|
19
|
+
logs;
|
|
20
|
+
constructor(policy = { timeoutMs: 5_000, maxAttempts: 2, retryDelayMs: 100 }, metrics, fetcher = fetch, logs) {
|
|
21
|
+
this.policy = policy;
|
|
22
|
+
this.metrics = metrics;
|
|
23
|
+
this.fetcher = fetcher;
|
|
24
|
+
this.logs = logs;
|
|
25
|
+
}
|
|
26
|
+
async json(capability, url, init, context) {
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
let lastError;
|
|
29
|
+
for (let attempt = 1; attempt <= this.policy.maxAttempts; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.fetcher(url, {
|
|
32
|
+
...init,
|
|
33
|
+
signal: AbortSignal.timeout(this.policy.timeoutMs),
|
|
34
|
+
headers: { ...init.headers, 'x-correlation-id': context.correlationId },
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok)
|
|
37
|
+
throw new Error(`External HTTP ${response.status}`);
|
|
38
|
+
this.recordLog({
|
|
39
|
+
capability, correlationId: context.correlationId, attempt,
|
|
40
|
+
outcome: 'success', durationMs: Date.now() - startedAt,
|
|
41
|
+
});
|
|
42
|
+
this.recordMetric(capability, 'success', Date.now() - startedAt);
|
|
43
|
+
return await response.json();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
lastError = error;
|
|
47
|
+
if (attempt < this.policy.maxAttempts) {
|
|
48
|
+
this.recordLog({
|
|
49
|
+
capability, correlationId: context.correlationId, attempt,
|
|
50
|
+
outcome: 'retry', durationMs: Date.now() - startedAt,
|
|
51
|
+
});
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, this.policy.retryDelayMs * attempt));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.recordLog({
|
|
56
|
+
capability, correlationId: context.correlationId, attempt,
|
|
57
|
+
outcome: 'failure', durationMs: Date.now() - startedAt,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.recordMetric(capability, 'failure', Date.now() - startedAt);
|
|
63
|
+
throw new ExternalCapabilityError(capability, lastError);
|
|
64
|
+
}
|
|
65
|
+
recordMetric(capability, outcome, durationMs) {
|
|
66
|
+
try {
|
|
67
|
+
this.metrics?.record(capability, outcome, durationMs);
|
|
68
|
+
}
|
|
69
|
+
catch { /* telemetry is non-critical */ }
|
|
70
|
+
}
|
|
71
|
+
recordLog(entry) {
|
|
72
|
+
try {
|
|
73
|
+
this.logs?.record(entry);
|
|
74
|
+
}
|
|
75
|
+
catch { /* telemetry is non-critical */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.ExternalHttpClient = ExternalHttpClient;
|