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:
parent
334cc5903a
commit
6b6210feee
8 changed files with 106 additions and 12 deletions
|
@ -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 })
|
||||||
|
|
58
packages/core/src/database/update-where.ts
Normal file
58
packages/core/src/database/update-where.ts
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
|
@ -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}
|
||||||
|
|
|
@ -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) }) }),
|
||||||
|
|
6
packages/core/src/utils/schema.ts
Normal file
6
packages/core/src/utils/schema.ts
Normal 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);
|
|
@ -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.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}} 不存在。',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue