@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.
- package/__tests__/browser-row-policy.test.ts +6 -6
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/items-path.test.ts +3 -2
- package/__tests__/related-items-transform.test.ts +17 -0
- package/__tests__/search-relevance.test.ts +199 -52
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
- package/dist/cjs/fetchers/csv-file.js +1 -1
- package/dist/cjs/fetchers/csv-file.js.map +1 -1
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/cjs/fetchers/google-sheets.js +1 -1
- package/dist/cjs/fetchers/google-sheets.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +1 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +1 -1
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.js +1 -1
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -3
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redshift.js +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +1 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/supabase.js +2 -2
- package/dist/cjs/fetchers/supabase.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -4
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/search-relevance.d.ts +67 -26
- package/dist/cjs/search-relevance.d.ts.map +1 -1
- package/dist/cjs/search-relevance.js +102 -29
- package/dist/cjs/search-relevance.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +1 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
- package/dist/esm/fetchers/csv-file.js +1 -1
- package/dist/esm/fetchers/csv-file.js.map +1 -1
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/esm/fetchers/google-sheets.js +1 -1
- package/dist/esm/fetchers/google-sheets.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +1 -1
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/mariadb.js +2 -2
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mysql.js +2 -2
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +4 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redshift.js +2 -2
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +1 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/supabase.js +3 -3
- package/dist/esm/fetchers/supabase.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +5 -5
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.js +2 -2
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/search-relevance.d.ts +67 -26
- package/dist/esm/search-relevance.d.ts.map +1 -1
- package/dist/esm/search-relevance.js +100 -27
- package/dist/esm/search-relevance.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +1 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/fetchers/csv-file.ts +6 -11
- package/src/fetchers/google-sheets.ts +6 -12
- package/src/fetchers/javascript.ts +5 -12
- package/src/fetchers/mariadb.ts +2 -2
- package/src/fetchers/mysql.ts +2 -2
- package/src/fetchers/postgresql.ts +9 -4
- package/src/fetchers/redshift.ts +2 -2
- package/src/fetchers/rest-api.ts +4 -11
- package/src/fetchers/supabase.ts +4 -4
- package/src/fetchers/teleport.ts +9 -4
- package/src/fetchers/turso.ts +2 -2
- package/src/search-relevance.ts +195 -61
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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,
|
|
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:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
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
|
|
82
|
-
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
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('
|
|
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(['
|
|
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, {
|
|
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
|
-
|
|
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
|
|
126
|
-
const
|
|
127
|
-
|
|
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(
|
|
135
|
-
|
|
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
|
|
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(
|
|
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(['
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
256
|
+
term[0],
|
|
257
|
+
term[0],
|
|
258
|
+
term[1],
|
|
259
|
+
term[1],
|
|
201
260
|
'published',
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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&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
|
-
{
|
|
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
|
|
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
|
|
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([
|
|
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;
|
|
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
|
|
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":";;;AAAA,kCAKiB;AACjB,wDAAkE;AAClE,6DAAsE;AAE/D,IAAM,iBAAiB,GAAG,UAC/B,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,WAAW,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;QAC7C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mDAAmD,EAAE,CAAA;KACtF;IAED,IAAI,MAAM,CAAC,WAAW,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE;QAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE;QAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;QAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;YAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAA;SACxE;QAED,KAAqB,UAAc,EAAd,KAAA,MAAM,CAAC,OAAO,EAAd,cAAc,EAAd,IAAc,EAAE;YAAhC,IAAM,MAAM,SAAA;YACf,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,EAAE;gBACxF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAA;aACrE;SACF;KACF;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhCY,QAAA,iBAAiB,qBAgC7B;AAUD,IAAM,mBAAmB,GAAG;IAC1B,OAAO,
|
|
1
|
+
{"version":3,"file":"csv-file.js","sourceRoot":"","sources":["../../../src/fetchers/csv-file.ts"],"names":[],"mappings":";;;AAAA,kCAKiB;AACjB,wDAAkE;AAClE,6DAAsE;AAE/D,IAAM,iBAAiB,GAAG,UAC/B,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,WAAW,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;QAC7C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mDAAmD,EAAE,CAAA;KACtF;IAED,IAAI,MAAM,CAAC,WAAW,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE;QAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,MAAM,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE;QAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;QAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;YAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAA;SACxE;QAED,KAAqB,UAAc,EAAd,KAAA,MAAM,CAAC,OAAO,EAAd,cAAc,EAAd,IAAc,EAAE;YAAhC,IAAM,MAAM,SAAA;YACf,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,EAAE;gBACxF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kCAAkC,EAAE,CAAA;aACrE;SACF;KACF;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhCY,QAAA,iBAAiB,qBAgC7B;AAUD,IAAM,mBAAmB,GAAG;IAC1B,OAAO,ijKA+IR,CAAA;AACD,CAAC,CAAA;AAEM,IAAM,sBAAsB,GAAG,UAAC,MAA+B;IACpE,IAAM,SAAS,GAAG,MAAuB,CAAA;IAEzC,0EAA0E;IAC1E,IAAM,gBAAgB,GAAG,SAAS,CAAC,gBAAgB,KAAK,KAAK,CAAA;IAC7D,IAAM,gBAAgB,GAAG,SAAS,CAAC,gBAAgB,KAAK,KAAK,CAAA;IAC7D,IAAM,aAAa,GAAG,SAAS,CAAC,OAAO,IAAI,EAAE,CAAA;IAE7C,IAAI,SAAS,CAAC,WAAW,EAAE;QACzB,IAAM,WAAW,GAAG,SAAS,CAAC,WAAW,CAAA;QACzC,OAAO,6BAAsB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,wCACjC,gBAAgB,wCAChB,gBAAgB,qCACnB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,iBAEnD,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,+CAA4B,GAAE,iBAE9B,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,oCAA4B,GAAE,iBAE9B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,8CAA2B,GAAE,iiFA+FoB,mBAAmB,EAAE,UAEvE,CAAA;KACE;SAAM;QACL,OAAO,uBAAgB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,UAAU,IAAI,EAAE,CAAC,+BACnD,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,iBAE7C,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,+CAA4B,GAAE,iBAE9B,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,oCAA4B,GAAE,iBAE9B,IAAA,qCAA6B,GAAE,kEAEkB,mBAAmB,EAAE,UAEvE,CAAA;KACE;AACH,CAAC,CAAA;AA7IY,QAAA,sBAAsB,0BA6IlC;AAED,yCAAyC;AAClC,IAAM,uBAAuB,GAAG,UAAC,OAAY;IAClD,OAAO,knCA6CR,CAAA;AACD,CAAC,CAAA;AA/CY,QAAA,uBAAuB,2BA+CnC"}
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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"}
|