@teleporthq/teleport-plugin-next-data-source 0.43.66 → 0.43.67
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__/array-mapper-cache-wiring.test.ts +11 -0
- package/__tests__/data-source-request-locale.test.ts +313 -0
- package/__tests__/gated-data-provider.test.ts +88 -0
- package/__tests__/localized-columns.test.ts +217 -0
- package/__tests__/numbered-pagination.test.ts +15 -0
- package/__tests__/product-filter-fields.ts +230 -0
- package/__tests__/product-filter-sql.fixture.json +7 -0
- package/__tests__/refetch-loading-state.test.ts +23 -0
- package/__tests__/search-clear-button.test.ts +122 -0
- package/dist/cjs/content-localization.d.ts +37 -0
- package/dist/cjs/content-localization.d.ts.map +1 -0
- package/dist/cjs/content-localization.js +29 -0
- package/dist/cjs/content-localization.js.map +1 -0
- package/dist/cjs/count-effect.d.ts +11 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +30 -2
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
- package/dist/cjs/fetchers/airtable.js +1 -1
- package/dist/cjs/fetchers/airtable.js.map +1 -1
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/cjs/fetchers/clickhouse.js +1 -1
- package/dist/cjs/fetchers/clickhouse.js.map +1 -1
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
- package/dist/cjs/fetchers/firestore.js +1 -1
- package/dist/cjs/fetchers/firestore.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +1 -1
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mongodb.js +1 -1
- package/dist/cjs/fetchers/mongodb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.d.ts.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 +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
- package/dist/cjs/fetchers/redshift.js +1 -1
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +10 -3
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.d.ts.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +60 -8
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/loading-state.d.ts +16 -1
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +69 -3
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/localized-columns.d.ts +56 -0
- package/dist/cjs/localized-columns.d.ts.map +1 -0
- package/dist/cjs/localized-columns.js +124 -0
- package/dist/cjs/localized-columns.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +123 -45
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-filter-fields.d.ts +87 -0
- package/dist/cjs/product-filter-fields.d.ts.map +1 -0
- package/dist/cjs/product-filter-fields.js +161 -0
- package/dist/cjs/product-filter-fields.js.map +1 -0
- package/dist/cjs/request-locale.d.ts +49 -0
- package/dist/cjs/request-locale.d.ts.map +1 -0
- package/dist/cjs/request-locale.js +130 -0
- package/dist/cjs/request-locale.js.map +1 -0
- package/dist/cjs/search-clear.d.ts +31 -0
- package/dist/cjs/search-clear.d.ts.map +1 -0
- package/dist/cjs/search-clear.js +103 -0
- package/dist/cjs/search-clear.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +16 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -2
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +11 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +59 -15
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/content-localization.d.ts +37 -0
- package/dist/esm/content-localization.d.ts.map +1 -0
- package/dist/esm/content-localization.js +24 -0
- package/dist/esm/content-localization.js.map +1 -0
- package/dist/esm/count-effect.d.ts +11 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +28 -1
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/fetchers/airtable.d.ts.map +1 -1
- package/dist/esm/fetchers/airtable.js +1 -1
- package/dist/esm/fetchers/airtable.js.map +1 -1
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/esm/fetchers/clickhouse.js +1 -1
- package/dist/esm/fetchers/clickhouse.js.map +1 -1
- package/dist/esm/fetchers/firestore.d.ts.map +1 -1
- package/dist/esm/fetchers/firestore.js +1 -1
- package/dist/esm/fetchers/firestore.js.map +1 -1
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
- package/dist/esm/fetchers/mariadb.js +1 -1
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
- package/dist/esm/fetchers/mongodb.js +1 -1
- package/dist/esm/fetchers/mongodb.js.map +1 -1
- package/dist/esm/fetchers/mysql.d.ts.map +1 -1
- package/dist/esm/fetchers/mysql.js +1 -1
- 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 +2 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redshift.d.ts.map +1 -1
- package/dist/esm/fetchers/redshift.js +1 -1
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +10 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.d.ts.map +1 -1
- package/dist/esm/fetchers/turso.js +1 -1
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +55 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/loading-state.d.ts +16 -1
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +69 -3
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/localized-columns.d.ts +56 -0
- package/dist/esm/localized-columns.d.ts.map +1 -0
- package/dist/esm/localized-columns.js +119 -0
- package/dist/esm/localized-columns.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +124 -46
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-filter-fields.d.ts +87 -0
- package/dist/esm/product-filter-fields.d.ts.map +1 -0
- package/dist/esm/product-filter-fields.js +154 -0
- package/dist/esm/product-filter-fields.js.map +1 -0
- package/dist/esm/request-locale.d.ts +49 -0
- package/dist/esm/request-locale.d.ts.map +1 -0
- package/dist/esm/request-locale.js +98 -0
- package/dist/esm/request-locale.js.map +1 -0
- package/dist/esm/search-clear.d.ts +31 -0
- package/dist/esm/search-clear.d.ts.map +1 -0
- package/dist/esm/search-clear.js +74 -0
- package/dist/esm/search-clear.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +16 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -2
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +11 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +59 -15
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/content-localization.ts +53 -0
- package/src/count-effect.ts +53 -2
- package/src/fetchers/airtable.ts +4 -2
- package/src/fetchers/clickhouse.ts +4 -2
- package/src/fetchers/firestore.ts +4 -2
- package/src/fetchers/mariadb.ts +4 -2
- package/src/fetchers/mongodb.ts +4 -2
- package/src/fetchers/mysql.ts +4 -2
- package/src/fetchers/postgresql.ts +16 -2
- package/src/fetchers/redshift.ts +4 -2
- package/src/fetchers/teleport.ts +48 -9
- package/src/fetchers/turso.ts +4 -2
- package/src/index.ts +79 -11
- package/src/loading-state.ts +134 -3
- package/src/localized-columns.ts +133 -0
- package/src/pagination-plugin.ts +177 -53
- package/src/product-filter-fields.ts +173 -0
- package/src/request-locale.ts +113 -0
- package/src/search-clear.ts +102 -0
- package/src/transformations/index.ts +30 -5
- package/src/utils.ts +83 -15
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import {
|
|
5
|
+
PRODUCT_ON_SALE_FILTER_FIELD,
|
|
6
|
+
PRODUCT_RATING_FILTER_FIELD,
|
|
7
|
+
PRODUCT_VARIANT_AXIS_FILTER_PREFIX,
|
|
8
|
+
PRODUCT_VARIANT_AXIS_KEY_PATTERN,
|
|
9
|
+
buildProductOnSaleSql,
|
|
10
|
+
buildProductRatingBucketSql,
|
|
11
|
+
buildProductVariantAxisExistsSql,
|
|
12
|
+
generateProductFilterClauseHelper,
|
|
13
|
+
} from '../src/product-filter-fields'
|
|
14
|
+
import { buildProductEffectivePriceSql } from '../src/product-price-sort'
|
|
15
|
+
import { generateTeleportFetcher } from '../src/fetchers/teleport'
|
|
16
|
+
import { generatePostgreSQLFetcher } from '../src/fetchers/postgresql'
|
|
17
|
+
import { generateMySQLFetcher } from '../src/fetchers/mysql'
|
|
18
|
+
import { generateDataSourceFetcher } from '../src/data-source-fetchers'
|
|
19
|
+
import { generateFilterTreeHelpersCode } from '../src/utils'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The storefront's product filters on things that are not a column: the price
|
|
23
|
+
* paid today, the star bucket, the on-sale flag and a variant axis. They are
|
|
24
|
+
* answered in SQL — the list is paginated — so the emitted helper is pinned
|
|
25
|
+
* here the same way the discount-aware sort is.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const PG_CONFIG = { connectionString: 'postgres://user:pw@host:5432/db' }
|
|
29
|
+
|
|
30
|
+
/** Runs the emitted helper as real JS and returns the clause + bound params. */
|
|
31
|
+
function runHelper(
|
|
32
|
+
tableName: string,
|
|
33
|
+
field: string,
|
|
34
|
+
operand: string,
|
|
35
|
+
value: unknown
|
|
36
|
+
): { clause: string | null; params: unknown[] } {
|
|
37
|
+
const params: unknown[] = []
|
|
38
|
+
let index = 1
|
|
39
|
+
const pushParam = (param: unknown) => {
|
|
40
|
+
params.push(param)
|
|
41
|
+
return '$' + index++
|
|
42
|
+
}
|
|
43
|
+
// tslint:disable-next-line:function-constructor
|
|
44
|
+
const factory = new Function(
|
|
45
|
+
`${generateProductFilterClauseHelper(tableName)}\nreturn productFilterClause`
|
|
46
|
+
)
|
|
47
|
+
const clause = factory()(field, operand, value, pushParam)
|
|
48
|
+
return { clause, params }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Runs the emitted `normalizeInOperand` as real JS. */
|
|
52
|
+
function runNormalize(operand: string, value: unknown): unknown {
|
|
53
|
+
// tslint:disable-next-line:function-constructor
|
|
54
|
+
const factory = new Function(`${generateFilterTreeHelpersCode()}\nreturn normalizeInOperand`)
|
|
55
|
+
return factory()(operand, value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('generateProductFilterClauseHelper', () => {
|
|
59
|
+
it('is the identity for every table but the products table', () => {
|
|
60
|
+
const helper = generateProductFilterClauseHelper('teleport_orders')
|
|
61
|
+
expect(helper).toContain('return null')
|
|
62
|
+
expect(helper).not.toContain('jsonb_array_elements')
|
|
63
|
+
expect(runHelper('teleport_orders', 'rating', '=', '4').clause).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('emits the SQL as double-quoted literals, carrying no template syntax', () => {
|
|
67
|
+
const helper = generateProductFilterClauseHelper('teleport_products')
|
|
68
|
+
expect(helper).not.toContain('`')
|
|
69
|
+
expect(helper).not.toContain('${')
|
|
70
|
+
expect(() => parse(helper, { sourceType: 'script' })).not.toThrow()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('leaves a plain column to the ordinary branches', () => {
|
|
74
|
+
expect(runHelper('teleport_products', 'brand', '=', 'Acme').clause).toBeNull()
|
|
75
|
+
expect(runHelper('teleport_products', 'brand', '=', ['Acme', 'Globex']).clause).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('filters price on what customers pay today, like the price sort', () => {
|
|
79
|
+
const { clause, params } = runHelper('teleport_products', 'price', '<=', '50')
|
|
80
|
+
expect(clause).toBe(`(${buildProductEffectivePriceSql()}) <= $1`)
|
|
81
|
+
expect(params).toEqual(['50'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('answers a star bucket over approved reviews only', () => {
|
|
85
|
+
const sql = buildProductRatingBucketSql()
|
|
86
|
+
expect(sql).toContain("r.status = 'approved'")
|
|
87
|
+
expect(sql).toContain('ROUND(AVG(r.rating))')
|
|
88
|
+
expect(sql).toContain('r.product_id = teleport_products.id')
|
|
89
|
+
|
|
90
|
+
const { clause, params } = runHelper('teleport_products', PRODUCT_RATING_FILTER_FIELD, '=', [
|
|
91
|
+
'4',
|
|
92
|
+
'5',
|
|
93
|
+
])
|
|
94
|
+
expect(clause).toBe(`${sql}::text = ANY($1::text[])`)
|
|
95
|
+
expect(params).toEqual([['4', '5']])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('reads the on-sale flag off the effective price', () => {
|
|
99
|
+
const sql = buildProductOnSaleSql()
|
|
100
|
+
expect(sql).toContain(`< "price"::numeric THEN 'true' ELSE 'false'`)
|
|
101
|
+
const { clause, params } = runHelper(
|
|
102
|
+
'teleport_products',
|
|
103
|
+
PRODUCT_ON_SALE_FILTER_FIELD,
|
|
104
|
+
'=',
|
|
105
|
+
'true'
|
|
106
|
+
)
|
|
107
|
+
expect(clause).toBe(`${sql} = $1`)
|
|
108
|
+
expect(params).toEqual(['true'])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('binds the axis key and the values, never interpolating them', () => {
|
|
112
|
+
const { clause, params } = runHelper(
|
|
113
|
+
'teleport_products',
|
|
114
|
+
`${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}size`,
|
|
115
|
+
'=',
|
|
116
|
+
['xl', 'l']
|
|
117
|
+
)
|
|
118
|
+
expect(clause).toBe(buildProductVariantAxisExistsSql('$1', '$2'))
|
|
119
|
+
expect(clause).toContain("ax->>'key' = $1")
|
|
120
|
+
expect(clause).toContain("v->>'value' = ANY($2::text[])")
|
|
121
|
+
expect(params).toEqual(['size', ['xl', 'l']])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('casts the JSON column only when it already looks like an array', () => {
|
|
125
|
+
const sql = buildProductVariantAxisExistsSql('$1', '$2')
|
|
126
|
+
expect(sql).toContain(`btrim("variant_options") NOT LIKE '[%'`)
|
|
127
|
+
expect(sql).toContain(`jsonb_typeof(ax->'values') = 'array'`)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('refuses an axis key the editor could never have written', () => {
|
|
131
|
+
expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test('size')).toBe(true)
|
|
132
|
+
expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test('shoe-size_2')).toBe(true)
|
|
133
|
+
expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test(`x' OR 1=1 --`)).toBe(false)
|
|
134
|
+
const { clause, params } = runHelper(
|
|
135
|
+
'teleport_products',
|
|
136
|
+
`${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}x' OR 1=1 --`,
|
|
137
|
+
'=',
|
|
138
|
+
['a']
|
|
139
|
+
)
|
|
140
|
+
expect(clause).toBe('FALSE')
|
|
141
|
+
expect(params).toEqual([])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('never lets a virtual field fall into a column branch', () => {
|
|
145
|
+
// A `>` on the on-sale flag has no meaning; the fallthrough would emit
|
|
146
|
+
// `on_sale > $1` and fail the whole query.
|
|
147
|
+
expect(runHelper('teleport_products', PRODUCT_ON_SALE_FILTER_FIELD, '>', 'x').clause).toBe(
|
|
148
|
+
'FALSE'
|
|
149
|
+
)
|
|
150
|
+
expect(
|
|
151
|
+
runHelper('teleport_products', `${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}size`, '>', ['xl'])
|
|
152
|
+
.clause
|
|
153
|
+
).toBe('FALSE')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('normalizeInOperand', () => {
|
|
158
|
+
it('turns "in" into "=" over an array, so every fetcher answers IN (...)', () => {
|
|
159
|
+
expect(runNormalize('in', 'a, b,,c')).toEqual({ operand: '=', value: ['a', 'b', 'c'] })
|
|
160
|
+
expect(runNormalize('in', ['a', 'b,c', 4])).toEqual({ operand: '=', value: ['a', 'b', 'c', 4] })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('reads an empty set as "no filter"', () => {
|
|
164
|
+
expect(runNormalize('in', '')).toBeNull()
|
|
165
|
+
expect(runNormalize('in', [])).toBeNull()
|
|
166
|
+
expect(runNormalize('in', ' , ')).toBeNull()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('passes every other operand through untouched', () => {
|
|
170
|
+
expect(runNormalize('=', 'a,b')).toEqual({ operand: '=', value: 'a,b' })
|
|
171
|
+
expect(runNormalize('array_overlap', 'a,b')).toEqual({ operand: 'array_overlap', value: 'a,b' })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('the generated routes', () => {
|
|
176
|
+
it('answer the virtual fields before any raw-field branch, in the fetcher a store uses', () => {
|
|
177
|
+
// Through the type dispatch, not one fetcher's export: `teleport_products`
|
|
178
|
+
// is a Teleport-hosted table, and the discount sort once shipped into the
|
|
179
|
+
// wrong fetcher with every test green.
|
|
180
|
+
const route = generateDataSourceFetcher({ type: 'teleport', config: {} }, 'teleport_products')
|
|
181
|
+
expect(route).toContain('function productFilterClause(field, operand, value, pushParam)')
|
|
182
|
+
const conditionAt = route.indexOf('const buildCondition = (condition) =>')
|
|
183
|
+
const hookAt = route.indexOf('const productClause = productFilterClause(', conditionAt)
|
|
184
|
+
const arrayBranchAt = route.indexOf('if (Array.isArray(value))', conditionAt)
|
|
185
|
+
expect(conditionAt).toBeGreaterThan(-1)
|
|
186
|
+
expect(hookAt).toBeGreaterThan(conditionAt)
|
|
187
|
+
expect(arrayBranchAt).toBeGreaterThan(hookAt)
|
|
188
|
+
expect(route).toContain('normalizeInOperand(condition.operand, condition.destination)')
|
|
189
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('embed the helper in the Postgres route too, and parse', () => {
|
|
193
|
+
const route = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products')
|
|
194
|
+
expect(route).toContain('const productClause = productFilterClause(')
|
|
195
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('give every other SQL fetcher the "in" normalisation, and nothing else', () => {
|
|
199
|
+
const route = generateMySQLFetcher({ host: 'h' }, 'orders')
|
|
200
|
+
expect(route).toContain('normalizeInOperand(condition.operand, condition.destination)')
|
|
201
|
+
expect(route).not.toContain('productFilterClause')
|
|
202
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('keep a non-products Teleport table on the ordinary branches', () => {
|
|
206
|
+
const route = generateTeleportFetcher({}, 'teleport_orders')
|
|
207
|
+
expect(route).toContain('function productFilterClause() {')
|
|
208
|
+
expect(route).not.toContain('jsonb_array_elements(CASE')
|
|
209
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('the SQL mirrored into database-provider-worker', () => {
|
|
214
|
+
// `database-provider-worker/src/lib/__tests__/product-filter-sql.fixture.json`
|
|
215
|
+
// is a copy of this file. Both repos pin their builders to it, so the canvas
|
|
216
|
+
// and the generated store can never disagree about what a filter means.
|
|
217
|
+
const fixture = JSON.parse(
|
|
218
|
+
readFileSync(join(__dirname, 'product-filter-sql.fixture.json'), 'utf8')
|
|
219
|
+
) as Record<string, string>
|
|
220
|
+
|
|
221
|
+
it('matches the shared fixture byte for byte', () => {
|
|
222
|
+
expect(buildProductEffectivePriceSql()).toBe(fixture.effectivePrice)
|
|
223
|
+
expect(buildProductRatingBucketSql()).toBe(fixture.ratingBucket)
|
|
224
|
+
expect(buildProductOnSaleSql()).toBe(fixture.onSale)
|
|
225
|
+
expect(buildProductVariantAxisExistsSql('__KEY__', '__VALUES__')).toBe(
|
|
226
|
+
fixture.variantAxisExists
|
|
227
|
+
)
|
|
228
|
+
expect(PRODUCT_VARIANT_AXIS_KEY_PATTERN).toBe(fixture.axisKeyPattern)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"effectivePrice": "COALESCE((\n SELECT ROUND(GREATEST(0,\n CASE\n WHEN entry.kind = 'percentage'\n THEN \"price\"::numeric * (1 - LEAST(entry.amount, 100) / 100)\n ELSE \"price\"::numeric - entry.amount\n END\n ), 2)\n FROM (\n SELECT\n d.value->>'type' AS kind,\n CASE\n WHEN d.value->>'value' ~ '^[0-9]+(\\.[0-9]+)?$' THEN (d.value->>'value')::numeric\n ELSE 0\n END AS amount,\n d.value->>'startsAt' AS starts_at,\n d.value->>'endsAt' AS ends_at,\n d.ord AS ord\n FROM jsonb_array_elements(CASE\n WHEN \"discounts\" IS NULL OR btrim(\"discounts\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"discounts\"::jsonb\n END) WITH ORDINALITY AS d(value, ord)\n ) AS entry\n WHERE entry.amount > 0\n AND entry.kind IN ('percentage', 'fixed')\n AND (entry.starts_at IS NULL OR entry.starts_at <= to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n AND (entry.ends_at IS NULL OR entry.ends_at > to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n ORDER BY entry.ord\n LIMIT 1\n ), \"price\"::numeric)",
|
|
3
|
+
"ratingBucket": "(SELECT ROUND(AVG(r.rating)) FROM teleport_product_reviews r WHERE r.product_id = teleport_products.id AND r.status = 'approved' AND r.rating IS NOT NULL)",
|
|
4
|
+
"onSale": "(CASE WHEN COALESCE((\n SELECT ROUND(GREATEST(0,\n CASE\n WHEN entry.kind = 'percentage'\n THEN \"price\"::numeric * (1 - LEAST(entry.amount, 100) / 100)\n ELSE \"price\"::numeric - entry.amount\n END\n ), 2)\n FROM (\n SELECT\n d.value->>'type' AS kind,\n CASE\n WHEN d.value->>'value' ~ '^[0-9]+(\\.[0-9]+)?$' THEN (d.value->>'value')::numeric\n ELSE 0\n END AS amount,\n d.value->>'startsAt' AS starts_at,\n d.value->>'endsAt' AS ends_at,\n d.ord AS ord\n FROM jsonb_array_elements(CASE\n WHEN \"discounts\" IS NULL OR btrim(\"discounts\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"discounts\"::jsonb\n END) WITH ORDINALITY AS d(value, ord)\n ) AS entry\n WHERE entry.amount > 0\n AND entry.kind IN ('percentage', 'fixed')\n AND (entry.starts_at IS NULL OR entry.starts_at <= to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n AND (entry.ends_at IS NULL OR entry.ends_at > to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n ORDER BY entry.ord\n LIMIT 1\n ), \"price\"::numeric) < \"price\"::numeric THEN 'true' ELSE 'false' END)",
|
|
5
|
+
"variantAxisExists": "EXISTS (SELECT 1 FROM jsonb_array_elements(CASE\n WHEN \"variant_options\" IS NULL OR btrim(\"variant_options\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"variant_options\"::jsonb\n END) AS ax, jsonb_array_elements(CASE WHEN jsonb_typeof(ax->'values') = 'array' THEN ax->'values' ELSE '[]'::jsonb END) AS v WHERE ax->>'key' = __KEY__ AND v->>'value' = ANY(__VALUES__::text[]))",
|
|
6
|
+
"axisKeyPattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
|
|
7
|
+
}
|
|
@@ -178,6 +178,28 @@ describe('pagination plugin — refetch loading state', () => {
|
|
|
178
178
|
expect(code).toContain('ds_0_fetchesInFlight.current = 0')
|
|
179
179
|
})
|
|
180
180
|
|
|
181
|
+
it('hands an overtaken response the newest rows, so a slow older request never paints over a newer one', async () => {
|
|
182
|
+
const code = await runPlugin({})
|
|
183
|
+
|
|
184
|
+
expect(code).toContain('const ds_0_fetchSeq = useRef(0)')
|
|
185
|
+
expect(code).toContain('const ds_0_latestData = useRef(undefined)')
|
|
186
|
+
// Numbered when it STARTS, before the flag goes up.
|
|
187
|
+
expect(code).toMatch(
|
|
188
|
+
/const __tqSeq = \+\+ds_0_fetchSeq\.current;?\s*ds_0_fetchesInFlight\.current \+= 1/
|
|
189
|
+
)
|
|
190
|
+
// Checked when it settles: overtaken → the newest settled rows (or its own
|
|
191
|
+
// while the successor is still in flight); newest → recorded and returned.
|
|
192
|
+
expect(code).toContain('if (__tqSeq !== ds_0_fetchSeq.current)')
|
|
193
|
+
expect(code).toContain(
|
|
194
|
+
'return ds_0_latestData.current !== undefined ? ds_0_latestData.current : __tqRows'
|
|
195
|
+
)
|
|
196
|
+
expect(code).toContain('ds_0_latestData.current = __tqRows')
|
|
197
|
+
// The guard sits on the chain BEFORE the settle handler.
|
|
198
|
+
expect(code.indexOf('__tqSeq !== ds_0_fetchSeq.current')).toBeLessThan(
|
|
199
|
+
code.indexOf('.finally(() => {')
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
181
203
|
it('keeps fetchData referentially stable so the provider does not refetch in a loop', async () => {
|
|
182
204
|
const code = await runPlugin({})
|
|
183
205
|
|
|
@@ -249,6 +271,7 @@ describe('pagination plugin — refetch loading state', () => {
|
|
|
249
271
|
const code = generator(chunk.content as types.Node).code
|
|
250
272
|
expect(code).not.toContain('ds_0_isFetching')
|
|
251
273
|
expect(code).not.toContain('ds_0_fetchesInFlight')
|
|
274
|
+
expect(code).not.toContain('ds_0_fetchSeq')
|
|
252
275
|
})
|
|
253
276
|
|
|
254
277
|
it('leaves a provider without a designed loading state untouched', async () => {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkType,
|
|
5
|
+
FileType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
ChunkDefinition,
|
|
8
|
+
} from '@teleporthq/teleport-types'
|
|
9
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
10
|
+
import { SEARCH_CLEAR_ATTR } from '../src/search-clear'
|
|
11
|
+
|
|
12
|
+
// tslint:disable:no-any
|
|
13
|
+
|
|
14
|
+
const element = (
|
|
15
|
+
tag: string,
|
|
16
|
+
attrs: Array<[string, string]>,
|
|
17
|
+
children: types.JSXElement['children'] = []
|
|
18
|
+
): types.JSXElement =>
|
|
19
|
+
types.jsxElement(
|
|
20
|
+
types.jsxOpeningElement(
|
|
21
|
+
types.jsxIdentifier(tag),
|
|
22
|
+
attrs.map(([name, value]) =>
|
|
23
|
+
types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
|
|
24
|
+
),
|
|
25
|
+
false
|
|
26
|
+
),
|
|
27
|
+
types.jsxClosingElement(types.jsxIdentifier(tag)),
|
|
28
|
+
children,
|
|
29
|
+
false
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const searchInput = (): types.JSXElement => element('input', [['className', 'search-input']])
|
|
33
|
+
const clearButton = (): types.JSXElement =>
|
|
34
|
+
element('button', [
|
|
35
|
+
['type', 'button'],
|
|
36
|
+
[SEARCH_CLEAR_ATTR, 'true'],
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A component whose JSX is the field the editor builds — `<div><input
|
|
41
|
+
* class=search-input/><button data-tq-search-clear/></div>` — plus, when
|
|
42
|
+
* asked, a stray marked button somewhere else on the page.
|
|
43
|
+
*/
|
|
44
|
+
const makeComponentChunk = (withStrayButton: boolean): ChunkDefinition => {
|
|
45
|
+
const field = element('div', [['data-tq-search-field', 'true']], [searchInput(), clearButton()])
|
|
46
|
+
const root = element(
|
|
47
|
+
'div',
|
|
48
|
+
[],
|
|
49
|
+
withStrayButton ? [field, element('div', [], [clearButton()])] : [field]
|
|
50
|
+
)
|
|
51
|
+
const body = types.blockStatement([types.returnStatement(root)])
|
|
52
|
+
const arrow = types.arrowFunctionExpression([types.identifier('props')], body)
|
|
53
|
+
const declaration = types.variableDeclaration('const', [
|
|
54
|
+
types.variableDeclarator(types.identifier('TestComponent'), arrow),
|
|
55
|
+
])
|
|
56
|
+
return {
|
|
57
|
+
name: 'jsx-component',
|
|
58
|
+
type: ChunkType.AST,
|
|
59
|
+
fileType: FileType.JS,
|
|
60
|
+
linkAfter: [],
|
|
61
|
+
content: declaration,
|
|
62
|
+
meta: {},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const makeUidlNode = (): any => ({
|
|
67
|
+
type: 'data-source-list',
|
|
68
|
+
content: {
|
|
69
|
+
renderPropIdentifier: 'items',
|
|
70
|
+
resourceDefinition: {
|
|
71
|
+
dataSourceId: 'ds1',
|
|
72
|
+
tableName: 'products',
|
|
73
|
+
dataSourceType: 'postgresql',
|
|
74
|
+
},
|
|
75
|
+
resource: { params: { queryColumns: { content: ['name'] } } },
|
|
76
|
+
nodes: {
|
|
77
|
+
success: {
|
|
78
|
+
type: 'cms-list-repeater',
|
|
79
|
+
content: {
|
|
80
|
+
renderPropIdentifier: 'product',
|
|
81
|
+
paginated: true,
|
|
82
|
+
perPage: 20,
|
|
83
|
+
searchEnabled: true,
|
|
84
|
+
searchDebounce: 300,
|
|
85
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const runPlugin = async (withStrayButton = false): Promise<string> => {
|
|
93
|
+
const chunk = makeComponentChunk(withStrayButton)
|
|
94
|
+
const structure: ComponentStructure = {
|
|
95
|
+
uidl: { name: 'TestComponent', node: makeUidlNode() },
|
|
96
|
+
chunks: [chunk],
|
|
97
|
+
dependencies: {},
|
|
98
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
99
|
+
} as never
|
|
100
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
101
|
+
return generator(chunk.content as types.Node).code
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
105
|
+
|
|
106
|
+
describe('pagination plugin — search clear button', () => {
|
|
107
|
+
it('empties the query on click and renders only while something is typed', async () => {
|
|
108
|
+
const flat = collapse(await runPlugin())
|
|
109
|
+
expect(flat).toContain('onClick={() => setDs_0_searchQuery("")}')
|
|
110
|
+
expect(flat).toContain('{ds_0_searchQuery ? <button type="button" data-tq-search-clear="true"')
|
|
111
|
+
expect(flat).toContain(': null}')
|
|
112
|
+
// The input beside it is the one the button clears.
|
|
113
|
+
expect(flat).toContain('value={ds_0_searchQuery}')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaves a marked button that shares no parent with a search input alone', async () => {
|
|
117
|
+
const flat = collapse(await runPlugin(true))
|
|
118
|
+
expect(flat.match(/setDs_0_searchQuery\(""\)/g)).toHaveLength(1)
|
|
119
|
+
// The stray one keeps its markup, unwired and unconditional.
|
|
120
|
+
expect(flat).toContain('<div><button type="button" data-tq-search-clear="true"></button></div>')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GeneratorOptions } from '@teleporthq/teleport-types';
|
|
2
|
+
/**
|
|
3
|
+
* The languages a project's DATABASE content is stored in.
|
|
4
|
+
*
|
|
5
|
+
* Translatable columns of the platform tables (`teleport_products.name`,
|
|
6
|
+
* `teleport_blog_posts.title`, …) hold the MAIN language, and every other
|
|
7
|
+
* language of the project owns a `<locale>_<column>` copy (`es_name`) — the
|
|
8
|
+
* editor creates those columns when a language is added. The generated
|
|
9
|
+
* fetcher resolves a row against the locale the visitor is reading in, so it
|
|
10
|
+
* needs to know which locale is the main one (never prefixed) and which
|
|
11
|
+
* locales may have a prefixed copy.
|
|
12
|
+
*/
|
|
13
|
+
export interface ContentLocalization {
|
|
14
|
+
/** The locale whose values live in the base columns, never in a prefixed copy. */
|
|
15
|
+
mainLocale: string;
|
|
16
|
+
/** Every other locale of the project — each may own a `<locale>_<column>` copy. */
|
|
17
|
+
secondaryLocales: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The options both a generator pass (`GeneratorOptions`) and a project plugin
|
|
21
|
+
* (the `ProjectUIDL` itself) can hand over — the two carry the same
|
|
22
|
+
* `internationalization` shape.
|
|
23
|
+
*/
|
|
24
|
+
export type LocalizedProjectOptions = Pick<GeneratorOptions, 'internationalization'>;
|
|
25
|
+
/**
|
|
26
|
+
* Derives the content localization from the project's i18n settings, or
|
|
27
|
+
* `undefined` for a single-language project — in which case nothing about a
|
|
28
|
+
* generated fetch mentions a locale, exactly as before languages existed.
|
|
29
|
+
*/
|
|
30
|
+
export declare const resolveContentLocalization: (options: LocalizedProjectOptions) => ContentLocalization | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* True when the project has a language setup at all — the same condition under
|
|
33
|
+
* which the Next generator emits its i18n routing (`skipI18n`), and therefore
|
|
34
|
+
* the condition under which a fetch has to say which language it is for.
|
|
35
|
+
*/
|
|
36
|
+
export declare const isLocalizedProject: (options: LocalizedProjectOptions) => boolean;
|
|
37
|
+
//# sourceMappingURL=content-localization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-localization.d.ts","sourceRoot":"","sources":["../../src/content-localization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,mFAAmF;IACnF,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,gBAAgB,EAAE,sBAAsB,CAAC,CAAA;AAEpF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,YAC5B,uBAAuB,KAC/B,mBAAmB,GAAG,SAUxB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,YAAa,uBAAuB,KAAG,OACnB,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isLocalizedProject = exports.resolveContentLocalization = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Derives the content localization from the project's i18n settings, or
|
|
6
|
+
* `undefined` for a single-language project — in which case nothing about a
|
|
7
|
+
* generated fetch mentions a locale, exactly as before languages existed.
|
|
8
|
+
*/
|
|
9
|
+
var resolveContentLocalization = function (options) {
|
|
10
|
+
var _a;
|
|
11
|
+
var internationalization = options.internationalization;
|
|
12
|
+
var mainLocale = (_a = internationalization === null || internationalization === void 0 ? void 0 : internationalization.main) === null || _a === void 0 ? void 0 : _a.locale;
|
|
13
|
+
if (!mainLocale) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
var secondaryLocales = Object.keys(internationalization.languages || {}).filter(function (locale) { return locale !== mainLocale; });
|
|
17
|
+
return { mainLocale: mainLocale, secondaryLocales: secondaryLocales };
|
|
18
|
+
};
|
|
19
|
+
exports.resolveContentLocalization = resolveContentLocalization;
|
|
20
|
+
/**
|
|
21
|
+
* True when the project has a language setup at all — the same condition under
|
|
22
|
+
* which the Next generator emits its i18n routing (`skipI18n`), and therefore
|
|
23
|
+
* the condition under which a fetch has to say which language it is for.
|
|
24
|
+
*/
|
|
25
|
+
var isLocalizedProject = function (options) {
|
|
26
|
+
return (0, exports.resolveContentLocalization)(options) !== undefined;
|
|
27
|
+
};
|
|
28
|
+
exports.isLocalizedProject = isLocalizedProject;
|
|
29
|
+
//# sourceMappingURL=content-localization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-localization.js","sourceRoot":"","sources":["../../src/content-localization.ts"],"names":[],"mappings":";;;AA2BA;;;;GAIG;AACI,IAAM,0BAA0B,GAAG,UACxC,OAAgC;;IAEhC,IAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAA;IACzD,IAAM,UAAU,GAAG,MAAA,oBAAoB,aAApB,oBAAoB,uBAApB,oBAAoB,CAAE,IAAI,0CAAE,MAAM,CAAA;IACrD,IAAI,CAAC,UAAU,EAAE;QACf,OAAO,SAAS,CAAA;KACjB;IACD,IAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/E,UAAC,MAAM,IAAK,OAAA,MAAM,KAAK,UAAU,EAArB,CAAqB,CAClC,CAAA;IACD,OAAO,EAAE,UAAU,YAAA,EAAE,gBAAgB,kBAAA,EAAE,CAAA;AACzC,CAAC,CAAA;AAZY,QAAA,0BAA0B,8BAYtC;AAED;;;;GAIG;AACI,IAAM,kBAAkB,GAAG,UAAC,OAAgC;IACjE,OAAA,IAAA,kCAA0B,EAAC,OAAO,CAAC,KAAK,SAAS;AAAjD,CAAiD,CAAA;AADtC,QAAA,kBAAkB,sBACoB"}
|
|
@@ -27,8 +27,18 @@ interface CountEffectOptions {
|
|
|
27
27
|
urlParams: types.ObjectProperty[];
|
|
28
28
|
/** Dependencies that should re-run the count. */
|
|
29
29
|
deps: types.Expression[];
|
|
30
|
+
/**
|
|
31
|
+
* Name of the `useRef(0)` counting the count requests STARTED. Each request
|
|
32
|
+
* notes its sequence number and a response overtaken by a newer request is
|
|
33
|
+
* dropped: two filter changes in quick succession start two counts, and the
|
|
34
|
+
* older, slower one landing last painted a page strip for a result set the
|
|
35
|
+
* list no longer shows.
|
|
36
|
+
*/
|
|
37
|
+
countSeqRefVar: string;
|
|
30
38
|
}
|
|
31
|
-
|
|
39
|
+
/** `const ds_N_countSeq = useRef(0)` */
|
|
40
|
+
export declare const buildCountSeqDeclaration: (countSeqRefVar: string) => types.Statement;
|
|
41
|
+
export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams' | 'countSeqRefVar'>) => types.Statement;
|
|
32
42
|
/**
|
|
33
43
|
* The whole `useEffect(...)` wrapper.
|
|
34
44
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;IACxB;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,eAAO,MAAM,wBAAwB,mBAAoB,MAAM,KAAG,MAAM,SAMpE,CAAA;AAqDJ,eAAO,MAAM,wBAAwB,YAC1B,KACP,kBAAkB,EAClB,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,CAC3E,KACA,MAAM,SA2DN,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAa,kBAAkB,KAAG,MAAM,mBAYvE,CAAA"}
|
package/dist/cjs/count-effect.js
CHANGED
|
@@ -23,8 +23,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.buildCountFetchEffect = exports.buildCountFetchStatement = void 0;
|
|
26
|
+
exports.buildCountFetchEffect = exports.buildCountFetchStatement = exports.buildCountSeqDeclaration = void 0;
|
|
27
27
|
var types = __importStar(require("@babel/types"));
|
|
28
|
+
/** `const ds_N_countSeq = useRef(0)` */
|
|
29
|
+
var buildCountSeqDeclaration = function (countSeqRefVar) {
|
|
30
|
+
return types.variableDeclaration('const', [
|
|
31
|
+
types.variableDeclarator(types.identifier(countSeqRefVar), types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])),
|
|
32
|
+
]);
|
|
33
|
+
};
|
|
34
|
+
exports.buildCountSeqDeclaration = buildCountSeqDeclaration;
|
|
35
|
+
var COUNT_SEQ_LOCAL = '__tqCountSeq';
|
|
36
|
+
var refCurrent = function (refVar) {
|
|
37
|
+
return types.memberExpression(types.identifier(refVar), types.identifier('current'));
|
|
38
|
+
};
|
|
28
39
|
/**
|
|
29
40
|
* `data.count === 0 ? 0 : Math.ceil(data.count / perPage)`
|
|
30
41
|
*
|
|
@@ -37,6 +48,19 @@ var buildMaxPagesExpression = function (perPage) {
|
|
|
37
48
|
types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(perPage)),
|
|
38
49
|
]));
|
|
39
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `const __tqCountSeq = ++ds_N_countSeq.current` — the request's sequence
|
|
53
|
+
* number, taken before the fetch starts.
|
|
54
|
+
*/
|
|
55
|
+
var buildCountSeqCapture = function (countSeqRefVar) {
|
|
56
|
+
return types.variableDeclaration('const', [
|
|
57
|
+
types.variableDeclarator(types.identifier(COUNT_SEQ_LOCAL), types.updateExpression('++', refCurrent(countSeqRefVar), true)),
|
|
58
|
+
]);
|
|
59
|
+
};
|
|
60
|
+
/** `if (__tqCountSeq !== ds_N_countSeq.current) return` — a newer count is in flight. */
|
|
61
|
+
var buildStaleCountGuard = function (countSeqRefVar) {
|
|
62
|
+
return types.ifStatement(types.binaryExpression('!==', types.identifier(COUNT_SEQ_LOCAL), refCurrent(countSeqRefVar)), types.returnStatement());
|
|
63
|
+
};
|
|
40
64
|
var buildCountFetchStatement = function (options) {
|
|
41
65
|
return types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
|
|
42
66
|
types.templateLiteral([
|
|
@@ -54,6 +78,7 @@ var buildCountFetchStatement = function (options) {
|
|
|
54
78
|
types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
|
|
55
79
|
]), types.identifier('then')), [
|
|
56
80
|
types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
|
|
81
|
+
buildStaleCountGuard(options.countSeqRefVar),
|
|
57
82
|
types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
|
|
58
83
|
types.expressionStatement(types.callExpression(types.identifier(options.setMaxPagesVar), [
|
|
59
84
|
buildMaxPagesExpression(options.perPage),
|
|
@@ -73,7 +98,10 @@ exports.buildCountFetchStatement = buildCountFetchStatement;
|
|
|
73
98
|
*/
|
|
74
99
|
var buildCountFetchEffect = function (options) {
|
|
75
100
|
return types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
|
|
76
|
-
types.arrowFunctionExpression([], types.blockStatement([
|
|
101
|
+
types.arrowFunctionExpression([], types.blockStatement([
|
|
102
|
+
buildCountSeqCapture(options.countSeqRefVar),
|
|
103
|
+
(0, exports.buildCountFetchStatement)(options),
|
|
104
|
+
])),
|
|
77
105
|
types.arrayExpression(options.deps),
|
|
78
106
|
]));
|
|
79
107
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"count-effect.js","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDAAqC;
|
|
1
|
+
{"version":3,"file":"count-effect.js","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"airtable.d.ts","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,WACzB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAcpC,CAAA;AAQD,eAAO,MAAM,uBAAuB,WAC1B,OAAO,MAAM,EAAE,OAAO,CAAC,aACpB,MAAM,KAChB,
|
|
1
|
+
{"version":3,"file":"airtable.d.ts","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,WACzB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAcpC,CAAA;AAQD,eAAO,MAAM,uBAAuB,WAC1B,OAAO,MAAM,EAAE,OAAO,CAAC,aACpB,MAAM,KAChB,MAuNF,CAAA"}
|
|
@@ -19,7 +19,7 @@ var generateAirtableFetcher = function (config, tableName) {
|
|
|
19
19
|
var airtableConfig = config;
|
|
20
20
|
var baseId = airtableConfig.baseId;
|
|
21
21
|
var personalAccessToken = airtableConfig.personalAccessToken;
|
|
22
|
-
return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateDateFormatterCode)(), "\n\n// Escape a string literal for Airtable formula grammar. Strings use\n// single quotes; embedded single-quote must be backslash-escaped.\n// Backslash itself also escapes.\nconst escapeAirtableString = (s) => {\n return \"'\" + String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\") + \"'\"\n}\n\n// Escape an Airtable field reference name for use inside curly braces.\n// Only close-brace and backslash are special inside the braces.\nconst escapeAirtableFieldRef = (name) => {\n return String(name).replace(/\\\\/g, '\\\\\\\\').replace(/}/g, '\\\\}')\n}\n\nconst buildAirtableSearchFormula = (rawQuery, rawQueryColumns) => {\n if (typeof rawQuery !== 'string' || rawQuery.trim() === '') return ''\n let cols = []\n if (rawQueryColumns) {\n const parsed = safeJSONParse(rawQueryColumns)\n cols = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n }\n const validCols = cols.filter((c) => typeof c === 'string' && c.length > 0)\n if (validCols.length === 0) return ''\n const literal = escapeAirtableString(rawQuery)\n const parts = validCols.map(\n (col) => 'SEARCH(' + literal + ', {' + escapeAirtableFieldRef(col) + '})'\n )\n return parts.length === 1 ? parts[0] : 'OR(' + parts.join(',') + ')'\n}\n\nexport default async function handler(req, res) {\n try {\n const { query, queryColumns, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n\n const queryParams = new URLSearchParams()\n const formulaParts = []\n\n if (view) {\n queryParams.append('view', view)\n }\n \n // Handle sorts - new array format\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n parsedSorts.forEach((sort, index) => {\n if (!sort.field) return\n queryParams.append(`sort[${index}][field]`, sort.field)\n queryParams.append(`sort[${index}][direction]`, (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n })\n }\n } else if (sortBy) {\n queryParams.append('sort[0][field]', sortBy)\n queryParams.append('sort[0][direction]', (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n }\n \n const perPageValue = limit || perPage || 100\n queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())\n \n const formatAirtableValue = (value) => {\n if (typeof value === 'string') {\n return `'${value.replace(/'/g, \"\\\\'\")}'`\n } else if (typeof value === 'number') {\n return String(value)\n } else if (typeof value === 'boolean') {\n return value ? 'TRUE()' : 'FALSE()'\n }\n return `'${String(value)}'`\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n const buildCondition = (condition) => {\n const field = condition.source\n const
|
|
22
|
+
return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateDateFormatterCode)(), "\n\n// Escape a string literal for Airtable formula grammar. Strings use\n// single quotes; embedded single-quote must be backslash-escaped.\n// Backslash itself also escapes.\nconst escapeAirtableString = (s) => {\n return \"'\" + String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\") + \"'\"\n}\n\n// Escape an Airtable field reference name for use inside curly braces.\n// Only close-brace and backslash are special inside the braces.\nconst escapeAirtableFieldRef = (name) => {\n return String(name).replace(/\\\\/g, '\\\\\\\\').replace(/}/g, '\\\\}')\n}\n\nconst buildAirtableSearchFormula = (rawQuery, rawQueryColumns) => {\n if (typeof rawQuery !== 'string' || rawQuery.trim() === '') return ''\n let cols = []\n if (rawQueryColumns) {\n const parsed = safeJSONParse(rawQueryColumns)\n cols = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n }\n const validCols = cols.filter((c) => typeof c === 'string' && c.length > 0)\n if (validCols.length === 0) return ''\n const literal = escapeAirtableString(rawQuery)\n const parts = validCols.map(\n (col) => 'SEARCH(' + literal + ', {' + escapeAirtableFieldRef(col) + '})'\n )\n return parts.length === 1 ? parts[0] : 'OR(' + parts.join(',') + ')'\n}\n\nexport default async function handler(req, res) {\n try {\n const { query, queryColumns, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n\n const queryParams = new URLSearchParams()\n const formulaParts = []\n\n if (view) {\n queryParams.append('view', view)\n }\n \n // Handle sorts - new array format\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n parsedSorts.forEach((sort, index) => {\n if (!sort.field) return\n queryParams.append(`sort[${index}][field]`, sort.field)\n queryParams.append(`sort[${index}][direction]`, (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n })\n }\n } else if (sortBy) {\n queryParams.append('sort[0][field]', sortBy)\n queryParams.append('sort[0][direction]', (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n }\n \n const perPageValue = limit || perPage || 100\n queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())\n \n const formatAirtableValue = (value) => {\n if (typeof value === 'string') {\n return `'${value.replace(/'/g, \"\\\\'\")}'`\n } else if (typeof value === 'number') {\n return String(value)\n } else if (typeof value === 'boolean') {\n return value ? 'TRUE()' : 'FALSE()'\n }\n return `'${String(value)}'`\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n const buildCondition = (condition) => {\n const field = condition.source\n const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)\n if (normalizedCondition === null) return null\n const value = normalizedCondition.value\n const operand = normalizedCondition.operand\n \n if (Array.isArray(value)) {\n if (value.length === 0) return null\n const arrayConditions = value.map((v) => `{${field}}=${formatAirtableValue(v)}`)\n return arrayConditions.length > 1\n ? `OR(${arrayConditions.join(',')})`\n : arrayConditions[0]\n }\n \n const operatorMap = {\n '=': '=',\n '!=': '!=',\n '>': '>',\n '<': '<',\n '>=': '>=',\n '<=': '<=',\n }\n const airtableOp = operatorMap[operand] || '='\n return `{${field}}${airtableOp}${formatAirtableValue(value)}`\n }\n \n // Airtable spells its logic as AND(a,b) / OR(a,b) rather than infix, so\n // the tree is walked here instead of with buildFilterTreeClause.\n const buildNode = (node) => {\n if (node.type === 'group') {\n const parts = node.children.map(buildNode).filter(Boolean)\n if (parts.length === 0) return null\n if (parts.length === 1) return parts[0]\n return node.operator === 'or'\n ? `OR(${parts.join(',')})`\n : `AND(${parts.join(',')})`\n }\n return buildCondition(node)\n }\n \n const filterFormula = buildNode(filterTree)\n if (filterFormula) {\n formulaParts.push(filterFormula)\n }\n }\n }\n\n // Apply the { query, queryColumns } search contract via the\n // Airtable SEARCH() function. Search-term string literal escaping\n // protects against single-quote injection; field-ref escaping\n // protects against close-brace or backslash breaking out of the\n // {field} reference.\n const searchFormula = buildAirtableSearchFormula(query, queryColumns)\n if (searchFormula) {\n formulaParts.push(searchFormula)\n }\n\n if (formulaParts.length === 1) {\n queryParams.append('filterByFormula', formulaParts[0])\n } else if (formulaParts.length > 1) {\n queryParams.append('filterByFormula', 'AND(' + formulaParts.join(',') + ')')\n }\n \n let url = `https://api.airtable.com/v0/").concat(baseId, "/${encodeURIComponent('").concat(tableName, "')}`\n if (queryParams.toString()) {\n url += `?${queryParams.toString()}`\n }\n \n const allRecords = []\n let airtableOffset\n const skipValue = offsetParam !== undefined ? parseInt(offsetParam) : (page ? (parseInt(page) - 1) * parseInt(perPageValue) : 0)\n const totalRecordsNeeded = skipValue + parseInt(perPageValue)\n \n do {\n const fetchUrl = airtableOffset ? `${url}&offset=${airtableOffset}` : url\n const response = await fetch(fetchUrl, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ").concat((0, utils_1.replaceSecretReference)(personalAccessToken, {
|
|
23
23
|
templateLiteral: true,
|
|
24
24
|
}), "`,\n 'Content-Type': 'application/json'\n }\n })\n \n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}))\n return res.status(response.status).json({\n success: false,\n error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n timestamp: Date.now()\n })\n }\n \n const data = await response.json()\n allRecords.push(...data.records)\n airtableOffset = data.offset\n \n if (allRecords.length >= totalRecordsNeeded || !airtableOffset) {\n break\n }\n } while (airtableOffset)\n \n const paginatedRecords = allRecords.slice(skipValue, skipValue + parseInt(perPageValue))\n \n const formattedRecords = paginatedRecords.map((record) => ({\n id: record.id,\n ...record.fields,\n createdTime: record.createdTime\n }))\n \n const safeData = JSON.parse(JSON.stringify(formattedRecords, 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('Airtable 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");
|
|
25
25
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"airtable.js","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":";;;AAAA,kCAKiB;AAEV,IAAM,sBAAsB,GAAG,UACpC,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,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ,EAAE;QACjF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAA;KAC/E;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhBY,QAAA,sBAAsB,0BAgBlC;AAQM,IAAM,uBAAuB,GAAG,UACrC,MAA+B,EAC/B,SAAiB;IAEjB,IAAM,cAAc,GAAG,MAAwB,CAAA;IAC/C,IAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAA;IACpC,IAAM,mBAAmB,GAAG,cAAc,CAAC,mBAAmB,CAAA;IAE9D,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,iCAAyB,GAAE,
|
|
1
|
+
{"version":3,"file":"airtable.js","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":";;;AAAA,kCAKiB;AAEV,IAAM,sBAAsB,GAAG,UACpC,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,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ,EAAE;QACjF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAA;KAC/E;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhBY,QAAA,sBAAsB,0BAgBlC;AAQM,IAAM,uBAAuB,GAAG,UACrC,MAA+B,EAC/B,SAAiB;IAEjB,IAAM,cAAc,GAAG,MAAwB,CAAA;IAC/C,IAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAA;IACpC,IAAM,mBAAmB,GAAG,cAAc,CAAC,mBAAmB,CAAA;IAE9D,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,iCAAyB,GAAE,41KA2IiB,MAAM,oCAA2B,SAAS,wkBAepD,IAAA,8BAAsB,EAAC,mBAAmB,EAAE;QACpE,eAAe,EAAE,IAAI;KACtB,CAAC,+1CA+CX,CAAA;AACD,CAAC,CAAA;AA1NY,QAAA,uBAAuB,2BA0NnC"}
|