@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
JavaScript
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