diff --git a/src/database/database.js b/src/database/database.js new file mode 100644 index 0000000..6c49582 --- /dev/null +++ b/src/database/database.js @@ -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 }; + } +}; diff --git a/src/database/schemas/inventory/filamentstock.schema.js b/src/database/schemas/inventory/filamentstock.schema.js new file mode 100644 index 0000000..f8bb0ca --- /dev/null +++ b/src/database/schemas/inventory/filamentstock.schema.js @@ -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); diff --git a/src/database/schemas/inventory/partstock.schema.js b/src/database/schemas/inventory/partstock.schema.js new file mode 100644 index 0000000..1003c10 --- /dev/null +++ b/src/database/schemas/inventory/partstock.schema.js @@ -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); diff --git a/src/database/schemas/inventory/stockaudit.schema.js b/src/database/schemas/inventory/stockaudit.schema.js new file mode 100644 index 0000000..c8403d4 --- /dev/null +++ b/src/database/schemas/inventory/stockaudit.schema.js @@ -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); diff --git a/src/database/schemas/inventory/stockevent.schema.js b/src/database/schemas/inventory/stockevent.schema.js new file mode 100644 index 0000000..ec04078 --- /dev/null +++ b/src/database/schemas/inventory/stockevent.schema.js @@ -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); diff --git a/src/database/schemas/management/auditlog.schema.js b/src/database/schemas/management/auditlog.schema.js new file mode 100644 index 0000000..ecf117b --- /dev/null +++ b/src/database/schemas/management/auditlog.schema.js @@ -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); diff --git a/src/database/schemas/management/documentsize.schema.js b/src/database/schemas/management/documentsize.schema.js new file mode 100644 index 0000000..0e2fd40 --- /dev/null +++ b/src/database/schemas/management/documentsize.schema.js @@ -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); diff --git a/src/database/schemas/management/documenttemplate.schema.js b/src/database/schemas/management/documenttemplate.schema.js new file mode 100644 index 0000000..5b7aa81 --- /dev/null +++ b/src/database/schemas/management/documenttemplate.schema.js @@ -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: '', + }, + 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); diff --git a/src/database/schemas/management/filament.schema.js b/src/database/schemas/management/filament.schema.js new file mode 100644 index 0000000..8254e71 --- /dev/null +++ b/src/database/schemas/management/filament.schema.js @@ -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); diff --git a/src/database/schemas/management/host.schema.js b/src/database/schemas/management/host.schema.js new file mode 100644 index 0000000..5d197b3 --- /dev/null +++ b/src/database/schemas/management/host.schema.js @@ -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); diff --git a/src/database/schemas/management/material.schema.js b/src/database/schemas/management/material.schema.js new file mode 100644 index 0000000..18c2892 --- /dev/null +++ b/src/database/schemas/management/material.schema.js @@ -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); diff --git a/src/database/schemas/management/notetype.schema.js b/src/database/schemas/management/notetype.schema.js new file mode 100644 index 0000000..9b0ce35 --- /dev/null +++ b/src/database/schemas/management/notetype.schema.js @@ -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); diff --git a/src/database/schemas/management/part.schema.js b/src/database/schemas/management/part.schema.js new file mode 100644 index 0000000..8a6abdb --- /dev/null +++ b/src/database/schemas/management/part.schema.js @@ -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); diff --git a/src/database/schemas/management/product.schema.js b/src/database/schemas/management/product.schema.js new file mode 100644 index 0000000..abc4570 --- /dev/null +++ b/src/database/schemas/management/product.schema.js @@ -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); diff --git a/src/database/schemas/management/user.schema.js b/src/database/schemas/management/user.schema.js new file mode 100644 index 0000000..125a706 --- /dev/null +++ b/src/database/schemas/management/user.schema.js @@ -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); diff --git a/src/database/schemas/management/vendor.schema.js b/src/database/schemas/management/vendor.schema.js new file mode 100644 index 0000000..dfdb1bf --- /dev/null +++ b/src/database/schemas/management/vendor.schema.js @@ -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); diff --git a/src/database/schemas/misc/note.schema.js b/src/database/schemas/misc/note.schema.js new file mode 100644 index 0000000..f32ff60 --- /dev/null +++ b/src/database/schemas/misc/note.schema.js @@ -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); diff --git a/src/database/schemas/production/gcodefile.schema.js b/src/database/schemas/production/gcodefile.schema.js new file mode 100644 index 0000000..0c97bd5 --- /dev/null +++ b/src/database/schemas/production/gcodefile.schema.js @@ -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); diff --git a/src/database/schemas/production/job.schema.js b/src/database/schemas/production/job.schema.js new file mode 100644 index 0000000..22ecd22 --- /dev/null +++ b/src/database/schemas/production/job.schema.js @@ -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); diff --git a/src/database/schemas/production/printer.schema.js b/src/database/schemas/production/printer.schema.js new file mode 100644 index 0000000..dde71e7 --- /dev/null +++ b/src/database/schemas/production/printer.schema.js @@ -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); diff --git a/src/database/schemas/production/subjob.schema.js b/src/database/schemas/production/subjob.schema.js new file mode 100644 index 0000000..a04cb02 --- /dev/null +++ b/src/database/schemas/production/subjob.schema.js @@ -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); diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d967e58 --- /dev/null +++ b/src/utils.js @@ -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); +}