feat(core): Add method (getMany) to folder repository to filter, sort, and paginate folders (no-changelog) (#13290)

This commit is contained in:
Ricardo Espinoza
2025-02-17 09:05:12 -05:00
committed by GitHub
parent 5b82f34773
commit 82d2fa6e67
8 changed files with 851 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
import type { Folder } from './folder';
import type { TagEntity } from './tag-entity';
@Entity({ name: 'folder_tag' })
export class FolderTagMapping {
@PrimaryColumn()
folderId: string;
@ManyToOne('Folder', 'tagMappings')
@JoinColumn({ name: 'folderId' })
folders: Folder[];
@PrimaryColumn()
tagId: string;
@ManyToOne('TagEntity', 'folderMappings')
@JoinColumn({ name: 'tagId' })
tags: TagEntity[];
}

View File

@@ -11,6 +11,7 @@ import { ExecutionData } from './execution-data';
import { ExecutionEntity } from './execution-entity';
import { ExecutionMetadata } from './execution-metadata';
import { Folder } from './folder';
import { FolderTagMapping } from './folder-tag-mapping';
import { InstalledNodes } from './installed-nodes';
import { InstalledPackages } from './installed-packages';
import { InvalidAuthToken } from './invalid-auth-token';
@@ -68,4 +69,5 @@ export const entities = {
TestRun,
TestCaseExecution,
Folder,
FolderTagMapping,
};

View File

@@ -2,6 +2,7 @@ import { Column, Entity, Index, ManyToMany, OneToMany } from '@n8n/typeorm';
import { IsString, Length } from 'class-validator';
import { WithTimestampsAndStringId } from './abstract-entity';
import type { FolderTagMapping } from './folder-tag-mapping';
import type { WorkflowEntity } from './workflow-entity';
import type { WorkflowTagMapping } from './workflow-tag-mapping';
@@ -18,4 +19,7 @@ export class TagEntity extends WithTimestampsAndStringId {
@OneToMany('WorkflowTagMapping', 'tags')
workflowMappings: WorkflowTagMapping[];
@OneToMany('FolderTagMapping', 'tags')
folderMappings: FolderTagMapping[];
}

View File

@@ -0,0 +1,600 @@
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import type { Folder } from '@/databases/entities/folder';
import type { Project } from '@/databases/entities/project';
import type { User } from '@/databases/entities/user';
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
import { createFolder } from '@test-integration/db/folders';
import { getPersonalProject } from '@test-integration/db/projects';
import { createTag } from '@test-integration/db/tags';
import { createMember, createOwner } from '@test-integration/db/users';
import { createWorkflow } from '@test-integration/db/workflows';
import * as testDb from '../../../../test/integration/shared/test-db';
import { FolderRepository } from '../folder.repository';
describe('FolderRepository', () => {
let folderRepository: FolderRepository;
beforeAll(async () => {
await testDb.init();
folderRepository = Container.get(FolderRepository);
});
afterEach(async () => {
await testDb.truncate(['Folder', 'Tag']);
});
afterAll(async () => {
await testDb.terminate();
});
describe('getMany', () => {
let project: Project;
let owner: User;
beforeAll(async () => {
owner = await createOwner();
project = await getPersonalProject(owner);
});
describe('filters', () => {
it('should return all folders if not filter is provided', async () => {
const folder1 = await createFolder(project, { name: 'folder1' });
const folder2 = await createFolder(project, { name: 'folder2' });
await Promise.all([folder1, folder2]);
const [folders, count] = await folderRepository.getMany();
expect(count).toBe(2);
expect(folders).toHaveLength(2);
expect(folders[1].name).toBe('folder1');
expect(folders[0].name).toBe('folder2');
folders.forEach((folder) => {
expect(folder).toMatchObject({
id: expect.any(String),
name: expect.any(String),
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
parentFolder: null,
project: {
id: expect.any(String),
name: expect.any(String),
type: expect.any(String),
icon: null,
},
tags: expect.any(Array),
});
});
});
it('should filter folders by project ID', async () => {
const anotherUser = await createMember();
const anotherProject = await getPersonalProject(anotherUser);
const folder1 = createFolder(project, { name: 'folder1' });
const folder2 = createFolder(anotherProject, { name: 'folder2' });
await Promise.all([folder1, folder2]);
const [folders, count] = await folderRepository.getMany({
filter: { projectId: project.id },
});
expect(count).toBe(1);
expect(folders).toHaveLength(1);
expect(folders[0].name).toBe('folder1');
expect(folders[0].project.id).toBe(project.id);
});
it('should filter folders by name case-insensitively', async () => {
const folder1 = createFolder(project, { name: 'Test Folder' });
const folder2 = createFolder(project, { name: 'Another Folder' });
const folder3 = createFolder(project, { name: 'test folder sub' });
await Promise.all([folder1, folder2, folder3]);
const [folders, count] = await folderRepository.getMany({
filter: { name: 'test' },
});
expect(count).toBe(2);
expect(folders).toHaveLength(2);
expect(folders.map((f) => f.name).sort()).toEqual(['Test Folder', 'test folder sub']);
});
it('should filter folders by parent folder ID', async () => {
const parentFolder = await createFolder(project, { name: 'Parent' });
await createFolder(project, {
name: 'Child 1',
parentFolder,
});
await createFolder(project, {
name: 'Child 2',
parentFolder,
});
await createFolder(project, { name: 'Unrelated' });
const [folders, count] = await folderRepository.getMany({
filter: { parentFolderId: parentFolder.id },
});
expect(count).toBe(2);
expect(folders).toHaveLength(2);
expect(folders.map((f) => f.name).sort()).toEqual(['Child 1', 'Child 2']);
folders.forEach((folder) => {
expect(folder.parentFolder?.id).toBe(parentFolder.id);
});
});
it('should filter folders by a single tag', async () => {
const tag1 = await createTag({ name: 'important' });
const tag2 = await createTag({ name: 'archived' });
await createFolder(project, {
name: 'Folder 1',
tags: [tag1],
});
await createFolder(project, {
name: 'Folder 2',
tags: [tag2],
});
const [folders, count] = await folderRepository.getMany({
filter: { tags: ['important'] },
});
expect(count).toBe(1);
expect(folders).toHaveLength(1);
expect(folders[0].name).toBe('Folder 1');
expect(folders[0].tags[0].name).toBe('important');
});
it('should filter folders by multiple tags (AND operator)', async () => {
const tag1 = await createTag({ name: 'important' });
const tag2 = await createTag({ name: 'active' });
const tag3 = await createTag({ name: 'archived' });
await createFolder(project, {
name: 'Folder 1',
tags: [tag1, tag2],
});
await createFolder(project, {
name: 'Folder 2',
tags: [tag1],
});
await createFolder(project, {
name: 'Folder 3',
tags: [tag3],
});
const [folders] = await folderRepository.getMany({
filter: { tags: ['important', 'active'] },
});
expect(folders).toHaveLength(1);
expect(folders[0].name).toBe('Folder 1');
});
it('should apply multiple filters together', async () => {
const tag1 = await createTag({ name: 'important' });
const tag2 = await createTag({ name: 'archived' });
const parentFolder = await createFolder(project, { name: 'Parent' });
await createFolder(project, {
name: 'Test Folder',
parentFolder,
tags: [tag1],
});
await createFolder(project, {
name: 'Test Another',
tags: [tag1],
});
await createFolder(project, {
name: 'Test Child',
parentFolder,
tags: [tag2],
});
const [folders, count] = await folderRepository.getMany({
filter: {
name: 'test',
parentFolderId: parentFolder.id,
tags: ['important'],
},
});
expect(count).toBe(1);
expect(folders).toHaveLength(1);
expect(folders[0].name).toBe('Test Folder');
expect(folders[0].parentFolder?.id).toBe(parentFolder.id);
expect(folders[0].tags[0].name).toBe('important');
});
});
describe('select', () => {
let testFolder: Folder;
let workflowWithTestFolder: WorkflowEntity;
beforeEach(async () => {
const parentFolder = await createFolder(project, { name: 'Parent Folder' });
const tag = await createTag({ name: 'test-tag' });
testFolder = await createFolder(project, {
name: 'Test Folder',
parentFolder,
tags: [tag],
});
workflowWithTestFolder = await createWorkflow({ parentFolder: testFolder });
});
it('should select only id and name when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
name: true,
},
});
expect(folders).toEqual([
{
id: expect.any(String),
name: 'Test Folder',
},
{
id: expect.any(String),
name: 'Parent Folder',
},
]);
});
it('should return id, name and tags when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
name: true,
tags: true,
},
});
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'tags']);
expect(folder.id).toBeDefined();
expect(folder.name).toBeDefined();
expect(Array.isArray(folder.tags)).toBeTruthy();
});
const folderWithTag = folders.find((f) => f.name === 'Test Folder');
expect(folderWithTag?.tags).toHaveLength(1);
expect(folderWithTag?.tags[0]).toEqual({
id: expect.any(String),
name: 'test-tag',
});
});
it('should return id, name and project when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
name: true,
project: true,
},
});
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'project']);
expect(folder.project).toEqual({
id: expect.any(String),
name: expect.any(String),
type: expect.any(String),
icon: null,
});
});
});
it('should return id, name and parentFolder when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
name: true,
parentFolder: true,
},
});
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'parentFolder']);
});
const parentFolder = folders.find((f) => f.name === 'Parent Folder');
expect(parentFolder?.parentFolder).toBeNull();
const childFolder = folders.find((f) => f.name === 'Test Folder');
expect(childFolder?.parentFolder).toEqual({
id: expect.any(String),
name: 'Parent Folder',
});
});
it('should return id, name and workflows when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
name: true,
workflows: true,
},
});
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(Object.keys(folder).sort()).toEqual(['id', 'name', 'workflows']);
expect(folder.id).toBeDefined();
expect(folder.name).toBeDefined();
expect(Array.isArray(folder.workflows)).toBeTruthy();
});
expect(folders[0].workflows).toHaveLength(1);
expect(folders[0].workflows[0].id).toBe(workflowWithTestFolder.id);
});
it('should return timestamps when specified', async () => {
const [folders] = await folderRepository.getMany({
select: {
id: true,
createdAt: true,
updatedAt: true,
},
});
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(Object.keys(folder).sort()).toEqual(['createdAt', 'id', 'updatedAt']);
expect(folder.createdAt).toBeInstanceOf(Date);
expect(folder.updatedAt).toBeInstanceOf(Date);
});
});
it('should return all properties when no select is specified', async () => {
const [folders] = await folderRepository.getMany();
expect(folders).toHaveLength(2);
folders.forEach((folder) => {
expect(folder).toMatchObject({
id: expect.any(String),
name: expect.any(String),
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
project: {
id: expect.any(String),
name: expect.any(String),
type: expect.any(String),
icon: null,
},
workflows: expect.any(Array),
tags: expect.any(Array),
});
});
});
});
describe('pagination', () => {
beforeEach(async () => {
// Create 5 folders sequentially and ensure consistent updatedAt order
await createFolder(project, {
name: 'Folder 1',
updatedAt: DateTime.now().minus({ minutes: 4 }).toJSDate(),
});
await createFolder(project, {
name: 'Folder 2',
updatedAt: DateTime.now().minus({ minutes: 3 }).toJSDate(),
});
await createFolder(project, {
name: 'Folder 3',
updatedAt: DateTime.now().minus({ minutes: 2 }).toJSDate(),
});
await createFolder(project, {
name: 'Folder 4',
updatedAt: DateTime.now().minus({ minutes: 1 }).toJSDate(),
});
await createFolder(project, {
name: 'Folder 5',
updatedAt: DateTime.now().toJSDate(),
});
});
it('should limit results when take is specified', async () => {
const [folders, count] = await folderRepository.getMany({
take: 3,
});
expect(count).toBe(5);
expect(folders).toHaveLength(3);
});
it('should skip results when skip is specified', async () => {
const [folders, count] = await folderRepository.getMany({
skip: 2,
take: 5,
});
expect(count).toBe(5);
expect(folders).toHaveLength(3);
expect(folders.map((f) => f.name)).toEqual(['Folder 3', 'Folder 2', 'Folder 1']);
});
it('should handle skip and take together', async () => {
const [folders, count] = await folderRepository.getMany({
skip: 1,
take: 2,
});
expect(count).toBe(5);
expect(folders).toHaveLength(2);
expect(folders.map((f) => f.name)).toEqual(['Folder 4', 'Folder 3']);
});
it('should handle take larger than remaining items', async () => {
const [folders, count] = await folderRepository.getMany({
skip: 3,
take: 10,
});
expect(count).toBe(5);
expect(folders).toHaveLength(2);
expect(folders.map((f) => f.name)).toEqual(['Folder 2', 'Folder 1']);
});
it('should handle zero take by returning all results', async () => {
const [folders, count] = await folderRepository.getMany({
take: 0,
});
expect(count).toBe(5);
expect(folders).toHaveLength(5);
expect(folders.map((f) => f.name)).toEqual([
'Folder 5',
'Folder 4',
'Folder 3',
'Folder 2',
'Folder 1',
]);
});
});
describe('sorting', () => {
beforeEach(async () => {
// Create 4 folders sequentially and ensure consistent updatedAt order
await createFolder(project, {
name: 'B Folder',
createdAt: DateTime.now().toJSDate(),
updatedAt: DateTime.now().toJSDate(),
});
await createFolder(project, {
name: 'A Folder',
createdAt: DateTime.now().plus({ minutes: 1 }).toJSDate(),
updatedAt: DateTime.now().plus({ minutes: 1 }).toJSDate(),
});
await createFolder(project, {
name: 'D Folder',
createdAt: DateTime.now().plus({ minutes: 2 }).toJSDate(),
updatedAt: DateTime.now().plus({ minutes: 2 }).toJSDate(),
});
await createFolder(project, {
name: 'C Folder',
createdAt: DateTime.now().plus({ minutes: 3 }).toJSDate(),
updatedAt: DateTime.now().plus({ minutes: 3 }).toJSDate(),
});
});
it('should sort by default (updatedAt:desc)', async () => {
const [folders] = await folderRepository.getMany();
expect(folders.map((f) => f.name)).toEqual([
'C Folder',
'D Folder',
'A Folder',
'B Folder',
]);
});
it('should sort by name:asc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'name:asc',
});
expect(folders.map((f) => f.name)).toEqual([
'A Folder',
'B Folder',
'C Folder',
'D Folder',
]);
});
it('should sort by name:desc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'name:desc',
});
expect(folders.map((f) => f.name)).toEqual([
'D Folder',
'C Folder',
'B Folder',
'A Folder',
]);
});
it('should sort by createdAt:asc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'createdAt:asc',
});
expect(folders.map((f) => f.name)).toEqual([
'B Folder',
'A Folder',
'D Folder',
'C Folder',
]);
});
it('should sort by createdAt:desc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'createdAt:desc',
});
expect(folders.map((f) => f.name)).toEqual([
'C Folder',
'D Folder',
'A Folder',
'B Folder',
]);
});
it('should sort by updatedAt:asc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'updatedAt:asc',
});
expect(folders.map((f) => f.name)).toEqual([
'B Folder',
'A Folder',
'D Folder',
'C Folder',
]);
});
it('should sort by updatedAt:desc', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'updatedAt:desc',
});
expect(folders.map((f) => f.name)).toEqual([
'C Folder',
'D Folder',
'A Folder',
'B Folder',
]);
});
it('should default to asc if order not specified', async () => {
const [folders] = await folderRepository.getMany({
sortBy: 'name',
});
expect(folders.map((f) => f.name)).toEqual([
'A Folder',
'B Folder',
'C Folder',
'D Folder',
]);
});
});
});
});

View File

@@ -1,11 +1,200 @@
import { Service } from '@n8n/di';
import type { SelectQueryBuilder } from '@n8n/typeorm';
import { DataSource, Repository } from '@n8n/typeorm';
import type { ListQuery } from '@/requests';
import { Folder } from '../entities/folder';
import { FolderTagMapping } from '../entities/folder-tag-mapping';
import { TagEntity } from '../entities/tag-entity';
@Service()
export class FolderRepository extends Repository<Folder> {
constructor(dataSource: DataSource) {
super(Folder, dataSource.manager);
}
async getMany(options: ListQuery.Options = {}): Promise<[Folder[], number]> {
const query = this.createQueryBuilder('folder');
this.applySelections(query, options.select);
this.applyFilters(query, options.filter);
this.applySorting(query, options.sortBy);
this.applyPagination(query, options);
return await query.getManyAndCount();
}
private applySelections(
query: SelectQueryBuilder<Folder>,
select?: Record<string, boolean>,
): void {
if (select) {
this.applyCustomSelect(query, select);
} else {
this.applyDefaultSelect(query);
}
}
private applyDefaultSelect(query: SelectQueryBuilder<Folder>): void {
query
.leftJoinAndSelect('folder.project', 'project')
.leftJoinAndSelect('folder.parentFolder', 'parentFolder')
.leftJoinAndSelect('folder.tags', 'tags')
.leftJoinAndSelect('folder.workflows', 'workflows')
.select([
'folder',
...this.getProjectFields('project'),
...this.getTagFields(),
...this.getParentFolderFields('parentFolder'),
'workflows.id',
]);
}
private applyCustomSelect(
query: SelectQueryBuilder<Folder>,
select?: Record<string, boolean>,
): void {
const selections = ['folder.id'];
this.addBasicFields(selections, select);
this.addRelationFields(query, selections, select);
query.select(selections);
}
private addBasicFields(selections: string[], select?: Record<string, boolean>): void {
if (select?.name) selections.push('folder.name');
if (select?.createdAt) selections.push('folder.createdAt');
if (select?.updatedAt) selections.push('folder.updatedAt');
}
private addRelationFields(
query: SelectQueryBuilder<Folder>,
selections: string[],
select?: Record<string, boolean>,
): void {
if (select?.project) {
query.leftJoin('folder.project', 'project');
selections.push(...this.getProjectFields('project'));
}
if (select?.tags) {
query.leftJoin('folder.tags', 'tags').addOrderBy('tags.createdAt', 'ASC');
selections.push(...this.getTagFields());
}
if (select?.parentFolder) {
query.leftJoin('folder.parentFolder', 'parentFolder');
selections.push(...this.getParentFolderFields('parentFolder'));
}
if (select?.workflows) {
query.leftJoinAndSelect('folder.workflows', 'workflows');
selections.push('workflows.id');
}
}
private getProjectFields(alias: string): string[] {
return [`${alias}.id`, `${alias}.name`, `${alias}.type`, `${alias}.icon`];
}
private getTagFields(): string[] {
return ['tags.id', 'tags.name'];
}
private getParentFolderFields(alias: string): string[] {
return [`${alias}.id`, `${alias}.name`];
}
private applyFilters(
query: SelectQueryBuilder<Folder>,
filter?: ListQuery.Options['filter'],
): void {
if (!filter) return;
this.applyBasicFilters(query, filter);
this.applyTagsFilter(query, Array.isArray(filter?.tags) ? filter.tags : undefined);
}
private applyBasicFilters(
query: SelectQueryBuilder<Folder>,
filter: ListQuery.Options['filter'],
): void {
if (filter?.projectId) {
query.andWhere('folder.projectId = :projectId', { projectId: filter.projectId });
}
if (filter?.name && typeof filter.name === 'string') {
query.andWhere('LOWER(folder.name) LIKE LOWER(:name)', {
name: `%${filter.name}%`,
});
}
if (filter?.parentFolderId) {
query.andWhere('folder.parentFolderId = :parentFolderId', {
parentFolderId: filter.parentFolderId,
});
}
}
private applyTagsFilter(query: SelectQueryBuilder<Folder>, tags?: string[]): void {
if (!Array.isArray(tags) || tags.length === 0) return;
const subQuery = this.createTagsSubQuery(query, tags);
query.andWhere(`folder.id IN (${subQuery.getQuery()})`).setParameters({
tagNames: tags,
tagCount: tags.length,
});
}
private createTagsSubQuery(
query: SelectQueryBuilder<Folder>,
tags: string[],
): SelectQueryBuilder<FolderTagMapping> {
return query
.subQuery()
.select('ft.folderId')
.from(FolderTagMapping, 'ft')
.innerJoin(TagEntity, 'filter_tags', 'filter_tags.id = ft.tagId')
.where('filter_tags.name IN (:...tagNames)', { tagNames: tags })
.groupBy('ft.folderId')
.having('COUNT(DISTINCT filter_tags.name) = :tagCount', {
tagCount: tags.length,
});
}
private applySorting(query: SelectQueryBuilder<Folder>, sortBy?: string): void {
if (!sortBy) {
query.orderBy('folder.updatedAt', 'DESC');
return;
}
const [field, order] = this.parseSortingParams(sortBy);
this.applySortingByField(query, field, order);
}
private parseSortingParams(sortBy: string): [string, 'DESC' | 'ASC'] {
const [field, order] = sortBy.split(':');
return [field, order?.toLowerCase() === 'desc' ? 'DESC' : 'ASC'];
}
private applySortingByField(
query: SelectQueryBuilder<Folder>,
field: string,
direction: 'DESC' | 'ASC',
): void {
if (field === 'name') {
query.orderBy('LOWER(folder.name)', direction);
} else if (['createdAt', 'updatedAt'].includes(field)) {
query.orderBy(`folder.${field}`, direction);
}
}
private applyPagination(query: SelectQueryBuilder<Folder>, options: ListQuery.Options): void {
if (options?.take) {
query.skip(options.skip ?? 0).take(options.take);
}
}
}

View File

@@ -33,6 +33,7 @@ import type { TagEntity } from '@/databases/entities/tag-entity';
import type { AssignableRole, GlobalRole, User } from '@/databases/entities/user';
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
import type { Folder } from './databases/entities/folder';
import type { ExternalHooks } from './external-hooks';
import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types';
@@ -92,6 +93,7 @@ export type IAnnotationTagWithCountDb = IAnnotationTagDb & UsageCount;
export interface IWorkflowDb extends IWorkflowBase {
triggerCount: number;
tags?: TagEntity[];
parentFolder?: Folder | null;
}
export interface IWorkflowToImport extends IWorkflowBase {

View File

@@ -0,0 +1,32 @@
import { Container } from '@n8n/di';
import type { Folder } from '@/databases/entities/folder';
import type { Project } from '@/databases/entities/project';
import type { TagEntity } from '@/databases/entities/tag-entity';
import { FolderRepository } from '@/databases/repositories/folder.repository';
import { randomName } from '@test-integration/random';
export const createFolder = async (
project: Project,
options: {
name?: string;
parentFolder?: Folder;
tags?: TagEntity[];
updatedAt?: Date;
createdAt?: Date;
} = {},
) => {
const folderRepository = Container.get(FolderRepository);
const folder = await folderRepository.save(
folderRepository.create({
name: options.name ?? randomName(),
project,
parentFolder: options.parentFolder ?? null,
tags: options.tags ?? [],
updatedAt: options.updatedAt ?? new Date(),
createdAt: options.updatedAt ?? new Date(),
}),
);
return folder;
};

View File

@@ -85,6 +85,7 @@ const repositories = [
'WorkflowStatistics',
'WorkflowTagMapping',
'ApiKey',
'Folder',
] as const;
/**