260320:1131 Refactor Overrall #01
Build and Deploy / deploy (push) Has been cancelled

This commit is contained in:
admin
2026-03-20 11:31:27 +07:00
parent f1b81a7d0d
commit 1d3479770b
147 changed files with 1745 additions and 1567 deletions
@@ -30,9 +30,10 @@ export class CryptoService {
let encrypted = cipher.update(stringValue, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
} catch (error: any) {
// Fix TS18046: Cast error to any or Error to access .message
this.logger.error(`Encryption failed: ${error.message}`);
} catch (error: unknown) {
this.logger.error(
`Encryption failed: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
@@ -49,10 +50,9 @@ export class CryptoService {
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error: any) {
// Fix TS18046: Cast error to any or Error to access .message
} catch (error: unknown) {
this.logger.warn(
`Decryption failed for value. Returning original text. Error: ${error.message}`,
`Decryption failed for value. Returning original text. Error: ${error instanceof Error ? error.message : String(error)}`
);
// กรณี Decrypt ไม่ได้ ให้คืนค่าเดิมเพื่อป้องกัน App Crash
return text;
@@ -12,7 +12,7 @@ export class RequestContextService {
this.cls.run(new Map(), fn);
}
static set(key: string, value: any) {
static set(key: string, value: unknown) {
const store = this.cls.getStore();
if (store) {
store.set(key, value);
@@ -0,0 +1,184 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { UuidResolverService } from './uuid-resolver.service';
// Mock uuid module to avoid ESM import issue with uuid@13
jest.mock('uuid', () => ({
validate: (str: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
str
),
v7: () => '01912345-6789-7abc-8def-0123456789ab',
}));
describe('UuidResolverService', () => {
let service: UuidResolverService;
let mockQuery: jest.Mock;
beforeEach(async () => {
mockQuery = jest.fn();
const module: TestingModule = await Test.createTestingModule({
providers: [
UuidResolverService,
{
provide: DataSource,
useValue: {
manager: { query: mockQuery },
},
},
],
}).compile();
service = module.get<UuidResolverService>(UuidResolverService);
});
afterEach(() => jest.clearAllMocks());
// ==========================================================
// resolve() — Core generic resolver
// ==========================================================
describe('resolve()', () => {
it('should return number directly when value is a number', async () => {
const result = await service.resolve('Project', 'projects', 'id', 42);
expect(result).toBe(42);
expect(mockQuery).not.toHaveBeenCalled();
});
it('should parse numeric string and return number', async () => {
const result = await service.resolve('Project', 'projects', 'id', '99');
expect(result).toBe(99);
expect(mockQuery).not.toHaveBeenCalled();
});
it('should look up UUID string and return PK from DB', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ id: 7 }]);
const result = await service.resolve('Project', 'projects', 'id', uuid);
expect(result).toBe(7);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `id` FROM `projects` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
it('should throw NotFoundException for invalid UUID string', async () => {
await expect(
service.resolve('Project', 'projects', 'id', 'not-a-uuid')
).rejects.toThrow(NotFoundException);
expect(mockQuery).not.toHaveBeenCalled();
});
it('should throw NotFoundException when UUID not found in DB', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([]);
await expect(
service.resolve('Project', 'projects', 'id', uuid)
).rejects.toThrow(NotFoundException);
});
});
// ==========================================================
// Named convenience resolvers
// ==========================================================
describe('resolveProjectId()', () => {
it('should delegate to resolve with projects table', async () => {
const result = await service.resolveProjectId(5);
expect(result).toBe(5);
});
it('should look up UUID for project', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ id: 5 }]);
const result = await service.resolveProjectId(uuid);
expect(result).toBe(5);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `id` FROM `projects` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
});
describe('resolveOrganizationId()', () => {
it('should delegate to resolve with organizations table', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ id: 3 }]);
const result = await service.resolveOrganizationId(uuid);
expect(result).toBe(3);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `id` FROM `organizations` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
});
describe('resolveCorrespondenceId()', () => {
it('should delegate to resolve with correspondences table', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ id: 10 }]);
const result = await service.resolveCorrespondenceId(uuid);
expect(result).toBe(10);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `id` FROM `correspondences` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
});
describe('resolveUserId()', () => {
it('should use user_id as PK column', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ user_id: 8 }]);
const result = await service.resolveUserId(uuid);
expect(result).toBe(8);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `user_id` FROM `users` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
});
describe('resolveContractId()', () => {
it('should delegate to resolve with contracts table', async () => {
const uuid = '01912345-6789-7abc-8def-0123456789ab';
mockQuery.mockResolvedValue([{ id: 2 }]);
const result = await service.resolveContractId(uuid);
expect(result).toBe(2);
expect(mockQuery).toHaveBeenCalledWith(
'SELECT `id` FROM `contracts` WHERE `uuid` = ? LIMIT 1',
[uuid]
);
});
});
// ==========================================================
// Edge cases
// ==========================================================
describe('edge cases', () => {
it('should handle zero as a valid number', async () => {
const result = await service.resolve('Project', 'projects', 'id', 0);
expect(result).toBe(0);
});
it('should handle string "0" as numeric', async () => {
const result = await service.resolve('Project', 'projects', 'id', '0');
expect(result).toBe(0);
});
it('should handle negative number', async () => {
const result = await service.resolve('Project', 'projects', 'id', -1);
expect(result).toBe(-1);
});
});
});
@@ -0,0 +1,105 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { validate as uuidValidate } from 'uuid';
/**
* Shared service to resolve hybrid identifiers (INT | UUID string) to internal INT IDs.
* Eliminates duplicated resolveId helpers across 8+ services.
*
* @see ADR-019 Hybrid Identifier Strategy
*/
@Injectable()
export class UuidResolverService {
constructor(private readonly dataSource: DataSource) {}
/**
* Checks if a string value is a numeric string (not UUID).
* Returns the parsed number or null if not numeric.
*/
private tryParseInt(value: string): number | null {
const num = Number(value);
return isNaN(num) ? null : num;
}
/**
* Low-level UUID lookup: find entity by uuid column, return pkColumn value.
*/
private async lookupByUuid(
entityName: string,
tableName: string,
pkColumn: string,
uuid: string
): Promise<number> {
if (!uuidValidate(uuid)) {
throw new NotFoundException(
`Invalid identifier for ${entityName}: ${uuid}`
);
}
const rows: Record<string, number>[] = await this.dataSource.manager.query(
`SELECT \`${pkColumn}\` FROM \`${tableName}\` WHERE \`uuid\` = ? LIMIT 1`,
[uuid]
);
if (!rows.length) {
throw new NotFoundException(`${entityName} with UUID ${uuid} not found`);
}
return rows[0][pkColumn];
}
/**
* Generic resolver: accepts INT or UUID string, returns internal INT ID.
* - If value is a number, returns it directly.
* - If value is a numeric string, parses and returns it.
* - If value is a UUID string, looks up the entity by uuid column.
*/
async resolve(
entityName: string,
tableName: string,
pkColumn: string,
value: number | string
): Promise<number> {
if (typeof value === 'number') return value;
const num = this.tryParseInt(value);
if (num !== null) return num;
return this.lookupByUuid(entityName, tableName, pkColumn, value);
}
/**
* Resolve projectId (INT or UUID string) to internal INT ID.
*/
async resolveProjectId(projectId: number | string): Promise<number> {
return this.resolve('Project', 'projects', 'id', projectId);
}
/**
* Resolve organizationId (INT or UUID string) to internal INT ID.
*/
async resolveOrganizationId(orgId: number | string): Promise<number> {
return this.resolve('Organization', 'organizations', 'id', orgId);
}
/**
* Resolve correspondenceId (INT or UUID string) to internal INT ID.
*/
async resolveCorrespondenceId(corrId: number | string): Promise<number> {
return this.resolve('Correspondence', 'correspondences', 'id', corrId);
}
/**
* Resolve userId (INT or UUID string) to internal user_id.
*/
async resolveUserId(userId: number | string): Promise<number> {
return this.resolve('User', 'users', 'user_id', userId);
}
/**
* Resolve contractId (INT or UUID string) to internal INT ID.
*/
async resolveContractId(contractId: number | string): Promise<number> {
return this.resolve('Contract', 'contracts', 'id', contractId);
}
}