Skip to content

Commit

Permalink
feat: export/import of modules now handles ALL versions of a module
Browse files Browse the repository at this point in the history
  • Loading branch information
niekcandaele committed Dec 18, 2024
1 parent dad18ad commit 76d0ec0
Show file tree
Hide file tree
Showing 34 changed files with 1,468 additions and 1,420 deletions.
120 changes: 120 additions & 0 deletions packages/app-api/src/controllers/Module/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { ITakaroQuery } from '@takaro/db';
import { ModuleService } from '../../service/Module/index.js';
import { ModuleCreateAPIDTO, ModuleOutputDTO, ModuleUpdateDTO } from '../../service/Module/dto.js';

import { ModuleTransferDTO, ICommand, ICommandArgument, ICronJob, IFunction, IHook } from '@takaro/modules';
import { PermissionCreateDTO } from '../../service/RoleService.js';
import { ModuleTransferVersionDTO } from '@takaro/modules';

export class ModuleOutputDTOAPI extends APIOutput<ModuleOutputDTO> {
@Type(() => ModuleOutputDTO)
@ValidateNested()
Expand Down Expand Up @@ -50,12 +54,19 @@ class ModuleSearchInputDTO extends ITakaroQuery<ModuleSearchInputAllowedFilters>
declare lessThan: RangeFilterCreatedAndUpdatedAt;
}

class ModuleExportDTOAPI extends APIOutput<ModuleTransferDTO<unknown>> {
@Type(() => ModuleTransferDTO)
@ValidateNested()
declare data: ModuleTransferDTO<unknown>;
}

@OpenAPI({
security: [{ domainAuth: [] }],
tags: ['Module'],
})
@JsonController('/module')
export class ModuleController {
// #region CRUD
@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.READ_MODULES]))
@ResponseSchema(ModuleOutputArrayDTOAPI)
@OpenAPI({
Expand Down Expand Up @@ -127,4 +138,113 @@ export class ModuleController {
await service.delete(params.id);
return apiResponse();
}
// #endregion CRUD
// #region Export/Import

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.READ_MODULES]))
@OpenAPI({
summary: 'Export a module',
description:
'Exports a module to a format that can be imported into another Takaro instance. This endpoint will export all known versions of the module',
})
@Post('/:id/export')
@ResponseSchema(ModuleExportDTOAPI)
async export(@Req() req: AuthenticatedRequest, @Params() params: ParamId) {
const service = new ModuleService(req.domainId);
const mod = await service.findOne(params.id);
if (!mod) throw new errors.NotFoundError('Module not found');
const versions = await service.findVersions({ filters: { moduleId: [params.id] } });

const preparedVersions = await Promise.all(
versions.results.map(
async (version) =>
new ModuleTransferVersionDTO({
tag: version.tag,
description: version.description,
configSchema: version.configSchema,
uiSchema: version.uiSchema,
commands: await Promise.all(
version.commands.map(
(_) =>
new ICommand({
function: _.function.code,
name: _.name,
trigger: _.trigger,
helpText: _.helpText,
arguments: _.arguments.map(
(arg) =>
new ICommandArgument({
name: arg.name,
type: arg.type,
defaultValue: arg.defaultValue,
helpText: arg.helpText,
position: arg.position,
}),
),
}),
),
),
hooks: await Promise.all(
version.hooks.map(
(_) =>
new IHook({
function: _.function.code,
name: _.name,
eventType: _.eventType,
}),
),
),
cronJobs: await Promise.all(
version.cronJobs.map(
(_) =>
new ICronJob({
function: _.function.code,
name: _.name,
temporalValue: _.temporalValue,
}),
),
),
functions: await Promise.all(
version.functions.map(
(_) =>
new IFunction({
function: _.code,
name: _.name,
}),
),
),
permissions: await Promise.all(
version.permissions.map(
(_) =>
new PermissionCreateDTO({
canHaveCount: _.canHaveCount,
description: _.description,
permission: _.permission,
friendlyName: _.friendlyName,
}),
),
),
}),
),
);

const output = new ModuleTransferDTO({
name: mod.name,
versions: preparedVersions,
});

return apiResponse(output);
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.MANAGE_MODULES]))
@OpenAPI({
summary: 'Import a module',
description: 'Imports a module from a format that was exported from another Takaro instance',
})
@Post('/import')
async import(@Req() req: AuthenticatedRequest, @Body() data: ModuleTransferDTO<unknown>) {
const service = new ModuleService(req.domainId);
return apiResponse(await service.import(data));
}
// #endregion Export/Import
}
113 changes: 1 addition & 112 deletions packages/app-api/src/controllers/Module/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@ import { Type } from 'class-transformer';
import { ParamId } from '../../lib/validators.js';
import { PERMISSIONS } from '@takaro/auth';
import { Response } from 'express';
import { errors } from '@takaro/util';
import { BuiltinModule, ICommand, ICommandArgument, ICronJob, IFunction, IHook } from '@takaro/modules';
import { AllowedFilters, RangeFilterCreatedAndUpdatedAt } from '../shared.js';
import { ModuleExportInputDTO, ModuleVersionCreateAPIDTO, ModuleVersionOutputDTO } from '../../service/Module/dto.js';
import { PermissionCreateDTO } from '../../service/RoleService.js';
import { ModuleVersionCreateAPIDTO, ModuleVersionOutputDTO } from '../../service/Module/dto.js';

export class ModuleVersionOutputDTOAPI extends APIOutput<ModuleVersionOutputDTO> {
@Type(() => ModuleVersionOutputDTO)
Expand Down Expand Up @@ -51,12 +48,6 @@ class ModuleVersionSearchInputDTO extends ITakaroQuery<ModuleVersionSearchInputA
declare lessThan: RangeFilterCreatedAndUpdatedAt;
}

class ModuleExportDTOAPI extends APIOutput<BuiltinModule<unknown>> {
@Type(() => BuiltinModule)
@ValidateNested()
declare data: BuiltinModule<unknown>;
}

@OpenAPI({
security: [{ domainAuth: [] }],
tags: ['Module'],
Expand Down Expand Up @@ -112,106 +103,4 @@ export class ModuleVersionController {
const result = await service.tagVersion(data.moduleId, data.tag);
return apiResponse(result);
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.READ_MODULES]))
@OpenAPI({
summary: 'Export a module version',
description: 'Exports a module to a format that can be imported into another Takaro instance',
})
@Post('/export')
@ResponseSchema(ModuleExportDTOAPI)
async export(@Req() req: AuthenticatedRequest, @Body() data: ModuleExportInputDTO) {
const service = new ModuleService(req.domainId);
const version = await service.findOneVersion(data.versionId);
if (!version) throw new errors.NotFoundError('Version not found');
const mod = await service.findOne(version.moduleId);
if (!mod) throw new errors.NotFoundError('Module not found');
if (!version) throw new errors.NotFoundError('Version not found');

const output = new BuiltinModule(
mod.name,
version.description,
version.tag,
version.configSchema,
version.uiSchema,
);
output.commands = await Promise.all(
version.commands.map(
(_) =>
new ICommand({
function: _.function.code,
name: _.name,
trigger: _.trigger,
helpText: _.helpText,
arguments: _.arguments.map(
(arg) =>
new ICommandArgument({
name: arg.name,
type: arg.type,
defaultValue: arg.defaultValue,
helpText: arg.helpText,
position: arg.position,
}),
),
}),
),
);

output.hooks = await Promise.all(
version.hooks.map(
(_) =>
new IHook({
function: _.function.code,
name: _.name,
eventType: _.eventType,
}),
),
);

output.cronJobs = await Promise.all(
version.cronJobs.map(
(_) =>
new ICronJob({
function: _.function.code,
name: _.name,
temporalValue: _.temporalValue,
}),
),
);

output.functions = await Promise.all(
version.functions.map(
(_) =>
new IFunction({
function: _.code,
name: _.name,
}),
),
);

output.permissions = await Promise.all(
version.permissions.map(
(_) =>
new PermissionCreateDTO({
canHaveCount: _.canHaveCount,
description: _.description,
permission: _.permission,
friendlyName: _.friendlyName,
}),
),
);

return apiResponse(output);
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.MANAGE_MODULES]))
@OpenAPI({
summary: 'Import a module version',
description: 'Imports a module from a format that was exported from another Takaro instance',
})
@Post('/import')
async import(@Req() req: AuthenticatedRequest, @Body() data: BuiltinModule<unknown>) {
const service = new ModuleService(req.domainId);
return apiResponse(await service.import(data));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,23 @@ const tests = [
},
})
).data.data[0];
const versions = (
await this.client.module.moduleVersionControllerSearchVersions({
filters: { moduleId: [mod.id], version: [builtin.version] },
})
).data.data;
const version = versions.find((v) => v.tag === builtin.version);
if (!version) throw new Error('Version not found');
const exportRes = await this.client.module.moduleVersionControllerExport({ versionId: version.id });
expect(exportRes.data.data).to.deep.equalInAnyOrder(builtin);
const exportRes = await this.client.module.moduleControllerExport(mod.id);
expect(exportRes.data.data.name).to.be.equal(builtin.name);

const expectedTags = builtin.versions.map((v) => v.tag);
for (const tag of expectedTags) {
const version = exportRes.data.data.versions.find((v) => v.tag === tag);
// Check that each builtin version is present in the exported module
expect(version).to.exist;
// Typescipt doesn't understand that `expect` already checks for existence
if (!version) throw new Error(`Version ${tag} not found in exported module`);
// Each version should contain hooks,commands,cronjobs,...
expect(version.hooks).to.exist;
expect(version.commands).to.exist;
expect(version.cronJobs).to.exist;
expect(version.functions).to.exist;
expect(version.permissions).to.exist;
}
},
}),
),
Expand All @@ -346,10 +354,8 @@ const tests = [
})
).data.data;
expect(mods).to.have.length(1);
const exportRes = await this.client.module.moduleVersionControllerExport({
versionId: mods[0].latestVersion.id,
});
await this.client.module.moduleVersionControllerImport(exportRes.data.data);
const exportRes = await this.client.module.moduleControllerExport(mods[0].id);
await this.client.module.moduleControllerImport(exportRes.data.data);
const modsAfter = (
await this.client.module.moduleControllerSearch({
filters: {
Expand Down
4 changes: 4 additions & 0 deletions packages/app-api/src/lib/systemConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function getEmptyConfigSchema(): Ajv.AnySchemaObject {
};
}

export function getEmptyUiSchema() {
return {};
}

export function getSystemConfigSchema(mod: ModuleVersionOutputDTO | ModuleVersionOutputDTOApi): string {
const systemConfigSchema = getEmptyConfigSchema();

Expand Down
5 changes: 0 additions & 5 deletions packages/app-api/src/service/Module/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,6 @@ export class ModuleVersionCreateAPIDTO extends TakaroDTO<ModuleVersionCreateAPID
moduleId: string;
}

export class ModuleExportInputDTO extends TakaroDTO<ModuleExportInputDTO> {
@IsUUID()
versionId: string;
}

export class InstallModuleDTO extends TakaroDTO<InstallModuleDTO> {
@IsUUID('4')
versionId: string;
Expand Down
Loading

0 comments on commit 76d0ec0

Please sign in to comment.