Compare commits
No commits in common. "c36765cc470881496cf168fddb3c585cc58239ed" and "7b4bedeee4a472ec4f1ff1bbfa2f07d00daeaddf" have entirely different histories.
c36765cc47
...
7b4bedeee4
@ -24,6 +24,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@ -34,6 +35,9 @@
|
|||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
|
"pdf-to-img": "^6.1.0",
|
||||||
|
"posthtml": "^0.16.7",
|
||||||
|
"puppeteer": "^24.31.0",
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
|||||||
804
pnpm-lock.yaml
generated
804
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,6 @@ import {
|
|||||||
deleteAuditLog,
|
deleteAuditLog,
|
||||||
expandObjectIds,
|
expandObjectIds,
|
||||||
editAuditLog,
|
editAuditLog,
|
||||||
editNotification,
|
|
||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
newAuditLog,
|
newAuditLog,
|
||||||
distributeNew,
|
distributeNew,
|
||||||
@ -424,8 +423,7 @@ export const editObject = async ({
|
|||||||
owner = undefined,
|
owner = undefined,
|
||||||
ownerType = undefined,
|
ownerType = undefined,
|
||||||
populate = [],
|
populate = [],
|
||||||
auditLog = true,
|
auditLog = true
|
||||||
notify = true
|
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
// Determine parentType from model name
|
// Determine parentType from model name
|
||||||
@ -466,24 +464,6 @@ export const editObject = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
notify == true &&
|
|
||||||
owner != undefined &&
|
|
||||||
ownerType != undefined &&
|
|
||||||
parentType !== 'notification' &&
|
|
||||||
parentType !== 'auditLog' &&
|
|
||||||
parentType !== 'userNotifier'
|
|
||||||
) {
|
|
||||||
await editNotification(
|
|
||||||
previousExpandedObject,
|
|
||||||
newExpandedObject,
|
|
||||||
id,
|
|
||||||
parentType,
|
|
||||||
owner,
|
|
||||||
ownerType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distribute update
|
// Distribute update
|
||||||
await distributeUpdate(updateData, id, parentType);
|
await distributeUpdate(updateData, id, parentType);
|
||||||
|
|
||||||
|
|||||||
@ -1,51 +1,9 @@
|
|||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
||||||
import { notificationModel } from './schemas/misc/notification.schema.js';
|
|
||||||
import { userNotifierModel } from './schemas/misc/usernotifier.schema.js';
|
|
||||||
import { models } from './schemas/models.js';
|
|
||||||
import { natsServer } from './nats.js';
|
import { natsServer } from './nats.js';
|
||||||
|
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
const NOTIFICATION_EXCLUDED_MODELS = ['notification', 'userNotifier', 'auditLog'];
|
|
||||||
const SENSITIVE_KEYS = ['secret'];
|
|
||||||
|
|
||||||
function omitSensitive(obj) {
|
|
||||||
if (obj == null || typeof obj !== 'object') return obj;
|
|
||||||
if (Array.isArray(obj)) return obj.map(omitSensitive);
|
|
||||||
const result = {};
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
if (SENSITIVE_KEYS.includes(key)) continue;
|
|
||||||
result[key] = omitSensitive(value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelEntryByType(parentType) {
|
|
||||||
return Object.values(models).find(
|
|
||||||
entry => entry.type === parentType || entry.model?.modelName === parentType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notificationUserFromOwner(owner, ownerType) {
|
|
||||||
if (!owner) return null;
|
|
||||||
if (ownerType === 'user') {
|
|
||||||
return owner;
|
|
||||||
}
|
|
||||||
if (ownerType === 'host') {
|
|
||||||
return {
|
|
||||||
_id: owner._id,
|
|
||||||
firstName: owner.name ?? 'unknown',
|
|
||||||
lastName: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
_id: owner._id,
|
|
||||||
firstName: owner.name ?? owner.firstName ?? 'unknown',
|
|
||||||
lastName: owner.lastName ?? ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
export const generateId = () => {
|
export const generateId = () => {
|
||||||
// 10 characters
|
// 10 characters
|
||||||
@ -486,86 +444,6 @@ async function distributeNew(id, type) {
|
|||||||
await natsServer.publish(`${type}s.new`, id);
|
await natsServer.publish(`${type}s.new`, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editNotification(
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
parentId,
|
|
||||||
parentType,
|
|
||||||
owner,
|
|
||||||
ownerType
|
|
||||||
) {
|
|
||||||
if (NOTIFICATION_EXCLUDED_MODELS.includes(parentType)) return;
|
|
||||||
|
|
||||||
const modelEntry = getModelEntryByType(parentType);
|
|
||||||
const user = notificationUserFromOwner(owner, ownerType);
|
|
||||||
const objectName =
|
|
||||||
oldValue?.name ?? newValue?.name ?? modelEntry?.label ?? parentType;
|
|
||||||
const changedOldValues = omitSensitive(getChangedValues(oldValue, newValue, true));
|
|
||||||
const changedNewValues = omitSensitive(getChangedValues(oldValue, newValue, false));
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.keys(changedOldValues).length === 0 ||
|
|
||||||
Object.keys(changedNewValues).length === 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await notfiyObjectUserNotifiers(
|
|
||||||
parentId,
|
|
||||||
parentType,
|
|
||||||
`${objectName} edited by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
|
||||||
`The ${parentType} ${parentId} has been updated.`,
|
|
||||||
'editObject',
|
|
||||||
{
|
|
||||||
old: changedOldValues,
|
|
||||||
new: changedNewValues,
|
|
||||||
objectType: parentType,
|
|
||||||
object: { _id: String(parentId ?? '') },
|
|
||||||
user: {
|
|
||||||
_id: String(user?._id ?? ''),
|
|
||||||
firstName: user?.firstName,
|
|
||||||
lastName: user?.lastName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function notfiyObjectUserNotifiers(
|
|
||||||
id,
|
|
||||||
objectType,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type = 'info',
|
|
||||||
metadata
|
|
||||||
) {
|
|
||||||
const userNotifiers = await userNotifierModel
|
|
||||||
.find({ object: id, objectType })
|
|
||||||
.populate('user');
|
|
||||||
for (const userNotifier of userNotifiers) {
|
|
||||||
await createNotification(
|
|
||||||
userNotifier.user._id,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
metadata
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNotification(user, title, message, type = 'info', metadata) {
|
|
||||||
const notification = new notificationModel({
|
|
||||||
user,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
metadata: omitSensitive(metadata ?? {})
|
|
||||||
});
|
|
||||||
await notification.save();
|
|
||||||
const value = notification.toJSON ? notification.toJSON() : notification;
|
|
||||||
await natsServer.publish(`notifications.${user._id ?? user}`, value);
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatternObjectIds(object) {
|
function flatternObjectIds(object) {
|
||||||
if (!object || typeof object !== 'object') {
|
if (!object || typeof object !== 'object') {
|
||||||
return object;
|
return object;
|
||||||
@ -668,9 +546,6 @@ export {
|
|||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
distributeNew,
|
distributeNew,
|
||||||
distributeStats,
|
distributeStats,
|
||||||
editNotification,
|
|
||||||
notfiyObjectUserNotifiers,
|
|
||||||
createNotification,
|
|
||||||
getFilter, // <-- add here
|
getFilter, // <-- add here
|
||||||
convertPropertiesString
|
convertPropertiesString
|
||||||
};
|
};
|
||||||
|
|||||||
@ -52,6 +52,12 @@ jest.unstable_mockModule('../../events/eventmanager.js', () => ({
|
|||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
|
||||||
|
TemplateManager: jest.fn().mockImplementation(() => ({
|
||||||
|
renderPDF: jest.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../utils.js', () => ({
|
jest.unstable_mockModule('../../utils.js', () => ({
|
||||||
getModelByName: jest.fn(name => ({ modelName: name }))
|
getModelByName: jest.fn(name => ({ modelName: name }))
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -44,6 +44,10 @@ jest.unstable_mockModule('../../activity/activitymanager.js', () => ({
|
|||||||
ActivityManager: jest.fn()
|
ActivityManager: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
|
||||||
|
TemplateManager: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../config.js', () => ({
|
jest.unstable_mockModule('../../config.js', () => ({
|
||||||
loadConfig: jest.fn(() => ({
|
loadConfig: jest.fn(() => ({
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@ -116,7 +116,12 @@ describe('SocketUser', () => {
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
emit: jest.fn()
|
emit: jest.fn()
|
||||||
};
|
};
|
||||||
mockSocketManager = {};
|
mockSocketManager = {
|
||||||
|
templateManager: {
|
||||||
|
renderTemplate: jest.fn(),
|
||||||
|
renderPDF: jest.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
socketUser = new SocketUser(mockSocket, mockSocketManager);
|
socketUser = new SocketUser(mockSocket, mockSocketManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { UpdateManager } from '../updates/updatemanager.js';
|
|||||||
import { ActionManager } from '../actions/actionmanager.js';
|
import { ActionManager } from '../actions/actionmanager.js';
|
||||||
import { getModelByName } from '../utils.js';
|
import { getModelByName } from '../utils.js';
|
||||||
import { EventManager } from '../events/eventmanager.js';
|
import { EventManager } from '../events/eventmanager.js';
|
||||||
|
import { TemplateManager } from '../templates/templatemanager.js';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export class SocketHost {
|
|||||||
this.updateManager = new UpdateManager(this);
|
this.updateManager = new UpdateManager(this);
|
||||||
this.actionManager = new ActionManager(this);
|
this.actionManager = new ActionManager(this);
|
||||||
this.eventManager = new EventManager(this);
|
this.eventManager = new EventManager(this);
|
||||||
|
this.templateManager = new TemplateManager(this);
|
||||||
this.codeAuth = new CodeAuth();
|
this.codeAuth = new CodeAuth();
|
||||||
this.setupSocketEventHandlers();
|
this.setupSocketEventHandlers();
|
||||||
}
|
}
|
||||||
@ -62,6 +64,14 @@ export class SocketHost {
|
|||||||
'unsubscribeObjectEvent',
|
'unsubscribeObjectEvent',
|
||||||
this.handleUnsubscribeObjectEventEvent.bind(this)
|
this.handleUnsubscribeObjectEventEvent.bind(this)
|
||||||
);
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplatePDF',
|
||||||
|
this.handleRenderTemplatePDFEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplateJPG',
|
||||||
|
this.handleRenderTemplateJPGEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
|
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
|
||||||
this.socket.on('disconnect', this.handleDisconnect.bind(this));
|
this.socket.on('disconnect', this.handleDisconnect.bind(this));
|
||||||
}
|
}
|
||||||
@ -172,8 +182,7 @@ export class SocketHost {
|
|||||||
populate: data.populate,
|
populate: data.populate,
|
||||||
owner: this.host,
|
owner: this.host,
|
||||||
ownerType: 'host',
|
ownerType: 'host',
|
||||||
auditLog: data?.auditLog,
|
auditLog: data?.auditLog
|
||||||
notify: data?.notify
|
|
||||||
});
|
});
|
||||||
callback(object);
|
callback(object);
|
||||||
}
|
}
|
||||||
@ -247,6 +256,26 @@ export class SocketHost {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplatePDFEvent(data, callback) {
|
||||||
|
const result = await this.templateManager.renderPDF(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplateJPGEvent(data, callback) {
|
||||||
|
const result = await this.templateManager.renderJPG(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
{ width: data.width }
|
||||||
|
);
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
|
||||||
async setDevicesState(state, online, connectedAt) {
|
async setDevicesState(state, online, connectedAt) {
|
||||||
logger.info('Setting devices state to', state, 'and online to', online);
|
logger.info('Setting devices state to', state, 'and online to', online);
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import log4js from 'log4js';
|
|||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
import { SocketUser } from './socketuser.js';
|
import { SocketUser } from './socketuser.js';
|
||||||
import { UpdateManager } from '../updates/updatemanager.js';
|
import { UpdateManager } from '../updates/updatemanager.js';
|
||||||
|
import { TemplateManager } from '../templates/templatemanager.js';
|
||||||
import { SocketHost } from './sockethost.js';
|
import { SocketHost } from './sockethost.js';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@ -16,6 +17,7 @@ export class SocketManager {
|
|||||||
constructor(server) {
|
constructor(server) {
|
||||||
this.socketUsers = new Map();
|
this.socketUsers = new Map();
|
||||||
this.socketHosts = new Map();
|
this.socketHosts = new Map();
|
||||||
|
this.templateManager = new TemplateManager(this);
|
||||||
|
|
||||||
// Use the provided HTTP server
|
// Use the provided HTTP server
|
||||||
// Create Socket.IO server - CORS applies to HTTP long-polling transport
|
// Create Socket.IO server - CORS applies to HTTP long-polling transport
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export class SocketUser {
|
|||||||
this.notificationManager = new NotificationManager(this);
|
this.notificationManager = new NotificationManager(this);
|
||||||
this.serverManager = new ServerManager(this);
|
this.serverManager = new ServerManager(this);
|
||||||
this.userSettingsManager = new UserSettingsManager(this);
|
this.userSettingsManager = new UserSettingsManager(this);
|
||||||
|
this.templateManager = socketManager.templateManager;
|
||||||
this.keycloakAuth = new KeycloakAuth();
|
this.keycloakAuth = new KeycloakAuth();
|
||||||
this.setupSocketEventHandlers();
|
this.setupSocketEventHandlers();
|
||||||
}
|
}
|
||||||
@ -84,6 +85,18 @@ export class SocketUser {
|
|||||||
'unsubscribeModelStats',
|
'unsubscribeModelStats',
|
||||||
this.handleUnsubscribeToStatsEvent.bind(this)
|
this.handleUnsubscribeToStatsEvent.bind(this)
|
||||||
);
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'previewTemplate',
|
||||||
|
this.handlePreviewTemplateEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplatePDF',
|
||||||
|
this.handleRenderTemplatePDFEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplateJPG',
|
||||||
|
this.handleRenderTemplateJPGEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
'generateHostOtp',
|
'generateHostOtp',
|
||||||
this.handleGenerateHostOtpEvent.bind(this)
|
this.handleGenerateHostOtpEvent.bind(this)
|
||||||
@ -296,6 +309,63 @@ export class SocketUser {
|
|||||||
await this.statsManager.removeStatsListener(data.objectType);
|
await this.statsManager.removeStatsListener(data.objectType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handlePreviewTemplateEvent(data, callback) {
|
||||||
|
try {
|
||||||
|
const result = await this.templateManager.renderTemplate(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.testObject,
|
||||||
|
data.scale
|
||||||
|
);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Preview template event error:', err);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplatePDFEvent(data, callback) {
|
||||||
|
try {
|
||||||
|
const result = await this.templateManager.renderPDF(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Render template PDF event error:', err);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplateJPGEvent(data, callback) {
|
||||||
|
try {
|
||||||
|
const result = await this.templateManager.renderJPG(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
{ width: data.width }
|
||||||
|
);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Render template JPG event error:', err);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleGenerateHostOtpEvent(data, callback) {
|
async handleGenerateHostOtpEvent(data, callback) {
|
||||||
const result = await generateHostOTP(data._id);
|
const result = await generateHostOTP(data._id);
|
||||||
callback(result);
|
callback(result);
|
||||||
|
|||||||
184
src/templates/__tests__/templatemanager.test.js
Normal file
184
src/templates/__tests__/templatemanager.test.js
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Mock fs before importing TemplateManager
|
||||||
|
jest.unstable_mockModule('fs', () => ({
|
||||||
|
default: {
|
||||||
|
readFileSync: jest.fn(filePath => {
|
||||||
|
if (filePath.endsWith('basetemplate.ejs'))
|
||||||
|
return '<html><%- content %></html>';
|
||||||
|
if (filePath.endsWith('styles.css')) return 'body { color: red; }';
|
||||||
|
if (filePath.endsWith('previewtemplate.ejs'))
|
||||||
|
return '<div class="preview"><%- content %></div>';
|
||||||
|
if (filePath.endsWith('rendertemplate.ejs'))
|
||||||
|
return '<div class="render"><%- content %></div>';
|
||||||
|
if (filePath.endsWith('contentplaceholder.ejs'))
|
||||||
|
return '<div class="placeholder"></div>';
|
||||||
|
if (filePath.endsWith('previewpagination.js'))
|
||||||
|
return 'console.log("pagination");';
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('ejs', () => ({
|
||||||
|
default: {
|
||||||
|
render: jest.fn(async (content, data) => `rendered: ${content}`),
|
||||||
|
compile: jest.fn(() => jest.fn())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('posthtml', () => ({
|
||||||
|
default: jest.fn(() => ({
|
||||||
|
process: jest.fn(async content => ({ html: `transformed: ${content}` }))
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/nats.js', () => ({
|
||||||
|
natsServer: {
|
||||||
|
publish: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
subscribe: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
removeSubscription: jest.fn().mockResolvedValue({ success: true })
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/redis.js', () => ({
|
||||||
|
redisServer: {
|
||||||
|
setKey: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getKey: jest.fn().mockResolvedValue(null),
|
||||||
|
deleteKey: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getKeysByPattern: jest.fn().mockResolvedValue([])
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../database/database.js', () => ({
|
||||||
|
getObject: jest.fn(),
|
||||||
|
listObjects: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule(
|
||||||
|
'../../database/schemas/management/documenttemplate.schema.js',
|
||||||
|
() => ({
|
||||||
|
documentTemplateModel: { modelName: 'DocumentTemplate' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../utils.js', () => ({
|
||||||
|
getModelByName: jest.fn(() => ({
|
||||||
|
schema: {
|
||||||
|
obj: { name: {}, status: {} }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../pdffactory.js', () => ({
|
||||||
|
generatePDF: jest.fn().mockResolvedValue(Buffer.from('pdf-data'))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('log4js', () => ({
|
||||||
|
default: {
|
||||||
|
getLogger: () => ({
|
||||||
|
level: 'info',
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
trace: jest.fn(),
|
||||||
|
info: jest.fn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../config.js', () => ({
|
||||||
|
loadConfig: jest.fn(() => ({
|
||||||
|
server: {
|
||||||
|
logLevel: 'info'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { TemplateManager } = await import('../templatemanager.js');
|
||||||
|
const { getObject } = await import('../../database/database.js');
|
||||||
|
const ejs = (await import('ejs')).default;
|
||||||
|
const { generatePDF } = await import('../pdffactory.js');
|
||||||
|
|
||||||
|
describe('TemplateManager', () => {
|
||||||
|
let templateManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
templateManager = new TemplateManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderTemplate', () => {
|
||||||
|
it('should render a template successfully', async () => {
|
||||||
|
const mockTemplate = {
|
||||||
|
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||||
|
global: false,
|
||||||
|
objectType: 'printer'
|
||||||
|
};
|
||||||
|
|
||||||
|
getObject.mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await templateManager.renderTemplate(
|
||||||
|
'temp-id',
|
||||||
|
'some content',
|
||||||
|
{ name: 'Test' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ id: 'temp-id' })
|
||||||
|
);
|
||||||
|
expect(ejs.render).toHaveBeenCalled();
|
||||||
|
expect(result).toHaveProperty('html');
|
||||||
|
expect(result.width).toBe(100);
|
||||||
|
expect(result.height).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if template not found', async () => {
|
||||||
|
getObject.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await templateManager.renderTemplate('invalid', 'content');
|
||||||
|
|
||||||
|
expect(result).toEqual({ error: 'Document template not found.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateTemplate', () => {
|
||||||
|
it('should return true for valid EJS', () => {
|
||||||
|
expect(templateManager.validateTemplate('<div><%= name %></div>')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid EJS', () => {
|
||||||
|
ejs.compile.mockImplementationOnce(() => {
|
||||||
|
throw new Error('syntax error');
|
||||||
|
});
|
||||||
|
expect(templateManager.validateTemplate('<% invalid %>')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderPDF', () => {
|
||||||
|
it('should render a PDF successfully', async () => {
|
||||||
|
const mockTemplate = {
|
||||||
|
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||||
|
global: false,
|
||||||
|
objectType: 'printer'
|
||||||
|
};
|
||||||
|
getObject.mockResolvedValue(mockTemplate);
|
||||||
|
|
||||||
|
const result = await templateManager.renderPDF('temp-id', 'content');
|
||||||
|
|
||||||
|
expect(generatePDF).toHaveBeenCalled();
|
||||||
|
expect(result.pdf).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('should format date correctly', () => {
|
||||||
|
const date = new Date('2023-01-01T12:00:00Z');
|
||||||
|
const formatted = templateManager.formatDate(date, 'YYYY-MM-DD');
|
||||||
|
expect(formatted).toBe('2023-01-01');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/templates/assets/basetemplate.ejs
Normal file
49
src/templates/assets/basetemplate.ejs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="initial-scale=1.0" />
|
||||||
|
<title>Document</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
<%- baseCSS %>
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewWrapper {
|
||||||
|
width: <%= (scaledWidth) || '50mm' %>;
|
||||||
|
height: <%= (scaledHeight) || '50mm' %>;
|
||||||
|
}
|
||||||
|
.previewDocument {
|
||||||
|
width: <%= (width) || '50mm' %>;
|
||||||
|
height: <%= (height) || '50mm' %>;
|
||||||
|
transform: scale(<%= scale || '1' %>);
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
.renderDocument {
|
||||||
|
width: <%= (scaledWidth) || '50mm' %>;
|
||||||
|
height: <%= (scaledHeight) || '50mm' %>;
|
||||||
|
transform: scale(<%= scale || '1' %>);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- content %>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
|
||||||
|
<script>
|
||||||
|
JsBarcode('.documentBarcode').init();
|
||||||
|
</script>
|
||||||
|
<% if (typeof previewPaginationScript !== 'undefined' && previewPaginationScript) { %>
|
||||||
|
<script>
|
||||||
|
<%- previewPaginationScript %>
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
src/templates/assets/contentplaceholder.ejs
Normal file
3
src/templates/assets/contentplaceholder.ejs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div class="contentPlaceholder">
|
||||||
|
<p>Content</p>
|
||||||
|
</div>
|
||||||
0
src/templates/assets/previewpagination.js
Normal file
0
src/templates/assets/previewpagination.js
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<div class="previewContainer">
|
||||||
|
<div class="previewWrapper">
|
||||||
|
<div class="previewDocument" id="content"><%- content %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
src/templates/assets/rendertemplate.ejs
Normal file
1
src/templates/assets/rendertemplate.ejs
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div class="renderDocument" id="content"><%- content %></div>
|
||||||
131
src/templates/assets/styles.css
Normal file
131
src/templates/assets/styles.css
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Figtree', sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Horizontal center */
|
||||||
|
align-items: center; /* Vertical center */
|
||||||
|
padding: 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.previewWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.previewDocument {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #000;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
transform-origin: top left;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentText {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.documentTitle {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.documentTitle {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.documentTitle {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.documentTitle {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
h4.documentTitle {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.documentFlex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.documentDivider {
|
||||||
|
background: black;
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPlaceholder {
|
||||||
|
border: 1px solid black;
|
||||||
|
max-height: 250px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
/* Angle of the stripes */ #ccc,
|
||||||
|
/* Light grey */ #ccc 10px,
|
||||||
|
/* End of first stripe */ #eee 10px,
|
||||||
|
/* Start of next stripe (slightly lighter grey) */ #eee 20px
|
||||||
|
/* End of second stripe */
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPlaceholder > p {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentBarcode {
|
||||||
|
width: auto !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentProgressBar {
|
||||||
|
height: 8px;
|
||||||
|
width: 260px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentProgressBarInner {
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentTableRow {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentTableRow td,
|
||||||
|
.documentTableRowHeader th,
|
||||||
|
.documentTableRowFooter td {
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentTableRowHeader {
|
||||||
|
background: #0000002e;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentTableRowFooter {
|
||||||
|
background: #0000002e;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
}
|
||||||
78
src/templates/pdfUtils.js
Normal file
78
src/templates/pdfUtils.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* PDF conversion utilities loaded on demand so startup
|
||||||
|
* does not require ESM-only pdf-to-img at module load time.
|
||||||
|
*/
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
async function importPackage(name) {
|
||||||
|
const packagePath = require.resolve(name);
|
||||||
|
return import(pathToFileURL(packagePath).href);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefault(module) {
|
||||||
|
return module?.default ?? module;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertPDFToImage(pdfInput, options = {}) {
|
||||||
|
const [pdfToImgModule, sharpModule] = await Promise.all([
|
||||||
|
importPackage('pdf-to-img'),
|
||||||
|
importPackage('sharp')
|
||||||
|
]);
|
||||||
|
const { pdf } = pdfToImgModule;
|
||||||
|
const sharp = resolveDefault(sharpModule);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { width, height, page_numbers, scale = 2, ...pdfOnlyOptions } =
|
||||||
|
options;
|
||||||
|
|
||||||
|
const pdfOptions = {
|
||||||
|
scale,
|
||||||
|
...pdfOnlyOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
const document = await pdf(pdfInput, pdfOptions);
|
||||||
|
const outputImages = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
page_numbers &&
|
||||||
|
Array.isArray(page_numbers) &&
|
||||||
|
page_numbers.length > 0
|
||||||
|
) {
|
||||||
|
for (const pageNum of page_numbers) {
|
||||||
|
let image = await document.getPage(pageNum);
|
||||||
|
|
||||||
|
if (width || height) {
|
||||||
|
const resizeOptions = {};
|
||||||
|
if (width) resizeOptions.width = width;
|
||||||
|
if (height) resizeOptions.height = height;
|
||||||
|
image = await sharp(image).resize(resizeOptions).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
outputImages.push(image);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for await (const image of document) {
|
||||||
|
let processedImage = image;
|
||||||
|
|
||||||
|
if (width || height) {
|
||||||
|
const resizeOptions = {};
|
||||||
|
if (width) resizeOptions.width = width;
|
||||||
|
if (height) resizeOptions.height = height;
|
||||||
|
processedImage = await sharp(image)
|
||||||
|
.resize(resizeOptions)
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
outputImages.push(processedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputImages;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting PDF to image:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/templates/pdffactory.js
Normal file
63
src/templates/pdffactory.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import log4js from 'log4js';
|
||||||
|
import { loadConfig } from '../config.js';
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const logger = log4js.getLogger('PDF Factory');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PDF from HTML content using Puppeteer
|
||||||
|
* @param {string} html - The HTML content to convert to PDF
|
||||||
|
* @param {Object} options - PDF generation options
|
||||||
|
* @param {number} options.width - Document width in mm
|
||||||
|
* @param {number} options.height - Document height in mm
|
||||||
|
* @returns {Promise<Buffer>} The PDF buffer
|
||||||
|
*/
|
||||||
|
export async function generatePDF(html, options = {}) {
|
||||||
|
try {
|
||||||
|
// Dynamically import puppeteer to handle cases where it might not be installed
|
||||||
|
const puppeteer = await import('puppeteer');
|
||||||
|
|
||||||
|
const browser = await puppeteer.default.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Set content with HTML
|
||||||
|
await page.setContent(html, {
|
||||||
|
waitUntil: 'networkidle0'
|
||||||
|
});
|
||||||
|
|
||||||
|
var height = `${options?.height || '50'}mm`;
|
||||||
|
|
||||||
|
if (options.height == 'auto') {
|
||||||
|
const calculatedHeight = await page.evaluate(() => {
|
||||||
|
return document.getElementById('content').scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
height = `${calculatedHeight}px`;
|
||||||
|
}
|
||||||
|
// Generate PDF with specified dimensions
|
||||||
|
const pdfBuffer = await page.pdf({
|
||||||
|
printBackground: true,
|
||||||
|
preferCSSPageSize: true,
|
||||||
|
width: options.width ? `${options.width}mm` : undefined,
|
||||||
|
height: height ? `${height}` : undefined,
|
||||||
|
margin: {
|
||||||
|
top: '0mm',
|
||||||
|
right: '0mm',
|
||||||
|
bottom: '0mm',
|
||||||
|
left: '0mm'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return pdfBuffer;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error generating PDF:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
657
src/templates/templatemanager.js
Normal file
657
src/templates/templatemanager.js
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
import ejs from 'ejs';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import posthtml from 'posthtml';
|
||||||
|
|
||||||
|
import { documentTemplateModel } from '../database/schemas/management/documenttemplate.schema.js';
|
||||||
|
import '../database/schemas/management/documentsize.schema.js';
|
||||||
|
// Load configuration
|
||||||
|
import { loadConfig } from '../config.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
|
import timezone from 'dayjs/plugin/timezone.js';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { getObject, listObjects } from '../database/database.js';
|
||||||
|
import { getModelByName } from '../utils.js';
|
||||||
|
import { generatePDF } from './pdffactory.js';
|
||||||
|
import { convertPDFToImage } from './pdfUtils.js';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Extend plugins
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Template Manager');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
let baseTemplate;
|
||||||
|
let baseCSS;
|
||||||
|
let previewTemplate;
|
||||||
|
let renderTemplateEjs;
|
||||||
|
let contentPlaceholder;
|
||||||
|
let previewPaginationScript;
|
||||||
|
|
||||||
|
async function loadTemplates() {
|
||||||
|
// Synchronously load files
|
||||||
|
baseTemplate = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/basetemplate.ejs'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
baseCSS = fs.readFileSync(join(__dirname, '/assets/styles.css'), 'utf8');
|
||||||
|
previewTemplate = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/previewtemplate.ejs'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
renderTemplateEjs = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/rendertemplate.ejs'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
contentPlaceholder = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/contentplaceholder.ejs'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
previewPaginationScript = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/previewpagination.js'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTemplates();
|
||||||
|
|
||||||
|
function getNodeStyles(attributes) {
|
||||||
|
var styles = '';
|
||||||
|
if (attributes?.padding) {
|
||||||
|
styles += `padding: ${attributes.padding};`;
|
||||||
|
}
|
||||||
|
if (attributes?.width) {
|
||||||
|
styles += `width: ${attributes.width};`;
|
||||||
|
}
|
||||||
|
if (attributes?.height) {
|
||||||
|
styles += `height: ${attributes.height};`;
|
||||||
|
}
|
||||||
|
if (attributes?.maxWidth) {
|
||||||
|
styles += `max-width: ${attributes.maxWidth};`;
|
||||||
|
}
|
||||||
|
if (attributes?.maxHeight) {
|
||||||
|
styles += `max-height: ${attributes.maxHeight};`;
|
||||||
|
}
|
||||||
|
if (attributes?.gap && attributes?.vertical != 'true') {
|
||||||
|
styles += `column-gap: ${attributes.gap};`;
|
||||||
|
}
|
||||||
|
if (attributes?.gap && attributes?.vertical == 'true') {
|
||||||
|
styles += `row-gap: ${attributes.gap};`;
|
||||||
|
}
|
||||||
|
if (attributes?.justify) {
|
||||||
|
styles += `justify-content: ${attributes.justify};`;
|
||||||
|
}
|
||||||
|
if (attributes?.align) {
|
||||||
|
styles += `align-items: ${attributes.align};`;
|
||||||
|
}
|
||||||
|
if (attributes?.border) {
|
||||||
|
styles += `border: ${attributes.border};`;
|
||||||
|
}
|
||||||
|
if (attributes?.borderRadius) {
|
||||||
|
styles += `border-radius: ${attributes.borderRadius};`;
|
||||||
|
}
|
||||||
|
if (attributes?.vertical == 'true') {
|
||||||
|
styles += `flex-direction: column;`;
|
||||||
|
}
|
||||||
|
if (attributes?.grow) {
|
||||||
|
styles += `flex-grow: ${attributes.grow};`;
|
||||||
|
}
|
||||||
|
if (attributes?.shrink) {
|
||||||
|
styles += `flex-shrink: ${attributes.shrink};`;
|
||||||
|
}
|
||||||
|
if (attributes?.color) {
|
||||||
|
styles += `color: ${attributes.color};`;
|
||||||
|
}
|
||||||
|
if (attributes?.background) {
|
||||||
|
styles += `background: ${attributes.background};`;
|
||||||
|
}
|
||||||
|
if (attributes?.scale) {
|
||||||
|
styles += `transform: scale(${attributes.scale});`;
|
||||||
|
}
|
||||||
|
if (attributes?.textAlign) {
|
||||||
|
styles += `text-align: ${attributes.textAlign};`;
|
||||||
|
}
|
||||||
|
if (attributes?.textSize) {
|
||||||
|
styles += `font-size: ${attributes.textSize};`;
|
||||||
|
}
|
||||||
|
if (attributes?.wordWrap) {
|
||||||
|
styles += `word-wrap: ${attributes.wordWrap};`;
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transformCustomElements(content) {
|
||||||
|
const result = await posthtml([
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Title1' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'h1',
|
||||||
|
attrs: { class: 'documentTitle' }
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Title2' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'h2',
|
||||||
|
attrs: { class: 'documentTitle' }
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Title3' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'h3',
|
||||||
|
attrs: { class: 'documentText' }
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Title4' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'h4',
|
||||||
|
attrs: { class: 'documentText' }
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Text' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'p',
|
||||||
|
attrs: { class: 'documentText', style: getNodeStyles(node.attrs) }
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Bold' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'strong',
|
||||||
|
attrs: {
|
||||||
|
style: 'font-weight: bold;',
|
||||||
|
class: 'documentBoldText',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Barcode' }, node => {
|
||||||
|
return {
|
||||||
|
tag: 'div',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'svg',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentBarcode',
|
||||||
|
'jsbarcode-displayValue': 'false',
|
||||||
|
'jsbarcode-value': node.content[0],
|
||||||
|
'jsbarcode-format': node.attrs.format,
|
||||||
|
'jsbarcode-width': node.attrs.barcodeWidth,
|
||||||
|
'jsbarcode-margin': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attrs: {
|
||||||
|
class: 'documentBarcodeContainer',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Container' }, node => ({
|
||||||
|
...node,
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentContainer',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Flex' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentFlex',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Divider' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'hr',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentDivider',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'ProgressBar' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentProgressBar',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentProgressBarInner',
|
||||||
|
style: `width: ${Math.round((node.content[0] || 0) * 100)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'DateTime' }, node => {
|
||||||
|
const dateTime = dayjs.utc(node.content[0]);
|
||||||
|
return {
|
||||||
|
content: [dateTime.format('YYYY-MM-DD hh:mm:ss')],
|
||||||
|
tag: 'span',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentDateTime',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Table' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'table',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTable',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Row' }, node => {
|
||||||
|
const rowType = node.attrs?.type?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// Transform Col children based on the row type (header/footer/body)
|
||||||
|
const transformCols = content => {
|
||||||
|
if (!Array.isArray(content)) return content;
|
||||||
|
return content.map(child => {
|
||||||
|
if (typeof child === 'string' || child == null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (child.tag !== 'Col') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseAttrs = {
|
||||||
|
...child.attrs,
|
||||||
|
style: getNodeStyles(child.attrs)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rowType === 'header') {
|
||||||
|
// Header row columns become table headers
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
tag: 'th',
|
||||||
|
attrs: baseAttrs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer and body rows both use <td>; footer is distinguished by the row class
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
tag: 'td',
|
||||||
|
attrs: baseAttrs
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = transformCols(node.content);
|
||||||
|
|
||||||
|
if (rowType === 'header') {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRowHeader',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowType === 'footer') {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRowFooter',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRow',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]).process(content);
|
||||||
|
|
||||||
|
return result.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TemplateManager {
|
||||||
|
constructor() {
|
||||||
|
this.fc = {
|
||||||
|
listObjects: this.listObjects.bind(this),
|
||||||
|
getObject: this.getObject.bind(this),
|
||||||
|
formatDate: this.formatDate.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Previews an EJS template by rendering it with provided data
|
||||||
|
* @param {string} templateString - The EJS template as a string
|
||||||
|
* @param {Object} data - Data object to pass to the template
|
||||||
|
* @param {Object} options - EJS rendering options
|
||||||
|
* @returns {Promise<string>} The rendered HTML string
|
||||||
|
*/
|
||||||
|
async renderTemplate(
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
data = {},
|
||||||
|
scale = 1,
|
||||||
|
options = {},
|
||||||
|
preview = true
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Set default options for EJS rendering
|
||||||
|
const defaultOptions = {
|
||||||
|
async: true,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
logger.debug('Rendering template:', id);
|
||||||
|
|
||||||
|
const documentTemplate = await getObject({
|
||||||
|
model: documentTemplateModel,
|
||||||
|
id,
|
||||||
|
populate: [
|
||||||
|
{ path: 'documentSize' },
|
||||||
|
{ path: 'parent', strictPopulate: false }
|
||||||
|
],
|
||||||
|
cached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (documentTemplate == null) {
|
||||||
|
return { error: 'Document template not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentSize = documentTemplate.documentSize;
|
||||||
|
if (documentSize == null) {
|
||||||
|
return { error: 'Document template size not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate content parameter
|
||||||
|
if (content == null || typeof content !== 'string') {
|
||||||
|
return { error: 'Template content is required and must be a string.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure data has default undefefined values and then merge with data
|
||||||
|
var templateData = {};
|
||||||
|
if (documentTemplate.global == true) {
|
||||||
|
templateData = { content: contentPlaceholder, fc: this.fc };
|
||||||
|
} else {
|
||||||
|
const objectType = documentTemplate?.objectType;
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
if (model == null) {
|
||||||
|
return { error: `Unknown object type: ${objectType}` };
|
||||||
|
}
|
||||||
|
const defaultKeys = Object.keys(model.schema.obj);
|
||||||
|
const defaultValues = {};
|
||||||
|
for (const key of defaultKeys) {
|
||||||
|
defaultValues[key] = null;
|
||||||
|
}
|
||||||
|
templateData = { ...defaultValues, ...data, fc: this.fc };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the template
|
||||||
|
const templateContent = await ejs.render(
|
||||||
|
content,
|
||||||
|
templateData,
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
var templateWithParentContent;
|
||||||
|
|
||||||
|
var parentTemplate = documentTemplate.parent;
|
||||||
|
|
||||||
|
if (documentTemplate.parent != undefined) {
|
||||||
|
if (typeof parentTemplate === 'string') {
|
||||||
|
parentTemplate = await getObject({
|
||||||
|
model: documentTemplateModel,
|
||||||
|
id: parentTemplate,
|
||||||
|
populate: [
|
||||||
|
{ path: 'documentSize' },
|
||||||
|
{ path: 'parent', strictPopulate: false }
|
||||||
|
],
|
||||||
|
cached: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate parent content
|
||||||
|
if (
|
||||||
|
parentTemplate.content == null ||
|
||||||
|
typeof parentTemplate.content !== 'string'
|
||||||
|
) {
|
||||||
|
logger.error(
|
||||||
|
'Parent template content is required and must be a string.',
|
||||||
|
parentTemplate.content
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
'Parent template content is required and must be a string.' +
|
||||||
|
parentTemplate.content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
templateWithParentContent = await ejs.render(
|
||||||
|
parentTemplate.content,
|
||||||
|
{ content: templateContent, fc: this.fc },
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
templateWithParentContent = templateContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rendered content before transformation
|
||||||
|
if (
|
||||||
|
templateWithParentContent == null ||
|
||||||
|
typeof templateWithParentContent !== 'string'
|
||||||
|
) {
|
||||||
|
return { error: 'Failed to render template content.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateHtml = await transformCustomElements(
|
||||||
|
templateWithParentContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate transformed HTML
|
||||||
|
if (templateHtml == null || typeof templateHtml !== 'string') {
|
||||||
|
return { error: 'Failed to transform template content.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerHtml = null;
|
||||||
|
|
||||||
|
if (preview == true) {
|
||||||
|
innerHtml = await ejs.render(
|
||||||
|
previewTemplate,
|
||||||
|
{ content: templateHtml },
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
innerHtml = await ejs.render(
|
||||||
|
renderTemplateEjs,
|
||||||
|
{ content: templateHtml },
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate inner HTML
|
||||||
|
if (innerHtml == null || typeof innerHtml !== 'string') {
|
||||||
|
return { error: 'Failed to render inner template content.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const infiniteHeight = documentSize.infiniteHeight == true;
|
||||||
|
|
||||||
|
const baseHtml = await ejs.render(
|
||||||
|
baseTemplate,
|
||||||
|
{
|
||||||
|
content: innerHtml,
|
||||||
|
width: `${documentSize.width}mm`,
|
||||||
|
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
|
||||||
|
scaledWidth: `${documentSize.width * scale}mm`,
|
||||||
|
scaledHeight: infiniteHeight
|
||||||
|
? 'auto'
|
||||||
|
: `${documentSize.height * scale}mm`,
|
||||||
|
scale: `${scale}`,
|
||||||
|
baseCSS: baseCSS,
|
||||||
|
previewPaginationScript: preview ? previewPaginationScript : ''
|
||||||
|
},
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewObject = {
|
||||||
|
html: baseHtml,
|
||||||
|
width: documentSize.width,
|
||||||
|
height: infiniteHeight ? 'auto' : documentSize.height,
|
||||||
|
infiniteHeight: infiniteHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
return previewObject;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error whilst previewing template:', error.message);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a template string is valid EJS syntax
|
||||||
|
* @param {string} templateString - The EJS template as a string
|
||||||
|
* @returns {boolean} True if template is valid, false otherwise
|
||||||
|
*/
|
||||||
|
validateTemplate(templateString) {
|
||||||
|
try {
|
||||||
|
// Try to compile the template to check for syntax errors
|
||||||
|
ejs.compile(templateString);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a template to PDF format
|
||||||
|
* @param {string} id - The document template ID
|
||||||
|
* @param {string} content - The template content
|
||||||
|
* @param {Object} data - Data object to pass to the template
|
||||||
|
* @param {number} scale - Scale factor for rendering
|
||||||
|
* @param {Object} options - EJS rendering options
|
||||||
|
* @returns {Promise<Object>} Object containing PDF buffer or error
|
||||||
|
*/
|
||||||
|
async renderPDF(id, content, data = {}, options = {}) {
|
||||||
|
try {
|
||||||
|
logger.debug('Rendering PDF for template:', id);
|
||||||
|
|
||||||
|
const renderedTemplate = await this.renderTemplate(
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
data,
|
||||||
|
1,
|
||||||
|
options,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (renderedTemplate.error != undefined) {
|
||||||
|
return { error: renderedTemplate.error };
|
||||||
|
}
|
||||||
|
const baseHtml = renderedTemplate.html;
|
||||||
|
|
||||||
|
// Generate PDF using PDF factory
|
||||||
|
const pdfBuffer = await generatePDF(baseHtml, {
|
||||||
|
width: renderedTemplate.width,
|
||||||
|
height: renderedTemplate.height,
|
||||||
|
infiniteHeight: renderedTemplate.infiniteHeight
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfObject = {
|
||||||
|
pdf: pdfBuffer
|
||||||
|
};
|
||||||
|
return pdfObject;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error whilst rendering PDF:', error.message);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a template to image format for receipt printers
|
||||||
|
* @param {string} id - The document template ID
|
||||||
|
* @param {string} content - The template content
|
||||||
|
* @param {Object} data - Data object to pass to the template
|
||||||
|
* @param {Object} options - Image conversion options (e.g. width, height)
|
||||||
|
* @returns {Promise<Object>} Object containing image buffers or error
|
||||||
|
*/
|
||||||
|
async renderJPG(id, content, data = {}, options = {}) {
|
||||||
|
try {
|
||||||
|
logger.debug('Rendering JPG for template:', id);
|
||||||
|
|
||||||
|
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||||
|
if (pdfResult.error != undefined) {
|
||||||
|
return { error: pdfResult.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = await convertPDFToImage(pdfResult.pdf, options);
|
||||||
|
return { images };
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error whilst rendering JPG:', error.message);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listObjects(objectType, filter = {}, populate = []) {
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
if (model == undefined) {
|
||||||
|
throw new Error('Farm Control: Object type not found.');
|
||||||
|
}
|
||||||
|
const objects = await listObjects({
|
||||||
|
model,
|
||||||
|
filter,
|
||||||
|
populate
|
||||||
|
});
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date, format) {
|
||||||
|
return dayjs(date).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject(objectType = undefined, id = undefined, populate = []) {
|
||||||
|
if (objectType == undefined || objectType == '') {
|
||||||
|
logger.warn('Object type is required');
|
||||||
|
return { error: 'Object type is required' };
|
||||||
|
}
|
||||||
|
if (id == undefined || id == '') {
|
||||||
|
logger.warn('Object ID is required');
|
||||||
|
return { error: 'Object ID is required' };
|
||||||
|
}
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
if (model == undefined) {
|
||||||
|
logger.warn('Object type not found:', objectType);
|
||||||
|
}
|
||||||
|
const object = await getObject({ model, id, cached: true, populate });
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user