mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(core,schemas): generate updateGuard
and update schema actions
This commit is contained in:
parent
c057024b7c
commit
9a995ef65f
7 changed files with 55 additions and 102 deletions
|
@ -1,49 +1,9 @@
|
|||
import {
|
||||
type CreateOrganization,
|
||||
type Organization,
|
||||
Organizations,
|
||||
type OrganizationKeys,
|
||||
} from '@logto/schemas';
|
||||
import { type OmitAutoSetFields } from '@logto/shared';
|
||||
import { Organizations } from '@logto/schemas';
|
||||
|
||||
import { type Pagination } from '#src/middleware/koa-pagination.js';
|
||||
import type OrganizationQueries from '#src/queries/organizations.js';
|
||||
import SchemaRouter, { type SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from './types.js';
|
||||
|
||||
type PostSchema = Omit<OmitAutoSetFields<CreateOrganization>, 'id'>;
|
||||
type PatchSchema = Partial<Omit<OmitAutoSetFields<Organization>, 'id'>>;
|
||||
|
||||
class OrganizationActions
|
||||
implements SchemaActions<OrganizationKeys, Organization, PostSchema, PatchSchema>
|
||||
{
|
||||
postGuard = Organizations.createGuard.omit({ id: true, createdAt: true });
|
||||
patchGuard = Organizations.guard.omit({ id: true, createdAt: true }).partial();
|
||||
|
||||
constructor(public readonly queries: OrganizationQueries) {}
|
||||
|
||||
public async get({ limit, offset }: Pick<Pagination, 'limit' | 'offset'>) {
|
||||
return Promise.all([this.queries.findTotalNumber(), this.queries.findAll(limit, offset)]);
|
||||
}
|
||||
|
||||
public async getById(id: string) {
|
||||
return this.queries.findById(id);
|
||||
}
|
||||
|
||||
public async post(data: PostSchema) {
|
||||
return this.queries.insert(data);
|
||||
}
|
||||
|
||||
public async patchById(id: string, data: PatchSchema) {
|
||||
return this.queries.updateById(id, data);
|
||||
}
|
||||
|
||||
public async deleteById(id: string) {
|
||||
return this.queries.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default function organizationRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
originalRouter,
|
||||
|
@ -52,7 +12,7 @@ export default function organizationRoutes<T extends AuthedRouter>(
|
|||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(Organizations, new OrganizationActions(organizations));
|
||||
const router = new SchemaRouter(Organizations, new SchemaActions(organizations));
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { type GeneratedSchema } from '@logto/schemas';
|
||||
import {
|
||||
generateStandardId,
|
||||
type UpdateWhereData,
|
||||
type OmitAutoSetFields,
|
||||
type SchemaLike,
|
||||
} from '@logto/shared';
|
||||
import { type UpdateWhereData, type OmitAutoSetFields, type SchemaLike } from '@logto/shared';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { buildDeleteByIdWithPool } from '#src/database/delete-by-id.js';
|
||||
|
@ -14,6 +9,11 @@ import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
|||
import { buildGetTotalRowCountWithPool } from '#src/database/row-count.js';
|
||||
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
||||
|
||||
/**
|
||||
* Query class that contains all the necessary CRUD queries for a schema. It is
|
||||
* designed to be used with SchemaActions for a SchemaRouter. You can also extend
|
||||
* this class to add more queries.
|
||||
*/
|
||||
export default class SchemaQueries<
|
||||
Key extends string,
|
||||
CreateSchema extends Partial<SchemaLike<Key> & { id: string }>,
|
||||
|
@ -55,9 +55,8 @@ export default class SchemaQueries<
|
|||
return this.#findById(id);
|
||||
}
|
||||
|
||||
async insert(data: Omit<OmitAutoSetFields<CreateSchema>, 'id'>): Promise<Readonly<Schema>>;
|
||||
async insert(data: OmitAutoSetFields<CreateSchema>): Promise<Readonly<Schema>> {
|
||||
return this.#insert({ id: generateStandardId(), ...data });
|
||||
return this.#insert(data);
|
||||
}
|
||||
|
||||
async updateById(
|
||||
|
|
|
@ -3,8 +3,9 @@ import { z } from 'zod';
|
|||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import SchemaQueries from './SchemaQueries.js';
|
||||
import SchemaRouter, { type SchemaActions } from './SchemaRouter.js';
|
||||
import { createRequester } from './test-utils.js';
|
||||
import { createRequester, createTestPool } from './test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
|
@ -26,9 +27,11 @@ describe('SchemaRouter', () => {
|
|||
fieldKeys: ['id'],
|
||||
createGuard: z.object({ id: z.string().optional() }),
|
||||
guard: z.object({ id: z.string() }),
|
||||
updateGuard: z.object({ id: z.string().optional() }),
|
||||
};
|
||||
const entities = [{ id: 'test' }, { id: 'test2' }] as const satisfies readonly Schema[];
|
||||
const actions: SchemaActions<'id', Schema, CreateSchema, CreateSchema> = {
|
||||
const actions: SchemaActions<'id', CreateSchema, Schema> = {
|
||||
queries: new SchemaQueries(createTestPool(), schema),
|
||||
get: jest.fn().mockResolvedValue([entities.length, entities]),
|
||||
getById: jest.fn(async (id) => {
|
||||
const entity = entities.find((entity) => entity.id === id);
|
||||
|
@ -37,9 +40,7 @@ describe('SchemaRouter', () => {
|
|||
}
|
||||
return entity;
|
||||
}),
|
||||
postGuard: z.object({ id: z.string().optional() }),
|
||||
post: jest.fn(async () => ({ id: 'test_new' })),
|
||||
patchGuard: z.object({ id: z.string().optional() }),
|
||||
patchById: jest.fn(async (id, data) => ({ id, ...data })),
|
||||
deleteById: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -1,36 +1,23 @@
|
|||
import { type SchemaLike, type GeneratedSchema } from '@logto/schemas';
|
||||
import { generateStandardId, type OmitAutoSetFields } from '@logto/shared';
|
||||
import Router, { type IRouterParamContext } from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination, { type Pagination } from '#src/middleware/koa-pagination.js';
|
||||
|
||||
import type SchemaQueries from './SchemaQueries.js';
|
||||
|
||||
/**
|
||||
* Actions configuration for a {@link SchemaRouter}. It contains the
|
||||
* necessary functions to handle the CRUD operations for a schema.
|
||||
*/
|
||||
export abstract class SchemaActions<
|
||||
export class SchemaActions<
|
||||
Key extends string,
|
||||
Schema extends SchemaLike<Key>,
|
||||
PostSchema extends Partial<Schema>,
|
||||
PatchSchema extends Partial<Schema>,
|
||||
CreateSchema extends Partial<SchemaLike<Key> & { id: string }>,
|
||||
Schema extends SchemaLike<Key> & { id: string },
|
||||
> {
|
||||
/**
|
||||
* The guard for the `POST /` request body. You can specify a partial
|
||||
* schema and disable some fields for business logic reasons, such as
|
||||
* id and createdAt.
|
||||
*
|
||||
* @see {@link post} for the function to create an entity.
|
||||
*/
|
||||
public abstract postGuard: z.ZodType<PostSchema>;
|
||||
|
||||
/**
|
||||
* The guard for the `PATCH /:id` request body. You can specify a
|
||||
* partial schema and disable some fields for business logic reasons.
|
||||
*
|
||||
* @see {@link patchById} for the function to update an entity.
|
||||
*/
|
||||
public abstract patchGuard: z.ZodType<PatchSchema>;
|
||||
constructor(public readonly queries: SchemaQueries<Key, CreateSchema, Schema>) {}
|
||||
|
||||
/**
|
||||
* The function for `GET /` route to get a list of entities.
|
||||
|
@ -40,23 +27,33 @@ export abstract class SchemaActions<
|
|||
* @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.
|
||||
*/
|
||||
public abstract get(
|
||||
pagination: Pick<Pagination, 'limit' | 'offset'>
|
||||
): Promise<[count: number, entities: readonly Schema[]]>;
|
||||
public async get({
|
||||
limit,
|
||||
offset,
|
||||
}: Pick<Pagination, 'limit' | 'offset'>): Promise<[count: number, entities: readonly Schema[]]> {
|
||||
return Promise.all([this.queries.findTotalNumber(), this.queries.findAll(limit, offset)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public abstract getById(id: string): Promise<Readonly<Schema>>;
|
||||
public async getById(id: string): Promise<Readonly<Schema>> {
|
||||
return this.queries.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The function for `POST /` route to create an entity.
|
||||
*
|
||||
* @param data The data of the entity to be created.
|
||||
* @returns The created entity.
|
||||
*/
|
||||
public abstract post(data: PostSchema): Promise<Readonly<Schema>>;
|
||||
public async post(data: Omit<OmitAutoSetFields<CreateSchema>, 'id'>): Promise<Readonly<Schema>>;
|
||||
public async post(data: OmitAutoSetFields<CreateSchema>): Promise<Readonly<Schema>> {
|
||||
return this.queries.insert({ id: generateStandardId(), ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* The function for `PATCH /:id` route to update the entity by ID.
|
||||
|
@ -65,14 +62,18 @@ export abstract class SchemaActions<
|
|||
* @param data The data of the entity to be updated.
|
||||
* @returns The updated entity.
|
||||
*/
|
||||
public abstract patchById(id: string, data: PatchSchema): Promise<Readonly<Schema>>;
|
||||
public async patchById(id: string, data: Partial<Schema>): Promise<Readonly<Schema>> {
|
||||
return this.queries.updateById(id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The function for `DELETE /:id` route to delete an entity by ID.
|
||||
*
|
||||
* @param id The ID of the entity to be deleted.
|
||||
*/
|
||||
public abstract deleteById(id: string): Promise<void>;
|
||||
public async deleteById(id: string): Promise<void> {
|
||||
return this.queries.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,16 +93,14 @@ export abstract class SchemaActions<
|
|||
*/
|
||||
export default class SchemaRouter<
|
||||
Key extends string,
|
||||
CreateSchema extends Partial<SchemaLike<Key>>,
|
||||
Schema extends SchemaLike<Key>,
|
||||
PostSchema extends Partial<Schema> = Partial<Schema>,
|
||||
PatchSchema extends Partial<Schema> = Partial<Schema>,
|
||||
CreateSchema extends Partial<SchemaLike<Key> & { id: string }>,
|
||||
Schema extends SchemaLike<Key> & { id: string },
|
||||
StateT = unknown,
|
||||
CustomT extends IRouterParamContext = IRouterParamContext,
|
||||
> extends Router<StateT, CustomT> {
|
||||
constructor(
|
||||
public readonly schema: GeneratedSchema<Key, CreateSchema, Schema>,
|
||||
public readonly actions: SchemaActions<Key, Schema, PostSchema, PatchSchema>
|
||||
public readonly actions: SchemaActions<Key, CreateSchema, Schema>
|
||||
) {
|
||||
super({ prefix: '/' + schema.table.replaceAll('_', '-') });
|
||||
|
||||
|
@ -120,7 +119,7 @@ export default class SchemaRouter<
|
|||
this.post(
|
||||
'/',
|
||||
koaGuard({
|
||||
body: actions.postGuard,
|
||||
body: schema.createGuard,
|
||||
response: schema.guard,
|
||||
status: [201, 422],
|
||||
}),
|
||||
|
@ -148,7 +147,7 @@ export default class SchemaRouter<
|
|||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: actions.patchGuard,
|
||||
body: schema.updateGuard,
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
|
|
|
@ -7,21 +7,13 @@ type ParseOptional<K> = undefined extends K
|
|||
? ZodOptional<ZodType<Exclude<K, undefined>>>
|
||||
: ZodType<K>;
|
||||
|
||||
export type CreateGuard<T extends Record<string, unknown>> = ZodObject<
|
||||
export type Guard<T extends SchemaLike<string>> = ZodObject<
|
||||
{
|
||||
[key in keyof T]-?: ParseOptional<T[key]>;
|
||||
},
|
||||
'strip',
|
||||
ZodTypeAny,
|
||||
T
|
||||
>;
|
||||
|
||||
export type Guard<T extends Record<string, unknown>> = ZodObject<
|
||||
{
|
||||
[key in keyof T]: ZodType<T[key]>;
|
||||
},
|
||||
'strip',
|
||||
ZodTypeAny,
|
||||
T,
|
||||
T
|
||||
>;
|
||||
|
||||
|
@ -36,6 +28,7 @@ export type GeneratedSchema<
|
|||
[key in Key]: string;
|
||||
};
|
||||
fieldKeys: readonly Key[];
|
||||
createGuard: CreateGuard<CreateSchema>;
|
||||
createGuard: Guard<CreateSchema>;
|
||||
guard: Guard<Schema>;
|
||||
updateGuard: Guard<Partial<Schema>>;
|
||||
}>;
|
||||
|
|
|
@ -162,7 +162,7 @@ const generate = async () => {
|
|||
}));
|
||||
|
||||
if (tableWithTypes.length > 0) {
|
||||
tsTypes.push('GeneratedSchema', 'Guard', 'CreateGuard');
|
||||
tsTypes.push('GeneratedSchema', 'Guard');
|
||||
}
|
||||
/* eslint-enable @silverhand/fp/no-mutating-methods */
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export const generateSchema = ({ name, comments, fields }: TableWithType) => {
|
|||
.map(({ name }) => `'${camelcase(name)}'`)
|
||||
.join(' | ')};`,
|
||||
'',
|
||||
`const createGuard: CreateGuard<${databaseEntryType}> = z.object({`,
|
||||
`const createGuard: Guard<${databaseEntryType}> = z.object({`,
|
||||
|
||||
...fields.map(
|
||||
// eslint-disable-next-line complexity
|
||||
|
@ -137,6 +137,7 @@ export const generateSchema = ({ name, comments, fields }: TableWithType) => {
|
|||
' ] as const,',
|
||||
' createGuard,',
|
||||
' guard,',
|
||||
' updateGuard: guard.partial(),',
|
||||
'});',
|
||||
].join('\n');
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue