n8n-nodes-variable 1.0.6 → 1.0.8
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
|
@@ -12,6 +12,7 @@ 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 SQLite database and shared across **all** workflows on this n8n instance
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -94,6 +95,18 @@ cooldowns → command cooldown per guild+user
|
|
|
94
95
|
guild_{{$json.guild.id}} → per-guild settings
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
### Cross-Workflow (Shared)
|
|
99
|
+
|
|
100
|
+
Variables are stored in a SQLite database file on the n8n host at:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The database and table are **created automatically on first use** — no setup required. Data survives instance restarts and is accessible from any workflow on the same n8n instance. This is ideal for cross-workflow counters, shared feature flags, or any state that multiple workflows need to read and write.
|
|
107
|
+
|
|
108
|
+
> **Note:** the database 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
|
+
|
|
97
110
|
---
|
|
98
111
|
|
|
99
112
|
## Examples
|
|
@@ -178,7 +191,28 @@ The variable `stats.runCount` persists between executions and increments each ti
|
|
|
178
191
|
|
|
179
192
|
---
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
### 5. Cross-workflow shared counter
|
|
195
|
+
|
|
196
|
+
Count total webhook hits across every workflow on the instance:
|
|
197
|
+
|
|
198
|
+
**In each workflow that receives a webhook — Increment shared counter:**
|
|
199
|
+
- Operation: `Increment Variable`
|
|
200
|
+
- Scope: `Cross-Workflow (Shared)`
|
|
201
|
+
- Namespace: `global_stats`
|
|
202
|
+
- Key: `webhook_hits`
|
|
203
|
+
- Amount: `1`
|
|
204
|
+
- Initialize If Missing: `true`
|
|
205
|
+
- Initial Value: `0`
|
|
206
|
+
|
|
207
|
+
**In a reporting workflow — Read the counter:**
|
|
208
|
+
- Operation: `Get Variable`
|
|
209
|
+
- Scope: `Cross-Workflow (Shared)`
|
|
210
|
+
- Namespace: `global_stats`
|
|
211
|
+
- Key: `webhook_hits`
|
|
212
|
+
|
|
213
|
+
Because the database is shared, all workflows see and update the same value.
|
|
214
|
+
|
|
215
|
+
---
|
|
182
216
|
|
|
183
217
|
| Mode | Description |
|
|
184
218
|
|---|---|
|
|
@@ -209,11 +243,9 @@ Workflow Global and Node Local variables use n8n's static data, which is:
|
|
|
209
243
|
|
|
210
244
|
- **Suitable for:** counters, feature flags, small state objects, per-user values in low-traffic bots
|
|
211
245
|
- **Not suitable for:** high-concurrency write operations (e.g., simultaneously updating the same counter from hundreds of parallel executions)
|
|
212
|
-
- **Not cross-workflow:** variables are scoped to the workflow they belong to
|
|
213
|
-
|
|
214
|
-
For high-volume or high-concurrency state, consider using a database node (Redis, Postgres, MongoDB) instead.
|
|
246
|
+
- **Not cross-workflow:** Workflow Global and Node Local variables are scoped to the workflow they belong to. Use the **Cross-Workflow (Shared)** scope to share state across workflows.
|
|
215
247
|
|
|
216
|
-
|
|
248
|
+
For high-volume or high-concurrency state, consider using a dedicated database node (Redis, Postgres, MongoDB) instead.
|
|
217
249
|
|
|
218
250
|
---
|
|
219
251
|
|
|
@@ -4,6 +4,7 @@ exports.Variable = void 0;
|
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
5
|
const valueParser_1 = require("./helpers/valueParser");
|
|
6
6
|
const storage_1 = require("./helpers/storage");
|
|
7
|
+
const dbStorage_1 = require("./helpers/dbStorage");
|
|
7
8
|
class Variable {
|
|
8
9
|
constructor() {
|
|
9
10
|
this.description = {
|
|
@@ -68,6 +69,11 @@ class Variable {
|
|
|
68
69
|
value: 'customNamespace',
|
|
69
70
|
description: 'Workflow global storage with a custom namespace expression.',
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Cross-Workflow (Shared)',
|
|
74
|
+
value: 'crossWorkflow',
|
|
75
|
+
description: 'Variables are stored in a shared local database file and are accessible across ALL workflows on this instance.',
|
|
76
|
+
},
|
|
71
77
|
],
|
|
72
78
|
default: 'workflowGlobal',
|
|
73
79
|
description: 'Where to store the variable',
|
|
@@ -102,7 +108,7 @@ class Variable {
|
|
|
102
108
|
description: 'Namespace to organize variables. Supports expressions like economy_{{$json.guild.id}}.',
|
|
103
109
|
displayOptions: {
|
|
104
110
|
show: {
|
|
105
|
-
scope: ['workflowGlobal', 'nodeLocal'],
|
|
111
|
+
scope: ['workflowGlobal', 'nodeLocal', 'crossWorkflow'],
|
|
106
112
|
},
|
|
107
113
|
},
|
|
108
114
|
},
|
|
@@ -347,9 +353,9 @@ class Variable {
|
|
|
347
353
|
name: 'includeMetadata',
|
|
348
354
|
type: 'boolean',
|
|
349
355
|
default: false,
|
|
350
|
-
description: 'Whether to store and expose createdAt/updatedAt/type metadata for
|
|
356
|
+
description: 'Whether to store and expose createdAt/updatedAt/type metadata for variables',
|
|
351
357
|
displayOptions: {
|
|
352
|
-
show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace'] },
|
|
358
|
+
show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace', 'crossWorkflow'] },
|
|
353
359
|
},
|
|
354
360
|
},
|
|
355
361
|
],
|
|
@@ -410,6 +416,7 @@ function resolveNamespace(ctx, scope, i) {
|
|
|
410
416
|
// ─── Operation dispatcher ─────────────────────────────────────────────────────
|
|
411
417
|
function executeOperation(ctx, operation, scope, namespace, itemJson, i, includeMetadata) {
|
|
412
418
|
const isLocal = scope === 'localExecution';
|
|
419
|
+
const isDb = scope === 'crossWorkflow';
|
|
413
420
|
const storagePath = isLocal
|
|
414
421
|
? ctx.getNodeParameter('localStoragePath', i, '_variables')
|
|
415
422
|
: '';
|
|
@@ -417,6 +424,8 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
417
424
|
const get = (key) => {
|
|
418
425
|
if (isLocal)
|
|
419
426
|
return (0, storage_1.localGetVariable)(itemJson, storagePath, namespace, key);
|
|
427
|
+
if (isDb)
|
|
428
|
+
return (0, dbStorage_1.dbGetVariable)(namespace, key)?.value;
|
|
420
429
|
const entry = (0, storage_1.staticGetVariable)(getStaticData(ctx, scope), namespace, key);
|
|
421
430
|
return entry?.value;
|
|
422
431
|
};
|
|
@@ -424,6 +433,9 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
424
433
|
if (isLocal) {
|
|
425
434
|
(0, storage_1.localSetVariable)(itemJson, storagePath, namespace, key, value);
|
|
426
435
|
}
|
|
436
|
+
else if (isDb) {
|
|
437
|
+
(0, dbStorage_1.dbSetVariable)(namespace, key, value, typeName, includeMetadata);
|
|
438
|
+
}
|
|
427
439
|
else {
|
|
428
440
|
(0, storage_1.staticSetVariable)(getStaticData(ctx, scope), namespace, key, value, typeName, includeMetadata);
|
|
429
441
|
}
|
|
@@ -431,16 +443,28 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
431
443
|
const del = (key) => {
|
|
432
444
|
if (isLocal)
|
|
433
445
|
return (0, storage_1.localDeleteVariable)(itemJson, storagePath, namespace, key);
|
|
446
|
+
if (isDb)
|
|
447
|
+
return (0, dbStorage_1.dbDeleteVariable)(namespace, key);
|
|
434
448
|
return (0, storage_1.staticDeleteVariable)(getStaticData(ctx, scope), namespace, key);
|
|
435
449
|
};
|
|
436
450
|
const has = (key) => {
|
|
437
451
|
if (isLocal)
|
|
438
452
|
return (0, storage_1.localHasVariable)(itemJson, storagePath, namespace, key);
|
|
453
|
+
if (isDb)
|
|
454
|
+
return (0, dbStorage_1.dbHasVariable)(namespace, key);
|
|
439
455
|
return (0, storage_1.staticHasVariable)(getStaticData(ctx, scope), namespace, key);
|
|
440
456
|
};
|
|
441
457
|
const list = () => {
|
|
442
458
|
if (isLocal)
|
|
443
459
|
return (0, storage_1.localListVariables)(itemJson, storagePath, namespace);
|
|
460
|
+
if (isDb) {
|
|
461
|
+
const raw = (0, dbStorage_1.dbListVariables)(namespace);
|
|
462
|
+
const result = {};
|
|
463
|
+
for (const [k, entry] of Object.entries(raw)) {
|
|
464
|
+
result[k] = entry.value;
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
444
468
|
const raw = (0, storage_1.staticListVariables)(getStaticData(ctx, scope), namespace);
|
|
445
469
|
// unwrap StoredVariableEntry to plain values
|
|
446
470
|
const result = {};
|
|
@@ -452,6 +476,8 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
452
476
|
const clear = () => {
|
|
453
477
|
if (isLocal)
|
|
454
478
|
return (0, storage_1.localClearNamespace)(itemJson, storagePath, namespace);
|
|
479
|
+
if (isDb)
|
|
480
|
+
return (0, dbStorage_1.dbClearNamespace)(namespace);
|
|
455
481
|
return (0, storage_1.staticClearNamespace)(getStaticData(ctx, scope), namespace);
|
|
456
482
|
};
|
|
457
483
|
const scopeLabel = scope;
|
|
@@ -581,7 +607,8 @@ function opIncrement(ctx, i, namespace, scopeLabel, get, set, has, direction) {
|
|
|
581
607
|
const initialValue = ctx.getNodeParameter('numericInitialValue', i, 0);
|
|
582
608
|
const opName = direction === 1 ? 'increment' : 'decrement';
|
|
583
609
|
let current;
|
|
584
|
-
|
|
610
|
+
const existed = has(key);
|
|
611
|
+
if (!existed) {
|
|
585
612
|
if (!initIfMissing) {
|
|
586
613
|
throw new Error(`Variable "${key}" does not exist in namespace "${namespace}". Enable "Initialize If Missing" to create it automatically.`);
|
|
587
614
|
}
|
|
@@ -602,7 +629,7 @@ function opIncrement(ctx, i, namespace, scopeLabel, get, set, has, direction) {
|
|
|
602
629
|
namespace,
|
|
603
630
|
key,
|
|
604
631
|
value: newValue,
|
|
605
|
-
previousValue:
|
|
632
|
+
previousValue: existed ? current : undefined,
|
|
606
633
|
};
|
|
607
634
|
}
|
|
608
635
|
function opAppendToArray(ctx, i, namespace, scopeLabel, get, set, has) {
|
|
@@ -717,7 +744,10 @@ function buildOutputItem(ctx, originalItem, updatedItemJson, result, outputMode,
|
|
|
717
744
|
};
|
|
718
745
|
}
|
|
719
746
|
if (outputMode === 'addField') {
|
|
720
|
-
|
|
747
|
+
// For Get Variable, use the dedicated output field name; otherwise use addFieldName
|
|
748
|
+
const fieldName = result.operation === 'get'
|
|
749
|
+
? ctx.getNodeParameter('getOutputFieldName', i, 'value')
|
|
750
|
+
: ctx.getNodeParameter('addFieldName', i, 'value');
|
|
721
751
|
return {
|
|
722
752
|
json: {
|
|
723
753
|
...updatedItemJson,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-workflow variable storage backed by a local SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* The database is auto-created at:
|
|
5
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
|
|
6
|
+
*
|
|
7
|
+
* No configuration is required — the database file and table are created
|
|
8
|
+
* automatically on first use and shared across all workflows on the instance.
|
|
9
|
+
*/
|
|
10
|
+
import type { StoredVariableEntry } from './types';
|
|
11
|
+
export declare function dbGetVariable(namespace: string, key: string): StoredVariableEntry | undefined;
|
|
12
|
+
export declare function dbSetVariable(namespace: string, key: string, value: unknown, typeName: string, includeMetadata: boolean): void;
|
|
13
|
+
export declare function dbDeleteVariable(namespace: string, key: string): boolean;
|
|
14
|
+
export declare function dbHasVariable(namespace: string, key: string): boolean;
|
|
15
|
+
export declare function dbListVariables(namespace: string): Record<string, StoredVariableEntry>;
|
|
16
|
+
export declare function dbClearNamespace(namespace: string): number;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-workflow variable storage backed by a local SQLite database.
|
|
4
|
+
*
|
|
5
|
+
* The database is auto-created at:
|
|
6
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
|
|
7
|
+
*
|
|
8
|
+
* No configuration is required — the database file and table are created
|
|
9
|
+
* automatically on first use and shared across all workflows on the instance.
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.dbGetVariable = dbGetVariable;
|
|
16
|
+
exports.dbSetVariable = dbSetVariable;
|
|
17
|
+
exports.dbDeleteVariable = dbDeleteVariable;
|
|
18
|
+
exports.dbHasVariable = dbHasVariable;
|
|
19
|
+
exports.dbListVariables = dbListVariables;
|
|
20
|
+
exports.dbClearNamespace = dbClearNamespace;
|
|
21
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const DB_FILENAME = 'n8n-nodes-variable.db';
|
|
26
|
+
function getDbPath() {
|
|
27
|
+
const userFolder = process.env['N8N_USER_FOLDER'] ?? path_1.default.join(os_1.default.homedir(), '.n8n');
|
|
28
|
+
try {
|
|
29
|
+
if (!fs_1.default.existsSync(userFolder)) {
|
|
30
|
+
fs_1.default.mkdirSync(userFolder, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// If we can't create the n8n folder, fall back to the OS temp directory
|
|
35
|
+
return path_1.default.join(os_1.default.tmpdir(), DB_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
return path_1.default.join(userFolder, DB_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
// Singleton connection — reused across all node executions in the same process
|
|
40
|
+
let _db = null;
|
|
41
|
+
function getDb() {
|
|
42
|
+
if (_db)
|
|
43
|
+
return _db;
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
_db = new better_sqlite3_1.default(dbPath);
|
|
46
|
+
// WAL journal mode allows concurrent reads alongside writes
|
|
47
|
+
_db.pragma('journal_mode = WAL');
|
|
48
|
+
_db.pragma('foreign_keys = ON');
|
|
49
|
+
// Auto-create the table if it doesn't exist
|
|
50
|
+
_db.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS variables (
|
|
52
|
+
namespace TEXT NOT NULL,
|
|
53
|
+
key TEXT NOT NULL,
|
|
54
|
+
value TEXT NOT NULL,
|
|
55
|
+
type TEXT,
|
|
56
|
+
created_at TEXT,
|
|
57
|
+
updated_at TEXT,
|
|
58
|
+
PRIMARY KEY (namespace, key)
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
return _db;
|
|
62
|
+
}
|
|
63
|
+
// ─── CRUD operations ─────────────────────────────────────────────────────────
|
|
64
|
+
function dbGetVariable(namespace, key) {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const row = db
|
|
67
|
+
.prepare('SELECT value, type, created_at, updated_at FROM variables WHERE namespace = ? AND key = ?')
|
|
68
|
+
.get(namespace, key);
|
|
69
|
+
if (!row)
|
|
70
|
+
return undefined;
|
|
71
|
+
return {
|
|
72
|
+
value: JSON.parse(row.value),
|
|
73
|
+
type: row.type ?? undefined,
|
|
74
|
+
createdAt: row.created_at ?? undefined,
|
|
75
|
+
updatedAt: row.updated_at ?? undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function dbSetVariable(namespace, key, value, typeName, includeMetadata) {
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const serialized = JSON.stringify(value);
|
|
82
|
+
if (includeMetadata) {
|
|
83
|
+
// Preserve the original created_at on conflict
|
|
84
|
+
db.prepare(`INSERT INTO variables (namespace, key, value, type, created_at, updated_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
86
|
+
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
87
|
+
value = excluded.value,
|
|
88
|
+
type = excluded.type,
|
|
89
|
+
updated_at = excluded.updated_at`).run(namespace, key, serialized, typeName, now, now);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
db.prepare(`INSERT INTO variables (namespace, key, value, type, created_at, updated_at)
|
|
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);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function dbDeleteVariable(namespace, key) {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const result = db
|
|
104
|
+
.prepare('DELETE FROM variables WHERE namespace = ? AND key = ?')
|
|
105
|
+
.run(namespace, key);
|
|
106
|
+
return result.changes > 0;
|
|
107
|
+
}
|
|
108
|
+
function dbHasVariable(namespace, key) {
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const row = db
|
|
111
|
+
.prepare('SELECT 1 FROM variables WHERE namespace = ? AND key = ? LIMIT 1')
|
|
112
|
+
.get(namespace, key);
|
|
113
|
+
return row !== undefined;
|
|
114
|
+
}
|
|
115
|
+
function dbListVariables(namespace) {
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const rows = db
|
|
118
|
+
.prepare('SELECT key, value, type, created_at, updated_at FROM variables WHERE namespace = ? ORDER BY key')
|
|
119
|
+
.all(namespace);
|
|
120
|
+
const result = {};
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
result[row.key] = {
|
|
123
|
+
value: JSON.parse(row.value),
|
|
124
|
+
type: row.type ?? undefined,
|
|
125
|
+
createdAt: row.created_at ?? undefined,
|
|
126
|
+
updatedAt: row.updated_at ?? undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function dbClearNamespace(namespace) {
|
|
132
|
+
const db = getDb();
|
|
133
|
+
const result = db
|
|
134
|
+
.prepare('DELETE FROM variables WHERE namespace = ?')
|
|
135
|
+
.run(namespace);
|
|
136
|
+
return result.changes;
|
|
137
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace';
|
|
1
|
+
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace' | 'crossWorkflow';
|
|
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.0.8",
|
|
4
4
|
"description": "Local and global scoped variables for n8n workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
41
42
|
"@types/jest": "^29.5.12",
|
|
42
43
|
"@types/node": "^18.19.50",
|
|
43
44
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
@@ -54,6 +55,9 @@
|
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"n8n-workflow": "*"
|
|
56
57
|
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"better-sqlite3": "^9.4.0"
|
|
60
|
+
},
|
|
57
61
|
"jest": {
|
|
58
62
|
"preset": "ts-jest",
|
|
59
63
|
"testEnvironment": "node",
|