0
Fork 0
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:
wangsijie 2023-01-05 15:43:01 +08:00 committed by GitHub
parent cf900d4aef
commit cb03d64e12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 11 deletions

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
import type { Resource, Scope } from '../db-entries/index.js';
export type ResourceResponse = Resource & { scopes: Scope[] };