0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: PATCH /application/:id

This commit is contained in:
Gao Sun 2021-08-27 00:33:13 +08:00
parent 334cc5903a
commit 6b6210feee
No known key found for this signature in database
GPG key ID: 0F0EFA2E36639F31
8 changed files with 106 additions and 12 deletions

View file

@ -32,19 +32,19 @@ type InsertIntoConfig = {
}; };
interface BuildInsertInto { interface BuildInsertInto {
<Schema extends SchemaLike<string>>( <Schema extends SchemaLike>(
pool: DatabasePoolType, pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>, { fieldKeys, ...rest }: GeneratedSchema<Schema>,
config: InsertIntoConfigReturning config: InsertIntoConfigReturning
): (data: OmitAutoSetFields<Schema>) => Promise<Schema>; ): (data: OmitAutoSetFields<Schema>) => Promise<Schema>;
<Schema extends SchemaLike<string>>( <Schema extends SchemaLike>(
pool: DatabasePoolType, pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>, { fieldKeys, ...rest }: GeneratedSchema<Schema>,
config?: InsertIntoConfig config?: InsertIntoConfig
): (data: OmitAutoSetFields<Schema>) => Promise<void>; ): (data: OmitAutoSetFields<Schema>) => Promise<void>;
} }
export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike<string>>( export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike>(
pool: DatabasePoolType, pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>, { fieldKeys, ...rest }: GeneratedSchema<Schema>,
config?: InsertIntoConfig | InsertIntoConfigReturning config?: InsertIntoConfig | InsertIntoConfigReturning
@ -55,7 +55,9 @@ export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike<strin
const onConflict = config?.onConflict; const onConflict = config?.onConflict;
return async (data: OmitAutoSetFields<Schema>): Promise<Schema | void> => { return async (data: OmitAutoSetFields<Schema>): Promise<Schema | void> => {
const result = await pool.query<Schema>(sql` const {
rows: [entry],
} = await pool.query<Schema>(sql`
insert into ${table} (${sql.join( insert into ${table} (${sql.join(
keys.map((key) => fields[key]), keys.map((key) => fields[key]),
sql`, ` sql`, `
@ -74,10 +76,6 @@ export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike<strin
)} )}
`); `);
const {
rows: [entry],
} = result;
assert( assert(
!returning || entry, !returning || entry,
new RequestError({ code: 'entity.create_failed', name: rest.tableSingular }) new RequestError({ code: 'entity.create_failed', name: rest.tableSingular })

View file

@ -0,0 +1,58 @@
import assert from 'assert';
import RequestError from '@/errors/RequestError';
import { SchemaLike, GeneratedSchema } from '@logto/schemas';
import { DatabasePoolType, sql } from 'slonik';
import { isKeyOf } from '@/utils/schema';
import { notFalsy, Truthy } from '@logto/essentials';
import { conditionalSql, convertToIdentifiers, convertToPrimitiveOrSql } from './utils';
export type UpdateWhereData<Schema extends SchemaLike> = {
set: Partial<Schema>;
where: Partial<Schema>;
};
interface BuildUpdateWhere {
<Schema extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning: true
): (data: UpdateWhereData<Schema>) => Promise<Schema>;
<Schema extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning?: false
): (data: UpdateWhereData<Schema>) => Promise<void>;
}
export const buildUpdateWhere: BuildUpdateWhere = <Schema extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning = false
) => {
const { table, fields } = convertToIdentifiers(schema);
const isKeyOfSchema = isKeyOf(schema);
const connectKeyValueWithEqualSign = (data: Partial<Schema>) =>
Object.entries(data)
.map(
([key, value]) =>
isKeyOfSchema(key) && sql`${fields[key]}=${convertToPrimitiveOrSql(key, value)}`
)
.filter((value): value is Truthy<typeof value> => notFalsy(value));
return async ({ set, where }: UpdateWhereData<Schema>) => {
const {
rows: [entry],
} = await pool.query<Schema>(sql`
update ${table}
set ${sql.join(connectKeyValueWithEqualSign(set), sql`, `)}
where ${sql.join(connectKeyValueWithEqualSign(where), sql`, `)}
${conditionalSql(returning, () => sql`returning *`)}
`);
assert(
!returning || entry,
new RequestError({ code: 'entity.update_failed', name: schema.tableSingular })
);
return entry;
};
};

View file

@ -1,6 +1,7 @@
import { buildInsertInto } from '@/database/insert-into'; import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool'; import pool from '@/database/pool';
import { convertToIdentifiers } from '@/database/utils'; import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers, OmitAutoSetFields } from '@/database/utils';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { ApplicationDBEntry, Applications } from '@logto/schemas'; import { ApplicationDBEntry, Applications } from '@logto/schemas';
import { sql } from 'slonik'; import { sql } from 'slonik';
@ -18,6 +19,13 @@ export const insertApplication = buildInsertInto<ApplicationDBEntry>(pool, Appli
returning: true, returning: true,
}); });
const updateApplication = buildUpdateWhere<ApplicationDBEntry>(pool, Applications, true);
export const updateApplicationById = async (
id: string,
set: Partial<OmitAutoSetFields<ApplicationDBEntry>>
) => updateApplication({ set, where: { id } });
export const deleteApplicationById = async (id: string) => { export const deleteApplicationById = async (id: string) => {
const { rowCount } = await pool.query(sql` const { rowCount } = await pool.query(sql`
delete from ${table} delete from ${table}

View file

@ -2,7 +2,11 @@ import Router from 'koa-router';
import { object, string } from 'zod'; import { object, string } from 'zod';
import { Applications } from '@logto/schemas'; import { Applications } from '@logto/schemas';
import koaGuard from '@/middleware/koa-guard'; import koaGuard from '@/middleware/koa-guard';
import { deleteApplicationById, insertApplication } from '@/queries/application'; import {
deleteApplicationById,
insertApplication,
updateApplicationById,
} from '@/queries/application';
import { buildIdGenerator } from '@/utils/id'; import { buildIdGenerator } from '@/utils/id';
import { generateOidcClientMetadata } from '@/oidc/utils'; import { generateOidcClientMetadata } from '@/oidc/utils';
@ -31,6 +35,24 @@ export default function applicationRoutes<StateT, ContextT>(router: Router<State
} }
); );
router.patch(
'/application/:id',
koaGuard({
params: object({ id: string().min(1) }),
// Consider `.deepPartial()` if OIDC client metadata bloats
body: Applications.guard.omit({ id: true, createdAt: true }).partial(),
}),
async (ctx, next) => {
const {
params: { id },
body,
} = ctx.guard;
ctx.body = await updateApplicationById(id, body);
return next();
}
);
router.delete( router.delete(
'/application/:id', '/application/:id',
koaGuard({ params: object({ id: string().min(1) }) }), koaGuard({ params: object({ id: string().min(1) }) }),

View file

@ -0,0 +1,6 @@
import { GeneratedSchema, SchemaLike } from '@logto/schemas';
export const isKeyOf =
<Schema extends SchemaLike>({ fieldKeys }: GeneratedSchema<Schema>) =>
(key: string): key is keyof Schema extends string ? keyof Schema : never =>
fieldKeys.includes(key);

View file

@ -39,6 +39,7 @@ const errors = {
}, },
entity: { entity: {
create_failed: 'Failed to create {{name}}.', create_failed: 'Failed to create {{name}}.',
update_failed: 'Failed to update {{name}}.',
not_exists: 'The {{name}} with ID `{{id}}` does not exist.', not_exists: 'The {{name}} with ID `{{id}}` does not exist.',
}, },
}; };

View file

@ -41,6 +41,7 @@ const errors = {
}, },
entity: { entity: {
create_failed: '创建 {{name}} 失败。', create_failed: '创建 {{name}} 失败。',
update_failed: '更新 {{name}} 失败。',
not_exists: 'ID 为 `{{id}}` 的 {{name}} 不存在。', not_exists: 'ID 为 `{{id}}` 的 {{name}} 不存在。',
}, },
}; };

View file

@ -8,11 +8,11 @@ export type Guard<T extends Record<string, unknown>> = ZodObject<
export type SchemaValuePrimitive = string | number | boolean | undefined; export type SchemaValuePrimitive = string | number | boolean | undefined;
export type SchemaValue = SchemaValuePrimitive | Record<string, unknown>; export type SchemaValue = SchemaValuePrimitive | Record<string, unknown>;
export type SchemaLike<Key extends string> = { export type SchemaLike<Key extends string = string> = {
[key in Key]: SchemaValue; [key in Key]: SchemaValue;
}; };
export type GeneratedSchema<Schema extends SchemaLike<string>> = keyof Schema extends string export type GeneratedSchema<Schema extends SchemaLike> = keyof Schema extends string
? Readonly<{ ? Readonly<{
table: string; table: string;
tableSingular: string; tableSingular: string;