0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor(core): update relation queries

This commit is contained in:
Gao Sun 2024-06-19 14:21:15 +08:00
parent 71ba7c4cc6
commit b543356bb3
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
13 changed files with 91 additions and 48 deletions

View file

@ -151,19 +151,28 @@ export const createApplicationLibrary = (queries: Queries) => {
) => { ) => {
if (organizationScopes) { if (organizationScopes) {
await userConsentOrganizationScopes.insert( await userConsentOrganizationScopes.insert(
...organizationScopes.map<[string, string]>((scope) => [applicationId, scope]) ...organizationScopes.map((scope) => ({
applicationId,
organizationScopeId: scope,
}))
); );
} }
if (resourceScopes) { if (resourceScopes) {
await userConsentResourceScopes.insert( await userConsentResourceScopes.insert(
...resourceScopes.map<[string, string]>((scope) => [applicationId, scope]) ...resourceScopes.map((scope) => ({
applicationId,
scopeId: scope,
}))
); );
} }
if (organizationResourceScopes) { if (organizationResourceScopes) {
await userConsentOrganizationResourceScopes.insert( await userConsentOrganizationResourceScopes.insert(
...organizationResourceScopes.map<[string, string]>((scope) => [applicationId, scope]) ...organizationResourceScopes.map((scope) => ({
applicationId,
scopeId: scope,
}))
); );
} }

View file

@ -82,7 +82,10 @@ export class OrganizationInvitationLibrary {
if (organizationRoleIds?.length) { if (organizationRoleIds?.length) {
await organizationQueries.relations.invitationsRoles.insert( await organizationQueries.relations.invitationsRoles.insert(
...organizationRoleIds.map<[string, string]>((roleId) => [invitation.id, roleId]) ...organizationRoleIds.map((roleId) => ({
organizationInvitationId: invitation.id,
organizationRoleId: roleId,
}))
); );
} }
@ -165,15 +168,18 @@ export class OrganizationInvitationLibrary {
}); });
} }
await organizationQueries.relations.users.insert([entity.organizationId, acceptedUserId]); await organizationQueries.relations.users.insert({
organizationId: entity.organizationId,
userId: acceptedUserId,
});
if (entity.organizationRoles.length > 0) { if (entity.organizationRoles.length > 0) {
await organizationQueries.relations.rolesUsers.insert( await organizationQueries.relations.rolesUsers.insert(
...entity.organizationRoles.map<[string, string, string]>((role) => [ ...entity.organizationRoles.map((role) => ({
entity.organizationId, organizationId: entity.organizationId,
role.id, organizationRoleId: role.id,
acceptedUserId, userId: acceptedUserId,
]) }))
); );
} }
break; break;

View file

@ -155,18 +155,18 @@ export const createUserLibrary = (queries: Queries) => {
if (organizations.length > 0) { if (organizations.length > 0) {
await organizationQueries.relations.users.insert( await organizationQueries.relations.users.insert(
...organizations.map<[string, string]>(({ organizationId }) => [ ...organizations.map(({ organizationId }) => ({
organizationId, organizationId,
user.id, userId: user.id,
]) }))
); );
const data = organizations.flatMap(({ organizationId, organizationRoleIds }) => const data = organizations.flatMap(({ organizationId, organizationRoleIds }) =>
organizationRoleIds.map<[string, string, string]>((organizationRoleId) => [ organizationRoleIds.map((organizationRoleId) => ({
organizationId, organizationId,
organizationRoleId, organizationRoleId,
user.id, userId: user.id,
]) }))
); );
if (data.length > 0) { if (data.length > 0) {
await organizationQueries.relations.rolesUsers.insert(...data); await organizationQueries.relations.rolesUsers.insert(...data);

View file

@ -226,7 +226,12 @@ export const buildHandler: (
if (organizationId) { if (organizationId) {
// Check membership // Check membership
if (!(await queries.organizations.relations.users.exists(organizationId, account.accountId))) { if (
!(await queries.organizations.relations.users.exists({
organizationId,
userId: account.accountId,
}))
) {
const error = new AccessDenied('user is not a member of the organization'); const error = new AccessDenied('user is not a member of the organization');
error.statusCode = 403; error.statusCode = 403;
throw error; throw error;

View file

@ -195,5 +195,5 @@ export const isOrganizationConsentedToApplication = async (
accountId: string, accountId: string,
organizationId: string organizationId: string
) => { ) => {
return userConsentOrganizations.exists(applicationId, accountId, organizationId); return userConsentOrganizations.exists({ applicationId, userId: accountId, organizationId });
}; };

View file

@ -101,11 +101,11 @@ export default function applicationUserConsentOrganizationRoutes<T extends Manag
await validateUserConsentOrganizationMembership(userId, organizationIds); await validateUserConsentOrganizationMembership(userId, organizationIds);
await applications.userConsentOrganizations.insert( await applications.userConsentOrganizations.insert(
...organizationIds.map<[string, string, string]>((organizationId) => [ ...organizationIds.map((organizationId) => ({
id, applicationId: id,
userId, userId,
organizationId, organizationId,
]) }))
); );
ctx.status = 201; ctx.status = 201;

View file

@ -177,12 +177,12 @@ async function handleSubmitRegister(
// Create tenant organization and assign the admin user to it. // Create tenant organization and assign the admin user to it.
// This is only for Cloud integration tests and data alignment, OSS still uses the legacy Management API user role. // This is only for Cloud integration tests and data alignment, OSS still uses the legacy Management API user role.
const organizationId = getTenantOrganizationId(defaultTenantId); const organizationId = getTenantOrganizationId(defaultTenantId);
await organizations.relations.users.insert([organizationId, id]); await organizations.relations.users.insert({ organizationId, userId: id });
await organizations.relations.rolesUsers.insert([ await organizations.relations.rolesUsers.insert({
organizationId, organizationId,
getTenantRole(TenantRole.Admin).id, organizationRoleId: getTenantRole(TenantRole.Admin).id,
id, userId: id,
]); });
} }
await assignInteractionResults(ctx, provider, { login: { accountId: id } }); await assignInteractionResults(ctx, provider, { login: { accountId: id } });

View file

@ -71,11 +71,11 @@ export default function consentRoutes<T extends IRouterParamContext>(
await validateUserConsentOrganizationMembership(userId, organizationIds); await validateUserConsentOrganizationMembership(userId, organizationIds);
await queries.applications.userConsentOrganizations.insert( await queries.applications.userConsentOrganizations.insert(
...organizationIds.map<[string, string, string]>((organizationId) => [ ...organizationIds.map((organizationId) => ({
applicationId, applicationId,
userId, userId,
organizationId, organizationId,
]) }))
); );
} }

View file

@ -127,8 +127,8 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
const { userIds, organizationRoleIds } = ctx.guard.body; const { userIds, organizationRoleIds } = ctx.guard.body;
await organizations.relations.rolesUsers.insert( await organizations.relations.rolesUsers.insert(
...organizationRoleIds.flatMap<[string, string, string]>((roleId) => ...organizationRoleIds.flatMap((roleId) =>
userIds.map<[string, string, string]>((userId) => [id, roleId, userId]) userIds.map((userId) => ({ organizationId: id, organizationRoleId: roleId, userId }))
) )
); );

View file

@ -24,7 +24,7 @@ export default function userRoleRelationRoutes(
const { id, userId } = ctx.guard.params; const { id, userId } = ctx.guard.params;
// Ensure membership // Ensure membership
if (!(await organizations.relations.users.exists(id, userId))) { if (!(await organizations.relations.users.exists({ organizationId: id, userId }))) {
throw new RequestError({ code: 'organization.require_membership', status: 422 }); throw new RequestError({ code: 'organization.require_membership', status: 422 });
} }
@ -87,7 +87,11 @@ export default function userRoleRelationRoutes(
const { organizationRoleIds } = ctx.guard.body; const { organizationRoleIds } = ctx.guard.body;
await organizations.relations.rolesUsers.insert( await organizations.relations.rolesUsers.insert(
...organizationRoleIds.map<[string, string, string]>((roleId) => [id, roleId, userId]) ...organizationRoleIds.map((roleId) => ({
organizationId: id,
organizationRoleId: roleId,
userId,
}))
); );
ctx.status = 201; ctx.status = 201;

View file

@ -101,13 +101,19 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
if (organizationScopeIds.length > 0) { if (organizationScopeIds.length > 0) {
await rolesScopes.insert( await rolesScopes.insert(
...organizationScopeIds.map<[string, string]>((id) => [role.id, id]) ...organizationScopeIds.map((id) => ({
organizationRoleId: role.id,
organizationScopeId: id,
}))
); );
} }
if (resourceScopeIds.length > 0) { if (resourceScopeIds.length > 0) {
await rolesResourceScopes.insert( await rolesResourceScopes.insert(
...resourceScopeIds.map<[string, string]>((id) => [role.id, id]) ...resourceScopeIds.map((id) => ({
organizationRoleId: role.id,
scopeId: id,
}))
); );
} }

View file

@ -1,5 +1,5 @@
import { type SchemaLike, type Table } from '@logto/shared'; import { type SchemaLike, type Table } from '@logto/shared';
import { type KeysToCamelCase } from '@silverhand/essentials'; import { type CamelCase, type KeysToCamelCase } from '@silverhand/essentials';
import { sql, type CommonQueryMethods } from '@silverhand/slonik'; import { sql, type CommonQueryMethods } from '@silverhand/slonik';
import snakecaseKeys from 'snakecase-keys'; import snakecaseKeys from 'snakecase-keys';
import { type z } from 'zod'; import { type z } from 'zod';
@ -9,6 +9,10 @@ import { DeletionError } from '#src/errors/SlonikError/index.js';
import { conditionalSql } from './sql.js'; import { conditionalSql } from './sql.js';
const camelCase = <T extends string>(string: T): CamelCase<T> =>
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
string.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase()) as CamelCase<T>;
type AtLeast2<T extends unknown[]> = `${T['length']}` extends '0' | '1' ? never : T; type AtLeast2<T extends unknown[]> = `${T['length']}` extends '0' | '1' ? never : T;
type TableInfo< type TableInfo<
@ -56,11 +60,11 @@ export type GetEntitiesOptions = {
* To insert a new relation, we can use the {@link RelationQueries.insert} method: * To insert a new relation, we can use the {@link RelationQueries.insert} method:
* *
* ```ts * ```ts
* await userGroupRelations.insert(['user-id-1', 'group-id-1']); * await userGroupRelations.insert({ userId: 'user-id-1', groupId: 'group-id-1' });
* // Insert multiple relations at once * // Insert multiple relations at once
* await userGroupRelations.insert( * await userGroupRelations.insert(
* ['user-id-1', 'group-id-1'], * { userId: 'user-id-1', groupId: 'group-id-1' },
* ['user-id-2', 'group-id-1'] * { userId: 'user-id-2', groupId: 'group-id-1' }
* ); * );
* ``` * ```
* *
@ -111,15 +115,15 @@ export default class RelationQueries<
* ```ts * ```ts
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups); * const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups);
* *
* userGroupRelations.insert(['user-id-1', 'group-id-1']); * userGroupRelations.insert({ userId: 'user-id-1', groupId: 'group-id-1' });
* // Insert multiple relations at once * // Insert multiple relations at once
* userGroupRelations.insert( * userGroupRelations.insert(
* ['user-id-1', 'group-id-1'], * { userId: 'user-id-1', groupId: 'group-id-1' },
* ['user-id-2', 'group-id-1'] * { userId: 'user-id-2', groupId: 'group-id-1' }
* ); * );
* ``` * ```
*/ */
async insert(...data: ReadonlyArray<string[] & { length: Length }>) { async insert(...data: ReadonlyArray<CamelCaseIdObject<Schemas[number]['tableSingular']>>) {
return this.pool.query(sql` return this.pool.query(sql`
insert into ${this.table} (${sql.join( insert into ${this.table} (${sql.join(
this.schemas.map(({ tableSingular }) => sql.identifier([tableSingular + '_id'])), this.schemas.map(({ tableSingular }) => sql.identifier([tableSingular + '_id'])),
@ -129,7 +133,10 @@ export default class RelationQueries<
data.map( data.map(
(relation) => (relation) =>
sql`(${sql.join( sql`(${sql.join(
relation.map((id) => sql`${id}`), this.schemas.map(
// @ts-expect-error `tableSingular` loses its type here
({ tableSingular }) => sql`${relation[camelCase(tableSingular + '_id')]}`
),
sql`, ` sql`, `
)})` )})`
), ),
@ -240,17 +247,20 @@ export default class RelationQueries<
* ```ts * ```ts
* const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups); * const userGroupRelations = new RelationQueries(pool, 'user_group_relations', Users, Groups);
* *
* userGroupRelations.exists('user-id-1', 'group-id-1'); * userGroupRelations.exists({ userId: 'user-id-1', groupId: 'group-id-1' });
* ``` * ```
*/ */
async exists(...ids: readonly string[] & { length: Length }) { async exists(ids: CamelCaseIdObject<Schemas[number]['tableSingular']>) {
return this.pool.exists(sql` return this.pool.exists(sql`
select select
from ${this.table} from ${this.table}
where ${sql.join( where ${sql.join(
this.schemas.map( this.schemas.map(
({ tableSingular }, index) => ({ tableSingular }) =>
sql`${sql.identifier([tableSingular + '_id'])} = ${ids[index] ?? sql`null`}` sql`${sql.identifier([tableSingular + '_id'])} = ${
// @ts-expect-error `tableSingular` loses its type here
ids[camelCase(tableSingular + '_id')]
}`
), ),
sql` and ` sql` and `
)} )}

View file

@ -251,7 +251,10 @@ export default class SchemaRouter<
} = ctx.guard; } = ctx.guard;
await relationQueries.insert( await relationQueries.insert(
...(relationIds?.map<[string, string]>((relationId) => [id, relationId]) ?? []) ...(relationIds?.map((relationId) => ({
[columns.schemaId]: id,
[columns.relationSchemaId]: relationId,
})) ?? [])
); );
appendHookContext(ctx, id); appendHookContext(ctx, id);
ctx.status = 201; ctx.status = 201;