grafeo-ogm 2.5.4 → 2.6.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +123 -0
  3. package/dist/cjs/compilers/fulltext.compiler.d.ts +17 -1
  4. package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
  5. package/dist/cjs/compilers/fulltext.compiler.js +35 -3
  6. package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
  7. package/dist/cjs/compilers/mutation.compiler.d.ts +39 -0
  8. package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
  9. package/dist/cjs/compilers/mutation.compiler.js +161 -11
  10. package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
  11. package/dist/cjs/compilers/selection.compiler.d.ts +65 -1
  12. package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
  13. package/dist/cjs/compilers/selection.compiler.js +521 -25
  14. package/dist/cjs/compilers/selection.compiler.js.map +1 -1
  15. package/dist/cjs/compilers/where.compiler.d.ts +4 -0
  16. package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
  17. package/dist/cjs/compilers/where.compiler.js +23 -0
  18. package/dist/cjs/compilers/where.compiler.js.map +1 -1
  19. package/dist/cjs/interface-model.d.ts.map +1 -1
  20. package/dist/cjs/interface-model.js +8 -1
  21. package/dist/cjs/interface-model.js.map +1 -1
  22. package/dist/cjs/model.d.ts +7 -0
  23. package/dist/cjs/model.d.ts.map +1 -1
  24. package/dist/cjs/model.js +20 -4
  25. package/dist/cjs/model.js.map +1 -1
  26. package/dist/cjs/ogm.d.ts +14 -0
  27. package/dist/cjs/ogm.d.ts.map +1 -1
  28. package/dist/cjs/ogm.js +36 -2
  29. package/dist/cjs/ogm.js.map +1 -1
  30. package/dist/cjs/utils/strict-mode.d.ts +39 -0
  31. package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
  32. package/dist/cjs/utils/strict-mode.js +59 -0
  33. package/dist/cjs/utils/strict-mode.js.map +1 -0
  34. package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
  35. package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
  36. package/dist/esm/compilers/fulltext.compiler.js +35 -3
  37. package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
  38. package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
  39. package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
  40. package/dist/esm/compilers/mutation.compiler.js +161 -11
  41. package/dist/esm/compilers/mutation.compiler.js.map +1 -1
  42. package/dist/esm/compilers/selection.compiler.d.ts +65 -1
  43. package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
  44. package/dist/esm/compilers/selection.compiler.js +521 -25
  45. package/dist/esm/compilers/selection.compiler.js.map +1 -1
  46. package/dist/esm/compilers/where.compiler.d.ts +4 -0
  47. package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
  48. package/dist/esm/compilers/where.compiler.js +23 -0
  49. package/dist/esm/compilers/where.compiler.js.map +1 -1
  50. package/dist/esm/interface-model.d.ts.map +1 -1
  51. package/dist/esm/interface-model.js +8 -1
  52. package/dist/esm/interface-model.js.map +1 -1
  53. package/dist/esm/model.d.ts +7 -0
  54. package/dist/esm/model.d.ts.map +1 -1
  55. package/dist/esm/model.js +20 -4
  56. package/dist/esm/model.js.map +1 -1
  57. package/dist/esm/ogm.d.ts +14 -0
  58. package/dist/esm/ogm.d.ts.map +1 -1
  59. package/dist/esm/ogm.js +36 -2
  60. package/dist/esm/ogm.js.map +1 -1
  61. package/dist/esm/utils/strict-mode.d.ts +39 -0
  62. package/dist/esm/utils/strict-mode.d.ts.map +1 -0
  63. package/dist/esm/utils/strict-mode.js +54 -0
  64. package/dist/esm/utils/strict-mode.js.map +1 -0
  65. package/package.json +1 -1
@@ -11,6 +11,7 @@ const types_1 = require("../policy/types");
11
11
  const utils_1 = require("../schema/utils");
12
12
  const validation_1 = require("../utils/validation");
13
13
  const write_coercion_1 = require("../utils/write-coercion");
14
+ const strict_mode_1 = require("../utils/strict-mode");
14
15
  const where_compiler_1 = require("./where.compiler");
15
16
  /** The root node `n` of an update — the pre-2.4.8 names, unprefixed. */
16
17
  const TOP_SCOPE = {
@@ -67,6 +68,7 @@ class MutationCompiler {
67
68
  this.narrowedRelDefs = new WeakSet();
68
69
  this.logger = options?.logger;
69
70
  this.policyWhereCompiler = options?.whereCompiler;
71
+ this.strictMode = options?.strictMode ?? (0, strict_mode_1.lenientStrictMode)();
70
72
  }
71
73
  /**
72
74
  * Resolve and compile the TARGET type's `read` policy predicate for a
@@ -285,6 +287,68 @@ class MutationCompiler {
285
287
  throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
286
288
  return targetNodeDef;
287
289
  }
290
+ /**
291
+ * v2.6.0 — keys of a connect / disconnect item that the compiler does not
292
+ * read. Pre-2.6.0 (and still by default) they are ignored, so a typo such
293
+ * as `{ wehre: … }` compiles to an item with NO filter: a connect links
294
+ * every target node, a disconnect unlinks every related one. Default: one
295
+ * warning per OGM; strict: `OGMError`, like an unknown write field.
296
+ */
297
+ checkLinkItemKeys(item, kind, relDef) {
298
+ const allowed = kind === 'connect' ? CONNECT_ITEM_KEYS : DISCONNECT_ITEM_KEYS;
299
+ const unknown = Object.keys(item).filter((k) => item[k] !== undefined && !allowed.has(k));
300
+ if (unknown.length === 0)
301
+ return;
302
+ const list = unknown.map((k) => `"${k}"`).join(', ');
303
+ const allowedList = [...allowed].map((k) => `"${k}"`).join(', ');
304
+ if (this.strictMode.strict)
305
+ throw new errors_1.OGMError(`Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}". Allowed: ${allowedList}.`);
306
+ const unfiltered = item.where == null;
307
+ this.strictMode.warn(`${kind}-unknown-key`, `${relDef.fieldName}\u0000${unknown.join(',')}`, `Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}" ` +
308
+ `${unknown.length > 1 ? 'are' : 'is'} ignored` +
309
+ (unfiltered
310
+ ? `; with no "where", the item ${kind === 'connect'
311
+ ? `connects EVERY "${relDef.target}" node`
312
+ : `unlinks EVERY related "${relDef.target}" node`}.`
313
+ : '.') +
314
+ ` Allowed: ${allowedList}.`, `throw an OGMError for unknown ${kind} item keys`);
315
+ }
316
+ /**
317
+ * v2.6.0 — `labels` on a MERGE path (upsert, createMany with
318
+ * skipDuplicates) are SET after the MERGE, so they are also added to an
319
+ * EXISTING node it matched. Default: unchanged, one warning; strict: the
320
+ * labels go in `ON CREATE SET`, onto newly created nodes only.
321
+ */
322
+ labelsOnMatchedNodes(nodeDef, op) {
323
+ const call = op === 'upsert' ? 'upsert' : 'createMany with skipDuplicates';
324
+ this.strictMode.warn('labels-on-matched-nodes', `${nodeDef.typeName}\u0000${op}`, `"labels" on ${call} (${nodeDef.typeName}) are added to every node the MERGE returns, so an existing node ` +
325
+ `it matched is relabelled too.`, 'apply "labels" only to the nodes it creates');
326
+ }
327
+ /**
328
+ * v2.6.0 — `edge` keys that are not properties of the relationship's
329
+ * `@relationshipProperties` type. Pre-2.6.0 (and still by default) each
330
+ * is written to the relationship as a stray property. Default: one
331
+ * warning per OGM; strict: `OGMError` (`Unknown field …`).
332
+ */
333
+ checkEdgeKeys(relDef, edge, input) {
334
+ if (!(0, validation_1.isPlainObject)(edge))
335
+ return;
336
+ const declared = relDef.properties
337
+ ? this.schema.relationshipProperties.get(relDef.properties)?.properties
338
+ : undefined;
339
+ for (const key of Object.keys(edge)) {
340
+ if (edge[key] === undefined || declared?.has(key))
341
+ continue;
342
+ const owner = relDef.properties
343
+ ? `the relationship properties type "${relDef.properties}"`
344
+ : `the relationship, which declares no @relationshipProperties`;
345
+ if (this.strictMode.strict)
346
+ throw new errors_1.OGMError(`Unknown field "${key}" in the edge input (${input}) of "${relDef.fieldName}": it is not a property of ` +
347
+ `${owner}. Declare it in the schema, or fix the key.`);
348
+ this.strictMode.warn('edge-unknown-key', `${relDef.fieldName}\u0000${key}`, `Unknown field "${key}" in the edge input of "${relDef.fieldName}" is not a property of ${owner}; ` +
349
+ `it is written to the relationship as a stray property.`, `throw an OGMError for unknown edge fields`);
350
+ }
351
+ }
288
352
  /**
289
353
  * Generate CREATE Cypher for one or more nodes.
290
354
  * Handles scalar properties, nested relationship creates, and connects.
@@ -578,6 +642,19 @@ class MutationCompiler {
578
642
  if (mergeProps.length === 0)
579
643
  throw new errors_1.OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
580
644
  `an @id or @unique field of ${nodeDef.typeName}.`);
645
+ // v2.6.0 — MERGE matches ALL its keys together, so `{ id, email }`
646
+ // misses a node that has the email under another id and creates a
647
+ // duplicate beside it (or fails on the unique constraint). Default:
648
+ // compiled as before, with one warning; strict: exactly one key.
649
+ if (mergeKeyNames.length > 1) {
650
+ const keys = mergeKeyNames.map((k) => `"${k}"`).join(', ');
651
+ if (this.strictMode.strict)
652
+ throw new errors_1.OGMError(`upsert "where" must name exactly one @id / @unique field of ${nodeDef.typeName}; got ${keys}. ` +
653
+ `MERGE matches all of them together, so a node that has one but not the others is not found ` +
654
+ `and a duplicate is created. Pass a single unique key.`);
655
+ this.strictMode.warn('upsert-multiple-unique-keys', `${nodeDef.typeName}\u0000${mergeKeyNames.join(',')}`, `upsert on ${nodeDef.typeName} with several unique keys in "where" (${keys}) MERGEs on all of them ` +
656
+ `together: a node that has one but not the others is not matched and a duplicate is created.`, 'require exactly one unique key in an upsert "where"');
657
+ }
581
658
  lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
582
659
  // ON CREATE SET — all properties from create input (scalar only)
583
660
  const createSets = [];
@@ -628,6 +705,11 @@ class MutationCompiler {
628
705
  continue;
629
706
  createSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = randomUUID()`);
630
707
  }
708
+ // v2.6.0 — strict: runtime labels go on the node only when MERGE creates
709
+ // it (see `labelsOnMatchedNodes`).
710
+ const labelsOnCreate = this.strictMode.strict && labels && labels.length > 0;
711
+ if (labelsOnCreate)
712
+ createSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
631
713
  if (createSets.length > 0)
632
714
  lines.push(`ON CREATE SET ${createSets.join(', ')}`);
633
715
  // ON MATCH SET — all properties from update input (scalar only)
@@ -651,9 +733,10 @@ class MutationCompiler {
651
733
  if (updateSets.length > 0)
652
734
  lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
653
735
  // Extra labels
654
- if (labels && labels.length > 0) {
736
+ if (labels && labels.length > 0 && !labelsOnCreate) {
655
737
  const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
656
738
  lines.push(`SET n:${validatedLabels}`);
739
+ this.labelsOnMatchedNodes(nodeDef, 'upsert');
657
740
  }
658
741
  lines.push('RETURN n');
659
742
  return { cypher: lines.join('\n'), params };
@@ -803,6 +886,9 @@ class MutationCompiler {
803
886
  onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
804
887
  params[paramName] = bound;
805
888
  }
889
+ // v2.6.0 — strict: labels only on the nodes MERGE creates.
890
+ if (this.strictMode.strict && labels && labels.length > 0)
891
+ onCreateSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
806
892
  if (onCreateSets.length > 0)
807
893
  lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
808
894
  }
@@ -822,9 +908,13 @@ class MutationCompiler {
822
908
  lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
823
909
  }
824
910
  // Extra labels
825
- if (labels && labels.length > 0) {
911
+ if (labels &&
912
+ labels.length > 0 &&
913
+ !(skipDuplicates && this.strictMode.strict)) {
826
914
  const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
827
915
  lines.push(`SET n:${validatedLabels}`);
916
+ if (skipDuplicates)
917
+ this.labelsOnMatchedNodes(nodeDef, 'createMany');
828
918
  }
829
919
  lines.push('RETURN count(n) AS count');
830
920
  return { cypher: lines.join('\n'), params };
@@ -974,6 +1064,7 @@ class MutationCompiler {
974
1064
  (0, validation_1.mergeParams)(params, propParams);
975
1065
  lines.push(`CREATE ${relPattern}`);
976
1066
  // Set edge properties if present
1067
+ this.checkEdgeKeys(relDef, edgeInput, 'nested create');
977
1068
  if (edgeInput && Object.keys(edgeInput).length > 0) {
978
1069
  const relVar = `r_edge_${startCounter + counter - 1}`;
979
1070
  // Re-match the relationship to set edge props
@@ -1112,6 +1203,11 @@ class MutationCompiler {
1112
1203
  scope,
1113
1204
  }));
1114
1205
  else {
1206
+ // Every item is a plain object here (fast-path eligibility).
1207
+ for (const item of spec) {
1208
+ this.checkLinkItemKeys(item, 'connect', relDef);
1209
+ this.checkEdgeKeys(relDef, item.edge, 'connect');
1210
+ }
1115
1211
  const targetVar = `${varPrefix}target`;
1116
1212
  const relVar = `${varPrefix}r`;
1117
1213
  const targetLabelStr = this.getCachedLabelString(targetNodeDef);
@@ -1238,6 +1334,8 @@ class MutationCompiler {
1238
1334
  // compiling it would MATCH, and link, every target node.
1239
1335
  if (!(0, validation_1.isPlainObject)(connectSpec))
1240
1336
  throw new errors_1.OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
1337
+ this.checkLinkItemKeys(connectSpec, 'connect', relDef);
1338
+ this.checkEdgeKeys(relDef, connectSpec.edge, 'connect');
1241
1339
  const lines = [];
1242
1340
  const whereSpec = connectSpec.where;
1243
1341
  const edgeInput = connectSpec.edge;
@@ -1379,21 +1477,25 @@ class MutationCompiler {
1379
1477
  const predicate = targetNodeDef
1380
1478
  ? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
1381
1479
  : undefined;
1480
+ const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
1382
1481
  const pattern = (0, utils_1.buildRelPattern)({
1383
1482
  sourceVar,
1384
1483
  relDef,
1385
- targetVar: predicate ? blanketTarget : '',
1484
+ targetVar: predicate || labelPredicate ? blanketTarget : '',
1386
1485
  edgeVar: relVar,
1387
- targetLabelRaw: this.blanketTargetLabel(relDef),
1486
+ targetLabelRaw: labelRaw,
1388
1487
  });
1389
1488
  lines.push(`OPTIONAL MATCH ${pattern}`);
1390
- if (predicate)
1391
- lines.push(`WHERE ${predicate}`);
1489
+ const blanketConditions = [labelPredicate, predicate].filter(Boolean);
1490
+ if (blanketConditions.length > 0)
1491
+ lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
1392
1492
  lines.push(`DELETE ${relVar}`);
1393
1493
  }
1394
1494
  else {
1395
1495
  // Specific disconnect with conditions
1396
1496
  const disconnectSpec = item;
1497
+ if ((0, validation_1.isPlainObject)(disconnectSpec))
1498
+ this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
1397
1499
  const whereSpec = disconnectSpec.where;
1398
1500
  const targetVar = `${varPrefix}target_${tag}_${si}`;
1399
1501
  const pattern = (0, utils_1.buildRelPattern)({
@@ -1432,6 +1534,33 @@ class MutationCompiler {
1432
1534
  this.narrowedRelDefs.add(narrowed);
1433
1535
  return narrowed;
1434
1536
  }
1537
+ /**
1538
+ * v2.6.0 — how a blanket disconnect (`null` / `{}`) restricts its target.
1539
+ *
1540
+ * Default (v2.5.3): only a relationship narrowed to a union / interface
1541
+ * member keeps that member's label; otherwise the target is anonymous,
1542
+ * so with two fields on one relationship type and direction
1543
+ * (`pets: [Pet]`, `toys: [Toy]`, both `HAS`) a blanket disconnect of one
1544
+ * also unlinks the other's nodes (warned once at OGM construction).
1545
+ *
1546
+ * Strict: the field's own target type — its label, or for an abstract
1547
+ * target a predicate over its members' labels on `targetVar`.
1548
+ */
1549
+ blanketRestriction(relDef, targetNodeDef, targetVar) {
1550
+ const narrowed = this.blanketTargetLabel(relDef);
1551
+ if (narrowed || !this.strictMode.strict || !targetNodeDef)
1552
+ return { labelRaw: narrowed, labelPredicate: null };
1553
+ if (abstractTarget(targetNodeDef.typeName, this.schema))
1554
+ return {
1555
+ labelRaw: undefined,
1556
+ labelPredicate: this.targetMatch(targetNodeDef, targetVar)
1557
+ .labelPredicate,
1558
+ };
1559
+ return {
1560
+ labelRaw: this.getCachedLabelString(targetNodeDef),
1561
+ labelPredicate: null,
1562
+ };
1563
+ }
1435
1564
  /** The label a blanket disconnect over `relDef` must keep (see above). */
1436
1565
  blanketTargetLabel(relDef) {
1437
1566
  if (!this.narrowedRelDefs.has(relDef))
@@ -1522,6 +1651,8 @@ class MutationCompiler {
1522
1651
  : [item.disconnect];
1523
1652
  for (let di = 0; di < disconnectItems.length; di++) {
1524
1653
  const disconnectSpec = disconnectItems[di];
1654
+ if ((0, validation_1.isPlainObject)(disconnectSpec))
1655
+ this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
1525
1656
  const whereSpec = disconnectSpec.where;
1526
1657
  // A nested `disconnect` needs the target bound, so an item that
1527
1658
  // carries one is never the anonymous blanket form.
@@ -1538,18 +1669,20 @@ class MutationCompiler {
1538
1669
  // disconnect: only unlink targets the caller can read.
1539
1670
  const blanketTarget = `${sourceVar}_disc${varCounter}`;
1540
1671
  const predicate = this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter);
1541
- if (predicate)
1672
+ const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
1673
+ if (predicate || labelPredicate)
1542
1674
  varCounter++;
1543
1675
  const pattern = (0, utils_1.buildRelPattern)({
1544
1676
  sourceVar,
1545
1677
  relDef,
1546
- targetVar: predicate ? blanketTarget : '',
1678
+ targetVar: predicate || labelPredicate ? blanketTarget : '',
1547
1679
  edgeVar: relVar,
1548
- targetLabelRaw: this.blanketTargetLabel(relDef),
1680
+ targetLabelRaw: labelRaw,
1549
1681
  });
1550
1682
  lines.push(`OPTIONAL MATCH ${pattern}`);
1551
- if (predicate)
1552
- lines.push(`WHERE ${predicate}`);
1683
+ const blanketConditions = [labelPredicate, predicate].filter(Boolean);
1684
+ if (blanketConditions.length > 0)
1685
+ lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
1553
1686
  lines.push(`DELETE ${relVar}`);
1554
1687
  }
1555
1688
  else {
@@ -1657,6 +1790,7 @@ class MutationCompiler {
1657
1790
  // create() accepts. Pre-2.4.5 this path never read
1658
1791
  // `createSpec.edge`, so they were silently dropped.
1659
1792
  const edgeInput = createSpec.edge;
1793
+ this.checkEdgeKeys(relDef, edgeInput, 'nested create');
1660
1794
  const edgeEntries = Object.entries(edgeInput ?? {}).filter(([, v]) => v !== undefined);
1661
1795
  const edgeVar = edgeEntries.length ? `${createVar}_e` : undefined;
1662
1796
  const relPattern = (0, utils_1.buildRelPattern)({
@@ -1727,6 +1861,7 @@ class MutationCompiler {
1727
1861
  // Pre-compute SET clauses for edge (relationship) properties
1728
1862
  const edgeClauses = [];
1729
1863
  const edgeParams = {};
1864
+ this.checkEdgeKeys(relDef, edgeUpdate, 'nested update');
1730
1865
  if (edgeUpdate)
1731
1866
  for (const [prop, val] of Object.entries(edgeUpdate)) {
1732
1867
  if (val === undefined)
@@ -1875,6 +2010,21 @@ MutationCompiler.OPERATOR_SUFFIXES = [
1875
2010
  * path to validate that every item shares the same shape; mismatched
1876
2011
  * shapes silently drop keys from the compiled WHERE / SET.
1877
2012
  */
2013
+ /**
2014
+ * v2.6.0 — the keys a connect / disconnect item is compiled from. `overwrite`
2015
+ * is offered by the generated `ConnectFieldInput` and accepted as before
2016
+ * (MERGE + SET always overwrites the edge properties it is given).
2017
+ */
2018
+ const CONNECT_ITEM_KEYS = new Set([
2019
+ 'where',
2020
+ 'edge',
2021
+ 'overwrite',
2022
+ 'connect',
2023
+ ]);
2024
+ const DISCONNECT_ITEM_KEYS = new Set([
2025
+ 'where',
2026
+ 'disconnect',
2027
+ ]);
1878
2028
  /** Keys that make a nested-mutation `where` connection-shaped. */
1879
2029
  const CONNECTION_WHERE_KEYS = new Set([
1880
2030
  'node',