mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(core): add unit tests for SchemaRouter
This commit is contained in:
parent
bb22ce3acc
commit
8e931d757f
3 changed files with 199 additions and 16 deletions
|
@ -13,12 +13,14 @@ export default function organizationRoutes<T extends AuthedRouter>(
|
|||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(Organizations, {
|
||||
findAll: async ({ limit, offset }) =>
|
||||
get: async ({ limit, offset }) =>
|
||||
Promise.all([organizations.findTotalNumber(), organizations.findAll(limit, offset)]),
|
||||
findById: organizations.findById,
|
||||
insert: organizations.insert,
|
||||
updateGuard: Organizations.guard.omit({ id: true, createdAt: true }).partial(),
|
||||
updateById: organizations.updateById,
|
||||
getById: organizations.findById,
|
||||
post: organizations.insert,
|
||||
patchById: {
|
||||
guard: Organizations.guard.omit({ id: true, createdAt: true }).partial(),
|
||||
run: organizations.updateById,
|
||||
},
|
||||
deleteById: organizations.deleteById,
|
||||
});
|
||||
|
||||
|
|
121
packages/core/src/utils/SchemaRouter.test.ts
Normal file
121
packages/core/src/utils/SchemaRouter.test.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { type GeneratedSchema } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import SchemaRouter, { type SchemaActions } from './SchemaRouter.js';
|
||||
import { createRequester } from './test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
type CreateSchema = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type Schema = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
describe('SchemaRouter', () => {
|
||||
const schema: GeneratedSchema<CreateSchema, Schema> = {
|
||||
table: 'test_table',
|
||||
tableSingular: 'test_table',
|
||||
fields: {
|
||||
id: 'id',
|
||||
},
|
||||
fieldKeys: ['id'],
|
||||
createGuard: z.object({ id: z.string().optional() }),
|
||||
guard: z.object({ id: z.string() }),
|
||||
};
|
||||
const entities = [{ id: 'test' }, { id: 'test2' }] as const satisfies readonly Schema[];
|
||||
const actions: SchemaActions<CreateSchema, Schema, CreateSchema> = {
|
||||
get: jest.fn().mockResolvedValue([entities.length, entities]),
|
||||
getById: jest.fn(async (id) => {
|
||||
const entity = entities.find((entity) => entity.id === id);
|
||||
if (!entity) {
|
||||
throw new RequestError({ code: 'entity.not_found', status: 404 });
|
||||
}
|
||||
return entity;
|
||||
}),
|
||||
post: jest.fn(async () => ({ id: 'test_new' })),
|
||||
patchById: {
|
||||
guard: z.object({ id: z.string().optional() }),
|
||||
run: jest.fn(async (id, data) => ({ id, ...data })),
|
||||
},
|
||||
deleteById: jest.fn(),
|
||||
};
|
||||
const schemaRouter = new SchemaRouter(schema, actions);
|
||||
const request = createRequester({ authedRoutes: (router) => router.use(schemaRouter.routes()) });
|
||||
const baseRoute = `/${schema.table.replaceAll('_', '-')}`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should be able to get all entities', async () => {
|
||||
const response = await request.get(baseRoute);
|
||||
|
||||
expect(actions.get).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
|
||||
expect(response.body).toStrictEqual(entities);
|
||||
});
|
||||
|
||||
it('should be able to get all entities with pagination', async () => {
|
||||
const response = await request.get(`${baseRoute}?page=1&page_size=10`);
|
||||
|
||||
expect(actions.get).toHaveBeenCalledWith(expect.objectContaining({ offset: 0, limit: 10 }));
|
||||
expect(response.body).toStrictEqual(entities);
|
||||
expect(response.header).toHaveProperty('total-number', '2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('post', () => {
|
||||
it('should be able to create an entity', async () => {
|
||||
const response = await request.post(baseRoute).send({});
|
||||
|
||||
expect(actions.post).toHaveBeenCalledWith({});
|
||||
expect(response.body).toStrictEqual({ id: 'test_new' });
|
||||
});
|
||||
|
||||
it('should throw with invalid input body', async () => {
|
||||
const response = await request.post(baseRoute).send({ id: 1 });
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should be able to get an entity by id', async () => {
|
||||
const response = await request.get(`${baseRoute}/test`);
|
||||
|
||||
expect(actions.getById).toHaveBeenCalledWith('test');
|
||||
expect(response.body).toStrictEqual(entities[0]);
|
||||
});
|
||||
|
||||
// This test case is actually nice-to-have. It's not required for the router to work.
|
||||
it('should throw with invalid id', async () => {
|
||||
const response = await request.get(`${baseRoute}/2`);
|
||||
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchById', () => {
|
||||
it('should be able to patch an entity by id', async () => {
|
||||
const response = await request.patch(`${baseRoute}/test`).send({ id: 'test_new' });
|
||||
|
||||
expect(actions.patchById.run).toHaveBeenCalledWith('test', { id: 'test_new' });
|
||||
expect(response.body).toStrictEqual({ id: 'test_new' });
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteById', () => {
|
||||
it('should be able to delete an entity by id', async () => {
|
||||
const response = await request.delete(`${baseRoute}/test`);
|
||||
|
||||
expect(actions.deleteById).toHaveBeenCalledWith('test');
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,19 +5,79 @@ import { z } from 'zod';
|
|||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination, { type Pagination } from '#src/middleware/koa-pagination.js';
|
||||
|
||||
type SchemaActions<
|
||||
/**
|
||||
* Actions configuration for a {@link SchemaRouter}.
|
||||
*/
|
||||
export type SchemaActions<
|
||||
CreateSchema extends SchemaLike,
|
||||
Schema extends CreateSchema,
|
||||
UpdateSchema extends Partial<Schema>,
|
||||
> = {
|
||||
findAll: (pagination: Pagination) => Promise<[count: number, entities: readonly Schema[]]>;
|
||||
findById: (id: string) => Promise<Readonly<Schema>>;
|
||||
insert: (data: CreateSchema) => Promise<Readonly<Schema>>;
|
||||
updateGuard: z.ZodType<UpdateSchema>;
|
||||
updateById: (id: string, data: UpdateSchema) => Promise<Readonly<Schema>>;
|
||||
/**
|
||||
* The function for `GET /` route to get a list of entities.
|
||||
*
|
||||
* @param pagination The request pagination info parsed from `koa-pagination`. If
|
||||
* pagination is disabled, the function should return all entities.
|
||||
* @returns A tuple of `[count, entities]`. `count` is the total count of entities
|
||||
* in the database; `entities` is the list of entities to be returned.
|
||||
*/
|
||||
get: (pagination: Pagination) => Promise<[count: number, entities: readonly Schema[]]>;
|
||||
/**
|
||||
* The function for `GET /:id` route to get an entity by ID.
|
||||
*
|
||||
* @param id The ID of the entity to be fetched.
|
||||
* @returns The entity to be returned.
|
||||
*/
|
||||
getById: (id: string) => Promise<Readonly<Schema>>;
|
||||
/**
|
||||
* The function for `POST /` route to create an entity.
|
||||
*
|
||||
* @param data The data of the entity to be created.
|
||||
* @returns The created entity.
|
||||
*/
|
||||
post: (data: CreateSchema) => Promise<Readonly<Schema>>;
|
||||
/**
|
||||
* The configuration for `PATCH /:id` route to update an entity by ID.
|
||||
* It contains a `guard` for the request body and a `run` function to update the entity.
|
||||
*/
|
||||
patchById: {
|
||||
/**
|
||||
* The guard for the request body. You can specify a partial schema and disable
|
||||
* some fields for business logic reasons.
|
||||
*/
|
||||
guard: z.ZodType<UpdateSchema>;
|
||||
/**
|
||||
* The function to update the entity by ID.
|
||||
*
|
||||
* @param id The ID of the entity to be updated.
|
||||
* @param data The data of the entity to be updated.
|
||||
* @returns The updated entity.
|
||||
*/
|
||||
run: (id: string, data: UpdateSchema) => Promise<Readonly<Schema>>;
|
||||
};
|
||||
/**
|
||||
* The function for `DELETE /:id` route to delete an entity by ID.
|
||||
*
|
||||
* @param id The ID of the entity to be deleted.
|
||||
*/
|
||||
deleteById: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A standard RESTful router for a schema.
|
||||
*
|
||||
* It provides the following routes by configuring the `actions`:
|
||||
*
|
||||
* - `GET /`: Get a list of entities.
|
||||
* - `POST /`: Create an entity.
|
||||
* - `GET /:id`: Get an entity by ID.
|
||||
* - `PATCH /:id`: Update an entity by ID.
|
||||
* - `DELETE /:id`: Delete an entity by ID.
|
||||
*
|
||||
* Browse the source code for more details about request/response validation.
|
||||
*
|
||||
* @see {@link SchemaActions} for the `actions` configuration.
|
||||
*/
|
||||
export default class SchemaRouter<
|
||||
CreateSchema extends SchemaLike,
|
||||
Schema extends CreateSchema,
|
||||
|
@ -36,7 +96,7 @@ export default class SchemaRouter<
|
|||
koaPagination({ isOptional: true }),
|
||||
koaGuard({ response: schema.guard.array(), status: [200] }),
|
||||
async (ctx, next) => {
|
||||
const [count, entities] = await actions.findAll(ctx.pagination);
|
||||
const [count, entities] = await actions.get(ctx.pagination);
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
|
@ -51,7 +111,7 @@ export default class SchemaRouter<
|
|||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.insert(ctx.guard.body);
|
||||
ctx.body = await actions.post(ctx.guard.body);
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
|
@ -65,7 +125,7 @@ export default class SchemaRouter<
|
|||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.findById(ctx.guard.params.id);
|
||||
ctx.body = await actions.getById(ctx.guard.params.id);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
@ -74,12 +134,12 @@ export default class SchemaRouter<
|
|||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: actions.updateGuard,
|
||||
body: actions.patchById.guard,
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
void actions.updateById(ctx.guard.params.id, ctx.guard.body);
|
||||
ctx.body = await actions.patchById.run(ctx.guard.params.id, ctx.guard.body);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue