Add inventory and management schemas for filament, part, and stock management

- Introduced new schemas for managing inventory, including filamentStock, partStock, stockAudit, stockEvent, and their respective models.
- Added management schemas for user, vendor, material, and various document types to enhance data structure and organization.
- Implemented necessary fields and relationships to support inventory tracking and management functionalities.
This commit is contained in:
Tom Butcher 2025-08-18 01:06:38 +01:00
parent 5584e61583
commit ce15d3dbfc
22 changed files with 1332 additions and 0 deletions

490
src/database/database.js Normal file
View File

@ -0,0 +1,490 @@
import _ from 'lodash';
import NodeCache from 'node-cache';
import {
deleteAuditLog,
expandObjectIds,
editAuditLog,
distributeUpdate,
newAuditLog,
distributeNew
} from './utils.js';
import log4js from 'log4js';
import { loadConfig } from '../config.js';
import { userModel } from './schemas/management/user.schema.js';
const config = loadConfig();
const logger = log4js.getLogger('Database');
const cacheLogger = log4js.getLogger('Local Cache');
logger.level = config.server.logLevel;
cacheLogger.level = config.server.logLevel;
const modelCaches = new Map();
const listCache = new NodeCache({
stdTTL: 30, // 30 sec expiration
checkperiod: 600, // 30 sec periodic cleanup
useClones: false // Don't clone objects for better performance
});
function getModelCache(model) {
const modelName = model.modelName;
const modelCache = modelCaches.get(modelName);
if (modelCache == undefined) {
logger.trace('Creating new model cache...');
const newModelCache = new NodeCache({
stdTTL: 30, // 30 sec expiration
checkperiod: 30, // 30 sec periodic cleanup
useClones: false // Don't clone objects for better performance
});
modelCaches.set(modelName, newModelCache);
return newModelCache;
}
logger.trace('Getting model cache...');
return modelCache;
}
export const retrieveObjectCache = ({ model, id }) => {
cacheLogger.trace('Retrieving:', {
model: model.modelName,
id
});
const modelCache = getModelCache(model);
const cachedObject = modelCache.get(id);
if (cachedObject == undefined) {
cacheLogger.trace('Miss:', {
model: model.modelName,
id
});
return undefined;
}
cacheLogger.trace('Hit:', {
model: model.modelName,
id
});
return cachedObject;
};
export const retrieveObjectsCache = ({ model }) => {
cacheLogger.trace('Retrieving:', {
model: model.modelName
});
const modelCache = getModelCache(model);
const modelCacheKeys = modelCache.keys();
const cachedList = listCache.get(model.modelName);
if (cachedList == true) {
const cachedObjects = modelCacheKeys.map(key => modelCache.get(key));
cacheLogger.trace('Hit:', {
model: model.modelName,
length: cachedObjects.length
});
return cachedObjects;
}
cacheLogger.trace('Miss:', {
model: model.modelName
});
return undefined;
};
export const updateObjectCache = ({ model, id, object }) => {
cacheLogger.trace('Updating:', {
model: model.modelName,
id
});
const modelCache = getModelCache(model);
const cachedObject = modelCache.get(id) || {};
const mergedObject = _.merge(cachedObject, object);
modelCache.set(id, mergedObject);
cacheLogger.trace('Updated:', {
model: model.modelName,
id
});
return mergedObject;
};
export const deleteObjectCache = ({ model, id }) => {
cacheLogger.trace('Deleting:', {
model: model.modelName,
id
});
modelCache.del(id);
cacheLogger.trace('Deleted:', {
model: model.modelName,
id
});
return mergedObject;
};
export const updateObjectsCache = ({ model, objects }) => {
cacheLogger.trace('Updating:', {
model: model.modelName,
length: objects.length
});
const modelCache = getModelCache(model);
objects.forEach(object => {
const cachedObject = modelCache.get(object._id) || {};
const mergedObject = _.merge(cachedObject, object);
modelCache.set(object._id, mergedObject);
});
listCache.set(model.modelName, true);
cacheLogger.trace('Updated:', {
model: model.modelName,
length: objects.length
});
return mergedObject;
};
// Reusable function to list objects with aggregation, filtering, search, sorting, and pagination
export const listObjects = async ({
model,
populate = [],
filter = {},
sort = '',
order = 'ascend',
project, // optional: override default projection
cached = false
}) => {
try {
logger.trace('Listing objects:', {
model,
populate,
page,
limit,
filter,
sort,
order,
project,
cache
});
var cacheKey = undefined;
var modelCache = getModelCache(model);
if (cached == true) {
const objectsCache = retrieveObjectsCache({ model });
if (objectsCache != undefined) {
return objectsCache;
}
}
// Fix: descend should be -1, ascend should be 1
const sortOrder = order === 'descend' ? -1 : 1;
if (!sort || sort === '') {
sort = 'createdAt';
}
// Translate parent._id to parent for Mongoose
if (filter['parent._id']) {
filter.parent = filter['parent._id'];
delete filter['parent._id'];
}
// Translate owner._id to owner for Mongoose
if (filter['owner._id']) {
filter.owner = filter['owner._id'];
delete filter['owner._id'];
}
// Use find with population and filter
let query = model.find(filter).sort({ [sort]: sortOrder });
// Handle populate (array or single value)
if (populate) {
if (Array.isArray(populate)) {
for (const pop of populate) {
query = query.populate(pop);
}
} else if (typeof populate === 'string' || typeof populate === 'object') {
query = query.populate(populate);
}
}
// Handle select (projection)
if (project) {
query = query.select(project);
}
query = query.lean();
const queryResult = await query;
const finalResult = expandObjectIds(queryResult);
updateObjectsCache({ model, objects });
logger.trace('Retreived from database:', {
model,
populate,
page,
limit,
filter,
sort,
order,
project,
cache
});
return finalResult;
} catch (error) {
logger.error('Object list error:', error);
return { error: error, code: 500 };
}
};
// Reusable function to get a single object by ID
export const getObject = async ({ model, id, populate, cached = false }) => {
try {
logger.trace('Getting object:', {
model,
id,
populate
});
if (cached == true) {
const cachedObject = retrieveObjectCache({ model, id });
if (cachedObject != undefined) {
return cachedObject;
}
}
let query = model.findById(id).lean();
// Handle populate (array or single value)
if (populate) {
if (Array.isArray(populate)) {
for (const pop of populate) {
query = query.populate(pop);
}
} else if (typeof populate === 'string' || typeof populate === 'object') {
query = query.populate(populate);
}
}
const finalResult = await query;
if (!finalResult) {
logger.warn('Object not found in database:', {
model,
id,
populate
});
return undefined;
}
logger.trace('Retreived object from database:', {
model,
id,
populate
});
updateObjectCache({
model: model,
id: finalResult._id.toString(),
object: finalResult
});
return finalResult;
} catch (error) {
logger.error('An error retreiving object:', error.message);
return undefined;
}
};
// Reusable function to get a single object by ID
export const getObjectByFilter = async ({ model, filter, populate }) => {
try {
logger.trace('Getting object:', {
model,
filter,
populate
});
let query = model.findOne(filter).lean();
// Handle populate (array or single value)
if (populate) {
if (Array.isArray(populate)) {
for (const pop of populate) {
query = query.populate(pop);
}
} else if (typeof populate === 'string' || typeof populate === 'object') {
query = query.populate(populate);
}
}
const finalResult = await query;
if (!finalResult) {
logger.warn('Object not found in database:', {
model,
filter,
populate
});
return undefined;
}
logger.trace('Retreived object from database:', {
model,
filter,
populate
});
updateObjectCache({
model: model,
id: finalResult._id.toString(),
object: finalResult
});
return finalResult;
} catch (error) {
logger.error('An error retreiving object:', error.message);
return undefined;
}
};
// Reusable function to edit an object by ID, with audit logging and distribution
export const editObject = async ({
model,
id,
updateData,
owner = undefined,
ownerType = undefined,
populate
}) => {
try {
// Determine parentType from model name
const parentType = model.modelName ? model.modelName : 'unknown';
// Fetch the and update object
var query = model.findByIdAndUpdate(id, updateData).lean();
if (populate) {
if (Array.isArray(populate)) {
for (const pop of populate) {
query = query.populate(pop);
}
} else if (typeof populate === 'string' || typeof populate === 'object') {
query = query.populate(populate);
}
}
const previousObject = await query;
if (!previousObject) {
return { error: `${parentType} not found.`, code: 404 };
}
const previousExpandedObject = expandObjectIds(previousObject);
if (owner != undefined && ownerType != undefined) {
// Audit log before update
await editAuditLog(
previousExpandedObject,
{ ...previousExpandedObject, ...updateData },
id,
parentType,
owner,
ownerType
);
}
// Distribute update
await distributeUpdate(updateData, id, parentType);
updateObjectCache({
model: model,
id: id.toString(),
object: { ...previousExpandedObject, ...updateData }
});
return { ...previousExpandedObject, ...updateData };
} catch (error) {
logger.error('editObject error:', error);
return { error: error.message, code: 500 };
}
};
// Reusable function to create a new object
export const newObject = async ({
model,
newData,
owner = null,
ownerType = undefined
}) => {
try {
const parentType = model.modelName ? model.modelName : 'unknown';
const result = await model.create(newData);
if (!result || result.length === 0) {
return { error: 'No object created.', code: 500 };
}
const created = result;
if (owner != undefined && ownerType != undefined) {
await newAuditLog(newData, created._id, parentType, owner, ownerType);
}
await distributeNew(created._id, parentType);
updateObjectCache({
model: model,
id: created._id.toString(),
object: { _id: created._id, ...newData }
});
return created;
} catch (error) {
logger.error('newObject error:', error);
return { error: error.message, code: 500 };
}
};
// Reusable function to delete an object by ID, with audit logging and distribution
export const deleteObject = async ({
model,
id,
owner = null,
ownerType = undefined
}) => {
try {
const parentType = model.modelName ? model.modelName : 'unknown';
// Delete the object
const result = await model.findByIdAndDelete(id);
if (!result) {
return { error: `${parentType} not found.`, code: 404 };
}
if (owner != undefined && ownerType != undefined) {
// Audit log the deletion
await deleteAuditLog(result, id, parentType, owner, ownerType);
}
deleteObjectCache({ model: model, id: id.toString() });
// Distribute the deletion event
await distributeUpdate({ deleted: true }, id, parentType);
return { deleted: true, id: id.toString() };
} catch (error) {
logger.error('deleteObject error:', error);
return { error: error.message, code: 500 };
}
};

View File

@ -0,0 +1,33 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
// Define the main filamentStock schema
const filamentStockSchema = new Schema(
{
state: {
type: { type: String, required: true },
percent: { type: String, required: true },
},
startingWeight: {
net: { type: Number, required: true },
gross: { type: Number, required: true },
},
currentWeight: {
net: { type: Number, required: true },
gross: { type: Number, required: true },
},
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament' },
},
{ timestamps: true }
);
// Add virtual id getter
filamentStockSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
filamentStockSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const filamentStockModel = mongoose.model('filamentStock', filamentStockSchema);

View File

@ -0,0 +1,25 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
// Define the main partStock schema
const partStockSchema = new Schema(
{
name: { type: String, required: true },
fileName: { type: String, required: false },
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part' },
startingQuantity: { type: Number, required: true },
currentQuantity: { type: Number, required: true },
},
{ timestamps: true }
);
// Add virtual id getter
partStockSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
partStockSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const partStockModel = mongoose.model('partStock', partStockSchema);

View File

@ -0,0 +1,38 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const stockAuditItemSchema = new Schema({
type: { type: String, enum: ['filament', 'part'], required: true },
stock: { type: Schema.Types.ObjectId, required: true },
expectedQuantity: { type: Number, required: true },
actualQuantity: { type: Number, required: true },
notes: { type: String },
});
const stockAuditSchema = new Schema(
{
type: { type: String, required: true },
status: {
type: String,
enum: ['pending', 'in_progress', 'completed', 'cancelled'],
default: 'pending',
required: true,
},
notes: { type: String },
items: [stockAuditItemSchema],
createdBy: { type: Schema.Types.ObjectId, ref: 'user', required: true },
completedAt: { type: Date },
},
{ timestamps: true }
);
// Add virtual id getter
stockAuditSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
stockAuditSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const stockAuditModel = mongoose.model('stockAudit', stockAuditSchema);

View File

@ -0,0 +1,43 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const stockEventSchema = new Schema(
{
value: { type: Number, required: true },
current: { type: Number, required: true },
unit: { type: String, required: true },
parent: {
type: Schema.Types.ObjectId,
refPath: 'parentType',
required: true,
},
parentType: {
type: String,
required: true,
enum: ['filamentStock', 'partStock', 'productStock'], // Add other models as needed
},
owner: {
type: Schema.Types.ObjectId,
refPath: 'ownerType',
required: true,
},
ownerType: {
type: String,
required: true,
enum: ['user', 'subJob', 'stockAudit'],
},
timestamp: { type: Date, default: Date.now },
},
{ timestamps: true }
);
// Add virtual id getter
stockEventSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
stockEventSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const stockEventModel = mongoose.model('stockEvent', stockEventSchema);

View File

@ -0,0 +1,64 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const auditLogSchema = new Schema(
{
changes: {
old: { type: Object, required: true },
new: { type: Object, required: true }
},
parent: {
type: Schema.Types.ObjectId,
refPath: 'parentType',
required: true
},
parentType: {
type: String,
required: true,
enum: [
'printer',
'job',
'subJob',
'filamentStock',
'stockEvent',
'vendor',
'part',
'product',
'material',
'filament',
'gcodeFile',
'noteType',
'note',
'user',
'host'
] // Add other models as needed
},
owner: {
type: Schema.Types.ObjectId,
refPath: 'ownerType',
required: true
},
ownerType: {
type: String,
required: true,
enum: ['user', 'printer', 'host']
},
operation: {
type: String,
required: true,
enum: ['edit', 'new', 'delete']
}
},
{ timestamps: true }
);
// Add virtual id getter
auditLogSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
auditLogSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const auditLogModel = mongoose.model('auditLog', auditLogSchema);

View File

@ -0,0 +1,33 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const documentSizeSchema = new Schema(
{
name: {
type: String,
required: true,
unique: true,
},
width: {
type: Number,
required: true,
default: 0,
},
height: {
type: Number,
required: true,
default: 0,
},
},
{ timestamps: true }
);
// Add virtual id getter
documentSizeSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
documentSizeSchema.set('toJSON', { virtuals: true });
export const documentSizeModel = mongoose.model('documentSize', documentSizeSchema);

View File

@ -0,0 +1,61 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const documentTemplateSchema = new Schema(
{
name: {
type: String,
required: true,
unique: true,
},
objectType: { type: String, required: false },
tags: [{ type: String }],
active: {
type: Boolean,
required: true,
default: true,
},
global: {
type: Boolean,
required: true,
default: false,
},
parent: {
type: Schema.Types.ObjectId,
ref: 'documentTemplate',
required: false,
},
documentSize: {
type: Schema.Types.ObjectId,
ref: 'documentSize',
required: true,
},
documentPrinters: [
{
type: Schema.Types.ObjectId,
ref: 'documentPrinter',
required: false,
},
],
content: {
type: String,
required: false,
default: '<Container></Container>',
},
testObject: {
type: Schema.Types.Mixed,
required: false,
},
},
{ timestamps: true }
);
// Add virtual id getter
documentTemplateSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
documentTemplateSchema.set('toJSON', { virtuals: true });
export const documentTemplateModel = mongoose.model('documentTemplate', documentTemplateSchema);

View File

@ -0,0 +1,26 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const filamentSchema = new mongoose.Schema({
name: { required: true, type: String },
barcode: { required: false, type: String },
url: { required: false, type: String },
image: { required: false, type: Buffer },
color: { required: true, type: String },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
type: { required: true, type: String },
cost: { required: true, type: Number },
diameter: { required: true, type: Number },
density: { required: true, type: Number },
createdAt: { required: true, type: Date },
updatedAt: { required: true, type: Date },
emptySpoolWeight: { required: true, type: Number },
});
filamentSchema.virtual('id').get(function () {
return this._id.toHexString();
});
filamentSchema.set('toJSON', { virtuals: true });
export const filamentModel = mongoose.model('filament', filamentSchema);

View File

@ -0,0 +1,66 @@
import mongoose from 'mongoose';
// Define the device schema
const deviceInfoSchema = new mongoose.Schema(
{
os: {
platform: { type: String },
type: { type: String },
release: { type: String },
arch: { type: String },
hostname: { type: String },
uptime: { type: Number }
},
cpu: {
cores: { type: Number },
model: { type: String },
speedMHz: { type: Number }
},
memory: {
totalGB: { type: String }, // stored as string from .toFixed(2), could also use Number
freeGB: { type: String }
},
network: {
type: mongoose.Schema.Types.Mixed // since it's an object with dynamic interface names
},
user: {
uid: { type: Number },
gid: { type: Number },
username: { type: String },
homedir: { type: String },
shell: { type: String }
},
process: {
nodeVersion: { type: String },
pid: { type: Number },
cwd: { type: String },
execPath: { type: String }
}
},
{ _id: false }
);
const hostSchema = new mongoose.Schema({
name: { required: true, type: String },
tags: [{ required: false, type: String }],
online: { required: true, type: Boolean, default: false },
state: {
type: { type: String, required: true, default: 'offline' },
message: { type: String, required: false },
percent: { type: Number, required: false }
},
active: { required: true, type: Boolean, default: true },
connectedAt: { required: false, type: Date },
authCode: { required: false, type: String },
otp: { required: false, type: String },
otpExpiresAt: { required: false, type: Date },
deviceInfo: deviceInfoSchema
});
hostSchema.virtual('id').get(function () {
return this._id.toHexString();
});
hostSchema.set('toJSON', { virtuals: true });
export const hostModel = mongoose.model('host', hostSchema);

View File

@ -0,0 +1,16 @@
import mongoose from 'mongoose';
const materialSchema = new mongoose.Schema({
name: { required: true, type: String },
url: { required: false, type: String },
image: { required: false, type: Buffer },
tags: [{ type: String }],
});
materialSchema.virtual('id').get(function () {
return this._id.toHexString();
});
materialSchema.set('toJSON', { virtuals: true });
export const materialModel = mongoose.model('material', materialSchema);

View File

@ -0,0 +1,32 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const noteTypeSchema = new Schema(
{
name: {
type: String,
required: true,
unique: true,
},
color: {
type: String,
required: false,
},
active: {
type: Boolean,
required: true,
default: true,
},
},
{ timestamps: true }
);
// Add virtual id getter
noteTypeSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
noteTypeSchema.set('toJSON', { virtuals: true });
export const noteTypeModel = mongoose.model('noteType', noteTypeSchema);

View File

@ -0,0 +1,27 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
// Define the main part schema
const partSchema = new Schema(
{
name: { type: String, required: true },
fileName: { type: String, required: false },
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product' },
globalPricing: { type: Boolean, default: true },
priceMode: { type: String, default: 'margin' },
amount: { type: Number, required: false },
margin: { type: Number, required: false },
},
{ timestamps: true }
);
// Add virtual id getter
partSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
partSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const partModel = mongoose.model('part', partSchema);

View File

@ -0,0 +1,26 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
// Define the main product schema
const productSchema = new Schema(
{
name: { type: String, required: true },
tags: [{ type: String }],
version: { type: String },
priceMode: { type: String, default: 'margin' },
margin: { type: Number, required: false },
amount: { type: Number, required: false },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
},
{ timestamps: true }
);
// Add virtual id getter
productSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
productSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const productModel = mongoose.model('product', productSchema);

View File

@ -0,0 +1,20 @@
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema(
{
username: { required: true, type: String },
name: { required: true, type: String },
firstName: { required: false, type: String },
lastName: { required: false, type: String },
email: { required: true, type: String },
},
{ timestamps: true }
);
userSchema.virtual('id').get(function () {
return this._id.toHexString();
});
userSchema.set('toJSON', { virtuals: true });
export const userModel = mongoose.model('user', userSchema);

View File

@ -0,0 +1,21 @@
import mongoose from 'mongoose';
const vendorSchema = new mongoose.Schema(
{
name: { required: true, type: String },
website: { required: false, type: String },
email: { required: false, type: String },
phone: { required: false, type: String },
contact: { required: false, type: String },
country: { required: false, type: String },
},
{ timestamps: true }
);
vendorSchema.virtual('id').get(function () {
return this._id.toHexString();
});
vendorSchema.set('toJSON', { virtuals: true });
export const vendorModel = mongoose.model('vendor', vendorSchema);

View File

@ -0,0 +1,41 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const noteSchema = new mongoose.Schema({
parent: {
type: Schema.Types.ObjectId,
required: true,
},
content: {
type: String,
required: true,
},
noteType: {
type: Schema.Types.ObjectId,
ref: 'noteType',
required: true,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now,
},
user: {
type: Schema.Types.ObjectId,
ref: 'user',
required: false,
},
});
noteSchema.virtual('id').get(function () {
return this._id.toHexString();
});
noteSchema.set('toJSON', { virtuals: true });
export const noteModel = mongoose.model('note', noteSchema);

View File

@ -0,0 +1,24 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const gcodeFileSchema = new mongoose.Schema({
name: { required: true, type: String },
gcodeFileName: { required: false, type: String },
gcodeFileInfo: { required: true, type: Object },
size: { type: Number, required: false },
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true },
parts: [{ type: Schema.Types.ObjectId, ref: 'part', required: true }],
cost: { type: Number, required: false },
createdAt: { type: Date },
updatedAt: { type: Date },
});
gcodeFileSchema.index({ name: 'text', brand: 'text' });
gcodeFileSchema.virtual('id').get(function () {
return this._id.toHexString();
});
gcodeFileSchema.set('toJSON', { virtuals: true });
export const gcodeFileModel = mongoose.model('gcodeFile', gcodeFileSchema);

View File

@ -0,0 +1,34 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const jobSchema = new mongoose.Schema({
state: {
type: { required: true, type: String },
},
printers: [{ type: Schema.Types.ObjectId, ref: 'printer', required: false }],
createdAt: { required: true, type: Date },
updatedAt: { required: true, type: Date },
startedAt: { required: false, type: Date },
finishedAt: { required: false, type: Date },
gcodeFile: {
type: Schema.Types.ObjectId,
ref: 'gcodeFile',
required: false,
},
quantity: {
type: Number,
required: true,
default: 1,
min: 1,
},
subJobs: [{ type: Schema.Types.ObjectId, ref: 'subJob', required: false }],
notes: [{ type: Schema.Types.ObjectId, ref: 'note', required: false }],
});
jobSchema.virtual('id').get(function () {
return this._id.toHexString();
});
jobSchema.set('toJSON', { virtuals: true });
export const jobModel = mongoose.model('job', jobSchema);

View File

@ -0,0 +1,72 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
// Define the moonraker connection schema
const moonrakerSchema = new Schema(
{
host: { type: String, required: true },
port: { type: Number, required: true },
protocol: { type: String, required: true },
apiKey: { type: String, default: null, required: false }
},
{ _id: false }
);
// Define the alert schema
const alertSchema = new Schema(
{
priority: { type: String, required: true }, // order to show
type: { type: String, required: true } // selectFilament, error, info, message,
},
{ timestamps: true, _id: false }
);
// Define the main FDM printer schema
const printerSchema = new Schema(
{
name: { type: String, required: true },
online: { type: Boolean, required: true, default: false },
state: {
type: { type: String, required: true, default: 'offline' },
progress: { type: Number, required: false, default: 0 }
},
connectedAt: { type: Date, default: null },
loadedFilament: {
type: Schema.Types.ObjectId,
ref: 'filament',
default: null
},
moonraker: { type: moonrakerSchema, required: true },
tags: [{ type: String }],
firmware: { type: String },
currentJob: { type: Schema.Types.ObjectId, ref: 'job' },
currentSubJob: { type: Schema.Types.ObjectId, ref: 'subJob' },
currentFilamentStock: { type: Schema.Types.ObjectId, ref: 'filamentStock' },
subJobs: [{ type: Schema.Types.ObjectId, ref: 'subJob' }],
vendor: {
type: Schema.Types.ObjectId,
ref: 'vendor',
default: null,
required: true
},
host: {
type: Schema.Types.ObjectId,
ref: 'host',
default: null,
required: true
},
alerts: [alertSchema]
},
{ timestamps: true }
);
// Add virtual id getter
printerSchema.virtual('id').get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
printerSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const printerModel = mongoose.model('printer', printerSchema);

View File

@ -0,0 +1,50 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const subJobSchema = new mongoose.Schema({
printer: {
type: Schema.Types.ObjectId,
ref: 'printer',
required: true,
},
job: {
type: Schema.Types.ObjectId,
ref: 'job',
required: true,
},
subJobId: {
type: String,
required: true,
},
gcodeFile: {
type: Schema.Types.ObjectId,
ref: 'gcodeFile',
required: true,
},
state: {
type: { required: true, type: String },
percent: { required: false, type: Number },
},
number: {
type: Number,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
startedAt: { required: false, type: Date },
finishedAt: { required: false, type: Date },
});
subJobSchema.virtual('id').get(function () {
return this._id.toHexString();
});
subJobSchema.set('toJSON', { virtuals: true });
export const subJobModel = mongoose.model('subJob', subJobSchema);

90
src/utils.js Normal file
View File

@ -0,0 +1,90 @@
import { editObject } from './database/database.js';
import { hostModel } from './database/schemas/management/host.schema.js';
import crypto from 'crypto';
import { nanoid } from 'nanoid';
import { loadConfig } from './config.js';
import { userModel } from './database/schemas/management/user.schema.js';
import { documentSizeModel } from './database/schemas/management/documentsize.schema.js';
import { documentTemplateModel } from './database/schemas/management/documenttemplate.schema.js';
import { printerModel } from './database/schemas/production/printer.schema.js';
import { subJobModel } from './database/schemas/production/subjob.schema.js';
import { jobModel } from './database/schemas/production/job.schema.js';
import { filamentStockModel } from './database/schemas/inventory/filamentstock.schema.js';
const config = loadConfig();
const authCodeLength = 64;
const modelList = [
hostModel,
userModel,
documentSizeModel,
documentTemplateModel,
printerModel,
jobModel,
subJobModel,
filamentStockModel
];
export async function generateHostOTP(id) {
const otp = crypto.randomInt(0, 1000000).toString().padStart(6, '0'); // 0 to 999999
const expiresAt = new Date(
Date.now() + (config.otpExpiryMins || 2) * 60 * 1000
); // 2 minutes in ms
const otpHost = await editObject({
model: hostModel,
id: id,
updateData: { otp: otp, otpExpiresAt: expiresAt }
});
return otpHost;
}
export function generateAuthCode() {
return nanoid(authCodeLength);
}
export function generateEtcId() {
return nanoid(24);
}
export function getChangedValues(oldObj, newObj, old = false) {
const changes = {};
// Check all keys in the new object
for (const key in newObj) {
// Skip if the key is _id or timestamps
if (key === '_id' || key === 'createdAt' || key === 'updatedAt') continue;
const oldVal = oldObj ? oldObj[key] : undefined;
const newVal = newObj[key];
// If both values are objects (but not arrays or null), recurse
if (
oldVal &&
newVal &&
typeof oldVal === 'object' &&
typeof newVal === 'object' &&
!Array.isArray(oldVal) &&
!Array.isArray(newVal) &&
oldVal !== null &&
newVal !== null
) {
const nestedChanges = this.getChangedValues(oldVal, newVal, old);
if (Object.keys(nestedChanges).length > 0) {
changes[key] = nestedChanges;
}
} else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
// If the old value is different from the new value, include it
changes[key] = old ? oldVal : newVal;
}
}
return changes;
}
export function getModelByName(modelName) {
return modelList.filter(model => model.modelName == modelName);
}