mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #4755 from logto-io/gao-org-featured
feat: organization featured members
This commit is contained in:
commit
9b1f0ced09
15 changed files with 311 additions and 135 deletions
|
@ -1,4 +1,4 @@
|
|||
import { type Organization } from '@logto/schemas';
|
||||
import { type OrganizationWithFeatured, RoleType } from '@logto/schemas';
|
||||
import { joinPath } from '@silverhand/essentials';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -11,6 +11,7 @@ import { defaultPageSize } from '@/consts';
|
|||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import Table from '@/ds-components/Table';
|
||||
import { type RequestError } from '@/hooks/use-api';
|
||||
import AssignedEntities from '@/pages/Roles/components/AssignedEntities';
|
||||
import { buildUrl } from '@/utils/url';
|
||||
|
||||
const pageSize = defaultPageSize;
|
||||
|
@ -19,8 +20,9 @@ const apiPathname = 'api/organizations';
|
|||
|
||||
function OrganizationsTable() {
|
||||
const [page, setPage] = useState(1);
|
||||
const { data: response, error } = useSWR<[Organization[], number], RequestError>(
|
||||
const { data: response, error } = useSWR<[OrganizationWithFeatured[], number], RequestError>(
|
||||
buildUrl(apiPathname, {
|
||||
showFeatured: '1',
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
})
|
||||
|
@ -53,7 +55,13 @@ function OrganizationsTable() {
|
|||
{
|
||||
title: t('organizations.members'),
|
||||
dataIndex: 'members',
|
||||
render: () => 'members', // TODO: render members
|
||||
render: ({ usersCount, featuredUsers }) => (
|
||||
<AssignedEntities
|
||||
type={RoleType.User}
|
||||
entities={featuredUsers ?? []}
|
||||
count={usersCount ?? 0}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rowIndexKey="id"
|
||||
|
|
|
@ -163,14 +163,18 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
|||
`);
|
||||
};
|
||||
|
||||
const findApplicationsByIds = async (applicationIds: string[]) =>
|
||||
applicationIds.length > 0
|
||||
? pool.any<Application>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id} in (${sql.join(applicationIds, sql`, `)})
|
||||
`)
|
||||
: [];
|
||||
const findApplicationsByIds = async (
|
||||
applicationIds: string[]
|
||||
): Promise<readonly Application[]> => {
|
||||
if (applicationIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return pool.any<Application>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id} in (${sql.join(applicationIds, sql`, `)})
|
||||
`);
|
||||
};
|
||||
|
||||
const deleteApplicationById = async (id: string) => {
|
||||
const { rowCount } = await pool.query(sql`
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
type OrganizationRoleWithScopes,
|
||||
type OrganizationWithRoles,
|
||||
type UserWithOrganizationRoles,
|
||||
type FeaturedUser,
|
||||
} from '@logto/schemas';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
@ -34,6 +35,35 @@ class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, type
|
|||
super(pool, OrganizationUserRelations.table, Organizations, Users);
|
||||
}
|
||||
|
||||
async getFeatured(
|
||||
organizationId: string
|
||||
): Promise<[totalNumber: number, users: readonly FeaturedUser[]]> {
|
||||
const users = convertToIdentifiers(Users, true);
|
||||
const relations = convertToIdentifiers(OrganizationUserRelations, true);
|
||||
const mainSql = sql`
|
||||
from ${relations.table}
|
||||
left join ${users.table}
|
||||
on ${relations.fields.userId} = ${users.fields.id}
|
||||
where ${relations.fields.organizationId} = ${organizationId}
|
||||
`;
|
||||
const [{ count }, data] = await Promise.all([
|
||||
this.pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
${mainSql}
|
||||
`),
|
||||
this.pool.any<FeaturedUser>(sql`
|
||||
select
|
||||
${users.fields.id},
|
||||
${users.fields.avatar},
|
||||
${users.fields.name}
|
||||
${mainSql}
|
||||
limit 3
|
||||
`),
|
||||
]);
|
||||
|
||||
return [Number(count), data];
|
||||
}
|
||||
|
||||
async getOrganizationsByUserId(userId: string): Promise<Readonly<OrganizationWithRoles[]>> {
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const organizations = convertToIdentifiers(Organizations, true);
|
||||
|
@ -62,7 +92,7 @@ class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, type
|
|||
organizationId: string,
|
||||
{ limit, offset }: GetEntitiesOptions,
|
||||
search?: SearchOptions<(typeof userSearchKeys)[number]>
|
||||
): Promise<[totalCount: number, entities: Readonly<UserWithOrganizationRoles[]>]> {
|
||||
): Promise<[totalNumber: number, entities: Readonly<UserWithOrganizationRoles[]>]> {
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const users = convertToIdentifiers(Users, true);
|
||||
const { fields } = convertToIdentifiers(OrganizationUserRelations, true);
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { OrganizationRoles, Organizations, userWithOrganizationRolesGuard } from '@logto/schemas';
|
||||
import { type Optional, cond } from '@silverhand/essentials';
|
||||
import {
|
||||
OrganizationRoles,
|
||||
type OrganizationWithFeatured,
|
||||
Organizations,
|
||||
featuredUserGuard,
|
||||
userWithOrganizationRolesGuard,
|
||||
} from '@logto/schemas';
|
||||
import { type Optional, cond, yes } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type SearchOptions } from '#src/database/utils.js';
|
||||
|
@ -8,6 +14,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import { userSearchKeys } from '#src/queries/user.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
import { parseSearchOptions } from '#src/utils/search.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
|
@ -22,11 +29,52 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
queries: { organizations },
|
||||
},
|
||||
] = args;
|
||||
|
||||
const router = new SchemaRouter(Organizations, organizations, {
|
||||
errorHandler,
|
||||
searchFields: ['name'],
|
||||
disabled: { get: true },
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
query: z.object({ q: z.string().optional(), showFeatured: z.string().optional() }),
|
||||
response: (
|
||||
Organizations.guard.merge(
|
||||
// For `showFeatured` query
|
||||
z
|
||||
.object({
|
||||
usersCount: z.number(),
|
||||
featuredUsers: featuredUserGuard.array(),
|
||||
})
|
||||
.partial()
|
||||
) satisfies z.ZodType<OrganizationWithFeatured>
|
||||
).array(),
|
||||
status: [200],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { query } = ctx.guard;
|
||||
const search = parseSearchOptions(['name'], query);
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const [count, entities] = await organizations.findAll(limit, offset, search);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = yes(query.showFeatured)
|
||||
? await Promise.all(
|
||||
entities.map(async (entity) => {
|
||||
const [usersCount, featuredUsers] = await organizations.relations.users.getFeatured(
|
||||
entity.id
|
||||
);
|
||||
return { ...entity, usersCount, featuredUsers };
|
||||
})
|
||||
)
|
||||
: entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// MARK: Organization - user relation routes
|
||||
router.addRelationRoutes(organizations.relations.users, undefined, { disabled: { get: true } });
|
||||
|
||||
|
|
|
@ -101,8 +101,6 @@ describe('role routes', () => {
|
|||
id: mockUser.id,
|
||||
avatar: mockUser.avatar,
|
||||
name: mockUser.name,
|
||||
username: mockUser.username,
|
||||
primaryEmail: mockUser.primaryEmail,
|
||||
},
|
||||
],
|
||||
applicationsCount: 0,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { RoleResponse } from '@logto/schemas';
|
||||
import { Applications, Roles, Users } from '@logto/schemas';
|
||||
import { Roles, featuredApplicationGuard, featuredUserGuard } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { tryThat } from '@silverhand/essentials';
|
||||
import { pickState, tryThat } from '@silverhand/essentials';
|
||||
import { object, string, z, number } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
@ -53,20 +53,9 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
|
|||
.merge(
|
||||
object({
|
||||
usersCount: number(),
|
||||
featuredUsers: Users.guard
|
||||
.pick({
|
||||
avatar: true,
|
||||
id: true,
|
||||
name: true,
|
||||
})
|
||||
.array(),
|
||||
featuredUsers: featuredUserGuard.array(),
|
||||
applicationsCount: number(),
|
||||
featuredApplications: Applications.guard
|
||||
.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
})
|
||||
.array(),
|
||||
featuredApplications: featuredApplicationGuard.array(),
|
||||
})
|
||||
)
|
||||
.array(),
|
||||
|
@ -113,15 +102,9 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
|
|||
return {
|
||||
...role,
|
||||
usersCount,
|
||||
featuredUsers: users.map(({ id, avatar, name, username, primaryEmail }) => ({
|
||||
id,
|
||||
avatar,
|
||||
name,
|
||||
username,
|
||||
primaryEmail,
|
||||
})),
|
||||
featuredUsers: users.map(pickState('id', 'avatar', 'name')),
|
||||
applicationsCount,
|
||||
featuredApplications: applications.map(({ id, name }) => ({ id, name })),
|
||||
featuredApplications: applications.map(pickState('id', 'name', 'type')),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
@ -167,7 +167,7 @@ export default class RelationQueries<
|
|||
* @param options Options for the query.
|
||||
* @param options.limit The maximum number of entities to return.
|
||||
* @param options.offset The number of entities to skip.
|
||||
* @returns A Promise that resolves an array of entities of the given schema.
|
||||
* @returns A Promise that resolves to the total number of entities and the entities.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
|
@ -182,7 +182,7 @@ export default class RelationQueries<
|
|||
forSchema: S,
|
||||
where: CamelCaseIdObject<Exclude<Schemas[number]['tableSingular'], S['tableSingular']>>,
|
||||
options?: GetEntitiesOptions
|
||||
): Promise<[totalCount: number, entities: ReadonlyArray<InferSchema<S>>]> {
|
||||
): Promise<[totalNumber: number, entities: ReadonlyArray<InferSchema<S>>]> {
|
||||
const { limit, offset } = options ?? {};
|
||||
const snakeCaseWhere = snakecaseKeys(where);
|
||||
const forTable = sql.identifier([forSchema.table]);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { type SchemaLike, type GeneratedSchema } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { cond, type Optional, type DeepPartial } from '@silverhand/essentials';
|
||||
import { type DeepPartial } from '@silverhand/essentials';
|
||||
import camelcase from 'camelcase';
|
||||
import deepmerge from 'deepmerge';
|
||||
import Router, { type IRouterParamContext } from 'koa-router';
|
||||
|
@ -12,6 +12,7 @@ import koaPagination from '#src/middleware/koa-pagination.js';
|
|||
|
||||
import { type TwoRelationsQueries } from './RelationQueries.js';
|
||||
import type SchemaQueries from './SchemaQueries.js';
|
||||
import { parseSearchOptions } from './search.js';
|
||||
|
||||
/**
|
||||
* Generate the pathname for from a table name.
|
||||
|
@ -129,14 +130,7 @@ export default class SchemaRouter<
|
|||
status: [200],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { q } = ctx.guard.query;
|
||||
const search: Optional<SearchOptions<Key>> = cond(
|
||||
q &&
|
||||
searchFields.length > 0 && {
|
||||
fields: searchFields,
|
||||
keyword: q,
|
||||
}
|
||||
);
|
||||
const search = parseSearchOptions(searchFields, ctx.guard.query);
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const [count, entities] = await queries.findAll(limit, offset, search);
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { SearchJointMode, SearchMatchMode } from '@logto/schemas';
|
||||
import type { Nullable, Optional } from '@silverhand/essentials';
|
||||
import { yes, conditionalString, conditional } from '@silverhand/essentials';
|
||||
import { yes, conditionalString, conditional, cond } from '@silverhand/essentials';
|
||||
import { sql } from 'slonik';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
import { type SearchOptions } from '#src/database/utils.js';
|
||||
|
||||
import assertThat from './assert-that.js';
|
||||
import { isEnum } from './type.js';
|
||||
|
||||
|
@ -281,3 +283,27 @@ export const buildConditionsFromSearch = (
|
|||
|
||||
return sql.join(conditions, getJointModeSql(joint));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the search query from the request query string and build the search options
|
||||
* for certain search fields.
|
||||
*
|
||||
* @param searchFields Search fields to be included in the search options.
|
||||
* @param guardedQuery The guarded query key-value object.
|
||||
* @returns The search options object, or `undefined` if no search query is found.
|
||||
*/
|
||||
export const parseSearchOptions = <Key extends string>(
|
||||
searchFields: readonly Key[],
|
||||
guardedQuery: {
|
||||
q?: string;
|
||||
}
|
||||
): Optional<SearchOptions<Key>> => {
|
||||
const { q } = guardedQuery;
|
||||
return cond(
|
||||
q &&
|
||||
searchFields.length > 0 && {
|
||||
fields: searchFields,
|
||||
keyword: q,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
type Organization,
|
||||
type OrganizationWithRoles,
|
||||
type UserWithOrganizationRoles,
|
||||
type OrganizationWithFeatured,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
|
@ -22,6 +23,11 @@ export class OrganizationApi extends ApiFactory<
|
|||
super('organizations');
|
||||
}
|
||||
|
||||
override async getList(query?: URLSearchParams): Promise<OrganizationWithFeatured[]> {
|
||||
// eslint-disable-next-line no-restricted-syntax -- This API has different response type
|
||||
return super.getList(query) as Promise<OrganizationWithFeatured[]>;
|
||||
}
|
||||
|
||||
async addUsers(id: string, userIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
|
||||
}
|
||||
|
|
|
@ -1,90 +1,127 @@
|
|||
import { generateStandardId } from '@logto/shared';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||
|
||||
const randomId = () => generateStandardId(4);
|
||||
import { UserApiTest } from '#src/helpers/user.js';
|
||||
|
||||
// Add additional layer of describe to run tests in band
|
||||
describe('organization APIs', () => {
|
||||
describe('organizations', () => {
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
const organizationApi = new OrganizationApiTest();
|
||||
const userApi = new UserApiTest();
|
||||
|
||||
afterEach(async () => {
|
||||
await organizationApi.cleanUp();
|
||||
afterEach(async () => {
|
||||
await Promise.all([organizationApi.cleanUp(), userApi.cleanUp()]);
|
||||
});
|
||||
|
||||
it('should get organizations successfully', async () => {
|
||||
await organizationApi.create({ name: 'test', description: 'A test organization.' });
|
||||
await organizationApi.create({ name: 'test2' });
|
||||
const organizations = await organizationApi.getList();
|
||||
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test', description: 'A test organization.' })
|
||||
);
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test2', description: null })
|
||||
);
|
||||
for (const organization of organizations) {
|
||||
expect(organization).not.toHaveProperty('usersCount');
|
||||
expect(organization).not.toHaveProperty('featuredUsers');
|
||||
}
|
||||
});
|
||||
|
||||
it('should get organizations with featured users', async () => {
|
||||
const [organization1, organization2] = await Promise.all([
|
||||
organizationApi.create({ name: 'test' }),
|
||||
organizationApi.create({ name: 'test' }),
|
||||
]);
|
||||
const createdUsers = await Promise.all(
|
||||
Array.from({ length: 5 }).map(async () => userApi.create({ name: 'featured' }))
|
||||
);
|
||||
await organizationApi.addUsers(
|
||||
organization1.id,
|
||||
createdUsers.map((user) => user.id)
|
||||
);
|
||||
|
||||
const organizations = await organizationApi.getList(
|
||||
new URLSearchParams({
|
||||
showFeatured: '1',
|
||||
})
|
||||
);
|
||||
|
||||
expect(organizations).toContainEqual(expect.objectContaining({ id: organization1.id }));
|
||||
expect(organizations).toContainEqual(expect.objectContaining({ id: organization2.id }));
|
||||
for (const organization of organizations) {
|
||||
expect(organization).toHaveProperty('usersCount');
|
||||
expect(organization).toHaveProperty('featuredUsers');
|
||||
if (organization.id === organization1.id) {
|
||||
expect(organization.usersCount).toBe(5);
|
||||
expect(organization.featuredUsers).toHaveLength(3);
|
||||
}
|
||||
|
||||
if (organization.id === organization2.id) {
|
||||
expect(organization.usersCount).toBe(0);
|
||||
expect(organization.featuredUsers).toHaveLength(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should get organizations with pagination', async () => {
|
||||
// Add organizations to exceed the default page size
|
||||
await Promise.all(
|
||||
Array.from({ length: 30 }).map(async () => organizationApi.create({ name: 'test' }))
|
||||
);
|
||||
|
||||
const organizations = await organizationApi.getList();
|
||||
expect(organizations).toHaveLength(20);
|
||||
|
||||
const organizations2 = await organizationApi.getList(
|
||||
new URLSearchParams({
|
||||
page: '2',
|
||||
page_size: '10',
|
||||
})
|
||||
);
|
||||
expect(organizations2.length).toBeGreaterThanOrEqual(10);
|
||||
expect(organizations2[0]?.id).not.toBeFalsy();
|
||||
expect(organizations2[0]?.id).toBe(organizations[10]?.id);
|
||||
});
|
||||
|
||||
it('should be able to create and get organizations by id', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
const organization = await organizationApi.get(createdOrganization.id);
|
||||
|
||||
expect(organization).toStrictEqual(createdOrganization);
|
||||
});
|
||||
|
||||
it('should fail when try to get an organization that does not exist', async () => {
|
||||
const response = await organizationApi.get('0').catch((error: unknown) => error);
|
||||
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should be able to update organization', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
const organization = await organizationApi.update(createdOrganization.id, {
|
||||
name: 'test2',
|
||||
description: 'test description.',
|
||||
});
|
||||
|
||||
it('should get organizations successfully', async () => {
|
||||
await organizationApi.create({ name: 'test', description: 'A test organization.' });
|
||||
await organizationApi.create({ name: 'test2' });
|
||||
const organizations = await organizationApi.getList();
|
||||
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test', description: 'A test organization.' })
|
||||
);
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test2', description: null })
|
||||
);
|
||||
});
|
||||
|
||||
it('should get organizations with pagination', async () => {
|
||||
// Add organizations to exceed the default page size
|
||||
await Promise.all(
|
||||
Array.from({ length: 30 }).map(async () => organizationApi.create({ name: 'test' }))
|
||||
);
|
||||
|
||||
const organizations = await organizationApi.getList();
|
||||
expect(organizations).toHaveLength(20);
|
||||
|
||||
const organizations2 = await organizationApi.getList(
|
||||
new URLSearchParams({
|
||||
page: '2',
|
||||
page_size: '10',
|
||||
})
|
||||
);
|
||||
expect(organizations2.length).toBeGreaterThanOrEqual(10);
|
||||
expect(organizations2[0]?.id).not.toBeFalsy();
|
||||
expect(organizations2[0]?.id).toBe(organizations[10]?.id);
|
||||
});
|
||||
|
||||
it('should be able to create and get organizations by id', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
const organization = await organizationApi.get(createdOrganization.id);
|
||||
|
||||
expect(organization).toStrictEqual(createdOrganization);
|
||||
});
|
||||
|
||||
it('should fail when try to get an organization that does not exist', async () => {
|
||||
const response = await organizationApi.get('0').catch((error: unknown) => error);
|
||||
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should be able to update organization', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
const organization = await organizationApi.update(createdOrganization.id, {
|
||||
name: 'test2',
|
||||
description: 'test description.',
|
||||
});
|
||||
expect(organization).toStrictEqual({
|
||||
...createdOrganization,
|
||||
name: 'test2',
|
||||
description: 'test description.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to delete organization', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
await organizationApi.delete(createdOrganization.id);
|
||||
const response = await organizationApi
|
||||
.get(createdOrganization.id)
|
||||
.catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should fail when try to delete an organization that does not exist', async () => {
|
||||
const response = await organizationApi.delete('0').catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
expect(organization).toStrictEqual({
|
||||
...createdOrganization,
|
||||
name: 'test2',
|
||||
description: 'test description.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to delete organization', async () => {
|
||||
const createdOrganization = await organizationApi.create({ name: 'test' });
|
||||
await organizationApi.delete(createdOrganization.id);
|
||||
const response = await organizationApi
|
||||
.get(createdOrganization.id)
|
||||
.catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should fail when try to delete an organization that does not exist', async () => {
|
||||
const response = await organizationApi.delete('0').catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
import type { Application } from '../db-entries/index.js';
|
||||
import { type z } from 'zod';
|
||||
|
||||
import { Applications, type Application } from '../db-entries/index.js';
|
||||
|
||||
export type ApplicationResponse = Application & { isAdmin: boolean };
|
||||
|
||||
/**
|
||||
* An application that is featured for display. Usually used in a list of resources that are
|
||||
* related to a group of applications.
|
||||
*/
|
||||
export type FeaturedApplication = Pick<Application, 'id' | 'name' | 'type'>;
|
||||
|
||||
/** The guard for {@link FeaturedApplication}. */
|
||||
export const featuredApplicationGuard = Applications.guard.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
}) satisfies z.ZodType<FeaturedApplication>;
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
Users,
|
||||
} from '../db-entries/index.js';
|
||||
|
||||
import { type FeaturedUser } from './user.js';
|
||||
|
||||
export type OrganizationRoleWithScopes = OrganizationRole & {
|
||||
scopes: Array<{
|
||||
id: string;
|
||||
|
@ -67,3 +69,12 @@ export const userWithOrganizationRolesGuard: z.ZodType<UserWithOrganizationRoles
|
|||
Users.guard.extend({
|
||||
organizationRoles: organizationRoleEntityGuard.array(),
|
||||
});
|
||||
|
||||
/**
|
||||
* The organization entity with optional `usersCount` and `featuredUsers` fields.
|
||||
* They are useful for displaying the organization list in the frontend.
|
||||
*/
|
||||
export type OrganizationWithFeatured = Organization & {
|
||||
usersCount?: number;
|
||||
featuredUsers?: FeaturedUser[];
|
||||
};
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import type { Application, Role, User } from '../db-entries/index.js';
|
||||
import type { Role } from '../db-entries/index.js';
|
||||
|
||||
import { type FeaturedApplication } from './application.js';
|
||||
import { type FeaturedUser } from './user.js';
|
||||
|
||||
export type RoleResponse = Role & {
|
||||
usersCount: number;
|
||||
featuredUsers: Array<Pick<User, 'avatar' | 'id' | 'name' | 'username' | 'primaryEmail'>>;
|
||||
featuredUsers: FeaturedUser[];
|
||||
applicationsCount: number;
|
||||
featuredApplications: Array<Pick<Application, 'id' | 'name'>>;
|
||||
featuredApplications: FeaturedApplication[];
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Users } from '../db-entries/index.js';
|
||||
import { type User, Users } from '../db-entries/index.js';
|
||||
import { MfaFactor } from '../foundations/index.js';
|
||||
|
||||
export const userInfoSelectFields = Object.freeze([
|
||||
|
@ -63,3 +63,16 @@ export enum AdminTenantRole {
|
|||
export enum PredefinedScope {
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
/**
|
||||
* A user that is featured for display. Usually used in a list of resources that are related to
|
||||
* a group of users.
|
||||
*/
|
||||
export type FeaturedUser = Pick<User, 'id' | 'avatar' | 'name'>;
|
||||
|
||||
/** The guard for {@link FeaturedUser}. */
|
||||
export const featuredUserGuard = Users.guard.pick({
|
||||
id: true,
|
||||
avatar: true,
|
||||
name: true,
|
||||
}) satisfies z.ZodType<FeaturedUser>;
|
||||
|
|
Loading…
Reference in a new issue