260326:1347 Fixing Refactor ADR-019 Naming convention uuid #01
This commit is contained in:
@@ -14,45 +14,45 @@ class TestEntity extends UuidBaseEntity {
|
|||||||
|
|
||||||
describe('UuidBaseEntity', () => {
|
describe('UuidBaseEntity', () => {
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// generateUuid() — @BeforeInsert hook
|
// generatePublicId() — @BeforeInsert hook
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
|
|
||||||
describe('generateUuid()', () => {
|
describe('generatePublicId()', () => {
|
||||||
it('should generate a UUIDv7 when uuid is not set', () => {
|
it('should generate a UUIDv7 when publicId is not set', () => {
|
||||||
const entity = new TestEntity();
|
const entity = new TestEntity();
|
||||||
expect(entity.uuid).toBeUndefined();
|
expect(entity.publicId).toBeUndefined();
|
||||||
|
|
||||||
entity.generateUuid();
|
entity.generatePublicId();
|
||||||
|
|
||||||
expect(entity.uuid).toBe('01912345-6789-7abc-8def-0123456789ab');
|
expect(entity.publicId).toBe('01912345-6789-7abc-8def-0123456789ab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not overwrite an existing uuid', () => {
|
it('should not overwrite an existing publicId', () => {
|
||||||
const entity = new TestEntity();
|
const entity = new TestEntity();
|
||||||
entity.uuid = 'existing-uuid-value-should-be-kept';
|
entity.publicId = 'existing-publicId-value-should-be-kept';
|
||||||
|
|
||||||
entity.generateUuid();
|
entity.generatePublicId();
|
||||||
|
|
||||||
expect(entity.uuid).toBe('existing-uuid-value-should-be-kept');
|
expect(entity.publicId).toBe('existing-publicId-value-should-be-kept');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not overwrite a pre-set UUIDv1 from DB default', () => {
|
it('should not overwrite a pre-set UUIDv1 from DB default', () => {
|
||||||
const entity = new TestEntity();
|
const entity = new TestEntity();
|
||||||
entity.uuid = '550e8400-e29b-11d4-a716-446655440000';
|
entity.publicId = '550e8400-e29b-11d4-a716-446655440000';
|
||||||
|
|
||||||
entity.generateUuid();
|
entity.generatePublicId();
|
||||||
|
|
||||||
expect(entity.uuid).toBe('550e8400-e29b-11d4-a716-446655440000');
|
expect(entity.publicId).toBe('550e8400-e29b-11d4-a716-446655440000');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate uuid when uuid is empty string', () => {
|
it('should generate publicId when publicId is empty string', () => {
|
||||||
const entity = new TestEntity();
|
const entity = new TestEntity();
|
||||||
entity.uuid = '';
|
entity.publicId = '';
|
||||||
|
|
||||||
entity.generateUuid();
|
entity.generatePublicId();
|
||||||
|
|
||||||
// Empty string is falsy, so generateUuid should assign a new value
|
// Empty string is falsy, so generatePublicId should assign a new value
|
||||||
expect(entity.uuid).toBe('01912345-6789-7abc-8def-0123456789ab');
|
expect(entity.publicId).toBe('01912345-6789-7abc-8def-0123456789ab');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,12 +66,12 @@ describe('UuidBaseEntity', () => {
|
|||||||
expect(entity).toBeInstanceOf(UuidBaseEntity);
|
expect(entity).toBeInstanceOf(UuidBaseEntity);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have uuid property accessible from subclass', () => {
|
it('should have publicId property accessible from subclass', () => {
|
||||||
const entity = new TestEntity();
|
const entity = new TestEntity();
|
||||||
entity.uuid = 'test-uuid';
|
entity.publicId = 'test-publicId';
|
||||||
entity.id = 42;
|
entity.id = 42;
|
||||||
|
|
||||||
expect(entity.uuid).toBe('test-uuid');
|
expect(entity.publicId).toBe('test-publicId');
|
||||||
expect(entity.id).toBe(42);
|
expect(entity.id).toBe(42);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,27 +2,29 @@ import { Column, BeforeInsert } from 'typeorm';
|
|||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base entity providing a UUID public identifier column.
|
* Abstract base entity providing a UUID public identifier.
|
||||||
* Uses MariaDB native UUID type (stored as BINARY(16) internally,
|
|
||||||
* auto-converts to string format — no transformer needed).
|
|
||||||
*
|
*
|
||||||
* App generates UUIDv7 via @BeforeInsert(); DB DEFAULT UUID() is fallback.
|
* Naming Convention (ADR-019 v1.8.1):
|
||||||
|
* - TypeScript Property: `publicId` — semantic name indicating this is the public-facing identifier
|
||||||
|
* - Database Column: `uuid` — MariaDB native UUID type (stored as BINARY(16))
|
||||||
*
|
*
|
||||||
* @see ADR-019 Hybrid Identifier Strategy
|
* This avoids confusion between the property name and the DB data type,
|
||||||
|
* while clearly indicating this is the ID exposed via API (not internal INT PK).
|
||||||
*/
|
*/
|
||||||
export abstract class UuidBaseEntity {
|
export abstract class UuidBaseEntity {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
|
name: 'uuid', // DB column name (MariaDB native UUID type)
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
comment: 'UUID Public Identifier (ADR-019)',
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
})
|
})
|
||||||
uuid!: string;
|
publicId!: string; // TypeScript property name — semantic, avoids type confusion
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
generateUuid(): void {
|
generatePublicId(): void {
|
||||||
if (!this.uuid) {
|
if (!this.publicId) {
|
||||||
this.uuid = uuidv7();
|
this.publicId = uuidv7();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class CirculationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findAll(searchDto: SearchCirculationDto, user: User) {
|
async findAll(searchDto: SearchCirculationDto, user: User) {
|
||||||
const { status, correspondenceUuid, page = 1, limit = 20 } = searchDto;
|
const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto;
|
||||||
const query = this.circulationRepo
|
const query = this.circulationRepo
|
||||||
.createQueryBuilder('c')
|
.createQueryBuilder('c')
|
||||||
.leftJoinAndSelect('c.creator', 'creator')
|
.leftJoinAndSelect('c.creator', 'creator')
|
||||||
@@ -103,9 +103,9 @@ export class CirculationService {
|
|||||||
.leftJoinAndSelect('routings.assignee', 'assignee')
|
.leftJoinAndSelect('routings.assignee', 'assignee')
|
||||||
.leftJoinAndSelect('c.correspondence', 'correspondence');
|
.leftJoinAndSelect('c.correspondence', 'correspondence');
|
||||||
|
|
||||||
if (correspondenceUuid) {
|
if (correspondencePublicId) {
|
||||||
query.where('correspondence.uuid = :corrUuid', {
|
query.where('correspondence.publicId = :corrPublicId', {
|
||||||
corrUuid: correspondenceUuid,
|
corrPublicId: correspondencePublicId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
query.where('c.organizationId = :orgId', {
|
query.where('c.organizationId = :orgId', {
|
||||||
@@ -136,14 +136,14 @@ export class CirculationService {
|
|||||||
return circulation;
|
return circulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByUuid(publicId: string) {
|
||||||
const circulation = await this.circulationRepo.findOne({
|
const circulation = await this.circulationRepo.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
relations: ['routings', 'routings.assignee', 'correspondence', 'creator'],
|
relations: ['routings', 'routings.assignee', 'correspondence', 'creator'],
|
||||||
order: { routings: { stepNumber: 'ASC' } },
|
order: { routings: { stepNumber: 'ASC' } },
|
||||||
});
|
});
|
||||||
if (!circulation)
|
if (!circulation)
|
||||||
throw new NotFoundException(`Circulation UUID ${uuid} not found`);
|
throw new NotFoundException(`Circulation publicId ${publicId} not found`);
|
||||||
return circulation;
|
return circulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class SearchCirculationDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID('all')
|
@IsUUID('all')
|
||||||
correspondenceUuid?: string; // กรองตาม correspondence UUID (ADR-019)
|
correspondencePublicId?: string; // กรองตาม correspondence publicId (ADR-019)
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -100,18 +100,20 @@ export class ContractService {
|
|||||||
return contract;
|
return contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByPublicId(publicId: string) {
|
||||||
const contract = await this.contractRepo.findOne({
|
const contract = await this.contractRepo.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
relations: ['project'],
|
relations: ['project'],
|
||||||
});
|
});
|
||||||
if (!contract)
|
if (!contract)
|
||||||
throw new NotFoundException(`Contract UUID ${uuid} not found`);
|
throw new NotFoundException(
|
||||||
|
`Contract with publicId ${publicId} not found`
|
||||||
|
);
|
||||||
return contract;
|
return contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(uuid: string, dto: UpdateContractDto) {
|
async update(publicId: string, dto: UpdateContractDto) {
|
||||||
const contract = await this.findOneByUuid(uuid);
|
const contract = await this.findOneByPublicId(publicId);
|
||||||
if (dto.projectId) {
|
if (dto.projectId) {
|
||||||
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
|
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
|
||||||
}
|
}
|
||||||
@@ -119,8 +121,8 @@ export class ContractService {
|
|||||||
return this.contractRepo.save(contract);
|
return this.contractRepo.save(contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(uuid: string) {
|
async remove(publicId: string) {
|
||||||
const contract = await this.findOneByUuid(uuid);
|
const contract = await this.findOneByPublicId(publicId);
|
||||||
return this.contractRepo.remove(contract);
|
return this.contractRepo.remove(contract);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,34 +4,18 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
BeforeInsert,
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { Exclude } from 'class-transformer';
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
|
||||||
import { Project } from '../../project/entities/project.entity';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
|
|
||||||
@Entity('contracts')
|
@Entity('contracts')
|
||||||
export class Contract extends BaseEntity {
|
export class Contract extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@Exclude()
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Expose({ name: 'id' })
|
// publicId inherited from UuidBaseEntity (DB column: uuid)
|
||||||
@Column({
|
|
||||||
type: 'uuid',
|
|
||||||
unique: true,
|
|
||||||
nullable: false,
|
|
||||||
comment: 'UUID Public Identifier (ADR-019)',
|
|
||||||
})
|
|
||||||
uuid!: string;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateUuid(): void {
|
|
||||||
if (!this.uuid) {
|
|
||||||
this.uuid = uuidv7();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Column({ name: 'project_id' })
|
@Column({ name: 'project_id' })
|
||||||
projectId!: number;
|
projectId!: number;
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class CorrespondenceWorkflowService {
|
|||||||
type: 'EMAIL',
|
type: 'EMAIL',
|
||||||
entityType: 'correspondence',
|
entityType: 'correspondence',
|
||||||
entityId: revision.correspondenceId,
|
entityId: revision.correspondenceId,
|
||||||
link: `/correspondences/${corrForNotify.uuid}`,
|
link: `/correspondences/${corrForNotify.publicId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export class CorrespondenceService {
|
|||||||
// Fire-and-forget search indexing (non-blocking, void intentional)
|
// Fire-and-forget search indexing (non-blocking, void intentional)
|
||||||
void this.searchService.indexDocument({
|
void this.searchService.indexDocument({
|
||||||
id: savedCorr.id,
|
id: savedCorr.id,
|
||||||
uuid: savedCorr.uuid,
|
publicId: savedCorr.publicId,
|
||||||
type: 'correspondence',
|
type: 'correspondence',
|
||||||
docNumber: docNumber.number,
|
docNumber: docNumber.number,
|
||||||
title: createDto.subject,
|
title: createDto.subject,
|
||||||
@@ -459,9 +459,9 @@ export class CorrespondenceService {
|
|||||||
return correspondence;
|
return correspondence;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByUuid(publicId: string) {
|
||||||
const correspondence = await this.correspondenceRepo.findOne({
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
relations: [
|
relations: [
|
||||||
'revisions',
|
'revisions',
|
||||||
'revisions.status',
|
'revisions.status',
|
||||||
@@ -474,16 +474,18 @@ export class CorrespondenceService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!correspondence) {
|
if (!correspondence) {
|
||||||
throw new NotFoundException(`Correspondence with UUID ${uuid} not found`);
|
throw new NotFoundException(
|
||||||
|
`Correspondence with UUID ${publicId} not found`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return correspondence;
|
return correspondence;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addReference(id: number, dto: AddReferenceDto) {
|
async addReference(id: number, dto: AddReferenceDto) {
|
||||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||||
// ADR-019: Resolve target UUID → internal INT id
|
// ADR-019: Resolve target publicId → internal INT id
|
||||||
const target = await this.correspondenceRepo.findOne({
|
const target = await this.correspondenceRepo.findOne({
|
||||||
where: { uuid: dto.targetUuid },
|
where: { publicId: dto.targetUuid },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
@@ -814,7 +816,7 @@ export class CorrespondenceService {
|
|||||||
// Re-index updated document in Elasticsearch (fire-and-forget)
|
// Re-index updated document in Elasticsearch (fire-and-forget)
|
||||||
void this.searchService.indexDocument({
|
void this.searchService.indexDocument({
|
||||||
id: updated.id,
|
id: updated.id,
|
||||||
uuid: updated.uuid,
|
publicId: updated.publicId,
|
||||||
type: 'correspondence',
|
type: 'correspondence',
|
||||||
docNumber: updated.correspondenceNumber,
|
docNumber: updated.correspondenceNumber,
|
||||||
title: updateDto.subject ?? updated.revisions?.[0]?.subject,
|
title: updateDto.subject ?? updated.revisions?.[0]?.subject,
|
||||||
@@ -896,8 +898,8 @@ export class CorrespondenceService {
|
|||||||
* Business Rule Implementation: EC-CORR-001 - Cancel Correspondence with Downstream Circulation
|
* Business Rule Implementation: EC-CORR-001 - Cancel Correspondence with Downstream Circulation
|
||||||
* Cancel correspondence and handle related circulations
|
* Cancel correspondence and handle related circulations
|
||||||
*/
|
*/
|
||||||
async cancel(uuid: string, reason: string, user: User) {
|
async cancel(publicId: string, reason: string, user: User) {
|
||||||
const correspondence = await this.findOneByUuid(uuid);
|
const correspondence = await this.findOneByUuid(publicId);
|
||||||
|
|
||||||
// Check if user has permission to cancel (Org Admin or Superadmin only)
|
// Check if user has permission to cancel (Org Admin or Superadmin only)
|
||||||
const permissions = await this.userService.getUserPermissions(user.user_id);
|
const permissions = await this.userService.getUserPermissions(user.user_id);
|
||||||
@@ -983,7 +985,7 @@ export class CorrespondenceService {
|
|||||||
// Re-index cancelled status in Elasticsearch (fire-and-forget)
|
// Re-index cancelled status in Elasticsearch (fire-and-forget)
|
||||||
void this.searchService.indexDocument({
|
void this.searchService.indexDocument({
|
||||||
id: correspondence.id,
|
id: correspondence.id,
|
||||||
uuid: correspondence.uuid,
|
publicId: correspondence.publicId,
|
||||||
type: 'correspondence',
|
type: 'correspondence',
|
||||||
docNumber: correspondence.correspondenceNumber,
|
docNumber: correspondence.correspondenceNumber,
|
||||||
title: currentRevision.subject,
|
title: currentRevision.subject,
|
||||||
@@ -1005,7 +1007,7 @@ export class CorrespondenceService {
|
|||||||
type: 'EMAIL',
|
type: 'EMAIL',
|
||||||
entityType: 'correspondence',
|
entityType: 'correspondence',
|
||||||
entityId: correspondence.id,
|
entityId: correspondence.id,
|
||||||
link: `/correspondences/${correspondence.uuid}`,
|
link: `/correspondences/${correspondence.publicId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1031,19 +1033,19 @@ export class CorrespondenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bulkCancel(
|
async bulkCancel(
|
||||||
uuids: string[],
|
publicIds: string[],
|
||||||
reason: string,
|
reason: string,
|
||||||
user: User
|
user: User
|
||||||
): Promise<{ succeeded: string[]; failed: string[] }> {
|
): Promise<{ succeeded: string[]; failed: string[] }> {
|
||||||
const succeeded: string[] = [];
|
const succeeded: string[] = [];
|
||||||
const failed: string[] = [];
|
const failed: string[] = [];
|
||||||
|
|
||||||
for (const uuid of uuids) {
|
for (const publicId of publicIds) {
|
||||||
try {
|
try {
|
||||||
await this.cancel(uuid, reason, user);
|
await this.cancel(publicId, reason, user);
|
||||||
succeeded.push(uuid);
|
succeeded.push(publicId);
|
||||||
} catch {
|
} catch {
|
||||||
failed.push(uuid);
|
failed.push(publicId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class DueDateReminderService {
|
|||||||
type: 'EMAIL',
|
type: 'EMAIL',
|
||||||
entityType: 'correspondence',
|
entityType: 'correspondence',
|
||||||
entityId: corr.id,
|
entityId: corr.id,
|
||||||
link: `/correspondences/${corr.uuid}`,
|
link: `/correspondences/${corr.publicId}`,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
|
|||||||
@@ -89,10 +89,12 @@ export class OrganizationService {
|
|||||||
return org;
|
return org;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByUuid(publicId: string) {
|
||||||
const org = await this.orgRepo.findOne({ where: { uuid } });
|
const org = await this.orgRepo.findOne({ where: { publicId } });
|
||||||
if (!org)
|
if (!org)
|
||||||
throw new NotFoundException(`Organization UUID ${uuid} not found`);
|
throw new NotFoundException(
|
||||||
|
`Organization publicId ${publicId} not found`
|
||||||
|
);
|
||||||
return org;
|
return org;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,15 @@
|
|||||||
import {
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
Entity,
|
import { Exclude } from 'class-transformer';
|
||||||
Column,
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
OneToMany,
|
|
||||||
BeforeInsert,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
|
||||||
import { Contract } from '../../contract/entities/contract.entity';
|
import { Contract } from '../../contract/entities/contract.entity';
|
||||||
|
|
||||||
@Entity('projects')
|
@Entity('projects')
|
||||||
export class Project extends BaseEntity {
|
export class Project extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@Exclude()
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Expose({ name: 'id' })
|
// publicId inherited from UuidBaseEntity (DB column: uuid)
|
||||||
@Column({
|
|
||||||
type: 'uuid',
|
|
||||||
unique: true,
|
|
||||||
nullable: false,
|
|
||||||
comment: 'UUID Public Identifier (ADR-019)',
|
|
||||||
})
|
|
||||||
uuid!: string;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
generateUuid(): void {
|
|
||||||
if (!this.uuid) {
|
|
||||||
this.uuid = uuidv7();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Column({ name: 'project_code', unique: true, length: 50 })
|
@Column({ name: 'project_code', unique: true, length: 50 })
|
||||||
projectCode!: string;
|
projectCode!: string;
|
||||||
|
|||||||
@@ -91,21 +91,23 @@ export class ProjectService {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByUuid(publicId: string) {
|
||||||
const project = await this.projectRepository.findOne({
|
const project = await this.projectRepository.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
relations: ['contracts'],
|
relations: ['contracts'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundException(`Project UUID ${uuid} not found`);
|
throw new NotFoundException(
|
||||||
|
`Project with publicId ${publicId} not found`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(uuid: string, updateDto: UpdateProjectDto) {
|
async update(publicId: string, updateDto: UpdateProjectDto) {
|
||||||
const project = await this.findOneByUuid(uuid);
|
const project = await this.findOneByUuid(publicId);
|
||||||
|
|
||||||
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
|
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
|
||||||
this.projectRepository.merge(project, updateDto);
|
this.projectRepository.merge(project, updateDto);
|
||||||
@@ -113,14 +115,14 @@ export class ProjectService {
|
|||||||
return this.projectRepository.save(project);
|
return this.projectRepository.save(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(uuid: string) {
|
async remove(publicId: string) {
|
||||||
const project = await this.findOneByUuid(uuid);
|
const project = await this.findOneByUuid(publicId);
|
||||||
// ใช้ Soft Delete
|
// ใช้ Soft Delete
|
||||||
return this.projectRepository.softRemove(project);
|
return this.projectRepository.softRemove(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findContracts(uuid: string) {
|
async findContracts(publicId: string) {
|
||||||
const project = await this.findOneByUuid(uuid);
|
const project = await this.findOneByUuid(publicId);
|
||||||
return project.contracts;
|
return project.contracts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class RfaController {
|
|||||||
@ApiOperation({ summary: 'Submit RFA to Workflow' })
|
@ApiOperation({ summary: 'Submit RFA to Workflow' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'uuid',
|
name: 'uuid',
|
||||||
description: 'RFA UUID (from correspondences.uuid)',
|
description: 'RFA publicId (from correspondences.publicId)',
|
||||||
})
|
})
|
||||||
@ApiBody({ type: SubmitRfaDto })
|
@ApiBody({ type: SubmitRfaDto })
|
||||||
@ApiResponse({ status: 200, description: 'RFA submitted successfully' })
|
@ApiResponse({ status: 200, description: 'RFA submitted successfully' })
|
||||||
@@ -83,7 +83,7 @@ export class RfaController {
|
|||||||
@ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' })
|
@ApiOperation({ summary: 'Process Workflow Action (Approve/Reject)' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'uuid',
|
name: 'uuid',
|
||||||
description: 'RFA UUID (from correspondences.uuid)',
|
description: 'RFA publicId (from correspondences.publicId)',
|
||||||
})
|
})
|
||||||
@ApiBody({ type: WorkflowActionDto })
|
@ApiBody({ type: WorkflowActionDto })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@@ -120,7 +120,7 @@ export class RfaController {
|
|||||||
@ApiOperation({ summary: 'Get RFA details with revisions and items' })
|
@ApiOperation({ summary: 'Get RFA details with revisions and items' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'uuid',
|
name: 'uuid',
|
||||||
description: 'RFA UUID (from correspondences.uuid)',
|
description: 'RFA publicId (from correspondences.publicId)',
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, description: 'RFA details' })
|
@ApiResponse({ status: 200, description: 'RFA details' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
@@ -130,7 +130,7 @@ export class RfaController {
|
|||||||
|
|
||||||
@Put(':uuid')
|
@Put(':uuid')
|
||||||
@ApiOperation({ summary: 'Update Draft RFA fields (EC-RFA-002: DFT only)' })
|
@ApiOperation({ summary: 'Update Draft RFA fields (EC-RFA-002: DFT only)' })
|
||||||
@ApiParam({ name: 'uuid', description: 'RFA UUID' })
|
@ApiParam({ name: 'uuid', description: 'RFA publicId' })
|
||||||
@ApiBody({ type: UpdateRfaDto })
|
@ApiBody({ type: UpdateRfaDto })
|
||||||
@ApiResponse({ status: 200, description: 'RFA updated successfully' })
|
@ApiResponse({ status: 200, description: 'RFA updated successfully' })
|
||||||
@RequirePermission('rfa.create')
|
@RequirePermission('rfa.create')
|
||||||
@@ -146,7 +146,7 @@ export class RfaController {
|
|||||||
@Delete(':uuid')
|
@Delete(':uuid')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Cancel Draft RFA (sets status to CC)' })
|
@ApiOperation({ summary: 'Cancel Draft RFA (sets status to CC)' })
|
||||||
@ApiParam({ name: 'uuid', description: 'RFA UUID' })
|
@ApiParam({ name: 'uuid', description: 'RFA publicId' })
|
||||||
@ApiResponse({ status: 200, description: 'RFA cancelled successfully' })
|
@ApiResponse({ status: 200, description: 'RFA cancelled successfully' })
|
||||||
@RequirePermission('rfa.create')
|
@RequirePermission('rfa.create')
|
||||||
@Audit('rfa.cancel', 'rfa')
|
@Audit('rfa.cancel', 'rfa')
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type CorrRevWithRfa = CorrespondenceRevision & { rfaRevision?: RfaRevision };
|
|||||||
|
|
||||||
/** RFA entity + a flat `revisions` convenience array for the frontend */
|
/** RFA entity + a flat `revisions` convenience array for the frontend */
|
||||||
export interface RfaMapped extends Rfa {
|
export interface RfaMapped extends Rfa {
|
||||||
uuid?: string; // ADR-019: top-level UUID from correspondence
|
publicId?: string; // ADR-019: top-level publicId from correspondence
|
||||||
revisions: CorrRevWithRfa[];
|
revisions: CorrRevWithRfa[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ export class RfaService {
|
|||||||
this.searchService
|
this.searchService
|
||||||
.indexDocument({
|
.indexDocument({
|
||||||
id: savedCorr.id,
|
id: savedCorr.id,
|
||||||
uuid: savedCorr.uuid, // ADR-019: index UUID for search
|
publicId: savedCorr.publicId, // ADR-019: index publicId for search
|
||||||
type: 'rfa',
|
type: 'rfa',
|
||||||
docNumber: docNumber.number,
|
docNumber: docNumber.number,
|
||||||
title: createDto.subject,
|
title: createDto.subject,
|
||||||
@@ -536,7 +536,7 @@ export class RfaService {
|
|||||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
return {
|
return {
|
||||||
...rfa,
|
...rfa,
|
||||||
uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level
|
publicId: rfa.correspondence?.publicId, // ADR-019: expose publicId at top level
|
||||||
revisions: revisions.map((cr) => ({
|
revisions: revisions.map((cr) => ({
|
||||||
...cr,
|
...cr,
|
||||||
...(cr.rfaRevision ?? {}),
|
...(cr.rfaRevision ?? {}),
|
||||||
@@ -557,27 +557,27 @@ export class RfaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ADR-019: Find RFA by the parent Correspondence UUID (public identifier).
|
* ADR-019: Find RFA by the parent Correspondence publicId (public identifier).
|
||||||
* Resolves correspondence.uuid → internal rfa.id
|
* Resolves correspondence.publicId → internal rfa.id
|
||||||
*/
|
*/
|
||||||
async findOneByUuid(uuid: string) {
|
async findOneByUuid(publicId: string) {
|
||||||
const correspondence = await this.correspondenceRepo.findOne({
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
select: ['id'],
|
select: ['id'],
|
||||||
});
|
});
|
||||||
if (!correspondence) {
|
if (!correspondence) {
|
||||||
throw new NotFoundException(`RFA with UUID ${uuid} not found`);
|
throw new NotFoundException(`RFA with publicId ${publicId} not found`);
|
||||||
}
|
}
|
||||||
return this.findOne(correspondence.id);
|
return this.findOne(correspondence.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuidRaw(uuid: string) {
|
async findOneByUuidRaw(publicId: string) {
|
||||||
const correspondence = await this.correspondenceRepo.findOne({
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
select: ['id'],
|
select: ['id'],
|
||||||
});
|
});
|
||||||
if (!correspondence) {
|
if (!correspondence) {
|
||||||
throw new NotFoundException(`RFA with UUID ${uuid} not found`);
|
throw new NotFoundException(`RFA with publicId ${publicId} not found`);
|
||||||
}
|
}
|
||||||
return this.findOne(correspondence.id, true);
|
return this.findOne(correspondence.id, true);
|
||||||
}
|
}
|
||||||
@@ -618,7 +618,7 @@ export class RfaService {
|
|||||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
const mappedRfa: RfaMapped = {
|
const mappedRfa: RfaMapped = {
|
||||||
...rfa,
|
...rfa,
|
||||||
uuid: rfa.correspondence?.uuid, // ADR-019: expose UUID at top level
|
publicId: rfa.correspondence?.publicId, // ADR-019: expose publicId at top level
|
||||||
revisions: revisions.map((cr) => ({
|
revisions: revisions.map((cr) => ({
|
||||||
...cr,
|
...cr,
|
||||||
...(cr.rfaRevision ?? {}),
|
...(cr.rfaRevision ?? {}),
|
||||||
@@ -846,8 +846,8 @@ export class RfaService {
|
|||||||
* Update a Draft RFA's revision fields (subject, body, remarks, description, dueDate).
|
* Update a Draft RFA's revision fields (subject, body, remarks, description, dueDate).
|
||||||
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
||||||
*/
|
*/
|
||||||
async update(uuid: string, dto: UpdateRfaDto, _user: User) {
|
async update(publicId: string, dto: UpdateRfaDto, _user: User) {
|
||||||
const rfa = await this.findOneByUuidRaw(uuid);
|
const rfa = await this.findOneByUuidRaw(publicId);
|
||||||
const corrRevisions =
|
const corrRevisions =
|
||||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||||
@@ -880,15 +880,15 @@ export class RfaService {
|
|||||||
await this.rfaRevisionRepo.save(currentRfaRev);
|
await this.rfaRevisionRepo.save(currentRfaRev);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.findOneByUuid(uuid);
|
return this.findOneByUuid(publicId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel (soft-delete) a Draft RFA by setting its status to CC.
|
* Cancel (soft-delete) a Draft RFA by setting its status to CC.
|
||||||
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
* EC-RFA-002: Only allowed when current revision is in DFT status.
|
||||||
*/
|
*/
|
||||||
async cancel(uuid: string, user: User) {
|
async cancel(publicId: string, user: User) {
|
||||||
const rfa = await this.findOneByUuidRaw(uuid);
|
const rfa = await this.findOneByUuidRaw(publicId);
|
||||||
const corrRevisions =
|
const corrRevisions =
|
||||||
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
(rfa.correspondence?.revisions as CorrRevWithRfa[] | undefined) ?? [];
|
||||||
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
const currentCorrRev = corrRevisions.find((r) => r.isCurrent);
|
||||||
|
|||||||
@@ -73,12 +73,18 @@ export class SearchService implements OnModuleInit {
|
|||||||
* Index เอกสาร (Create/Update)
|
* Index เอกสาร (Create/Update)
|
||||||
*/
|
*/
|
||||||
async indexDocument(
|
async indexDocument(
|
||||||
doc: Record<string, unknown> & { type: string; id?: number; uuid?: string }
|
doc: Record<string, unknown> & {
|
||||||
|
type: string;
|
||||||
|
id?: number;
|
||||||
|
publicId?: string;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.esService.index({
|
return await this.esService.index({
|
||||||
index: this.indexName,
|
index: this.indexName,
|
||||||
id: doc.uuid ? `${doc.type}_${doc.uuid}` : `${doc.type}_${doc.id}`, // ADR-019: prefer UUID key
|
id: doc.publicId
|
||||||
|
? `${doc.type}_${doc.publicId}`
|
||||||
|
: `${doc.type}_${doc.id}`, // ADR-019: prefer publicId key
|
||||||
document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน
|
document: doc, // ✅ Library รุ่นใหม่ใช้ 'document' แทน 'body' ในบางเวอร์ชัน
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export class TransmittalController {
|
|||||||
@Get(':uuid')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Transmittal details' })
|
@ApiOperation({ summary: 'Get Transmittal details' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'uuid',
|
name: 'publicId',
|
||||||
description: 'Transmittal UUID (from correspondences.uuid)',
|
description: 'Transmittal publicId (from correspondences.publicId)',
|
||||||
})
|
})
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
|
|||||||
@@ -159,16 +159,18 @@ export class TransmittalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ADR-019: Find Transmittal by parent Correspondence UUID (public identifier).
|
* ADR-019: Find Transmittal by parent Correspondence publicId (public identifier).
|
||||||
* Resolves correspondence.uuid → internal correspondenceId (INT)
|
* Resolves correspondence.publicId → internal correspondenceId (INT)
|
||||||
*/
|
*/
|
||||||
async findOneByUuid(uuid: string): Promise<Transmittal> {
|
async findOneByUuid(publicId: string): Promise<Transmittal> {
|
||||||
const correspondence = await this.dataSource.manager.findOne(
|
const correspondence = await this.dataSource.manager.findOne(
|
||||||
Correspondence,
|
Correspondence,
|
||||||
{ where: { uuid }, select: ['id'] }
|
{ where: { publicId }, select: ['id'] }
|
||||||
);
|
);
|
||||||
if (!correspondence) {
|
if (!correspondence) {
|
||||||
throw new NotFoundException(`Transmittal with UUID ${uuid} not found`);
|
throw new NotFoundException(
|
||||||
|
`Transmittal with publicId ${publicId} not found`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return this.findOne(correspondence.id);
|
return this.findOne(correspondence.id);
|
||||||
}
|
}
|
||||||
@@ -218,10 +220,10 @@ export class TransmittalService {
|
|||||||
.take(limit)
|
.take(limit)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
// ADR-019: Map correspondence.uuid to top level for frontend convenience
|
// ADR-019: Map correspondence.publicId to top level for frontend convenience
|
||||||
const mappedItems = items.map((t) => ({
|
const mappedItems = items.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
uuid: t.correspondence?.uuid,
|
publicId: t.correspondence?.publicId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class UserService {
|
|||||||
.leftJoinAndSelect('assignments.role', 'role')
|
.leftJoinAndSelect('assignments.role', 'role')
|
||||||
.select([
|
.select([
|
||||||
'user.user_id',
|
'user.user_id',
|
||||||
'user.uuid',
|
'user.publicId',
|
||||||
'user.username',
|
'user.username',
|
||||||
'user.email',
|
'user.email',
|
||||||
'user.firstName',
|
'user.firstName',
|
||||||
@@ -156,9 +156,9 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByUuid(uuid: string): Promise<User> {
|
async findOneByUuid(publicId: string): Promise<User> {
|
||||||
const user = await this.usersRepository.findOne({
|
const user = await this.usersRepository.findOne({
|
||||||
where: { uuid },
|
where: { publicId },
|
||||||
relations: [
|
relations: [
|
||||||
'preference',
|
'preference',
|
||||||
'assignments',
|
'assignments',
|
||||||
@@ -168,7 +168,7 @@ export class UserService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
throw new NotFoundException(`User with publicId ${publicId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function TagsPage() {
|
|||||||
accessorKey: 'tag_name',
|
accessorKey: 'tag_name',
|
||||||
header: 'Tag Name',
|
header: 'Tag Name',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const color = String(row.original.color_code || 'default');
|
const color = String(row.original.colorCode || 'default');
|
||||||
const isHex = color.startsWith('#');
|
const isHex = color.startsWith('#');
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -45,7 +45,7 @@ export default function TagsPage() {
|
|||||||
className="w-3 h-3 rounded-full border border-border"
|
className="w-3 h-3 rounded-full border border-border"
|
||||||
style={{ backgroundColor: isHex ? color : color === 'default' ? '#e2e8f0' : color }}
|
style={{ backgroundColor: isHex ? color : color === 'default' ? '#e2e8f0' : color }}
|
||||||
/>
|
/>
|
||||||
{String(row.original.tag_name)}
|
{String(row.original.tagName)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -97,13 +97,13 @@ export default function TagsPage() {
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag_name',
|
name: 'tagName',
|
||||||
label: 'Tag Name',
|
label: 'Tag Name',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'color_code',
|
name: 'colorCode',
|
||||||
label: 'Color Code (Hex or Name)',
|
label: 'Color Code (Hex or Name)',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ export default function WorkflowEditPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const dto: CreateWorkflowDefinitionDto = {
|
const dto: CreateWorkflowDefinitionDto = {
|
||||||
workflow_code: workflowData.workflowType || 'CORRESPONDENCE',
|
workflowCode: workflowData.workflowType || 'CORRESPONDENCE',
|
||||||
dsl: {
|
dsl: {
|
||||||
workflowName: workflowData.workflowName,
|
workflowName: workflowData.workflowName,
|
||||||
description: workflowData.description,
|
description: workflowData.description,
|
||||||
dslDefinition: workflowData.dslDefinition,
|
dslDefinition: workflowData.dslDefinition,
|
||||||
},
|
},
|
||||||
is_active: workflowData.isActive,
|
isActive: workflowData.isActive,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|||||||
@@ -124,13 +124,13 @@ export default function CirculationDetailPage() {
|
|||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Organization</p>
|
<p className="text-sm text-muted-foreground">Organization</p>
|
||||||
<p className="font-medium">{circulation.organization?.organization_name || '-'}</p>
|
<p className="font-medium">{circulation.organization?.organizationName || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Created By</p>
|
<p className="text-sm text-muted-foreground">Created By</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{circulation.creator
|
{circulation.creator
|
||||||
? `${circulation.creator.first_name || ''} ${circulation.creator.last_name || ''}`.trim() ||
|
? `${circulation.creator.firstName || ''} ${circulation.creator.lastName || ''}`.trim() ||
|
||||||
circulation.creator.username
|
circulation.creator.username
|
||||||
: '-'}
|
: '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -146,7 +146,7 @@ export default function CirculationDetailPage() {
|
|||||||
href={`/correspondences/${circulation.correspondence.uuid}`}
|
href={`/correspondences/${circulation.correspondence.uuid}`}
|
||||||
className="font-medium text-primary hover:underline"
|
className="font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{circulation.correspondence.correspondence_number}
|
{circulation.correspondence.correspondenceNumber}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -166,13 +166,13 @@ export default function CirculationDetailPage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{getInitials(routing.assignee?.first_name, routing.assignee?.last_name)}
|
{getInitials(routing.assignee?.firstName, routing.assignee?.lastName)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{routing.assignee
|
{routing.assignee
|
||||||
? `${routing.assignee.first_name || ''} ${routing.assignee.last_name || ''}`.trim() ||
|
? `${routing.assignee.firstName || ''} ${routing.assignee.lastName || ''}`.trim() ||
|
||||||
routing.assignee.username
|
routing.assignee.username
|
||||||
: 'Unassigned'}
|
: 'Unassigned'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function CirculationList({ data }: CirculationListProps) {
|
|||||||
header: 'Organization',
|
header: 'Organization',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const org = row.original.organization;
|
const org = row.original.organization;
|
||||||
return org?.organization_name || '-';
|
return org?.organizationName || '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function RoutingStep({ routing }: { routing: CirculationRouting }) {
|
|||||||
const meta = ROUTING_STATUS_META[routing.status] ?? ROUTING_STATUS_META.PENDING;
|
const meta = ROUTING_STATUS_META[routing.status] ?? ROUTING_STATUS_META.PENDING;
|
||||||
const Icon = meta.icon;
|
const Icon = meta.icon;
|
||||||
const assigneeName = routing.assignee
|
const assigneeName = routing.assignee
|
||||||
? `${routing.assignee.first_name ?? ''} ${routing.assignee.last_name ?? ''}`.trim() ||
|
? `${routing.assignee.firstName ?? ''} ${routing.assignee.lastName ?? ''}`.trim() ||
|
||||||
routing.assignee.username
|
routing.assignee.username
|
||||||
: '—';
|
: '—';
|
||||||
|
|
||||||
|
|||||||
@@ -74,16 +74,16 @@ export function TagManager({ uuid, canEdit }: TagManagerProps) {
|
|||||||
key={tag.id}
|
key={tag.id}
|
||||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border"
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${getTagColor(tag.color_code)}22`,
|
backgroundColor: `${getTagColor(tag.colorCode)}22`,
|
||||||
borderColor: `${getTagColor(tag.color_code)}66`,
|
borderColor: `${getTagColor(tag.colorCode)}66`,
|
||||||
color: getTagColor(tag.color_code) === '#e2e8f0' ? 'inherit' : getTagColor(tag.color_code),
|
color: getTagColor(tag.colorCode) === '#e2e8f0' ? 'inherit' : getTagColor(tag.colorCode),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||||
style={{ backgroundColor: getTagColor(tag.color_code) }}
|
style={{ backgroundColor: getTagColor(tag.colorCode) }}
|
||||||
/>
|
/>
|
||||||
{tag.tag_name}
|
{tag.tagName}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemove(tag.id)}
|
onClick={() => handleRemove(tag.id)}
|
||||||
@@ -117,9 +117,9 @@ export function TagManager({ uuid, canEdit }: TagManagerProps) {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="w-2 h-2 rounded-full shrink-0"
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
style={{ backgroundColor: getTagColor(tag.color_code) }}
|
style={{ backgroundColor: getTagColor(tag.colorCode) }}
|
||||||
/>
|
/>
|
||||||
{tag.tag_name}
|
{tag.tagName}
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export interface CirculationRouting {
|
|||||||
* Main Circulation entity
|
* Main Circulation entity
|
||||||
*/
|
*/
|
||||||
export interface Circulation {
|
export interface Circulation {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceId?: number;
|
correspondenceId?: number;
|
||||||
organizationId: number;
|
organizationId: number;
|
||||||
@@ -53,18 +53,18 @@ export interface Circulation {
|
|||||||
// Joined relations from API
|
// Joined relations from API
|
||||||
routings?: CirculationRouting[];
|
routings?: CirculationRouting[];
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
};
|
};
|
||||||
organization?: {
|
organization?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
};
|
};
|
||||||
creator?: {
|
creator?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
username: string;
|
username: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export interface Organization {
|
export interface Organization {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -17,7 +17,7 @@ export interface Attachment {
|
|||||||
|
|
||||||
// Used in List View mainly
|
// Used in List View mainly
|
||||||
export interface CorrespondenceRevision {
|
export interface CorrespondenceRevision {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
revisionNumber: number;
|
revisionNumber: number;
|
||||||
revisionLabel?: string; // e.g. "A", "00"
|
revisionLabel?: string; // e.g. "A", "00"
|
||||||
@@ -42,21 +42,21 @@ export interface CorrespondenceRevision {
|
|||||||
|
|
||||||
// Nested Relation from Backend Refactor
|
// Nested Relation from Backend Refactor
|
||||||
correspondence: {
|
correspondence: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
isInternal: boolean;
|
isInternal: boolean;
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { uuid: string; id?: number; projectName: string; projectCode: string };
|
project?: { publicId: string; id?: number; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
||||||
export interface Correspondence {
|
export interface Correspondence {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -67,7 +67,7 @@ export interface Correspondence {
|
|||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { uuid: string; id?: number; projectName: string; projectCode: string };
|
project?: { publicId: string; id?: number; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
revisions?: CorrespondenceRevision[]; // Nested revisions
|
revisions?: CorrespondenceRevision[]; // Nested revisions
|
||||||
recipients?: {
|
recipients?: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Entity Interfaces
|
// Entity Interfaces
|
||||||
export interface DrawingRevision {
|
export interface DrawingRevision {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
revisionId?: number; // Excluded from API responses (ADR-019)
|
revisionId?: number; // Excluded from API responses (ADR-019)
|
||||||
revisionNumber: string;
|
revisionNumber: string;
|
||||||
title?: string; // Added
|
title?: string; // Added
|
||||||
@@ -15,7 +15,7 @@ export interface DrawingRevision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractDrawing {
|
export interface ContractDrawing {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
contractDrawingNo: string;
|
contractDrawingNo: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -28,7 +28,7 @@ export interface ContractDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShopDrawing {
|
export interface ShopDrawing {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -41,7 +41,7 @@ export interface ShopDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AsBuiltDrawing {
|
export interface AsBuiltDrawing {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -54,7 +54,7 @@ export interface AsBuiltDrawing {
|
|||||||
|
|
||||||
// Unified Type for List
|
// Unified Type for List
|
||||||
export interface Drawing {
|
export interface Drawing {
|
||||||
uuid?: string;
|
publicId?: string; // ADR-019: exposed as 'id' in API responses
|
||||||
drawingId?: number; // Excluded from API responses (ADR-019)
|
drawingId?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
title: string; // Display title (from current revision for Shop/AsBuilt)
|
title: string; // Display title (from current revision for Shop/AsBuilt)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Notification {
|
export interface Notification {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
notificationId?: number; // Excluded from API responses (ADR-019)
|
notificationId?: number; // Excluded from API responses (ADR-019)
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Organization {
|
export interface Organization {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
|
|||||||
@@ -2,33 +2,33 @@ export interface RFAItem {
|
|||||||
id?: number;
|
id?: number;
|
||||||
itemType: 'SHOP' | 'AS_BUILT';
|
itemType: 'SHOP' | 'AS_BUILT';
|
||||||
shopDrawingRevision?: {
|
shopDrawingRevision?: {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
revisionLabel?: string;
|
revisionLabel?: string;
|
||||||
revisionNumber?: number;
|
revisionNumber?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
legacyDrawingNumber?: string;
|
legacyDrawingNumber?: string;
|
||||||
attachments?: { id?: number; url?: string; name?: string }[];
|
attachments?: { id?: number; url?: string; name?: string }[];
|
||||||
shopDrawing?: {
|
shopDrawing?: {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
drawingNumber?: string;
|
drawingNumber?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
asBuiltDrawingRevision?: {
|
asBuiltDrawingRevision?: {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
revisionLabel?: string;
|
revisionLabel?: string;
|
||||||
revisionNumber?: number;
|
revisionNumber?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
legacyDrawingNumber?: string;
|
legacyDrawingNumber?: string;
|
||||||
attachments?: { id?: number; url?: string; name?: string }[];
|
attachments?: { id?: number; url?: string; name?: string }[];
|
||||||
asBuiltDrawing?: {
|
asBuiltDrawing?: {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
drawingNumber?: string;
|
drawingNumber?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RFA {
|
export interface RFA {
|
||||||
uuid: string; // ADR-019: from correspondence.uuid
|
publicId: string; // ADR-019: from correspondence.publicId
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
rfaTypeId: number;
|
rfaTypeId: number;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
@@ -56,14 +56,14 @@ export interface RFA {
|
|||||||
};
|
};
|
||||||
// Shared Correspondence Relation
|
// Shared Correspondence Relation
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
project?: {
|
project?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
projectCode: string;
|
projectCode: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
type: 'correspondence' | 'rfa' | 'drawing';
|
type: 'correspondence' | 'rfa' | 'drawing';
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface TransmittalItem {
|
|||||||
* Main Transmittal entity
|
* Main Transmittal entity
|
||||||
*/
|
*/
|
||||||
export interface Transmittal {
|
export interface Transmittal {
|
||||||
uuid: string; // ADR-019: from correspondence.uuid
|
publicId: string; // ADR-019: from correspondence.publicId
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceId?: number | string;
|
correspondenceId?: number | string;
|
||||||
transmittalNo: string;
|
transmittalNo: string;
|
||||||
@@ -39,7 +39,7 @@ export interface Transmittal {
|
|||||||
// Joined relations from API
|
// Joined relations from API
|
||||||
items?: TransmittalItem[];
|
items?: TransmittalItem[];
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
uuid: string;
|
publicId: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface UserOrganization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
uuid: string;
|
publicId: string; // ADR-019: exposed as 'id' in API responses
|
||||||
userId?: number; // Excluded from API responses (ADR-019)
|
userId?: number; // Excluded from API responses (ADR-019)
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -179,19 +179,30 @@ UNIQUE INDEX idx_{table}_uuid (uuid)
|
|||||||
import { Column, BeforeInsert } from 'typeorm';
|
import { Column, BeforeInsert } from 'typeorm';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
export abstract class BaseUuidEntity {
|
/**
|
||||||
|
* Abstract base entity providing a UUID public identifier.
|
||||||
|
*
|
||||||
|
* Naming Convention:
|
||||||
|
* - TypeScript Property: `publicId` — semantic name indicating this is the public-facing identifier
|
||||||
|
* - Database Column: `uuid` — MariaDB native UUID type (stored as BINARY(16))
|
||||||
|
*
|
||||||
|
* This avoids confusion between the property name and the DB data type,
|
||||||
|
* while clearly indicating this is the ID exposed via API (not internal INT PK).
|
||||||
|
*/
|
||||||
|
export abstract class UuidBaseEntity {
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
|
name: 'uuid', // DB column name (MariaDB native UUID type)
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
comment: 'UUID Public Identifier (ADR-019)',
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
})
|
})
|
||||||
uuid!: string;
|
publicId!: string; // TypeScript property name — semantic, avoids type confusion
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
generateUuid(): void {
|
generateUuid(): void {
|
||||||
if (!this.uuid) {
|
if (!this.publicId) {
|
||||||
this.uuid = uuidv7();
|
this.publicId = uuidv7();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +220,10 @@ import { BaseUuidEntity } from '../../common/entities/base-uuid.entity';
|
|||||||
@Entity('correspondences')
|
@Entity('correspondences')
|
||||||
export class Correspondence extends BaseUuidEntity {
|
export class Correspondence extends BaseUuidEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: number;
|
id!: number; // Internal INT PK — never exposed via API
|
||||||
|
|
||||||
|
// publicId (from BaseUuidEntity) is the UUID exposed via API
|
||||||
|
// DB Column: uuid (MariaDB native UUID type)
|
||||||
|
|
||||||
// ... existing columns
|
// ... existing columns
|
||||||
}
|
}
|
||||||
@@ -243,9 +257,9 @@ async findOne(@Param('uuid', ParseUUIDPipe) uuid: string) {
|
|||||||
### Service Pattern
|
### Service Pattern
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async findByUuid(uuid: string): Promise<CorrespondenceDto> {
|
async findByUuid(publicId: string): Promise<CorrespondenceDto> {
|
||||||
const entity = await this.repository.findOne({
|
const entity = await this.repository.findOne({
|
||||||
where: { uuid },
|
where: { publicId }, // Use publicId property (DB column is 'uuid')
|
||||||
});
|
});
|
||||||
if (!entity) throw new NotFoundException();
|
if (!entity) throw new NotFoundException();
|
||||||
return this.toDto(entity);
|
return this.toDto(entity);
|
||||||
@@ -256,17 +270,17 @@ async findByUuid(uuid: string): Promise<CorrespondenceDto> {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CorrespondenceResponseDto {
|
export class CorrespondenceResponseDto {
|
||||||
// ✅ Expose UUID as 'id' in API response
|
// ✅ Expose publicId as 'id' in API response
|
||||||
@Expose({ name: 'id' })
|
@Expose({ name: 'id' })
|
||||||
uuid!: string;
|
publicId!: string;
|
||||||
|
|
||||||
// ❌ Never expose internal INT id
|
// ❌ Never expose internal INT id
|
||||||
// id: number; — REMOVED from response
|
// id: number; — REMOVED from response
|
||||||
|
|
||||||
// ... other fields
|
// ... other fields
|
||||||
// For FK references, also use UUID
|
// For FK references, also use publicId
|
||||||
@Expose({ name: 'project_id' })
|
@Expose({ name: 'project_id' })
|
||||||
projectUuid!: string;
|
projectPublicId!: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -454,10 +468,10 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
|||||||
|
|
||||||
### Phase 2: Backend (Dual-Mode)
|
### Phase 2: Backend (Dual-Mode)
|
||||||
|
|
||||||
- เพิ่ม `uuid` field ใน TypeORM Entities
|
- เพิ่ม `publicId` field ใน TypeORM Entities (DB column ยังชื่อ `uuid`)
|
||||||
- สร้าง `BaseUuidEntity` class
|
- สร้าง `BaseUuidEntity` class ด้วย `publicId` property
|
||||||
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
||||||
- API Response รวม UUID เป็น `id` field
|
- API Response รวม UUID เป็น `id` field (via @Expose)
|
||||||
|
|
||||||
### Phase 3: Frontend (Gradual Migration)
|
### Phase 3: Frontend (Gradual Migration)
|
||||||
|
|
||||||
@@ -486,4 +500,4 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md_
|
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน `05-07-hybrid-uuid-implementation-plan.md`_
|
||||||
|
|||||||
Reference in New Issue
Block a user