Add formatTraceData utility and integrate into logging
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
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:
parent
15132fefa0
commit
aad49bfcfd
91
src/auth/__tests__/auth.test.js
Normal file
91
src/auth/__tests__/auth.test.js
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
171
src/database/schemas/production/filamentprofile.schema.js
Normal file
171
src/database/schemas/production/filamentprofile.schema.js
Normal 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
|
||||
);
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
|
||||
107
src/database/schemas/production/printerprofile.schema.js
Normal file
107
src/database/schemas/production/printerprofile.schema.js
Normal 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
|
||||
);
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user