Files
iistwin/server/workers/automation-worker-code.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

119 lines
4.5 KiB
TypeScript

/**
* Automation worker code as a CommonJS string.
*
* Passed to `new Worker(AUTOMATION_WORKER_CODE, { eval: true })` so that the
* worker runs in any environment (dev with tsx, production dist/) without
* requiring a separate file to be present on disk.
*
* The source-of-truth logic lives here. esbuild inlines this string into the
* bundle when the server is built for production.
*/
export const AUTOMATION_WORKER_CODE = `
'use strict';
const { workerData, parentPort } = require('node:worker_threads');
const vm = require('vm');
if (!parentPort) throw new Error('Must be run as a Worker Thread');
const { code, organizationId, triggerData } = workerData;
const logs = [];
let callId = 0;
const pending = new Map();
parentPort.on('message', function(msg) {
if (msg.type !== 'storage-response') return;
const p = pending.get(msg.callId);
if (!p) return;
pending.delete(msg.callId);
if (msg.error !== undefined) {
p.reject(new Error(msg.error));
} else {
p.resolve(msg.result);
}
});
function callStorage(method, args) {
return new Promise(function(resolve, reject) {
const id = ++callId;
pending.set(id, { resolve, reject });
parentPort.postMessage({ type: 'storage-call', callId: id, method, args });
});
}
const ctx = {
organizationId,
triggerData,
result: undefined,
log: function() {
const args = Array.prototype.slice.call(arguments);
logs.push(args.map(function(a) {
return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a);
}).join(' '));
},
forms: {
list: function() { return callStorage('getFormsByOrganization', [organizationId]); },
get: function(formId) { return callStorage('getForm', [formId, organizationId]); },
getFields: function(formId) { return callStorage('getFormFields', [formId, organizationId]); },
getStatuses: function(formId) { return callStorage('getFormStatuses', [formId, organizationId]); },
},
users: {
get: function(userId) { return callStorage('getUser', [userId]); },
list: function() { return callStorage('getUsersByOrganization', [organizationId]); },
update: function(userId, updates) { return callStorage('updateUser', [userId, updates]); },
updateStatus: function(userId, statusId) { return callStorage('updateUser', [userId, { statusId }]); },
},
tasks: {
list: function(formId) { return callStorage('getTasksByForm', [formId, organizationId]); },
get: function(taskId) { return callStorage('getTask', [taskId, organizationId]); },
create: function(data) {
return callStorage('createTask', [Object.assign({}, data, { organizationId })]);
},
update: function(taskId, updates) {
return callStorage('updateTask', [taskId, organizationId, updates]);
},
delete: function(taskId) {
return callStorage('deleteTask', [taskId, organizationId]);
},
getFieldValues: function(taskId) {
return callStorage('getTaskFieldValues', [taskId, organizationId]);
},
getAssignees: function(taskId) {
return callStorage('getTaskAssignees', [taskId, organizationId]);
},
setFieldValue: async function(taskId, fieldCode, value) {
const fields = await callStorage('getTaskFieldValues', [taskId, organizationId]);
const task = await callStorage('getTask', [taskId, organizationId]);
if (!task) throw new Error('Task ' + taskId + ' not found');
const formFields = await callStorage('getFormFields', [task.formId, organizationId]);
const field = formFields.find(function(f) { return f.fieldCode === fieldCode; });
if (!field) throw new Error('Field ' + fieldCode + ' not found');
const existing = fields.find(function(v) { return v.fieldId === field.id; });
if (existing) {
return callStorage('updateTaskFieldValue', [taskId, field.id, organizationId, { value: String(value) }]);
} else {
return callStorage('createTaskFieldValue', [{ taskId, fieldId: field.id, value: String(value) }]);
}
},
},
};
async function run() {
try {
const wrappedCode = '(async () => { ' + code + ' })()';
const sandbox = vm.createContext({
ctx,
console: { log: ctx.log, error: ctx.log, warn: ctx.log },
});
const script = new vm.Script(wrappedCode);
await script.runInContext(sandbox, { timeout: 5000 });
parentPort.postMessage({ type: 'done', logs, result: ctx.result });
} catch (err) {
const message = err && err.message ? err.message : String(err);
logs.push('ERROR: ' + message);
parentPort.postMessage({ type: 'done', logs, error: message });
}
}
run();
`;