UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

445 lines (444 loc) 18.3 kB
import "../printer-api.interface.js"; import { validateInput } from "../../handlers/validators.js"; import { exportPrintersFloorsYamlSchema, importPrinterPositionsSchema, importPrintersFloorsYamlSchema } from "../validators/yaml-service.validation.js"; import { dump, load } from "js-yaml"; //#region src/services/core/yaml.service.ts var YamlService = class YamlService { logger; constructor(loggerFactory, printerTagService, printerService, printerCache, floorStore, floorService, userService, roleService, settingsStore) { this.printerTagService = printerTagService; this.printerService = printerService; this.printerCache = printerCache; this.floorStore = floorStore; this.floorService = floorService; this.userService = userService; this.roleService = roleService; this.settingsStore = settingsStore; this.logger = loggerFactory(YamlService.name); } async importYaml(yamlBuffer) { const importSpec = await load(yamlBuffer); const databaseTypeSqlite = importSpec.databaseType === "sqlite"; const { exportPrinters, exportFloorGrid, exportSettings, exportUsers } = importSpec.config; this.normalizeYamlData(importSpec, databaseTypeSqlite); const importData = await validateInput(importSpec, importPrintersFloorsYamlSchema); if (exportSettings || exportUsers) { await this.validateSystemTablesEmpty(importSpec); if (exportSettings && importSpec.settings) { this.logger.log("Importing settings"); await this.importSettings(importSpec.settings); } if (exportUsers && importSpec.users && importSpec.users.length > 0) { this.logger.log(`Importing users (${importSpec.users.length} users)`); await this.importUsers(importSpec.users, databaseTypeSqlite); } } if (exportFloorGrid && importData.floors?.length) for (const floor of importData.floors) await validateInput(floor, importPrinterPositionsSchema); this.logger.log("Analysing printers for import"); const { updateByPropertyPrinters, insertPrinters } = await this.analysePrintersUpsert(importData.printers ?? [], importData.config.printerComparisonStrategiesByPriority); this.logger.log("Analysing floors for import"); const { updateByPropertyFloors, insertFloors } = await this.analyseFloorsUpsert(importData.floors ?? [], importData.config.floorComparisonStrategiesByPriority); this.logger.log("Analysing tags for import"); const { updateByNameTags, insertTags } = await this.analyseUpsertTags(importData.tags ?? []); this.logger.log(`Performing pure insert printers (${insertPrinters.length} printers)`); const printerIdMap = {}; for (const newPrinter of insertPrinters) try { const state = await this.printerService.create({ ...newPrinter }); if (!newPrinter.id) throw new Error(`Saved ID was empty ${JSON.stringify(newPrinter)}`); printerIdMap[newPrinter.id] = state.id; } catch (error) { this.logger.error(`Failed to create printer ${newPrinter.name}:`, error); } this.logger.log(`Performing update import printers (${updateByPropertyPrinters.length} printers)`); for (const updatePrinterSpec of updateByPropertyPrinters) try { const updateId = updatePrinterSpec.printerId; const updatedPrinter = updatePrinterSpec.value; if (typeof updateId === "string") throw new Error("Cannot update a printer by string id in sqlite mode"); const originalPrinterId = updatedPrinter.id; delete updatePrinterSpec.value.id; updatedPrinter.id = updateId; const state = await this.printerService.update(updateId, updatedPrinter); if (!updatePrinterSpec.printerId) throw new Error("Saved ID was empty"); printerIdMap[originalPrinterId] = state.id; } catch (error) { this.logger.error(`Failed to update printer ${updatePrinterSpec.value.name}:`, error); } this.logger.log(`Performing pure create floors (${insertFloors.length} floors)`); const floorIdMap = {}; for (const newFloor of insertFloors) try { const originalFloorId = newFloor.id; delete newFloor.id; const knownPrinterPositions = []; if (exportFloorGrid && exportPrinters) { for (const floorPosition of newFloor.printers) { const knownPrinterId = printerIdMap[floorPosition.printerId]; if (!knownPrinterId) continue; delete floorPosition.id; delete floorPosition.floorId; floorPosition.printerId = knownPrinterId; knownPrinterPositions.push(floorPosition); } newFloor.printers = knownPrinterPositions; } floorIdMap[originalFloorId] = (await this.floorStore.create({ ...newFloor })).id; } catch (error) { this.logger.error(`Failed to create floor ${newFloor.name}:`, error); } this.logger.log(`Performing update of floors (${updateByPropertyFloors.length} floors)`); for (const updateFloorSpec of updateByPropertyFloors) try { const updateId = updateFloorSpec.floorId; if (typeof updateId === "string") throw new Error("Cannot update a floor by string id in sqlite mode"); const updatedFloor = updateFloorSpec.value; const originalFloorId = updatedFloor.id; delete updatedFloor.id; const knownPrinters = []; if (exportFloorGrid && exportPrinters) { for (const floorPosition of updatedFloor?.printers) { const knownPrinterId = printerIdMap[floorPosition.printerId]; if (!knownPrinterId) continue; delete floorPosition.id; delete floorPosition.floorId; floorPosition.printerId = knownPrinterId; floorPosition.floorId = updateId; knownPrinters.push(floorPosition); } updatedFloor.id = updateId; updatedFloor.printers = knownPrinters; } floorIdMap[originalFloorId] = (await this.floorStore.update(updateId, updatedFloor)).id; } catch (error) { this.logger.error(`Failed to update floor ${updateFloorSpec.value.name}:`, error); } await this.floorStore.loadStore(); this.logger.log(`Performing pure create tags (${insertTags.length} tags)`); for (const tag of insertTags) try { const createdTag = await this.printerTagService.createTag({ name: tag.name, color: tag.color }); for (const printer of tag.printers) { const knownPrinterId = printerIdMap[printer.printerId]; if (!knownPrinterId) continue; try { await this.printerTagService.addPrinterToTag(createdTag.id, knownPrinterId); } catch (error) { this.logger.error(`Failed to add printer ${knownPrinterId} to tag ${tag.name}:`, error); } } } catch (error) { this.logger.error(`Failed to create tag ${tag.name}:`, error); } this.logger.log(`Performing update of tag printer links (${updateByNameTags.length} tags)`); for (const updateTagSpec of updateByNameTags) try { const existingTag = await this.printerTagService.getPrintersByTag(updateTagSpec.tagId); const existingPrinterIds = existingTag.printers.map((p) => p.printerId); const wantedTargetPrinterIds = updateTagSpec.value.printers.filter((p) => !!printerIdMap[p.printerId]).map((p) => printerIdMap[p.printerId]); for (const unwantedId of existingPrinterIds.filter((eid) => !wantedTargetPrinterIds.includes(eid))) try { await this.printerTagService.removePrinterFromTag(existingTag.id, unwantedId); } catch (error) { this.logger.error(`Failed to remove printer ${unwantedId} from tag ${existingTag.name}:`, error); } for (const nonExistingNewId of wantedTargetPrinterIds.filter((eid) => !existingPrinterIds.includes(eid))) try { await this.printerTagService.addPrinterToTag(existingTag.id, nonExistingNewId); } catch (error) { this.logger.error(`Failed to add printer ${nonExistingNewId} to tag ${existingTag.name}:`, error); } } catch (error) { this.logger.error(`Failed to update tag ${updateTagSpec.value.name}:`, error); } return { updateByPropertyPrinters, updateByPropertyFloors, insertPrinters, insertFloors, printerIdMap, floorIdMap }; } async importSettings(settings) { if (settings.server) await this.settingsStore.updateServerSettings(settings.server); if (settings.timeout) await this.settingsStore.updateTimeoutSettings(settings.timeout); if (settings.frontend) await this.settingsStore.updateFrontendSettings(settings.frontend); if (settings.wizard?.wizardCompleted) { const importedWizardVersion = settings.wizard.wizardVersion; this.logger.log(`Marking wizard as completed with version: ${importedWizardVersion}`); await this.settingsStore.setWizardCompleted(importedWizardVersion); } if (settings.credential) { const { jwtExpiresIn, refreshTokenAttempts, refreshTokenExpiry, slicerApiKey } = settings.credential; if (jwtExpiresIn || refreshTokenAttempts || refreshTokenExpiry) { await this.settingsStore.updateCoreCredentialSettings({ jwtExpiresIn: jwtExpiresIn ?? (await this.settingsStore.getCredentialSettings()).jwtExpiresIn, refreshTokenAttempts: refreshTokenAttempts ?? (await this.settingsStore.getCredentialSettings()).refreshTokenAttempts, refreshTokenExpiry: refreshTokenExpiry ?? (await this.settingsStore.getCredentialSettings()).refreshTokenExpiry }); this.logger.log("Imported credential settings"); } if (slicerApiKey) { await this.settingsStore.setSlicerApiKey(slicerApiKey); this.logger.log("Imported slicer API key"); } } await this.settingsStore.loadSettings(); this.logger.log("Settings imported successfully"); } async importUsers(users, databaseTypeSqlite) { const allRoles = this.roleService.roles; for (const user of users) { if (databaseTypeSqlite && typeof user.id === "string") user.id = Number.parseInt(user.id); const roleNames = (user.roles ?? []).filter((roleName) => allRoles.some((r) => r.name === roleName)); await this.userService.register({ username: user.username, password: "temporary-password-to-be-replaced", roles: roleNames, isRootUser: user.isRootUser ?? false, isDemoUser: user.isDemoUser ?? false, isVerified: user.isVerified ?? false, needsPasswordChange: user.needsPasswordChange ?? true }); await this.userService.updatePasswordHashUnsafeByUsername(user.username, user.passwordHash); } this.logger.log(`Imported ${users.length} users`); } async validateSystemTablesEmpty(importSpec) { const errors = []; if (!this.settingsStore.getWizardSettings()?.wizardCompleted) return; if (importSpec.settings && importSpec.config.exportSettings) { const existingSettings = this.settingsStore.getSettings(); if (existingSettings && Object.keys(existingSettings).length > 0) errors.push("Settings table is not empty. Cannot import settings when existing settings are present."); } if (importSpec.users && importSpec.users.length > 0 && importSpec.config.exportUsers) { if ((await this.userService.listUsers(1)).length > 0) errors.push("Users table is not empty. Cannot import users when existing users are present."); } if (errors.length > 0) throw new Error(`Import validation failed:\n${errors.join("\n")}`); } async analysePrintersUpsert(upsertPrinters, comparisonStrategies) { const existingPrinters = await this.printerService.list(); const names = existingPrinters.map((p) => p.name.toLowerCase()); const urls = existingPrinters.map((p) => p.printerURL); const ids = existingPrinters.map((p) => p.id.toString()); const insertPrinters = []; const updateByPropertyPrinters = []; for (const printer of upsertPrinters) for (const strategy of [...comparisonStrategies, "new"]) if (strategy === "name") { const comparedName = printer.name.toLowerCase(); const foundIndex = names.findIndex((n) => n === comparedName); if (foundIndex !== -1) { if (!ids[foundIndex]) throw new Error("Update ID is undefined"); updateByPropertyPrinters.push({ strategy: "name", printerId: Number.parseInt(ids[foundIndex]), value: printer }); break; } } else if (strategy === "url") { const comparedName = printer.printerURL.toLowerCase(); const foundIndex = urls.findIndex((n) => n === comparedName); if (foundIndex !== -1) { if (!ids[foundIndex]) throw new Error("Update ID is undefined"); updateByPropertyPrinters.push({ strategy: "url", printerId: Number.parseInt(ids[foundIndex]), value: printer }); break; } } else if (strategy === "new") { if (!printer.id) throw new Error(JSON.stringify(printer)); insertPrinters.push(printer); break; } return { updateByPropertyPrinters, insertPrinters }; } async analyseFloorsUpsert(upsertFloors, comparisonStrategy) { const existingFloors = await this.floorService.list(); const names = existingFloors.map((p) => p.name.toLowerCase()); const floorLevels = existingFloors.map((p) => p.order); const ids = existingFloors.map((p) => p.id.toString()); const insertFloors = []; const updateByPropertyFloors = []; for (const floor of upsertFloors) for (const strategy of [comparisonStrategy, "new"]) if (strategy === "name") { const comparedProperty = floor.name.toLowerCase(); const foundIndex = names.findIndex((n) => n === comparedProperty); if (foundIndex !== -1) { if (!ids[foundIndex]) throw new Error("IDS not found, floor name"); updateByPropertyFloors.push({ strategy: "name", floorId: Number.parseInt(ids[foundIndex]), value: floor }); break; } } else if (strategy === "floor") { const comparedProperty = floor.order; const foundIndex = floorLevels.findIndex((n) => n === comparedProperty); if (foundIndex !== -1) { if (!ids[foundIndex]) throw new Error("IDS not found, floor level"); updateByPropertyFloors.push({ strategy: "floor", floorId: Number.parseInt(ids[foundIndex]), value: floor }); break; } } else if (strategy === "new") { insertFloors.push(floor); break; } return { updateByPropertyFloors, insertFloors }; } async analyseUpsertTags(upsertTags) { if (!upsertTags?.length) return { updateByNameTags: [], insertTags: [] }; const existingTags = await this.printerTagService.listTags(); const names = existingTags.map((p) => p.name.toLowerCase()); const ids = existingTags.map((p) => p.id.toString()); const insertTags = []; const updateByNameTags = []; for (const tag of upsertTags) { const comparedProperty = tag.name.toLowerCase(); const foundIndex = names.indexOf(comparedProperty); if (foundIndex === -1) insertTags.push(tag); else { if (!ids[foundIndex]) throw new Error("IDS not found, tag name"); updateByNameTags.push({ strategy: "name", tagId: Number.parseInt(ids[foundIndex]), value: tag }); break; } } return { insertTags, updateByNameTags }; } async exportYaml(options) { const input = await validateInput(options, exportPrintersFloorsYamlSchema); const { exportFloors, exportPrinters, exportFloorGrid, exportTags, exportSettings, exportUsers } = input; const dumpedObject = { version: process.env.npm_package_version, exportedAt: /* @__PURE__ */ new Date(), databaseType: "sqlite", config: input, printers: void 0, floors: void 0, tags: void 0, settings: void 0, users: void 0, user_roles: void 0 }; if (exportPrinters) dumpedObject.printers = (await this.printerService.list()).map((p) => { const printerId = p.id; const { apiKey, username, password } = this.printerCache.getLoginDto(printerId); return { id: printerId, disabledReason: p.disabledReason, enabled: p.enabled, printerType: p.printerType, dateAdded: p.dateAdded, name: p.name, printerURL: p.printerURL, apiKey, username, password, assignee: p.assignee, flowRate: p.flowRate, feedRate: p.feedRate }; }); if (exportFloors) dumpedObject.floors = (await this.floorStore.listCache()).map((f) => { const dumpedFloor = { id: f.id, order: f.order, name: f.name, printers: void 0 }; if (exportFloorGrid) dumpedFloor.printers = f.printers.map((p) => { return { printerId: p.printerId, x: p.x, y: p.y }; }); return dumpedFloor; }); if (exportTags) dumpedObject.tags = (await this.printerTagService.listTags()).map((t) => { return { name: t.name, id: t.id, printers: t.printers.map((p) => { return { printerId: p.printerId }; }) }; }); if (exportSettings) dumpedObject.settings = { ...this.settingsStore.getSettings(), ...this.settingsStore.getSettingsSensitive() }; if (exportUsers) dumpedObject.users = (await this.userService.listUsers(1e3)).map((u) => { const userDto = this.userService.toDto(u); return { id: userDto.id, username: userDto.username, isDemoUser: userDto.isDemoUser, isRootUser: userDto.isRootUser, isVerified: userDto.isVerified, needsPasswordChange: userDto.needsPasswordChange, passwordHash: u.passwordHash, createdAt: userDto.createdAt, roles: userDto.roles }; }); return dump(dumpedObject, {}); } normalizeYamlData(importSpec, databaseTypeSqlite) { for (const printer of importSpec.printers) { if (!printer.name && printer.printerName) { printer.name = printer.printerName; delete printer.printerName; } if (printer.settingsAppearance?.name) { printer.name = printer.settingsAppearance?.name; delete printer.settingsAppearance?.name; } if (databaseTypeSqlite && typeof printer.id === "string") printer.id = Number.parseInt(printer.id); if (![ 0, 1, 2, 3 ].includes(printer.printerType)) printer.printerType = 0; } for (const floor of importSpec.floors) { if (floor.floor !== void 0 && floor.order === void 0) { floor.order = floor.floor; delete floor.floor; } if (databaseTypeSqlite) { if (typeof floor.id === "string") floor.id = Number.parseInt(floor.id); for (const printer of floor.printers) if (typeof printer.printerId === "string") printer.printerId = Number.parseInt(printer.printerId); } } if (importSpec.groups && !importSpec.tags) { importSpec.tags = importSpec.groups; delete importSpec.groups; } if (importSpec.config.exportGroups !== void 0 && importSpec.config.exportTags === void 0) { importSpec.config.exportTags = importSpec.config.exportGroups; delete importSpec.config.exportGroups; } } }; //#endregion export { YamlService }; //# sourceMappingURL=yaml.service.js.map