Enhance branch management with foreign key relation handling

- Updated `BranchesModule` to include foreign key relations in list and write responses, ensuring they are represented as nested objects using `pickRelation`.
- Introduced new `relation-response.mdc` file to define guidelines for embedding foreign key relations.
- Modified `BranchesRepository` to support fetching related `division`, `createdByUser`, and `updatedByUser` data.
- Updated DTOs and service methods to reflect changes in response structure, removing direct foreign key IDs.
- Added unit tests to validate the new relation handling in branches service and repository.
- Enhanced e2e tests to verify the correct structure of branch responses with nested relations.
This commit is contained in:
shancheas
2026-08-26 13:44:13 +07:00
parent c9f9b31abf
commit f635ebeda0
13 changed files with 392 additions and 68 deletions
@@ -26,6 +26,7 @@ describe('BranchesRepository', () => {
const del = jest.fn();
const transaction = jest.fn();
const $dynamic = jest.fn();
const leftJoin = jest.fn();
const db = {
select,
@@ -56,6 +57,13 @@ describe('BranchesRepository', () => {
updatedBy: 'user-1',
};
const joinedRow = {
branch: row,
division: { id: 'div-1', code: 'JKT', name: 'Jakarta' },
createdByUser: { id: 'user-1', username: 'admin' },
updatedByUser: { id: 'user-1', username: 'admin' },
};
const createInput = {
code: 'JKT_01',
name: 'Jakarta Pusat',
@@ -68,16 +76,34 @@ describe('BranchesRepository', () => {
userId: 'user-1',
};
beforeEach(async () => {
jest.clearAllMocks();
where.mockImplementation(() => ({ limit, orderBy }));
orderBy.mockImplementation(() => ({ limit }));
limit.mockImplementation(() => ({ offset }));
offset.mockResolvedValue([row]);
from.mockImplementation(() => ({
const joinChain = () => {
const chain: {
leftJoin: jest.Mock;
where: typeof where;
$dynamic: typeof $dynamic;
} = {
leftJoin: jest.fn(),
where,
$dynamic,
};
chain.leftJoin.mockReturnValue(chain);
return chain;
};
beforeEach(async () => {
jest.clearAllMocks();
where.mockImplementation(() => ({ limit, orderBy, returning }));
orderBy.mockImplementation(() => ({ limit }));
limit.mockImplementation(() => ({
offset,
then: (
resolve: (value: (typeof joinedRow)[]) => unknown,
reject?: (reason: unknown) => unknown,
) => Promise.resolve([joinedRow]).then(resolve, reject),
}));
offset.mockResolvedValue([joinedRow]);
from.mockImplementation(() => joinChain());
leftJoin.mockImplementation(() => joinChain());
$dynamic.mockReturnValue({ where });
select.mockImplementation(() => ({ from }));
values.mockReturnValue({ returning });
@@ -86,7 +112,6 @@ describe('BranchesRepository', () => {
update.mockReturnValue({ set });
del.mockReturnValue({ where });
returning.mockResolvedValue([row]);
where.mockImplementation(() => ({ limit, orderBy, returning }));
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [BranchesRepository, { provide: DRIZZLE, useValue: db }],
@@ -95,13 +120,15 @@ describe('BranchesRepository', () => {
});
it('findById maps a row to domain Branch', async () => {
limit.mockResolvedValueOnce([row]);
const branch = await repository.findById('br-1');
expect(branch).toMatchObject({
id: 'br-1',
code: 'JKT_01',
name: 'Jakarta Pusat',
createdBy: 'user-1',
division: { id: 'div-1', code: 'JKT', name: 'Jakarta' },
createdByUser: { id: 'user-1', username: 'admin' },
updatedByUser: { id: 'user-1', username: 'admin' },
});
expect(branch?.phone.value).toBe('+6281234567890');
expect(branch?.status.value).toBe('draft');
@@ -109,12 +136,11 @@ describe('BranchesRepository', () => {
});
it('findById returns null when missing', async () => {
limit.mockResolvedValueOnce([]);
limit.mockImplementationOnce(() => Promise.resolve([]));
await expect(repository.findById('missing')).resolves.toBeNull();
});
it('findByCode maps a row', async () => {
limit.mockResolvedValueOnce([row]);
const branch = await repository.findByCode('JKT_01');
expect(branch?.code).toBe('JKT_01');
});
@@ -127,17 +153,10 @@ describe('BranchesRepository', () => {
}),
}))
.mockImplementationOnce(() => ({
from: () => ({
$dynamic: () => ({
where: () => ({
orderBy: () => ({
limit: () => ({
offset: () => Promise.resolve([row]),
}),
}),
}),
}),
}),
from: () => {
const chain = joinChain();
return chain;
},
}));
const result = await repository.list({
@@ -158,12 +177,18 @@ describe('BranchesRepository', () => {
});
expect(result.total).toBe(1);
expect(result.data[0].code).toBe('JKT_01');
expect(result.data[0].division).toEqual({
id: 'div-1',
code: 'JKT',
name: 'Jakarta',
});
});
it('create inserts and maps unique violations', async () => {
returning.mockResolvedValueOnce([row]);
const created = await repository.create(createInput);
expect(created.code).toBe('JKT_01');
expect(created.createdByUser).toEqual({ id: 'user-1', username: 'admin' });
returning.mockRejectedValueOnce({ code: '23505' });
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
@@ -209,17 +234,17 @@ describe('BranchesRepository', () => {
'user-1',
);
expect(updated.id).toBe('br-1');
expect(updated.updatedByUser).toEqual({ id: 'user-1', username: 'admin' });
});
it('update throws when missing', async () => {
limit.mockResolvedValueOnce([]);
limit.mockImplementationOnce(() => Promise.resolve([]));
await expect(
repository.update('missing', { userId: 'user-1' }),
).rejects.toBeInstanceOf(NotFoundException);
});
it('update maps a row when present', async () => {
limit.mockResolvedValueOnce([row]);
returning.mockResolvedValueOnce([row]);
const updated = await repository.update('br-1', {
name: 'Jakarta Selatan',