From b543356bb3ffa75bd3518d50735aa4ea9812c8f3 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 19 Jun 2024 14:21:15 +0800 Subject: [PATCH] refactor(core): update relation queries --- packages/core/src/libraries/application.ts | 15 ++++++-- .../src/libraries/organization-invitation.ts | 20 +++++++---- packages/core/src/libraries/user.ts | 12 +++---- .../core/src/oidc/grants/refresh-token.ts | 7 +++- packages/core/src/oidc/resource.ts | 2 +- .../application-user-consent-organization.ts | 6 ++-- .../interaction/actions/submit-interaction.ts | 10 +++--- .../src/routes/interaction/consent/index.ts | 4 +-- .../core/src/routes/organization/index.ts | 4 +-- .../organization/index.user-role-relations.ts | 8 +++-- .../core/src/routes/organization/roles.ts | 10 ++++-- packages/core/src/utils/RelationQueries.ts | 36 ++++++++++++------- packages/core/src/utils/SchemaRouter.ts | 5 ++- 13 files changed, 91 insertions(+), 48 deletions(-) diff --git a/packages/core/src/libraries/application.ts b/packages/core/src/libraries/application.ts index 048cb0a65..bbb707b90 100644 --- a/packages/core/src/libraries/application.ts +++ b/packages/core/src/libraries/application.ts @@ -151,19 +151,28 @@ export const createApplicationLibrary = (queries: Queries) => { ) => { if (organizationScopes) { await userConsentOrganizationScopes.insert( - ...organizationScopes.map<[string, string]>((scope) => [applicationId, scope]) + ...organizationScopes.map((scope) => ({ + applicationId, + organizationScopeId: scope, + })) ); } if (resourceScopes) { await userConsentResourceScopes.insert( - ...resourceScopes.map<[string, string]>((scope) => [applicationId, scope]) + ...resourceScopes.map((scope) => ({ + applicationId, + scopeId: scope, + })) ); } if (organizationResourceScopes) { await userConsentOrganizationResourceScopes.insert( - ...organizationResourceScopes.map<[string, string]>((scope) => [applicationId, scope]) + ...organizationResourceScopes.map((scope) => ({ + applicationId, + scopeId: scope, + })) ); } diff --git a/packages/core/src/libraries/organization-invitation.ts b/packages/core/src/libraries/organization-invitation.ts index a89a8717f..e582ca6d0 100644 --- a/packages/core/src/libraries/organization-invitation.ts +++ b/packages/core/src/libraries/organization-invitation.ts @@ -82,7 +82,10 @@ export class OrganizationInvitationLibrary { if (organizationRoleIds?.length) { 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) { await organizationQueries.relations.rolesUsers.insert( - ...entity.organizationRoles.map<[string, string, string]>((role) => [ - entity.organizationId, - role.id, - acceptedUserId, - ]) + ...entity.organizationRoles.map((role) => ({ + organizationId: entity.organizationId, + organizationRoleId: role.id, + userId: acceptedUserId, + })) ); } break; diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 3cfbecfd9..5248442c1 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -155,18 +155,18 @@ export const createUserLibrary = (queries: Queries) => { if (organizations.length > 0) { await organizationQueries.relations.users.insert( - ...organizations.map<[string, string]>(({ organizationId }) => [ + ...organizations.map(({ organizationId }) => ({ organizationId, - user.id, - ]) + userId: user.id, + })) ); const data = organizations.flatMap(({ organizationId, organizationRoleIds }) => - organizationRoleIds.map<[string, string, string]>((organizationRoleId) => [ + organizationRoleIds.map((organizationRoleId) => ({ organizationId, organizationRoleId, - user.id, - ]) + userId: user.id, + })) ); if (data.length > 0) { await organizationQueries.relations.rolesUsers.insert(...data); diff --git a/packages/core/src/oidc/grants/refresh-token.ts b/packages/core/src/oidc/grants/refresh-token.ts index d0fbd4b87..d481cf13e 100644 --- a/packages/core/src/oidc/grants/refresh-token.ts +++ b/packages/core/src/oidc/grants/refresh-token.ts @@ -226,7 +226,12 @@ export const buildHandler: ( if (organizationId) { // 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'); error.statusCode = 403; throw error; diff --git a/packages/core/src/oidc/resource.ts b/packages/core/src/oidc/resource.ts index 48dd40860..ab51a0a18 100644 --- a/packages/core/src/oidc/resource.ts +++ b/packages/core/src/oidc/resource.ts @@ -195,5 +195,5 @@ export const isOrganizationConsentedToApplication = async ( accountId: string, organizationId: string ) => { - return userConsentOrganizations.exists(applicationId, accountId, organizationId); + return userConsentOrganizations.exists({ applicationId, userId: accountId, organizationId }); }; diff --git a/packages/core/src/routes/applications/application-user-consent-organization.ts b/packages/core/src/routes/applications/application-user-consent-organization.ts index 67fc3df42..575ea72a3 100644 --- a/packages/core/src/routes/applications/application-user-consent-organization.ts +++ b/packages/core/src/routes/applications/application-user-consent-organization.ts @@ -101,11 +101,11 @@ export default function applicationUserConsentOrganizationRoutes((organizationId) => [ - id, + ...organizationIds.map((organizationId) => ({ + applicationId: id, userId, organizationId, - ]) + })) ); ctx.status = 201; diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 53c5367dc..922d9e408 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -177,12 +177,12 @@ async function handleSubmitRegister( // 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. const organizationId = getTenantOrganizationId(defaultTenantId); - await organizations.relations.users.insert([organizationId, id]); - await organizations.relations.rolesUsers.insert([ + await organizations.relations.users.insert({ organizationId, userId: id }); + await organizations.relations.rolesUsers.insert({ organizationId, - getTenantRole(TenantRole.Admin).id, - id, - ]); + organizationRoleId: getTenantRole(TenantRole.Admin).id, + userId: id, + }); } await assignInteractionResults(ctx, provider, { login: { accountId: id } }); diff --git a/packages/core/src/routes/interaction/consent/index.ts b/packages/core/src/routes/interaction/consent/index.ts index a5e7a9011..dd21ca6c4 100644 --- a/packages/core/src/routes/interaction/consent/index.ts +++ b/packages/core/src/routes/interaction/consent/index.ts @@ -71,11 +71,11 @@ export default function consentRoutes( await validateUserConsentOrganizationMembership(userId, organizationIds); await queries.applications.userConsentOrganizations.insert( - ...organizationIds.map<[string, string, string]>((organizationId) => [ + ...organizationIds.map((organizationId) => ({ applicationId, userId, organizationId, - ]) + })) ); } diff --git a/packages/core/src/routes/organization/index.ts b/packages/core/src/routes/organization/index.ts index c34560ed4..9797cf775 100644 --- a/packages/core/src/routes/organization/index.ts +++ b/packages/core/src/routes/organization/index.ts @@ -127,8 +127,8 @@ export default function organizationRoutes( const { userIds, organizationRoleIds } = ctx.guard.body; await organizations.relations.rolesUsers.insert( - ...organizationRoleIds.flatMap<[string, string, string]>((roleId) => - userIds.map<[string, string, string]>((userId) => [id, roleId, userId]) + ...organizationRoleIds.flatMap((roleId) => + userIds.map((userId) => ({ organizationId: id, organizationRoleId: roleId, userId })) ) ); diff --git a/packages/core/src/routes/organization/index.user-role-relations.ts b/packages/core/src/routes/organization/index.user-role-relations.ts index 283156b93..1f7b4e105 100644 --- a/packages/core/src/routes/organization/index.user-role-relations.ts +++ b/packages/core/src/routes/organization/index.user-role-relations.ts @@ -24,7 +24,7 @@ export default function userRoleRelationRoutes( const { id, userId } = ctx.guard.params; // 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 }); } @@ -87,7 +87,11 @@ export default function userRoleRelationRoutes( const { organizationRoleIds } = ctx.guard.body; 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; diff --git a/packages/core/src/routes/organization/roles.ts b/packages/core/src/routes/organization/roles.ts index 2571f9a47..e53980a3c 100644 --- a/packages/core/src/routes/organization/roles.ts +++ b/packages/core/src/routes/organization/roles.ts @@ -101,13 +101,19 @@ export default function organizationRoleRoutes( if (organizationScopeIds.length > 0) { await rolesScopes.insert( - ...organizationScopeIds.map<[string, string]>((id) => [role.id, id]) + ...organizationScopeIds.map((id) => ({ + organizationRoleId: role.id, + organizationScopeId: id, + })) ); } if (resourceScopeIds.length > 0) { await rolesResourceScopes.insert( - ...resourceScopeIds.map<[string, string]>((id) => [role.id, id]) + ...resourceScopeIds.map((id) => ({ + organizationRoleId: role.id, + scopeId: id, + })) ); } diff --git a/packages/core/src/utils/RelationQueries.ts b/packages/core/src/utils/RelationQueries.ts index 739a7cec1..f294af1f4 100644 --- a/packages/core/src/utils/RelationQueries.ts +++ b/packages/core/src/utils/RelationQueries.ts @@ -1,5 +1,5 @@ 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 snakecaseKeys from 'snakecase-keys'; import { type z } from 'zod'; @@ -9,6 +9,10 @@ import { DeletionError } from '#src/errors/SlonikError/index.js'; import { conditionalSql } from './sql.js'; +const camelCase = (string: T): CamelCase => + // 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; + type AtLeast2 = `${T['length']}` extends '0' | '1' ? never : T; type TableInfo< @@ -56,11 +60,11 @@ export type GetEntitiesOptions = { * To insert a new relation, we can use the {@link RelationQueries.insert} method: * * ```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 * await userGroupRelations.insert( - * ['user-id-1', 'group-id-1'], - * ['user-id-2', 'group-id-1'] + * { userId: 'user-id-1', groupId: 'group-id-1' }, + * { userId: 'user-id-2', groupId: 'group-id-1' } * ); * ``` * @@ -111,15 +115,15 @@ export default class RelationQueries< * ```ts * 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 * userGroupRelations.insert( - * ['user-id-1', 'group-id-1'], - * ['user-id-2', 'group-id-1'] + * { userId: 'user-id-1', groupId: 'group-id-1' }, + * { userId: 'user-id-2', groupId: 'group-id-1' } * ); * ``` */ - async insert(...data: ReadonlyArray) { + async insert(...data: ReadonlyArray>) { return this.pool.query(sql` insert into ${this.table} (${sql.join( this.schemas.map(({ tableSingular }) => sql.identifier([tableSingular + '_id'])), @@ -129,7 +133,10 @@ export default class RelationQueries< data.map( (relation) => 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`, ` )})` ), @@ -240,17 +247,20 @@ export default class RelationQueries< * ```ts * 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) { 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`}` + ({ tableSingular }) => + sql`${sql.identifier([tableSingular + '_id'])} = ${ + // @ts-expect-error `tableSingular` loses its type here + ids[camelCase(tableSingular + '_id')] + }` ), sql` and ` )} diff --git a/packages/core/src/utils/SchemaRouter.ts b/packages/core/src/utils/SchemaRouter.ts index 0e797637a..19252da4f 100644 --- a/packages/core/src/utils/SchemaRouter.ts +++ b/packages/core/src/utils/SchemaRouter.ts @@ -251,7 +251,10 @@ export default class SchemaRouter< } = ctx.guard; await relationQueries.insert( - ...(relationIds?.map<[string, string]>((relationId) => [id, relationId]) ?? []) + ...(relationIds?.map((relationId) => ({ + [columns.schemaId]: id, + [columns.relationSchemaId]: relationId, + })) ?? []) ); appendHookContext(ctx, id); ctx.status = 201;