n8n-nodes-variable 1.0.8 → 1.1.0
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.
- package/README.md +58 -5
- package/dist/credentials/N8nVariableNodeApi.credentials.d.ts +7 -0
- package/dist/credentials/N8nVariableNodeApi.credentials.js +31 -0
- package/dist/nodes/Variable/Variable.node.js +235 -3
- package/dist/nodes/Variable/helpers/dataTableStorage.d.ts +8 -0
- package/dist/nodes/Variable/helpers/dataTableStorage.js +162 -0
- package/dist/nodes/Variable/helpers/dbStorage.d.ts +6 -5
- package/dist/nodes/Variable/helpers/dbStorage.js +75 -84
- package/dist/nodes/Variable/helpers/types.d.ts +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ The **Variable** node lets you store, retrieve, update, and delete named variabl
|
|
|
12
12
|
- **Workflow Global** — persists across executions using n8n's workflow static data
|
|
13
13
|
- **Node Local** — persists for the specific node instance
|
|
14
14
|
- **Custom Namespace** — workflow global storage with a fully dynamic namespace string (great for per-user / per-guild data)
|
|
15
|
-
- **Cross-Workflow (Shared)** — variables are stored in a local
|
|
15
|
+
- **Cross-Workflow (Shared)** — variables are stored in a local JSON file and shared across **all** workflows on this n8n instance
|
|
16
|
+
- **Cross-Workflow (Data Tables)** — variables are stored in n8n's built-in **Data Tables**, visible in the Data Tables UI tab and accessible from any workflow via the n8n API
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -97,15 +98,40 @@ guild_{{$json.guild.id}} → per-guild settings
|
|
|
97
98
|
|
|
98
99
|
### Cross-Workflow (Shared)
|
|
99
100
|
|
|
100
|
-
Variables are stored in a
|
|
101
|
+
Variables are stored in a JSON file on the n8n host at:
|
|
101
102
|
|
|
102
103
|
```
|
|
103
|
-
${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.
|
|
104
|
+
${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
The
|
|
107
|
+
The file is **created automatically on first use** — no setup or dependencies required. Writes are performed atomically (write to a temporary file, then rename) to prevent corruption. Data survives instance restarts and is accessible from any workflow on the same n8n instance.
|
|
107
108
|
|
|
108
|
-
> **Note:** the
|
|
109
|
+
> **Note:** the file lives on the n8n host machine. If you run n8n in a container or cloud environment, ensure the `.n8n` data directory is persisted to a volume so data is not lost on container restarts.
|
|
110
|
+
|
|
111
|
+
### Cross-Workflow (Data Tables)
|
|
112
|
+
|
|
113
|
+
Variables are stored as rows in an n8n **Data Table**, making them visible and editable directly in the n8n UI under the **Data Tables** tab.
|
|
114
|
+
|
|
115
|
+
Each namespace maps to one table named `var_<namespace>` (e.g., namespace `global_stats` → table `var_global_stats`). The table is **created automatically on first use**. Each row stores a `key` and a `value` (JSON-serialised).
|
|
116
|
+
|
|
117
|
+
#### Prerequisites
|
|
118
|
+
|
|
119
|
+
1. **Enable the n8n API** — in your n8n instance go to **Settings → API** and create an API key.
|
|
120
|
+
2. **Create a credential** — add a new credential of type **n8n Variable Node API** and enter:
|
|
121
|
+
- **n8n Instance URL** — the base URL your n8n instance is reachable at *from within the n8n process itself* (e.g. `http://localhost:5678` for local/Docker installs, or `https://your-instance.example.com` for cloud).
|
|
122
|
+
- **API Key** — the key generated in step 1.
|
|
123
|
+
3. In the Variable node, set **Scope** to **Cross-Workflow (Data Tables)** and select the credential.
|
|
124
|
+
|
|
125
|
+
#### What gets stored
|
|
126
|
+
|
|
127
|
+
| Column | Content |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `key` | The variable key string |
|
|
130
|
+
| `value` | The variable value, JSON-serialised (numbers, booleans, arrays, and objects all round-trip correctly) |
|
|
131
|
+
|
|
132
|
+
> **Tip:** Because the data lives in a real Data Table you can query it with the built-in **n8n Data Table** node, view and edit it in the UI, and use it as a lightweight shared datastore without any external database.
|
|
133
|
+
|
|
134
|
+
> **Note for Docker users:** HTTP calls made by the Variable node originate from inside the container. Use `http://localhost:5678` (or the container's own hostname/service name in Docker Compose) as the base URL — not the external host address.
|
|
109
135
|
|
|
110
136
|
---
|
|
111
137
|
|
|
@@ -214,6 +240,33 @@ Because the database is shared, all workflows see and update the same value.
|
|
|
214
240
|
|
|
215
241
|
---
|
|
216
242
|
|
|
243
|
+
### 6. Cross-workflow shared counter (Data Tables)
|
|
244
|
+
|
|
245
|
+
Same counter as example 5, but stored in n8n Data Tables so you can see and edit the value in the UI.
|
|
246
|
+
|
|
247
|
+
**Prerequisites:** Create an **n8n Variable Node API** credential (see the *Cross-Workflow (Data Tables)* scope section above).
|
|
248
|
+
|
|
249
|
+
**Increment on each webhook hit:**
|
|
250
|
+
- Operation: `Increment Variable`
|
|
251
|
+
- Scope: `Cross-Workflow (Data Tables)`
|
|
252
|
+
- Credential: *(select your n8n Variable Node API credential)*
|
|
253
|
+
- Namespace: `global_stats`
|
|
254
|
+
- Key: `webhook_hits`
|
|
255
|
+
- Amount: `1`
|
|
256
|
+
- Initialize If Missing: `true`
|
|
257
|
+
- Initial Value: `0`
|
|
258
|
+
|
|
259
|
+
**Read the counter from any other workflow:**
|
|
260
|
+
- Operation: `Get Variable`
|
|
261
|
+
- Scope: `Cross-Workflow (Data Tables)`
|
|
262
|
+
- Credential: *(same credential)*
|
|
263
|
+
- Namespace: `global_stats`
|
|
264
|
+
- Key: `webhook_hits`
|
|
265
|
+
|
|
266
|
+
n8n automatically creates a Data Table called `var_global_stats` with `key` and `value` columns. You can inspect or edit the data at any time from the **Data Tables** tab in the n8n sidebar.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
217
270
|
| Mode | Description |
|
|
218
271
|
|---|---|
|
|
219
272
|
| **Preserve Input + Add Result** _(default)_ | Keep all input fields and add a result object at `variable` (or your chosen field name) |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.N8nVariableNodeApi = void 0;
|
|
4
|
+
class N8nVariableNodeApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'n8nVariableNodeApi';
|
|
7
|
+
this.displayName = 'n8n Variable Node API';
|
|
8
|
+
this.documentationUrl = 'https://docs.n8n.io/api/authentication/';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'n8n Instance URL',
|
|
12
|
+
name: 'baseUrl',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: 'http://localhost:5678',
|
|
15
|
+
placeholder: 'https://your-n8n-instance.example.com',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'The base URL of your n8n instance (no trailing slash)',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: 'API Key',
|
|
21
|
+
name: 'apiKey',
|
|
22
|
+
type: 'string',
|
|
23
|
+
typeOptions: { password: true },
|
|
24
|
+
default: '',
|
|
25
|
+
required: true,
|
|
26
|
+
description: 'Your n8n API key. Generate one in Settings → API.',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.N8nVariableNodeApi = N8nVariableNodeApi;
|
|
@@ -5,10 +5,22 @@ const n8n_workflow_1 = require("n8n-workflow");
|
|
|
5
5
|
const valueParser_1 = require("./helpers/valueParser");
|
|
6
6
|
const storage_1 = require("./helpers/storage");
|
|
7
7
|
const dbStorage_1 = require("./helpers/dbStorage");
|
|
8
|
+
const dataTableStorage_1 = require("./helpers/dataTableStorage");
|
|
8
9
|
class Variable {
|
|
9
10
|
constructor() {
|
|
10
11
|
this.description = {
|
|
11
12
|
displayName: 'Variable',
|
|
13
|
+
credentials: [
|
|
14
|
+
{
|
|
15
|
+
name: 'n8nVariableNodeApi',
|
|
16
|
+
required: false,
|
|
17
|
+
displayOptions: {
|
|
18
|
+
show: {
|
|
19
|
+
scope: ['crossWorkflowDataTable'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
12
24
|
name: 'variable',
|
|
13
25
|
icon: 'file:variable.svg',
|
|
14
26
|
group: ['transform'],
|
|
@@ -74,6 +86,11 @@ class Variable {
|
|
|
74
86
|
value: 'crossWorkflow',
|
|
75
87
|
description: 'Variables are stored in a shared local database file and are accessible across ALL workflows on this instance.',
|
|
76
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Cross-Workflow (Data Tables)',
|
|
91
|
+
value: 'crossWorkflowDataTable',
|
|
92
|
+
description: 'Variables are stored in n8n Data Tables (visible in the Data Tables tab). Requires n8n API credentials.',
|
|
93
|
+
},
|
|
77
94
|
],
|
|
78
95
|
default: 'workflowGlobal',
|
|
79
96
|
description: 'Where to store the variable',
|
|
@@ -108,7 +125,7 @@ class Variable {
|
|
|
108
125
|
description: 'Namespace to organize variables. Supports expressions like economy_{{$json.guild.id}}.',
|
|
109
126
|
displayOptions: {
|
|
110
127
|
show: {
|
|
111
|
-
scope: ['workflowGlobal', 'nodeLocal', 'crossWorkflow'],
|
|
128
|
+
scope: ['workflowGlobal', 'nodeLocal', 'crossWorkflow', 'crossWorkflowDataTable'],
|
|
112
129
|
},
|
|
113
130
|
},
|
|
114
131
|
},
|
|
@@ -353,7 +370,7 @@ class Variable {
|
|
|
353
370
|
name: 'includeMetadata',
|
|
354
371
|
type: 'boolean',
|
|
355
372
|
default: false,
|
|
356
|
-
description: 'Whether to store and expose createdAt/updatedAt/type metadata for variables',
|
|
373
|
+
description: 'Whether to store and expose createdAt/updatedAt/type metadata for variables (not supported for Data Tables scope)',
|
|
357
374
|
displayOptions: {
|
|
358
375
|
show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace', 'crossWorkflow'] },
|
|
359
376
|
},
|
|
@@ -398,7 +415,13 @@ async function processItem(ctx, item, i) {
|
|
|
398
415
|
(0, valueParser_1.validateNamespace)(resolvedNamespace);
|
|
399
416
|
// Clone item JSON so we don't mutate the input
|
|
400
417
|
const itemJson = { ...item.json };
|
|
401
|
-
|
|
418
|
+
let result;
|
|
419
|
+
if (scope === 'crossWorkflowDataTable') {
|
|
420
|
+
result = await executeDataTableOperation(ctx, operation, resolvedNamespace, i);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
result = executeOperation(ctx, operation, scope, resolvedNamespace, itemJson, i, includeMetadata);
|
|
424
|
+
}
|
|
402
425
|
return buildOutputItem(ctx, item, itemJson, result, outputMode, i);
|
|
403
426
|
}
|
|
404
427
|
// ─── Namespace resolution ─────────────────────────────────────────────────────
|
|
@@ -502,6 +525,215 @@ function getStaticData(ctx, scope) {
|
|
|
502
525
|
return ctx.getWorkflowStaticData('node');
|
|
503
526
|
return ctx.getWorkflowStaticData('global');
|
|
504
527
|
}
|
|
528
|
+
// ─── Data Table scope async executor ─────────────────────────────────────────
|
|
529
|
+
async function executeDataTableOperation(ctx, operation, namespace, i) {
|
|
530
|
+
const scopeLabel = 'crossWorkflowDataTable';
|
|
531
|
+
switch (operation) {
|
|
532
|
+
case 'set': {
|
|
533
|
+
const key = getKey(ctx, i);
|
|
534
|
+
const valueType = ctx.getNodeParameter('valueType', i, 'auto');
|
|
535
|
+
const rawValue = ctx.getNodeParameter('value', i, '');
|
|
536
|
+
const overwrite = ctx.getNodeParameter('overwriteExisting', i, true);
|
|
537
|
+
if (!overwrite) {
|
|
538
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
539
|
+
if (exists) {
|
|
540
|
+
throw new Error(`Variable "${key}" already exists in namespace "${namespace}". Enable "Overwrite If Exists" to update it.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const parsed = (0, valueParser_1.parseValueByType)(rawValue, valueType);
|
|
544
|
+
const typeName = valueType === 'auto' ? (0, valueParser_1.inferValueType)(parsed) : valueType;
|
|
545
|
+
await (0, dataTableStorage_1.dtSetVariable)(ctx, namespace, key, parsed, typeName, false);
|
|
546
|
+
return { operation: 'set', scope: scopeLabel, namespace, key, value: parsed };
|
|
547
|
+
}
|
|
548
|
+
case 'get': {
|
|
549
|
+
const key = getKey(ctx, i);
|
|
550
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
551
|
+
const useDefault = ctx.getNodeParameter('useDefaultValue', i, false);
|
|
552
|
+
const defaultValue = ctx.getNodeParameter('defaultValue', i, '');
|
|
553
|
+
let value;
|
|
554
|
+
if (exists) {
|
|
555
|
+
const entry = await (0, dataTableStorage_1.dtGetVariable)(ctx, namespace, key);
|
|
556
|
+
value = entry?.value;
|
|
557
|
+
}
|
|
558
|
+
else if (useDefault) {
|
|
559
|
+
value = defaultValue;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
throw new Error(`Variable "${key}" does not exist in namespace "${namespace}". Enable "Use Default Value" or create the variable first.`);
|
|
563
|
+
}
|
|
564
|
+
return { operation: 'get', scope: scopeLabel, namespace, key, value, exists };
|
|
565
|
+
}
|
|
566
|
+
case 'delete': {
|
|
567
|
+
const key = getKey(ctx, i);
|
|
568
|
+
const existed = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
569
|
+
const deleted = await (0, dataTableStorage_1.dtDeleteVariable)(ctx, namespace, key);
|
|
570
|
+
return { operation: 'delete', scope: scopeLabel, namespace, key, exists: existed, deleted };
|
|
571
|
+
}
|
|
572
|
+
case 'has': {
|
|
573
|
+
const key = getKey(ctx, i);
|
|
574
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
575
|
+
return { operation: 'has', scope: scopeLabel, namespace, key, exists };
|
|
576
|
+
}
|
|
577
|
+
case 'list': {
|
|
578
|
+
const includeValues = ctx.getNodeParameter('includeValues', i, true);
|
|
579
|
+
const raw = await (0, dataTableStorage_1.dtListVariables)(ctx, namespace);
|
|
580
|
+
const keys = Object.keys(raw);
|
|
581
|
+
const result = {
|
|
582
|
+
operation: 'list',
|
|
583
|
+
scope: scopeLabel,
|
|
584
|
+
namespace,
|
|
585
|
+
keys,
|
|
586
|
+
count: keys.length,
|
|
587
|
+
};
|
|
588
|
+
if (includeValues) {
|
|
589
|
+
const vars = {};
|
|
590
|
+
for (const [k, entry] of Object.entries(raw)) {
|
|
591
|
+
vars[k] = entry.value;
|
|
592
|
+
}
|
|
593
|
+
result.variables = vars;
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
case 'clear': {
|
|
598
|
+
const confirmation = ctx.getNodeParameter('clearConfirmation', i, '');
|
|
599
|
+
if (confirmation.trim() !== 'CLEAR') {
|
|
600
|
+
throw new Error('Clear cancelled: you must type CLEAR (uppercase) in the Confirmation field to proceed.');
|
|
601
|
+
}
|
|
602
|
+
const count = await (0, dataTableStorage_1.dtClearNamespace)(ctx, namespace);
|
|
603
|
+
return { operation: 'clear', scope: scopeLabel, namespace, count, cleared: true };
|
|
604
|
+
}
|
|
605
|
+
case 'increment':
|
|
606
|
+
case 'decrement': {
|
|
607
|
+
const key = getKey(ctx, i);
|
|
608
|
+
const amount = ctx.getNodeParameter('incrementAmount', i, 1);
|
|
609
|
+
const initIfMissing = ctx.getNodeParameter('initIfMissingNumeric', i, true);
|
|
610
|
+
const initialValue = ctx.getNodeParameter('numericInitialValue', i, 0);
|
|
611
|
+
const direction = operation === 'increment' ? 1 : -1;
|
|
612
|
+
let current;
|
|
613
|
+
const existed = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
614
|
+
if (!existed) {
|
|
615
|
+
if (!initIfMissing) {
|
|
616
|
+
throw new Error(`Variable "${key}" does not exist in namespace "${namespace}". Enable "Initialize If Missing" to create it automatically.`);
|
|
617
|
+
}
|
|
618
|
+
current = initialValue;
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const entry = await (0, dataTableStorage_1.dtGetVariable)(ctx, namespace, key);
|
|
622
|
+
const existing = entry?.value;
|
|
623
|
+
if (typeof existing !== 'number' || !Number.isFinite(existing)) {
|
|
624
|
+
throw new Error(`Cannot ${operation} "${key}": current value is not a finite number (got ${typeof existing}: ${JSON.stringify(existing)}).`);
|
|
625
|
+
}
|
|
626
|
+
current = existing;
|
|
627
|
+
}
|
|
628
|
+
const newValue = current + direction * Math.abs(amount);
|
|
629
|
+
await (0, dataTableStorage_1.dtSetVariable)(ctx, namespace, key, newValue, 'number', false);
|
|
630
|
+
return {
|
|
631
|
+
operation,
|
|
632
|
+
scope: scopeLabel,
|
|
633
|
+
namespace,
|
|
634
|
+
key,
|
|
635
|
+
value: newValue,
|
|
636
|
+
previousValue: existed ? current : undefined,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case 'appendToArray': {
|
|
640
|
+
const key = getKey(ctx, i);
|
|
641
|
+
const valueType = ctx.getNodeParameter('valueType', i, 'auto');
|
|
642
|
+
const rawValue = ctx.getNodeParameter('value', i, '');
|
|
643
|
+
const initIfMissing = ctx.getNodeParameter('initIfMissingArray', i, true);
|
|
644
|
+
let arr;
|
|
645
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
646
|
+
if (!exists) {
|
|
647
|
+
if (!initIfMissing) {
|
|
648
|
+
throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create an empty array automatically.`);
|
|
649
|
+
}
|
|
650
|
+
arr = [];
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const entry = await (0, dataTableStorage_1.dtGetVariable)(ctx, namespace, key);
|
|
654
|
+
const existing = entry?.value;
|
|
655
|
+
if (!Array.isArray(existing)) {
|
|
656
|
+
throw new Error(`Cannot append to "${key}": existing value is not an array (got ${typeof existing}).`);
|
|
657
|
+
}
|
|
658
|
+
arr = [...existing];
|
|
659
|
+
}
|
|
660
|
+
const parsed = (0, valueParser_1.parseValueByType)(rawValue, valueType);
|
|
661
|
+
arr.push(parsed);
|
|
662
|
+
await (0, dataTableStorage_1.dtSetVariable)(ctx, namespace, key, arr, 'array', false);
|
|
663
|
+
return { operation: 'appendToArray', scope: scopeLabel, namespace, key, value: arr };
|
|
664
|
+
}
|
|
665
|
+
case 'mergeObject': {
|
|
666
|
+
const key = getKey(ctx, i);
|
|
667
|
+
const objectJsonRaw = ctx.getNodeParameter('objectJson', i, '{}');
|
|
668
|
+
const useDeepMerge = ctx.getNodeParameter('deepMerge', i, false);
|
|
669
|
+
const initIfMissing = ctx.getNodeParameter('initIfMissingObject', i, true);
|
|
670
|
+
let incoming;
|
|
671
|
+
try {
|
|
672
|
+
const p = typeof objectJsonRaw === 'object' && objectJsonRaw !== null
|
|
673
|
+
? objectJsonRaw
|
|
674
|
+
: JSON.parse(String(objectJsonRaw));
|
|
675
|
+
if (typeof p !== 'object' || p === null || Array.isArray(p))
|
|
676
|
+
throw new Error('not a plain object');
|
|
677
|
+
incoming = p;
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
throw new Error(`Object (JSON) must be a plain object. Got: ${String(objectJsonRaw).slice(0, 100)}`);
|
|
681
|
+
}
|
|
682
|
+
let base;
|
|
683
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
684
|
+
if (!exists) {
|
|
685
|
+
if (!initIfMissing) {
|
|
686
|
+
throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create an empty object automatically.`);
|
|
687
|
+
}
|
|
688
|
+
base = {};
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const entry = await (0, dataTableStorage_1.dtGetVariable)(ctx, namespace, key);
|
|
692
|
+
const existing = entry?.value;
|
|
693
|
+
if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
|
|
694
|
+
throw new Error(`Cannot merge into "${key}": existing value is not a plain object (got ${Array.isArray(existing) ? 'array' : typeof existing}).`);
|
|
695
|
+
}
|
|
696
|
+
base = { ...existing };
|
|
697
|
+
}
|
|
698
|
+
const merged = useDeepMerge ? (0, valueParser_1.deepMerge)(base, incoming) : { ...base, ...incoming };
|
|
699
|
+
await (0, dataTableStorage_1.dtSetVariable)(ctx, namespace, key, merged, 'object', false);
|
|
700
|
+
return { operation: 'mergeObject', scope: scopeLabel, namespace, key, value: merged };
|
|
701
|
+
}
|
|
702
|
+
case 'toggleBoolean': {
|
|
703
|
+
const key = getKey(ctx, i);
|
|
704
|
+
const initIfMissing = ctx.getNodeParameter('initIfMissingBoolean', i, true);
|
|
705
|
+
const initialValue = ctx.getNodeParameter('booleanInitialValue', i, false);
|
|
706
|
+
let current;
|
|
707
|
+
const exists = await (0, dataTableStorage_1.dtHasVariable)(ctx, namespace, key);
|
|
708
|
+
if (!exists) {
|
|
709
|
+
if (!initIfMissing) {
|
|
710
|
+
throw new Error(`Variable "${key}" does not exist. Enable "Initialize If Missing" to create it automatically.`);
|
|
711
|
+
}
|
|
712
|
+
current = initialValue;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
const entry = await (0, dataTableStorage_1.dtGetVariable)(ctx, namespace, key);
|
|
716
|
+
const existing = entry?.value;
|
|
717
|
+
if (typeof existing !== 'boolean') {
|
|
718
|
+
throw new Error(`Cannot toggle "${key}": existing value is not a boolean (got ${typeof existing}: ${JSON.stringify(existing)}).`);
|
|
719
|
+
}
|
|
720
|
+
current = existing;
|
|
721
|
+
}
|
|
722
|
+
const newValue = !current;
|
|
723
|
+
await (0, dataTableStorage_1.dtSetVariable)(ctx, namespace, key, newValue, 'boolean', false);
|
|
724
|
+
return {
|
|
725
|
+
operation: 'toggleBoolean',
|
|
726
|
+
scope: scopeLabel,
|
|
727
|
+
namespace,
|
|
728
|
+
key,
|
|
729
|
+
value: newValue,
|
|
730
|
+
previousValue: current,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
default:
|
|
734
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
505
737
|
// ─── Individual operations ────────────────────────────────────────────────────
|
|
506
738
|
function opSet(ctx, i, namespace, scopeLabel, get, set, has) {
|
|
507
739
|
const key = getKey(ctx, i);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IExecuteFunctions } from 'n8n-workflow';
|
|
2
|
+
import type { StoredVariableEntry } from './types';
|
|
3
|
+
export declare function dtGetVariable(ctx: IExecuteFunctions, namespace: string, key: string): Promise<StoredVariableEntry | undefined>;
|
|
4
|
+
export declare function dtSetVariable(ctx: IExecuteFunctions, namespace: string, key: string, value: unknown, _typeName: string, _includeMetadata: boolean): Promise<void>;
|
|
5
|
+
export declare function dtDeleteVariable(ctx: IExecuteFunctions, namespace: string, key: string): Promise<boolean>;
|
|
6
|
+
export declare function dtHasVariable(ctx: IExecuteFunctions, namespace: string, key: string): Promise<boolean>;
|
|
7
|
+
export declare function dtListVariables(ctx: IExecuteFunctions, namespace: string): Promise<Record<string, StoredVariableEntry>>;
|
|
8
|
+
export declare function dtClearNamespace(ctx: IExecuteFunctions, namespace: string): Promise<number>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dtGetVariable = dtGetVariable;
|
|
4
|
+
exports.dtSetVariable = dtSetVariable;
|
|
5
|
+
exports.dtDeleteVariable = dtDeleteVariable;
|
|
6
|
+
exports.dtHasVariable = dtHasVariable;
|
|
7
|
+
exports.dtListVariables = dtListVariables;
|
|
8
|
+
exports.dtClearNamespace = dtClearNamespace;
|
|
9
|
+
// In-process cache: namespace -> tableId
|
|
10
|
+
const tableIdCache = new Map();
|
|
11
|
+
function makeTableName(namespace) {
|
|
12
|
+
// Sanitize namespace to be URL-safe for table names
|
|
13
|
+
return `var_${namespace.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
14
|
+
}
|
|
15
|
+
function makeKeyFilter(key) {
|
|
16
|
+
return {
|
|
17
|
+
type: 'and',
|
|
18
|
+
filters: [{ columnName: 'key', condition: 'eq', value: key }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function apiRequest(ctx, method, path, body, qs) {
|
|
22
|
+
const creds = await ctx.getCredentials('n8nVariableNodeApi');
|
|
23
|
+
const baseUrl = String(creds.baseUrl).replace(/\/+$/, '');
|
|
24
|
+
const apiKey = String(creds.apiKey);
|
|
25
|
+
const options = {
|
|
26
|
+
method,
|
|
27
|
+
url: `${baseUrl}/api/v1${path}`,
|
|
28
|
+
headers: {
|
|
29
|
+
'X-N8N-API-KEY': apiKey,
|
|
30
|
+
},
|
|
31
|
+
json: true,
|
|
32
|
+
};
|
|
33
|
+
if (qs !== undefined) {
|
|
34
|
+
options.qs = qs;
|
|
35
|
+
}
|
|
36
|
+
if (body !== undefined) {
|
|
37
|
+
options.body = body;
|
|
38
|
+
}
|
|
39
|
+
return ctx.helpers.httpRequest(options);
|
|
40
|
+
}
|
|
41
|
+
async function findTableId(ctx, namespace) {
|
|
42
|
+
const tName = makeTableName(namespace);
|
|
43
|
+
const resp = await apiRequest(ctx, 'GET', '/data-tables', undefined, { limit: 250 });
|
|
44
|
+
const tables = resp.data ?? [];
|
|
45
|
+
return tables.find((t) => t.name === tName)?.id;
|
|
46
|
+
}
|
|
47
|
+
async function getOrCreateTable(ctx, namespace, forceRefresh = false) {
|
|
48
|
+
if (!forceRefresh && tableIdCache.has(namespace)) {
|
|
49
|
+
return tableIdCache.get(namespace);
|
|
50
|
+
}
|
|
51
|
+
const existing = await findTableId(ctx, namespace);
|
|
52
|
+
if (existing) {
|
|
53
|
+
tableIdCache.set(namespace, existing);
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
// Create the table with key + value columns
|
|
57
|
+
const created = await apiRequest(ctx, 'POST', '/data-tables', {
|
|
58
|
+
name: makeTableName(namespace),
|
|
59
|
+
columns: [
|
|
60
|
+
{ name: 'key', type: 'string' },
|
|
61
|
+
{ name: 'value', type: 'string' },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const newId = created.id;
|
|
65
|
+
tableIdCache.set(namespace, newId);
|
|
66
|
+
return newId;
|
|
67
|
+
}
|
|
68
|
+
async function dtGetVariable(ctx, namespace, key) {
|
|
69
|
+
const tableId = await getOrCreateTable(ctx, namespace);
|
|
70
|
+
const resp = await apiRequest(ctx, 'GET', `/data-tables/${tableId}/rows`, undefined, {
|
|
71
|
+
filter: JSON.stringify(makeKeyFilter(key)),
|
|
72
|
+
limit: 1,
|
|
73
|
+
});
|
|
74
|
+
const rows = resp.data ?? [];
|
|
75
|
+
if (rows.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
const row = rows[0];
|
|
78
|
+
try {
|
|
79
|
+
return { value: JSON.parse(String(row['value'])) };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { value: row['value'] };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function dtSetVariable(ctx, namespace, key, value, _typeName, _includeMetadata) {
|
|
86
|
+
const tableId = await getOrCreateTable(ctx, namespace);
|
|
87
|
+
const serialized = JSON.stringify(value);
|
|
88
|
+
// Try update first; if nothing was updated, insert
|
|
89
|
+
const updateResult = await apiRequest(ctx, 'PATCH', `/data-tables/${tableId}/rows/update`, {
|
|
90
|
+
filter: makeKeyFilter(key),
|
|
91
|
+
data: { value: serialized },
|
|
92
|
+
returnData: true,
|
|
93
|
+
dryRun: false,
|
|
94
|
+
});
|
|
95
|
+
const updatedRows = Array.isArray(updateResult) ? updateResult : [];
|
|
96
|
+
if (updatedRows.length === 0) {
|
|
97
|
+
await apiRequest(ctx, 'POST', `/data-tables/${tableId}/rows`, {
|
|
98
|
+
data: [{ key, value: serialized }],
|
|
99
|
+
returnType: 'count',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function dtDeleteVariable(ctx, namespace, key) {
|
|
104
|
+
const exists = await dtHasVariable(ctx, namespace, key);
|
|
105
|
+
if (!exists)
|
|
106
|
+
return false;
|
|
107
|
+
const tableId = await getOrCreateTable(ctx, namespace);
|
|
108
|
+
await apiRequest(ctx, 'DELETE', `/data-tables/${tableId}/rows/delete`, {
|
|
109
|
+
filter: makeKeyFilter(key),
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
async function dtHasVariable(ctx, namespace, key) {
|
|
114
|
+
const tableId = await getOrCreateTable(ctx, namespace);
|
|
115
|
+
const resp = await apiRequest(ctx, 'GET', `/data-tables/${tableId}/rows`, undefined, {
|
|
116
|
+
filter: JSON.stringify(makeKeyFilter(key)),
|
|
117
|
+
limit: 1,
|
|
118
|
+
});
|
|
119
|
+
const rows = resp.data ?? [];
|
|
120
|
+
return rows.length > 0;
|
|
121
|
+
}
|
|
122
|
+
async function dtListVariables(ctx, namespace) {
|
|
123
|
+
const tableId = await getOrCreateTable(ctx, namespace);
|
|
124
|
+
const allRows = [];
|
|
125
|
+
let cursor;
|
|
126
|
+
do {
|
|
127
|
+
const qs = { limit: 250 };
|
|
128
|
+
if (cursor)
|
|
129
|
+
qs['cursor'] = cursor;
|
|
130
|
+
const resp = await apiRequest(ctx, 'GET', `/data-tables/${tableId}/rows`, undefined, qs);
|
|
131
|
+
const typed = resp;
|
|
132
|
+
allRows.push(...(typed.data ?? []));
|
|
133
|
+
cursor = typed.nextCursor;
|
|
134
|
+
} while (cursor);
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const row of allRows) {
|
|
137
|
+
const k = String(row['key'] ?? '');
|
|
138
|
+
if (!k)
|
|
139
|
+
continue;
|
|
140
|
+
try {
|
|
141
|
+
result[k] = { value: JSON.parse(String(row['value'])) };
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
result[k] = { value: row['value'] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
async function dtClearNamespace(ctx, namespace) {
|
|
150
|
+
const tableId = await findTableId(ctx, namespace);
|
|
151
|
+
if (!tableId)
|
|
152
|
+
return 0;
|
|
153
|
+
// Count rows before deleting
|
|
154
|
+
const vars = await dtListVariables(ctx, namespace);
|
|
155
|
+
const count = Object.keys(vars).length;
|
|
156
|
+
if (count > 0) {
|
|
157
|
+
// Delete the entire table; it will be recreated on next use
|
|
158
|
+
await apiRequest(ctx, 'DELETE', `/data-tables/${tableId}`);
|
|
159
|
+
tableIdCache.delete(namespace);
|
|
160
|
+
}
|
|
161
|
+
return count;
|
|
162
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cross-workflow variable storage backed by a local
|
|
2
|
+
* Cross-workflow variable storage backed by a local JSON file.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.
|
|
4
|
+
* The file is auto-created at:
|
|
5
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
6
6
|
*
|
|
7
|
-
* No configuration is required
|
|
8
|
-
*
|
|
7
|
+
* No configuration is required. Writes are performed atomically (write to a
|
|
8
|
+
* temporary file, then rename) to prevent data corruption on unexpected
|
|
9
|
+
* process exit. No native dependencies are needed.
|
|
9
10
|
*/
|
|
10
11
|
import type { StoredVariableEntry } from './types';
|
|
11
12
|
export declare function dbGetVariable(namespace: string, key: string): StoredVariableEntry | undefined;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Cross-workflow variable storage backed by a local
|
|
3
|
+
* Cross-workflow variable storage backed by a local JSON file.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.
|
|
5
|
+
* The file is auto-created at:
|
|
6
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
7
7
|
*
|
|
8
|
-
* No configuration is required
|
|
9
|
-
*
|
|
8
|
+
* No configuration is required. Writes are performed atomically (write to a
|
|
9
|
+
* temporary file, then rename) to prevent data corruption on unexpected
|
|
10
|
+
* process exit. No native dependencies are needed.
|
|
10
11
|
*/
|
|
11
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -18,12 +19,11 @@ exports.dbDeleteVariable = dbDeleteVariable;
|
|
|
18
19
|
exports.dbHasVariable = dbHasVariable;
|
|
19
20
|
exports.dbListVariables = dbListVariables;
|
|
20
21
|
exports.dbClearNamespace = dbClearNamespace;
|
|
21
|
-
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
22
22
|
const fs_1 = __importDefault(require("fs"));
|
|
23
23
|
const os_1 = __importDefault(require("os"));
|
|
24
24
|
const path_1 = __importDefault(require("path"));
|
|
25
|
-
const
|
|
26
|
-
function
|
|
25
|
+
const DATA_FILENAME = 'n8n-nodes-variable-data.json';
|
|
26
|
+
function getDataPath() {
|
|
27
27
|
const userFolder = process.env['N8N_USER_FOLDER'] ?? path_1.default.join(os_1.default.homedir(), '.n8n');
|
|
28
28
|
try {
|
|
29
29
|
if (!fs_1.default.existsSync(userFolder)) {
|
|
@@ -31,107 +31,98 @@ function getDbPath() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
catch {
|
|
34
|
-
// If
|
|
35
|
-
return path_1.default.join(os_1.default.tmpdir(),
|
|
34
|
+
// If the n8n user folder can't be created, fall back to the OS temp dir
|
|
35
|
+
return path_1.default.join(os_1.default.tmpdir(), DATA_FILENAME);
|
|
36
36
|
}
|
|
37
|
-
return path_1.default.join(userFolder,
|
|
37
|
+
return path_1.default.join(userFolder, DATA_FILENAME);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
PRIMARY KEY (namespace, key)
|
|
59
|
-
)
|
|
60
|
-
`);
|
|
61
|
-
return _db;
|
|
39
|
+
function readData() {
|
|
40
|
+
const dataPath = getDataPath();
|
|
41
|
+
try {
|
|
42
|
+
const content = fs_1.default.readFileSync(dataPath, 'utf-8');
|
|
43
|
+
const parsed = JSON.parse(content);
|
|
44
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File doesn't exist yet or is corrupt — start fresh
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
function writeData(data) {
|
|
54
|
+
const dataPath = getDataPath();
|
|
55
|
+
const tmpPath = `${dataPath}.tmp`;
|
|
56
|
+
fs_1.default.writeFileSync(tmpPath, JSON.stringify(data), 'utf-8');
|
|
57
|
+
fs_1.default.renameSync(tmpPath, dataPath);
|
|
62
58
|
}
|
|
63
59
|
// ─── CRUD operations ─────────────────────────────────────────────────────────
|
|
64
60
|
function dbGetVariable(namespace, key) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
.get(namespace, key);
|
|
69
|
-
if (!row)
|
|
61
|
+
const data = readData();
|
|
62
|
+
const entry = data[namespace]?.[key];
|
|
63
|
+
if (!entry)
|
|
70
64
|
return undefined;
|
|
71
65
|
return {
|
|
72
|
-
value:
|
|
73
|
-
type:
|
|
74
|
-
createdAt:
|
|
75
|
-
updatedAt:
|
|
66
|
+
value: entry.value,
|
|
67
|
+
type: entry.type,
|
|
68
|
+
createdAt: entry.created_at,
|
|
69
|
+
updatedAt: entry.updated_at,
|
|
76
70
|
};
|
|
77
71
|
}
|
|
78
72
|
function dbSetVariable(namespace, key, value, typeName, includeMetadata) {
|
|
79
|
-
const
|
|
73
|
+
const data = readData();
|
|
74
|
+
if (!data[namespace]) {
|
|
75
|
+
data[namespace] = {};
|
|
76
|
+
}
|
|
80
77
|
const now = new Date().toISOString();
|
|
81
|
-
const serialized = JSON.stringify(value);
|
|
82
78
|
if (includeMetadata) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
const existing = data[namespace][key];
|
|
80
|
+
data[namespace][key] = {
|
|
81
|
+
value,
|
|
82
|
+
type: typeName,
|
|
83
|
+
// Preserve original created_at on updates
|
|
84
|
+
created_at: existing?.created_at ?? now,
|
|
85
|
+
updated_at: now,
|
|
86
|
+
};
|
|
90
87
|
}
|
|
91
88
|
else {
|
|
92
|
-
|
|
93
|
-
VALUES (?, ?, ?, NULL, NULL, NULL)
|
|
94
|
-
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
95
|
-
value = excluded.value,
|
|
96
|
-
type = NULL,
|
|
97
|
-
created_at = NULL,
|
|
98
|
-
updated_at = NULL`).run(namespace, key, serialized);
|
|
89
|
+
data[namespace][key] = { value };
|
|
99
90
|
}
|
|
91
|
+
writeData(data);
|
|
100
92
|
}
|
|
101
93
|
function dbDeleteVariable(namespace, key) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
const data = readData();
|
|
95
|
+
if (!data[namespace] || !(key in data[namespace])) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
delete data[namespace][key];
|
|
99
|
+
writeData(data);
|
|
100
|
+
return true;
|
|
107
101
|
}
|
|
108
102
|
function dbHasVariable(namespace, key) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
.prepare('SELECT 1 FROM variables WHERE namespace = ? AND key = ? LIMIT 1')
|
|
112
|
-
.get(namespace, key);
|
|
113
|
-
return row !== undefined;
|
|
103
|
+
const data = readData();
|
|
104
|
+
return Object.prototype.hasOwnProperty.call(data[namespace] ?? {}, key);
|
|
114
105
|
}
|
|
115
106
|
function dbListVariables(namespace) {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
.prepare('SELECT key, value, type, created_at, updated_at FROM variables WHERE namespace = ? ORDER BY key')
|
|
119
|
-
.all(namespace);
|
|
107
|
+
const data = readData();
|
|
108
|
+
const ns = data[namespace] ?? {};
|
|
120
109
|
const result = {};
|
|
121
|
-
for (const
|
|
122
|
-
result[
|
|
123
|
-
value:
|
|
124
|
-
type:
|
|
125
|
-
createdAt:
|
|
126
|
-
updatedAt:
|
|
110
|
+
for (const [k, entry] of Object.entries(ns)) {
|
|
111
|
+
result[k] = {
|
|
112
|
+
value: entry.value,
|
|
113
|
+
type: entry.type,
|
|
114
|
+
createdAt: entry.created_at,
|
|
115
|
+
updatedAt: entry.updated_at,
|
|
127
116
|
};
|
|
128
117
|
}
|
|
129
118
|
return result;
|
|
130
119
|
}
|
|
131
120
|
function dbClearNamespace(namespace) {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
const data = readData();
|
|
122
|
+
const count = Object.keys(data[namespace] ?? {}).length;
|
|
123
|
+
if (count > 0) {
|
|
124
|
+
delete data[namespace];
|
|
125
|
+
writeData(data);
|
|
126
|
+
}
|
|
127
|
+
return count;
|
|
137
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace' | 'crossWorkflow';
|
|
1
|
+
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace' | 'crossWorkflow' | 'crossWorkflowDataTable';
|
|
2
2
|
export type VariableOperation = 'set' | 'get' | 'delete' | 'has' | 'list' | 'clear' | 'increment' | 'decrement' | 'appendToArray' | 'mergeObject' | 'toggleBoolean';
|
|
3
3
|
export type ValueType = 'string' | 'number' | 'boolean' | 'json' | 'array' | 'object' | 'auto';
|
|
4
4
|
export type OutputMode = 'preserveAndAdd' | 'resultOnly' | 'addField';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-variable",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local and global scoped variables for n8n workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
],
|
|
33
33
|
"n8n": {
|
|
34
34
|
"n8nNodesApiVersion": 1,
|
|
35
|
-
"credentials": [
|
|
35
|
+
"credentials": [
|
|
36
|
+
"dist/credentials/N8nVariableNodeApi.credentials.js"
|
|
37
|
+
],
|
|
36
38
|
"nodes": [
|
|
37
39
|
"dist/nodes/Variable/Variable.node.js"
|
|
38
40
|
]
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
|
-
"@types/better-sqlite3": "^7.6.11",
|
|
42
43
|
"@types/jest": "^29.5.12",
|
|
43
44
|
"@types/node": "^18.19.50",
|
|
44
45
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
@@ -55,9 +56,6 @@
|
|
|
55
56
|
"peerDependencies": {
|
|
56
57
|
"n8n-workflow": "*"
|
|
57
58
|
},
|
|
58
|
-
"dependencies": {
|
|
59
|
-
"better-sqlite3": "^9.4.0"
|
|
60
|
-
},
|
|
61
59
|
"jest": {
|
|
62
60
|
"preset": "ts-jest",
|
|
63
61
|
"testEnvironment": "node",
|