mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): list resources with scopes (#2819)
This commit is contained in:
parent
cf900d4aef
commit
cb03d64e12
8 changed files with 129 additions and 11 deletions
|
@ -32,6 +32,20 @@ export const mockResource: Resource = {
|
|||
accessTokenTtl: 3600,
|
||||
};
|
||||
|
||||
export const mockResource2: Resource = {
|
||||
id: 'logto_api2',
|
||||
name: 'management api',
|
||||
indicator: 'logto.dev/api',
|
||||
accessTokenTtl: 3600,
|
||||
};
|
||||
|
||||
export const mockResource3: Resource = {
|
||||
id: 'logto_api3',
|
||||
name: 'management api',
|
||||
indicator: 'logto.dev/api',
|
||||
accessTokenTtl: 3600,
|
||||
};
|
||||
|
||||
export const mockScope: Scope = {
|
||||
id: 'scope_id',
|
||||
name: 'read:users',
|
||||
|
|
49
packages/core/src/libraries/resource.test.ts
Normal file
49
packages/core/src/libraries/resource.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { createMockUtils } from '@logto/shared/esm';
|
||||
|
||||
import { mockResource, mockResource2, mockResource3, mockScope } from '#src/__mocks__/index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsmWithActual } = createMockUtils(jest);
|
||||
|
||||
const { findScopesByResourceIds } = await mockEsmWithActual('#src/queries/scope.js', () => ({
|
||||
findScopesByResourceIds: jest.fn(async () => [
|
||||
{ ...mockScope, id: '1', resourceId: mockResource.id },
|
||||
{ ...mockScope, id: '2', resourceId: mockResource.id },
|
||||
{ ...mockScope, id: '3', resourceId: mockResource2.id },
|
||||
]),
|
||||
}));
|
||||
|
||||
const { attachScopesToResources } = await import('./resource.js');
|
||||
|
||||
describe('attachScopesToResources', () => {
|
||||
beforeEach(() => {
|
||||
findScopesByResourceIds.mockClear();
|
||||
});
|
||||
|
||||
it('should find and attach scopes to each resource', async () => {
|
||||
await expect(
|
||||
attachScopesToResources([mockResource, mockResource2, mockResource3])
|
||||
).resolves.toEqual([
|
||||
{
|
||||
...mockResource,
|
||||
scopes: [
|
||||
{ ...mockScope, id: '1', resourceId: mockResource.id },
|
||||
{ ...mockScope, id: '2', resourceId: mockResource.id },
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockResource2,
|
||||
scopes: [{ ...mockScope, id: '3', resourceId: mockResource2.id }],
|
||||
},
|
||||
{
|
||||
...mockResource3,
|
||||
scopes: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty array input', async () => {
|
||||
await expect(attachScopesToResources([])).resolves.toEqual([]);
|
||||
expect(findScopesByResourceIds).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
15
packages/core/src/libraries/resource.ts
Normal file
15
packages/core/src/libraries/resource.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { Resource, ResourceResponse } from '@logto/schemas';
|
||||
|
||||
import { findScopesByResourceIds } from '#src/queries/scope.js';
|
||||
|
||||
export const attachScopesToResources = async (
|
||||
resources: readonly Resource[]
|
||||
): Promise<ResourceResponse[]> => {
|
||||
const resourceIds = resources.map(({ id }) => id);
|
||||
const scopes = resourceIds.length > 0 ? await findScopesByResourceIds(resourceIds) : [];
|
||||
|
||||
return resources.map((resource) => ({
|
||||
...resource,
|
||||
scopes: scopes.filter(({ resourceId }) => resourceId === resource.id),
|
||||
}));
|
||||
};
|
|
@ -19,6 +19,13 @@ export const findScopesByResourceId = async (resourceId: string) =>
|
|||
where ${fields.resourceId}=${resourceId}
|
||||
`);
|
||||
|
||||
export const findScopesByResourceIds = async (resourceIds: string[]) =>
|
||||
envSet.pool.any<Scope>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.resourceId} in (${sql.join(resourceIds, sql`, `)})
|
||||
`);
|
||||
|
||||
export const insertScope = buildInsertInto<CreateScope, Scope>(Scopes, {
|
||||
returning: true,
|
||||
});
|
||||
|
|
|
@ -6,7 +6,15 @@ import { createRequester } from '#src/utils/test-utils.js';
|
|||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
|
||||
|
||||
await mockEsmWithActual('#src/libraries/resource.js', () => ({
|
||||
attachScopesToResources: async (resources: Resource[]) =>
|
||||
resources.map((resource) => ({
|
||||
...resource,
|
||||
scopes: [],
|
||||
})),
|
||||
}));
|
||||
|
||||
const { findResourceById } = mockEsm('#src/queries/resource.js', () => ({
|
||||
findTotalNumberOfResources: async () => ({ count: 10 }),
|
||||
|
@ -49,6 +57,13 @@ describe('resource routes', () => {
|
|||
expect(response.header).toHaveProperty('total-number', '10');
|
||||
});
|
||||
|
||||
it('GET /resources?includeScopes=true', async () => {
|
||||
const response = await resourceRequest.get('/resources?includeScopes=true');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([{ ...mockResource, scopes: [] }]);
|
||||
expect(response.header).toHaveProperty('total-number', '10');
|
||||
});
|
||||
|
||||
it('POST /resources', async () => {
|
||||
const name = 'user api';
|
||||
const indicator = 'logto.dev/user';
|
||||
|
|
|
@ -2,6 +2,8 @@ import { buildIdGenerator } from '@logto/core-kit';
|
|||
import { Resources, Scopes } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import { isTrue } from '#src/env-set/parameters.js';
|
||||
import { attachScopesToResources } from '#src/libraries/resource.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import {
|
||||
|
@ -25,19 +27,31 @@ const resourceId = buildIdGenerator(21);
|
|||
const scoupeId = resourceId;
|
||||
|
||||
export default function resourceRoutes<T extends AuthedRouter>(router: T) {
|
||||
router.get('/resources', koaPagination(), async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
router.get(
|
||||
'/resources',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
query: object({
|
||||
includeScopes: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const {
|
||||
query: { includeScopes },
|
||||
} = ctx.guard;
|
||||
|
||||
const [{ count }, resources] = await Promise.all([
|
||||
findTotalNumberOfResources(),
|
||||
findAllResources(limit, offset),
|
||||
]);
|
||||
const [{ count }, resources] = await Promise.all([
|
||||
findTotalNumberOfResources(),
|
||||
findAllResources(limit, offset),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = resources;
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = isTrue(includeScopes) ? await attachScopesToResources(resources) : resources;
|
||||
|
||||
return next();
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/resources',
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './user.js';
|
|||
export * from './logto-config.js';
|
||||
export * from './interactions.js';
|
||||
export * from './search.js';
|
||||
export * from './resource.js';
|
||||
|
|
3
packages/schemas/src/types/resource.ts
Normal file
3
packages/schemas/src/types/resource.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import type { Resource, Scope } from '../db-entries/index.js';
|
||||
|
||||
export type ResourceResponse = Resource & { scopes: Scope[] };
|
Loading…
Reference in a new issue