diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index e70784bdd..19a7e7907 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -66,6 +66,11 @@ export const mockScope: Scope = { createdAt: 1_645_334_775_356, }; +export const mockScopeWithResource = { + ...mockScope, + resource: mockResource, +}; + export const mockRole: Role = { tenantId: 'fake_tenant', id: 'role_id', diff --git a/packages/core/src/routes/role.scope.test.ts b/packages/core/src/routes/role.scope.test.ts index 5a9056401..3f1893ef8 100644 --- a/packages/core/src/routes/role.scope.test.ts +++ b/packages/core/src/routes/role.scope.test.ts @@ -1,7 +1,7 @@ import type { Role } from '@logto/schemas'; import { pickDefault } from '@logto/shared/esm'; -import { mockRole, mockScope, mockResource } from '#src/__mocks__/index.js'; +import { mockRole, mockScope, mockResource, mockScopeWithResource } from '#src/__mocks__/index.js'; import { mockId, mockStandardId } from '#src/test-utils/nanoid.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -68,7 +68,16 @@ describe('role scope routes', () => { findScopesByIds.mockResolvedValueOnce([mockScope]); const response = await roleRequester.get(`/roles/${mockRole.id}/scopes`); expect(response.status).toEqual(200); - expect(response.body).toEqual([mockScope]); + expect(response.body).toEqual([mockScopeWithResource]); + }); + + it('GET /roles/:id/scopes (with pagination)', async () => { + findRoleById.mockResolvedValueOnce(mockRole); + findRolesScopesByRoleId.mockResolvedValueOnce([]); + findScopesByIds.mockResolvedValueOnce([mockScope]); + const response = await roleRequester.get(`/roles/${mockRole.id}/scopes?page=1`); + expect(response.status).toEqual(200); + expect(response.body).toEqual([mockScopeWithResource]); }); it('POST /roles/:id/scopes', async () => { diff --git a/packages/core/src/routes/role.scope.ts b/packages/core/src/routes/role.scope.ts index a103c86aa..c7da9f815 100644 --- a/packages/core/src/routes/role.scope.ts +++ b/packages/core/src/routes/role.scope.ts @@ -1,4 +1,5 @@ -import type { ScopeResponse } from '@logto/schemas'; +import type { Scope, ScopeResponse } from '@logto/schemas'; +import { scopeResponseGuard } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { tryThat } from '@silverhand/essentials'; import { object, string } from 'zod'; @@ -21,11 +22,26 @@ export default function roleScopeRoutes( scopes: { findScopeById, findScopesByIds, countScopesByScopeIds, searchScopesByScopeIds }, } = queries; + const attachResourceToScopes = async (scopes: readonly Scope[]): Promise => { + const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId)); + return scopes.map((scope) => { + const resource = resources.find(({ id }) => id === scope.resourceId); + + assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`)); + + return { + ...scope, + resource, + }; + }); + }; + router.get( '/roles/:id/scopes', koaPagination({ isOptional: true }), koaGuard({ params: object({ id: string().min(1) }), + response: scopeResponseGuard.array(), }), async (ctx, next) => { const { @@ -43,7 +59,9 @@ export default function roleScopeRoutes( const scopeIds = rolesScopes.map(({ scopeId }) => scopeId); if (disabled) { - ctx.body = await searchScopesByScopeIds(scopeIds, search); + const scopes = await searchScopesByScopeIds(scopeIds, search); + + ctx.body = await attachResourceToScopes(scopes); return next(); } @@ -53,21 +71,9 @@ export default function roleScopeRoutes( searchScopesByScopeIds(scopeIds, search, limit, offset), ]); - const resources = await findResourcesByIds(scopes.map(({ resourceId }) => resourceId)); - const result: ScopeResponse[] = scopes.map((scope) => { - const resource = resources.find(({ id }) => id === scope.resourceId); - - assertThat(resource, new Error(`Cannot find resource for id ${scope.resourceId}`)); - - return { - ...scope, - resource, - }; - }); - // Return totalCount to pagination middleware ctx.pagination.totalCount = count; - ctx.body = result; + ctx.body = await attachResourceToScopes(scopes); return next(); }, diff --git a/packages/schemas/src/types/scope.ts b/packages/schemas/src/types/scope.ts index 81bfd8f99..2b268a9a3 100644 --- a/packages/schemas/src/types/scope.ts +++ b/packages/schemas/src/types/scope.ts @@ -1,3 +1,9 @@ -import type { Resource, Scope } from '../db-entries/index.js'; +import { type z } from 'zod'; -export type ScopeResponse = Scope & { resource: Resource }; +import { Resources, Scopes } from '../db-entries/index.js'; + +export const scopeResponseGuard = Scopes.guard.extend({ + resource: Resources.guard, +}); + +export type ScopeResponse = z.infer;