0
Fork 0
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:
Gao Sun 2023-10-09 17:15:46 +08:00
parent c1df440682
commit bb22ce3acc
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
11 changed files with 219 additions and 10 deletions

View 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);
}
};

View file

@ -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.

View file

@ -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> = {

View file

@ -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;

View 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);
}
}

View file

@ -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);

View 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());
}

View file

@ -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>>;

View file

@ -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,

View 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();
}
);
}
}

View file

@ -0,0 +1,5 @@
import { type CommonQueryMethods } from 'slonik';
export default abstract class TenantQueries {
constructor(public readonly pool: CommonQueryMethods) {}
}