From bfea0b0fdd51cb7480647332a4178f44f00c7610 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 15 Nov 2023 17:20:31 +0800 Subject: [PATCH] refactor(core): fix org apis (#4890) --- .../core/src/routes/organization/roles.ts | 64 ++++++++++++++++++- .../src/helpers/organization.ts | 12 +++- .../src/tests/api/organization-role.test.ts | 27 +++++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/packages/core/src/routes/organization/roles.ts b/packages/core/src/routes/organization/roles.ts index 50b474c4a..3aa6d0667 100644 --- a/packages/core/src/routes/organization/roles.ts +++ b/packages/core/src/routes/organization/roles.ts @@ -1,5 +1,13 @@ -import { OrganizationRoles } from '@logto/schemas'; +import { + type CreateOrganizationRole, + OrganizationRoles, + organizationRoleWithScopesGuard, +} from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { z } from 'zod'; +import koaGuard from '#src/middleware/koa-guard.js'; +import koaPagination from '#src/middleware/koa-pagination.js'; import SchemaRouter from '#src/utils/SchemaRouter.js'; import { type AuthedRouter, type RouterInitArgs } from '../types.js'; @@ -20,9 +28,63 @@ export default function organizationRoleRoutes( ]: RouterInitArgs ) { const router = new SchemaRouter(OrganizationRoles, roles, { + disabled: { get: true, post: true }, errorHandler, searchFields: ['name'], }); + + router.get( + '/', + koaPagination(), + koaGuard({ + response: organizationRoleWithScopesGuard.array(), + status: [200], + }), + async (ctx, next) => { + const { limit, offset } = ctx.pagination; + const [count, entities] = await roles.findAll(limit, offset); + + ctx.pagination.totalCount = count; + ctx.body = entities; + return next(); + } + ); + + /** Allows to carry an initial set of scopes for creating a new organization role. */ + type CreateOrganizationRolePayload = Omit & { + organizationScopeIds: string[]; + }; + + const createGuard: z.ZodType = + OrganizationRoles.createGuard + .omit({ + id: true, + }) + .extend({ + organizationScopeIds: z.array(z.string()).default([]), + }); + + router.post( + '/', + koaGuard({ + body: createGuard, + response: OrganizationRoles.guard, + status: [201, 422], + }), + async (ctx, next) => { + const { organizationScopeIds: scopeIds, ...data } = ctx.guard.body; + const role = await roles.insert({ id: generateStandardId(), ...data }); + + if (scopeIds.length > 0) { + await rolesScopes.insert(...scopeIds.map<[string, string]>((id) => [role.id, id])); + } + + ctx.body = role; + ctx.status = 201; + return next(); + } + ); + router.addRelationRoutes(rolesScopes, 'scopes'); originalRouter.use(router.routes()); diff --git a/packages/integration-tests/src/helpers/organization.ts b/packages/integration-tests/src/helpers/organization.ts index 1ff7b6d30..71a6f13f4 100644 --- a/packages/integration-tests/src/helpers/organization.ts +++ b/packages/integration-tests/src/helpers/organization.ts @@ -1,4 +1,9 @@ -import { type OrganizationScope, type OrganizationRole, type Organization } from '@logto/schemas'; +import { + type OrganizationScope, + type OrganizationRole, + type Organization, + type OrganizationRoleWithScopes, +} from '@logto/schemas'; import { trySafe } from '@silverhand/essentials'; import { @@ -20,10 +25,11 @@ export class OrganizationRoleApiTest extends OrganizationRoleApi { return this.#roles; } - override async create(data: CreateOrganizationRolePostData): Promise { + override async create(data: CreateOrganizationRolePostData): Promise { const created = await super.create(data); this.roles.push(created); - return created; + // eslint-disable-next-line no-restricted-syntax -- to override the type + return created as OrganizationRoleWithScopes; } /** diff --git a/packages/integration-tests/src/tests/api/organization-role.test.ts b/packages/integration-tests/src/tests/api/organization-role.test.ts index 18901fb25..ab15da51c 100644 --- a/packages/integration-tests/src/tests/api/organization-role.test.ts +++ b/packages/integration-tests/src/tests/api/organization-role.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert'; import { generateStandardId } from '@logto/shared'; -import { isKeyInObject } from '@silverhand/essentials'; +import { isKeyInObject, pick } from '@silverhand/essentials'; import { HTTPError } from 'got'; import { OrganizationRoleApiTest, OrganizationScopeApiTest } from '#src/helpers/organization.js'; @@ -12,9 +12,10 @@ const randomId = () => generateStandardId(4); describe('organization role APIs', () => { describe('organization roles', () => { const roleApi = new OrganizationRoleApiTest(); + const scopeApi = new OrganizationScopeApiTest(); afterEach(async () => { - await roleApi.cleanUp(); + await Promise.all([roleApi.cleanUp(), scopeApi.cleanUp()]); }); it('should fail if the name of the new organization role already exists', async () => { @@ -71,6 +72,28 @@ describe('organization role APIs', () => { expect(role).toStrictEqual(createdRole); }); + it('should be able to create a new organization with initial scopes', async () => { + const [scope1, scope2] = await Promise.all([ + scopeApi.create({ name: 'test' + randomId() }), + scopeApi.create({ name: 'test' + randomId() }), + ]); + const createdRole = await roleApi.create({ + name: 'test' + randomId(), + description: 'test description.', + organizationScopeIds: [scope1.id, scope2.id], + }); + const scopes = await roleApi.getScopes(createdRole.id); + const roles = await roleApi.getList(); + const roleWithScopes = roles.find((role) => role.id === createdRole.id); + + for (const scope of [scope1, scope2]) { + expect(roleWithScopes?.scopes).toContainEqual( + expect.objectContaining(pick(scope, 'id', 'name')) + ); + expect(scopes).toContainEqual(expect.objectContaining(pick(scope, 'id', 'name'))); + } + }); + it('should fail when try to get an organization role that does not exist', async () => { const response = await roleApi.get('0').catch((error: unknown) => error);