mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(core): org basic apis
This commit is contained in:
parent
c1df440682
commit
bb22ce3acc
11 changed files with 219 additions and 10 deletions
packages/core/src
database
middleware
queries
routes
tenants
utils
15
packages/core/src/database/delete-by-id.ts
Normal file
15
packages/core/src/database/delete-by-id.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { type CommonQueryMethods, sql } from 'slonik';
|
||||
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
|
||||
export const buildDeleteByIdWithPool =
|
||||
(pool: CommonQueryMethods, table: string) => async (id: string) => {
|
||||
const { rowCount } = await pool.query(sql`
|
||||
delete from ${sql.identifier([table])}
|
||||
where ${sql.identifier(['id'])}=${id};
|
||||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError(table, id);
|
||||
}
|
||||
};
|
|
@ -1,6 +1,18 @@
|
|||
import type { CommonQueryMethods, IdentifierSqlToken } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
export const buildGetTotalRowCountWithPool =
|
||||
(pool: CommonQueryMethods, table: string) => async () => {
|
||||
// Postgres returns a biging for count(*), which is then converted to a string by query library.
|
||||
// We need to convert it to a number.
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
from ${sql.identifier([table])}
|
||||
`);
|
||||
|
||||
return { count: Number(count) };
|
||||
};
|
||||
|
||||
export const getTotalRowCountWithPool =
|
||||
(pool: CommonQueryMethods) => async (table: IdentifierSqlToken) => {
|
||||
// Postgres returns a biging for count(*), which is then converted to a string by query library.
|
||||
|
|
|
@ -23,7 +23,7 @@ export type GuardConfig<QueryT, BodyT, ParametersT, ResponseT, FilesT> = {
|
|||
* // e.g. parse '?key1=foo'
|
||||
* z.object({ key1: z.string() })
|
||||
*/
|
||||
query?: ZodType<QueryT>;
|
||||
query?: ZodType<QueryT, ZodTypeDef, unknown>;
|
||||
/**
|
||||
* Guard JSON request body. You can treat the body like a normal object.
|
||||
*
|
||||
|
@ -33,7 +33,7 @@ export type GuardConfig<QueryT, BodyT, ParametersT, ResponseT, FilesT> = {
|
|||
* key2: z.object({ key3: z.number() }).array(),
|
||||
* })
|
||||
*/
|
||||
body?: ZodType<BodyT>;
|
||||
body?: ZodType<BodyT, ZodTypeDef, unknown>;
|
||||
/**
|
||||
* Guard `koa-router` path parameters (i.e. `ctx.params`).
|
||||
*
|
||||
|
@ -41,19 +41,19 @@ export type GuardConfig<QueryT, BodyT, ParametersT, ResponseT, FilesT> = {
|
|||
* // e.g. parse '/foo/:key1'
|
||||
* z.object({ key1: z.string() })
|
||||
*/
|
||||
params?: ZodType<ParametersT>;
|
||||
params?: ZodType<ParametersT, ZodTypeDef, unknown>;
|
||||
/**
|
||||
* Guard response body.
|
||||
*
|
||||
* @example z.object({ key1: z.string() })
|
||||
*/
|
||||
response?: ZodType<ResponseT>;
|
||||
response?: ZodType<ResponseT, ZodTypeDef, unknown>;
|
||||
/**
|
||||
* Guard response status code. It produces a `ServerError` (500)
|
||||
* if the response does not satisfy any of the given value(s).
|
||||
*/
|
||||
status?: number | number[];
|
||||
files?: ZodType<FilesT>;
|
||||
files?: ZodType<FilesT, ZodTypeDef, unknown>;
|
||||
};
|
||||
|
||||
export type GuardedRequest<QueryT, BodyT, ParametersT, FilesT> = {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { number } from 'zod';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { buildLink } from '#src/utils/pagination.js';
|
||||
|
||||
type Pagination = {
|
||||
export type Pagination = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
totalCount?: number;
|
||||
|
|
48
packages/core/src/queries/organizations.ts
Normal file
48
packages/core/src/queries/organizations.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { type Organization, Organizations } from '@logto/schemas';
|
||||
import { type OmitAutoSetFields } from '@logto/shared';
|
||||
|
||||
import { buildDeleteByIdWithPool } from '#src/database/delete-by-id.js';
|
||||
import { buildFindAllEntitiesWithPool } from '#src/database/find-all-entities.js';
|
||||
import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js';
|
||||
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
||||
import { buildGetTotalRowCountWithPool } from '#src/database/row-count.js';
|
||||
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
||||
import TenantQueries from '#src/utils/TenantQueries.js';
|
||||
|
||||
export default class OrganizationQueries extends TenantQueries {
|
||||
#findTotalNumber = buildGetTotalRowCountWithPool(this.pool, Organizations.table);
|
||||
#findAll = buildFindAllEntitiesWithPool(this.pool)(Organizations);
|
||||
#findById = buildFindEntityByIdWithPool(this.pool)(Organizations);
|
||||
#insert = buildInsertIntoWithPool(this.pool)(Organizations, { returning: true });
|
||||
#updateById = buildUpdateWhereWithPool(this.pool)(Organizations, true);
|
||||
#deleteById = buildDeleteByIdWithPool(this.pool, Organizations.table);
|
||||
|
||||
async findTotalNumber(): Promise<number> {
|
||||
const { count } = await this.#findTotalNumber();
|
||||
return count;
|
||||
}
|
||||
|
||||
async findAll(limit: number, offset: number): Promise<readonly Organization[]> {
|
||||
return this.#findAll(limit, offset);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Readonly<Organization>> {
|
||||
return this.#findById(id);
|
||||
}
|
||||
|
||||
async insert(data: OmitAutoSetFields<Organization>): Promise<Readonly<Organization>> {
|
||||
return this.#insert(data);
|
||||
}
|
||||
|
||||
async updateById(
|
||||
id: string,
|
||||
data: Partial<OmitAutoSetFields<Organization>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'replace'
|
||||
): Promise<Readonly<Organization>> {
|
||||
return this.#updateById({ set: data, where: { id }, jsonbMode });
|
||||
}
|
||||
|
||||
async deleteById(id: string): Promise<void> {
|
||||
await this.#deleteById(id);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import hookRoutes from './hook.js';
|
|||
import interactionRoutes from './interaction/index.js';
|
||||
import logRoutes from './log.js';
|
||||
import logtoConfigRoutes from './logto-config.js';
|
||||
import organizationRoutes from './organizations.js';
|
||||
import resourceRoutes from './resource.js';
|
||||
import roleRoutes from './role.js';
|
||||
import roleScopeRoutes from './role.scope.js';
|
||||
|
@ -63,6 +64,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
verificationCodeRoutes(managementRouter, tenant);
|
||||
userAssetsRoutes(managementRouter, tenant);
|
||||
domainRoutes(managementRouter, tenant);
|
||||
organizationRoutes(managementRouter, tenant);
|
||||
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
wellKnownRoutes(anonymousRouter, tenant);
|
||||
|
|
26
packages/core/src/routes/organizations.ts
Normal file
26
packages/core/src/routes/organizations.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Organizations } from '@logto/schemas';
|
||||
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from './types.js';
|
||||
|
||||
export default function organizationRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
originalRouter,
|
||||
{
|
||||
queries: { organizations },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(Organizations, {
|
||||
findAll: 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,
|
||||
deleteById: organizations.deleteById,
|
||||
});
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
|
@ -8,10 +8,9 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
|
||||
export type AnonymousRouter = Router<unknown, WithLogContext & WithI18nContext>;
|
||||
|
||||
export type AuthedRouter = Router<
|
||||
unknown,
|
||||
WithAuthContext & WithLogContext & WithI18nContext & ExtendableContext
|
||||
>;
|
||||
type AuthedRouterContext = WithAuthContext & WithLogContext & WithI18nContext & ExtendableContext;
|
||||
|
||||
export type AuthedRouter = Router<unknown, AuthedRouterContext>;
|
||||
|
||||
type RouterInit<T> = (router: T, tenant: TenantContext) => void;
|
||||
export type RouterInitArgs<T> = Parameters<RouterInit<T>>;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { createHooksQueries } from '#src/queries/hooks.js';
|
|||
import { createLogQueries } from '#src/queries/log.js';
|
||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||
import { createOidcModelInstanceQueries } from '#src/queries/oidc-model-instance.js';
|
||||
import OrganizationQueries from '#src/queries/organizations.js';
|
||||
import { createPasscodeQueries } from '#src/queries/passcode.js';
|
||||
import { createResourceQueries } from '#src/queries/resource.js';
|
||||
import { createRolesScopesQueries } from '#src/queries/roles-scopes.js';
|
||||
|
@ -41,6 +42,7 @@ export default class Queries {
|
|||
hooks = createHooksQueries(this.pool);
|
||||
domains = createDomainsQueries(this.pool);
|
||||
dailyActiveUsers = createDailyActiveUsersQueries(this.pool);
|
||||
organizations = new OrganizationQueries(this.pool);
|
||||
|
||||
constructor(
|
||||
public readonly pool: CommonQueryMethods,
|
||||
|
|
100
packages/core/src/utils/SchemaRouter.ts
Normal file
100
packages/core/src/utils/SchemaRouter.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { type SchemaLike, type GeneratedSchema } from '@logto/schemas';
|
||||
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';
|
||||
|
||||
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>>;
|
||||
deleteById: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export default class SchemaRouter<
|
||||
CreateSchema extends SchemaLike,
|
||||
Schema extends CreateSchema,
|
||||
UpdateSchema extends Partial<Schema> = Partial<Schema>,
|
||||
StateT = unknown,
|
||||
CustomT extends IRouterParamContext = IRouterParamContext,
|
||||
> extends Router<StateT, CustomT> {
|
||||
constructor(
|
||||
public readonly schema: GeneratedSchema<CreateSchema, Schema>,
|
||||
public readonly actions: SchemaActions<CreateSchema, Schema, UpdateSchema>
|
||||
) {
|
||||
super({ prefix: '/' + schema.table.replaceAll('_', '-') });
|
||||
|
||||
this.get(
|
||||
'/',
|
||||
koaPagination({ isOptional: true }),
|
||||
koaGuard({ response: schema.guard.array(), status: [200] }),
|
||||
async (ctx, next) => {
|
||||
const [count, entities] = await actions.findAll(ctx.pagination);
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.post(
|
||||
'/',
|
||||
koaGuard({
|
||||
body: schema.createGuard,
|
||||
response: schema.guard,
|
||||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.insert(ctx.guard.body);
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.get(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.findById(ctx.guard.params.id);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.patch(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: actions.updateGuard,
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
void actions.updateById(ctx.guard.params.id, ctx.guard.body);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.delete(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
await actions.deleteById(ctx.guard.params.id);
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
5
packages/core/src/utils/TenantQueries.ts
Normal file
5
packages/core/src/utils/TenantQueries.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { type CommonQueryMethods } from 'slonik';
|
||||
|
||||
export default abstract class TenantQueries {
|
||||
constructor(public readonly pool: CommonQueryMethods) {}
|
||||
}
|
Loading…
Add table
Reference in a new issue