n8n-nodes-variable 1.0.9 → 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 CHANGED
@@ -13,6 +13,7 @@ The **Variable** node lets you store, retrieve, update, and delete named variabl
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
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
 
@@ -107,6 +108,31 @@ The file is **created automatically on first use** — no setup or dependencies
107
108
 
108
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.
109
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.
135
+
110
136
  ---
111
137
 
112
138
  ## Examples
@@ -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,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class N8nVariableNodeApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -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
- const result = executeOperation(ctx, operation, scope, resolvedNamespace, itemJson, i, includeMetadata);
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,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.9",
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,7 +32,9 @@
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
  ]