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 { 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);
|
||||||
|
|||||||
@ -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(
|
||||||
model,
|
`Listing objects: ${formatTraceData({
|
||||||
populate,
|
model,
|
||||||
filter,
|
populate,
|
||||||
sort,
|
filter,
|
||||||
order,
|
sort,
|
||||||
project,
|
order,
|
||||||
cached
|
project,
|
||||||
});
|
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,16 +179,18 @@ export const listObjects = async ({
|
|||||||
|
|
||||||
const finalResult = expandObjectIds(queryResult);
|
const finalResult = expandObjectIds(queryResult);
|
||||||
|
|
||||||
logger.trace('Retreived from database:', {
|
logger.trace(
|
||||||
model,
|
`Retreived from database: ${formatTraceData({
|
||||||
populate,
|
model,
|
||||||
filter,
|
populate,
|
||||||
sort,
|
filter,
|
||||||
order,
|
sort,
|
||||||
project,
|
order,
|
||||||
cached,
|
project,
|
||||||
length: finalResult.length
|
cached,
|
||||||
});
|
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(
|
||||||
model,
|
`Getting object: ${formatTraceData({
|
||||||
id,
|
model,
|
||||||
populate
|
id,
|
||||||
});
|
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(
|
||||||
model,
|
`Retreived object from database: ${formatTraceData({
|
||||||
id,
|
model,
|
||||||
populate
|
id,
|
||||||
});
|
populate,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
|
||||||
logger.trace(finalResult);
|
logger.trace(formatTraceData(finalResult));
|
||||||
|
|
||||||
await updateObjectCache({
|
await updateObjectCache({
|
||||||
model: model,
|
model: model,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
|
||||||
);
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
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 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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 }
|
||||||
);
|
);
|
||||||
|
|||||||
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 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) {
|
||||||
|
|||||||
@ -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(
|
||||||
objectType,
|
`Filtered ${eventName} event: ${formatTraceData({
|
||||||
filter: normalizedFilter,
|
objectType,
|
||||||
value
|
filter: normalizedFilter,
|
||||||
});
|
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,
|
||||||
|
|||||||
@ -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 = {};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user