diff --git a/.changeset/strange-oranges-sort.md b/.changeset/strange-oranges-sort.md new file mode 100644 index 000000000..77ca91d97 --- /dev/null +++ b/.changeset/strange-oranges-sort.md @@ -0,0 +1,10 @@ +--- +"@logto/core": patch +--- + +fix: incorrect pagination behavior in organization role scopes APIs + +- Fix `/api/organization-roles/{id}/scopes` and `/api/organization-roles/{id}/resource-scopes` endpoints to: + - Return all scopes when no pagination parameters are provided + - Support optional pagination when query parameters are present +- Fix Console to properly display all organization role scopes on the organization template page diff --git a/packages/core/src/routes/organization-role/index.openapi.json b/packages/core/src/routes/organization-role/index.openapi.json index fbdcbcf4d..6cb02922a 100644 --- a/packages/core/src/routes/organization-role/index.openapi.json +++ b/packages/core/src/routes/organization-role/index.openapi.json @@ -102,7 +102,7 @@ "/api/organization-roles/{id}/scopes": { "get": { "summary": "Get organization role scopes", - "description": "Get all organization scopes that are assigned to the specified organization role.", + "description": "Get organization scopes that are assigned to the specified organization role with optional pagination.", "responses": { "200": { "description": "A list of organization scopes." @@ -174,7 +174,7 @@ "/api/organization-roles/{id}/resource-scopes": { "get": { "summary": "Get organization role resource scopes", - "description": "Get all resource scopes that are assigned to the specified organization role.", + "description": "Get resource scopes that are assigned to the specified organization role with optional pagination.", "responses": { "200": { "description": "A list of resource scopes." diff --git a/packages/core/src/routes/organization-role/index.ts b/packages/core/src/routes/organization-role/index.ts index ba036ca00..c5972a147 100644 --- a/packages/core/src/routes/organization-role/index.ts +++ b/packages/core/src/routes/organization-role/index.ts @@ -127,8 +127,8 @@ export default function organizationRoleRoutes( } ); - router.addRelationRoutes(rolesScopes, 'scopes'); - router.addRelationRoutes(rolesResourceScopes, 'resource-scopes'); + router.addRelationRoutes(rolesScopes, 'scopes', { isPaginationOptional: true }); + router.addRelationRoutes(rolesResourceScopes, 'resource-scopes', { isPaginationOptional: true }); originalRouter.use(router.routes()); } diff --git a/packages/core/src/routes/organization-scope/index.openapi.json b/packages/core/src/routes/organization-scope/index.openapi.json index 48f6835eb..3eea303db 100644 --- a/packages/core/src/routes/organization-scope/index.openapi.json +++ b/packages/core/src/routes/organization-scope/index.openapi.json @@ -9,7 +9,7 @@ "/api/organization-scopes": { "get": { "summary": "Get organization scopes", - "description": "Get organization scopes that match with pagination.", + "description": "Get organization scopes that match with optional pagination.", "responses": { "200": { "description": "A list of organization scopes." diff --git a/packages/core/src/routes/organization-scope/index.ts b/packages/core/src/routes/organization-scope/index.ts index bb7f7d396..dec606682 100644 --- a/packages/core/src/routes/organization-scope/index.ts +++ b/packages/core/src/routes/organization-scope/index.ts @@ -19,6 +19,7 @@ export default function organizationScopeRoutes( middlewares: [], errorHandler, searchFields: ['name'], + isPaginationOptional: true, }); originalRouter.use(router.routes()); diff --git a/packages/core/src/utils/SchemaRouter.ts b/packages/core/src/utils/SchemaRouter.ts index c8c3ae3c1..1b131c1a8 100644 --- a/packages/core/src/utils/SchemaRouter.ts +++ b/packages/core/src/utils/SchemaRouter.ts @@ -86,6 +86,11 @@ type SchemaRouterConfig = { * If not provided, the `schema.guard` will be used. */ entityGuard?: z.ZodTypeAny; + /** + * If the GET route's pagination is optional. + * @default false + */ + isPaginationOptional?: boolean; }; type RelationRoutesConfig = { @@ -313,12 +318,12 @@ export default class SchemaRouter< #addRoutes() { const { queries, schema, config } = this; - const { disabled, searchFields, idLength, entityGuard } = config; + const { disabled, searchFields, idLength, entityGuard, isPaginationOptional } = config; if (!disabled.get) { this.get( '/', - koaPagination(), + koaPagination({ isOptional: isPaginationOptional }), koaGuard({ query: z.object({ q: z.string().optional() }), response: (entityGuard ?? schema.guard).array(), diff --git a/packages/integration-tests/src/tests/api/hook/hook.trigger.data.test.ts b/packages/integration-tests/src/tests/api/hook/hook.trigger.data.test.ts index 0562b297a..7db2b48f2 100644 --- a/packages/integration-tests/src/tests/api/hook/hook.trigger.data.test.ts +++ b/packages/integration-tests/src/tests/api/hook/hook.trigger.data.test.ts @@ -299,6 +299,10 @@ describe('organization scope data hook events', () => { expect(organizationScopeCreateHook?.payload.event).toBe('OrganizationScope.Created'); }); + afterAll(async () => { + await organizationScopeApi.cleanUp(); + }); + it.each(organizationScopeDataHookTestCases)( 'test case %#: %p', async ({ route, event, method, endpoint, payload }) => { diff --git a/packages/integration-tests/src/tests/api/organization/organization-scope.test.ts b/packages/integration-tests/src/tests/api/organization/organization-scope.test.ts index 146f49979..1029c5444 100644 --- a/packages/integration-tests/src/tests/api/organization/organization-scope.test.ts +++ b/packages/integration-tests/src/tests/api/organization/organization-scope.test.ts @@ -39,15 +39,21 @@ describe('organization scope APIs', () => { expect(scopes).toContainEqual(expect.objectContaining({ name: name2, description: null })); }); - it('should get organization scopes with pagination', async () => { - // Add 20 scopes to exceed the default page size + it('should get organization scopes with optional pagination', async () => { + const pageSize = 20; + // Create scopes to exceed the default page size + const createCount = pageSize + 10; await Promise.all( - Array.from({ length: 30 }).map(async () => scopeApi.create({ name: 'test' + randomId() })) + Array.from({ length: createCount }).map(async () => + scopeApi.create({ name: 'test' + randomId() }) + ) ); + // Should return all scopes if no pagination is provided const scopes = await scopeApi.getList(); - expect(scopes).toHaveLength(20); + expect(scopes).toHaveLength(createCount); + // Should return paginated scopes based on specified page number and page size const scopes2 = await scopeApi.getList( new URLSearchParams({ page: '2', diff --git a/packages/integration-tests/src/tests/api/organization/organization-user.test.ts b/packages/integration-tests/src/tests/api/organization/organization-user.test.ts index d08d3e66b..d710f435c 100644 --- a/packages/integration-tests/src/tests/api/organization/organization-user.test.ts +++ b/packages/integration-tests/src/tests/api/organization/organization-user.test.ts @@ -390,6 +390,9 @@ describe('organization user APIs', () => { await organizationApi.addUserRoles(organization.id, user.id, [role2.id]); const newScopes = await organizationApi.getUserOrganizationScopes(organization.id, user.id); expect(newScopes.map(({ name }) => name)).toEqual([scope1.name]); + + // Clean up + await organizationApi.cleanUp(); }); }); });