n8n-nodes-variable 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,744 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Variable = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const valueParser_1 = require("./helpers/valueParser");
6
+ const storage_1 = require("./helpers/storage");
7
+ class Variable {
8
+ constructor() {
9
+ this.description = {
10
+ displayName: 'Variable',
11
+ name: 'variable',
12
+ icon: 'file:variable.svg',
13
+ group: ['transform'],
14
+ version: 1,
15
+ subtitle: '={{$parameter["operation"]}}',
16
+ description: 'Store, retrieve, update, and delete workflow variables.',
17
+ defaults: {
18
+ name: 'Variable',
19
+ },
20
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
21
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
22
+ properties: [
23
+ // ── Operation ─────────────────────────────────────────────────────────
24
+ {
25
+ displayName: 'Operation',
26
+ name: 'operation',
27
+ type: 'options',
28
+ noDataExpression: true,
29
+ options: [
30
+ { name: 'Set Variable', value: 'set', action: 'Set a variable', description: 'Create or update a variable' },
31
+ { name: 'Get Variable', value: 'get', action: 'Get a variable', description: 'Retrieve a variable value' },
32
+ { name: 'Delete Variable', value: 'delete', action: 'Delete a variable', description: 'Remove a variable' },
33
+ { name: 'Has Variable', value: 'has', action: 'Check if a variable exists', description: 'Returns true/false if a variable exists' },
34
+ { name: 'List Variables', value: 'list', action: 'List variables', description: 'List all variables in a namespace' },
35
+ { name: 'Clear Variables', value: 'clear', action: 'Clear all variables', description: 'Remove all variables in a namespace' },
36
+ { name: 'Increment Variable', value: 'increment', action: 'Increment a variable', description: 'Add a number to a numeric variable' },
37
+ { name: 'Decrement Variable', value: 'decrement', action: 'Decrement a variable', description: 'Subtract a number from a numeric variable' },
38
+ { name: 'Append to Array', value: 'appendToArray', action: 'Append to an array variable', description: 'Push a value onto an array variable' },
39
+ { name: 'Merge Object', value: 'mergeObject', action: 'Merge into an object variable', description: 'Merge a JSON object into an object variable' },
40
+ { name: 'Toggle Boolean', value: 'toggleBoolean', action: 'Toggle a boolean variable', description: 'Flip a boolean variable between true and false' },
41
+ ],
42
+ default: 'set',
43
+ },
44
+ // ── Scope ──────────────────────────────────────────────────────────────
45
+ {
46
+ displayName: 'Scope',
47
+ name: 'scope',
48
+ type: 'options',
49
+ noDataExpression: true,
50
+ options: [
51
+ {
52
+ name: 'Local (This Execution)',
53
+ value: 'localExecution',
54
+ description: 'Variables exist only for this workflow run. Stored on the item JSON.',
55
+ },
56
+ {
57
+ name: 'Workflow Global',
58
+ value: 'workflowGlobal',
59
+ description: 'Variables persist across workflow executions for this workflow.',
60
+ },
61
+ {
62
+ name: 'Node Local',
63
+ value: 'nodeLocal',
64
+ description: 'Variables persist for this specific node instance.',
65
+ },
66
+ {
67
+ name: 'Custom Namespace',
68
+ value: 'customNamespace',
69
+ description: 'Workflow global storage with a custom namespace expression.',
70
+ },
71
+ ],
72
+ default: 'workflowGlobal',
73
+ description: 'Where to store the variable',
74
+ },
75
+ // ── Local Storage Path (local scope only) ─────────────────────────────
76
+ {
77
+ displayName: 'Local Storage Path',
78
+ name: 'localStoragePath',
79
+ type: 'string',
80
+ default: '_variables',
81
+ description: 'The key on the item JSON where local variables are stored',
82
+ displayOptions: { show: { scope: ['localExecution'] } },
83
+ },
84
+ // ── Custom Namespace Name ─────────────────────────────────────────────
85
+ {
86
+ displayName: 'Custom Namespace',
87
+ name: 'customNamespaceName',
88
+ type: 'string',
89
+ default: '',
90
+ required: true,
91
+ placeholder: 'e.g. economy, cooldowns, guild_{{$json.guild.id}}',
92
+ description: 'The namespace to use. Supports expressions.',
93
+ displayOptions: { show: { scope: ['customNamespace'] } },
94
+ },
95
+ // ── Namespace (non-local scopes) ──────────────────────────────────────
96
+ {
97
+ displayName: 'Namespace',
98
+ name: 'namespace',
99
+ type: 'string',
100
+ default: 'default',
101
+ placeholder: 'e.g. economy, cooldowns',
102
+ description: 'Namespace to organize variables. Supports expressions like economy_{{$json.guild.id}}.',
103
+ displayOptions: {
104
+ show: {
105
+ scope: ['workflowGlobal', 'nodeLocal'],
106
+ },
107
+ },
108
+ },
109
+ // ── Key ───────────────────────────────────────────────────────────────
110
+ {
111
+ displayName: 'Key',
112
+ name: 'key',
113
+ type: 'string',
114
+ default: '',
115
+ required: true,
116
+ placeholder: 'e.g. balance_{{$json.user.id}}',
117
+ description: 'The variable key. Supports expressions.',
118
+ displayOptions: {
119
+ hide: { operation: ['list', 'clear'] },
120
+ },
121
+ },
122
+ // ── Value Type (set / appendToArray) ──────────────────────────────────
123
+ {
124
+ displayName: 'Value Type',
125
+ name: 'valueType',
126
+ type: 'options',
127
+ options: [
128
+ { name: 'Auto (preserve expression type)', value: 'auto' },
129
+ { name: 'String', value: 'string' },
130
+ { name: 'Number', value: 'number' },
131
+ { name: 'Boolean', value: 'boolean' },
132
+ { name: 'JSON', value: 'json' },
133
+ { name: 'Array', value: 'array' },
134
+ { name: 'Object', value: 'object' },
135
+ ],
136
+ default: 'auto',
137
+ description: 'How to interpret the value',
138
+ displayOptions: {
139
+ show: { operation: ['set', 'appendToArray'] },
140
+ },
141
+ },
142
+ // ── Value ─────────────────────────────────────────────────────────────
143
+ {
144
+ displayName: 'Value',
145
+ name: 'value',
146
+ type: 'string',
147
+ default: '',
148
+ description: 'The value to store. Supports expressions.',
149
+ displayOptions: {
150
+ show: { operation: ['set', 'appendToArray'] },
151
+ },
152
+ },
153
+ // ── Overwrite Existing (set) ──────────────────────────────────────────
154
+ {
155
+ displayName: 'Overwrite If Exists',
156
+ name: 'overwriteExisting',
157
+ type: 'boolean',
158
+ default: true,
159
+ description: 'Whether to overwrite the variable if it already exists',
160
+ displayOptions: { show: { operation: ['set'] } },
161
+ },
162
+ // ── Get operation fields ──────────────────────────────────────────────
163
+ {
164
+ displayName: 'Use Default Value',
165
+ name: 'useDefaultValue',
166
+ type: 'boolean',
167
+ default: false,
168
+ description: 'Whether to return a default value when the variable does not exist',
169
+ displayOptions: { show: { operation: ['get'] } },
170
+ },
171
+ {
172
+ displayName: 'Default Value',
173
+ name: 'defaultValue',
174
+ type: 'string',
175
+ default: '',
176
+ description: 'Value to return when the variable does not exist',
177
+ displayOptions: {
178
+ show: {
179
+ operation: ['get'],
180
+ useDefaultValue: [true],
181
+ },
182
+ },
183
+ },
184
+ {
185
+ displayName: 'Output Field Name',
186
+ name: 'getOutputFieldName',
187
+ type: 'string',
188
+ default: 'value',
189
+ description: 'The field name to put the retrieved value in on the output item',
190
+ displayOptions: { show: { operation: ['get'] } },
191
+ },
192
+ // ── Increment / Decrement ─────────────────────────────────────────────
193
+ {
194
+ displayName: 'Amount',
195
+ name: 'incrementAmount',
196
+ type: 'number',
197
+ default: 1,
198
+ description: 'The amount to add (increment) or subtract (decrement)',
199
+ displayOptions: { show: { operation: ['increment', 'decrement'] } },
200
+ },
201
+ {
202
+ displayName: 'Initialize If Missing',
203
+ name: 'initIfMissingNumeric',
204
+ type: 'boolean',
205
+ default: true,
206
+ description: 'Whether to create the variable with an initial value if it does not exist',
207
+ displayOptions: { show: { operation: ['increment', 'decrement'] } },
208
+ },
209
+ {
210
+ displayName: 'Initial Value',
211
+ name: 'numericInitialValue',
212
+ type: 'number',
213
+ default: 0,
214
+ description: 'The starting value if the variable does not exist yet',
215
+ displayOptions: {
216
+ show: {
217
+ operation: ['increment', 'decrement'],
218
+ initIfMissingNumeric: [true],
219
+ },
220
+ },
221
+ },
222
+ // ── Append to Array ───────────────────────────────────────────────────
223
+ {
224
+ displayName: 'Initialize If Missing',
225
+ name: 'initIfMissingArray',
226
+ type: 'boolean',
227
+ default: true,
228
+ description: 'Whether to create an empty array if the variable does not exist',
229
+ displayOptions: { show: { operation: ['appendToArray'] } },
230
+ },
231
+ // ── Merge Object ──────────────────────────────────────────────────────
232
+ {
233
+ displayName: 'Object (JSON)',
234
+ name: 'objectJson',
235
+ type: 'string',
236
+ default: '{}',
237
+ description: 'The JSON object to merge in. Supports expressions.',
238
+ typeOptions: { rows: 4 },
239
+ displayOptions: { show: { operation: ['mergeObject'] } },
240
+ },
241
+ {
242
+ displayName: 'Deep Merge',
243
+ name: 'deepMerge',
244
+ type: 'boolean',
245
+ default: false,
246
+ description: 'Whether to recursively merge nested objects instead of shallow-merging',
247
+ displayOptions: { show: { operation: ['mergeObject'] } },
248
+ },
249
+ {
250
+ displayName: 'Initialize If Missing',
251
+ name: 'initIfMissingObject',
252
+ type: 'boolean',
253
+ default: true,
254
+ description: 'Whether to create an empty object if the variable does not exist',
255
+ displayOptions: { show: { operation: ['mergeObject'] } },
256
+ },
257
+ // ── Toggle Boolean ────────────────────────────────────────────────────
258
+ {
259
+ displayName: 'Initialize If Missing',
260
+ name: 'initIfMissingBoolean',
261
+ type: 'boolean',
262
+ default: true,
263
+ description: 'Whether to initialize the variable if it does not exist',
264
+ displayOptions: { show: { operation: ['toggleBoolean'] } },
265
+ },
266
+ {
267
+ displayName: 'Initial Value',
268
+ name: 'booleanInitialValue',
269
+ type: 'boolean',
270
+ default: false,
271
+ description: 'The starting boolean value if the variable does not exist. The toggle will flip this immediately.',
272
+ displayOptions: {
273
+ show: {
274
+ operation: ['toggleBoolean'],
275
+ initIfMissingBoolean: [true],
276
+ },
277
+ },
278
+ },
279
+ // ── List ──────────────────────────────────────────────────────────────
280
+ {
281
+ displayName: 'Include Values',
282
+ name: 'includeValues',
283
+ type: 'boolean',
284
+ default: true,
285
+ description: 'Whether to include variable values in the output, or only the keys',
286
+ displayOptions: { show: { operation: ['list'] } },
287
+ },
288
+ // ── Clear ─────────────────────────────────────────────────────────────
289
+ {
290
+ displayName: 'Confirmation',
291
+ name: 'clearConfirmation',
292
+ type: 'string',
293
+ default: '',
294
+ required: true,
295
+ placeholder: 'Type CLEAR to confirm',
296
+ description: 'Type the word CLEAR (uppercase) to confirm deleting all variables in this namespace',
297
+ displayOptions: { show: { operation: ['clear'] } },
298
+ },
299
+ // ── Output Mode ───────────────────────────────────────────────────────
300
+ {
301
+ displayName: 'Output Mode',
302
+ name: 'outputMode',
303
+ type: 'options',
304
+ options: [
305
+ {
306
+ name: 'Preserve Input + Add Result',
307
+ value: 'preserveAndAdd',
308
+ description: 'Keep all input fields and add the result object',
309
+ },
310
+ {
311
+ name: 'Result Only',
312
+ value: 'resultOnly',
313
+ description: 'Output only the operation result object',
314
+ },
315
+ {
316
+ name: 'Add / Update Field',
317
+ value: 'addField',
318
+ description: 'Add or update a single field on the input item with the variable value',
319
+ },
320
+ ],
321
+ default: 'preserveAndAdd',
322
+ description: 'How to construct the output item',
323
+ },
324
+ {
325
+ displayName: 'Result Field Name',
326
+ name: 'resultFieldName',
327
+ type: 'string',
328
+ default: 'variable',
329
+ description: 'The field name that will hold the operation result on the output item',
330
+ displayOptions: {
331
+ show: { outputMode: ['preserveAndAdd', 'resultOnly'] },
332
+ },
333
+ },
334
+ {
335
+ displayName: 'Field Name',
336
+ name: 'addFieldName',
337
+ type: 'string',
338
+ default: 'value',
339
+ description: 'The field name to set on the output item with the variable value',
340
+ displayOptions: {
341
+ show: { outputMode: ['addField'] },
342
+ },
343
+ },
344
+ // ── Include Metadata ──────────────────────────────────────────────────
345
+ {
346
+ displayName: 'Include Metadata',
347
+ name: 'includeMetadata',
348
+ type: 'boolean',
349
+ default: false,
350
+ description: 'Whether to store and expose createdAt/updatedAt/type metadata for workflow-global and node-local variables',
351
+ displayOptions: {
352
+ show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace'] },
353
+ },
354
+ },
355
+ ],
356
+ };
357
+ }
358
+ async execute() {
359
+ const items = this.getInputData();
360
+ const returnData = [];
361
+ for (let i = 0; i < items.length; i++) {
362
+ try {
363
+ const result = await processItem(this, items[i], i);
364
+ returnData.push(result);
365
+ }
366
+ catch (error) {
367
+ if (this.continueOnFail()) {
368
+ returnData.push({
369
+ json: { error: error.message },
370
+ pairedItem: { item: i },
371
+ });
372
+ continue;
373
+ }
374
+ if (error instanceof n8n_workflow_1.NodeOperationError)
375
+ throw error;
376
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
377
+ }
378
+ }
379
+ return [returnData];
380
+ }
381
+ }
382
+ exports.Variable = Variable;
383
+ // ─── Core item processor ──────────────────────────────────────────────────────
384
+ async function processItem(ctx, item, i) {
385
+ const operation = ctx.getNodeParameter('operation', i);
386
+ const scope = ctx.getNodeParameter('scope', i);
387
+ const outputMode = ctx.getNodeParameter('outputMode', i, 'preserveAndAdd');
388
+ const includeMetadata = scope !== 'localExecution'
389
+ ? ctx.getNodeParameter('includeMetadata', i, false)
390
+ : false;
391
+ const resolvedNamespace = resolveNamespace(ctx, scope, i);
392
+ (0, valueParser_1.validateNamespace)(resolvedNamespace);
393
+ // Clone item JSON so we don't mutate the input
394
+ const itemJson = { ...item.json };
395
+ const result = executeOperation(ctx, operation, scope, resolvedNamespace, itemJson, i, includeMetadata);
396
+ return buildOutputItem(ctx, item, itemJson, result, outputMode, i);
397
+ }
398
+ // ─── Namespace resolution ─────────────────────────────────────────────────────
399
+ function resolveNamespace(ctx, scope, i) {
400
+ if (scope === 'customNamespace') {
401
+ return ctx.getNodeParameter('customNamespaceName', i, '').trim();
402
+ }
403
+ if (scope === 'localExecution') {
404
+ // For local scope the namespace is still configurable via the namespace param
405
+ // but we default to 'default'. We'll use a simpler path here.
406
+ return 'default';
407
+ }
408
+ return ctx.getNodeParameter('namespace', i, 'default').trim() || 'default';
409
+ }
410
+ // ─── Operation dispatcher ─────────────────────────────────────────────────────
411
+ function executeOperation(ctx, operation, scope, namespace, itemJson, i, includeMetadata) {
412
+ const isLocal = scope === 'localExecution';
413
+ const storagePath = isLocal
414
+ ? ctx.getNodeParameter('localStoragePath', i, '_variables')
415
+ : '';
416
+ // Helpers to get/set/delete/has/list/clear depending on scope
417
+ const get = (key) => {
418
+ if (isLocal)
419
+ return (0, storage_1.localGetVariable)(itemJson, storagePath, namespace, key);
420
+ const entry = (0, storage_1.staticGetVariable)(getStaticData(ctx, scope), namespace, key);
421
+ return entry?.value;
422
+ };
423
+ const set = (key, value, typeName) => {
424
+ if (isLocal) {
425
+ (0, storage_1.localSetVariable)(itemJson, storagePath, namespace, key, value);
426
+ }
427
+ else {
428
+ (0, storage_1.staticSetVariable)(getStaticData(ctx, scope), namespace, key, value, typeName, includeMetadata);
429
+ }
430
+ };
431
+ const del = (key) => {
432
+ if (isLocal)
433
+ return (0, storage_1.localDeleteVariable)(itemJson, storagePath, namespace, key);
434
+ return (0, storage_1.staticDeleteVariable)(getStaticData(ctx, scope), namespace, key);
435
+ };
436
+ const has = (key) => {
437
+ if (isLocal)
438
+ return (0, storage_1.localHasVariable)(itemJson, storagePath, namespace, key);
439
+ return (0, storage_1.staticHasVariable)(getStaticData(ctx, scope), namespace, key);
440
+ };
441
+ const list = () => {
442
+ if (isLocal)
443
+ return (0, storage_1.localListVariables)(itemJson, storagePath, namespace);
444
+ const raw = (0, storage_1.staticListVariables)(getStaticData(ctx, scope), namespace);
445
+ // unwrap StoredVariableEntry to plain values
446
+ const result = {};
447
+ for (const [k, entry] of Object.entries(raw)) {
448
+ result[k] = entry.value;
449
+ }
450
+ return result;
451
+ };
452
+ const clear = () => {
453
+ if (isLocal)
454
+ return (0, storage_1.localClearNamespace)(itemJson, storagePath, namespace);
455
+ return (0, storage_1.staticClearNamespace)(getStaticData(ctx, scope), namespace);
456
+ };
457
+ const scopeLabel = scope;
458
+ switch (operation) {
459
+ case 'set': return opSet(ctx, i, namespace, scopeLabel, get, set, has);
460
+ case 'get': return opGet(ctx, i, namespace, scopeLabel, get, has);
461
+ case 'delete': return opDelete(ctx, i, namespace, scopeLabel, del, has);
462
+ case 'has': return opHas(ctx, i, namespace, scopeLabel, has);
463
+ case 'list': return opList(ctx, i, namespace, scopeLabel, list);
464
+ case 'clear': return opClear(ctx, i, namespace, scopeLabel, clear);
465
+ case 'increment': return opIncrement(ctx, i, namespace, scopeLabel, get, set, has, 1);
466
+ case 'decrement': return opIncrement(ctx, i, namespace, scopeLabel, get, set, has, -1);
467
+ case 'appendToArray': return opAppendToArray(ctx, i, namespace, scopeLabel, get, set, has);
468
+ case 'mergeObject': return opMergeObject(ctx, i, namespace, scopeLabel, get, set, has);
469
+ case 'toggleBoolean': return opToggleBoolean(ctx, i, namespace, scopeLabel, get, set, has);
470
+ default:
471
+ throw new Error(`Unknown operation: ${operation}`);
472
+ }
473
+ }
474
+ function getStaticData(ctx, scope) {
475
+ if (scope === 'nodeLocal')
476
+ return ctx.getWorkflowStaticData('node');
477
+ return ctx.getWorkflowStaticData('global');
478
+ }
479
+ // ─── Individual operations ────────────────────────────────────────────────────
480
+ function opSet(ctx, i, namespace, scopeLabel, get, set, has) {
481
+ const key = getKey(ctx, i);
482
+ const valueType = ctx.getNodeParameter('valueType', i, 'auto');
483
+ const rawValue = ctx.getNodeParameter('value', i, '');
484
+ const overwrite = ctx.getNodeParameter('overwriteExisting', i, true);
485
+ if (!overwrite && has(key)) {
486
+ throw new Error(`Variable "${key}" already exists in namespace "${namespace}". Enable "Overwrite If Exists" to update it.`);
487
+ }
488
+ const parsed = (0, valueParser_1.parseValueByType)(rawValue, valueType);
489
+ const typeName = valueType === 'auto' ? (0, valueParser_1.inferValueType)(parsed) : valueType;
490
+ set(key, parsed, typeName);
491
+ return {
492
+ operation: 'set',
493
+ scope: scopeLabel,
494
+ namespace,
495
+ key,
496
+ value: parsed,
497
+ };
498
+ }
499
+ function opGet(ctx, i, namespace, scopeLabel, get, has) {
500
+ const key = getKey(ctx, i);
501
+ const exists = has(key);
502
+ const useDefault = ctx.getNodeParameter('useDefaultValue', i, false);
503
+ const defaultValue = ctx.getNodeParameter('defaultValue', i, '');
504
+ let value;
505
+ if (exists) {
506
+ value = get(key);
507
+ }
508
+ else if (useDefault) {
509
+ value = defaultValue;
510
+ }
511
+ else {
512
+ throw new Error(`Variable "${key}" does not exist in namespace "${namespace}". Enable "Use Default Value" or create the variable first.`);
513
+ }
514
+ return {
515
+ operation: 'get',
516
+ scope: scopeLabel,
517
+ namespace,
518
+ key,
519
+ value,
520
+ exists,
521
+ };
522
+ }
523
+ function opDelete(ctx, i, namespace, scopeLabel, del, has) {
524
+ const key = getKey(ctx, i);
525
+ const existed = has(key);
526
+ const deleted = del(key);
527
+ return {
528
+ operation: 'delete',
529
+ scope: scopeLabel,
530
+ namespace,
531
+ key,
532
+ exists: existed,
533
+ deleted,
534
+ };
535
+ }
536
+ function opHas(ctx, i, namespace, scopeLabel, has) {
537
+ const key = getKey(ctx, i);
538
+ const exists = has(key);
539
+ return {
540
+ operation: 'has',
541
+ scope: scopeLabel,
542
+ namespace,
543
+ key,
544
+ exists,
545
+ };
546
+ }
547
+ function opList(ctx, i, namespace, scopeLabel, list) {
548
+ const includeValues = ctx.getNodeParameter('includeValues', i, true);
549
+ const vars = list();
550
+ const keys = Object.keys(vars);
551
+ const result = {
552
+ operation: 'list',
553
+ scope: scopeLabel,
554
+ namespace,
555
+ keys,
556
+ count: keys.length,
557
+ };
558
+ if (includeValues) {
559
+ result.variables = vars;
560
+ }
561
+ return result;
562
+ }
563
+ function opClear(ctx, i, namespace, scopeLabel, clearFn) {
564
+ const confirmation = ctx.getNodeParameter('clearConfirmation', i, '');
565
+ if (confirmation.trim() !== 'CLEAR') {
566
+ throw new Error('Clear cancelled: you must type CLEAR (uppercase) in the Confirmation field to proceed.');
567
+ }
568
+ const count = clearFn();
569
+ return {
570
+ operation: 'clear',
571
+ scope: scopeLabel,
572
+ namespace,
573
+ count,
574
+ cleared: true,
575
+ };
576
+ }
577
+ function opIncrement(ctx, i, namespace, scopeLabel, get, set, has, direction) {
578
+ const key = getKey(ctx, i);
579
+ const amount = ctx.getNodeParameter('incrementAmount', i, 1);
580
+ const initIfMissing = ctx.getNodeParameter('initIfMissingNumeric', i, true);
581
+ const initialValue = ctx.getNodeParameter('numericInitialValue', i, 0);
582
+ const opName = direction === 1 ? 'increment' : 'decrement';
583
+ let current;
584
+ if (!has(key)) {
585
+ if (!initIfMissing) {
586
+ throw new Error(`Variable "${key}" does not exist in namespace "${namespace}". Enable "Initialize If Missing" to create it automatically.`);
587
+ }
588
+ current = initialValue;
589
+ }
590
+ else {
591
+ const existing = get(key);
592
+ if (typeof existing !== 'number' || !Number.isFinite(existing)) {
593
+ throw new Error(`Cannot ${opName} "${key}": current value is not a finite number (got ${typeof existing}: ${JSON.stringify(existing)}).`);
594
+ }
595
+ current = existing;
596
+ }
597
+ const newValue = current + direction * Math.abs(amount);
598
+ set(key, newValue, 'number');
599
+ return {
600
+ operation: opName,
601
+ scope: scopeLabel,
602
+ namespace,
603
+ key,
604
+ value: newValue,
605
+ previousValue: has(key) ? current : undefined,
606
+ };
607
+ }
608
+ function opAppendToArray(ctx, i, namespace, scopeLabel, get, set, has) {
609
+ const key = getKey(ctx, i);
610
+ const valueType = ctx.getNodeParameter('valueType', i, 'auto');
611
+ const rawValue = ctx.getNodeParameter('value', i, '');
612
+ const initIfMissing = ctx.getNodeParameter('initIfMissingArray', i, true);
613
+ let arr;
614
+ if (!has(key)) {
615
+ if (!initIfMissing) {
616
+ throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create an empty array automatically.`);
617
+ }
618
+ arr = [];
619
+ }
620
+ else {
621
+ const existing = get(key);
622
+ if (!Array.isArray(existing)) {
623
+ throw new Error(`Cannot append to "${key}": existing value is not an array (got ${typeof existing}).`);
624
+ }
625
+ arr = [...existing];
626
+ }
627
+ const parsed = (0, valueParser_1.parseValueByType)(rawValue, valueType);
628
+ arr.push(parsed);
629
+ set(key, arr, 'array');
630
+ return {
631
+ operation: 'appendToArray',
632
+ scope: scopeLabel,
633
+ namespace,
634
+ key,
635
+ value: arr,
636
+ };
637
+ }
638
+ function opMergeObject(ctx, i, namespace, scopeLabel, get, set, has) {
639
+ const key = getKey(ctx, i);
640
+ const objectJsonRaw = ctx.getNodeParameter('objectJson', i, '{}');
641
+ const useDeepMerge = ctx.getNodeParameter('deepMerge', i, false);
642
+ const initIfMissing = ctx.getNodeParameter('initIfMissingObject', i, true);
643
+ // Parse incoming object
644
+ let incoming;
645
+ try {
646
+ const parsed = typeof objectJsonRaw === 'object' && objectJsonRaw !== null
647
+ ? objectJsonRaw
648
+ : JSON.parse(String(objectJsonRaw));
649
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
650
+ throw new Error('not a plain object');
651
+ }
652
+ incoming = parsed;
653
+ }
654
+ catch {
655
+ throw new Error(`Object (JSON) must be a plain object. Got: ${String(objectJsonRaw).slice(0, 100)}`);
656
+ }
657
+ let base;
658
+ if (!has(key)) {
659
+ if (!initIfMissing) {
660
+ throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create an empty object automatically.`);
661
+ }
662
+ base = {};
663
+ }
664
+ else {
665
+ const existing = get(key);
666
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
667
+ throw new Error(`Cannot merge into "${key}": existing value is not a plain object (got ${Array.isArray(existing) ? 'array' : typeof existing}).`);
668
+ }
669
+ base = { ...existing };
670
+ }
671
+ const merged = useDeepMerge ? (0, valueParser_1.deepMerge)(base, incoming) : { ...base, ...incoming };
672
+ set(key, merged, 'object');
673
+ return {
674
+ operation: 'mergeObject',
675
+ scope: scopeLabel,
676
+ namespace,
677
+ key,
678
+ value: merged,
679
+ };
680
+ }
681
+ function opToggleBoolean(ctx, i, namespace, scopeLabel, get, set, has) {
682
+ const key = getKey(ctx, i);
683
+ const initIfMissing = ctx.getNodeParameter('initIfMissingBoolean', i, true);
684
+ const initialValue = ctx.getNodeParameter('booleanInitialValue', i, false);
685
+ let current;
686
+ if (!has(key)) {
687
+ if (!initIfMissing) {
688
+ throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create it automatically.`);
689
+ }
690
+ current = initialValue;
691
+ }
692
+ else {
693
+ const existing = get(key);
694
+ if (typeof existing !== 'boolean') {
695
+ throw new Error(`Cannot toggle "${key}": existing value is not a boolean (got ${typeof existing}: ${JSON.stringify(existing)}).`);
696
+ }
697
+ current = existing;
698
+ }
699
+ const newValue = !current;
700
+ set(key, newValue, 'boolean');
701
+ return {
702
+ operation: 'toggleBoolean',
703
+ scope: scopeLabel,
704
+ namespace,
705
+ key,
706
+ value: newValue,
707
+ previousValue: current,
708
+ };
709
+ }
710
+ // ─── Output builder ───────────────────────────────────────────────────────────
711
+ function buildOutputItem(ctx, originalItem, updatedItemJson, result, outputMode, i) {
712
+ if (outputMode === 'resultOnly') {
713
+ const fieldName = ctx.getNodeParameter('resultFieldName', i, 'variable');
714
+ return {
715
+ json: { [fieldName]: result },
716
+ pairedItem: { item: i },
717
+ };
718
+ }
719
+ if (outputMode === 'addField') {
720
+ const fieldName = ctx.getNodeParameter('addFieldName', i, 'value');
721
+ return {
722
+ json: {
723
+ ...updatedItemJson,
724
+ [fieldName]: result.value,
725
+ },
726
+ pairedItem: { item: i },
727
+ };
728
+ }
729
+ // preserveAndAdd (default)
730
+ const fieldName = ctx.getNodeParameter('resultFieldName', i, 'variable');
731
+ return {
732
+ json: {
733
+ ...updatedItemJson,
734
+ [fieldName]: result,
735
+ },
736
+ pairedItem: { item: i },
737
+ };
738
+ }
739
+ // ─── Utility ──────────────────────────────────────────────────────────────────
740
+ function getKey(ctx, i) {
741
+ const key = ctx.getNodeParameter('key', i, '').trim();
742
+ (0, valueParser_1.validateKey)(key);
743
+ return key;
744
+ }