Add formatTraceData utility and integrate into logging
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-07-20 02:11:56 +01:00
parent 15132fefa0
commit aad49bfcfd
14 changed files with 572 additions and 72 deletions

View File

@ -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,
});
});
});

View File

@ -5,7 +5,7 @@ import { redisServer } from '../database/redis.js';
import { editObject, getObject, listObjects } from '../database/database.js'; import { editObject, getObject, listObjects } from '../database/database.js';
import { hostModel } from '../database/schemas/management/host.schema.js'; import { hostModel } from '../database/schemas/management/host.schema.js';
import { userModel } from '../database/schemas/management/user.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(); const config = loadConfig();
@ -21,20 +21,11 @@ const SESSION_KEY_PREFIX = 'session:';
export class SessionAuth { export class SessionAuth {
constructor() { constructor() {
this.config = config.auth; this.config = config.auth;
this.tokenCache = new Map();
} }
async verifyToken(token) { async verifyToken(token) {
if (!token) return { valid: false }; 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 { try {
const key = SESSION_KEY_PREFIX + token; const key = SESSION_KEY_PREFIX + token;
const session = await redisServer.getKey(key); 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 }; return { valid: true, user: session.user };
} catch (error) { } catch (error) {
logger.error('Session verification error:', error.message); logger.error('Session verification error:', error.message);
@ -77,9 +63,9 @@ export class KeycloakAuth extends SessionAuth {}
export class CodeAuth { export class CodeAuth {
async verifyCode(id, authCode) { async verifyCode(id, authCode) {
try { try {
logger.trace('Verifying code:', { id, authCode }); logger.trace(`Verifying code: ${formatTraceData({ id, authCode })}`);
const host = await getObject({ model: hostModel, id, cached: true }); const host = await getObject({ model: hostModel, id, cached: true });
logger.trace('Host retrieved:', host); logger.trace(`Host retrieved: ${formatTraceData(host)}`);
if (host == undefined) { if (host == undefined) {
const error = 'Host not found.'; const error = 'Host not found.';
logger.warn(error, 'Host:', id); logger.warn(error, 'Host:', id);
@ -100,7 +86,7 @@ export class CodeAuth {
logger.warn(error, 'Host:', id); logger.warn(error, 'Host:', id);
return { valid: false, error: error }; return { valid: false, error: error };
} }
logger.trace('Code verification successful:', host); logger.trace(`Code verification successful: ${formatTraceData(host)}`);
return { valid: true, host: host }; return { valid: true, host: host };
} catch (error) { } catch (error) {
logger.error('Code verification error:', error.message); logger.error('Code verification error:', error.message);

View File

@ -10,7 +10,7 @@ import {
} from './utils.js'; } from './utils.js';
import log4js from 'log4js'; import log4js from 'log4js';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { getQueryToCacheKey } from '../utils.js'; import { formatTraceData, getQueryToCacheKey } from '../utils.js';
import { redisServer } from './redis.js'; import { redisServer } from './redis.js';
import { auditLogModel } from './schemas/management/auditlog.schema.js'; import { auditLogModel } from './schemas/management/auditlog.schema.js';
@ -61,7 +61,7 @@ export const updateObjectCache = async ({
// Get all keys matching the filter pattern // Get all keys matching the filter pattern
const matchingKeys = await redisServer.getKeysByPattern(cacheKeyFilter); 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 // Merge the object with each cached object and update
const mergedObjects = []; const mergedObjects = [];
@ -124,15 +124,17 @@ export const listObjects = async ({
cached = false cached = false
}) => { }) => {
try { try {
logger.trace('Listing objects:', { logger.trace(
`Listing objects: ${formatTraceData({
model, model,
populate, populate,
filter, filter,
sort, sort,
order, order,
project, project,
cached cached,
}); })}`
);
// Fix: descend should be -1, ascend should be 1 // Fix: descend should be -1, ascend should be 1
const sortOrder = order === 'descend' ? -1 : 1; const sortOrder = order === 'descend' ? -1 : 1;
@ -177,7 +179,8 @@ export const listObjects = async ({
const finalResult = expandObjectIds(queryResult); const finalResult = expandObjectIds(queryResult);
logger.trace('Retreived from database:', { logger.trace(
`Retreived from database: ${formatTraceData({
model, model,
populate, populate,
filter, filter,
@ -185,8 +188,9 @@ export const listObjects = async ({
order, order,
project, project,
cached, cached,
length: finalResult.length length: finalResult.length,
}); })}`
);
return finalResult; return finalResult;
} catch (error) { } catch (error) {
logger.error('Object list error:', error); logger.error('Object list error:', error);
@ -202,11 +206,13 @@ export const getObject = async ({
cached = false cached = false
}) => { }) => {
try { try {
logger.trace('Getting object:', { logger.trace(
`Getting object: ${formatTraceData({
model, model,
id, id,
populate populate,
}); })}`
);
if (cached == true) { if (cached == true) {
const cachedObject = await retrieveObjectCache({ model, id, populate }); const cachedObject = await retrieveObjectCache({ model, id, populate });
@ -238,13 +244,15 @@ export const getObject = async ({
return undefined; return undefined;
} }
logger.trace('Retreived object from database:', { logger.trace(
`Retreived object from database: ${formatTraceData({
model, model,
id, id,
populate populate,
}); })}`
);
logger.trace(finalResult); logger.trace(formatTraceData(finalResult));
await updateObjectCache({ await updateObjectCache({
model: model, model: model,

View File

@ -1,6 +1,7 @@
import { connect } from '@nats-io/transport-node'; import { connect } from '@nats-io/transport-node';
import log4js from 'log4js'; import log4js from 'log4js';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger('Nats'); const logger = log4js.getLogger('Nats');
@ -72,7 +73,7 @@ class NatsServer {
try { try {
client.publish(subject, this.textEncoder.encode(payload)); 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 }; return { success: true };
} catch (error) { } catch (error) {
logger.error(`Failed to publish to subject ${subject}:`, error); logger.error(`Failed to publish to subject ${subject}:`, error);
@ -94,7 +95,9 @@ class NatsServer {
); );
const responseData = this.textDecoder.decode(response.data); 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 to parse as JSON, fallback to string
try { try {

View File

@ -1,6 +1,7 @@
import { createClient } from 'redis'; import { createClient } from 'redis';
import log4js from 'log4js'; import log4js from 'log4js';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger('Redis'); const logger = log4js.getLogger('Redis');
@ -80,7 +81,7 @@ class RedisServer {
COUNT: 100 COUNT: 100
}); });
cursor = result.cursor; cursor = result.cursor;
logger.trace('Result:', result); logger.trace(`Result: ${formatTraceData(result)}`);
keys.push(...result.keys); keys.push(...result.keys);
} while (cursor !== '0'); } while (cursor !== '0');
return keys; return keys;

View File

@ -8,28 +8,28 @@ const userSettingsSchema = new mongoose.Schema({
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'user', ref: 'user',
required: true, required: true,
unique: true unique: true,
}, },
defaults: { defaults: {
viewMode: { type: Schema.Types.Mixed, default: () => ({}) }, viewMode: { type: Schema.Types.Mixed, default: () => ({}) },
filterSidebarVisibility: { filterSidebarVisibility: {
type: Schema.Types.Mixed, type: Schema.Types.Mixed,
default: () => ({}) default: () => ({}),
}, },
columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) }, columnVisibility: { type: Schema.Types.Mixed, default: () => ({}) },
collapseState: { type: Schema.Types.Mixed, default: () => ({}) } collapseState: { type: Schema.Types.Mixed, default: () => ({}) },
}, },
createdAt: { createdAt: {
type: Date, type: Date,
required: true, required: true,
default: Date.now default: Date.now,
}, },
updatedAt: { updatedAt: {
type: Date, type: Date,
required: true, required: true,
default: Date.now default: Date.now,
} },
}); });
userSettingsSchema.virtual('id').get(function () { userSettingsSchema.virtual('id').get(function () {
@ -38,7 +38,4 @@ userSettingsSchema.virtual('id').get(function () {
userSettingsSchema.set('toJSON', { virtuals: true }); userSettingsSchema.set('toJSON', { virtuals: true });
export const userSettingsModel = mongoose.model( export const userSettingsModel = mongoose.model('userSettings', userSettingsSchema);
'userSettings',
userSettingsSchema
);

View File

@ -1,6 +1,8 @@
import { jobModel } from './production/job.schema.js'; import { jobModel } from './production/job.schema.js';
import { subJobModel } from './production/subjob.schema.js'; import { subJobModel } from './production/subjob.schema.js';
import { printerModel } from './production/printer.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 { filamentModel } from './management/filament.schema.js';
import { filamentSkuModel } from './management/filamentsku.schema.js'; import { filamentSkuModel } from './management/filamentsku.schema.js';
import { gcodeFileModel } from './production/gcodefile.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js';
@ -55,6 +57,20 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'Printer', 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: { FIL: {
model: filamentModel, model: filamentModel,
idField: '_id', idField: '_id',

View File

@ -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
);

View File

@ -1,7 +1,15 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; 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( const jobSchema = new mongoose.Schema(
{ {
@ -84,6 +92,95 @@ jobSchema.statics.history = async function (from, to) {
return results; 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 () { jobSchema.virtual('id').get(function () {
return this._id; return this._id;
}); });

View File

@ -25,6 +25,18 @@ const alertSchema = new Schema(
{ timestamps: true, _id: false } { 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 // Define the main printer schema
const printerSchema = new Schema( const printerSchema = new Schema(
{ {
@ -53,6 +65,7 @@ const printerSchema = new Schema(
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', default: null }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', default: null },
host: { type: Schema.Types.ObjectId, ref: 'host', default: null }, host: { type: Schema.Types.ObjectId, ref: 'host', default: null },
alerts: [alertSchema], alerts: [alertSchema],
pendingSlicerUploads: { type: [pendingSlicerUploadSchema], default: [] },
}, },
{ timestamps: true } { timestamps: true }
); );

View File

@ -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
);

View File

@ -1,5 +1,6 @@
import log4js from 'log4js'; import log4js from 'log4js';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { formatTraceData } from '../utils.js';
import { natsServer } from '../database/nats.js'; import { natsServer } from '../database/nats.js';
const config = loadConfig(); const config = loadConfig();
@ -53,7 +54,7 @@ export class StatsManager {
async sendStats(type, stats) { async sendStats(type, stats) {
try { 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); await natsServer.publish(`${type}s.stats`, stats);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@ -3,6 +3,7 @@ import _ from 'lodash';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { natsServer } from '../database/nats.js'; import { natsServer } from '../database/nats.js';
import { expandObjectIds } from '../database/utils.js'; import { expandObjectIds } from '../database/utils.js';
import { formatTraceData } from '../utils.js';
const config = loadConfig(); const config = loadConfig();
// Setup logger // Setup logger
@ -102,11 +103,13 @@ export class UpdateManager {
); );
if (!matches) { if (!matches) {
logger.trace(`Filtered ${eventName} event:`, { logger.trace(
`Filtered ${eventName} event: ${formatTraceData({
objectType, objectType,
filter: normalizedFilter, filter: normalizedFilter,
value value,
}); })}`
);
return; return;
} }
@ -126,7 +129,7 @@ export class UpdateManager {
); );
await natsServer.subscribe(subject, owner, async (key, value) => { await natsServer.subscribe(subject, owner, async (key, value) => {
logger.trace('Object new event:', value); logger.trace(`Object new event: ${formatTraceData(value)}`);
this.emitObjectTypeEvent( this.emitObjectTypeEvent(
'objectNew', 'objectNew',
objectType, objectType,
@ -148,7 +151,7 @@ export class UpdateManager {
); );
await natsServer.subscribe(subject, owner, async (key, value) => { await natsServer.subscribe(subject, owner, async (key, value) => {
logger.trace('Object delete event:', value); logger.trace(`Object delete event: ${formatTraceData(value)}`);
this.emitObjectTypeEvent( this.emitObjectTypeEvent(
'objectDelete', 'objectDelete',
objectType, objectType,

View File

@ -40,6 +40,12 @@ export function generateEtcId() {
return nanoid(24); 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) { export function getChangedValues(oldObj, newObj, old = false) {
const changes = {}; const changes = {};