@teleporthq/teleport-plugin-next-data-source 0.44.12 → 0.44.14

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 (89) hide show
  1. package/__tests__/browser-row-policy.test.ts +6 -6
  2. package/__tests__/fetchers.test.ts +1 -1
  3. package/__tests__/items-path.test.ts +3 -2
  4. package/__tests__/related-items-transform.test.ts +17 -0
  5. package/__tests__/search-relevance.test.ts +199 -52
  6. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  7. package/dist/cjs/fetchers/csv-file.js +1 -1
  8. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  9. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  10. package/dist/cjs/fetchers/google-sheets.js +1 -1
  11. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  12. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  13. package/dist/cjs/fetchers/javascript.js +1 -1
  14. package/dist/cjs/fetchers/javascript.js.map +1 -1
  15. package/dist/cjs/fetchers/mariadb.js +1 -1
  16. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  17. package/dist/cjs/fetchers/mysql.js +1 -1
  18. package/dist/cjs/fetchers/mysql.js.map +1 -1
  19. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/postgresql.js +3 -3
  21. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  22. package/dist/cjs/fetchers/redshift.js +1 -1
  23. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  24. package/dist/cjs/fetchers/rest-api.js +1 -1
  25. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  26. package/dist/cjs/fetchers/supabase.js +2 -2
  27. package/dist/cjs/fetchers/supabase.js.map +1 -1
  28. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/teleport.js +4 -4
  30. package/dist/cjs/fetchers/teleport.js.map +1 -1
  31. package/dist/cjs/fetchers/turso.js +1 -1
  32. package/dist/cjs/fetchers/turso.js.map +1 -1
  33. package/dist/cjs/search-relevance.d.ts +67 -26
  34. package/dist/cjs/search-relevance.d.ts.map +1 -1
  35. package/dist/cjs/search-relevance.js +102 -29
  36. package/dist/cjs/search-relevance.js.map +1 -1
  37. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  38. package/dist/cjs/transformations/blog-post.js +1 -1
  39. package/dist/cjs/transformations/blog-post.js.map +1 -1
  40. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  41. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  42. package/dist/esm/fetchers/csv-file.js +1 -1
  43. package/dist/esm/fetchers/csv-file.js.map +1 -1
  44. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  45. package/dist/esm/fetchers/google-sheets.js +1 -1
  46. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  47. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  48. package/dist/esm/fetchers/javascript.js +1 -1
  49. package/dist/esm/fetchers/javascript.js.map +1 -1
  50. package/dist/esm/fetchers/mariadb.js +2 -2
  51. package/dist/esm/fetchers/mariadb.js.map +1 -1
  52. package/dist/esm/fetchers/mysql.js +2 -2
  53. package/dist/esm/fetchers/mysql.js.map +1 -1
  54. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  55. package/dist/esm/fetchers/postgresql.js +4 -4
  56. package/dist/esm/fetchers/postgresql.js.map +1 -1
  57. package/dist/esm/fetchers/redshift.js +2 -2
  58. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  59. package/dist/esm/fetchers/rest-api.js +1 -1
  60. package/dist/esm/fetchers/rest-api.js.map +1 -1
  61. package/dist/esm/fetchers/supabase.js +3 -3
  62. package/dist/esm/fetchers/supabase.js.map +1 -1
  63. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  64. package/dist/esm/fetchers/teleport.js +5 -5
  65. package/dist/esm/fetchers/teleport.js.map +1 -1
  66. package/dist/esm/fetchers/turso.js +2 -2
  67. package/dist/esm/fetchers/turso.js.map +1 -1
  68. package/dist/esm/search-relevance.d.ts +67 -26
  69. package/dist/esm/search-relevance.d.ts.map +1 -1
  70. package/dist/esm/search-relevance.js +100 -27
  71. package/dist/esm/search-relevance.js.map +1 -1
  72. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  73. package/dist/esm/transformations/blog-post.js +1 -1
  74. package/dist/esm/transformations/blog-post.js.map +1 -1
  75. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +2 -2
  77. package/src/fetchers/csv-file.ts +6 -11
  78. package/src/fetchers/google-sheets.ts +6 -12
  79. package/src/fetchers/javascript.ts +5 -12
  80. package/src/fetchers/mariadb.ts +2 -2
  81. package/src/fetchers/mysql.ts +2 -2
  82. package/src/fetchers/postgresql.ts +9 -4
  83. package/src/fetchers/redshift.ts +2 -2
  84. package/src/fetchers/rest-api.ts +4 -11
  85. package/src/fetchers/supabase.ts +4 -4
  86. package/src/fetchers/teleport.ts +9 -4
  87. package/src/fetchers/turso.ts +2 -2
  88. package/src/search-relevance.ts +195 -61
  89. package/src/transformations/blog-post.ts +5 -1
@@ -213,7 +213,7 @@ const internal = { 'x-internal-data-secret': SECRET }
213
213
  const SORT_BY_NAME = JSON.stringify([{ field: 'name', order: 'asc' }])
214
214
 
215
215
  const mainQuery = (result: RouteResult): string =>
216
- (result.queries.find((q) => /^\s*SELECT \* FROM/i.test(q.sql)) || { sql: '' }).sql
216
+ (result.queries.find((q) => /^\s*SELECT (\S+\.)?\* FROM/i.test(q.sql)) || { sql: '' }).sql
217
217
 
218
218
  const POSTS = [
219
219
  {
@@ -325,24 +325,24 @@ describe('blog posts: a visitor reads the published posts, never an author email
325
325
  expect((await posts.run({ handler: 'getCount', query })).status).toBe(200)
326
326
  }
327
327
  const searched = await posts.run({ query: { query: 'ana', queryColumns } })
328
- expect(mainQuery(searched)).toContain('"title"::text ILIKE')
328
+ expect(mainQuery(searched)).toContain('lower("title"::text)')
329
329
 
330
330
  const admin = await posts.run({
331
331
  query: { query: 'ana', queryColumns },
332
332
  session: { role: 'admin' },
333
333
  })
334
- expect(mainQuery(admin)).toContain('"author_email"::text ILIKE')
334
+ expect(mainQuery(admin)).toContain('lower("author_email"::text)')
335
335
  })
336
336
 
337
337
  it('searches a visitor only through the columns it is served', async () => {
338
338
  const guest = await posts.run({ query: { query: 'ana' } })
339
339
  expect(guest.status).toBe(200)
340
340
  const sql = mainQuery(guest)
341
- expect(sql).toContain('"title"::text ILIKE')
341
+ expect(sql).toContain('lower("title"::text)')
342
342
  expect(sql).not.toContain('"author_email"')
343
343
 
344
344
  const admin = await posts.run({ query: { query: 'ana' }, session: { role: 'admin' } })
345
- expect(mainQuery(admin)).toContain('"author_email"::text ILIKE')
345
+ expect(mainQuery(admin)).toContain('lower("author_email"::text)')
346
346
  })
347
347
 
348
348
  it('applies to the generic Postgres route too', async () => {
@@ -484,7 +484,7 @@ describe("members: a page carries a member's public profile, never their account
484
484
 
485
485
  await users.pageFetch({ query: 'bo', queryColumns: JSON.stringify(['name', 'email']) })
486
486
  const searched = mainQuery({ queries: users.queries } as RouteResult)
487
- expect(searched).toContain('"name"::text ILIKE')
487
+ expect(searched).toContain('lower("name"::text)')
488
488
  expect(searched).not.toContain('"email"')
489
489
  })
490
490
 
@@ -337,7 +337,7 @@ describe('generateDataSourceFetcher', () => {
337
337
 
338
338
  expect(code).toContain('import { Client, types as pgTypes } from')
339
339
  expect(code).toContain('export default async function handler')
340
- expect(code).toContain('SELECT * FROM "users"')
340
+ expect(code).toContain(`'"users".*' : '*'} FROM "users"`)
341
341
  expect(code).toContain('localhost')
342
342
  })
343
343
 
@@ -108,9 +108,10 @@ describe.each<[string, FetcherSetup]>([
108
108
  setup.fetchImpl
109
109
  )
110
110
 
111
- // "row" and "2" each match somewhere in the name: row 2, row 12, row 20–25.
111
+ // "row" and "2" each match somewhere in the name: row 2, row 12, row 20–25;
112
+ // "2" as a whole word first, then at the start of one (20–25), then inside one (12).
112
113
  const data = result.data as { templates: Array<{ id: number }> }
113
- expect(data.templates.map((r) => r.id)).toEqual([2, 12, 20, 21, 22, 23, 24, 25])
114
+ expect(data.templates.map((r) => r.id)).toEqual([2, 20, 21, 22, 23, 24, 25, 12])
114
115
  })
115
116
 
116
117
  it('returns the payload untouched without the param', async () => {
@@ -174,6 +174,23 @@ describe('blog post transform — related posts', () => {
174
174
  expect(related.map((entry) => entry.id)).toEqual(['b'])
175
175
  expect(related[0].relatedPosts).toEqual([])
176
176
  })
177
+
178
+ it('carries no body in a related card; the post keeps its own', () => {
179
+ const built = buildBlogPost(
180
+ post('a', { content: '<p>Post a body</p>', related_post_ids: '["b"]' }),
181
+ {
182
+ relatedPostsById: mapOf([
183
+ post('b', { content: '<p>A long body nobody reads in a card</p>', excerpt: 'Short' }),
184
+ ]),
185
+ }
186
+ )
187
+
188
+ const related = built.relatedPosts as Array<Record<string, unknown>>
189
+ expect(built.content).toBe('<p>Post a body</p>')
190
+ expect(related[0].content).toBe('')
191
+ expect(related[0].excerpt).toBe('Short')
192
+ expect(related[0].readingTimeMinutes).toBe(1)
193
+ })
177
194
  })
178
195
 
179
196
  describe('the transform wrapper', () => {
@@ -3,14 +3,18 @@ import { parse } from '@babel/parser'
3
3
  import { generateDataSourceFetcher } from '../src/data-source-fetchers'
4
4
  import { generateTeleportCountFetcher } from '../src/fetchers/teleport'
5
5
  import { generateCSVFileFetcher } from '../src/fetchers/csv-file'
6
+ import { generateSearchRankHelperCode, generateSearchTermsCode } from '../src/search-relevance'
6
7
 
7
8
  /**
8
- * A list's search: every typed word must appear in one of the searched
9
- * columns, and the matches are ordered by where the words are — rows whose
10
- * first searched column (a title) holds every word, then rows whose second
11
- * one does, and so on, rows with the words spread over several columns last —
12
- * with the list's own sort deciding within each group. The order is part of
13
- * the query, so a page is cut from the ranked rows, never ranked after.
9
+ * A list's search: the typed text is split into terms at white space, the
10
+ * punctuation around them dropped; every term must appear in one of the
11
+ * searched columns. Postgres and the in-memory routes read the stored text
12
+ * like the typed text — tags, entities and punctuation as spaces — so markup
13
+ * never matches. The matches are ordered column by column: by how much of the
14
+ * typed text the title holds (a whole word 3, a word start 2, inside a word
15
+ * 1), then the summary, and so on, with the list's own sort among rows that
16
+ * score alike. The order is part of the query, so a page is cut from the
17
+ * ranked rows, never ranked after.
14
18
  */
15
19
 
16
20
  interface Query {
@@ -78,26 +82,71 @@ const runPgRoute = async (
78
82
  return queries.filter((recorded) => / FROM "?articles"?/.test(recorded.sql))
79
83
  }
80
84
 
81
- const titleMatch = (placeholders: string[], column: string) =>
82
- placeholders.map((ph) => `"${column}"::text ILIKE ${ph} ESCAPE '|'`).join(' AND ')
85
+ const searchText = (position: number) => `"__tq_search"."__tq_search_${position}"`
86
+ const like = (position: number, placeholder: string, before: string, after: string) =>
87
+ `${searchText(position)} LIKE ('${before}' || ${placeholder}::text || '${after}') ESCAPE '|'`
88
+ const columnScore = (position: number, placeholders: string[]) =>
89
+ '(' +
90
+ placeholders
91
+ .map(
92
+ (ph) =>
93
+ `CASE WHEN ${like(position, ph, '% ', ' %')} THEN 3` +
94
+ ` WHEN ${like(position, ph, '% ', '%')} THEN 2` +
95
+ ` WHEN ${like(position, ph, '%', '%')} THEN 1 ELSE 0 END`
96
+ )
97
+ .join(' + ') +
98
+ ') DESC'
83
99
 
84
100
  describe.each(['teleport', 'postgresql'] as const)('the %s list route', (type) => {
85
101
  const route = pgRoute(type)
102
+ const table = type === 'teleport' ? 'articles' : '"articles"'
86
103
 
87
104
  it('parses as valid JavaScript', () => {
88
105
  expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
89
106
  })
90
107
 
91
- it('needs every typed word, each in any searched column', async () => {
92
- const [main] = await runPgRoute(route, { query: 'Create form', queryColumns: SEARCHED })
108
+ it('needs every typed term, each in any searched column, case and punctuation aside', async () => {
109
+ const [main] = await runPgRoute(route, { query: 'Create "form?"', queryColumns: SEARCHED })
110
+ expect(main.sql).toContain(
111
+ `WHERE ((${like(0, '$1', '%', '%')} OR ${like(1, '$1', '%', '%')} OR ${like(
112
+ 2,
113
+ '$1',
114
+ '%',
115
+ '%'
116
+ )})` +
117
+ ` AND (${like(0, '$2', '%', '%')} OR ${like(1, '$2', '%', '%')} OR ${like(
118
+ 2,
119
+ '$2',
120
+ '%',
121
+ '%'
122
+ )}))`
123
+ )
124
+ expect(main.params).toEqual(['create', 'form'])
125
+ })
126
+
127
+ it("reads each searched column once per row, as text without markup or punctuation, and serves only the table's columns", async () => {
128
+ const [main] = await runPgRoute(route, { query: 'form', queryColumns: SEARCHED })
129
+ expect(main.sql).toMatch(
130
+ new RegExp(
131
+ `^SELECT ${table.replace(/"/g, '"')}\\.\\* FROM ${table} CROSS JOIN LATERAL \\(SELECT `
132
+ )
133
+ )
93
134
  expect(main.sql).toContain(
94
- `WHERE (("title"::text ILIKE $1 ESCAPE '|' OR "excerpt"::text ILIKE $1 ESCAPE '|' OR "content"::text ILIKE $1 ESCAPE '|')` +
95
- ` AND ("title"::text ILIKE $2 ESCAPE '|' OR "excerpt"::text ILIKE $2 ESCAPE '|' OR "content"::text ILIKE $2 ESCAPE '|'))`
135
+ `(' ' || regexp_replace(lower("title"::text), '<[^>]*>|&[a-z0-9#]+;|[]`
96
136
  )
97
- expect(main.params).toEqual(['%create%', '%form%'])
137
+ expect(main.sql).toContain(`, ' ', 'g') || ' ') AS "__tq_search_0", `)
138
+ expect(main.sql).toContain(` AS "__tq_search_2" OFFSET 0) AS "__tq_search" WHERE `)
98
139
  })
99
140
 
100
- it('orders title matches, then excerpt, then content, then spread words, before the sort and the page', async () => {
141
+ it('reads punctuation inside a term as a space, like the stored text', async () => {
142
+ const [main] = await runPgRoute(route, {
143
+ query: 'e-commerce what’s 50%',
144
+ queryColumns: SEARCHED,
145
+ })
146
+ expect(main.params).toEqual(['e commerce', 'what s', '50'])
147
+ })
148
+
149
+ it('orders by the title score, then the excerpt, then the content, before the sort and the page', async () => {
101
150
  const [main] = await runPgRoute(route, {
102
151
  query: 'create form',
103
152
  queryColumns: SEARCHED,
@@ -106,46 +155,51 @@ describe.each(['teleport', 'postgresql'] as const)('the %s list route', (type) =
106
155
  perPage: '12',
107
156
  page: '2',
108
157
  })
109
- const rank =
110
- `CASE WHEN ${titleMatch(['$1', '$2'], 'title')} THEN 0` +
111
- ` WHEN ${titleMatch(['$1', '$2'], 'excerpt')} THEN 1` +
112
- ` WHEN ${titleMatch(['$1', '$2'], 'content')} THEN 2 ELSE 3 END`
158
+ const rank = [0, 1, 2].map((position) => columnScore(position, ['$1', '$2'])).join(', ')
113
159
  expect(main.sql).toContain(` ORDER BY ${rank}, sort_order ASC NULLS LAST`)
114
160
  expect(main.sql).toMatch(/ LIMIT 12 OFFSET 12$/)
115
- expect(main.params).toEqual(['%create%', '%form%', 'published'])
161
+ expect(main.params).toEqual(['create', 'form', 'published'])
116
162
  })
117
163
 
118
- it('ranks the matches when the list has no sort of its own', async () => {
119
- const [main] = await runPgRoute(route, { query: 'form', queryColumns: SEARCHED })
164
+ it('ranks the matches when the list has no sort of its own, even over one column', async () => {
165
+ const [main] = await runPgRoute(route, {
166
+ query: 'form',
167
+ queryColumns: JSON.stringify(['title']),
168
+ })
120
169
  expect(main.sql).toMatch(
121
- / ORDER BY CASE WHEN "title"::text ILIKE \$1 ESCAPE '\|' THEN 0 .* ELSE 3 END(, "id" ASC)?$/
170
+ new RegExp(` ORDER BY \\(CASE WHEN .* THEN 3 .* ELSE 0 END\\) DESC(, "id" ASC)?$`)
122
171
  )
123
172
  })
124
173
 
125
- it('leaves the order alone without a search, or with one searched column', async () => {
126
- const sorts = JSON.stringify([{ field: 'sort_order', order: 'asc' }])
127
- const [plain] = await runPgRoute(route, { sorts })
128
- const [single] = await runPgRoute(route, {
129
- sorts,
130
- query: 'form',
131
- queryColumns: JSON.stringify(['title']),
174
+ it('leaves the query alone without a search', async () => {
175
+ const [plain] = await runPgRoute(route, {
176
+ sorts: JSON.stringify([{ field: 'sort_order', order: 'asc' }]),
132
177
  })
178
+ expect(plain.sql).toMatch(new RegExp(`^SELECT \\* FROM ${table}`))
179
+ expect(plain.sql).not.toContain('LATERAL')
133
180
  expect(plain.sql).not.toContain('CASE WHEN')
134
- expect(single.sql).not.toContain('CASE WHEN')
135
- expect(single.sql).toContain(' ORDER BY sort_order ASC NULLS LAST')
181
+ expect(plain.sql).toContain(' ORDER BY sort_order ASC NULLS LAST')
182
+ })
183
+
184
+ it('treats a search of only punctuation as no search', async () => {
185
+ const [main] = await runPgRoute(route, { query: ' ?! - ', queryColumns: SEARCHED })
186
+ expect(main.sql).not.toContain('LATERAL')
187
+ expect(main.params).toEqual([])
136
188
  })
137
189
  })
138
190
 
139
191
  describe('the built-in database count route', () => {
140
- it('counts the rows matching every word, without ranking them', async () => {
192
+ it('counts the rows matching every term, through the same texts, without ranking them', async () => {
141
193
  const [count] = await runPgRoute(
142
194
  pgRoute('teleport', true),
143
195
  { query: 'create form', queryColumns: SEARCHED },
144
196
  'getCount'
145
197
  )
146
- expect(count.sql).toMatch(/^SELECT COUNT\(\*\) FROM articles WHERE \(\(.*\) AND \(.*\)\)$/)
198
+ expect(count.sql).toMatch(
199
+ /^SELECT COUNT\(\*\) FROM articles CROSS JOIN LATERAL \(SELECT [\s\S]* OFFSET 0\) AS "__tq_search" WHERE \(\(.*\) AND \(.*\)\)$/
200
+ )
147
201
  expect(count.sql).not.toContain('CASE')
148
- expect(count.params).toEqual(['%create%', '%form%'])
202
+ expect(count.params).toEqual(['create', 'form'])
149
203
  })
150
204
  })
151
205
 
@@ -155,7 +209,7 @@ describe('the MySQL list route', () => {
155
209
  'articles'
156
210
  )
157
211
 
158
- it('binds the ranking patterns after every WHERE parameter, as the ORDER BY follows the WHERE', async () => {
212
+ it('binds the ranking terms after every WHERE parameter, as the ORDER BY follows the WHERE', async () => {
159
213
  const queries: Query[] = []
160
214
  const mysql = {
161
215
  escapeId: (name: string) => '`' + name + '`',
@@ -178,7 +232,7 @@ describe('the MySQL list route', () => {
178
232
  await handler(
179
233
  {
180
234
  query: {
181
- query: 'create form',
235
+ query: '"e-commerce" form?',
182
236
  queryColumns: JSON.stringify(['title', 'content']),
183
237
  filters: JSON.stringify([{ source: 'status', destination: 'published', operand: '=' }]),
184
238
  sorts: JSON.stringify([{ field: 'sort_order', order: 'asc' }]),
@@ -189,32 +243,121 @@ describe('the MySQL list route', () => {
189
243
  const [main] = queries
190
244
  const placeholders = (main.sql.match(/\?/g) || []).length
191
245
  expect(placeholders).toBe(main.params.length)
246
+ const title = "CONCAT(' ', LOWER(CAST(`title` AS CHAR)), ' ')"
192
247
  expect(main.sql).toContain(
193
- "ORDER BY CASE WHEN LOWER(CAST(`title` AS CHAR)) LIKE LOWER(?) ESCAPE '|' AND LOWER(CAST(`title` AS CHAR)) LIKE LOWER(?) ESCAPE '|' THEN 0"
248
+ `ORDER BY (CASE WHEN ${title} LIKE CONCAT('% ', ?, ' %') ESCAPE '|' THEN 3` +
249
+ ` WHEN ${title} LIKE CONCAT('% ', ?, '%') ESCAPE '|' THEN 2` +
250
+ ` WHEN ${title} LIKE CONCAT('%', ?, '%') ESCAPE '|' THEN 1 ELSE 0 END`
194
251
  )
195
- expect(main.sql).toContain('ELSE 2 END, `sort_order` ASC')
252
+ expect(main.sql).toContain('ELSE 0 END) DESC, `sort_order` ASC')
253
+ // The stored text is read raw here, so a term keeps the punctuation inside it.
254
+ const term = ['e-commerce', 'form']
196
255
  expect(main.params).toEqual([
197
- '%create%',
198
- '%create%',
199
- '%form%',
200
- '%form%',
256
+ term[0],
257
+ term[0],
258
+ term[1],
259
+ term[1],
201
260
  'published',
202
- '%create%',
203
- '%form%',
204
- '%create%',
205
- '%form%',
261
+ ...[term[0], term[0], term[0], term[1], term[1], term[1]],
262
+ ...[term[0], term[0], term[0], term[1], term[1], term[1]],
263
+ ])
264
+ })
265
+ })
266
+
267
+ const evaluate = (code: string, exports: string) =>
268
+ new Function(`${code}\nreturn { ${exports} }`)() as Record<
269
+ string,
270
+ (...args: unknown[]) => unknown
271
+ >
272
+
273
+ describe('the typed terms', () => {
274
+ const normalised = evaluate(generateSearchTermsCode(true), 'searchTerms').searchTerms
275
+ const raw = evaluate(generateSearchTermsCode(false), 'searchTerms').searchTerms
276
+
277
+ it('drops case, repeats and the punctuation around a term', () => {
278
+ expect(normalised(' How do I create a FORM? "create" ')).toEqual([
279
+ 'how',
280
+ 'do',
281
+ 'i',
282
+ 'create',
283
+ 'a',
284
+ 'form',
206
285
  ])
286
+ expect(raw('(reCAPTCHA), "form?"')).toEqual(['recaptcha', 'form'])
287
+ })
288
+
289
+ it('reads the punctuation inside a term as a space only where the stored text is read the same way', () => {
290
+ expect(normalised('e-commerce what’s Q&A')).toEqual(['e commerce', 'what s', 'q a'])
291
+ expect(raw('e-commerce what’s Q&A')).toEqual(['e-commerce', 'what’s', 'q&a'])
292
+ })
293
+
294
+ it('finds nothing to search in blank text or punctuation alone', () => {
295
+ expect(normalised('')).toEqual([])
296
+ expect(normalised(' - ? ')).toEqual([])
297
+ expect(normalised(undefined)).toEqual([])
298
+ expect(raw(' % _ ')).toEqual([])
299
+ })
300
+ })
301
+
302
+ describe('a list searched in memory', () => {
303
+ const helpers = evaluate(generateSearchRankHelperCode(), 'matchesSearch, rankBySearch')
304
+ const valueOf = (row: Record<string, unknown>, column: string) => row[column]
305
+
306
+ it("never matches a word that is only in a rich-text body's markup", () => {
307
+ const row = {
308
+ content:
309
+ '<p><strong>Tip:</strong> see <a href="https://x.io" rel="noopener">Q&amp;A</a></p><aside class="tq-callout" data-kind="note"><p>Done.</p></aside>',
310
+ }
311
+ const matches = (query: string) => helpers.matchesSearch(row, query, ['content'], valueOf)
312
+ expect(matches('strong')).toBe(false)
313
+ expect(matches('href')).toBe(false)
314
+ expect(matches('noopener')).toBe(false)
315
+ expect(matches('amp')).toBe(false)
316
+ expect(matches('callout')).toBe(false)
317
+ expect(matches('tip see')).toBe(true)
318
+ expect(matches('Q&A')).toBe(true)
319
+ expect(matches('done.')).toBe(true)
320
+ })
321
+
322
+ it('finds a typed apostrophe in either form', () => {
323
+ const row = { title: 'What’s new' }
324
+ expect(helpers.matchesSearch(row, "what's", ['title'], valueOf)).toBe(true)
325
+ expect(helpers.matchesSearch({ title: "What's new" }, 'what’s', ['title'], valueOf)).toBe(true)
326
+ })
327
+
328
+ it('puts whole words before word starts before parts of words, the title before the rest', () => {
329
+ const rows = [
330
+ { id: 1, title: 'Other platforms', content: 'create' },
331
+ { id: 2, title: 'Forms and more', content: 'create' },
332
+ { id: 3, title: 'Unrelated', content: 'create a form' },
333
+ { id: 4, title: 'How to create a form', content: '' },
334
+ { id: 5, title: 'Create pages', content: 'a form' },
335
+ ]
336
+ const ranked = helpers.rankBySearch(
337
+ rows,
338
+ 'create form',
339
+ ['title', 'content'],
340
+ valueOf
341
+ ) as Array<{
342
+ id: number
343
+ }>
344
+ expect(ranked.map((row) => row.id)).toEqual([4, 5, 2, 1, 3])
207
345
  })
208
346
  })
209
347
 
210
348
  describe('a list searched in memory (CSV file)', () => {
211
349
  const rows = [
212
- { id: 1, title: 'Publishing your site', excerpt: 'Go live', content: 'Create a form first' },
350
+ {
351
+ id: 1,
352
+ title: 'Publishing your site',
353
+ excerpt: 'Go live',
354
+ content: '<p>Create a <strong>form</strong> first</p>',
355
+ },
213
356
  { id: 2, title: 'Forms', excerpt: 'How to create a form', content: '' },
214
357
  { id: 3, title: 'How to create a form', excerpt: '', content: '' },
215
358
  { id: 4, title: 'Create pages', excerpt: 'A form on a page', content: '' },
216
359
  { id: 5, title: 'Create a contact form', excerpt: '', content: '' },
217
- { id: 6, title: 'Unrelated', excerpt: '', content: '' },
360
+ { id: 6, title: 'Unrelated', excerpt: '', content: '<p><strong>Strong</strong> words</p>' },
218
361
  ]
219
362
  const code = generateCSVFileFetcher({
220
363
  parsedData: rows,
@@ -238,13 +381,13 @@ describe('a list searched in memory (CSV file)', () => {
238
381
  return payload.data.map((row) => row.id)
239
382
  }
240
383
 
241
- it('ranks title, then excerpt, then content, then spread words; the sort within each', async () => {
384
+ it('ranks by the title, then the excerpt, then the content; the sort among rows that score alike', async () => {
242
385
  const ids = await run({
243
- query: 'create form',
386
+ query: 'create form?',
244
387
  queryColumns: SEARCHED,
245
388
  sorts: JSON.stringify([{ field: 'id', order: 'desc' }]),
246
389
  })
247
- expect(ids).toEqual([5, 3, 2, 1, 4])
390
+ expect(ids).toEqual([5, 3, 4, 2, 1])
248
391
  })
249
392
 
250
393
  it('cuts the page from the ranked rows', async () => {
@@ -255,6 +398,10 @@ describe('a list searched in memory (CSV file)', () => {
255
398
  perPage: '2',
256
399
  page: '2',
257
400
  })
258
- expect(ids).toEqual([2, 1])
401
+ expect(ids).toEqual([4, 2])
402
+ })
403
+
404
+ it('matches the text of a rich-text column, not its tags', async () => {
405
+ expect(await run({ query: 'strong', queryColumns: SEARCHED })).toEqual([6])
259
406
  })
260
407
  })
@@ -1 +1 @@
1
- {"version":3,"file":"csv-file.d.ts","sourceRoot":"","sources":["../../../src/fetchers/csv-file.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,iBAAiB,WACpB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CA8BpC,CAAA;AAkKD,eAAO,MAAM,sBAAsB,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MA6IxE,CAAA;AAGD,eAAO,MAAM,uBAAuB,YAAa,GAAG,KAAG,MA+CtD,CAAA"}
1
+ {"version":3,"file":"csv-file.d.ts","sourceRoot":"","sources":["../../../src/fetchers/csv-file.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,iBAAiB,WACpB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CA8BpC,CAAA;AA6JD,eAAO,MAAM,sBAAsB,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MA6IxE,CAAA;AAGD,eAAO,MAAM,uBAAuB,YAAa,GAAG,KAAG,MA+CtD,CAAA"}
@@ -32,7 +32,7 @@ var validateCSVConfig = function (config) {
32
32
  };
33
33
  exports.validateCSVConfig = validateCSVConfig;
34
34
  var generateHandlerBody = function () {
35
- return "\n try {\n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n \n const labelToIdMap = {}\n columns.forEach((col) => {\n if (col.label && col.id) {\n labelToIdMap[col.label] = col.id\n }\n })\n \n let filteredData = [...data]\n \n if (query) {\n const searchTerms = query.toLowerCase().split(' ').filter(Boolean)\n \n if (queryColumns) {\n const searchColumns = safeJSONParse(queryColumns)\n filteredData = filteredData.filter((item) => {\n return searchTerms.every((term) => searchColumns.some((col) => {\n const field = labelToIdMap[col] || col\n const value = getNestedValue(item, field)\n return value && String(value).toLowerCase().includes(term)\n }))\n })\n } else {\n filteredData = filteredData.filter((item) => {\n try {\n const stringified = JSON.stringify(item).toLowerCase()\n return searchTerms.every((term) => stringified.includes(term))\n } catch {\n return false\n }\n })\n }\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n filteredData = filteredData.filter((item) =>\n evaluateFilterTree(filterTree, (condition) => {\n const field = labelToIdMap[condition.source] || condition.source\n const value = getNestedValue(item, field)\n const target = condition.destination\n const operand = condition.operand\n \n if (Array.isArray(target)) {\n if (operand === '!=') {\n return !target.includes(value)\n }\n return target.includes(value)\n }\n \n return compareValues(value, target, operand)\n })\n )\n }\n }\n \n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n filteredData.sort((a, b) => {\n for (const sort of parsedSorts) {\n if (!sort.field) continue\n const field = labelToIdMap[sort.field] || sort.field\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n if (comparison !== 0) return comparison * sortOrderValue\n }\n return 0\n })\n }\n } else if (sortBy) {\n const field = labelToIdMap[sortBy] || sortBy\n filteredData.sort((a, b) => {\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n return comparison * sortOrderValue\n })\n }\n \n if (query && queryColumns) {\n filteredData = rankBySearch(filteredData, query, safeJSONParse(queryColumns), (item, col) =>\n getNestedValue(item, labelToIdMap[col] || col)\n )\n }\n \n const limitValue = limit || perPage\n const offsetValue = offsetParam !== undefined ? parseInt(offsetParam) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : 0)\n \n if (limitValue) {\n filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))\n }\n \n const safeData = JSON.parse(JSON.stringify(filteredData, dateReplacer))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('CSV fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n }\n";
35
+ return "\n try {\n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n \n const labelToIdMap = {}\n columns.forEach((col) => {\n if (col.label && col.id) {\n labelToIdMap[col.label] = col.id\n }\n })\n \n let filteredData = [...data]\n \n if (query) {\n if (queryColumns) {\n const searchColumns = safeJSONParse(queryColumns)\n filteredData = filteredData.filter((item) =>\n matchesSearch(item, query, searchColumns, (row, col) => getNestedValue(row, labelToIdMap[col] || col))\n )\n } else {\n const terms = searchTerms(query)\n filteredData = filteredData.filter((item) => {\n try {\n const text = searchText(item)\n return terms.every((term) => text.indexOf(term) !== -1)\n } catch {\n return false\n }\n })\n }\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n filteredData = filteredData.filter((item) =>\n evaluateFilterTree(filterTree, (condition) => {\n const field = labelToIdMap[condition.source] || condition.source\n const value = getNestedValue(item, field)\n const target = condition.destination\n const operand = condition.operand\n \n if (Array.isArray(target)) {\n if (operand === '!=') {\n return !target.includes(value)\n }\n return target.includes(value)\n }\n \n return compareValues(value, target, operand)\n })\n )\n }\n }\n \n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n filteredData.sort((a, b) => {\n for (const sort of parsedSorts) {\n if (!sort.field) continue\n const field = labelToIdMap[sort.field] || sort.field\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n if (comparison !== 0) return comparison * sortOrderValue\n }\n return 0\n })\n }\n } else if (sortBy) {\n const field = labelToIdMap[sortBy] || sortBy\n filteredData.sort((a, b) => {\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n return comparison * sortOrderValue\n })\n }\n \n if (query && queryColumns) {\n filteredData = rankBySearch(filteredData, query, safeJSONParse(queryColumns), (item, col) =>\n getNestedValue(item, labelToIdMap[col] || col)\n )\n }\n \n const limitValue = limit || perPage\n const offsetValue = offsetParam !== undefined ? parseInt(offsetParam) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : 0)\n \n if (limitValue) {\n filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))\n }\n \n const safeData = JSON.parse(JSON.stringify(filteredData, dateReplacer))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('CSV fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n }\n";
36
36
  };
37
37
  var generateCSVFileFetcher = function (config) {
38
38
  var csvConfig = config;
@@ -1 +1 @@
1
- {"version":3,"file":"csv-file.js","sourceRoot":"","sources":["../../../src/fetchers/csv-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"csv-file.js","sourceRoot":"","sources":["../../../src/fetchers/csv-file.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"google-sheets.d.ts","sourceRoot":"","sources":["../../../src/fetchers/google-sheets.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,0BAA0B,WAC7B,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAwBpC,CAAA;AAcD,eAAO,MAAM,2BAA2B,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MAsW7E,CAAA"}
1
+ {"version":3,"file":"google-sheets.d.ts","sourceRoot":"","sources":["../../../src/fetchers/google-sheets.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,0BAA0B,WAC7B,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAwBpC,CAAA;AAcD,eAAO,MAAM,2BAA2B,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MAgW7E,CAAA"}
@@ -30,7 +30,7 @@ var generateGoogleSheetsFetcher = function (config) {
30
30
  var autoDetectHeader = sheetsConfig.autoDetectHeader;
31
31
  var firstRowIsHeader = sheetsConfig.firstRowIsHeader;
32
32
  var configColumns = sheetsConfig.columns || [];
33
- return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, search_relevance_1.generateSearchRankHelperCode)(), "\n\n").concat((0, utils_1.generateSortFilterHelperCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, header_detection_1.generateHeaderDetectionCode)(), "\n\nexport default async function handler(req, res) {\n try {\n const sheetUrl = ").concat(JSON.stringify(sheetsConfig.sheetUrl), "\n let sheetId = ").concat(JSON.stringify(sheetsConfig.sheetId), "\n const range = ").concat(JSON.stringify(sheetsConfig.range || 'A1:Z1000'), "\n const maxRows = ").concat(sheetsConfig.maxRows || 0, "\n const autoDetectHeader = ").concat(autoDetectHeader !== undefined ? autoDetectHeader : true, "\n const firstRowIsHeader = ").concat(firstRowIsHeader !== undefined ? firstRowIsHeader : true, "\n \n if (!sheetId && sheetUrl) {\n const match = sheetUrl.match(/\\/d\\/([a-zA-Z0-9-_]+)/)\n sheetId = match ? match[1] : undefined\n }\n \n if (!sheetId) {\n return res.status(400).json({\n success: false,\n error: 'Invalid Google Sheets URL or Sheet ID',\n timestamp: Date.now()\n })\n }\n \n let gid = undefined\n if (sheetUrl) {\n const gidMatch = sheetUrl.match(/[#&]gid=([0-9]+)/)\n gid = gidMatch ? gidMatch[1] : undefined\n }\n \n let url = `https://docs.google.com/spreadsheets/d/${sheetId}/gviz/tq?tqx=out:json&range=${range}`\n \n if (gid) {\n url += `&gid=${gid}`\n }\n \n if (maxRows && maxRows > 0) {\n url += `&tq=limit ${maxRows}`\n }\n \n const response = await fetch(url)\n \n if (!response.ok) {\n return res.status(response.status).json({\n success: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n timestamp: Date.now()\n })\n }\n \n const text = await response.text()\n const jsonMatch = text.match(/google\\.visualization\\.Query\\.setResponse\\((.*)\\);/)\n \n if (!jsonMatch) {\n return res.status(500).json({\n success: false,\n error: 'Unable to parse Google Sheets response',\n timestamp: Date.now()\n })\n }\n \n const data = JSON.parse(jsonMatch[1])\n \n if (data.status === 'error') {\n return res.status(500).json({\n success: false,\n error: data.errors?.[0]?.detailed_message || 'Failed to fetch Google Sheets data',\n timestamp: Date.now()\n })\n }\n \n const formatDateValue = (date) => {\n const options = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n }\n \n const timeOptions = {\n hour: '2-digit',\n minute: '2-digit',\n }\n \n const hasTime = date.getHours() !== 0 || date.getMinutes() !== 0 || date.getSeconds() !== 0\n \n if (hasTime) {\n return date.toLocaleString('en-US', { ...options, ...timeOptions })\n }\n \n return date.toLocaleDateString('en-US', options)\n }\n \n const parseGoogleSheetsValue = (value) => {\n if (typeof value === 'string') {\n const dateMatch = value.match(/^Date\\((\\d+),(\\d+),(\\d+)(?:,(\\d+),(\\d+),(\\d+))?\\)$/)\n if (dateMatch) {\n const year = parseInt(dateMatch[1], 10)\n const month = parseInt(dateMatch[2], 10)\n const day = parseInt(dateMatch[3], 10)\n const hour = dateMatch[4] ? parseInt(dateMatch[4], 10) : 0\n const minute = dateMatch[5] ? parseInt(dateMatch[5], 10) : 0\n const second = dateMatch[6] ? parseInt(dateMatch[6], 10) : 0\n const date = new Date(year, month, day, hour, minute, second)\n return formatDateValue(date)\n }\n }\n return value\n }\n \n const table = data.table\n const rawRows = table.rows || []\n \n if (rawRows.length === 0) {\n return res.status(200).json({\n success: true,\n data: [],\n timestamp: Date.now()\n })\n }\n \n const firstRow = rawRows[0]\n const firstRowValues = firstRow.c.map((cell) => cell?.v ?? cell?.f ?? null)\n \n const shouldAutoDetect = autoDetectHeader !== false\n let hasHeaderRow = false\n \n if (shouldAutoDetect) {\n hasHeaderRow = detectHeaderRow(firstRowValues, rawRows)\n } else {\n hasHeaderRow = firstRowIsHeader !== false\n }\n \n const configColumns = ").concat(JSON.stringify(configColumns), "\n const useConfigColumns = configColumns && configColumns.length > 0\n \n let columns\n let dataRows\n \n if (hasHeaderRow && rawRows.length > 1) {\n columns = table.cols.map((col, index) => {\n const configCol = useConfigColumns ? configColumns[index] : null\n return {\n id: configCol?.id || col.id || `col_${index}`,\n label: String(firstRowValues[index] || col.label || configCol?.label || `Column ${index + 1}`),\n type: configCol?.type || col.type || 'string'\n }\n })\n dataRows = rawRows.slice(1)\n } else {\n columns = table.cols.map((col, index) => {\n const configCol = useConfigColumns ? configColumns[index] : null\n return {\n id: configCol?.id || col.id || `col_${index}`,\n label: configCol?.label || col.label || `Column ${index + 1}`,\n type: configCol?.type || col.type || 'string'\n }\n })\n dataRows = rawRows\n }\n \n if (maxRows && dataRows.length > maxRows) {\n dataRows = dataRows.slice(0, maxRows)\n }\n \n const rows = dataRows.map((row) => {\n const rowData = {}\n row.c.forEach((cell, index) => {\n const columnId = columns[index]?.id || `col_${index}`\n const rawValue = cell?.v ?? cell?.f ?? null\n rowData[columnId] = parseGoogleSheetsValue(rawValue)\n })\n return rowData\n })\n \n const columnsWithData = columns.filter((col, index) => {\n const hasHeaderData = col.label && col.label !== `Column ${index + 1}`\n const hasDataInColumn = rows.some((row) => {\n const value = row[col.id]\n return value !== null && value !== undefined && value !== ''\n })\n return hasHeaderData || hasDataInColumn\n })\n \n const filteredRows = rows.map((row) => {\n const filteredRow = {}\n columnsWithData.forEach((col) => {\n filteredRow[col.id] = row[col.id]\n })\n return filteredRow\n })\n \n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n \n // Create label-to-ID mapping for filters and sorts\n const labelToIdMap = {}\n columnsWithData.forEach((col) => {\n labelToIdMap[col.label] = col.id\n })\n \n let filteredData = [...filteredRows]\n \n if (query) {\n const searchTerms = query.toLowerCase().split(' ').filter(Boolean)\n \n if (queryColumns) {\n const searchColumns = safeJSONParse(queryColumns)\n filteredData = filteredData.filter((item) => {\n return searchTerms.every((term) => searchColumns.some((col) => {\n // Map label to column ID\n const field = labelToIdMap[col] || col\n const value = getNestedValue(item, field)\n return value && String(value).toLowerCase().includes(term)\n }))\n })\n } else {\n filteredData = filteredData.filter((item) => {\n try {\n const stringified = JSON.stringify(item).toLowerCase()\n return searchTerms.every((term) => stringified.includes(term))\n } catch {\n return false\n }\n })\n }\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n filteredData = filteredData.filter((item) =>\n evaluateFilterTree(filterTree, (condition) => {\n // Map label to column ID\n const field = labelToIdMap[condition.source] || condition.source\n const value = getNestedValue(item, field)\n const target = condition.destination\n const operand = condition.operand\n \n if (Array.isArray(target)) {\n if (operand === '!=') {\n return !target.includes(value)\n }\n return target.includes(value)\n }\n \n return compareValues(value, target, operand)\n })\n )\n }\n }\n \n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n filteredData.sort((a, b) => {\n for (const sort of parsedSorts) {\n if (!sort.field) continue\n // Map label to column ID\n const field = labelToIdMap[sort.field] || sort.field\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n if (comparison !== 0) return comparison * sortOrderValue\n }\n return 0\n })\n }\n } else if (sortBy) {\n filteredData.sort((a, b) => {\n // Map label to column ID\n const field = labelToIdMap[sortBy] || sortBy\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n return comparison * sortOrderValue\n })\n }\n \n if (query && queryColumns) {\n filteredData = rankBySearch(filteredData, query, safeJSONParse(queryColumns), (item, col) =>\n getNestedValue(item, labelToIdMap[col] || col)\n )\n }\n \n const limitValue = limit || perPage\n const offsetValue = offsetParam !== undefined ? parseInt(offsetParam) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : 0)\n \n if (limitValue) {\n filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))\n }\n \n const safeData = JSON.parse(JSON.stringify(filteredData))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Google Sheets fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n }\n}\n");
33
+ return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, search_relevance_1.generateSearchRankHelperCode)(), "\n\n").concat((0, utils_1.generateSortFilterHelperCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, header_detection_1.generateHeaderDetectionCode)(), "\n\nexport default async function handler(req, res) {\n try {\n const sheetUrl = ").concat(JSON.stringify(sheetsConfig.sheetUrl), "\n let sheetId = ").concat(JSON.stringify(sheetsConfig.sheetId), "\n const range = ").concat(JSON.stringify(sheetsConfig.range || 'A1:Z1000'), "\n const maxRows = ").concat(sheetsConfig.maxRows || 0, "\n const autoDetectHeader = ").concat(autoDetectHeader !== undefined ? autoDetectHeader : true, "\n const firstRowIsHeader = ").concat(firstRowIsHeader !== undefined ? firstRowIsHeader : true, "\n \n if (!sheetId && sheetUrl) {\n const match = sheetUrl.match(/\\/d\\/([a-zA-Z0-9-_]+)/)\n sheetId = match ? match[1] : undefined\n }\n \n if (!sheetId) {\n return res.status(400).json({\n success: false,\n error: 'Invalid Google Sheets URL or Sheet ID',\n timestamp: Date.now()\n })\n }\n \n let gid = undefined\n if (sheetUrl) {\n const gidMatch = sheetUrl.match(/[#&]gid=([0-9]+)/)\n gid = gidMatch ? gidMatch[1] : undefined\n }\n \n let url = `https://docs.google.com/spreadsheets/d/${sheetId}/gviz/tq?tqx=out:json&range=${range}`\n \n if (gid) {\n url += `&gid=${gid}`\n }\n \n if (maxRows && maxRows > 0) {\n url += `&tq=limit ${maxRows}`\n }\n \n const response = await fetch(url)\n \n if (!response.ok) {\n return res.status(response.status).json({\n success: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n timestamp: Date.now()\n })\n }\n \n const text = await response.text()\n const jsonMatch = text.match(/google\\.visualization\\.Query\\.setResponse\\((.*)\\);/)\n \n if (!jsonMatch) {\n return res.status(500).json({\n success: false,\n error: 'Unable to parse Google Sheets response',\n timestamp: Date.now()\n })\n }\n \n const data = JSON.parse(jsonMatch[1])\n \n if (data.status === 'error') {\n return res.status(500).json({\n success: false,\n error: data.errors?.[0]?.detailed_message || 'Failed to fetch Google Sheets data',\n timestamp: Date.now()\n })\n }\n \n const formatDateValue = (date) => {\n const options = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n }\n \n const timeOptions = {\n hour: '2-digit',\n minute: '2-digit',\n }\n \n const hasTime = date.getHours() !== 0 || date.getMinutes() !== 0 || date.getSeconds() !== 0\n \n if (hasTime) {\n return date.toLocaleString('en-US', { ...options, ...timeOptions })\n }\n \n return date.toLocaleDateString('en-US', options)\n }\n \n const parseGoogleSheetsValue = (value) => {\n if (typeof value === 'string') {\n const dateMatch = value.match(/^Date\\((\\d+),(\\d+),(\\d+)(?:,(\\d+),(\\d+),(\\d+))?\\)$/)\n if (dateMatch) {\n const year = parseInt(dateMatch[1], 10)\n const month = parseInt(dateMatch[2], 10)\n const day = parseInt(dateMatch[3], 10)\n const hour = dateMatch[4] ? parseInt(dateMatch[4], 10) : 0\n const minute = dateMatch[5] ? parseInt(dateMatch[5], 10) : 0\n const second = dateMatch[6] ? parseInt(dateMatch[6], 10) : 0\n const date = new Date(year, month, day, hour, minute, second)\n return formatDateValue(date)\n }\n }\n return value\n }\n \n const table = data.table\n const rawRows = table.rows || []\n \n if (rawRows.length === 0) {\n return res.status(200).json({\n success: true,\n data: [],\n timestamp: Date.now()\n })\n }\n \n const firstRow = rawRows[0]\n const firstRowValues = firstRow.c.map((cell) => cell?.v ?? cell?.f ?? null)\n \n const shouldAutoDetect = autoDetectHeader !== false\n let hasHeaderRow = false\n \n if (shouldAutoDetect) {\n hasHeaderRow = detectHeaderRow(firstRowValues, rawRows)\n } else {\n hasHeaderRow = firstRowIsHeader !== false\n }\n \n const configColumns = ").concat(JSON.stringify(configColumns), "\n const useConfigColumns = configColumns && configColumns.length > 0\n \n let columns\n let dataRows\n \n if (hasHeaderRow && rawRows.length > 1) {\n columns = table.cols.map((col, index) => {\n const configCol = useConfigColumns ? configColumns[index] : null\n return {\n id: configCol?.id || col.id || `col_${index}`,\n label: String(firstRowValues[index] || col.label || configCol?.label || `Column ${index + 1}`),\n type: configCol?.type || col.type || 'string'\n }\n })\n dataRows = rawRows.slice(1)\n } else {\n columns = table.cols.map((col, index) => {\n const configCol = useConfigColumns ? configColumns[index] : null\n return {\n id: configCol?.id || col.id || `col_${index}`,\n label: configCol?.label || col.label || `Column ${index + 1}`,\n type: configCol?.type || col.type || 'string'\n }\n })\n dataRows = rawRows\n }\n \n if (maxRows && dataRows.length > maxRows) {\n dataRows = dataRows.slice(0, maxRows)\n }\n \n const rows = dataRows.map((row) => {\n const rowData = {}\n row.c.forEach((cell, index) => {\n const columnId = columns[index]?.id || `col_${index}`\n const rawValue = cell?.v ?? cell?.f ?? null\n rowData[columnId] = parseGoogleSheetsValue(rawValue)\n })\n return rowData\n })\n \n const columnsWithData = columns.filter((col, index) => {\n const hasHeaderData = col.label && col.label !== `Column ${index + 1}`\n const hasDataInColumn = rows.some((row) => {\n const value = row[col.id]\n return value !== null && value !== undefined && value !== ''\n })\n return hasHeaderData || hasDataInColumn\n })\n \n const filteredRows = rows.map((row) => {\n const filteredRow = {}\n columnsWithData.forEach((col) => {\n filteredRow[col.id] = row[col.id]\n })\n return filteredRow\n })\n \n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n \n // Create label-to-ID mapping for filters and sorts\n const labelToIdMap = {}\n columnsWithData.forEach((col) => {\n labelToIdMap[col.label] = col.id\n })\n \n let filteredData = [...filteredRows]\n \n if (query) {\n if (queryColumns) {\n const searchColumns = safeJSONParse(queryColumns)\n filteredData = filteredData.filter((item) =>\n matchesSearch(item, query, searchColumns, (row, col) => getNestedValue(row, labelToIdMap[col] || col))\n )\n } else {\n const terms = searchTerms(query)\n filteredData = filteredData.filter((item) => {\n try {\n const text = searchText(item)\n return terms.every((term) => text.indexOf(term) !== -1)\n } catch {\n return false\n }\n })\n }\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n filteredData = filteredData.filter((item) =>\n evaluateFilterTree(filterTree, (condition) => {\n // Map label to column ID\n const field = labelToIdMap[condition.source] || condition.source\n const value = getNestedValue(item, field)\n const target = condition.destination\n const operand = condition.operand\n \n if (Array.isArray(target)) {\n if (operand === '!=') {\n return !target.includes(value)\n }\n return target.includes(value)\n }\n \n return compareValues(value, target, operand)\n })\n )\n }\n }\n \n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n filteredData.sort((a, b) => {\n for (const sort of parsedSorts) {\n if (!sort.field) continue\n // Map label to column ID\n const field = labelToIdMap[sort.field] || sort.field\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n if (comparison !== 0) return comparison * sortOrderValue\n }\n return 0\n })\n }\n } else if (sortBy) {\n filteredData.sort((a, b) => {\n // Map label to column ID\n const field = labelToIdMap[sortBy] || sortBy\n const aVal = getNestedValue(a, field)\n const bVal = getNestedValue(b, field)\n const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1\n \n let comparison = 0\n if (aVal === null || aVal === undefined) {\n comparison = bVal === null || bVal === undefined ? 0 : -1\n } else if (bVal === null || bVal === undefined) {\n comparison = 1\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n comparison = aVal - bVal\n } else if (aVal instanceof Date && bVal instanceof Date) {\n comparison = aVal.getTime() - bVal.getTime()\n } else {\n const aStr = String(aVal)\n const bStr = String(bVal)\n if (aStr < bStr) comparison = -1\n else if (aStr > bStr) comparison = 1\n }\n \n return comparison * sortOrderValue\n })\n }\n \n if (query && queryColumns) {\n filteredData = rankBySearch(filteredData, query, safeJSONParse(queryColumns), (item, col) =>\n getNestedValue(item, labelToIdMap[col] || col)\n )\n }\n \n const limitValue = limit || perPage\n const offsetValue = offsetParam !== undefined ? parseInt(offsetParam) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : 0)\n \n if (limitValue) {\n filteredData = filteredData.slice(offsetValue, offsetValue + parseInt(limitValue))\n }\n \n const safeData = JSON.parse(JSON.stringify(filteredData))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Google Sheets fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n }\n}\n");
34
34
  };
35
35
  exports.generateGoogleSheetsFetcher = generateGoogleSheetsFetcher;
36
36
  //# sourceMappingURL=google-sheets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"google-sheets.js","sourceRoot":"","sources":["../../../src/fetchers/google-sheets.ts"],"names":[],"mappings":";;;AAAA,kCAIiB;AACjB,wDAAkE;AAClE,6DAAsE;AAE/D,IAAM,0BAA0B,GAAG,UACxC,MAA+B;IAE/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAA;KACxE;IAED,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE;QACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAA;KAC9D;IAED,IAAI,MAAM,CAAC,QAAQ,EAAE;QACnB,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE;YACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAA;SAC/D;QAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE;YAC7D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAA;SACrE;KACF;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AA1BY,QAAA,0BAA0B,8BA0BtC;AAcM,IAAM,2BAA2B,GAAG,UAAC,MAA+B;IACzE,IAAM,YAAY,GAAG,MAA4B,CAAA;IACjD,IAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAA;IACtD,IAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAA;IACtD,IAAM,aAAa,GAAG,YAAY,CAAC,OAAO,IAAI,EAAE,CAAA;IAEhD,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,+CAA4B,GAAE,iBAE9B,IAAA,oCAA4B,GAAE,iBAE9B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,8CAA2B,GAAE,kGAIR,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,iCACxC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,iCACpC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,IAAI,UAAU,CAAC,mCAC9C,YAAY,CAAC,OAAO,IAAI,CAAC,4CAChB,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,4CACxD,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,6iHA2H3D,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,g6OAiNxD,CAAA;AACD,CAAC,CAAA;AAtWY,QAAA,2BAA2B,+BAsWvC"}
1
+ {"version":3,"file":"google-sheets.js","sourceRoot":"","sources":["../../../src/fetchers/google-sheets.ts"],"names":[],"mappings":";;;AAAA,kCAIiB;AACjB,wDAAkE;AAClE,6DAAsE;AAE/D,IAAM,0BAA0B,GAAG,UACxC,MAA+B;IAE/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAA;KACxE;IAED,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE;QACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAA;KAC9D;IAED,IAAI,MAAM,CAAC,QAAQ,EAAE;QACnB,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE;YACvC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAA;SAC/D;QAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE;YAC7D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAA;SACrE;KACF;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AA1BY,QAAA,0BAA0B,8BA0BtC;AAcM,IAAM,2BAA2B,GAAG,UAAC,MAA+B;IACzE,IAAM,YAAY,GAAG,MAA4B,CAAA;IACjD,IAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAA;IACtD,IAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAA;IACtD,IAAM,aAAa,GAAG,YAAY,CAAC,OAAO,IAAI,EAAE,CAAA;IAEhD,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,+CAA4B,GAAE,iBAE9B,IAAA,oCAA4B,GAAE,iBAE9B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,8CAA2B,GAAE,kGAIR,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,iCACxC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,iCACpC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,IAAI,UAAU,CAAC,mCAC9C,YAAY,CAAC,OAAO,IAAI,CAAC,4CAChB,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,4CACxD,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,6iHA2H3D,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,kpOA2MxD,CAAA;AACD,CAAC,CAAA;AAhWY,QAAA,2BAA2B,+BAgWvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"javascript.d.ts","sourceRoot":"","sources":["../../../src/fetchers/javascript.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,WAC3B,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CA2BpC,CAAA;AAMD,eAAO,MAAM,yBAAyB,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MAsL3E,CAAA;AAGD,eAAO,MAAM,8BAA8B,YAAa,GAAG,KAAG,MAkD7D,CAAA"}
1
+ {"version":3,"file":"javascript.d.ts","sourceRoot":"","sources":["../../../src/fetchers/javascript.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,WAC3B,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CA2BpC,CAAA;AAMD,eAAO,MAAM,yBAAyB,WAAY,OAAO,MAAM,EAAE,OAAO,CAAC,KAAG,MA+K3E,CAAA;AAGD,eAAO,MAAM,8BAA8B,YAAa,GAAG,KAAG,MAkD7D,CAAA"}