mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
Merge pull request #4659 from logto-io/gao-org-apis-5
feat(core): organization - user relation apis
This commit is contained in:
commit
718053739c
53 changed files with 570 additions and 206 deletions
|
@ -3,7 +3,7 @@ import { sql } from 'slonik';
|
|||
|
||||
export const buildGetTotalRowCountWithPool =
|
||||
(pool: CommonQueryMethods, table: string) => async () => {
|
||||
// Postgres returns a biging for count(*), which is then converted to a string by query library.
|
||||
// Postgres returns a bigint for count(*), which is then converted to a string by query library.
|
||||
// We need to convert it to a number.
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
|
@ -15,7 +15,7 @@ export const buildGetTotalRowCountWithPool =
|
|||
|
||||
export const getTotalRowCountWithPool =
|
||||
(pool: CommonQueryMethods) => async (table: IdentifierSqlToken) => {
|
||||
// Postgres returns a biging for count(*), which is then converted to a string by query library.
|
||||
// Postgres returns a bigint for count(*), which is then converted to a string by query library.
|
||||
// We need to convert it to a number.
|
||||
const { count } = await pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
OrganizationScopes,
|
||||
OrganizationRoleScopeRelations,
|
||||
Users,
|
||||
OrganizationUserRelations,
|
||||
OrganizationRoleUserRelations,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
|
||||
|
@ -33,7 +35,15 @@ export default class OrganizationQueries extends SchemaQueries<
|
|||
OrganizationScopes
|
||||
),
|
||||
/** Queries for organization - user relations. */
|
||||
users: new RelationQueries(this.pool, 'organization_user_relations', Organizations, Users),
|
||||
users: new RelationQueries(this.pool, OrganizationUserRelations.table, Organizations, Users),
|
||||
/** Queries for organization - organization role - user relations. */
|
||||
rolesUsers: new RelationQueries(
|
||||
this.pool,
|
||||
OrganizationRoleUserRelations.table,
|
||||
Organizations,
|
||||
OrganizationRoles,
|
||||
Users
|
||||
),
|
||||
};
|
||||
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
|
|
|
@ -95,7 +95,7 @@ const usersLibraries = {
|
|||
),
|
||||
} satisfies Partial<Libraries['users']>;
|
||||
|
||||
const adminUserRoutes = await pickDefault(import('./admin-user.js'));
|
||||
const adminUserRoutes = await pickDefault(import('./basics.js'));
|
||||
|
||||
describe('adminUserRoutes', () => {
|
||||
const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
|
|
@ -14,11 +14,10 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { transpileUserMfaVerifications } from '#src/utils/user.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function adminUserRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||
) {
|
||||
export default function adminUserBasicsRoutes<T extends AuthedRouter>(...args: RouterInitArgs<T>) {
|
||||
const [router, { queries, libraries }] = args;
|
||||
const {
|
||||
oidcModelInstances: { revokeInstanceByUserId },
|
||||
users: {
|
13
packages/core/src/routes/admin-user/index.ts
Normal file
13
packages/core/src/routes/admin-user/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
import adminUserBasicsRoutes from './basics.js';
|
||||
import adminUserRoleRoutes from './role.js';
|
||||
import adminUserSearchRoutes from './search.js';
|
||||
import adminUserSocialRoutes from './social.js';
|
||||
|
||||
export default function adminUserRoutes<T extends AuthedRouter>(...args: RouterInitArgs<T>) {
|
||||
adminUserBasicsRoutes(...args);
|
||||
adminUserRoleRoutes(...args);
|
||||
adminUserSearchRoutes(...args);
|
||||
adminUserSocialRoutes(...args);
|
||||
}
|
|
@ -35,7 +35,7 @@ const { findRolesByRoleIds } = roles;
|
|||
|
||||
const tenantContext = new MockTenant(undefined, { usersRoles, users, roles });
|
||||
|
||||
const roleRoutes = await pickDefault(import('./admin-user-role.js'));
|
||||
const roleRoutes = await pickDefault(import('./role.js'));
|
||||
|
||||
describe('user role routes', () => {
|
||||
const roleRequester = createRequester({ authedRoutes: roleRoutes, tenantContext });
|
|
@ -10,7 +10,7 @@ import koaRoleRlsErrorHandler from '#src/middleware/koa-role-rls-error-handler.j
|
|||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { parseSearchParamsForSearch } from '#src/utils/search.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function adminUserRoleRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
|
@ -60,7 +60,7 @@ const usersLibraries = {
|
|||
),
|
||||
} satisfies Partial<Libraries['users']>;
|
||||
|
||||
const adminUserRoutes = await pickDefault(import('./admin-user-search.js'));
|
||||
const adminUserRoutes = await pickDefault(import('./search.js'));
|
||||
|
||||
describe('adminUserRoutes', () => {
|
||||
const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
|
|
@ -6,7 +6,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import { parseSearchParamsForSearch } from '#src/utils/search.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function adminUserSearchRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
|
@ -70,7 +70,7 @@ const mockedConnectors = {
|
|||
|
||||
const { findUserById, updateUserById, deleteUserIdentity } = mockedQueries.users;
|
||||
|
||||
const adminUserSocialRoutes = await pickDefault(import('./admin-user-social.js'));
|
||||
const adminUserSocialRoutes = await pickDefault(import('./social.js'));
|
||||
|
||||
describe('Admin user social identities APIs', () => {
|
||||
const tenantContext = new MockTenant(undefined, mockedQueries, mockedConnectors, {
|
|
@ -12,7 +12,7 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function adminUserSocialRoutes<T extends AuthedRouter>(
|
||||
...[router, tenant]: RouterInitArgs<T>
|
|
@ -10,10 +10,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
|
||||
import koaAuth from '../middleware/koa-auth/index.js';
|
||||
|
||||
import adminUserRoleRoutes from './admin-user-role.js';
|
||||
import adminUserSearchRoutes from './admin-user-search.js';
|
||||
import adminUserSocialRoutes from './admin-user-social.js';
|
||||
import adminUserRoutes from './admin-user.js';
|
||||
import adminUserRoutes from './admin-user/index.js';
|
||||
import applicationRoleRoutes from './application-role.js';
|
||||
import applicationRoutes from './application.js';
|
||||
import authnRoutes from './authn.js';
|
||||
|
@ -25,9 +22,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 organizationRoutes from './organization/index.js';
|
||||
import resourceRoutes from './resource.js';
|
||||
import roleRoutes from './role.js';
|
||||
import roleScopeRoutes from './role.scope.js';
|
||||
|
@ -54,9 +49,6 @@ const createRouters = (tenant: TenantContext) => {
|
|||
resourceRoutes(managementRouter, tenant);
|
||||
signInExperiencesRoutes(managementRouter, tenant);
|
||||
adminUserRoutes(managementRouter, tenant);
|
||||
adminUserSearchRoutes(managementRouter, tenant);
|
||||
adminUserRoleRoutes(managementRouter, tenant);
|
||||
adminUserSocialRoutes(managementRouter, tenant);
|
||||
logRoutes(managementRouter, tenant);
|
||||
roleRoutes(managementRouter, tenant);
|
||||
roleScopeRoutes(managementRouter, tenant);
|
||||
|
@ -67,8 +59,6 @@ const createRouters = (tenant: TenantContext) => {
|
|||
userAssetsRoutes(managementRouter, tenant);
|
||||
domainRoutes(managementRouter, tenant);
|
||||
organizationRoutes(managementRouter, tenant);
|
||||
organizationScopeRoutes(managementRouter, tenant);
|
||||
organizationRoleRoutes(managementRouter, tenant);
|
||||
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
wellKnownRoutes(anonymousRouter, tenant);
|
||||
|
|
110
packages/core/src/routes/organization/index.ts
Normal file
110
packages/core/src/routes/organization/index.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { OrganizationRoles, Organizations } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
import organizationRoleRoutes from './roles.js';
|
||||
import organizationScopeRoutes from './scopes.js';
|
||||
|
||||
export default function organizationRoutes<T extends AuthedRouter>(...args: RouterInitArgs<T>) {
|
||||
const [
|
||||
originalRouter,
|
||||
{
|
||||
queries: { organizations, users },
|
||||
},
|
||||
] = args;
|
||||
const router = new SchemaRouter(Organizations, new SchemaActions(organizations));
|
||||
|
||||
router.addRelationRoutes(organizations.relations.users);
|
||||
|
||||
// Manually add these routes since I don't want to over-engineer the `SchemaRouter`
|
||||
// MARK: Organization - user - organization role relation routes
|
||||
const params = Object.freeze({ id: z.string().min(1), userId: z.string().min(1) } as const);
|
||||
const pathname = '/:id/users/:userId/roles';
|
||||
|
||||
router.get(
|
||||
pathname,
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
params: z.object(params),
|
||||
response: OrganizationRoles.guard.array(),
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id, userId } = ctx.guard.params;
|
||||
|
||||
// Ensure both the organization and the role exist
|
||||
await Promise.all([organizations.findById(id), users.findUserById(userId)]);
|
||||
|
||||
const [totalCount, entities] = await organizations.relations.rolesUsers.getEntities(
|
||||
OrganizationRoles,
|
||||
{
|
||||
organizationId: id,
|
||||
userId,
|
||||
},
|
||||
ctx.pagination
|
||||
);
|
||||
|
||||
ctx.pagination.totalCount = totalCount;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
pathname,
|
||||
koaGuard({
|
||||
params: z.object(params),
|
||||
body: z.object({ roleIds: z.string().min(1).array().nonempty() }),
|
||||
status: [201, 404, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id, userId } = ctx.guard.params;
|
||||
const { roleIds } = ctx.guard.body;
|
||||
|
||||
// Ensure membership
|
||||
if (!(await organizations.relations.users.exists(id, userId))) {
|
||||
throw new RequestError({ code: 'organization.require_membership', status: 422 });
|
||||
}
|
||||
|
||||
await organizations.relations.rolesUsers.insert(
|
||||
...roleIds.map<[string, string, string]>((roleId) => [id, roleId, userId])
|
||||
);
|
||||
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
`${pathname}/:roleId`,
|
||||
koaGuard({
|
||||
params: z.object({ ...params, roleId: z.string().min(1) }),
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id, roleId, userId } = ctx.guard.params;
|
||||
|
||||
await organizations.relations.rolesUsers.delete({
|
||||
organizationId: id,
|
||||
organizationRoleId: roleId,
|
||||
userId,
|
||||
});
|
||||
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// MARK: Mount sub-routes
|
||||
organizationRoleRoutes(...args);
|
||||
organizationScopeRoutes(...args);
|
||||
|
||||
// Add routes to the router
|
||||
originalRouter.use(router.routes());
|
||||
}
|
|
@ -3,7 +3,6 @@ import {
|
|||
type OrganizationRole,
|
||||
type OrganizationRoleKeys,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes,
|
||||
} from '@logto/schemas';
|
||||
import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
@ -12,7 +11,7 @@ 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';
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
class OrganizationRoleActions extends SchemaActions<
|
||||
OrganizationRoleKeys,
|
||||
|
@ -85,7 +84,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
router.addRelationRoutes(OrganizationScopes, rolesScopes, 'scopes');
|
||||
router.addRelationRoutes(rolesScopes, 'scopes');
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
|
@ -2,7 +2,7 @@ import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
|||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { OrganizationScopeActions } from './organization-scopes.js';
|
||||
import { OrganizationScopeActions } from './scopes.js';
|
||||
|
||||
describe('OrganizationScopeActions', () => {
|
||||
it('should throw RequestError if UniqueIntegrityConstraintViolationError is thrown inside', async () => {
|
|
@ -9,7 +9,7 @@ import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from './types.js';
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
export class OrganizationScopeActions extends SchemaActions<
|
||||
OrganizationScopeKeys,
|
|
@ -1,18 +0,0 @@
|
|||
import { Organizations } from '@logto/schemas';
|
||||
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from './types.js';
|
||||
|
||||
export default function organizationRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
originalRouter,
|
||||
{
|
||||
queries: { organizations },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(Organizations, new SchemaActions(organizations));
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { conditionalSql } from '@logto/shared';
|
||||
import { type KeysToCamelCase } from '@silverhand/essentials';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
@ -17,8 +18,13 @@ type CamelCaseIdObject<T extends string> = KeysToCamelCase<{
|
|||
[Key in `${T}_id`]: string;
|
||||
}>;
|
||||
|
||||
type GetEntitiesOptions = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Query class for relation tables that connect several tables by their entry ids.
|
||||
* Query class for relation tables that connect several tables by their entity ids.
|
||||
*
|
||||
* 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:
|
||||
|
@ -41,13 +47,13 @@ type CamelCaseIdObject<T extends string> = KeysToCamelCase<{
|
|||
* );
|
||||
* ```
|
||||
*
|
||||
* To get all entries for a specific table, we can use the {@link RelationQueries.getEntries} method:
|
||||
* To get all entities for a specific table, we can use the {@link RelationQueries.getEntities} method:
|
||||
*
|
||||
* ```ts
|
||||
* await userGroupRelations.getEntries(Users, { groupId: 'group-id-1' });
|
||||
* await userGroupRelations.getEntities(Users, { groupId: 'group-id-1' });
|
||||
* ```
|
||||
*
|
||||
* This will return all entries for the `users` table that are connected to the
|
||||
* This will return all entities for the `users` table that are connected to the
|
||||
* group with the id `group-id-1`.
|
||||
*/
|
||||
export default class RelationQueries<
|
||||
|
@ -75,11 +81,14 @@ export default class RelationQueries<
|
|||
}
|
||||
|
||||
/**
|
||||
* Insert new entries into the relation table.
|
||||
* Insert new entities into the relation table.
|
||||
*
|
||||
* Each entry must contain the same number of ids as the number of relations, and
|
||||
* Each entity 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.
|
||||
*
|
||||
* @param data Entities to insert.
|
||||
* @returns A Promise that resolves to the query result.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups);
|
||||
|
@ -91,9 +100,6 @@ export default class RelationQueries<
|
|||
* ['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`
|
||||
|
@ -117,7 +123,7 @@ export default class RelationQueries<
|
|||
/**
|
||||
* Delete a relation from the relation table.
|
||||
*
|
||||
* @param data The ids of the entries to delete. The keys must be in camel case
|
||||
* @param data The ids of the entities to delete. The keys must be in camel case
|
||||
* and end with `Id`.
|
||||
* @returns A Promise that resolves to the query result.
|
||||
*
|
||||
|
@ -141,29 +147,34 @@ export default class RelationQueries<
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all entries for a specific schema that are connected to the given ids.
|
||||
* Get all entities for a specific schema that are connected to the given ids.
|
||||
*
|
||||
* @param forSchema The schema to get the entries for.
|
||||
* @param where Other ids to filter the entries by. The keys must be in camel case
|
||||
* @param forSchema The schema to get the entities for.
|
||||
* @param where Other ids to filter the entities by. The keys must be in camel case
|
||||
* and end with `Id`.
|
||||
* @returns A Promise that resolves an array of entries of the given schema.
|
||||
* @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.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups);
|
||||
*
|
||||
* userGroupRelations.getEntries(Users, { groupId: 'group-id-1' });
|
||||
* userGroupRelations.getEntities(Users, { groupId: 'group-id-1' });
|
||||
* // With pagination
|
||||
* userGroupRelations.getEntities(Users, { groupId: 'group-id-1' }, { limit: 10, offset: 20 });
|
||||
* ```
|
||||
*/
|
||||
async getEntries<S extends Schemas[number]>(
|
||||
async getEntities<S extends Schemas[number]>(
|
||||
forSchema: S,
|
||||
where: CamelCaseIdObject<Exclude<Schemas[number]['tableSingular'], S['tableSingular']>>
|
||||
): Promise<ReadonlyArray<InferSchema<S>>> {
|
||||
where: CamelCaseIdObject<Exclude<Schemas[number]['tableSingular'], S['tableSingular']>>,
|
||||
options: GetEntitiesOptions = {}
|
||||
): Promise<[totalCount: number, entities: ReadonlyArray<InferSchema<S>>]> {
|
||||
const { limit, offset } = options;
|
||||
const snakeCaseWhere = snakecaseKeys(where);
|
||||
const forTable = sql.identifier([forSchema.table]);
|
||||
|
||||
const { rows } = await this.pool.query<InferSchema<S>>(sql`
|
||||
select ${forTable}.*
|
||||
const mainSql = sql`
|
||||
from ${this.table}
|
||||
join ${forTable} on ${sql.identifier([
|
||||
this.relationTable,
|
||||
|
@ -174,9 +185,51 @@ export default class RelationQueries<
|
|||
([column, value]) => sql`${sql.identifier([column])} = ${value}`
|
||||
),
|
||||
sql` and `
|
||||
)};
|
||||
`);
|
||||
)}
|
||||
`;
|
||||
|
||||
return rows;
|
||||
const [{ count }, { rows }] = await Promise.all([
|
||||
// Postgres returns a bigint for count(*), which is then converted to a string by query library.
|
||||
// We need to convert it to a number.
|
||||
this.pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
${mainSql}
|
||||
`),
|
||||
this.pool.query<InferSchema<S>>(sql`
|
||||
select ${forTable}.* ${mainSql}
|
||||
${conditionalSql(limit, (limit) => sql`limit ${limit}`)}
|
||||
${conditionalSql(offset, (offset) => sql`offset ${offset}`)}
|
||||
`),
|
||||
]);
|
||||
|
||||
return [Number(count), rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a relation exists.
|
||||
*
|
||||
* @param ids The ids of the entities to check. The order of the ids must match the order of the relations.
|
||||
* @returns A Promise that resolves to `true` if the relation exists, otherwise `false`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups);
|
||||
*
|
||||
* userGroupRelations.exists('user-id-1', 'group-id-1');
|
||||
* ```
|
||||
*/
|
||||
async exists(...ids: readonly string[] & { length: Length }) {
|
||||
return this.pool.exists(sql`
|
||||
select
|
||||
from ${this.table}
|
||||
where ${sql.join(
|
||||
this.schemas.map(
|
||||
({ tableSingular }, index) =>
|
||||
sql`${sql.identifier([tableSingular + '_id'])} = ${ids[index] ?? sql`null`}`
|
||||
),
|
||||
sql` and `
|
||||
)}
|
||||
limit 1
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,13 +50,15 @@ export class SchemaActions<
|
|||
*
|
||||
* @param pagination The request pagination info parsed from `koa-pagination`. The
|
||||
* function should honor the pagination info and return the correct entities.
|
||||
* @returns A tuple of `[count, entities]`. `count` is the total count of entities
|
||||
* in the database; `entities` is the list of entities to be returned.
|
||||
* @returns A tuple of `[totalCount, entities]`. `totalCount` is the total count of
|
||||
* entities in the database; `entities` is the list of entities to be returned.
|
||||
*/
|
||||
public async get({
|
||||
limit,
|
||||
offset,
|
||||
}: Pick<Pagination, 'limit' | 'offset'>): Promise<[count: number, entities: readonly Schema[]]> {
|
||||
}: Pick<Pagination, 'limit' | 'offset'>): Promise<
|
||||
[totalCount: number, entries: readonly Schema[]]
|
||||
> {
|
||||
return Promise.all([this.queries.findTotalNumber(), this.queries.findAll(limit, offset)]);
|
||||
}
|
||||
|
||||
|
@ -268,21 +270,21 @@ export default class SchemaRouter<
|
|||
* singular form with `Ids` suffix. For example, if the relation schema's table name is
|
||||
* `organization_roles`, the `[relationSchemaIds]` will be `organizationRoleIds`.
|
||||
*
|
||||
* @param relationSchema The schema of the relation to be added.
|
||||
* @param relationQueries The queries for the relation.
|
||||
* @param relationQueries The queries class for the relation.
|
||||
* @param pathname The pathname of the relation. If not provided, it will be
|
||||
* the camel case of the relation schema's table name.
|
||||
* @see {@link RelationQueries} for the `relationQueries` configuration.
|
||||
*/
|
||||
addRelationRoutes<
|
||||
RelationKey extends string,
|
||||
RelationCreateSchema extends Partial<SchemaLike<RelationKey> & { id: string }>,
|
||||
RelationSchema extends SchemaLike<RelationKey> & { id: string },
|
||||
RelationCreateSchema extends Partial<SchemaLike<string> & { id: string }>,
|
||||
RelationSchema extends SchemaLike<string> & { id: string },
|
||||
>(
|
||||
relationSchema: GeneratedSchema<RelationKey, RelationCreateSchema, RelationSchema>,
|
||||
relationQueries: RelationQueries<[typeof this.schema, typeof relationSchema]>,
|
||||
pathname = tableToPathname(relationSchema.table)
|
||||
relationQueries: RelationQueries<
|
||||
[typeof this.schema, GeneratedSchema<string, RelationCreateSchema, RelationSchema>]
|
||||
>,
|
||||
pathname = tableToPathname(relationQueries.schemas[1].table)
|
||||
) {
|
||||
const relationSchema = relationQueries.schemas[1];
|
||||
const columns = {
|
||||
schemaId: camelCaseSchemaId(this.schema),
|
||||
relationSchemaId: camelCaseSchemaId(relationSchema),
|
||||
|
@ -291,6 +293,7 @@ export default class SchemaRouter<
|
|||
|
||||
this.get(
|
||||
`/:id/${pathname}`,
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: relationSchema.guard.array(),
|
||||
|
@ -302,9 +305,16 @@ export default class SchemaRouter<
|
|||
// Ensure that the main entry exists
|
||||
await this.actions.getById(id);
|
||||
|
||||
ctx.body = await relationQueries.getEntries(relationSchema, {
|
||||
[columns.schemaId]: id,
|
||||
});
|
||||
const [totalCount, entities] = await relationQueries.getEntities(
|
||||
relationSchema,
|
||||
{
|
||||
[columns.schemaId]: id,
|
||||
},
|
||||
ctx.pagination
|
||||
);
|
||||
|
||||
ctx.pagination.totalCount = totalCount;
|
||||
ctx.body = entities;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
@ -314,8 +324,7 @@ export default class SchemaRouter<
|
|||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: z.object({ [columns.relationSchemaIds]: z.string().min(1).array().nonempty() }),
|
||||
response: relationSchema.guard.array(),
|
||||
status: [200, 404, 422],
|
||||
status: [201, 404, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
@ -326,8 +335,7 @@ export default class SchemaRouter<
|
|||
await relationQueries.insert(
|
||||
...(relationIds?.map<[string, string]>((relationId) => [id, relationId]) ?? [])
|
||||
);
|
||||
|
||||
ctx.body = await relationQueries.getEntries(relationSchema, { [columns.schemaId]: id });
|
||||
ctx.status = 201;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -15,8 +15,10 @@ class OrganizationRoleApi extends ApiFactory<
|
|||
await authedAdminApi.post(`${this.path}/${id}/scopes`, { json: { organizationScopeIds } });
|
||||
}
|
||||
|
||||
async getScopes(id: string): Promise<OrganizationScope[]> {
|
||||
return authedAdminApi.get(`${this.path}/${id}/scopes`).json<OrganizationScope[]>();
|
||||
async getScopes(id: string, searchParams?: URLSearchParams): Promise<OrganizationScope[]> {
|
||||
return authedAdminApi
|
||||
.get(`${this.path}/${id}/scopes`, { searchParams })
|
||||
.json<OrganizationScope[]>();
|
||||
}
|
||||
|
||||
async deleteScope(id: string, scopeId: string): Promise<void> {
|
||||
|
|
|
@ -1,11 +1,36 @@
|
|||
import { type Organization } from '@logto/schemas';
|
||||
import { type Role, type Organization } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from './api.js';
|
||||
import { ApiFactory } from './factory.js';
|
||||
|
||||
class OrganizationApi extends ApiFactory<Organization, { name: string; description?: string }> {
|
||||
constructor() {
|
||||
super('organizations');
|
||||
}
|
||||
|
||||
async addUsers(id: string, userIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
|
||||
}
|
||||
|
||||
async getUsers(id: string): Promise<Organization[]> {
|
||||
return authedAdminApi.get(`${this.path}/${id}/users`).json<Organization[]>();
|
||||
}
|
||||
|
||||
async deleteUser(id: string, userId: string): Promise<void> {
|
||||
await authedAdminApi.delete(`${this.path}/${id}/users/${userId}`);
|
||||
}
|
||||
|
||||
async addUserRoles(id: string, userId: string, roleIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/users/${userId}/roles`, { json: { roleIds } });
|
||||
}
|
||||
|
||||
async getUserRoles(id: string, userId: string): Promise<Role[]> {
|
||||
return authedAdminApi.get(`${this.path}/${id}/users/${userId}/roles`).json<Role[]>();
|
||||
}
|
||||
|
||||
async deleteUserRole(id: string, userId: string, roleId: string): Promise<void> {
|
||||
await authedAdminApi.delete(`${this.path}/${id}/users/${userId}/roles/${roleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** API methods for operating organizations. */
|
||||
|
|
|
@ -31,33 +31,30 @@ describe('organization role APIs', () => {
|
|||
|
||||
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 organizationScopeIds = [scope1.id, scope2.id];
|
||||
const scopes = await Promise.all(
|
||||
Array.from({ length: 20 }).map(async () => scopeApi.create({ name: 'test' + randomId() }))
|
||||
);
|
||||
const organizationScopeIds = scopes.map((scope) => scope.id);
|
||||
const role = await roleApi.create({ name, organizationScopeIds });
|
||||
|
||||
expect(role).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
name,
|
||||
const roleScopes = await roleApi.getScopes(role.id);
|
||||
expect(roleScopes).toHaveLength(20);
|
||||
|
||||
// Check pagination
|
||||
const roleScopes2 = await roleApi.getScopes(
|
||||
role.id,
|
||||
new URLSearchParams({
|
||||
page: '2',
|
||||
page_size: '10',
|
||||
})
|
||||
);
|
||||
|
||||
// Check scopes under a role after API is implemented
|
||||
const scopes = await roleApi.getScopes(role.id);
|
||||
expect(scopes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: scope1.name,
|
||||
})
|
||||
);
|
||||
expect(scopes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: scope2.name,
|
||||
})
|
||||
);
|
||||
expect(roleScopes2).toHaveLength(10);
|
||||
expect(roleScopes2[0]?.id).not.toBeFalsy();
|
||||
expect(roleScopes2[0]?.id).toBe(roleScopes[10]?.id);
|
||||
|
||||
await Promise.all([scopeApi.delete(scope1.id), scopeApi.delete(scope2.id)]);
|
||||
await Promise.all(scopes.map(async (scope) => scopeApi.delete(scope.id)));
|
||||
await roleApi.delete(role.id);
|
||||
});
|
||||
|
||||
it('should get organization roles successfully', async () => {
|
||||
|
@ -141,34 +138,7 @@ describe('organization role APIs', () => {
|
|||
});
|
||||
|
||||
describe('organization role - scope relations', () => {
|
||||
it('should be able to get scopes of a role', async () => {
|
||||
const [role, scope1, scope2] = await Promise.all([
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
]);
|
||||
await roleApi.addScopes(role.id, [scope1.id, scope2.id]);
|
||||
const scopes = await roleApi.getScopes(role.id);
|
||||
|
||||
expect(scopes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: scope1.name,
|
||||
})
|
||||
);
|
||||
expect(scopes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: scope2.name,
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
roleApi.delete(role.id),
|
||||
scopeApi.delete(scope1.id),
|
||||
scopeApi.delete(scope2.id),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to add scopes to a role', async () => {
|
||||
it('should be able to add and get scopes of a role', async () => {
|
||||
const [role, scope1, scope2] = await Promise.all([
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
|
|
|
@ -1,78 +1,162 @@
|
|||
import assert from 'node:assert';
|
||||
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { createUser, deleteUser } from '#src/api/admin-user.js';
|
||||
import { roleApi } from '#src/api/organization-role.js';
|
||||
import { organizationApi } from '#src/api/organization.js';
|
||||
|
||||
describe('organizations', () => {
|
||||
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();
|
||||
const randomId = () => generateStandardId(4);
|
||||
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test', description: 'A test organization.' })
|
||||
);
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test2', description: null })
|
||||
);
|
||||
});
|
||||
// Add additional layer of describe to run tests in band
|
||||
describe('organization APIs', () => {
|
||||
describe('organizations', () => {
|
||||
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();
|
||||
|
||||
it('should get organizations with pagination', async () => {
|
||||
// Add 20 organizations to exceed the default page size
|
||||
await Promise.all(
|
||||
Array.from({ length: 30 }).map(async () => organizationApi.create({ name: 'test' }))
|
||||
);
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test', description: 'A test organization.' })
|
||||
);
|
||||
expect(organizations).toContainEqual(
|
||||
expect.objectContaining({ name: 'test2', description: null })
|
||||
);
|
||||
|
||||
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.',
|
||||
await Promise.all(
|
||||
organizations.map(async (organization) => organizationApi.delete(organization.id))
|
||||
);
|
||||
});
|
||||
expect(organization).toStrictEqual({
|
||||
...createdOrganization,
|
||||
name: 'test2',
|
||||
description: 'test description.',
|
||||
|
||||
it('should get organizations with pagination', async () => {
|
||||
// Add organizations to exceed the default page size
|
||||
const allOrganizations = 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);
|
||||
|
||||
await Promise.all(
|
||||
allOrganizations.map(async (organization) => organizationApi.delete(organization.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);
|
||||
await organizationApi.delete(createdOrganization.id);
|
||||
});
|
||||
|
||||
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.',
|
||||
});
|
||||
await organizationApi.delete(createdOrganization.id);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
describe('organization - user relations', () => {
|
||||
it('should be able to add and get organization users', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const [user1, user2] = await Promise.all([
|
||||
createUser({ username: 'test' + randomId() }),
|
||||
createUser({ username: 'test' + randomId() }),
|
||||
]);
|
||||
|
||||
await organizationApi.addUsers(organization.id, [user1.id, user2.id]);
|
||||
const users = await organizationApi.getUsers(organization.id);
|
||||
expect(users).toContainEqual(expect.objectContaining({ id: user1.id }));
|
||||
expect(users).toContainEqual(expect.objectContaining({ id: user2.id }));
|
||||
await Promise.all([
|
||||
organizationApi.delete(organization.id),
|
||||
deleteUser(user1.id),
|
||||
deleteUser(user2.id),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to delete organization user', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const user = await createUser({ username: 'test' + randomId() });
|
||||
|
||||
await organizationApi.addUsers(organization.id, [user.id]);
|
||||
await organizationApi.deleteUser(organization.id, user.id);
|
||||
const users = await organizationApi.getUsers(organization.id);
|
||||
expect(users).not.toContainEqual(user);
|
||||
await Promise.all([organizationApi.delete(organization.id), deleteUser(user.id)]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
describe('organization - user - organization role relation routes', () => {
|
||||
it("should be able to add and get user's organization roles", async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const user = await createUser({ username: 'test' + randomId() });
|
||||
const [role1, role2] = await Promise.all([
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
]);
|
||||
|
||||
const response = await organizationApi
|
||||
.addUserRoles(organization.id, user.id, [role1.id, role2.id])
|
||||
.catch((error: unknown) => error);
|
||||
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.statusCode).toBe(422);
|
||||
expect(JSON.parse(String(response.response.body))).toMatchObject(
|
||||
expect.objectContaining({ code: 'organization.require_membership' })
|
||||
);
|
||||
|
||||
await organizationApi.addUsers(organization.id, [user.id]);
|
||||
await organizationApi.addUserRoles(organization.id, user.id, [role1.id, role2.id]);
|
||||
const roles = await organizationApi.getUserRoles(organization.id, user.id);
|
||||
expect(roles).toContainEqual(expect.objectContaining({ id: role1.id }));
|
||||
expect(roles).toContainEqual(expect.objectContaining({ id: role2.id }));
|
||||
await Promise.all([
|
||||
organizationApi.delete(organization.id),
|
||||
deleteUser(user.id),
|
||||
roleApi.delete(role1.id),
|
||||
roleApi.delete(role2.id),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/de/errors/organization.ts
Normal file
6
packages/phrases/src/locales/de/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
5
packages/phrases/src/locales/en/errors/organization.ts
Normal file
5
packages/phrases/src/locales/en/errors/organization.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const organization = {
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/es/errors/organization.ts
Normal file
6
packages/phrases/src/locales/es/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/fr/errors/organization.ts
Normal file
6
packages/phrases/src/locales/fr/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/it/errors/organization.ts
Normal file
6
packages/phrases/src/locales/it/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/ja/errors/organization.ts
Normal file
6
packages/phrases/src/locales/ja/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/ko/errors/organization.ts
Normal file
6
packages/phrases/src/locales/ko/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
6
packages/phrases/src/locales/ru/errors/organization.ts
Normal file
6
packages/phrases/src/locales/ru/errors/organization.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
|
@ -8,6 +8,7 @@ import hook from './hook.js';
|
|||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
import organization from './organization.js';
|
||||
import password from './password.js';
|
||||
import request from './request.js';
|
||||
import resource from './resource.js';
|
||||
|
@ -44,6 +45,7 @@ const errors = {
|
|||
domain,
|
||||
subscription,
|
||||
application,
|
||||
organization,
|
||||
};
|
||||
|
||||
export default Object.freeze(errors);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const organization = {
|
||||
/** UNTRANSLATED */
|
||||
require_membership: 'The user must be a member of the organization to proceed.',
|
||||
};
|
||||
|
||||
export default Object.freeze(organization);
|
Loading…
Add table
Reference in a new issue