From aad49bfcfdd2ba699a05db86ac660dc468ceca1e Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 20 Jul 2026 02:11:56 +0100 Subject: [PATCH] Add formatTraceData utility and integrate into logging - Introduced a new utility function `formatTraceData` to standardize the formatting of trace data for logging purposes. - Updated various modules including auth, database, and stats manager to utilize `formatTraceData` for improved log clarity and consistency. - Added new schemas for printer profiles and filament profiles to enhance the data model. - Implemented tests for session authentication to ensure robust functionality and error handling. --- src/auth/__tests__/auth.test.js | 91 ++++++++++ src/auth/auth.js | 22 +-- src/database/database.js | 72 ++++---- src/database/nats.js | 7 +- src/database/redis.js | 3 +- .../schemas/misc/usersettings.schema.js | 17 +- src/database/schemas/models.js | 16 ++ .../production/filamentprofile.schema.js | 171 ++++++++++++++++++ src/database/schemas/production/job.schema.js | 99 +++++++++- .../schemas/production/printer.schema.js | 13 ++ .../production/printerprofile.schema.js | 107 +++++++++++ src/stats/statsmanager.js | 3 +- src/updates/updatemanager.js | 17 +- src/utils.js | 6 + 14 files changed, 572 insertions(+), 72 deletions(-) create mode 100644 src/auth/__tests__/auth.test.js create mode 100644 src/database/schemas/production/filamentprofile.schema.js create mode 100644 src/database/schemas/production/printerprofile.schema.js diff --git a/src/auth/__tests__/auth.test.js b/src/auth/__tests__/auth.test.js new file mode 100644 index 0000000..8e1da14 --- /dev/null +++ b/src/auth/__tests__/auth.test.js @@ -0,0 +1,91 @@ +import { jest } from '@jest/globals'; + +const redisGetKeyMock = jest.fn(); + +jest.unstable_mockModule('../../config.js', () => ({ + loadConfig: jest.fn(() => ({ + server: { logLevel: 'info' }, + auth: { requiredRoles: [] }, + })), +})); + +jest.unstable_mockModule('../../database/redis.js', () => ({ + redisServer: { getKey: redisGetKeyMock }, +})); + +jest.unstable_mockModule('../../database/database.js', () => ({ + editObject: jest.fn(), + getObject: jest.fn(), + listObjects: jest.fn(), +})); + +jest.unstable_mockModule('../../database/schemas/management/host.schema.js', () => ({ + hostModel: {}, +})); + +jest.unstable_mockModule('../../database/schemas/management/user.schema.js', () => ({ + userModel: {}, +})); + +jest.unstable_mockModule('../../utils.js', () => ({ + generateAuthCode: jest.fn(), + formatTraceData: (value) => (typeof value === 'string' ? value : JSON.stringify(value)), +})); + +jest.unstable_mockModule('log4js', () => ({ + default: { + getLogger: () => ({ + level: 'info', + info: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + }), + }, +})); + +const { SessionAuth } = await import('../auth.js'); + +describe('SessionAuth', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('accepts the API shared Redis session shape', async () => { + const user = { _id: 'user-1', username: 'slicer-user' }; + redisGetKeyMock.mockResolvedValue({ + sessionToken: 'slicer-token', + user, + expiresAt: Date.now() + 10000, + source: 'slicer', + }); + + await expect(new SessionAuth().verifyToken('slicer-token')).resolves.toEqual({ + valid: true, + user, + }); + expect(redisGetKeyMock).toHaveBeenCalledWith('session:slicer-token'); + }); + + it('does not cache a token deleted by slicer-session replacement', async () => { + const user = { _id: 'user-1', username: 'slicer-user' }; + redisGetKeyMock + .mockResolvedValueOnce({ user, expiresAt: Date.now() + 10000 }) + .mockResolvedValueOnce(null); + const auth = new SessionAuth(); + + await expect(auth.verifyToken('old-token')).resolves.toEqual({ valid: true, user }); + await expect(auth.verifyToken('old-token')).resolves.toEqual({ valid: false }); + expect(redisGetKeyMock).toHaveBeenCalledTimes(2); + }); + + it('rejects expired sessions', async () => { + redisGetKeyMock.mockResolvedValue({ + user: { _id: 'user-1' }, + expiresAt: Date.now() - 1, + }); + + await expect(new SessionAuth().verifyToken('expired-token')).resolves.toEqual({ + valid: false, + }); + }); +}); diff --git a/src/auth/auth.js b/src/auth/auth.js index eb31f50..15b0a4b 100644 --- a/src/auth/auth.js +++ b/src/auth/auth.js @@ -5,7 +5,7 @@ import { redisServer } from '../database/redis.js'; import { editObject, getObject, listObjects } from '../database/database.js'; import { hostModel } from '../database/schemas/management/host.schema.js'; import { userModel } from '../database/schemas/management/user.schema.js'; -import { generateAuthCode } from '../utils.js'; +import { formatTraceData, generateAuthCode } from '../utils.js'; const config = loadConfig(); @@ -21,20 +21,11 @@ const SESSION_KEY_PREFIX = 'session:'; export class SessionAuth { constructor() { this.config = config.auth; - this.tokenCache = new Map(); } async verifyToken(token) { if (!token) return { valid: false }; - if (this.tokenCache.has(token)) { - const cached = this.tokenCache.get(token); - if (cached.expiresAt > Date.now()) { - return { valid: true, user: cached.user }; - } - this.tokenCache.delete(token); - } - try { const key = SESSION_KEY_PREFIX + token; const session = await redisServer.getKey(key); @@ -58,11 +49,6 @@ export class SessionAuth { } } - this.tokenCache.set(token, { - expiresAt: session.expiresAt, - user: session.user - }); - return { valid: true, user: session.user }; } catch (error) { logger.error('Session verification error:', error.message); @@ -77,9 +63,9 @@ export class KeycloakAuth extends SessionAuth {} export class CodeAuth { async verifyCode(id, authCode) { try { - logger.trace('Verifying code:', { id, authCode }); + logger.trace(`Verifying code: ${formatTraceData({ id, authCode })}`); const host = await getObject({ model: hostModel, id, cached: true }); - logger.trace('Host retrieved:', host); + logger.trace(`Host retrieved: ${formatTraceData(host)}`); if (host == undefined) { const error = 'Host not found.'; logger.warn(error, 'Host:', id); @@ -100,7 +86,7 @@ export class CodeAuth { logger.warn(error, 'Host:', id); return { valid: false, error: error }; } - logger.trace('Code verification successful:', host); + logger.trace(`Code verification successful: ${formatTraceData(host)}`); return { valid: true, host: host }; } catch (error) { logger.error('Code verification error:', error.message); diff --git a/src/database/database.js b/src/database/database.js index e6e10c5..d8a6102 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -10,7 +10,7 @@ import { } from './utils.js'; import log4js from 'log4js'; import { loadConfig } from '../config.js'; -import { getQueryToCacheKey } from '../utils.js'; +import { formatTraceData, getQueryToCacheKey } from '../utils.js'; import { redisServer } from './redis.js'; import { auditLogModel } from './schemas/management/auditlog.schema.js'; @@ -61,7 +61,7 @@ export const updateObjectCache = async ({ // Get all keys matching the filter pattern const matchingKeys = await redisServer.getKeysByPattern(cacheKeyFilter); - logger.trace('Matching keys:', matchingKeys); + logger.trace(`Matching keys: ${formatTraceData(matchingKeys)}`); // Merge the object with each cached object and update const mergedObjects = []; @@ -124,15 +124,17 @@ export const listObjects = async ({ cached = false }) => { try { - logger.trace('Listing objects:', { - model, - populate, - filter, - sort, - order, - project, - cached - }); + logger.trace( + `Listing objects: ${formatTraceData({ + model, + populate, + filter, + sort, + order, + project, + cached, + })}` + ); // Fix: descend should be -1, ascend should be 1 const sortOrder = order === 'descend' ? -1 : 1; @@ -177,16 +179,18 @@ export const listObjects = async ({ const finalResult = expandObjectIds(queryResult); - logger.trace('Retreived from database:', { - model, - populate, - filter, - sort, - order, - project, - cached, - length: finalResult.length - }); + logger.trace( + `Retreived from database: ${formatTraceData({ + model, + populate, + filter, + sort, + order, + project, + cached, + length: finalResult.length, + })}` + ); return finalResult; } catch (error) { logger.error('Object list error:', error); @@ -202,11 +206,13 @@ export const getObject = async ({ cached = false }) => { try { - logger.trace('Getting object:', { - model, - id, - populate - }); + logger.trace( + `Getting object: ${formatTraceData({ + model, + id, + populate, + })}` + ); if (cached == true) { const cachedObject = await retrieveObjectCache({ model, id, populate }); @@ -238,13 +244,15 @@ export const getObject = async ({ return undefined; } - logger.trace('Retreived object from database:', { - model, - id, - populate - }); + logger.trace( + `Retreived object from database: ${formatTraceData({ + model, + id, + populate, + })}` + ); - logger.trace(finalResult); + logger.trace(formatTraceData(finalResult)); await updateObjectCache({ model: model, diff --git a/src/database/nats.js b/src/database/nats.js index d021ce1..8c4d3ee 100644 --- a/src/database/nats.js +++ b/src/database/nats.js @@ -1,6 +1,7 @@ import { connect } from '@nats-io/transport-node'; import log4js from 'log4js'; import { loadConfig } from '../config.js'; +import { formatTraceData } from '../utils.js'; const config = loadConfig(); const logger = log4js.getLogger('Nats'); @@ -72,7 +73,7 @@ class NatsServer { try { client.publish(subject, this.textEncoder.encode(payload)); - logger.trace(`Published to subject: ${subject}, data: ${payload}`); + logger.trace(`Published to subject: ${subject}, data: ${formatTraceData(payload)}`); return { success: true }; } catch (error) { logger.error(`Failed to publish to subject ${subject}:`, error); @@ -94,7 +95,9 @@ class NatsServer { ); const responseData = this.textDecoder.decode(response.data); - logger.trace(`Request to subject: ${subject}, response: ${responseData}`); + logger.trace( + `Request to subject: ${subject}, response: ${formatTraceData(responseData)}` + ); // Try to parse as JSON, fallback to string try { diff --git a/src/database/redis.js b/src/database/redis.js index 15e25f4..30493aa 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -1,6 +1,7 @@ import { createClient } from 'redis'; import log4js from 'log4js'; import { loadConfig } from '../config.js'; +import { formatTraceData } from '../utils.js'; const config = loadConfig(); const logger = log4js.getLogger('Redis'); @@ -80,7 +81,7 @@ class RedisServer { COUNT: 100 }); cursor = result.cursor; - logger.trace('Result:', result); + logger.trace(`Result: ${formatTraceData(result)}`); keys.push(...result.keys); } while (cursor !== '0'); return keys; diff --git a/src/database/schemas/misc/usersettings.schema.js b/src/database/schemas/misc/usersettings.schema.js index d643f50..44d8c07 100644 --- a/src/database/schemas/misc/usersettings.schema.js +++ b/src/database/schemas/misc/usersettings.schema.js @@ -8,28 +8,28 @@ const userSettingsSchema = new mongoose.Schema({ type: Schema.Types.ObjectId, ref: 'user', required: true, - unique: true + unique: true, }, defaults: { viewMode: { type: Schema.Types.Mixed, default: () => ({}) }, filterSidebarVisibility: { type: Schema.Types.Mixed, - default: () => ({}) + default: () => ({}), }, columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) }, - collapseState: { type: Schema.Types.Mixed, default: () => ({}) } + collapseState: { type: Schema.Types.Mixed, default: () => ({}) }, }, createdAt: { type: Date, required: true, - default: Date.now + default: Date.now, }, updatedAt: { type: Date, required: true, - default: Date.now - } + default: Date.now, + }, }); userSettingsSchema.virtual('id').get(function () { @@ -38,7 +38,4 @@ userSettingsSchema.virtual('id').get(function () { userSettingsSchema.set('toJSON', { virtuals: true }); -export const userSettingsModel = mongoose.model( - 'userSettings', - userSettingsSchema -); +export const userSettingsModel = mongoose.model('userSettings', userSettingsSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 44b07a0..b55486a 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -1,6 +1,8 @@ import { jobModel } from './production/job.schema.js'; import { subJobModel } from './production/subjob.schema.js'; import { printerModel } from './production/printer.schema.js'; +import { printerProfileModel } from './production/printerprofile.schema.js'; +import { filamentProfileModel } from './production/filamentprofile.schema.js'; import { filamentModel } from './management/filament.schema.js'; import { filamentSkuModel } from './management/filamentsku.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js'; @@ -55,6 +57,20 @@ export const models = { referenceField: '_reference', label: 'Printer', }, + PPF: { + model: printerProfileModel, + idField: '_id', + type: 'printerProfile', + referenceField: '_reference', + label: 'Printer Profile', + }, + FPF: { + model: filamentProfileModel, + idField: '_id', + type: 'filamentProfile', + referenceField: '_reference', + label: 'Filament Profile', + }, FIL: { model: filamentModel, idField: '_id', diff --git a/src/database/schemas/production/filamentprofile.schema.js b/src/database/schemas/production/filamentprofile.schema.js new file mode 100644 index 0000000..2c5564c --- /dev/null +++ b/src/database/schemas/production/filamentprofile.schema.js @@ -0,0 +1,171 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const filamentProfileSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true }, + filamentType: { type: String, required: true }, + filament: { + type: mongoose.Schema.Types.ObjectId, + refPath: 'filamentType', + required: true, + }, + filamentIsSupport: { type: Boolean }, + filamentSoluble: { type: Boolean }, + filamentPrintable: { type: Number }, + filamentAdhesivenessCategory: { type: Number }, + temperatureVitrification: { type: Number }, + idleTemperature: { type: Number }, + pelletFlowCoefficient: { type: Number }, + requiredNozzleHrc: { type: Number }, + filamentFlowRatio: { type: Number }, + enablePressureAdvance: { type: Boolean }, + pressureAdvance: { type: Number }, + adaptivePressureAdvance: { type: Boolean }, + adaptivePressureAdvanceBridges: { type: Boolean }, + adaptivePressureAdvanceOverhangs: { type: Boolean }, + adaptivePressureAdvanceModel: { type: String }, + activateChamberTempControl: { type: Boolean }, + chamberTemperature: { type: Number }, + chamberMinimalTemperature: { type: Number }, + nozzleTemperatureInitialLayer: { type: Number }, + nozzleTemperature: { type: Number }, + nozzleTemperatureRangeLow: { type: Number }, + nozzleTemperatureRangeHigh: { type: Number }, + hotPlateTempInitialLayer: { type: Number }, + hotPlateTemp: { type: Number }, + coolPlateTempInitialLayer: { type: Number }, + coolPlateTemp: { type: Number }, + engPlateTempInitialLayer: { type: Number }, + engPlateTemp: { type: Number }, + texturedPlateTempInitialLayer: { type: Number }, + texturedPlateTemp: { type: Number }, + texturedCoolPlateTempInitialLayer: { type: Number }, + texturedCoolPlateTemp: { type: Number }, + supertackPlateTempInitialLayer: { type: Number }, + supertackPlateTemp: { type: Number }, + filamentAdaptiveVolumetricSpeed: { type: Boolean }, + filamentMaxVolumetricSpeed: { type: Number }, + volumetricSpeedCoefficients: { type: String }, + closeFanTheFirstXLayers: { type: Number }, + fullFanSpeedLayer: { type: Number }, + fanMinSpeed: { type: Number }, + fanMaxSpeed: { type: Number }, + reduceFanStopStartFreq: { type: Boolean }, + slowDownForLayerCooling: { type: Boolean }, + dontSlowDownOuterWall: { type: Boolean }, + slowDownMinSpeed: { type: Number }, + slowDownLayerTime: { type: Number }, + fanCoolingLayerTime: { type: Number }, + enableOverhangBridgeFan: { type: Boolean }, + overhangFanThreshold: { type: String }, + overhangFanSpeed: { type: Number }, + internalBridgeFanSpeed: { type: Number }, + supportMaterialInterfaceFanSpeed: { type: Number }, + ironingFanSpeed: { type: Number }, + initialLayerFanSpeed: { type: Number }, + firstXLayerFanSpeed: { type: Number }, + additionalCoolingFanSpeed: { type: Number }, + additionalFanFullSpeedLayer: { type: Number }, + closeAdditionalFanFirstXLayers: { type: Boolean }, + activateAirFiltration: { type: Boolean }, + activateAirFiltrationDuringPrint: { type: Boolean }, + activateAirFiltrationOnCompletion: { type: Boolean }, + duringPrintExhaustFanSpeed: { type: Number }, + completePrintExhaustFanSpeed: { type: Number }, + filamentRetractionLength: { type: Number }, + filamentRetractionSpeed: { type: Number }, + filamentDeretractionSpeed: { type: Number }, + filamentRetractionMinimumTravel: { type: Number }, + filamentRetractWhenChangingLayer: { type: Boolean }, + filamentRetractBeforeWipe: { type: Number }, + filamentRetractAfterWipe: { type: Number }, + filamentRetractRestartExtra: { type: Number }, + filamentRetractLiftAbove: { type: Number }, + filamentRetractLiftBelow: { type: Number }, + filamentRetractLiftEnforce: { type: String }, + filamentWipe: { type: Boolean }, + filamentWipeDistance: { type: Number }, + filamentZHop: { type: Number }, + filamentZHopTypes: { type: String }, + filamentLongRetractionsWhenCut: { type: Boolean }, + filamentRetractionDistancesWhenCut: { type: Number }, + longRetractionsWhenEc: { type: Boolean }, + retractionDistancesWhenEc: { type: Number }, + filamentLoadingSpeed: { type: Number }, + filamentLoadingSpeedStart: { type: Number }, + filamentUnloadingSpeed: { type: Number }, + filamentUnloadingSpeedStart: { type: Number }, + filamentChangeLength: { type: Number }, + filamentChangeLengthNc: { type: Number }, + filamentToolchangeDelay: { type: Number }, + filamentExtruderCompatibility: { type: Number }, + filamentExtruderVariant: { type: String }, + filamentMultitoolRamming: { type: Boolean }, + filamentMultitoolRammingFlow: { type: Number }, + filamentMultitoolRammingVolume: { type: Number }, + filamentRammingParameters: { type: String }, + filamentRammingTravelTime: { type: Number }, + filamentRammingTravelTimeNc: { type: Number }, + filamentRammingVolumetricSpeed: { type: Number }, + filamentRammingVolumetricSpeedNc: { type: Number }, + filamentMinimalPurgeOnWipeTower: { type: Number }, + filamentTowerInterfacePreExtrusionDist: { type: Number }, + filamentTowerInterfacePreExtrusionLength: { type: Number }, + filamentTowerInterfacePrintTemp: { type: Number }, + filamentTowerInterfacePurgeVolume: { type: Number }, + filamentTowerIroningArea: { type: Number }, + filamentCoolingBeforeTower: { type: Number }, + filamentCoolingInitialSpeed: { type: Number }, + filamentCoolingFinalSpeed: { type: Number }, + filamentCoolingMoves: { type: Number }, + filamentFlushTemp: { type: Number }, + filamentFlushTempFast: { type: Number }, + filamentFlushVolumetricSpeed: { type: Number }, + filamentPreCoolingTemperature: { type: Number }, + filamentPreCoolingTemperatureNc: { type: Number }, + filamentPreheatTemperatureDelta: { type: Number }, + filamentPrimeVolumeNc: { type: Number }, + filamentRetractLengthNc: { type: Number }, + filamentStampingDistance: { type: Number }, + filamentStampingLoadingSpeed: { type: Number }, + filamentStartGcode: { type: String }, + filamentEndGcode: { type: String }, + filamentChangeExtrusionRoleGcode: { type: String }, + filamentShrink: { type: String }, + filamentShrinkageCompensationZ: { type: String }, + filamentDevAmsDryingTemperature: { type: Number }, + filamentDevAmsDryingTime: { type: Number }, + filamentDevAmsDryingHeatDistortionTemperature: { type: Number }, + filamentDevAmsDryingAmsLimitations: { type: String }, + filamentDevChamberDryingBedTemperature: { type: Number }, + filamentDevChamberDryingTime: { type: Number }, + filamentDevDryingCoolingTemperature: { type: Number }, + filamentDevDryingSofteningTemperature: { type: Number }, + compatiblePrinters: [ + { type: mongoose.Schema.Types.ObjectId, ref: 'printerProfile' }, + ], + compatiblePrintersCondition: { type: String }, + compatiblePrints: [{ type: String }], + compatiblePrintsCondition: { type: String }, + filamentNotes: { type: String }, + }, + { timestamps: true } +); + +filamentProfileSchema.index({ + name: 'text', + filamentNotes: 'text', +}); + +filamentProfileSchema.virtual('id').get(function () { + return this._id; +}); + +filamentProfileSchema.set('toJSON', { virtuals: true }); + +export const filamentProfileModel = mongoose.model( + 'filamentProfile', + filamentProfileSchema +); diff --git a/src/database/schemas/production/job.schema.js b/src/database/schemas/production/job.schema.js index 07ea0d2..71a2f02 100644 --- a/src/database/schemas/production/job.schema.js +++ b/src/database/schemas/production/job.schema.js @@ -1,7 +1,15 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; -import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; +import { + aggregateRollups, + aggregateRollupsHistory, + listObjects, + editObject, + newObject, + deleteObject, +} from '../../database.js'; +import { subJobModel } from './subjob.schema.js'; const jobSchema = new mongoose.Schema( { @@ -84,6 +92,95 @@ jobSchema.statics.history = async function (from, to) { return results; }; +const normalizeId = (value) => value?._id ?? value; + +jobSchema.statics.recalculate = async function (job, user) { + const jobId = job._id || job; + if (!jobId || job?.state?.type !== 'draft') { + return; + } + + const quantity = job.quantity || 1; + const printers = (job.printers || []).map((printer) => normalizeId(printer)); + const gcodeFile = normalizeId(job.gcodeFile); + + const existingSubJobs = await listObjects({ + model: subJobModel, + filter: { job: jobId }, + sort: 'number', + order: 'ascend', + pagination: false, + }); + + if (existingSubJobs?.error) { + throw existingSubJobs; + } + + const existingSubJobsByNumber = new Map( + existingSubJobs.map((subJob) => [subJob.number, subJob]) + ); + + var printerCount = 0; + + for (let i = 0; i < quantity; i++) { + const subJobNumber = i + 1; + const printer = printers[printerCount]; + const subJobUpdateData = { + updatedAt: new Date(), + printer, + gcodeFile, + number: subJobNumber, + }; + + const existingSubJob = existingSubJobsByNumber.get(subJobNumber); + + if (existingSubJob) { + const subJobResult = await editObject({ + model: subJobModel, + id: existingSubJob._id, + updateData: subJobUpdateData, + user, + }); + if (subJobResult.error) { + throw subJobResult; + } + } else { + const subJobResult = await newObject({ + model: subJobModel, + newData: { + createdAt: new Date(), + ...subJobUpdateData, + job: jobId, + state: { type: 'draft' }, + }, + user, + }); + if (subJobResult.error) { + throw subJobResult; + } + } + + if (printerCount >= printers.length - 1) { + printerCount = 0; + } else { + printerCount += 1; + } + } + + for (const existingSubJob of existingSubJobs) { + if (existingSubJob.number > quantity) { + const deleteResult = await deleteObject({ + model: subJobModel, + id: existingSubJob._id, + user, + }); + if (deleteResult.error) { + throw deleteResult; + } + } + } +}; + jobSchema.virtual('id').get(function () { return this._id; }); diff --git a/src/database/schemas/production/printer.schema.js b/src/database/schemas/production/printer.schema.js index 35b8398..89c7962 100644 --- a/src/database/schemas/production/printer.schema.js +++ b/src/database/schemas/production/printer.schema.js @@ -25,6 +25,18 @@ const alertSchema = new Schema( { timestamps: true, _id: false } ); +const pendingSlicerUploadSchema = new Schema( + { + file: { type: Schema.Types.ObjectId, ref: 'file', required: true }, + gcodeFile: { type: Schema.Types.ObjectId, ref: 'gcodeFile', default: null }, + job: { type: Schema.Types.ObjectId, ref: 'job', default: null }, + shouldPrint: { type: Boolean, required: true, default: false }, + new: { type: Boolean, required: true, default: true }, + properties: { type: Object, required: true, default: () => ({}) }, + }, + { timestamps: true } +); + // Define the main printer schema const printerSchema = new Schema( { @@ -53,6 +65,7 @@ const printerSchema = new Schema( vendor: { type: Schema.Types.ObjectId, ref: 'vendor', default: null }, host: { type: Schema.Types.ObjectId, ref: 'host', default: null }, alerts: [alertSchema], + pendingSlicerUploads: { type: [pendingSlicerUploadSchema], default: [] }, }, { timestamps: true } ); diff --git a/src/database/schemas/production/printerprofile.schema.js b/src/database/schemas/production/printerprofile.schema.js new file mode 100644 index 0000000..ca63778 --- /dev/null +++ b/src/database/schemas/production/printerprofile.schema.js @@ -0,0 +1,107 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const minMaxNumberSchema = { + min: { type: Number }, + max: { type: Number } +}; + +const thumbnailSchema = new mongoose.Schema( + { + width: { type: Number }, + height: { type: Number } + }, + { _id: true } +); + +const bedExcludeAreaSchema = new mongoose.Schema( + { + x: { type: Number }, + y: { type: Number } + }, + { _id: true } +); + +const extruderSchema = new mongoose.Schema( + { + nozzleDiameter: { type: Number }, + nozzleVolume: { type: Number }, + minLayerHeight: { type: Number }, + maxLayerHeight: { type: Number }, + positionOffsetX: { type: Number }, + positionOffsetY: { type: Number }, + retractionLength: { type: Number }, + retractionExtraLengthOnRestart: { type: Number }, + retractionSpeed: { type: Number }, + deretractionSpeed: { type: Number }, + retractionTravelDistanceThreshold: { type: Number }, + retractOnLayerChange: { type: Boolean }, + wipeWhileRetracting: { type: Boolean }, + wipeDistance: { type: Number }, + retractAmountBeforeWipe: { type: Number }, + retractAmountAfterWipe: { type: Number }, + zHopOnSurfaces: { type: String }, + zHopType: { type: String }, + zHopHeight: { type: Number }, + zHopTravelingAngle: { type: Number }, + zHopOnlyLiftZAbove: { type: Number }, + zHopOnlyLiftZBelow: { type: Number }, + materialSwitchRetractionLength: { type: Number }, + materialSwitchExtraLengthOnRestart: { type: Number }, + longRetractionWhenCut: { type: Boolean } + }, + { _id: true } +); + +const printerProfileSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true }, + printer: { type: mongoose.Schema.Types.ObjectId, ref: 'printer', default: null }, + extruders: [extruderSchema], + printBedWidth: { type: Number }, + printBedHeight: { type: Number }, + originX: { type: Number }, + originY: { type: Number }, + bedExcludeArea: [bedExcludeAreaSchema], + printableHeight: { type: Number }, + supportMultiBedTypes: { type: Boolean }, + bestObjectPositionX: { type: Number }, + bestObjectPositionY: { type: Number }, + zOffset: { type: Number }, + preferredOrientation: { type: Number }, + machineMaxAccelerationRetracting: minMaxNumberSchema, + machineMaxSpeedE: minMaxNumberSchema, + machineMaxSpeedX: minMaxNumberSchema, + machineMaxSpeedY: minMaxNumberSchema, + machinePauseGcode: { type: String }, + machineStartGcode: { type: String }, + machineEndGcode: { type: String }, + layerChangeGcode: { type: String }, + beforeLayerChangeGcode: { type: String }, + printerNotes: { type: String }, + scanFirstLayer: { type: Boolean }, + machineLoadFilamentTime: { type: Number }, + machineUnloadFilamentTime: { type: Number }, + thumbnails: [thumbnailSchema], + auxiliaryFan: { type: Boolean }, + machineMaxJunctionDeviation: minMaxNumberSchema + }, + { timestamps: true } +); + +printerProfileSchema.index({ + name: 'text', + printerNotes: 'text' +}); + +printerProfileSchema.virtual('id').get(function () { + return this._id; +}); + +printerProfileSchema.set('toJSON', { virtuals: true }); + +export const printerProfileModel = mongoose.model( + 'printerProfile', + printerProfileSchema +); diff --git a/src/stats/statsmanager.js b/src/stats/statsmanager.js index 72ffed5..55d8cc6 100644 --- a/src/stats/statsmanager.js +++ b/src/stats/statsmanager.js @@ -1,5 +1,6 @@ import log4js from 'log4js'; import { loadConfig } from '../config.js'; +import { formatTraceData } from '../utils.js'; import { natsServer } from '../database/nats.js'; const config = loadConfig(); @@ -53,7 +54,7 @@ export class StatsManager { async sendStats(type, stats) { try { - logger.trace(`Publishing stats: type=${type}, stats:`, stats); + logger.trace(`Publishing stats: type=${type}, stats: ${formatTraceData(stats)}`); await natsServer.publish(`${type}s.stats`, stats); return { success: true }; } catch (error) { diff --git a/src/updates/updatemanager.js b/src/updates/updatemanager.js index ee73492..e8b7d1c 100644 --- a/src/updates/updatemanager.js +++ b/src/updates/updatemanager.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import { loadConfig } from '../config.js'; import { natsServer } from '../database/nats.js'; import { expandObjectIds } from '../database/utils.js'; +import { formatTraceData } from '../utils.js'; const config = loadConfig(); // Setup logger @@ -102,11 +103,13 @@ export class UpdateManager { ); if (!matches) { - logger.trace(`Filtered ${eventName} event:`, { - objectType, - filter: normalizedFilter, - value - }); + logger.trace( + `Filtered ${eventName} event: ${formatTraceData({ + objectType, + filter: normalizedFilter, + value, + })}` + ); return; } @@ -126,7 +129,7 @@ export class UpdateManager { ); await natsServer.subscribe(subject, owner, async (key, value) => { - logger.trace('Object new event:', value); + logger.trace(`Object new event: ${formatTraceData(value)}`); this.emitObjectTypeEvent( 'objectNew', objectType, @@ -148,7 +151,7 @@ export class UpdateManager { ); await natsServer.subscribe(subject, owner, async (key, value) => { - logger.trace('Object delete event:', value); + logger.trace(`Object delete event: ${formatTraceData(value)}`); this.emitObjectTypeEvent( 'objectDelete', objectType, diff --git a/src/utils.js b/src/utils.js index 363a9f3..6455714 100644 --- a/src/utils.js +++ b/src/utils.js @@ -40,6 +40,12 @@ export function generateEtcId() { return nanoid(24); } +export function formatTraceData(value, maxLength = 128) { + if (value === undefined) return 'undefined'; + const text = typeof value === 'string' ? value : JSON.stringify(value); + return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; +} + export function getChangedValues(oldObj, newObj, old = false) { const changes = {};