@teleporthq/teleport-plugin-next-data-source 0.40.15
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/ARRAY_MAPPER_PAGINATION.md +1128 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/SEARCH_IMPLEMENTATION_SUMMARY.md +983 -0
- package/__tests__/fetchers.test.ts +545 -0
- package/__tests__/integration.test.ts +561 -0
- package/__tests__/mocks.ts +241 -0
- package/__tests__/pagination.test.ts +31 -0
- package/__tests__/plugin.test.ts +577 -0
- package/__tests__/utils.test.ts +430 -0
- package/__tests__/validation.test.ts +348 -0
- package/dist/cjs/array-mapper-pagination.d.ts +32 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -0
- package/dist/cjs/array-mapper-pagination.js +77 -0
- package/dist/cjs/array-mapper-pagination.js.map +1 -0
- package/dist/cjs/count-fetchers.d.ts +12 -0
- package/dist/cjs/count-fetchers.d.ts.map +1 -0
- package/dist/cjs/count-fetchers.js +46 -0
- package/dist/cjs/count-fetchers.js.map +1 -0
- package/dist/cjs/data-source-fetchers.d.ts +14 -0
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -0
- package/dist/cjs/data-source-fetchers.js +185 -0
- package/dist/cjs/data-source-fetchers.js.map +1 -0
- package/dist/cjs/fetchers/airtable.d.ts +6 -0
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -0
- package/dist/cjs/fetchers/airtable.js +27 -0
- package/dist/cjs/fetchers/airtable.js.map +1 -0
- package/dist/cjs/fetchers/clickhouse.d.ts +6 -0
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/cjs/fetchers/clickhouse.js +29 -0
- package/dist/cjs/fetchers/clickhouse.js.map +1 -0
- package/dist/cjs/fetchers/csv-file.d.ts +7 -0
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -0
- package/dist/cjs/fetchers/csv-file.js +36 -0
- package/dist/cjs/fetchers/csv-file.js.map +1 -0
- package/dist/cjs/fetchers/firestore.d.ts +6 -0
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -0
- package/dist/cjs/fetchers/firestore.js +35 -0
- package/dist/cjs/fetchers/firestore.js.map +1 -0
- package/dist/cjs/fetchers/google-sheets.d.ts +6 -0
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/cjs/fetchers/google-sheets.js +30 -0
- package/dist/cjs/fetchers/google-sheets.js.map +1 -0
- package/dist/cjs/fetchers/index.d.ts +17 -0
- package/dist/cjs/fetchers/index.d.ts.map +1 -0
- package/dist/cjs/fetchers/index.js +56 -0
- package/dist/cjs/fetchers/index.js.map +1 -0
- package/dist/cjs/fetchers/javascript.d.ts +7 -0
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -0
- package/dist/cjs/fetchers/javascript.js +40 -0
- package/dist/cjs/fetchers/javascript.js.map +1 -0
- package/dist/cjs/fetchers/mariadb.d.ts +3 -0
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mariadb.js +23 -0
- package/dist/cjs/fetchers/mariadb.js.map +1 -0
- package/dist/cjs/fetchers/mongodb.d.ts +7 -0
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mongodb.js +52 -0
- package/dist/cjs/fetchers/mongodb.js.map +1 -0
- package/dist/cjs/fetchers/mysql.d.ts +3 -0
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -0
- package/dist/cjs/fetchers/mysql.js +30 -0
- package/dist/cjs/fetchers/mysql.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts +3 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -0
- package/dist/cjs/fetchers/postgresql.js +25 -0
- package/dist/cjs/fetchers/postgresql.js.map +1 -0
- package/dist/cjs/fetchers/redis.d.ts +6 -0
- package/dist/cjs/fetchers/redis.d.ts.map +1 -0
- package/dist/cjs/fetchers/redis.js +46 -0
- package/dist/cjs/fetchers/redis.js.map +1 -0
- package/dist/cjs/fetchers/redshift.d.ts +2 -0
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -0
- package/dist/cjs/fetchers/redshift.js +24 -0
- package/dist/cjs/fetchers/redshift.js.map +1 -0
- package/dist/cjs/fetchers/rest-api.d.ts +6 -0
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -0
- package/dist/cjs/fetchers/rest-api.js +58 -0
- package/dist/cjs/fetchers/rest-api.js.map +1 -0
- package/dist/cjs/fetchers/static-collection.d.ts +7 -0
- package/dist/cjs/fetchers/static-collection.d.ts.map +1 -0
- package/dist/cjs/fetchers/static-collection.js +24 -0
- package/dist/cjs/fetchers/static-collection.js.map +1 -0
- package/dist/cjs/fetchers/supabase.d.ts +7 -0
- package/dist/cjs/fetchers/supabase.d.ts.map +1 -0
- package/dist/cjs/fetchers/supabase.js +42 -0
- package/dist/cjs/fetchers/supabase.js.map +1 -0
- package/dist/cjs/fetchers/turso.d.ts +6 -0
- package/dist/cjs/fetchers/turso.d.ts.map +1 -0
- package/dist/cjs/fetchers/turso.js +25 -0
- package/dist/cjs/fetchers/turso.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +325 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts +5 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -0
- package/dist/cjs/pagination-plugin.js +1484 -0
- package/dist/cjs/pagination-plugin.js.map +1 -0
- package/dist/cjs/pagination-with-count.d.ts +6 -0
- package/dist/cjs/pagination-with-count.d.ts.map +1 -0
- package/dist/cjs/pagination-with-count.js +63 -0
- package/dist/cjs/pagination-with-count.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/cjs/utils.d.ts +31 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +763 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/cjs/validation.d.ts +5 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +29 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/array-mapper-pagination.d.ts +32 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -0
- package/dist/esm/array-mapper-pagination.js +72 -0
- package/dist/esm/array-mapper-pagination.js.map +1 -0
- package/dist/esm/count-fetchers.d.ts +12 -0
- package/dist/esm/count-fetchers.d.ts.map +1 -0
- package/dist/esm/count-fetchers.js +35 -0
- package/dist/esm/count-fetchers.js.map +1 -0
- package/dist/esm/data-source-fetchers.d.ts +14 -0
- package/dist/esm/data-source-fetchers.d.ts.map +1 -0
- package/dist/esm/data-source-fetchers.js +179 -0
- package/dist/esm/data-source-fetchers.js.map +1 -0
- package/dist/esm/fetchers/airtable.d.ts +6 -0
- package/dist/esm/fetchers/airtable.d.ts.map +1 -0
- package/dist/esm/fetchers/airtable.js +22 -0
- package/dist/esm/fetchers/airtable.js.map +1 -0
- package/dist/esm/fetchers/clickhouse.d.ts +6 -0
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/esm/fetchers/clickhouse.js +24 -0
- package/dist/esm/fetchers/clickhouse.js.map +1 -0
- package/dist/esm/fetchers/csv-file.d.ts +7 -0
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -0
- package/dist/esm/fetchers/csv-file.js +30 -0
- package/dist/esm/fetchers/csv-file.js.map +1 -0
- package/dist/esm/fetchers/firestore.d.ts +6 -0
- package/dist/esm/fetchers/firestore.d.ts.map +1 -0
- package/dist/esm/fetchers/firestore.js +30 -0
- package/dist/esm/fetchers/firestore.js.map +1 -0
- package/dist/esm/fetchers/google-sheets.d.ts +6 -0
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/esm/fetchers/google-sheets.js +25 -0
- package/dist/esm/fetchers/google-sheets.js.map +1 -0
- package/dist/esm/fetchers/index.d.ts +17 -0
- package/dist/esm/fetchers/index.d.ts.map +1 -0
- package/dist/esm/fetchers/index.js +17 -0
- package/dist/esm/fetchers/index.js.map +1 -0
- package/dist/esm/fetchers/javascript.d.ts +7 -0
- package/dist/esm/fetchers/javascript.d.ts.map +1 -0
- package/dist/esm/fetchers/javascript.js +34 -0
- package/dist/esm/fetchers/javascript.js.map +1 -0
- package/dist/esm/fetchers/mariadb.d.ts +3 -0
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -0
- package/dist/esm/fetchers/mariadb.js +18 -0
- package/dist/esm/fetchers/mariadb.js.map +1 -0
- package/dist/esm/fetchers/mongodb.d.ts +7 -0
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -0
- package/dist/esm/fetchers/mongodb.js +46 -0
- package/dist/esm/fetchers/mongodb.js.map +1 -0
- package/dist/esm/fetchers/mysql.d.ts +3 -0
- package/dist/esm/fetchers/mysql.d.ts.map +1 -0
- package/dist/esm/fetchers/mysql.js +25 -0
- package/dist/esm/fetchers/mysql.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts +3 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -0
- package/dist/esm/fetchers/postgresql.js +20 -0
- package/dist/esm/fetchers/postgresql.js.map +1 -0
- package/dist/esm/fetchers/redis.d.ts +6 -0
- package/dist/esm/fetchers/redis.d.ts.map +1 -0
- package/dist/esm/fetchers/redis.js +41 -0
- package/dist/esm/fetchers/redis.js.map +1 -0
- package/dist/esm/fetchers/redshift.d.ts +2 -0
- package/dist/esm/fetchers/redshift.d.ts.map +1 -0
- package/dist/esm/fetchers/redshift.js +20 -0
- package/dist/esm/fetchers/redshift.js.map +1 -0
- package/dist/esm/fetchers/rest-api.d.ts +6 -0
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -0
- package/dist/esm/fetchers/rest-api.js +53 -0
- package/dist/esm/fetchers/rest-api.js.map +1 -0
- package/dist/esm/fetchers/static-collection.d.ts +7 -0
- package/dist/esm/fetchers/static-collection.d.ts.map +1 -0
- package/dist/esm/fetchers/static-collection.js +18 -0
- package/dist/esm/fetchers/static-collection.js.map +1 -0
- package/dist/esm/fetchers/supabase.d.ts +7 -0
- package/dist/esm/fetchers/supabase.d.ts.map +1 -0
- package/dist/esm/fetchers/supabase.js +36 -0
- package/dist/esm/fetchers/supabase.js.map +1 -0
- package/dist/esm/fetchers/turso.d.ts +6 -0
- package/dist/esm/fetchers/turso.d.ts.map +1 -0
- package/dist/esm/fetchers/turso.js +20 -0
- package/dist/esm/fetchers/turso.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +306 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts +5 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -0
- package/dist/esm/pagination-plugin.js +1457 -0
- package/dist/esm/pagination-plugin.js.map +1 -0
- package/dist/esm/pagination-with-count.d.ts +6 -0
- package/dist/esm/pagination-with-count.d.ts.map +1 -0
- package/dist/esm/pagination-with-count.js +34 -0
- package/dist/esm/pagination-with-count.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/utils.d.ts +31 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +722 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/validation.d.ts +5 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +25 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +33 -0
- package/src/array-mapper-pagination.ts +113 -0
- package/src/count-fetchers.ts +99 -0
- package/src/data-source-fetchers.ts +313 -0
- package/src/fetchers/airtable.ts +153 -0
- package/src/fetchers/clickhouse.ts +127 -0
- package/src/fetchers/csv-file.ts +163 -0
- package/src/fetchers/firestore.ts +138 -0
- package/src/fetchers/google-sheets.ts +189 -0
- package/src/fetchers/index.ts +32 -0
- package/src/fetchers/javascript.ts +150 -0
- package/src/fetchers/mariadb.ts +230 -0
- package/src/fetchers/mongodb.ts +239 -0
- package/src/fetchers/mysql.ts +237 -0
- package/src/fetchers/postgresql.ts +247 -0
- package/src/fetchers/redis.ts +152 -0
- package/src/fetchers/redshift.ts +138 -0
- package/src/fetchers/rest-api.ts +148 -0
- package/src/fetchers/static-collection.ts +149 -0
- package/src/fetchers/supabase.ts +246 -0
- package/src/fetchers/turso.ts +131 -0
- package/src/index.ts +352 -0
- package/src/pagination-plugin.ts +2335 -0
- package/src/pagination-with-count.ts +89 -0
- package/src/utils.ts +1013 -0
- package/src/validation.ts +32 -0
- package/tsconfig.json +9 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UIDLDataSourceItemNode,
|
|
3
|
+
UIDLDataSourceListNode,
|
|
4
|
+
ChunkDefinition,
|
|
5
|
+
GeneratorOptions,
|
|
6
|
+
FileType,
|
|
7
|
+
UIDLDataSource,
|
|
8
|
+
DataSourceType,
|
|
9
|
+
ChunkType,
|
|
10
|
+
} from '@teleporthq/teleport-types'
|
|
11
|
+
import * as types from '@babel/types'
|
|
12
|
+
import { ASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
13
|
+
import { StringUtils } from '@teleporthq/teleport-shared'
|
|
14
|
+
import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
|
|
15
|
+
|
|
16
|
+
const VALID_DATA_SOURCE_TYPES: DataSourceType[] = [
|
|
17
|
+
'rest-api',
|
|
18
|
+
'postgresql',
|
|
19
|
+
'mysql',
|
|
20
|
+
'mariadb',
|
|
21
|
+
'amazon-redshift',
|
|
22
|
+
'mongodb',
|
|
23
|
+
'cockroachdb',
|
|
24
|
+
'tidb',
|
|
25
|
+
'redis',
|
|
26
|
+
'firestore',
|
|
27
|
+
'clickhouse',
|
|
28
|
+
'airtable',
|
|
29
|
+
'supabase',
|
|
30
|
+
'turso',
|
|
31
|
+
'javascript',
|
|
32
|
+
'google-sheets',
|
|
33
|
+
'csv-file',
|
|
34
|
+
'static-collection',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
export const sanitizeFileName = (input: string): string => {
|
|
38
|
+
if (!input || typeof input !== 'string') {
|
|
39
|
+
return 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
input
|
|
44
|
+
// Remove path traversal attempts
|
|
45
|
+
.replace(/\.\./g, '')
|
|
46
|
+
.replace(/[\/\\]/g, '-')
|
|
47
|
+
// Remove invalid filename characters
|
|
48
|
+
.replace(/[<>:"|?*\x00-\x1F]/g, '')
|
|
49
|
+
// Replace spaces with dashes
|
|
50
|
+
.replace(/\s+/g, '-')
|
|
51
|
+
// Remove leading/trailing dashes and dots
|
|
52
|
+
.replace(/^[-_.]+|[-_.]+$/g, '')
|
|
53
|
+
// Limit length to prevent filesystem issues
|
|
54
|
+
.substring(0, 200) || 'unknown'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const isNonEmptyString = (value: unknown): value is string => {
|
|
59
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const hoistLoadingFromRepeater = (dataSourceNode: any): void => {
|
|
63
|
+
if (!dataSourceNode || !dataSourceNode.content) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const children = dataSourceNode.children || []
|
|
68
|
+
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (child.type === 'cms-list-repeater' && child.content?.nodes?.loading) {
|
|
71
|
+
if (!dataSourceNode.content.nodes) {
|
|
72
|
+
dataSourceNode.content.nodes = {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
!dataSourceNode.content.nodes.loading &&
|
|
77
|
+
child.content.nodes.loading.content?.children?.length > 0
|
|
78
|
+
) {
|
|
79
|
+
dataSourceNode.content.nodes.loading = child.content.nodes.loading
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (dataSourceNode.content.nodes?.success?.content?.children) {
|
|
87
|
+
for (const child of dataSourceNode.content.nodes.success.content.children) {
|
|
88
|
+
if (child.type === 'cms-list-repeater' && child.content?.nodes?.loading) {
|
|
89
|
+
if (
|
|
90
|
+
!dataSourceNode.content.nodes.loading &&
|
|
91
|
+
child.content.nodes.loading.content?.children?.length > 0
|
|
92
|
+
) {
|
|
93
|
+
dataSourceNode.content.nodes.loading = child.content.nodes.loading
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// tslint:disable-next-line:no-any
|
|
103
|
+
export const validateResourceDefinition = (
|
|
104
|
+
resourceDefinition: any
|
|
105
|
+
): {
|
|
106
|
+
isValid: boolean
|
|
107
|
+
error?: string
|
|
108
|
+
} => {
|
|
109
|
+
if (!resourceDefinition || typeof resourceDefinition !== 'object') {
|
|
110
|
+
return { isValid: false, error: 'Resource definition is missing or invalid' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { dataSourceId, tableName, dataSourceType } = resourceDefinition
|
|
114
|
+
|
|
115
|
+
if (!isNonEmptyString(dataSourceId)) {
|
|
116
|
+
return { isValid: false, error: 'Data source ID is missing or invalid' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isNonEmptyString(dataSourceType)) {
|
|
120
|
+
return { isValid: false, error: 'Data source type is missing or invalid' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!VALID_DATA_SOURCE_TYPES.includes(dataSourceType as DataSourceType)) {
|
|
124
|
+
return { isValid: false, error: `Invalid data source type: ${dataSourceType}` }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// tableName is optional for some data sources (like REST API, JavaScript, etc.)
|
|
128
|
+
if (tableName !== undefined && !isNonEmptyString(tableName)) {
|
|
129
|
+
return { isValid: false, error: 'Table name must be a non-empty string when provided' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { isValid: true }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// tslint:disable-next-line:no-any
|
|
136
|
+
export const validateNodeContent = (content: any): { isValid: boolean; error?: string } => {
|
|
137
|
+
if (!content || typeof content !== 'object') {
|
|
138
|
+
return { isValid: false, error: 'Node content is missing or invalid' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!content.resourceDefinition) {
|
|
142
|
+
return { isValid: false, error: 'Resource definition is missing from node content' }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// key is optional - we can generate one from the resource definition if missing
|
|
146
|
+
|
|
147
|
+
return { isValid: true }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const validateDataSourceConfig = (
|
|
151
|
+
dataSource: UIDLDataSource
|
|
152
|
+
): { isValid: boolean; error?: string } => {
|
|
153
|
+
if (!dataSource || typeof dataSource !== 'object') {
|
|
154
|
+
return { isValid: false, error: 'Data source is missing or invalid' }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!isNonEmptyString(dataSource.id)) {
|
|
158
|
+
return { isValid: false, error: 'Data source ID is missing' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isNonEmptyString(dataSource.type)) {
|
|
162
|
+
return { isValid: false, error: 'Data source type is missing' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!dataSource.config || typeof dataSource.config !== 'object') {
|
|
166
|
+
return { isValid: false, error: 'Data source config is missing or invalid' }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { isValid: true }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const generateSafeFileName = (
|
|
173
|
+
dataSourceType: string,
|
|
174
|
+
tableName: string,
|
|
175
|
+
dataSourceId: string
|
|
176
|
+
): string => {
|
|
177
|
+
const sanitizedType = sanitizeFileName(dataSourceType)
|
|
178
|
+
const sanitizedTable = sanitizeFileName(tableName || 'data')
|
|
179
|
+
const sanitizedId = sanitizeFileName(dataSourceId)
|
|
180
|
+
|
|
181
|
+
// Create a unique identifier using parts of the dataSourceId
|
|
182
|
+
const shortId = sanitizedId.substring(0, 8)
|
|
183
|
+
|
|
184
|
+
const baseName = `${sanitizedType}-${sanitizedTable}-${shortId}`
|
|
185
|
+
return StringUtils.camelCaseToDashCase(baseName)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const extractDataSourceIntoNextAPIFolder = (
|
|
189
|
+
node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
|
|
190
|
+
dataSources: Record<string, UIDLDataSource>,
|
|
191
|
+
componentChunk: ChunkDefinition,
|
|
192
|
+
extractedResources: GeneratorOptions['extractedResources']
|
|
193
|
+
) => {
|
|
194
|
+
try {
|
|
195
|
+
// Validate node content structure
|
|
196
|
+
const contentValidation = validateNodeContent(node.content)
|
|
197
|
+
if (!contentValidation.isValid) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { resourceDefinition } = node.content
|
|
202
|
+
|
|
203
|
+
// Validate resource definition
|
|
204
|
+
const resourceValidation = validateResourceDefinition(resourceDefinition)
|
|
205
|
+
if (!resourceValidation.isValid) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { dataSourceId, tableName, dataSourceType } = resourceDefinition
|
|
210
|
+
|
|
211
|
+
// Check if dataSources object exists
|
|
212
|
+
if (!dataSources || typeof dataSources !== 'object') {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if data source exists
|
|
217
|
+
const dataSource = dataSources[dataSourceId]
|
|
218
|
+
if (!dataSource) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Validate data source configuration
|
|
223
|
+
const configValidation = validateDataSourceConfig(dataSource)
|
|
224
|
+
if (!configValidation.isValid) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if component chunk has meta and nodesLookup
|
|
229
|
+
if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Generate safe file name
|
|
234
|
+
const fileName = generateSafeFileName(dataSourceType, tableName || 'data', dataSourceId)
|
|
235
|
+
|
|
236
|
+
// Check if file name is valid
|
|
237
|
+
if (!fileName || fileName === 'unknown') {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Find JSX node by searching through nodesLookup
|
|
242
|
+
// The JSX generator creates keys like: ds-{dataSourceId}-{timestamp}
|
|
243
|
+
// We need to find the node that matches our resourceDefinition
|
|
244
|
+
// IMPORTANT: Match by both dataSourceId AND renderPropIdentifier to handle multiple DataProviders with same data source
|
|
245
|
+
let jsxNode: types.JSXElement | null = null
|
|
246
|
+
const targetRenderProp = node.content?.renderPropIdentifier
|
|
247
|
+
|
|
248
|
+
// tslint:disable-next-line:no-any
|
|
249
|
+
for (const jsxElement of Object.values(componentChunk.meta.nodesLookup)) {
|
|
250
|
+
// tslint:disable-next-line:no-any
|
|
251
|
+
if ((jsxElement as any).type === 'JSXElement') {
|
|
252
|
+
const attrs = (jsxElement as types.JSXElement).openingElement.attributes
|
|
253
|
+
|
|
254
|
+
// Look for resourceDefinition attribute
|
|
255
|
+
// tslint:disable-next-line:no-any
|
|
256
|
+
const resourceDefAttr = attrs.find(
|
|
257
|
+
(attr) =>
|
|
258
|
+
(attr as any).type === 'JSXAttribute' &&
|
|
259
|
+
(attr as types.JSXAttribute).name.name === 'resourceDefinition'
|
|
260
|
+
) as types.JSXAttribute | undefined
|
|
261
|
+
|
|
262
|
+
// Look for name attribute to match with renderPropIdentifier
|
|
263
|
+
// tslint:disable-next-line:no-any
|
|
264
|
+
const nameAttr = attrs.find(
|
|
265
|
+
(attr) =>
|
|
266
|
+
(attr as any).type === 'JSXAttribute' &&
|
|
267
|
+
(attr as types.JSXAttribute).name.name === 'name'
|
|
268
|
+
) as types.JSXAttribute | undefined
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
resourceDefAttr &&
|
|
272
|
+
resourceDefAttr.value &&
|
|
273
|
+
resourceDefAttr.value.type === 'JSXExpressionContainer'
|
|
274
|
+
) {
|
|
275
|
+
const expr = resourceDefAttr.value.expression
|
|
276
|
+
if (expr.type === 'ObjectExpression') {
|
|
277
|
+
// Check if this matches our resourceDefinition
|
|
278
|
+
const props = expr.properties as types.ObjectProperty[]
|
|
279
|
+
// tslint:disable-next-line:no-any
|
|
280
|
+
const idProp = props.find((p: any) => p.key?.value === 'dataSourceId')
|
|
281
|
+
// tslint:disable-next-line:no-any
|
|
282
|
+
const idValue = (idProp as any)?.value?.value
|
|
283
|
+
|
|
284
|
+
// Match by dataSourceId
|
|
285
|
+
const dataSourceMatches = idValue === dataSourceId
|
|
286
|
+
|
|
287
|
+
// Also check name prop matches renderPropIdentifier
|
|
288
|
+
let renderPropMatches = !targetRenderProp // If no targetRenderProp, don't check it
|
|
289
|
+
|
|
290
|
+
if (targetRenderProp && nameAttr && nameAttr.value) {
|
|
291
|
+
// The name attribute value is usually a StringLiteral or JSXExpressionContainer
|
|
292
|
+
if (nameAttr.value.type === 'StringLiteral') {
|
|
293
|
+
if (nameAttr.value.value === targetRenderProp) {
|
|
294
|
+
renderPropMatches = true
|
|
295
|
+
}
|
|
296
|
+
} else if (nameAttr.value.type === 'JSXExpressionContainer') {
|
|
297
|
+
const nameExpr = nameAttr.value.expression
|
|
298
|
+
if (nameExpr.type === 'StringLiteral' && nameExpr.value === targetRenderProp) {
|
|
299
|
+
renderPropMatches = true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Before matching, check if this node already has fetchData - if so, skip it
|
|
305
|
+
// This is crucial for handling multiple DataProviders with same dataSourceId and name
|
|
306
|
+
const alreadyHasFetchData = attrs.some(
|
|
307
|
+
(attr) => (attr as types.JSXAttribute).name?.name === 'fetchData'
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if (dataSourceMatches && renderPropMatches && !alreadyHasFetchData) {
|
|
311
|
+
jsxNode = jsxElement as types.JSXElement
|
|
312
|
+
break
|
|
313
|
+
} else if (dataSourceMatches && renderPropMatches && alreadyHasFetchData) {
|
|
314
|
+
// Continue searching for the next matching node without fetchData
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!jsxNode || jsxNode.type !== 'JSXElement') {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Ensure opening element and attributes exist
|
|
326
|
+
if (!jsxNode.openingElement || !Array.isArray(jsxNode.openingElement.attributes)) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if this node has already been processed (has fetchData attribute)
|
|
331
|
+
const existingFetchData = jsxNode.openingElement.attributes.find(
|
|
332
|
+
(attr) => (attr as types.JSXAttribute).name?.name === 'fetchData'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if (existingFetchData) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if there are resource params
|
|
340
|
+
const resourceParams = node.content.resource?.params || {}
|
|
341
|
+
const hasParams = Object.keys(resourceParams).length > 0
|
|
342
|
+
|
|
343
|
+
// Build resource path - use template literal if params exist
|
|
344
|
+
let resourcePath: types.StringLiteral | types.TemplateLiteral
|
|
345
|
+
if (hasParams) {
|
|
346
|
+
resourcePath = types.templateLiteral(
|
|
347
|
+
[
|
|
348
|
+
types.templateElement({ raw: `/api/${fileName}?`, cooked: `/api/${fileName}?` }),
|
|
349
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
350
|
+
],
|
|
351
|
+
[types.newExpression(types.identifier('URLSearchParams'), [types.identifier('params')])]
|
|
352
|
+
)
|
|
353
|
+
} else {
|
|
354
|
+
resourcePath = types.stringLiteral(`/api/${fileName}`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const fetchAST = types.callExpression(
|
|
358
|
+
types.memberExpression(
|
|
359
|
+
types.callExpression(types.identifier('fetch'), [
|
|
360
|
+
resourcePath,
|
|
361
|
+
types.objectExpression([
|
|
362
|
+
types.objectProperty(
|
|
363
|
+
types.identifier('headers'),
|
|
364
|
+
types.objectExpression([
|
|
365
|
+
types.objectProperty(
|
|
366
|
+
types.stringLiteral('Content-Type'),
|
|
367
|
+
types.stringLiteral('application/json')
|
|
368
|
+
),
|
|
369
|
+
])
|
|
370
|
+
),
|
|
371
|
+
]),
|
|
372
|
+
]),
|
|
373
|
+
types.identifier('then')
|
|
374
|
+
),
|
|
375
|
+
[
|
|
376
|
+
types.arrowFunctionExpression(
|
|
377
|
+
[types.identifier('res')],
|
|
378
|
+
types.callExpression(
|
|
379
|
+
types.memberExpression(types.identifier('res'), types.identifier('json')),
|
|
380
|
+
[]
|
|
381
|
+
)
|
|
382
|
+
),
|
|
383
|
+
]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const dataExpression = ASTUtils.generateMemberExpressionASTFromPath([
|
|
387
|
+
'response',
|
|
388
|
+
'data',
|
|
389
|
+
]) as types.OptionalMemberExpression
|
|
390
|
+
|
|
391
|
+
// If there are params, the arrow function should accept params
|
|
392
|
+
const resourceAST = types.arrowFunctionExpression(
|
|
393
|
+
hasParams ? [types.identifier('params')] : [],
|
|
394
|
+
types.callExpression(types.memberExpression(fetchAST, types.identifier('then'), false), [
|
|
395
|
+
types.arrowFunctionExpression([types.identifier('response')], dataExpression),
|
|
396
|
+
])
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
jsxNode.openingElement.attributes.unshift(
|
|
400
|
+
types.jSXAttribute(
|
|
401
|
+
types.jsxIdentifier('fetchData'),
|
|
402
|
+
types.jsxExpressionContainer(resourceAST)
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const hasPersistDataAttr = jsxNode.openingElement.attributes.some(
|
|
407
|
+
(attr) =>
|
|
408
|
+
attr.type === 'JSXAttribute' &&
|
|
409
|
+
(attr as types.JSXAttribute).name?.name === 'persistDataDuringLoading'
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if (!hasPersistDataAttr) {
|
|
413
|
+
jsxNode.openingElement.attributes.push(
|
|
414
|
+
types.jsxAttribute(
|
|
415
|
+
types.jsxIdentifier('persistDataDuringLoading'),
|
|
416
|
+
types.jsxExpressionContainer(types.booleanLiteral(true))
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Ensure extracted resources object exists
|
|
422
|
+
if (!extractedResources || typeof extractedResources !== 'object') {
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if a utils file already exists for this data source (from getStaticProps)
|
|
427
|
+
// If so, create an API route that re-exports handler from it to avoid code duplication
|
|
428
|
+
if (extractedResources[`utils/${fileName}`]) {
|
|
429
|
+
const apiRouteCode = `import dataSourceModule from '../../utils/data-sources/${fileName}'
|
|
430
|
+
|
|
431
|
+
export default dataSourceModule.handler
|
|
432
|
+
`
|
|
433
|
+
|
|
434
|
+
extractedResources[`api/${fileName}`] = {
|
|
435
|
+
fileName,
|
|
436
|
+
fileType: FileType.JS,
|
|
437
|
+
path: ['pages', 'api'],
|
|
438
|
+
content: apiRouteCode,
|
|
439
|
+
}
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Generate fetcher code with BOTH fetchData and handler
|
|
444
|
+
let fetcherCode: string
|
|
445
|
+
try {
|
|
446
|
+
fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '')
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
extractedResources[`api/${fileName}`] = {
|
|
452
|
+
fileName,
|
|
453
|
+
fileType: FileType.JS,
|
|
454
|
+
path: ['pages', 'api'],
|
|
455
|
+
content: fetcherCode,
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// Catch any unexpected errors to prevent plugin from crashing
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export const isEmbeddedDataSource = (dataSourceType: string): boolean => {
|
|
463
|
+
return ['javascript', 'csv-file', 'static-collection'].includes(dataSourceType)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export const replaceSecretReference = (
|
|
467
|
+
value: unknown,
|
|
468
|
+
options: { templateLiteral?: boolean } = {}
|
|
469
|
+
): string => {
|
|
470
|
+
const { templateLiteral = false } = options
|
|
471
|
+
// Handle null and undefined
|
|
472
|
+
if (value === null) {
|
|
473
|
+
return 'null'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (value === undefined) {
|
|
477
|
+
return 'undefined'
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Handle non-string values
|
|
481
|
+
if (typeof value !== 'string') {
|
|
482
|
+
try {
|
|
483
|
+
return JSON.stringify(value)
|
|
484
|
+
} catch (error) {
|
|
485
|
+
// Handle circular references or non-serializable values
|
|
486
|
+
return 'null'
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check if it's a secret reference
|
|
491
|
+
if (value.startsWith('teleporthq.secrets.')) {
|
|
492
|
+
const envVar = value.replace('teleporthq.secrets.', '')
|
|
493
|
+
|
|
494
|
+
// Validate environment variable name
|
|
495
|
+
// Must be alphanumeric, underscores, and start with letter or underscore
|
|
496
|
+
if (/^[A-Z_][A-Z0-9_]*$/i.test(envVar)) {
|
|
497
|
+
return templateLiteral ? `\${process.env.${envVar}}` : `process.env.${envVar}`
|
|
498
|
+
} else {
|
|
499
|
+
// Invalid env var name, use as regular string
|
|
500
|
+
return JSON.stringify(value)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Regular string value
|
|
505
|
+
try {
|
|
506
|
+
return JSON.stringify(value)
|
|
507
|
+
} catch (error) {
|
|
508
|
+
// Fallback for any serialization issues
|
|
509
|
+
return '""'
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const sanitizeNumericParam = (value: unknown, defaultValue: number = 0): number => {
|
|
514
|
+
if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
|
|
515
|
+
return Math.max(0, Math.floor(value))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (typeof value === 'string') {
|
|
519
|
+
const parsed = parseInt(value, 10)
|
|
520
|
+
if (!isNaN(parsed) && isFinite(parsed)) {
|
|
521
|
+
return Math.max(0, parsed)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return defaultValue
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export const sanitizePort = (port: unknown, defaultPort: number): number => {
|
|
529
|
+
const sanitized = sanitizeNumericParam(port, defaultPort)
|
|
530
|
+
// Ensure port is in valid range (1-65535)
|
|
531
|
+
if (sanitized < 1 || sanitized > 65535) {
|
|
532
|
+
return defaultPort
|
|
533
|
+
}
|
|
534
|
+
return sanitized
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export const isValidUrl = (url: unknown): boolean => {
|
|
538
|
+
if (typeof url !== 'string' || !url) {
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const parsed = new URL(url)
|
|
544
|
+
// Check for valid protocols
|
|
545
|
+
return ['http:', 'https:', 'mongodb:', 'redis:', 'postgresql:', 'mysql:'].includes(
|
|
546
|
+
parsed.protocol
|
|
547
|
+
)
|
|
548
|
+
} catch {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export const sanitizeIdentifier = (identifier: unknown): string => {
|
|
554
|
+
if (typeof identifier !== 'string' || !identifier) {
|
|
555
|
+
return ''
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Remove dangerous characters, only allow safe ones
|
|
559
|
+
return identifier.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 64)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// tslint:disable-next-line:no-any
|
|
563
|
+
export const extractDataSourceIntoGetStaticProps = (
|
|
564
|
+
node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
|
|
565
|
+
dataSources: Record<string, UIDLDataSource>,
|
|
566
|
+
componentChunk: ChunkDefinition,
|
|
567
|
+
getStaticPropsChunk: any,
|
|
568
|
+
chunks: any[],
|
|
569
|
+
extractedResources: GeneratorOptions['extractedResources'],
|
|
570
|
+
dependencies: Record<string, any>
|
|
571
|
+
): { success: boolean; chunk?: any } => {
|
|
572
|
+
try {
|
|
573
|
+
// Validate node content
|
|
574
|
+
const contentValidation = validateNodeContent(node.content)
|
|
575
|
+
if (!contentValidation.isValid) {
|
|
576
|
+
return { success: false }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const { resourceDefinition } = node.content
|
|
580
|
+
const resourceValidation = validateResourceDefinition(resourceDefinition)
|
|
581
|
+
if (!resourceValidation.isValid) {
|
|
582
|
+
return { success: false }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const { dataSourceId, tableName, dataSourceType } = resourceDefinition
|
|
586
|
+
|
|
587
|
+
if (!dataSources || typeof dataSources !== 'object') {
|
|
588
|
+
return { success: false }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const dataSource = dataSources[dataSourceId]
|
|
592
|
+
if (!dataSource) {
|
|
593
|
+
return { success: false }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const configValidation = validateDataSourceConfig(dataSource)
|
|
597
|
+
if (!configValidation.isValid) {
|
|
598
|
+
return { success: false }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
|
|
602
|
+
return { success: false }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Generate prop key first
|
|
606
|
+
const sanitizedDsName = StringUtils.dashCaseToCamelCase(
|
|
607
|
+
sanitizeFileName(dataSource.name || dataSourceId)
|
|
608
|
+
)
|
|
609
|
+
const sanitizedTableName = StringUtils.dashCaseToCamelCase(
|
|
610
|
+
sanitizeFileName(tableName || 'data')
|
|
611
|
+
)
|
|
612
|
+
const propKey = `${sanitizedDsName}_${sanitizedTableName}_data`
|
|
613
|
+
|
|
614
|
+
// Find ALL JSX nodes matching this dataSourceId AND tableName and add initialData to ALL of them
|
|
615
|
+
const matchingJsxNodes: types.JSXElement[] = []
|
|
616
|
+
|
|
617
|
+
// Helper function to recursively traverse the AST and find all matching JSXElements
|
|
618
|
+
const traverseAST = (astNode: any) => {
|
|
619
|
+
if (!astNode || typeof astNode !== 'object') {
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (astNode.type === 'JSXElement') {
|
|
624
|
+
const jsxElement = astNode as types.JSXElement
|
|
625
|
+
const attrs = jsxElement.openingElement.attributes
|
|
626
|
+
|
|
627
|
+
const resourceDefAttr = attrs.find(
|
|
628
|
+
(attr) =>
|
|
629
|
+
(attr as any).type === 'JSXAttribute' &&
|
|
630
|
+
(attr as types.JSXAttribute).name.name === 'resourceDefinition'
|
|
631
|
+
) as types.JSXAttribute | undefined
|
|
632
|
+
|
|
633
|
+
if (
|
|
634
|
+
resourceDefAttr &&
|
|
635
|
+
resourceDefAttr.value &&
|
|
636
|
+
resourceDefAttr.value.type === 'JSXExpressionContainer'
|
|
637
|
+
) {
|
|
638
|
+
const expr = resourceDefAttr.value.expression
|
|
639
|
+
if (expr.type === 'ObjectExpression') {
|
|
640
|
+
const props = expr.properties as types.ObjectProperty[]
|
|
641
|
+
// tslint:disable-next-line:no-any
|
|
642
|
+
const idProp = props.find((p: any) => p.key?.value === 'dataSourceId')
|
|
643
|
+
// tslint:disable-next-line:no-any
|
|
644
|
+
const idValue = (idProp as any)?.value?.value
|
|
645
|
+
|
|
646
|
+
// Also check tableName to ensure we're matching the right data source
|
|
647
|
+
// tslint:disable-next-line:no-any
|
|
648
|
+
const tableNameProp = props.find((p: any) => p.key?.value === 'tableName')
|
|
649
|
+
// tslint:disable-next-line:no-any
|
|
650
|
+
const tableNameValue = (tableNameProp as any)?.value?.value
|
|
651
|
+
|
|
652
|
+
if (idValue === dataSourceId && tableNameValue === tableName) {
|
|
653
|
+
matchingJsxNodes.push(jsxElement)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Recursively traverse all properties
|
|
660
|
+
for (const key in astNode) {
|
|
661
|
+
if (astNode.hasOwnProperty(key)) {
|
|
662
|
+
const value = astNode[key]
|
|
663
|
+
if (Array.isArray(value)) {
|
|
664
|
+
value.forEach((item) => traverseAST(item))
|
|
665
|
+
} else if (typeof value === 'object') {
|
|
666
|
+
traverseAST(value)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Traverse the entire component AST content
|
|
673
|
+
traverseAST(componentChunk.content)
|
|
674
|
+
|
|
675
|
+
if (matchingJsxNodes.length === 0) {
|
|
676
|
+
return { success: false }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Update ALL matching JSX nodes with initialData
|
|
680
|
+
for (const jsxNode of matchingJsxNodes) {
|
|
681
|
+
// For SSR/SSG with initialData, rename 'children' to 'renderSuccess'
|
|
682
|
+
const childrenAttrIndex = jsxNode.openingElement.attributes.findIndex(
|
|
683
|
+
(attr) => (attr as types.JSXAttribute).name?.name === 'children'
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
if (childrenAttrIndex !== -1) {
|
|
687
|
+
const childrenAttr = jsxNode.openingElement.attributes[
|
|
688
|
+
childrenAttrIndex
|
|
689
|
+
] as types.JSXAttribute
|
|
690
|
+
const renderSuccessAttr = types.jsxAttribute(
|
|
691
|
+
types.jsxIdentifier('renderSuccess'),
|
|
692
|
+
childrenAttr.value
|
|
693
|
+
)
|
|
694
|
+
jsxNode.openingElement.attributes[childrenAttrIndex] = renderSuccessAttr
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Remove existing initialData and persistDataDuringLoading attributes to avoid duplicates
|
|
698
|
+
jsxNode.openingElement.attributes = jsxNode.openingElement.attributes.filter(
|
|
699
|
+
(attr) =>
|
|
700
|
+
(attr as types.JSXAttribute).name?.name !== 'initialData' &&
|
|
701
|
+
(attr as types.JSXAttribute).name?.name !== 'persistDataDuringLoading'
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
// Add initialData attribute
|
|
705
|
+
const initialDataAttr = types.jsxAttribute(
|
|
706
|
+
types.jsxIdentifier('initialData'),
|
|
707
|
+
types.jsxExpressionContainer(
|
|
708
|
+
types.memberExpression(types.identifier('props'), types.identifier(propKey))
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
jsxNode.openingElement.attributes.push(initialDataAttr)
|
|
712
|
+
|
|
713
|
+
// Add persistDataDuringLoading={true}
|
|
714
|
+
const persistDataAttr = types.jsxAttribute(
|
|
715
|
+
types.jsxIdentifier('persistDataDuringLoading'),
|
|
716
|
+
types.jsxExpressionContainer(types.booleanLiteral(true))
|
|
717
|
+
)
|
|
718
|
+
jsxNode.openingElement.attributes.push(persistDataAttr)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Generate safe file name for the fetcher
|
|
722
|
+
const fileName = generateSafeFileName(dataSourceType, tableName || 'data', dataSourceId)
|
|
723
|
+
|
|
724
|
+
if (!fileName || fileName === 'unknown') {
|
|
725
|
+
return { success: false }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Generate fetcher code with core function
|
|
729
|
+
let fetcherCode: string
|
|
730
|
+
try {
|
|
731
|
+
fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '')
|
|
732
|
+
} catch (error) {
|
|
733
|
+
// tslint:disable-next-line:no-console
|
|
734
|
+
console.error(`Failed to generate fetcher for ${dataSourceType} (${dataSourceId}):`, error)
|
|
735
|
+
return { success: false }
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Ensure extracted resources object exists
|
|
739
|
+
if (!extractedResources || typeof extractedResources !== 'object') {
|
|
740
|
+
return { success: false }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Add the fetcher to utils folder (for server-side use)
|
|
744
|
+
// Use a unique key to avoid conflicts with API routes
|
|
745
|
+
extractedResources[`utils/${fileName}`] = {
|
|
746
|
+
fileName,
|
|
747
|
+
fileType: FileType.JS,
|
|
748
|
+
path: ['utils', 'data-sources'],
|
|
749
|
+
content: fetcherCode,
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Add dependency for the fetcher
|
|
753
|
+
const fetcherImportName = StringUtils.dashCaseToCamelCase(fileName)
|
|
754
|
+
dependencies[fetcherImportName] = {
|
|
755
|
+
type: 'local',
|
|
756
|
+
path: `../utils/data-sources/${fileName}`,
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Build params object from resource params
|
|
760
|
+
// tslint:disable-next-line:no-any
|
|
761
|
+
const resourceParams = (node.content as any).resource?.params || {}
|
|
762
|
+
const paramsProperties: types.ObjectProperty[] = []
|
|
763
|
+
|
|
764
|
+
// tslint:disable-next-line:no-any
|
|
765
|
+
Object.entries(resourceParams).forEach(([key, value]: [string, any]) => {
|
|
766
|
+
if (value.type === 'static') {
|
|
767
|
+
let astValue: any
|
|
768
|
+
|
|
769
|
+
if (value.content === null || value.content === undefined) {
|
|
770
|
+
astValue = types.nullLiteral()
|
|
771
|
+
} else if (Array.isArray(value.content)) {
|
|
772
|
+
// Handle array values (like queryColumns)
|
|
773
|
+
astValue = types.arrayExpression(
|
|
774
|
+
value.content.map((item: any) => {
|
|
775
|
+
if (typeof item === 'string') {
|
|
776
|
+
return types.stringLiteral(item)
|
|
777
|
+
}
|
|
778
|
+
if (typeof item === 'number') {
|
|
779
|
+
return types.numericLiteral(item)
|
|
780
|
+
}
|
|
781
|
+
if (typeof item === 'boolean') {
|
|
782
|
+
return types.booleanLiteral(item)
|
|
783
|
+
}
|
|
784
|
+
return types.nullLiteral()
|
|
785
|
+
})
|
|
786
|
+
)
|
|
787
|
+
} else if (typeof value.content === 'string') {
|
|
788
|
+
astValue = types.stringLiteral(value.content)
|
|
789
|
+
} else if (typeof value.content === 'number') {
|
|
790
|
+
astValue = types.numericLiteral(value.content)
|
|
791
|
+
} else if (typeof value.content === 'boolean') {
|
|
792
|
+
astValue = types.booleanLiteral(value.content)
|
|
793
|
+
} else {
|
|
794
|
+
astValue = types.nullLiteral()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
paramsProperties.push(types.objectProperty(types.stringLiteral(key), astValue))
|
|
798
|
+
}
|
|
799
|
+
// Note: We don't handle 'expr' or 'dynamic' params in getStaticProps
|
|
800
|
+
// as those should be detected by hasResourceDynamicParams check earlier
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
const fetchCallExpression = types.callExpression(
|
|
804
|
+
types.memberExpression(types.identifier(fetcherImportName), types.identifier('fetchData')),
|
|
805
|
+
[types.objectExpression(paramsProperties)]
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
const safeFetchExpression = createSafeFetchExpression(fetchCallExpression, propKey)
|
|
809
|
+
|
|
810
|
+
// Get response value path (if specified)
|
|
811
|
+
const responseMemberAST = node.content.valuePath
|
|
812
|
+
? ASTUtils.generateMemberExpressionASTFromPath([
|
|
813
|
+
propKey,
|
|
814
|
+
...ASTUtils.parseValuePath(node.content.valuePath),
|
|
815
|
+
])
|
|
816
|
+
: types.identifier(propKey)
|
|
817
|
+
|
|
818
|
+
// Create or update getStaticProps chunk
|
|
819
|
+
let tryBlock: types.TryStatement | null = null
|
|
820
|
+
|
|
821
|
+
if (!getStaticPropsChunk) {
|
|
822
|
+
tryBlock = types.tryStatement(
|
|
823
|
+
types.blockStatement([
|
|
824
|
+
types.returnStatement(
|
|
825
|
+
types.objectExpression([
|
|
826
|
+
types.objectProperty(
|
|
827
|
+
types.identifier('props'),
|
|
828
|
+
types.objectExpression([
|
|
829
|
+
types.objectProperty(types.identifier(propKey), responseMemberAST, false, false),
|
|
830
|
+
])
|
|
831
|
+
),
|
|
832
|
+
types.objectProperty(types.identifier('revalidate'), types.numericLiteral(1)),
|
|
833
|
+
])
|
|
834
|
+
),
|
|
835
|
+
]),
|
|
836
|
+
types.catchClause(
|
|
837
|
+
types.identifier('error'),
|
|
838
|
+
types.blockStatement([
|
|
839
|
+
types.expressionStatement(
|
|
840
|
+
types.callExpression(
|
|
841
|
+
types.memberExpression(types.identifier('console'), types.identifier('error')),
|
|
842
|
+
[types.stringLiteral('Error in getStaticProps:'), types.identifier('error')]
|
|
843
|
+
)
|
|
844
|
+
),
|
|
845
|
+
types.returnStatement(
|
|
846
|
+
types.objectExpression([
|
|
847
|
+
types.objectProperty(types.identifier('props'), types.objectExpression([])),
|
|
848
|
+
])
|
|
849
|
+
),
|
|
850
|
+
])
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
getStaticPropsChunk = {
|
|
855
|
+
name: 'getStaticProps',
|
|
856
|
+
type: ChunkType.AST,
|
|
857
|
+
fileType: FileType.JS,
|
|
858
|
+
content: types.exportNamedDeclaration(
|
|
859
|
+
types.functionDeclaration(
|
|
860
|
+
types.identifier('getStaticProps'),
|
|
861
|
+
[types.identifier('context')],
|
|
862
|
+
types.blockStatement([tryBlock]),
|
|
863
|
+
false,
|
|
864
|
+
true
|
|
865
|
+
)
|
|
866
|
+
),
|
|
867
|
+
linkAfter: ['jsx-component'],
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
chunks.push(getStaticPropsChunk)
|
|
871
|
+
} else {
|
|
872
|
+
// Update existing getStaticProps
|
|
873
|
+
const functionDeclaration = (getStaticPropsChunk.content as types.ExportNamedDeclaration)
|
|
874
|
+
.declaration as types.FunctionDeclaration
|
|
875
|
+
const functionBody = functionDeclaration.body.body
|
|
876
|
+
tryBlock = functionBody.find(
|
|
877
|
+
(subNode) => subNode.type === 'TryStatement'
|
|
878
|
+
) as types.TryStatement
|
|
879
|
+
|
|
880
|
+
if (!tryBlock) {
|
|
881
|
+
return { success: false }
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (!tryBlock) {
|
|
886
|
+
return { success: false }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Ensure props include current data source
|
|
890
|
+
const returnStatement: types.ReturnStatement = tryBlock.block.body.find(
|
|
891
|
+
(subNode) => subNode.type === 'ReturnStatement'
|
|
892
|
+
) as types.ReturnStatement
|
|
893
|
+
|
|
894
|
+
if (!returnStatement) {
|
|
895
|
+
return { success: false }
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const propsObject = (returnStatement.argument as types.ObjectExpression).properties.find(
|
|
899
|
+
(property) => ((property as types.ObjectProperty).key as types.Identifier).name === 'props'
|
|
900
|
+
) as types.ObjectProperty
|
|
901
|
+
|
|
902
|
+
const propsValue = propsObject.value as types.ObjectExpression
|
|
903
|
+
|
|
904
|
+
// Check if propKey already exists in the parallel fetch metadata
|
|
905
|
+
const parallelFetchMeta = getStaticPropsChunk.meta?.parallelFetch as
|
|
906
|
+
| ParallelFetchMeta
|
|
907
|
+
| undefined
|
|
908
|
+
const existingInFetchMeta = parallelFetchMeta?.names.includes(propKey)
|
|
909
|
+
|
|
910
|
+
// Always ensure both the fetch and the prop are in sync
|
|
911
|
+
if (!existingInFetchMeta) {
|
|
912
|
+
// Add to parallel fetch metadata
|
|
913
|
+
registerParallelFetch(getStaticPropsChunk, tryBlock, propKey, safeFetchExpression)
|
|
914
|
+
|
|
915
|
+
// Check if prop exists in return object
|
|
916
|
+
const existingProp = propsValue.properties.find(
|
|
917
|
+
(prop) =>
|
|
918
|
+
prop.type === 'ObjectProperty' &&
|
|
919
|
+
prop.key.type === 'Identifier' &&
|
|
920
|
+
(prop.key as types.Identifier).name === propKey
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
// Add prop if it doesn't exist
|
|
924
|
+
if (!existingProp) {
|
|
925
|
+
propsValue.properties.unshift(
|
|
926
|
+
types.objectProperty(types.identifier(propKey), responseMemberAST, false, false)
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return { success: true, chunk: getStaticPropsChunk }
|
|
932
|
+
} catch (error) {
|
|
933
|
+
return { success: false }
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
interface ParallelFetchMeta {
|
|
938
|
+
names: string[]
|
|
939
|
+
expressions: types.Expression[]
|
|
940
|
+
declaration?: types.VariableDeclaration
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const createSafeFetchExpression = (
|
|
944
|
+
fetchCallExpression: types.CallExpression,
|
|
945
|
+
label: string
|
|
946
|
+
): types.Expression => {
|
|
947
|
+
const errorIdentifier = types.identifier('error')
|
|
948
|
+
|
|
949
|
+
const catchHandler = types.arrowFunctionExpression(
|
|
950
|
+
[errorIdentifier],
|
|
951
|
+
types.blockStatement([
|
|
952
|
+
types.expressionStatement(
|
|
953
|
+
types.callExpression(
|
|
954
|
+
types.memberExpression(types.identifier('console'), types.identifier('error')),
|
|
955
|
+
[types.stringLiteral(`Error fetching ${label}:`), errorIdentifier]
|
|
956
|
+
)
|
|
957
|
+
),
|
|
958
|
+
types.returnStatement(types.arrayExpression([])),
|
|
959
|
+
])
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
return types.callExpression(
|
|
963
|
+
types.memberExpression(fetchCallExpression, types.identifier('catch')),
|
|
964
|
+
[catchHandler]
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const registerParallelFetch = (
|
|
969
|
+
getStaticPropsChunk: ChunkDefinition,
|
|
970
|
+
tryBlock: types.TryStatement,
|
|
971
|
+
propKey: string,
|
|
972
|
+
expression: types.Expression
|
|
973
|
+
) => {
|
|
974
|
+
if (!getStaticPropsChunk.meta) {
|
|
975
|
+
getStaticPropsChunk.meta = {}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const meta =
|
|
979
|
+
(getStaticPropsChunk.meta.parallelFetchData as ParallelFetchMeta) ??
|
|
980
|
+
((getStaticPropsChunk.meta.parallelFetchData = {
|
|
981
|
+
names: [],
|
|
982
|
+
expressions: [],
|
|
983
|
+
}) as ParallelFetchMeta)
|
|
984
|
+
|
|
985
|
+
meta.names.push(propKey)
|
|
986
|
+
meta.expressions.push(expression)
|
|
987
|
+
|
|
988
|
+
updateParallelFetchStatement(tryBlock, meta)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const updateParallelFetchStatement = (tryBlock: types.TryStatement, meta: ParallelFetchMeta) => {
|
|
992
|
+
if (meta.declaration) {
|
|
993
|
+
const existingIndex = tryBlock.block.body.indexOf(meta.declaration)
|
|
994
|
+
if (existingIndex !== -1) {
|
|
995
|
+
tryBlock.block.body.splice(existingIndex, 1)
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const promiseAllCall = types.awaitExpression(
|
|
1000
|
+
types.callExpression(
|
|
1001
|
+
types.memberExpression(types.identifier('Promise'), types.identifier('all')),
|
|
1002
|
+
[types.arrayExpression(meta.expressions.map((expression) => expression))]
|
|
1003
|
+
)
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
const arrayPattern = types.arrayPattern(meta.names.map((name) => types.identifier(name)))
|
|
1007
|
+
|
|
1008
|
+
meta.declaration = types.variableDeclaration('const', [
|
|
1009
|
+
types.variableDeclarator(arrayPattern, promiseAllCall),
|
|
1010
|
+
])
|
|
1011
|
+
|
|
1012
|
+
tryBlock.block.body.unshift(meta.declaration)
|
|
1013
|
+
}
|