fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
118
server/workers/automation-worker-code.ts
Normal file
118
server/workers/automation-worker-code.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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();
|
||||
`;
|
||||
Reference in New Issue
Block a user