refactor(core): Remove roleId indirection (no-changelog) (#8413)
This commit is contained in:
committed by
GitHub
parent
1affebd85e
commit
d6deceacde
@@ -1,5 +1,5 @@
|
||||
import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typeorm';
|
||||
import { Table, TableColumn } from 'typeorm';
|
||||
import { Table, TableColumn, TableForeignKey } from 'typeorm';
|
||||
import LazyPromise from 'p-lazy';
|
||||
import { Column } from './Column';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
@@ -118,6 +118,42 @@ export class DropColumns extends TableOperation {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ForeignKeyOperation extends TableOperation {
|
||||
protected foreignKey: TableForeignKey;
|
||||
|
||||
constructor(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
[referencedTableName, referencedColumnName]: [string, string],
|
||||
prefix: string,
|
||||
queryRunner: QueryRunner,
|
||||
customConstraintName?: string,
|
||||
) {
|
||||
super(tableName, prefix, queryRunner);
|
||||
|
||||
this.foreignKey = new TableForeignKey({
|
||||
name: customConstraintName,
|
||||
columnNames: [columnName],
|
||||
referencedTableName: `${prefix}${referencedTableName}`,
|
||||
referencedColumnNames: [referencedColumnName],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AddForeignKey extends ForeignKeyOperation {
|
||||
async execute(queryRunner: QueryRunner) {
|
||||
const { tableName, prefix } = this;
|
||||
return await queryRunner.createForeignKey(`${prefix}${tableName}`, this.foreignKey);
|
||||
}
|
||||
}
|
||||
|
||||
export class DropForeignKey extends ForeignKeyOperation {
|
||||
async execute(queryRunner: QueryRunner) {
|
||||
const { tableName, prefix } = this;
|
||||
return await queryRunner.dropForeignKey(`${prefix}${tableName}`, this.foreignKey);
|
||||
}
|
||||
}
|
||||
|
||||
class ModifyNotNull extends TableOperation {
|
||||
constructor(
|
||||
tableName: string,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { QueryRunner } from 'typeorm';
|
||||
import { Column } from './Column';
|
||||
import { AddColumns, AddNotNull, CreateTable, DropColumns, DropNotNull, DropTable } from './Table';
|
||||
import {
|
||||
AddColumns,
|
||||
AddForeignKey,
|
||||
AddNotNull,
|
||||
CreateTable,
|
||||
DropColumns,
|
||||
DropForeignKey,
|
||||
DropNotNull,
|
||||
DropTable,
|
||||
} from './Table';
|
||||
import { CreateIndex, DropIndex } from './Indices';
|
||||
|
||||
export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunner) => ({
|
||||
@@ -26,6 +35,36 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne
|
||||
dropIndex: (tableName: string, columnNames: string[], customIndexName?: string) =>
|
||||
new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName),
|
||||
|
||||
addForeignKey: (
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
reference: [string, string],
|
||||
customConstraintName?: string,
|
||||
) =>
|
||||
new AddForeignKey(
|
||||
tableName,
|
||||
columnName,
|
||||
reference,
|
||||
tablePrefix,
|
||||
queryRunner,
|
||||
customConstraintName,
|
||||
),
|
||||
|
||||
dropForeignKey: (
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
reference: [string, string],
|
||||
customConstraintName?: string,
|
||||
) =>
|
||||
new DropForeignKey(
|
||||
tableName,
|
||||
columnName,
|
||||
reference,
|
||||
tablePrefix,
|
||||
queryRunner,
|
||||
customConstraintName,
|
||||
),
|
||||
|
||||
addNotNull: (tableName: string, columnName: string) =>
|
||||
new AddNotNull(tableName, columnName, tablePrefix, queryRunner),
|
||||
dropNotNull: (tableName: string, columnName: string) =>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Column, Entity, OneToMany, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { IsString, Length } from 'class-validator';
|
||||
|
||||
import type { User } from './User';
|
||||
import type { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { SharedCredentials } from './SharedCredentials';
|
||||
import { WithTimestamps } from './AbstractEntity';
|
||||
import { idStringifier } from '../utils/transformers';
|
||||
|
||||
export type RoleNames = 'owner' | 'member' | 'user' | 'editor' | 'admin';
|
||||
export type RoleScopes = 'global' | 'workflow' | 'credential';
|
||||
|
||||
@Entity()
|
||||
@Unique(['scope', 'name'])
|
||||
export class Role extends WithTimestamps {
|
||||
@PrimaryColumn({ transformer: idStringifier })
|
||||
id: string;
|
||||
|
||||
@Column({ length: 32 })
|
||||
@IsString({ message: 'Role name must be of type string.' })
|
||||
@Length(1, 32, { message: 'Role name must be 1 to 32 characters long.' })
|
||||
name: RoleNames;
|
||||
|
||||
@Column()
|
||||
scope: RoleScopes;
|
||||
|
||||
@OneToMany('User', 'globalRole')
|
||||
globalForUsers: User[];
|
||||
|
||||
@OneToMany('SharedWorkflow', 'role')
|
||||
sharedWorkflows: SharedWorkflow[];
|
||||
|
||||
@OneToMany('SharedCredentials', 'role')
|
||||
sharedCredentials: SharedCredentials[];
|
||||
|
||||
get cacheKey() {
|
||||
return `role:${this.scope}:${this.name}`;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { CredentialsEntity } from './CredentialsEntity';
|
||||
import { User } from './User';
|
||||
import { Role } from './Role';
|
||||
import { WithTimestamps } from './AbstractEntity';
|
||||
|
||||
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
|
||||
|
||||
@Entity()
|
||||
export class SharedCredentials extends WithTimestamps {
|
||||
@ManyToOne('Role', 'sharedCredentials', { nullable: false })
|
||||
role: Role;
|
||||
|
||||
@Column()
|
||||
roleId: string;
|
||||
role: CredentialSharingRole;
|
||||
|
||||
@ManyToOne('User', 'sharedCredentials')
|
||||
user: User;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { WorkflowEntity } from './WorkflowEntity';
|
||||
import { User } from './User';
|
||||
import { Role } from './Role';
|
||||
import { WithTimestamps } from './AbstractEntity';
|
||||
|
||||
export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor' | 'workflow:user';
|
||||
|
||||
@Entity()
|
||||
export class SharedWorkflow extends WithTimestamps {
|
||||
@ManyToOne('Role', 'sharedWorkflows', { nullable: false })
|
||||
role: Role;
|
||||
|
||||
@Column()
|
||||
roleId: string;
|
||||
role: WorkflowSharingRole;
|
||||
|
||||
@ManyToOne('User', 'sharedWorkflows')
|
||||
user: User;
|
||||
|
||||
@@ -6,13 +6,11 @@ import {
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
BeforeInsert,
|
||||
} from 'typeorm';
|
||||
import { IsEmail, IsString, Length } from 'class-validator';
|
||||
import type { IUser, IUserSettings } from 'n8n-workflow';
|
||||
import { Role } from './Role';
|
||||
import type { SharedWorkflow } from './SharedWorkflow';
|
||||
import type { SharedCredentials } from './SharedCredentials';
|
||||
import { NoXss } from '../utils/customValidators';
|
||||
@@ -23,10 +21,13 @@ import type { AuthIdentity } from './AuthIdentity';
|
||||
import { ownerPermissions, memberPermissions, adminPermissions } from '@/permissions/roles';
|
||||
import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions';
|
||||
|
||||
const STATIC_SCOPE_MAP: Record<string, Scope[]> = {
|
||||
owner: ownerPermissions,
|
||||
member: memberPermissions,
|
||||
admin: adminPermissions,
|
||||
export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member';
|
||||
export type AssignableRole = Exclude<GlobalRole, 'global:owner'>;
|
||||
|
||||
const STATIC_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
|
||||
'global:owner': ownerPermissions,
|
||||
'global:member': memberPermissions,
|
||||
'global:admin': adminPermissions,
|
||||
};
|
||||
|
||||
@Entity()
|
||||
@@ -72,11 +73,8 @@ export class User extends WithTimestamps implements IUser {
|
||||
})
|
||||
settings: IUserSettings | null;
|
||||
|
||||
@ManyToOne('Role', 'globalForUsers', { nullable: false })
|
||||
globalRole: Role;
|
||||
|
||||
@Column()
|
||||
globalRoleId: string;
|
||||
role: GlobalRole;
|
||||
|
||||
@OneToMany('AuthIdentity', 'user')
|
||||
authIdentities: AuthIdentity[];
|
||||
@@ -127,11 +125,11 @@ export class User extends WithTimestamps implements IUser {
|
||||
|
||||
@AfterLoad()
|
||||
computeIsOwner(): void {
|
||||
this.isOwner = this.globalRole?.name === 'owner';
|
||||
this.isOwner = this.role === 'global:owner';
|
||||
}
|
||||
|
||||
get globalScopes() {
|
||||
return STATIC_SCOPE_MAP[this.globalRole?.name] ?? [];
|
||||
return STATIC_SCOPE_MAP[this.role] ?? [];
|
||||
}
|
||||
|
||||
hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): boolean {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { EventDestinations } from './EventDestinations';
|
||||
import { ExecutionEntity } from './ExecutionEntity';
|
||||
import { InstalledNodes } from './InstalledNodes';
|
||||
import { InstalledPackages } from './InstalledPackages';
|
||||
import { Role } from './Role';
|
||||
import { Settings } from './Settings';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
@@ -29,7 +28,6 @@ export const entities = {
|
||||
ExecutionEntity,
|
||||
InstalledNodes,
|
||||
InstalledPackages,
|
||||
Role,
|
||||
Settings,
|
||||
SharedCredentials,
|
||||
SharedWorkflow,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { MigrationContext, ReversibleMigration } from '@db/types';
|
||||
|
||||
type Table = 'user' | 'shared_workflow' | 'shared_credentials';
|
||||
|
||||
const idColumns: Record<Table, string> = {
|
||||
user: 'id',
|
||||
shared_credentials: 'credentialsId',
|
||||
shared_workflow: 'workflowId',
|
||||
};
|
||||
|
||||
const roleScopes: Record<Table, string> = {
|
||||
user: 'global',
|
||||
shared_credentials: 'credential',
|
||||
shared_workflow: 'workflow',
|
||||
};
|
||||
|
||||
const foreignKeySuffixes: Record<Table, string> = {
|
||||
user: 'f0609be844f9200ff4365b1bb3d',
|
||||
shared_credentials: 'c68e056637562000b68f480815a',
|
||||
shared_workflow: '3540da03964527aa24ae014b780',
|
||||
};
|
||||
|
||||
export class DropRoleMapping1705429061930 implements ReversibleMigration {
|
||||
async up(context: MigrationContext) {
|
||||
await this.migrateUp('user', context);
|
||||
await this.migrateUp('shared_workflow', context);
|
||||
await this.migrateUp('shared_credentials', context);
|
||||
}
|
||||
|
||||
async down(context: MigrationContext) {
|
||||
await this.migrateDown('shared_workflow', context);
|
||||
await this.migrateDown('shared_credentials', context);
|
||||
await this.migrateDown('user', context);
|
||||
}
|
||||
|
||||
private async migrateUp(
|
||||
table: Table,
|
||||
{
|
||||
dbType,
|
||||
escape,
|
||||
runQuery,
|
||||
schemaBuilder: { addNotNull, addColumns, dropColumns, dropForeignKey, column },
|
||||
tablePrefix,
|
||||
}: MigrationContext,
|
||||
) {
|
||||
await addColumns(table, [column('role').text]);
|
||||
|
||||
const roleTable = escape.tableName('role');
|
||||
const tableName = escape.tableName(table);
|
||||
const idColumn = escape.columnName(idColumns[table]);
|
||||
const roleColumnName = table === 'user' ? 'globalRoleId' : 'roleId';
|
||||
const roleColumn = escape.columnName(roleColumnName);
|
||||
const scope = roleScopes[table];
|
||||
const isMySQL = ['mariadb', 'mysqldb'].includes(dbType);
|
||||
const roleField = isMySQL ? `CONCAT('${scope}:', R.name)` : `'${scope}:' || R.name`;
|
||||
const subQuery = `
|
||||
SELECT ${roleField} as role, T.${idColumn} as id
|
||||
FROM ${tableName} T
|
||||
LEFT JOIN ${roleTable} R
|
||||
ON T.${roleColumn} = R.id and R.scope = '${scope}'`;
|
||||
const swQuery = isMySQL
|
||||
? `UPDATE ${tableName}, (${subQuery}) as mapping
|
||||
SET ${tableName}.role = mapping.role
|
||||
WHERE ${tableName}.${idColumn} = mapping.id`
|
||||
: `UPDATE ${tableName}
|
||||
SET role = mapping.role
|
||||
FROM (${subQuery}) as mapping
|
||||
WHERE ${tableName}.${idColumn} = mapping.id`;
|
||||
await runQuery(swQuery);
|
||||
|
||||
await addNotNull(table, 'role');
|
||||
|
||||
await dropForeignKey(
|
||||
table,
|
||||
roleColumnName,
|
||||
['role', 'id'],
|
||||
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
|
||||
);
|
||||
await dropColumns(table, [roleColumnName]);
|
||||
}
|
||||
|
||||
private async migrateDown(
|
||||
table: Table,
|
||||
{
|
||||
dbType,
|
||||
escape,
|
||||
runQuery,
|
||||
schemaBuilder: { addNotNull, addColumns, dropColumns, addForeignKey, column },
|
||||
tablePrefix,
|
||||
}: MigrationContext,
|
||||
) {
|
||||
const roleColumnName = table === 'user' ? 'globalRoleId' : 'roleId';
|
||||
await addColumns(table, [column(roleColumnName).int]);
|
||||
|
||||
const roleTable = escape.tableName('role');
|
||||
const tableName = escape.tableName(table);
|
||||
const idColumn = escape.columnName(idColumns[table]);
|
||||
const roleColumn = escape.columnName(roleColumnName);
|
||||
const scope = roleScopes[table];
|
||||
const isMySQL = ['mariadb', 'mysqldb'].includes(dbType);
|
||||
const roleField = isMySQL ? `CONCAT('${scope}:', R.name)` : `'${scope}:' || R.name`;
|
||||
const subQuery = `
|
||||
SELECT R.id as role_id, T.${idColumn} as id
|
||||
FROM ${tableName} T
|
||||
LEFT JOIN ${roleTable} R
|
||||
ON T.role = ${roleField} and R.scope = '${scope}'`;
|
||||
const query = isMySQL
|
||||
? `UPDATE ${tableName}, (${subQuery}) as mapping
|
||||
SET ${tableName}.${roleColumn} = mapping.role_id
|
||||
WHERE ${tableName}.${idColumn} = mapping.id`
|
||||
: `UPDATE ${tableName}
|
||||
SET ${roleColumn} = mapping.role_id
|
||||
FROM (${subQuery}) as mapping
|
||||
WHERE ${tableName}.${idColumn} = mapping.id`;
|
||||
await runQuery(query);
|
||||
|
||||
await addNotNull(table, roleColumnName);
|
||||
await addForeignKey(
|
||||
table,
|
||||
roleColumnName,
|
||||
['role', 'id'],
|
||||
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
|
||||
);
|
||||
|
||||
await dropColumns(table, ['role']);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-Execut
|
||||
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
|
||||
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
|
||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
||||
|
||||
export const mysqlMigrations: Migration[] = [
|
||||
InitialMigration1588157391238,
|
||||
@@ -105,4 +106,5 @@ export const mysqlMigrations: Migration[] = [
|
||||
AddWorkflowMetadata1695128658538,
|
||||
ModifyWorkflowHistoryNodesAndConnections1695829275184,
|
||||
AddGlobalAdminRole1700571993961,
|
||||
DropRoleMapping1705429061930,
|
||||
];
|
||||
|
||||
@@ -50,6 +50,7 @@ import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWor
|
||||
import { MigrateToTimestampTz1694091729095 } from './1694091729095-MigrateToTimestampTz';
|
||||
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
|
||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
||||
|
||||
export const postgresMigrations: Migration[] = [
|
||||
InitialMigration1587669153312,
|
||||
@@ -103,4 +104,5 @@ export const postgresMigrations: Migration[] = [
|
||||
MigrateToTimestampTz1694091729095,
|
||||
ModifyWorkflowHistoryNodesAndConnections1695829275184,
|
||||
AddGlobalAdminRole1700571993961,
|
||||
DropRoleMapping1705429061930,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DropRoleMapping1705429061930 as BaseMigration } from '../common/1705429061930-DropRoleMapping';
|
||||
|
||||
export class DropRoleMapping1705429061930 extends BaseMigration {
|
||||
transaction = false as const;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftD
|
||||
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
|
||||
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
|
||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||
import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping';
|
||||
|
||||
const sqliteMigrations: Migration[] = [
|
||||
InitialMigration1588102412422,
|
||||
@@ -99,6 +100,7 @@ const sqliteMigrations: Migration[] = [
|
||||
AddWorkflowMetadata1695128658538,
|
||||
ModifyWorkflowHistoryNodesAndConnections1695829275184,
|
||||
AddGlobalAdminRole1700571993961,
|
||||
DropRoleMapping1705429061930,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
||||
@@ -45,7 +45,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||
|
||||
type Select = Array<keyof CredentialsEntity>;
|
||||
|
||||
const defaultRelations = ['shared', 'shared.role', 'shared.user'];
|
||||
const defaultRelations = ['shared', 'shared.user'];
|
||||
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
|
||||
|
||||
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
|
||||
@@ -81,7 +81,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||
const findManyOptions: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };
|
||||
|
||||
if (withSharings) {
|
||||
findManyOptions.relations = ['shared', 'shared.user', 'shared.role'];
|
||||
findManyOptions.relations = ['shared', 'shared.user'];
|
||||
}
|
||||
|
||||
return await this.find(findManyOptions);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Service } from 'typedi';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
import type { RoleNames, RoleScopes } from '../entities/Role';
|
||||
import { Role } from '../entities/Role';
|
||||
import { User } from '../entities/User';
|
||||
|
||||
@Service()
|
||||
export class RoleRepository extends Repository<Role> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(Role, dataSource.manager);
|
||||
}
|
||||
|
||||
async findRole(scope: RoleScopes, name: RoleNames) {
|
||||
return await this.findOne({ where: { scope, name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }`
|
||||
*/
|
||||
async countUsersByRole() {
|
||||
type Row = { role_name: string; count: number | string };
|
||||
|
||||
const rows: Row[] = await this.createQueryBuilder('role')
|
||||
.select('role.name')
|
||||
.addSelect('COUNT(user.id)', 'count')
|
||||
.innerJoin(User, 'user', 'role.id = user.globalRoleId')
|
||||
.groupBy('role.name')
|
||||
.getRawMany();
|
||||
|
||||
return rows.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.role_name] = typeof item.count === 'number' ? item.count : parseInt(item.count, 10);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async getIdsInScopeWorkflowByNames(roleNames: RoleNames[]) {
|
||||
return await this.find({
|
||||
select: ['id'],
|
||||
where: { name: In(roleNames), scope: 'workflow' },
|
||||
}).then((role) => role.map(({ id }) => id));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Service } from 'typedi';
|
||||
import type { EntityManager, FindOptionsWhere } from 'typeorm';
|
||||
import type { EntityManager } from 'typeorm';
|
||||
import { DataSource, In, Not, Repository } from 'typeorm';
|
||||
import { SharedCredentials } from '../entities/SharedCredentials';
|
||||
import type { User } from '../entities/User';
|
||||
import type { Role } from '../entities/Role';
|
||||
|
||||
@Service()
|
||||
export class SharedCredentialsRepository extends Repository<SharedCredentials> {
|
||||
@@ -26,15 +25,15 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
|
||||
|
||||
async findByCredentialIds(credentialIds: string[]) {
|
||||
return await this.find({
|
||||
relations: ['credentials', 'role', 'user'],
|
||||
relations: ['credentials', 'user'],
|
||||
where: {
|
||||
credentialsId: In(credentialIds),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async makeOwnerOfAllCredentials(user: User, role: Role) {
|
||||
return await this.update({ userId: Not(user.id), roleId: role.id }, { user });
|
||||
async makeOwnerOfAllCredentials(user: User) {
|
||||
return await this.update({ userId: Not(user.id), role: 'credential:owner' }, { user });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,23 +41,22 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
|
||||
*/
|
||||
async getAccessibleCredentials(userId: string) {
|
||||
const sharings = await this.find({
|
||||
relations: ['role'],
|
||||
where: {
|
||||
userId,
|
||||
role: { name: In(['owner', 'user']), scope: 'credential' },
|
||||
role: In(['credential:owner', 'credential:user']),
|
||||
},
|
||||
});
|
||||
|
||||
return sharings.map((s) => s.credentialsId);
|
||||
}
|
||||
|
||||
async findSharings(userIds: string[], roleId?: string) {
|
||||
const where: FindOptionsWhere<SharedCredentials> = { userId: In(userIds) };
|
||||
|
||||
// If credential sharing is not enabled, get only credentials owned by this user
|
||||
if (roleId) where.roleId = roleId;
|
||||
|
||||
return await this.find({ where });
|
||||
async findOwnedSharings(userIds: string[]) {
|
||||
return await this.find({
|
||||
where: {
|
||||
userId: In(userIds),
|
||||
role: 'credential:owner',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByIds(transaction: EntityManager, sharedCredentialsIds: string[], user?: User) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Service } from 'typedi';
|
||||
import { DataSource, Repository, In, Not } from 'typeorm';
|
||||
import type { EntityManager, FindOptionsSelect, FindOptionsWhere } from 'typeorm';
|
||||
import { SharedWorkflow } from '../entities/SharedWorkflow';
|
||||
import type { EntityManager, FindManyOptions, FindOptionsWhere } from 'typeorm';
|
||||
import { SharedWorkflow, type WorkflowSharingRole } from '../entities/SharedWorkflow';
|
||||
import { type User } from '../entities/User';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { Role } from '../entities/Role';
|
||||
import type { WorkflowEntity } from '../entities/WorkflowEntity';
|
||||
|
||||
@Service()
|
||||
@@ -35,22 +34,29 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
|
||||
async findByWorkflowIds(workflowIds: string[]) {
|
||||
return await this.find({
|
||||
relations: ['role', 'user'],
|
||||
relations: ['user'],
|
||||
where: {
|
||||
role: {
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
},
|
||||
role: 'workflow:owner',
|
||||
workflowId: In(workflowIds),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findSharingRole(
|
||||
userId: string,
|
||||
workflowId: string,
|
||||
): Promise<WorkflowSharingRole | undefined> {
|
||||
return await this.findOne({
|
||||
select: ['role'],
|
||||
where: { workflowId, userId },
|
||||
}).then((shared) => shared?.role);
|
||||
}
|
||||
|
||||
async findSharing(
|
||||
workflowId: string,
|
||||
user: User,
|
||||
scope: Scope,
|
||||
{ roles, extraRelations }: { roles?: string[]; extraRelations?: string[] } = {},
|
||||
{ roles, extraRelations }: { roles?: WorkflowSharingRole[]; extraRelations?: string[] } = {},
|
||||
) {
|
||||
const where: FindOptionsWhere<SharedWorkflow> = {
|
||||
workflow: { id: workflowId },
|
||||
@@ -61,18 +67,18 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
}
|
||||
|
||||
if (roles) {
|
||||
where.role = { name: In(roles) };
|
||||
where.role = In(roles);
|
||||
}
|
||||
|
||||
const relations = ['workflow', 'role'];
|
||||
const relations = ['workflow'];
|
||||
|
||||
if (extraRelations) relations.push(...extraRelations);
|
||||
|
||||
return await this.findOne({ relations, where });
|
||||
}
|
||||
|
||||
async makeOwnerOfAllWorkflows(user: User, role: Role) {
|
||||
return await this.update({ userId: Not(user.id), roleId: role.id }, { user });
|
||||
async makeOwnerOfAllWorkflows(user: User) {
|
||||
return await this.update({ userId: Not(user.id), role: 'workflow:owner' }, { user });
|
||||
}
|
||||
|
||||
async getSharing(
|
||||
@@ -102,14 +108,14 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
): Promise<SharedWorkflow[]> {
|
||||
return await this.find({
|
||||
where: {
|
||||
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
|
||||
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }),
|
||||
...(options.workflowIds && { workflowId: In(options.workflowIds) }),
|
||||
},
|
||||
...(options.relations && { relations: options.relations }),
|
||||
});
|
||||
}
|
||||
|
||||
async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[], roleId: string) {
|
||||
async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[]) {
|
||||
const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => {
|
||||
if (user.isPending) {
|
||||
return acc;
|
||||
@@ -117,7 +123,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
const entity: Partial<SharedWorkflow> = {
|
||||
workflowId: workflow.id,
|
||||
userId: user.id,
|
||||
roleId,
|
||||
role: 'workflow:editor',
|
||||
};
|
||||
acc.push(this.create(entity));
|
||||
return acc;
|
||||
@@ -126,12 +132,15 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
return await transaction.save(newSharedWorkflows);
|
||||
}
|
||||
|
||||
async findWithFields(workflowIds: string[], { fields }: { fields: string[] }) {
|
||||
async findWithFields(
|
||||
workflowIds: string[],
|
||||
{ select }: Pick<FindManyOptions<SharedWorkflow>, 'select'>,
|
||||
) {
|
||||
return await this.find({
|
||||
where: {
|
||||
workflowId: In(workflowIds),
|
||||
},
|
||||
select: fields as FindOptionsSelect<SharedWorkflow>,
|
||||
select,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Service } from 'typedi';
|
||||
import type { EntityManager, FindManyOptions } from 'typeorm';
|
||||
import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
|
||||
import { User } from '../entities/User';
|
||||
import type { ListQuery } from '@/requests';
|
||||
|
||||
import { type GlobalRole, User } from '../entities/User';
|
||||
@Service()
|
||||
export class UserRepository extends Repository<User> {
|
||||
constructor(dataSource: DataSource) {
|
||||
@@ -13,7 +13,6 @@ export class UserRepository extends Repository<User> {
|
||||
async findManyByIds(userIds: string[]) {
|
||||
return await this.find({
|
||||
where: { id: In(userIds) },
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +27,6 @@ export class UserRepository extends Repository<User> {
|
||||
async findManyByEmail(emails: string[]) {
|
||||
return await this.find({
|
||||
where: { email: In(emails) },
|
||||
relations: ['globalRole'],
|
||||
select: ['email', 'password', 'id'],
|
||||
});
|
||||
}
|
||||
@@ -43,15 +41,30 @@ export class UserRepository extends Repository<User> {
|
||||
email,
|
||||
password: Not(IsNull()),
|
||||
},
|
||||
relations: ['authIdentities', 'globalRole'],
|
||||
relations: ['authIdentities'],
|
||||
});
|
||||
}
|
||||
|
||||
async toFindManyOptions(listQueryOptions?: ListQuery.Options, globalOwnerRoleId?: string) {
|
||||
/** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */
|
||||
async countUsersByRole() {
|
||||
const rows = (await this.createQueryBuilder()
|
||||
.select(['role', 'COUNT(role) as count'])
|
||||
.groupBy('role')
|
||||
.execute()) as Array<{ role: GlobalRole; count: string }>;
|
||||
return rows.reduce(
|
||||
(acc, row) => {
|
||||
acc[row.role] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<GlobalRole, number>,
|
||||
);
|
||||
}
|
||||
|
||||
async toFindManyOptions(listQueryOptions?: ListQuery.Options) {
|
||||
const findManyOptions: FindManyOptions<User> = {};
|
||||
|
||||
if (!listQueryOptions) {
|
||||
findManyOptions.relations = ['globalRole', 'authIdentities'];
|
||||
findManyOptions.relations = ['authIdentities'];
|
||||
return findManyOptions;
|
||||
}
|
||||
|
||||
@@ -62,7 +75,7 @@ export class UserRepository extends Repository<User> {
|
||||
if (skip) findManyOptions.skip = skip;
|
||||
|
||||
if (take && !select) {
|
||||
findManyOptions.relations = ['globalRole', 'authIdentities'];
|
||||
findManyOptions.relations = ['authIdentities'];
|
||||
}
|
||||
|
||||
if (take && select && !select?.id) {
|
||||
@@ -74,11 +87,8 @@ export class UserRepository extends Repository<User> {
|
||||
|
||||
findManyOptions.where = otherFilters;
|
||||
|
||||
if (isOwner !== undefined && globalOwnerRoleId) {
|
||||
findManyOptions.relations = ['globalRole'];
|
||||
findManyOptions.where.globalRole = {
|
||||
id: isOwner ? globalOwnerRoleId : Not(globalOwnerRoleId),
|
||||
};
|
||||
if (isOwner !== undefined) {
|
||||
findManyOptions.where.role = isOwner ? 'global:owner' : Not('global:owner');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
async getAllActive() {
|
||||
return await this.find({
|
||||
where: { active: true },
|
||||
relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'],
|
||||
relations: ['shared', 'shared.user'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
async findById(workflowId: string) {
|
||||
return await this.findOne({
|
||||
where: { id: workflowId },
|
||||
relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'],
|
||||
relations: ['shared', 'shared.user'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
versionId: true,
|
||||
shared: { userId: true, roleId: true },
|
||||
shared: { userId: true, role: true },
|
||||
};
|
||||
|
||||
delete select?.ownedBy; // remove non-entity field, handled after query
|
||||
@@ -152,7 +152,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
select.tags = { id: true, name: true };
|
||||
}
|
||||
|
||||
if (isOwnedByIncluded) relations.push('shared', 'shared.role', 'shared.user');
|
||||
if (isOwnedByIncluded) relations.push('shared', 'shared.user');
|
||||
|
||||
if (typeof where.name === 'string' && where.name !== '') {
|
||||
where.name = Like(`%${where.name}%`);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { StatisticsNames, WorkflowStatistics } from '../entities/WorkflowStatist
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
|
||||
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
|
||||
import { Role } from '@/databases/entities/Role';
|
||||
|
||||
type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists';
|
||||
type StatisticsUpsertResult = StatisticsInsertResult | 'update';
|
||||
@@ -110,12 +109,11 @@ export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics>
|
||||
'shared_workflow',
|
||||
'shared_workflow.workflowId = workflow_statistics.workflowId',
|
||||
)
|
||||
.innerJoin(Role, 'role', 'role.id = shared_workflow.roleId')
|
||||
.where('shared_workflow.userId = :userId', { userId })
|
||||
.andWhere('workflow.active = :isActive', { isActive: true })
|
||||
.andWhere('workflow_statistics.name = :name', { name: StatisticsNames.productionSuccess })
|
||||
.andWhere('workflow_statistics.count >= 5')
|
||||
.andWhere('role.name = :roleName', { roleName: 'owner' })
|
||||
.andWhere('role = :roleName', { roleName: 'workflow:owner' })
|
||||
.getCount();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user