mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor(core): reorg organization queries
This commit is contained in:
parent
0f08608b96
commit
f9750ace90
4 changed files with 210 additions and 203 deletions
|
@ -34,7 +34,7 @@ import validatePresence from 'oidc-provider/lib/helpers/validate_presence.js';
|
|||
import instance from 'oidc-provider/lib/helpers/weak_cache.js';
|
||||
|
||||
import { type EnvSet } from '#src/env-set/index.js';
|
||||
import type OrganizationQueries from '#src/queries/organizations.js';
|
||||
import type OrganizationQueries from '#src/queries/organization/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { getSharedResourceServerData, reversedResourceAccessTokenTtl } from '../resource.js';
|
||||
|
|
203
packages/core/src/queries/organization/index.ts
Normal file
203
packages/core/src/queries/organization/index.ts
Normal file
|
@ -0,0 +1,203 @@
|
|||
import {
|
||||
type Organization,
|
||||
type CreateOrganization,
|
||||
type OrganizationKeys,
|
||||
Organizations,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes,
|
||||
OrganizationRoleScopeRelations,
|
||||
type OrganizationRoleKeys,
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
type OrganizationRoleWithScopes,
|
||||
OrganizationInvitations,
|
||||
type OrganizationInvitationKeys,
|
||||
type CreateOrganizationInvitation,
|
||||
type OrganizationInvitation,
|
||||
type OrganizationInvitationEntity,
|
||||
MagicLinks,
|
||||
OrganizationInvitationRoleRelations,
|
||||
} from '@logto/schemas';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { type SearchOptions, buildSearchSql, buildJsonObjectSql } from '#src/database/utils.js';
|
||||
import { TwoRelationsQueries } from '#src/utils/RelationQueries.js';
|
||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||
|
||||
import { RoleUserRelationQueries, UserRelationQueries } from './relations.js';
|
||||
|
||||
class OrganizationRolesQueries extends SchemaQueries<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
override async findById(id: string): Promise<Readonly<OrganizationRoleWithScopes>> {
|
||||
return this.pool.one(this.#findWithScopesSql(id));
|
||||
}
|
||||
|
||||
override async findAll(
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: SearchOptions<OrganizationRoleKeys>
|
||||
): Promise<[totalNumber: number, rows: Readonly<OrganizationRoleWithScopes[]>]> {
|
||||
return Promise.all([
|
||||
this.findTotalNumber(search),
|
||||
this.pool.any(this.#findWithScopesSql(undefined, limit, offset, search)),
|
||||
]);
|
||||
}
|
||||
|
||||
#findWithScopesSql(
|
||||
roleId?: string,
|
||||
limit = 1,
|
||||
offset = 0,
|
||||
search?: SearchOptions<OrganizationRoleKeys>
|
||||
) {
|
||||
const { table, fields } = convertToIdentifiers(OrganizationRoles, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleScopeRelations, true);
|
||||
const scopes = convertToIdentifiers(OrganizationScopes, true);
|
||||
|
||||
return sql<OrganizationRoleWithScopes>`
|
||||
select
|
||||
${table}.*,
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${scopes.fields.id},
|
||||
'name', ${scopes.fields.name}
|
||||
) order by ${scopes.fields.name}
|
||||
) filter (where ${scopes.fields.id} is not null),
|
||||
'[]'
|
||||
) as scopes -- left join could produce nulls as scopes
|
||||
from ${table}
|
||||
left join ${relations.table}
|
||||
on ${relations.fields.organizationRoleId} = ${fields.id}
|
||||
left join ${scopes.table}
|
||||
on ${relations.fields.organizationScopeId} = ${scopes.fields.id}
|
||||
${conditionalSql(roleId, (id) => {
|
||||
return sql`where ${fields.id} = ${id}`;
|
||||
})}
|
||||
${buildSearchSql(OrganizationRoles, search)}
|
||||
group by ${fields.id}
|
||||
${conditionalSql(this.orderBy, ({ field, order }) => {
|
||||
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
|
||||
})}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
class OrganizationInvitationsQueries extends SchemaQueries<
|
||||
OrganizationInvitationKeys,
|
||||
CreateOrganizationInvitation,
|
||||
OrganizationInvitation
|
||||
> {
|
||||
override async findById(id: string): Promise<Readonly<OrganizationInvitationEntity>> {
|
||||
return this.pool.one(this.#findEntity(id));
|
||||
}
|
||||
|
||||
override async findAll(
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: SearchOptions<OrganizationInvitationKeys>
|
||||
): Promise<[totalNumber: number, rows: Readonly<OrganizationInvitationEntity[]>]> {
|
||||
return Promise.all([
|
||||
this.findTotalNumber(search),
|
||||
this.pool.any(this.#findEntity(undefined, limit, offset, search)),
|
||||
]);
|
||||
}
|
||||
|
||||
#findEntity(
|
||||
invitationId?: string,
|
||||
limit = 1,
|
||||
offset = 0,
|
||||
search?: SearchOptions<OrganizationInvitationKeys>
|
||||
) {
|
||||
const { table, fields } = convertToIdentifiers(OrganizationInvitations, true);
|
||||
const magicLinks = convertToIdentifiers(MagicLinks, true);
|
||||
const roleRelations = convertToIdentifiers(OrganizationInvitationRoleRelations, true);
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
|
||||
return sql<OrganizationInvitationEntity>`
|
||||
select
|
||||
${table}.*,
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${roles.fields.id},
|
||||
'name', ${roles.fields.name}
|
||||
) order by ${roles.fields.name}
|
||||
) filter (where ${roles.fields.id} is not null),
|
||||
'[]'
|
||||
) as "organizationRoles", -- left join could produce nulls
|
||||
${buildJsonObjectSql(magicLinks.fields)} as "magicLink"
|
||||
from ${table}
|
||||
left join ${roleRelations.table}
|
||||
on ${roleRelations.fields.invitationId} = ${fields.id}
|
||||
left join ${roles.table}
|
||||
on ${roles.fields.id} = ${roleRelations.fields.organizationRoleId}
|
||||
left join ${magicLinks.table}
|
||||
on ${magicLinks.fields.id} = ${fields.magicLinkId}
|
||||
${conditionalSql(invitationId, (id) => {
|
||||
return sql`where ${fields.id} = ${id}`;
|
||||
})}
|
||||
${buildSearchSql(OrganizationInvitations, search)}
|
||||
group by ${fields.id}, ${magicLinks.fields.id}
|
||||
${conditionalSql(this.orderBy, ({ field, order }) => {
|
||||
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
|
||||
})}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default class OrganizationQueries extends SchemaQueries<
|
||||
OrganizationKeys,
|
||||
CreateOrganization,
|
||||
Organization
|
||||
> {
|
||||
/**
|
||||
* Queries for roles in the organization template.
|
||||
* @see {@link OrganizationRoles}
|
||||
*/
|
||||
roles = new OrganizationRolesQueries(this.pool, OrganizationRoles, {
|
||||
field: 'name',
|
||||
order: 'asc',
|
||||
});
|
||||
|
||||
/**
|
||||
* Queries for scopes in the organization template.
|
||||
* @see {@link OrganizationScopes}
|
||||
*/
|
||||
scopes = new SchemaQueries(this.pool, OrganizationScopes, { field: 'name', order: 'asc' });
|
||||
|
||||
/**
|
||||
* Queries for organization invitations.
|
||||
* @see {@link OrganizationInvitations}
|
||||
*/
|
||||
invitations = new OrganizationInvitationsQueries(this.pool, OrganizationInvitations, {
|
||||
field: 'createdAt',
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
/** Queries for relations that connected with organization-related entities. */
|
||||
relations = {
|
||||
/** Queries for organization role - organization scope relations. */
|
||||
rolesScopes: new TwoRelationsQueries(
|
||||
this.pool,
|
||||
OrganizationRoleScopeRelations.table,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes
|
||||
),
|
||||
/** Queries for organization - user relations. */
|
||||
users: new UserRelationQueries(this.pool),
|
||||
/** Queries for organization - organization role - user relations. */
|
||||
rolesUsers: new RoleUserRelationQueries(this.pool),
|
||||
};
|
||||
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
super(pool, Organizations);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
/* eslint-disable max-lines -- refactor in the next pull */
|
||||
import {
|
||||
type Organization,
|
||||
type CreateOrganization,
|
||||
type OrganizationKeys,
|
||||
Organizations,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes,
|
||||
|
@ -10,41 +6,24 @@ import {
|
|||
Users,
|
||||
OrganizationUserRelations,
|
||||
OrganizationRoleUserRelations,
|
||||
type OrganizationRoleKeys,
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
type OrganizationRoleWithScopes,
|
||||
type OrganizationWithRoles,
|
||||
type UserWithOrganizationRoles,
|
||||
type FeaturedUser,
|
||||
type OrganizationScopeEntity,
|
||||
OrganizationInvitations,
|
||||
type OrganizationInvitationKeys,
|
||||
type CreateOrganizationInvitation,
|
||||
type OrganizationInvitation,
|
||||
type OrganizationInvitationEntity,
|
||||
MagicLinks,
|
||||
OrganizationInvitationRoleRelations,
|
||||
} from '@logto/schemas';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import {
|
||||
type SearchOptions,
|
||||
buildSearchSql,
|
||||
expandFields,
|
||||
buildJsonObjectSql,
|
||||
} from '#src/database/utils.js';
|
||||
import { type SearchOptions, buildSearchSql, expandFields } from '#src/database/utils.js';
|
||||
import RelationQueries, {
|
||||
type GetEntitiesOptions,
|
||||
TwoRelationsQueries,
|
||||
} from '#src/utils/RelationQueries.js';
|
||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||
|
||||
import { type userSearchKeys } from './user.js';
|
||||
import { type userSearchKeys } from '../user.js';
|
||||
|
||||
/** The query class for the organization - user relation. */
|
||||
class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, typeof Users> {
|
||||
export class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, typeof Users> {
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
super(pool, OrganizationUserRelations.table, Organizations, Users);
|
||||
}
|
||||
|
@ -174,68 +153,7 @@ class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, type
|
|||
}
|
||||
}
|
||||
|
||||
class OrganizationRolesQueries extends SchemaQueries<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
override async findById(id: string): Promise<Readonly<OrganizationRoleWithScopes>> {
|
||||
return this.pool.one(this.#findWithScopesSql(id));
|
||||
}
|
||||
|
||||
override async findAll(
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: SearchOptions<OrganizationRoleKeys>
|
||||
): Promise<[totalNumber: number, rows: Readonly<OrganizationRoleWithScopes[]>]> {
|
||||
return Promise.all([
|
||||
this.findTotalNumber(search),
|
||||
this.pool.any(this.#findWithScopesSql(undefined, limit, offset, search)),
|
||||
]);
|
||||
}
|
||||
|
||||
#findWithScopesSql(
|
||||
roleId?: string,
|
||||
limit = 1,
|
||||
offset = 0,
|
||||
search?: SearchOptions<OrganizationRoleKeys>
|
||||
) {
|
||||
const { table, fields } = convertToIdentifiers(OrganizationRoles, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleScopeRelations, true);
|
||||
const scopes = convertToIdentifiers(OrganizationScopes, true);
|
||||
|
||||
return sql<OrganizationRoleWithScopes>`
|
||||
select
|
||||
${table}.*,
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${scopes.fields.id},
|
||||
'name', ${scopes.fields.name}
|
||||
) order by ${scopes.fields.name}
|
||||
) filter (where ${scopes.fields.id} is not null),
|
||||
'[]'
|
||||
) as scopes -- left join could produce nulls as scopes
|
||||
from ${table}
|
||||
left join ${relations.table}
|
||||
on ${relations.fields.organizationRoleId} = ${fields.id}
|
||||
left join ${scopes.table}
|
||||
on ${relations.fields.organizationScopeId} = ${scopes.fields.id}
|
||||
${conditionalSql(roleId, (id) => {
|
||||
return sql`where ${fields.id} = ${id}`;
|
||||
})}
|
||||
${buildSearchSql(OrganizationRoles, search)}
|
||||
group by ${fields.id}
|
||||
${conditionalSql(this.orderBy, ({ field, order }) => {
|
||||
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
|
||||
})}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
class RoleUserRelationQueries extends RelationQueries<
|
||||
export class RoleUserRelationQueries extends RelationQueries<
|
||||
[typeof Organizations, typeof OrganizationRoles, typeof Users]
|
||||
> {
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
|
@ -304,117 +222,3 @@ class RoleUserRelationQueries extends RelationQueries<
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
class OrganizationInvitationsQueries extends SchemaQueries<
|
||||
OrganizationInvitationKeys,
|
||||
CreateOrganizationInvitation,
|
||||
OrganizationInvitation
|
||||
> {
|
||||
override async findById(id: string): Promise<Readonly<OrganizationInvitationEntity>> {
|
||||
return this.pool.one(this.#findEntity(id));
|
||||
}
|
||||
|
||||
override async findAll(
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: SearchOptions<OrganizationInvitationKeys>
|
||||
): Promise<[totalNumber: number, rows: Readonly<OrganizationInvitationEntity[]>]> {
|
||||
return Promise.all([
|
||||
this.findTotalNumber(search),
|
||||
this.pool.any(this.#findEntity(undefined, limit, offset, search)),
|
||||
]);
|
||||
}
|
||||
|
||||
#findEntity(
|
||||
invitationId?: string,
|
||||
limit = 1,
|
||||
offset = 0,
|
||||
search?: SearchOptions<OrganizationInvitationKeys>
|
||||
) {
|
||||
const { table, fields } = convertToIdentifiers(OrganizationInvitations, true);
|
||||
const magicLinks = convertToIdentifiers(MagicLinks, true);
|
||||
const roleRelations = convertToIdentifiers(OrganizationInvitationRoleRelations, true);
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
|
||||
return sql<OrganizationInvitationEntity>`
|
||||
select
|
||||
${table}.*,
|
||||
coalesce(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', ${roles.fields.id},
|
||||
'name', ${roles.fields.name}
|
||||
) order by ${roles.fields.name}
|
||||
) filter (where ${roles.fields.id} is not null),
|
||||
'[]'
|
||||
) as "organizationRoles", -- left join could produce nulls
|
||||
${buildJsonObjectSql(magicLinks.fields)} as "magicLink"
|
||||
from ${table}
|
||||
left join ${roleRelations.table}
|
||||
on ${roleRelations.fields.invitationId} = ${fields.id}
|
||||
left join ${roles.table}
|
||||
on ${roles.fields.id} = ${roleRelations.fields.organizationRoleId}
|
||||
left join ${magicLinks.table}
|
||||
on ${magicLinks.fields.id} = ${fields.magicLinkId}
|
||||
${conditionalSql(invitationId, (id) => {
|
||||
return sql`where ${fields.id} = ${id}`;
|
||||
})}
|
||||
${buildSearchSql(OrganizationInvitations, search)}
|
||||
group by ${fields.id}, ${magicLinks.fields.id}
|
||||
${conditionalSql(this.orderBy, ({ field, order }) => {
|
||||
return sql`order by ${fields[field]} ${order === 'desc' ? sql`desc` : sql`asc`}`;
|
||||
})}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
export default class OrganizationQueries extends SchemaQueries<
|
||||
OrganizationKeys,
|
||||
CreateOrganization,
|
||||
Organization
|
||||
> {
|
||||
/**
|
||||
* Queries for roles in the organization template.
|
||||
* @see {@link OrganizationRoles}
|
||||
*/
|
||||
roles = new OrganizationRolesQueries(this.pool, OrganizationRoles, {
|
||||
field: 'name',
|
||||
order: 'asc',
|
||||
});
|
||||
|
||||
/**
|
||||
* Queries for scopes in the organization template.
|
||||
* @see {@link OrganizationScopes}
|
||||
*/
|
||||
scopes = new SchemaQueries(this.pool, OrganizationScopes, { field: 'name', order: 'asc' });
|
||||
|
||||
/**
|
||||
* Queries for organization invitations.
|
||||
* @see {@link OrganizationInvitations}
|
||||
*/
|
||||
invitations = new OrganizationInvitationsQueries(this.pool, OrganizationInvitations, {
|
||||
field: 'createdAt',
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
/** Queries for relations that connected with organization-related entities. */
|
||||
relations = {
|
||||
/** Queries for organization role - organization scope relations. */
|
||||
rolesScopes: new TwoRelationsQueries(
|
||||
this.pool,
|
||||
OrganizationRoleScopeRelations.table,
|
||||
OrganizationRoles,
|
||||
OrganizationScopes
|
||||
),
|
||||
/** Queries for organization - user relations. */
|
||||
users: new UserRelationQueries(this.pool),
|
||||
/** Queries for organization - organization role - user relations. */
|
||||
rolesUsers: new RoleUserRelationQueries(this.pool),
|
||||
};
|
||||
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
super(pool, Organizations);
|
||||
}
|
||||
}
|
||||
/* eslint-enable max-lines */
|
|
@ -13,7 +13,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 OrganizationQueries from '#src/queries/organizations.js';
|
||||
import OrganizationQueries from '#src/queries/organization/index.js';
|
||||
import { createPasscodeQueries } from '#src/queries/passcode.js';
|
||||
import { createResourceQueries } from '#src/queries/resource.js';
|
||||
import { createRolesScopesQueries } from '#src/queries/roles-scopes.js';
|
||||
|
|
Loading…
Add table
Reference in a new issue