This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user