mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): organization role APIs
This commit is contained in:
parent
d5a87623de
commit
57af573fe1
12 changed files with 484 additions and 66 deletions
|
@ -72,6 +72,7 @@
|
|||
"otplib": "^12.0.1",
|
||||
"p-retry": "^6.0.0",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"redis": "^4.6.5",
|
||||
"roarr": "^7.11.0",
|
||||
|
@ -99,6 +100,7 @@
|
|||
"@types/koa__cors": "^4.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/oidc-provider": "^8.0.0",
|
||||
"@types/pluralize": "^0.0.31",
|
||||
"@types/qrcode": "^1.5.2",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/sinon": "^10.0.13",
|
||||
|
|
20
packages/core/src/queries/organization-roles.ts
Normal file
20
packages/core/src/queries/organization-roles.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {
|
||||
type OrganizationRoleKeys,
|
||||
OrganizationRoles,
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||
|
||||
/** Class of queries for roles in the organization template. */
|
||||
export default class OrganizationRoleQueries extends SchemaQueries<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
super(pool, OrganizationRoles);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import hookRoutes from './hook.js';
|
|||
import interactionRoutes from './interaction/index.js';
|
||||
import logRoutes from './log.js';
|
||||
import logtoConfigRoutes from './logto-config.js';
|
||||
import organizationRoleRoutes from './organization-roles.js';
|
||||
import organizationScopeRoutes from './organization-scopes.js';
|
||||
import organizationRoutes from './organizations.js';
|
||||
import resourceRoutes from './resource.js';
|
||||
|
@ -67,6 +68,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
domainRoutes(managementRouter, tenant);
|
||||
organizationRoutes(managementRouter, tenant);
|
||||
organizationScopeRoutes(managementRouter, tenant);
|
||||
organizationRoleRoutes(managementRouter, tenant);
|
||||
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
wellKnownRoutes(anonymousRouter, tenant);
|
||||
|
|
83
packages/core/src/routes/organization-roles.ts
Normal file
83
packages/core/src/routes/organization-roles.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
type OrganizationRoleKeys,
|
||||
OrganizationRoles,
|
||||
} from '@logto/schemas';
|
||||
import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from './types.js';
|
||||
|
||||
class OrganizationRoleActions extends SchemaActions<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
override async post(
|
||||
data: Omit<CreateOrganizationRole, 'id'>
|
||||
): Promise<Readonly<OrganizationRole>> {
|
||||
try {
|
||||
return await super.post(data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UniqueIntegrityConstraintViolationError) {
|
||||
throw new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function organizationRoleRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
originalRouter,
|
||||
{
|
||||
queries: { organizationRoles, organizationRoleScopeRelations },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const actions = new OrganizationRoleActions(organizationRoles);
|
||||
const router = new SchemaRouter(OrganizationRoles, actions, { disabled: { post: true } });
|
||||
|
||||
/** Allows to carry an initial set of scopes for creating a new organization role. */
|
||||
type CreateOrganizationRolePayload = Omit<CreateOrganizationRole, 'id'> & { scopeIds: string[] };
|
||||
|
||||
const createGuard: z.ZodType<CreateOrganizationRolePayload, z.ZodTypeDef, unknown> =
|
||||
OrganizationRoles.createGuard
|
||||
.omit({
|
||||
id: true,
|
||||
})
|
||||
.extend({
|
||||
scopeIds: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
koaGuard({
|
||||
body: createGuard,
|
||||
response: OrganizationRoles.guard,
|
||||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { scopeIds, ...data } = ctx.guard.body;
|
||||
const role = await actions.post(data);
|
||||
|
||||
if (scopeIds.length > 0) {
|
||||
await organizationRoleScopeRelations.insert(
|
||||
...scopeIds.map<[string, string]>((id) => [role.id, id])
|
||||
);
|
||||
}
|
||||
|
||||
ctx.body = role;
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
OrganizationRoleScopeRelations,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes,
|
||||
} from '@logto/schemas';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { type WellKnownCache } from '#src/caches/well-known.js';
|
||||
|
@ -11,6 +16,7 @@ import { createHooksQueries } from '#src/queries/hooks.js';
|
|||
import { createLogQueries } from '#src/queries/log.js';
|
||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||
import { createOidcModelInstanceQueries } from '#src/queries/oidc-model-instance.js';
|
||||
import OrganizationRoleQueries from '#src/queries/organization-roles.js';
|
||||
import OrganizationScopeQueries from '#src/queries/organization-scopes.js';
|
||||
import OrganizationQueries from '#src/queries/organizations.js';
|
||||
import { createPasscodeQueries } from '#src/queries/passcode.js';
|
||||
|
@ -22,6 +28,7 @@ import { createSignInExperienceQueries } from '#src/queries/sign-in-experience.j
|
|||
import { createUserQueries } from '#src/queries/user.js';
|
||||
import { createUsersRolesQueries } from '#src/queries/users-roles.js';
|
||||
import { createVerificationStatusQueries } from '#src/queries/verification-status.js';
|
||||
import RelationQueries from '#src/utils/RelationQueries.js';
|
||||
|
||||
export default class Queries {
|
||||
applications = createApplicationQueries(this.pool);
|
||||
|
@ -46,6 +53,14 @@ export default class Queries {
|
|||
organizations = new OrganizationQueries(this.pool);
|
||||
/** Organization template scope queries. */
|
||||
organizationScopes = new OrganizationScopeQueries(this.pool);
|
||||
/** Organization template role queries. */
|
||||
organizationRoles = new OrganizationRoleQueries(this.pool);
|
||||
organizationRoleScopeRelations = new RelationQueries(
|
||||
this.pool,
|
||||
OrganizationRoleScopeRelations.table,
|
||||
OrganizationRoles.table,
|
||||
OrganizationScopes.table
|
||||
);
|
||||
|
||||
constructor(
|
||||
public readonly pool: CommonQueryMethods,
|
||||
|
|
108
packages/core/src/utils/RelationQueries.ts
Normal file
108
packages/core/src/utils/RelationQueries.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import pluralize from 'pluralize';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
type AtLeast2<T extends unknown[]> = `${T['length']}` extends '0' | '1' ? never : T;
|
||||
|
||||
type RemoveLiteral<T extends string, L extends string> = T extends L ? Exclude<T, L> : T;
|
||||
|
||||
/**
|
||||
* Query class for relation tables that connect several tables by their entry ids.
|
||||
*
|
||||
* @example
|
||||
* Let's say we have two tables `users` and `groups` and a relation table
|
||||
* `user_group_relations`. Then we can create a `RelationQueries` instance like this:
|
||||
*
|
||||
* ```ts
|
||||
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', 'users', 'groups');
|
||||
* ```
|
||||
*
|
||||
* To insert a new relation, we can use the {@link RelationQueries.insert} method:
|
||||
*
|
||||
* ```ts
|
||||
* await userGroupRelations.insert(['user-id-1', 'group-id-1']);
|
||||
* // Insert multiple relations at once
|
||||
* await userGroupRelations.insert(
|
||||
* ['user-id-1', 'group-id-1'],
|
||||
* ['user-id-2', 'group-id-1']
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* To get all entries for a specific table, we can use the {@link RelationQueries.getEntries} method:
|
||||
*
|
||||
* ```ts
|
||||
* await userGroupRelations.getEntries('users', { group_id: 'group-id-1' });
|
||||
* ```
|
||||
*
|
||||
* This will return all entries for the `users` table that are connected to the
|
||||
* group with the id `group-id-1`.
|
||||
*/
|
||||
export default class RelationQueries<
|
||||
SnakeCaseRelations extends Array<Lowercase<string>>,
|
||||
Length = AtLeast2<SnakeCaseRelations>['length'],
|
||||
> {
|
||||
protected get table() {
|
||||
return sql.identifier([this.relationTable]);
|
||||
}
|
||||
|
||||
public readonly relations: SnakeCaseRelations;
|
||||
|
||||
/**
|
||||
* @param pool The database pool.
|
||||
* @param relationTable The name of the relation table.
|
||||
* @param relations The names of the tables that are connected by the relation table.
|
||||
*/
|
||||
constructor(
|
||||
public readonly pool: CommonQueryMethods,
|
||||
public readonly relationTable: string,
|
||||
...relations: Readonly<SnakeCaseRelations>
|
||||
) {
|
||||
this.relations = relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new entries into the relation table.
|
||||
*
|
||||
* Each entry must contain the same number of ids as the number of relations, and
|
||||
* the order of the ids must match the order of the relations.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', 'users', 'groups');
|
||||
*
|
||||
* userGroupRelations.insert(['user-id-1', 'group-id-1']);
|
||||
* // Insert multiple relations at once
|
||||
* userGroupRelations.insert(
|
||||
* ['user-id-1', 'group-id-1'],
|
||||
* ['user-id-2', 'group-id-1']
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @param data Entries to insert.
|
||||
* @returns A Promise that resolves to the query result.
|
||||
*/
|
||||
async insert(...data: ReadonlyArray<string[] & { length: Length }>) {
|
||||
return this.pool.query(sql`
|
||||
insert into ${this.table} (${sql.join(
|
||||
this.relations.map((relation) => sql.identifier([pluralize(relation, 1) + '_id'])),
|
||||
sql`, `
|
||||
)})
|
||||
values ${sql.join(
|
||||
data.map(
|
||||
(relation) =>
|
||||
sql`(${sql.join(
|
||||
relation.map((id) => sql`${id}`),
|
||||
sql`, `
|
||||
)})`
|
||||
),
|
||||
sql`, `
|
||||
)};
|
||||
`);
|
||||
}
|
||||
|
||||
async getEntries<L extends SnakeCaseRelations[number]>(
|
||||
forRelation: L,
|
||||
where: Record<RemoveLiteral<SnakeCaseRelations[number], L>, unknown>
|
||||
) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { type SchemaLike, type GeneratedSchema, type Guard } from '@logto/schemas';
|
||||
import { generateStandardId, type OmitAutoSetFields } from '@logto/shared';
|
||||
import { type DeepPartial } from '@silverhand/essentials';
|
||||
import deepmerge from 'deepmerge';
|
||||
import Router, { type IRouterParamContext } from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -76,6 +78,22 @@ export class SchemaActions<
|
|||
}
|
||||
}
|
||||
|
||||
type SchemaRouterConfig = {
|
||||
/** Disable certain routes for the router. */
|
||||
disabled: {
|
||||
/** Disable `GET /` route. */
|
||||
get: boolean;
|
||||
/** Disable `POST /` route. */
|
||||
post: boolean;
|
||||
/** Disable `GET /:id` route. */
|
||||
getById: boolean;
|
||||
/** Disable `PATCH /:id` route. */
|
||||
patchById: boolean;
|
||||
/** Disable `DELETE /:id` route. */
|
||||
deleteById: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A standard RESTful router for a schema.
|
||||
*
|
||||
|
@ -98,77 +116,105 @@ export default class SchemaRouter<
|
|||
StateT = unknown,
|
||||
CustomT extends IRouterParamContext = IRouterParamContext,
|
||||
> extends Router<StateT, CustomT> {
|
||||
public readonly config: SchemaRouterConfig;
|
||||
|
||||
constructor(
|
||||
public readonly schema: GeneratedSchema<Key, CreateSchema, Schema>,
|
||||
public readonly actions: SchemaActions<Key, CreateSchema, Schema>
|
||||
public readonly actions: SchemaActions<Key, CreateSchema, Schema>,
|
||||
config: DeepPartial<SchemaRouterConfig> = {}
|
||||
) {
|
||||
super({ prefix: '/' + schema.table.replaceAll('_', '-') });
|
||||
|
||||
this.get(
|
||||
'/',
|
||||
koaPagination(),
|
||||
koaGuard({ response: schema.guard.array(), status: [200] }),
|
||||
async (ctx, next) => {
|
||||
const [count, entities] = await actions.get(ctx.pagination);
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
this.config = deepmerge<SchemaRouterConfig, DeepPartial<SchemaRouterConfig>>(
|
||||
{
|
||||
disabled: {
|
||||
get: false,
|
||||
post: false,
|
||||
getById: false,
|
||||
patchById: false,
|
||||
deleteById: false,
|
||||
},
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
this.post(
|
||||
'/',
|
||||
koaGuard({
|
||||
// eslint-disable-next-line no-restricted-syntax -- `.omit()` doesn't play well for generic types
|
||||
body: schema.createGuard.omit({ id: true }) as Guard<Omit<CreateSchema, 'id'>>,
|
||||
response: schema.guard,
|
||||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.post(ctx.guard.body);
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
const { disabled } = this.config;
|
||||
|
||||
this.get(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.getById(ctx.guard.params.id);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
if (!disabled.get) {
|
||||
this.get(
|
||||
'/',
|
||||
koaPagination(),
|
||||
koaGuard({ response: schema.guard.array(), status: [200] }),
|
||||
async (ctx, next) => {
|
||||
const [count, entities] = await actions.get(ctx.pagination);
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.patch(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: schema.updateGuard,
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.patchById(ctx.guard.params.id, ctx.guard.body);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
if (!disabled.post) {
|
||||
this.post(
|
||||
'/',
|
||||
koaGuard({
|
||||
// eslint-disable-next-line no-restricted-syntax -- `.omit()` doesn't play well for generic types
|
||||
body: schema.createGuard.omit({ id: true }) as Guard<Omit<CreateSchema, 'id'>>,
|
||||
response: schema.guard,
|
||||
status: [201],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.post(ctx.guard.body);
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
await actions.deleteById(ctx.guard.params.id);
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
if (!disabled.getById) {
|
||||
this.get(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.getById(ctx.guard.params.id);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!disabled.patchById) {
|
||||
this.patch(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: schema.updateGuard,
|
||||
response: schema.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await actions.patchById(ctx.guard.params.id, ctx.guard.body);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!disabled.deleteById) {
|
||||
this.delete(
|
||||
'/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
await actions.deleteById(ctx.guard.params.id);
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
packages/integration-tests/src/api/organization-role.ts
Normal file
14
packages/integration-tests/src/api/organization-role.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { type OrganizationRole } from '@logto/schemas';
|
||||
|
||||
import { ApiFactory } from './factory.js';
|
||||
|
||||
class OrganizationRoleApi extends ApiFactory<
|
||||
OrganizationRole,
|
||||
{ name: string; description?: string; scopeIds?: string[] }
|
||||
> {
|
||||
constructor() {
|
||||
super('organization-roles');
|
||||
}
|
||||
}
|
||||
|
||||
export const roleApi = new OrganizationRoleApi();
|
|
@ -0,0 +1,115 @@
|
|||
import assert from 'node:assert';
|
||||
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { isKeyInObject } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { roleApi } from '#src/api/organization-role.js';
|
||||
import { scopeApi } from '#src/api/organization-scope.js';
|
||||
|
||||
const randomId = () => generateStandardId(4);
|
||||
|
||||
describe('organization roles', () => {
|
||||
it('should fail if the name of the new organization role already exists', async () => {
|
||||
const name = 'test' + randomId();
|
||||
await roleApi.create({ name });
|
||||
const response = await roleApi.create({ name }).catch((error: unknown) => error);
|
||||
|
||||
assert(response instanceof HTTPError);
|
||||
|
||||
const { statusCode, body: raw } = response.response;
|
||||
const body: unknown = JSON.parse(String(raw));
|
||||
expect(statusCode).toBe(400);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe('entity.duplicate_value_of_unique_field');
|
||||
});
|
||||
|
||||
it('should be able to create a role with some scopes', async () => {
|
||||
const name = 'test' + randomId();
|
||||
const [scope1, scope2] = await Promise.all([
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
]);
|
||||
const scopeIds = [scope1.id, scope2.id];
|
||||
const role = await roleApi.create({ name, scopeIds });
|
||||
|
||||
expect(role).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
name,
|
||||
})
|
||||
);
|
||||
|
||||
// TODO: Check scopes under a role after API is implemented
|
||||
await Promise.all([scopeApi.delete(scope1.id), scopeApi.delete(scope2.id)]);
|
||||
});
|
||||
|
||||
it('should get organization roles successfully', async () => {
|
||||
const [name1, name2] = ['test' + randomId(), 'test' + randomId()];
|
||||
await roleApi.create({ name: name1, description: 'A test organization role.' });
|
||||
await roleApi.create({ name: name2 });
|
||||
const roles = await roleApi.getList();
|
||||
|
||||
expect(roles).toContainEqual(
|
||||
expect.objectContaining({ name: name1, description: 'A test organization role.' })
|
||||
);
|
||||
expect(roles).toContainEqual(expect.objectContaining({ name: name2, description: null }));
|
||||
});
|
||||
|
||||
it('should get organization roles with pagination', async () => {
|
||||
// Add 20 roles to exceed the default page size
|
||||
await Promise.all(
|
||||
Array.from({ length: 30 }).map(async () => roleApi.create({ name: 'test' + randomId() }))
|
||||
);
|
||||
|
||||
const roles = await roleApi.getList();
|
||||
expect(roles).toHaveLength(20);
|
||||
|
||||
const roles2 = await roleApi.getList(
|
||||
new URLSearchParams({
|
||||
page: '2',
|
||||
page_size: '10',
|
||||
})
|
||||
);
|
||||
expect(roles2.length).toBeGreaterThanOrEqual(10);
|
||||
expect(roles2[0]?.id).not.toBeFalsy();
|
||||
expect(roles2[0]?.id).toBe(roles[10]?.id);
|
||||
});
|
||||
|
||||
it('should be able to create and get organization roles by id', async () => {
|
||||
const createdRole = await roleApi.create({ name: 'test' + randomId() });
|
||||
const role = await roleApi.get(createdRole.id);
|
||||
|
||||
expect(role).toStrictEqual(createdRole);
|
||||
});
|
||||
|
||||
it('should fail when try to get an organization role that does not exist', async () => {
|
||||
const response = await roleApi.get('0').catch((error: unknown) => error);
|
||||
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should be able to update organization role', async () => {
|
||||
const createdRole = await roleApi.create({ name: 'test' + randomId() });
|
||||
const newName = 'test' + randomId();
|
||||
const role = await roleApi.update(createdRole.id, {
|
||||
name: newName,
|
||||
description: 'test description.',
|
||||
});
|
||||
expect(role).toStrictEqual({
|
||||
...createdRole,
|
||||
name: newName,
|
||||
description: 'test description.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to delete organization role', async () => {
|
||||
const createdRole = await roleApi.create({ name: 'test' + randomId() });
|
||||
await roleApi.delete(createdRole.id);
|
||||
const response = await roleApi.get(createdRole.id).catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should fail when try to delete an organization role that does not exist', async () => {
|
||||
const response = await roleApi.delete('0').catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
|
@ -21,9 +21,11 @@ export type GeneratedSchema<
|
|||
Key extends string,
|
||||
CreateSchema extends Partial<SchemaLike<Key>>,
|
||||
Schema extends SchemaLike<Key>,
|
||||
Table extends string = string,
|
||||
TableSingular extends string = string,
|
||||
> = Readonly<{
|
||||
table: string;
|
||||
tableSingular: string;
|
||||
table: Table;
|
||||
tableSingular: TableSingular;
|
||||
fields: {
|
||||
[key in Key]: string;
|
||||
};
|
||||
|
|
|
@ -126,7 +126,13 @@ export const generateSchema = ({ name, comments, fields }: TableWithType) => {
|
|||
'',
|
||||
`export const ${camelcase(name, {
|
||||
pascalCase: true,
|
||||
})}: GeneratedSchema<${modelName}Keys, ${databaseEntryType}, ${modelName}> = Object.freeze({`,
|
||||
})}: GeneratedSchema<
|
||||
${modelName}Keys,
|
||||
${databaseEntryType},
|
||||
${modelName},
|
||||
'${name}',
|
||||
'${pluralize(name, 1)}'
|
||||
> = Object.freeze({`,
|
||||
` table: '${name}',`,
|
||||
` tableSingular: '${pluralize(name, 1)}',`,
|
||||
' fields: {',
|
||||
|
|
|
@ -3262,6 +3262,9 @@ importers:
|
|||
pg-protocol:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
pluralize:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
qrcode:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
|
@ -3338,6 +3341,9 @@ importers:
|
|||
'@types/oidc-provider':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
'@types/pluralize':
|
||||
specifier: ^0.0.31
|
||||
version: 0.0.31
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2
|
||||
|
@ -17150,7 +17156,6 @@ packages:
|
|||
/pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
|
|
Loading…
Reference in a new issue