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:
parent
71ba7c4cc6
commit
b543356bb3
13 changed files with 91 additions and 48 deletions
|
@ -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,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 });
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 } });
|
||||||
|
|
|
@ -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,
|
||||||
])
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 `
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue