@teleporthq/teleport-plugin-next-data-source 0.43.63 → 0.43.64

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 (125) hide show
  1. package/__tests__/filter-groups.test.ts +174 -0
  2. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  3. package/dist/cjs/fetchers/airtable.js +1 -1
  4. package/dist/cjs/fetchers/airtable.js.map +1 -1
  5. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  6. package/dist/cjs/fetchers/clickhouse.js +1 -1
  7. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  8. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  9. package/dist/cjs/fetchers/csv-file.js +3 -3
  10. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  11. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  12. package/dist/cjs/fetchers/firestore.js +1 -1
  13. package/dist/cjs/fetchers/firestore.js.map +1 -1
  14. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  15. package/dist/cjs/fetchers/google-sheets.js +1 -1
  16. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  17. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  18. package/dist/cjs/fetchers/javascript.js +1 -1
  19. package/dist/cjs/fetchers/javascript.js.map +1 -1
  20. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  21. package/dist/cjs/fetchers/mariadb.js +1 -1
  22. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  23. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  24. package/dist/cjs/fetchers/mongodb.js +1 -1
  25. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  26. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  27. package/dist/cjs/fetchers/mysql.js +1 -1
  28. package/dist/cjs/fetchers/mysql.js.map +1 -1
  29. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/postgresql.js +1 -1
  31. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  32. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  33. package/dist/cjs/fetchers/redis.js +1 -1
  34. package/dist/cjs/fetchers/redis.js.map +1 -1
  35. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  36. package/dist/cjs/fetchers/redshift.js +2 -2
  37. package/dist/cjs/fetchers/redshift.js.map +1 -1
  38. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  39. package/dist/cjs/fetchers/rest-api.js +2 -2
  40. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  41. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/supabase.js +1 -1
  43. package/dist/cjs/fetchers/supabase.js.map +1 -1
  44. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/teleport.js +1 -1
  46. package/dist/cjs/fetchers/teleport.js.map +1 -1
  47. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  48. package/dist/cjs/fetchers/turso.js +1 -1
  49. package/dist/cjs/fetchers/turso.js.map +1 -1
  50. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  51. package/dist/cjs/utils.d.ts +32 -0
  52. package/dist/cjs/utils.d.ts.map +1 -1
  53. package/dist/cjs/utils.js +116 -18
  54. package/dist/cjs/utils.js.map +1 -1
  55. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  56. package/dist/esm/fetchers/airtable.js +2 -2
  57. package/dist/esm/fetchers/airtable.js.map +1 -1
  58. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  59. package/dist/esm/fetchers/clickhouse.js +2 -2
  60. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  61. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  62. package/dist/esm/fetchers/csv-file.js +4 -4
  63. package/dist/esm/fetchers/csv-file.js.map +1 -1
  64. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  65. package/dist/esm/fetchers/firestore.js +2 -2
  66. package/dist/esm/fetchers/firestore.js.map +1 -1
  67. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  68. package/dist/esm/fetchers/google-sheets.js +2 -2
  69. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  70. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  71. package/dist/esm/fetchers/javascript.js +2 -2
  72. package/dist/esm/fetchers/javascript.js.map +1 -1
  73. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  74. package/dist/esm/fetchers/mariadb.js +2 -2
  75. package/dist/esm/fetchers/mariadb.js.map +1 -1
  76. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  77. package/dist/esm/fetchers/mongodb.js +2 -2
  78. package/dist/esm/fetchers/mongodb.js.map +1 -1
  79. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  80. package/dist/esm/fetchers/mysql.js +2 -2
  81. package/dist/esm/fetchers/mysql.js.map +1 -1
  82. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  83. package/dist/esm/fetchers/postgresql.js +2 -2
  84. package/dist/esm/fetchers/postgresql.js.map +1 -1
  85. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  86. package/dist/esm/fetchers/redis.js +2 -2
  87. package/dist/esm/fetchers/redis.js.map +1 -1
  88. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  89. package/dist/esm/fetchers/redshift.js +3 -3
  90. package/dist/esm/fetchers/redshift.js.map +1 -1
  91. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  92. package/dist/esm/fetchers/rest-api.js +3 -3
  93. package/dist/esm/fetchers/rest-api.js.map +1 -1
  94. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  95. package/dist/esm/fetchers/supabase.js +2 -2
  96. package/dist/esm/fetchers/supabase.js.map +1 -1
  97. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  98. package/dist/esm/fetchers/teleport.js +2 -2
  99. package/dist/esm/fetchers/teleport.js.map +1 -1
  100. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  101. package/dist/esm/fetchers/turso.js +2 -2
  102. package/dist/esm/fetchers/turso.js.map +1 -1
  103. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  104. package/dist/esm/utils.d.ts +32 -0
  105. package/dist/esm/utils.d.ts.map +1 -1
  106. package/dist/esm/utils.js +112 -17
  107. package/dist/esm/utils.js.map +1 -1
  108. package/package.json +3 -3
  109. package/src/fetchers/airtable.ts +35 -36
  110. package/src/fetchers/clickhouse.ts +26 -36
  111. package/src/fetchers/csv-file.ts +13 -21
  112. package/src/fetchers/firestore.ts +41 -35
  113. package/src/fetchers/google-sheets.ts +15 -23
  114. package/src/fetchers/javascript.ts +11 -21
  115. package/src/fetchers/mariadb.ts +33 -44
  116. package/src/fetchers/mongodb.ts +66 -81
  117. package/src/fetchers/mysql.ts +33 -45
  118. package/src/fetchers/postgresql.ts +52 -62
  119. package/src/fetchers/redis.ts +6 -5
  120. package/src/fetchers/redshift.ts +29 -41
  121. package/src/fetchers/rest-api.ts +11 -21
  122. package/src/fetchers/supabase.ts +104 -41
  123. package/src/fetchers/teleport.ts +53 -64
  124. package/src/fetchers/turso.ts +27 -39
  125. package/src/utils.ts +188 -31
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateFilterTreeHelpersCode,
5
6
  generateSearchEscapeHelpersCode,
6
7
  } from '../utils'
7
8
 
@@ -42,6 +43,8 @@ export const generateTursoFetcher = (
42
43
 
43
44
  ${generateSafeJSONParseCode()}
44
45
 
46
+ ${generateFilterTreeHelpersCode()}
47
+
45
48
  ${generateSearchEscapeHelpersCode()}
46
49
 
47
50
  ${generateDateFormatterCode()}
@@ -92,52 +95,37 @@ export default async function handler(req, res) {
92
95
  }
93
96
 
94
97
  if (filters) {
95
- const parsedFilters = safeJSONParse(filters)
98
+ const filterTree = normalizeFilterTree(safeJSONParse(filters))
96
99
 
97
- if (Array.isArray(parsedFilters)) {
98
- parsedFilters.forEach((filter) => {
99
- if (!filter.source || filter.destination === undefined) return
100
-
101
- const field = sanitizeIdentifier(filter.source)
102
- const value = filter.destination
103
- const operand = filter.operand || '='
100
+ if (filterTree) {
101
+ const buildCondition = (condition) => {
102
+ const field = sanitizeIdentifier(condition.source)
103
+ const value = condition.destination
104
+ const operand = condition.operand
104
105
 
105
106
  if (Array.isArray(value)) {
106
- if (value.length === 0) return
107
+ if (value.length === 0) return null
107
108
  const placeholders = value.map(() => '?').join(', ')
108
109
  queryParams.push(...value)
109
- if (operand === '!=') {
110
- whereClauses.push(\`\${field} NOT IN (\${placeholders})\`)
111
- } else {
112
- whereClauses.push(\`\${field} IN (\${placeholders})\`)
113
- }
114
- } else {
115
- if (value === null) {
116
- if (operand === '=') {
117
- whereClauses.push(\`\${field} IS NULL\`)
118
- } else if (operand === '!=') {
119
- whereClauses.push(\`\${field} IS NOT NULL\`)
120
- }
121
- } else {
122
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
123
- const sqlOperator = validOps.includes(operand) ? operand : '='
124
- whereClauses.push(\`\${field} \${sqlOperator} ?\`)
125
- queryParams.push(value)
126
- }
110
+ return operand === '!='
111
+ ? \`\${field} NOT IN (\${placeholders})\`
112
+ : \`\${field} IN (\${placeholders})\`
127
113
  }
128
- })
129
- } else {
130
- Object.entries(parsedFilters).forEach(([key, value]) => {
131
- const field = sanitizeIdentifier(key)
132
- if (Array.isArray(value)) {
133
- const placeholders = value.map(() => '?').join(', ')
134
- queryParams.push(...value)
135
- whereClauses.push(\`\${field} IN (\${placeholders})\`)
136
- } else {
137
- whereClauses.push(\`\${field} = ?\`)
138
- queryParams.push(value)
114
+
115
+ if (value === null) {
116
+ if (operand === '=') return \`\${field} IS NULL\`
117
+ if (operand === '!=') return \`\${field} IS NOT NULL\`
118
+ return null
139
119
  }
140
- })
120
+
121
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
122
+ const sqlOperator = validOps.includes(operand) ? operand : '='
123
+ queryParams.push(value)
124
+ return \`\${field} \${sqlOperator} ?\`
125
+ }
126
+
127
+ const clause = buildFilterTreeClause(filterTree, buildCondition)
128
+ if (clause) whereClauses.push(clause)
141
129
  }
142
130
  }
143
131
 
package/src/utils.ts CHANGED
@@ -596,6 +596,146 @@ const dateReplacer = (key, value) => {
596
596
  }`
597
597
  }
598
598
 
599
+ /**
600
+ * Every leaf condition of a filters array, unwrapping any `{ type: 'group' }`
601
+ * envelopes the inspector wrapped them in. Generator-side checks that ask
602
+ * "does any destination reference state / a route param?" must look through
603
+ * groups — asking `entry.destination` of a group silently answers "no", which
604
+ * is how a dynamic filter ends up baked into initialData at build time.
605
+ */
606
+ export const collectFilterLeafConditions = (entries: unknown): unknown[] => {
607
+ if (!Array.isArray(entries)) {
608
+ return []
609
+ }
610
+ const leaves: unknown[] = []
611
+ const walk = (entry: unknown): void => {
612
+ if (!entry || typeof entry !== 'object') {
613
+ return
614
+ }
615
+ const node = entry as { type?: string; children?: unknown[] }
616
+ if (node.type === 'group') {
617
+ ;(node.children || []).forEach(walk)
618
+ return
619
+ }
620
+ leaves.push(entry)
621
+ }
622
+ entries.forEach(walk)
623
+ return leaves
624
+ }
625
+
626
+ /**
627
+ * Rebuilds a filters array keeping its group structure, dropping leaf
628
+ * conditions that `keepCondition` rejects and any group left empty by that.
629
+ * Used so the SSG emit can discard unresolvable dynamic destinations without
630
+ * flattening an `or` group into ANDs on the way.
631
+ */
632
+ export const pruneFilterTree = (
633
+ entries: unknown[],
634
+ keepCondition: (condition: unknown) => boolean
635
+ ): unknown[] => {
636
+ const prune = (entry: unknown): unknown | null => {
637
+ if (!entry || typeof entry !== 'object') {
638
+ return null
639
+ }
640
+ const node = entry as { type?: string; children?: unknown[] }
641
+ if (node.type === 'group') {
642
+ const children = (node.children || []).map(prune).filter(Boolean)
643
+ if (children.length === 0) {
644
+ return null
645
+ }
646
+ return { ...node, children }
647
+ }
648
+ return keepCondition(entry) ? entry : null
649
+ }
650
+ return entries.map(prune).filter(Boolean) as unknown[]
651
+ }
652
+
653
+ /**
654
+ * Runtime helpers for the filter TREE the GUI authors.
655
+ *
656
+ * The inspector emits filters as a tree — a root `{ type: 'group', operator,
657
+ * children }` whose children are `{ type: 'condition', source, destination,
658
+ * operand }` leaves or further groups (an `or` group nested inside the root
659
+ * `and` is how "A and (B or C)" is expressed). Older projects, and params built
660
+ * by hand, still send a flat array of bare conditions or a plain `{ key: value }`
661
+ * map.
662
+ *
663
+ * Every fetcher used to hand-roll `parsedFilters.forEach(f => { if (!f.source)
664
+ * return ... })`, which silently DROPPED any group — a group has no `.source`,
665
+ * so the whole filter became a no-op and the page rendered unfiltered rows.
666
+ * These helpers exist so no fetcher has to know the shape: `normalizeFilterTree`
667
+ * turns all three input forms into one tree, and the walkers below consume it.
668
+ */
669
+ export const generateFilterTreeHelpersCode = (): string => {
670
+ return `const normalizeFilterTree = (parsed) => {
671
+ if (parsed === null || parsed === undefined) return null
672
+
673
+ const toNode = (entry) => {
674
+ if (!entry || typeof entry !== 'object') return null
675
+ if (entry.type === 'group') {
676
+ const children = Array.isArray(entry.children)
677
+ ? entry.children.map(toNode).filter(Boolean)
678
+ : []
679
+ if (children.length === 0) return null
680
+ return { type: 'group', operator: entry.operator === 'or' ? 'or' : 'and', children }
681
+ }
682
+ if (!entry.source || entry.destination === undefined) return null
683
+ return {
684
+ type: 'condition',
685
+ source: entry.source,
686
+ destination: entry.destination,
687
+ operand: entry.operand || '=',
688
+ }
689
+ }
690
+
691
+ const children = Array.isArray(parsed)
692
+ ? parsed.map(toNode).filter(Boolean)
693
+ : Object.entries(parsed).map(([source, destination]) =>
694
+ toNode({ source, destination, operand: '=' })
695
+ ).filter(Boolean)
696
+
697
+ if (children.length === 0) return null
698
+ // A single root group is already the tree; wrapping it in another and-group
699
+ // would only add a redundant level.
700
+ if (children.length === 1 && children[0].type === 'group') return children[0]
701
+ return { type: 'group', operator: 'and', children }
702
+ }
703
+
704
+ const evaluateFilterTree = (node, testCondition) => {
705
+ if (!node) return true
706
+ if (node.type === 'group') {
707
+ if (node.children.length === 0) return true
708
+ return node.operator === 'or'
709
+ ? node.children.some((child) => evaluateFilterTree(child, testCondition))
710
+ : node.children.every((child) => evaluateFilterTree(child, testCondition))
711
+ }
712
+ return testCondition(node)
713
+ }
714
+
715
+ const buildFilterTreeClause = (node, buildCondition) => {
716
+ if (!node) return null
717
+ if (node.type === 'group') {
718
+ const parts = []
719
+ for (const child of node.children) {
720
+ const clause = buildFilterTreeClause(child, buildCondition)
721
+ if (clause) parts.push(clause)
722
+ }
723
+ if (parts.length === 0) return null
724
+ if (parts.length === 1) return parts[0]
725
+ return '(' + parts.join(node.operator === 'or' ? ' OR ' : ' AND ') + ')'
726
+ }
727
+ return buildCondition(node)
728
+ }
729
+
730
+ const flattenFilterConditions = (node) => {
731
+ if (!node) return []
732
+ if (node.type === 'group') {
733
+ return node.children.reduce((acc, child) => acc.concat(flattenFilterConditions(child)), [])
734
+ }
735
+ return [node]
736
+ }`
737
+ }
738
+
599
739
  export const generateSortFilterHelperCode = (): string => {
600
740
  return `function getNestedValue(obj, path) {
601
741
  if (!obj || typeof obj !== 'object') return undefined
@@ -801,13 +941,14 @@ export const hasUnresolvableDynamicParams = (
801
941
  }
802
942
 
803
943
  if (param.type === 'static' && Array.isArray(param.content)) {
804
- const hasDynamicDestinations = param.content.some(
944
+ const leafConditions = collectFilterLeafConditions(param.content)
945
+ const hasDynamicDestinations = leafConditions.some(
805
946
  (item: any) =>
806
947
  item && typeof item === 'object' && ASTUtils.isUIDLDynamicReference(item?.destination)
807
948
  )
808
949
 
809
950
  if (hasDynamicDestinations && dynamicRouteAttr) {
810
- const allResolvable = param.content.every((item: any) => {
951
+ const allResolvable = leafConditions.every((item: any) => {
811
952
  if (!item || typeof item !== 'object') {
812
953
  return true
813
954
  }
@@ -1165,9 +1306,9 @@ export const extractDataSourceIntoGetStaticProps = (
1165
1306
  // tslint:disable-next-line:no-any
1166
1307
  const resourceParamsForFilters = (node.content as any).resource?.params || {}
1167
1308
  const nodeFilters = resourceParamsForFilters.filters?.content || []
1168
- const hasDynamicFilterDestinations =
1169
- Array.isArray(nodeFilters) &&
1170
- nodeFilters.some((f: any) => ASTUtils.isUIDLDynamicReference(f?.destination))
1309
+ const hasDynamicFilterDestinations = collectFilterLeafConditions(nodeFilters).some((f: any) =>
1310
+ ASTUtils.isUIDLDynamicReference(f?.destination)
1311
+ )
1171
1312
 
1172
1313
  // Update all target JSX nodes with initialData (only those that don't already have it)
1173
1314
  for (const jsxNode of nodesToUpdate) {
@@ -1291,9 +1432,12 @@ export const extractDataSourceIntoGetStaticProps = (
1291
1432
  if (key === 'sorts' || key === 'filters' || key === 'queryColumns') {
1292
1433
  let itemsToProcess = validItems
1293
1434
  if (key === 'filters') {
1294
- // Keep filter items that are either static or route-resolvable (via dynamicRouteAttr)
1295
- // Filter out items with unresolvable dynamic destinations
1296
- itemsToProcess = validItems.filter((item: any) => {
1435
+ // Keep filter conditions that are either static or route-resolvable
1436
+ // (via dynamicRouteAttr) and drop the ones with unresolvable dynamic
1437
+ // destinations. Pruned through the tree so a condition nested in a
1438
+ // group is judged on its own destination, and an `or` group keeps
1439
+ // its shape on the way to the data source.
1440
+ itemsToProcess = pruneFilterTree(validItems, (item: any) => {
1297
1441
  if (!ASTUtils.isUIDLDynamicReference(item?.destination)) {
1298
1442
  return true
1299
1443
  }
@@ -1310,36 +1454,49 @@ export const extractDataSourceIntoGetStaticProps = (
1310
1454
  const hasRouteResolvableFilters =
1311
1455
  key === 'filters' &&
1312
1456
  dynamicRouteAttr &&
1313
- itemsToProcess.some(
1457
+ collectFilterLeafConditions(itemsToProcess).some(
1314
1458
  (item: any) =>
1315
1459
  item &&
1316
1460
  typeof item === 'object' &&
1317
1461
  ASTUtils.isUIDLDynamicReference(item?.destination)
1318
1462
  )
1319
1463
 
1320
- const arrayExpr = types.arrayExpression(
1321
- itemsToProcess.map((item: any) => {
1322
- if (typeof item === 'string') {
1323
- return types.stringLiteral(item)
1324
- }
1325
- if (typeof item === 'number') {
1326
- return types.numericLiteral(item)
1327
- }
1328
- if (typeof item === 'boolean') {
1329
- return types.booleanLiteral(item)
1330
- }
1331
- if (typeof item === 'object' && item !== null) {
1332
- if (
1333
- hasRouteResolvableFilters &&
1334
- ASTUtils.isUIDLDynamicReference(item?.destination)
1335
- ) {
1336
- return buildFilterObjectAST(item, dynamicRouteAttr)
1337
- }
1338
- return ASTUtils.objectToObjectExpression(item)
1339
- }
1464
+ // Recurses through groups so a route-resolvable destination nested
1465
+ // inside one still gets its `context.params.*` AST instead of being
1466
+ // emitted as a raw UIDL dynamic-reference object.
1467
+ const buildFilterEntryAST = (item: any): types.Expression => {
1468
+ if (typeof item === 'string') {
1469
+ return types.stringLiteral(item)
1470
+ }
1471
+ if (typeof item === 'number') {
1472
+ return types.numericLiteral(item)
1473
+ }
1474
+ if (typeof item === 'boolean') {
1475
+ return types.booleanLiteral(item)
1476
+ }
1477
+ if (typeof item !== 'object' || item === null) {
1340
1478
  return types.nullLiteral()
1341
- })
1342
- )
1479
+ }
1480
+ if (item.type === 'group' && Array.isArray(item.children)) {
1481
+ return types.objectExpression([
1482
+ types.objectProperty(types.identifier('type'), types.stringLiteral('group')),
1483
+ types.objectProperty(
1484
+ types.identifier('operator'),
1485
+ types.stringLiteral(item.operator === 'or' ? 'or' : 'and')
1486
+ ),
1487
+ types.objectProperty(
1488
+ types.identifier('children'),
1489
+ types.arrayExpression(item.children.map(buildFilterEntryAST))
1490
+ ),
1491
+ ])
1492
+ }
1493
+ if (hasRouteResolvableFilters && ASTUtils.isUIDLDynamicReference(item?.destination)) {
1494
+ return buildFilterObjectAST(item, dynamicRouteAttr)
1495
+ }
1496
+ return ASTUtils.objectToObjectExpression(item)
1497
+ }
1498
+
1499
+ const arrayExpr = types.arrayExpression(itemsToProcess.map(buildFilterEntryAST))
1343
1500
  // Wrap in JSON.stringify()
1344
1501
  astValue = types.callExpression(
1345
1502
  types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),