@teleporthq/teleport-plugin-next-data-source 0.43.41 → 0.43.44
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__/ecommerce-product-variant-swatch.test.ts +93 -0
- package/__tests__/fetchers.test.ts +11 -0
- package/dist/cjs/array-mapper-registry.d.ts +0 -1
- package/dist/cjs/array-mapper-registry.d.ts.map +1 -1
- package/dist/cjs/array-mapper-registry.js +0 -3
- package/dist/cjs/array-mapper-registry.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts +3 -3
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +4 -4
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +1 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts +2 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -3
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +12 -3
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +8 -7
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +12 -19
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts +2 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +36 -2
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +2 -1
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +9 -3
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +3 -3
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +4 -4
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/array-mapper-registry.d.ts +0 -1
- package/dist/esm/array-mapper-registry.d.ts.map +1 -1
- package/dist/esm/array-mapper-registry.js +0 -3
- package/dist/esm/array-mapper-registry.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts +3 -3
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +4 -4
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +1 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts +2 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +3 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +12 -3
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +8 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +12 -19
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/transformations/ecommerce-product.d.ts +2 -1
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +36 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +2 -1
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +9 -3
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +3 -3
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +4 -4
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/array-mapper-registry.ts +0 -5
- package/src/data-source-fetchers.ts +10 -5
- package/src/fetchers/postgresql.ts +17 -0
- package/src/fetchers/teleport.ts +38 -3
- package/src/filter-utils.ts +32 -5
- package/src/index.ts +9 -4
- package/src/pagination-plugin.ts +20 -42
- package/src/transformations/ecommerce-product.ts +430 -1
- package/src/transformations/index.ts +28 -4
- package/src/utils.ts +12 -4
|
@@ -1,10 +1,74 @@
|
|
|
1
|
+
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flattens the nested category tree (`ecommerceSettings.categories`, baked at
|
|
5
|
+
* export time) into an `id -> {name, slug, translations}` lookup map,
|
|
6
|
+
* JSON-embedded into the generated transform below so a product's
|
|
7
|
+
* `category_ids` can be resolved to display names with no runtime DB lookup
|
|
8
|
+
* (there is no DB table for the taxonomy — it lives only in the UIDL).
|
|
9
|
+
* `translations` is carried through unresolved (a per-language `{name,
|
|
10
|
+
* description}` map) so `buildEcommerceProduct` can resolve it against the
|
|
11
|
+
* SAME per-request `currentLanguage`/`mainLanguage` used for every other i18n
|
|
12
|
+
* field — `slug` is never localized (it's the language-neutral join key).
|
|
13
|
+
* Tolerant of a missing/malformed tree.
|
|
14
|
+
*/
|
|
15
|
+
const flattenCategoriesById = (
|
|
16
|
+
categories: UIDLEcommerceCategory[] | undefined
|
|
17
|
+
): Record<
|
|
18
|
+
string,
|
|
19
|
+
{ name: string; slug: string; translations?: UIDLEcommerceCategory['translations'] }
|
|
20
|
+
> => {
|
|
21
|
+
const byId: Record<
|
|
22
|
+
string,
|
|
23
|
+
{ name: string; slug: string; translations?: UIDLEcommerceCategory['translations'] }
|
|
24
|
+
> = {}
|
|
25
|
+
const visit = (nodes: UIDLEcommerceCategory[] | undefined): void => {
|
|
26
|
+
if (!Array.isArray(nodes)) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
for (const node of nodes) {
|
|
30
|
+
if (node && typeof node.id === 'string') {
|
|
31
|
+
byId[node.id] = {
|
|
32
|
+
name: node.name || '',
|
|
33
|
+
slug: node.slug || '',
|
|
34
|
+
translations: node.translations,
|
|
35
|
+
}
|
|
36
|
+
visit(node.children)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
visit(categories)
|
|
41
|
+
return byId
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
/**
|
|
2
45
|
* Generates JavaScript code for e-commerce product data transformation.
|
|
3
46
|
* Transforms raw snake_case database records into the camelCase shape
|
|
4
47
|
* that UIDL components expect.
|
|
5
48
|
*/
|
|
6
|
-
export const generateEcommerceProductTransformationCode = (
|
|
49
|
+
export const generateEcommerceProductTransformationCode = (
|
|
50
|
+
categories?: UIDLEcommerceCategory[]
|
|
51
|
+
): string => {
|
|
52
|
+
const categoriesByIdJson = JSON.stringify(flattenCategoriesById(categories))
|
|
53
|
+
|
|
7
54
|
return `
|
|
55
|
+
// Category taxonomy (id -> {name, slug, translations}), baked in at export
|
|
56
|
+
// time — see flattenCategoriesById in ecommerce-product.ts. Empty when the
|
|
57
|
+
// store has no category taxonomy, or products.category_ids simply resolves
|
|
58
|
+
// to nothing.
|
|
59
|
+
var PRODUCT_CATEGORIES_BY_ID = ${categoriesByIdJson}
|
|
60
|
+
|
|
61
|
+
// Resolve a baked category's name to currentLang/mainLang from its
|
|
62
|
+
// translations map (same per-request locale as every other i18n field via
|
|
63
|
+
// resolveI18nField), falling back to the main-language name.
|
|
64
|
+
function resolveCategoryName(categoryInfo, currentLang, mainLang) {
|
|
65
|
+
if (currentLang && mainLang && currentLang !== mainLang && categoryInfo.translations) {
|
|
66
|
+
var override = categoryInfo.translations[currentLang]
|
|
67
|
+
if (override && override.name) return override.name
|
|
68
|
+
}
|
|
69
|
+
return categoryInfo.name
|
|
70
|
+
}
|
|
71
|
+
|
|
8
72
|
// ============================================================
|
|
9
73
|
// E-Commerce Product Transformation
|
|
10
74
|
// ============================================================
|
|
@@ -64,6 +128,16 @@ function buildEcommerceProduct(record, options) {
|
|
|
64
128
|
// Tags - parse as JSON array (handles JSON strings, arrays, comma-separated)
|
|
65
129
|
var tags = parseJsonArray(tagsRaw)
|
|
66
130
|
|
|
131
|
+
// Variant AXES (translatable). The i18n-resolved column already carries the
|
|
132
|
+
// display labels for the current language (with the SAME stable keys as the
|
|
133
|
+
// base column); we just parse + normalize. The purchasable combinations live
|
|
134
|
+
// in teleport_product_variants and are fetched by the variant picker widget.
|
|
135
|
+
var variantOptionsRaw = resolveI18nField(record, 'variant_options', 'variantOptions', currentLang, mainLang)
|
|
136
|
+
var variantOptions = normalizeVariantOptions(variantOptionsRaw)
|
|
137
|
+
// Emitted as a STRING ('true'/'false') to match the strict-equality checks the
|
|
138
|
+
// storefront uses (same convention as outOfStock).
|
|
139
|
+
var requiresVariantSelection = variantOptions.length > 0 ? 'true' : 'false'
|
|
140
|
+
|
|
67
141
|
// Provider fields
|
|
68
142
|
var providerType = pickFirst(record.provider_type, record.providerType) || null
|
|
69
143
|
var providerProductId = pickFirst(record.provider_product_id, record.providerProductId) || null
|
|
@@ -72,6 +146,66 @@ function buildEcommerceProduct(record, options) {
|
|
|
72
146
|
// Metadata
|
|
73
147
|
var metadata = parseJsonObject(record.metadata)
|
|
74
148
|
|
|
149
|
+
// Visible custom properties -> [{key,value,kind,color,imageUrl}] for the
|
|
150
|
+
// storefront pills mapper. MUST mirror buildVisibleProductProperties /
|
|
151
|
+
// classifyProductProperty in
|
|
152
|
+
// apps/gui/app/project-page/features/e-commerce/utils/product-property-display.ts
|
|
153
|
+
// (the canvas-preview SSOT) field-for-field: same reserved-key exclusions,
|
|
154
|
+
// same default-VISIBLE (opt-out) rule, same color/image classification. A
|
|
155
|
+
// property is shown unless it is a reserved key, has an empty value, or is
|
|
156
|
+
// explicitly hidden (false) in the _tqPropVisibility map -- so it also
|
|
157
|
+
// renders for products whose form was never opened (no visibility map yet).
|
|
158
|
+
var propVisibility = parseJsonObject(metadata._tqPropVisibility)
|
|
159
|
+
var visibleProperties = []
|
|
160
|
+
Object.keys(metadata).forEach(function (propKey) {
|
|
161
|
+
if (isReservedProductMetadataKey(propKey)) return
|
|
162
|
+
if (propVisibility[propKey] === false) return
|
|
163
|
+
var rawValue = metadata[propKey]
|
|
164
|
+
if (rawValue == null) return
|
|
165
|
+
var propValue = String(rawValue)
|
|
166
|
+
if (propValue.trim() === '') return
|
|
167
|
+
visibleProperties.push(classifyProductProperty(propKey, propValue))
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Assigned category ids -> resolved {id,name,slug} objects for the
|
|
171
|
+
// storefront category pills mapper. The taxonomy itself is not in the DB
|
|
172
|
+
// (categories are authored in the GUI and live only in
|
|
173
|
+
// ecommerceSettings.categories); resolved here against PRODUCT_CATEGORIES_BY_ID,
|
|
174
|
+
// baked in at export time (see flattenCategoriesById). Unknown/stale ids
|
|
175
|
+
// (a deleted category) are silently dropped.
|
|
176
|
+
var categoryIds = parseJsonArray(record.category_ids)
|
|
177
|
+
var categories = []
|
|
178
|
+
for (var ci = 0; ci < categoryIds.length; ci++) {
|
|
179
|
+
var categoryInfo = PRODUCT_CATEGORIES_BY_ID[categoryIds[ci]]
|
|
180
|
+
if (categoryInfo) {
|
|
181
|
+
categories.push({
|
|
182
|
+
id: categoryIds[ci],
|
|
183
|
+
name: resolveCategoryName(categoryInfo, currentLang, mainLang),
|
|
184
|
+
slug: categoryInfo.slug,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Purchasable variant COMBINATIONS for this product ({id, options map, price,
|
|
190
|
+
// stock, image}). Empty for flat products. Fetched in ONE batched query by
|
|
191
|
+
// transformRecords and passed in via options.variantsByProductId, keyed by
|
|
192
|
+
// product id. The storefront card/details read these to default-select the
|
|
193
|
+
// first in-stock variant, disable dead options, and resolve the chosen id.
|
|
194
|
+
var variantsByProductId = options.variantsByProductId || {}
|
|
195
|
+
var rawVariants = variantsByProductId[id] || []
|
|
196
|
+
var variants = []
|
|
197
|
+
for (var vi = 0; vi < rawVariants.length; vi++) {
|
|
198
|
+
var vrow = rawVariants[vi] || {}
|
|
199
|
+
var vopts = parseJsonObject(vrow.options)
|
|
200
|
+
variants.push({
|
|
201
|
+
id: vrow.id != null ? String(vrow.id) : '',
|
|
202
|
+
options: vopts && typeof vopts === 'object' ? vopts : {},
|
|
203
|
+
price: safeNumber(vrow.price, null),
|
|
204
|
+
quantity: safeNumber(vrow.quantity, null),
|
|
205
|
+
image_url: vrow.image_url ? resolveAssetUrl(vrow.image_url, assetMap) : null,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
75
209
|
// -------------------------------------------------------
|
|
76
210
|
// Image resolution (complex multi-step process)
|
|
77
211
|
// -------------------------------------------------------
|
|
@@ -127,6 +261,44 @@ function buildEcommerceProduct(record, options) {
|
|
|
127
261
|
// stock", same as the upstream cart-availability rewriter).
|
|
128
262
|
var outOfStock = (quantity !== null && !isNaN(quantity) && quantity <= 0) ? 'true' : 'false'
|
|
129
263
|
|
|
264
|
+
// Per-value availability + default-selection flags, derived from the
|
|
265
|
+
// purchasable combinations, so the storefront picker renders its
|
|
266
|
+
// Default / Selected / Unavailable states with NO runtime fetch. The card
|
|
267
|
+
// (N instances) first-paints from these data flags; the details page seeds
|
|
268
|
+
// its selection from the same combinations. axisValueKey (axisKey|valueKey)
|
|
269
|
+
// disambiguates same-named values across axes (Size "s" vs Sleeve "s").
|
|
270
|
+
// Emitted as STRINGS to match the storefront's string-equality convention.
|
|
271
|
+
var variantAxisKeys = []
|
|
272
|
+
for (var ak = 0; ak < variantOptions.length; ak++) { variantAxisKeys.push(variantOptions[ak].key) }
|
|
273
|
+
var firstVariant = pickFirstAvailableVariant(variants, variantAxisKeys)
|
|
274
|
+
var deadValueKeys = computeDeadVariantKeys(variants, variantOptions)
|
|
275
|
+
for (var ai = 0; ai < variantOptions.length; ai++) {
|
|
276
|
+
var vax = variantOptions[ai]
|
|
277
|
+
for (var vj = 0; vj < vax.values.length; vj++) {
|
|
278
|
+
var vval = vax.values[vj]
|
|
279
|
+
var composite = vax.key + '|' + vval.value
|
|
280
|
+
vval.axisValueKey = composite
|
|
281
|
+
vval.isDead = deadValueKeys[composite] ? 'true' : 'false'
|
|
282
|
+
vval.isDefault =
|
|
283
|
+
firstVariant && firstVariant.options && firstVariant.options[vax.key] === vval.value
|
|
284
|
+
? 'true'
|
|
285
|
+
: 'false'
|
|
286
|
+
// Whether THIS value renders as a colour swatch vs a text pill. A colour
|
|
287
|
+
// axis whose value has no resolvable colour degrades to a text pill so it
|
|
288
|
+
// is never an invisible empty circle. Emitted as a string to match the
|
|
289
|
+
// picker's string-equality rendering gate. MUST mirror the renderer
|
|
290
|
+
// enrichment in packages/renderer/src/utils/ecommerce-products.ts.
|
|
291
|
+
vval.showSwatch = vax.type === 'color' && vval.color ? 'true' : 'false'
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
var defaultVariantId = firstVariant && isVariantInStock(firstVariant) ? firstVariant.id : ''
|
|
295
|
+
var defaultVariantPrice = firstVariant ? formatVariantPrice(firstVariant.price, price) : ''
|
|
296
|
+
// Stringified companions: a data-* attr bound to an ARRAY renders
|
|
297
|
+
// '[object Object]', so the picker's on-mount/click workflows read the
|
|
298
|
+
// combinations + axes from these JSON strings via getAttribute.
|
|
299
|
+
var variantOptionsJson = safeStringifyJson(variantOptions)
|
|
300
|
+
var variantsJson = safeStringifyJson(variants)
|
|
301
|
+
|
|
130
302
|
return {
|
|
131
303
|
id: id,
|
|
132
304
|
name: name,
|
|
@@ -147,12 +319,14 @@ function buildEcommerceProduct(record, options) {
|
|
|
147
319
|
weightUnit: weightUnit,
|
|
148
320
|
dimensions: dimensions,
|
|
149
321
|
category: category,
|
|
322
|
+
categories: categories,
|
|
150
323
|
tags: tags,
|
|
151
324
|
imageAlt: imageAlt,
|
|
152
325
|
providerType: providerType,
|
|
153
326
|
providerProductId: providerProductId,
|
|
154
327
|
default_price: defaultPrice,
|
|
155
328
|
metadata: metadata,
|
|
329
|
+
visibleProperties: visibleProperties,
|
|
156
330
|
mainImage: mainImage,
|
|
157
331
|
imageUrl: mainImage,
|
|
158
332
|
image_url: mainImage,
|
|
@@ -162,12 +336,267 @@ function buildEcommerceProduct(record, options) {
|
|
|
162
336
|
updatedAt: updatedAt,
|
|
163
337
|
created: created,
|
|
164
338
|
outOfStock: outOfStock,
|
|
339
|
+
variantOptions: variantOptions,
|
|
340
|
+
requiresVariantSelection: requiresVariantSelection,
|
|
341
|
+
variants: variants,
|
|
342
|
+
variantOptionsJson: variantOptionsJson,
|
|
343
|
+
variantsJson: variantsJson,
|
|
344
|
+
defaultVariantId: defaultVariantId,
|
|
345
|
+
defaultVariantPrice: defaultVariantPrice,
|
|
165
346
|
}
|
|
166
347
|
}
|
|
167
348
|
|
|
349
|
+
// --- Variant availability helpers (mirror packages/renderer ecommerce-products) ---
|
|
350
|
+
// A variant is in stock when its quantity is null (unlimited) or > 0.
|
|
351
|
+
function isVariantInStock(v) {
|
|
352
|
+
return !v ? false : v.quantity == null || v.quantity > 0
|
|
353
|
+
}
|
|
354
|
+
function variantCoversAllAxes(v, axisKeys) {
|
|
355
|
+
if (!v || !v.options) return false
|
|
356
|
+
for (var i = 0; i < axisKeys.length; i++) {
|
|
357
|
+
if (v.options[axisKeys[i]] == null) return false
|
|
358
|
+
}
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
// First in-stock combination covering every axis; falls back to the first
|
|
362
|
+
// covering combination (even if OOS) so a fully-OOS product still has a
|
|
363
|
+
// visual default; null when no combination covers the axes.
|
|
364
|
+
function pickFirstAvailableVariant(variants, axisKeys) {
|
|
365
|
+
if (!Array.isArray(variants) || !axisKeys.length) return null
|
|
366
|
+
for (var i = 0; i < variants.length; i++) {
|
|
367
|
+
if (variantCoversAllAxes(variants[i], axisKeys) && isVariantInStock(variants[i])) return variants[i]
|
|
368
|
+
}
|
|
369
|
+
for (var j = 0; j < variants.length; j++) {
|
|
370
|
+
if (variantCoversAllAxes(variants[j], axisKeys)) return variants[j]
|
|
371
|
+
}
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
// Map of composite "axisKey|valueKey" -> true for every value that appears in
|
|
375
|
+
// NO in-stock combination (rendered as the Unavailable state).
|
|
376
|
+
function computeDeadVariantKeys(variants, axes) {
|
|
377
|
+
// No combinations attached (flat product, zero-variant product, or editor
|
|
378
|
+
// preview) -> nothing is dead; every value stays selectable. Dead-marking is
|
|
379
|
+
// only meaningful relative to combinations that actually exist.
|
|
380
|
+
if (!Array.isArray(variants) || variants.length === 0) return {}
|
|
381
|
+
var alive = {}
|
|
382
|
+
for (var i = 0; i < variants.length; i++) {
|
|
383
|
+
var v = variants[i]
|
|
384
|
+
if (!isVariantInStock(v)) continue
|
|
385
|
+
for (var k in v.options) {
|
|
386
|
+
if (Object.prototype.hasOwnProperty.call(v.options, k)) alive[k + '|' + v.options[k]] = true
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
var dead = {}
|
|
390
|
+
for (var a = 0; a < axes.length; a++) {
|
|
391
|
+
var ax = axes[a]
|
|
392
|
+
for (var b = 0; b < ax.values.length; b++) {
|
|
393
|
+
var key = ax.key + '|' + ax.values[b].value
|
|
394
|
+
if (!alive[key]) dead[key] = true
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return dead
|
|
398
|
+
}
|
|
399
|
+
// Variant price with base-price inheritance (null variant price -> product price).
|
|
400
|
+
function formatVariantPrice(vprice, basePrice) {
|
|
401
|
+
var p = vprice == null || isNaN(vprice) ? basePrice : vprice
|
|
402
|
+
return String(Number(p).toFixed(2))
|
|
403
|
+
}
|
|
404
|
+
function safeStringifyJson(obj) {
|
|
405
|
+
try {
|
|
406
|
+
return JSON.stringify(obj)
|
|
407
|
+
} catch (e) {
|
|
408
|
+
return '[]'
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Parse + normalize the variant_options JSON into render-ready axes. Tolerant of
|
|
413
|
+
// null / malformed / partial data (returns []). Keeps ONLY the fields the pill
|
|
414
|
+
// widget needs; the labels are already language-resolved by the caller.
|
|
415
|
+
function normalizeVariantOptions(raw) {
|
|
416
|
+
if (!raw) return []
|
|
417
|
+
var parsed = raw
|
|
418
|
+
if (typeof raw === 'string') {
|
|
419
|
+
try { parsed = JSON.parse(raw) } catch (e) { return [] }
|
|
420
|
+
}
|
|
421
|
+
if (!Array.isArray(parsed)) return []
|
|
422
|
+
var out = []
|
|
423
|
+
for (var i = 0; i < parsed.length; i++) {
|
|
424
|
+
var axis = parsed[i]
|
|
425
|
+
if (!axis || typeof axis !== 'object' || !Array.isArray(axis.values)) continue
|
|
426
|
+
var name = typeof axis.name === 'string' ? axis.name : ''
|
|
427
|
+
var key = typeof axis.key === 'string' && axis.key ? axis.key : name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
428
|
+
if (!key) continue
|
|
429
|
+
var type = axis.type === 'color' ? 'color' : 'text'
|
|
430
|
+
var values = []
|
|
431
|
+
for (var j = 0; j < axis.values.length; j++) {
|
|
432
|
+
var v = axis.values[j]
|
|
433
|
+
if (!v || typeof v !== 'object') continue
|
|
434
|
+
var label = typeof v.label === 'string' ? v.label : ''
|
|
435
|
+
var value = typeof v.value === 'string' && v.value ? v.value : label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
436
|
+
if (!value) continue
|
|
437
|
+
var entry = { value: value, label: label }
|
|
438
|
+
if (type === 'color' && typeof v.color === 'string' && v.color) entry.color = v.color
|
|
439
|
+
values.push(entry)
|
|
440
|
+
}
|
|
441
|
+
if (!values.length) continue
|
|
442
|
+
out.push({ key: key, name: name, type: type, values: values })
|
|
443
|
+
}
|
|
444
|
+
return out
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// --- Custom-property classification (MUST mirror product-property-display.ts) ---
|
|
448
|
+
|
|
449
|
+
// Product-attribute keys that can leak into the free-form \`metadata\` map (e.g.
|
|
450
|
+
// from a payment-provider sync or legacy data) but are NOT user custom
|
|
451
|
+
// properties. Kept in sync with the GUI's RESERVED_PRODUCT_METADATA_KEYS.
|
|
452
|
+
var RESERVED_PRODUCT_METADATA_KEYS = {
|
|
453
|
+
paymentType: true, recurringInterval: true, recurringIntervalCount: true,
|
|
454
|
+
price: true, currency: true, status: true, sku: true, weightUnit: true,
|
|
455
|
+
dimensions: true, category: true, brand: true, gtin: true, mpn: true,
|
|
456
|
+
condition: true, tags: true,
|
|
457
|
+
}
|
|
458
|
+
function isReservedProductMetadataKey(key) {
|
|
459
|
+
return !!RESERVED_PRODUCT_METADATA_KEYS[key] ||
|
|
460
|
+
key.indexOf('mainImage') === 0 ||
|
|
461
|
+
key.indexOf('secondaryImage') === 0 ||
|
|
462
|
+
key.indexOf('_tq') === 0
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
var CSS_NAMED_COLORS = {
|
|
466
|
+
aliceblue: 1, antiquewhite: 1, aqua: 1, aquamarine: 1, azure: 1, beige: 1, bisque: 1,
|
|
467
|
+
black: 1, blanchedalmond: 1, blue: 1, blueviolet: 1, brown: 1, burlywood: 1,
|
|
468
|
+
cadetblue: 1, chartreuse: 1, chocolate: 1, coral: 1, cornflowerblue: 1, cornsilk: 1,
|
|
469
|
+
crimson: 1, cyan: 1, darkblue: 1, darkcyan: 1, darkgoldenrod: 1, darkgray: 1,
|
|
470
|
+
darkgrey: 1, darkgreen: 1, darkkhaki: 1, darkmagenta: 1, darkolivegreen: 1,
|
|
471
|
+
darkorange: 1, darkorchid: 1, darkred: 1, darksalmon: 1, darkseagreen: 1,
|
|
472
|
+
darkslateblue: 1, darkslategray: 1, darkslategrey: 1, darkturquoise: 1,
|
|
473
|
+
darkviolet: 1, deeppink: 1, deepskyblue: 1, dimgray: 1, dimgrey: 1, dodgerblue: 1,
|
|
474
|
+
firebrick: 1, floralwhite: 1, forestgreen: 1, fuchsia: 1, gainsboro: 1,
|
|
475
|
+
ghostwhite: 1, gold: 1, goldenrod: 1, gray: 1, grey: 1, green: 1, greenyellow: 1,
|
|
476
|
+
honeydew: 1, hotpink: 1, indianred: 1, indigo: 1, ivory: 1, khaki: 1, lavender: 1,
|
|
477
|
+
lavenderblush: 1, lawngreen: 1, lemonchiffon: 1, lightblue: 1, lightcoral: 1,
|
|
478
|
+
lightcyan: 1, lightgoldenrodyellow: 1, lightgray: 1, lightgrey: 1, lightgreen: 1,
|
|
479
|
+
lightpink: 1, lightsalmon: 1, lightseagreen: 1, lightskyblue: 1, lightslategray: 1,
|
|
480
|
+
lightslategrey: 1, lightsteelblue: 1, lightyellow: 1, lime: 1, limegreen: 1,
|
|
481
|
+
linen: 1, magenta: 1, maroon: 1, mediumaquamarine: 1, mediumblue: 1,
|
|
482
|
+
mediumorchid: 1, mediumpurple: 1, mediumseagreen: 1, mediumslateblue: 1,
|
|
483
|
+
mediumspringgreen: 1, mediumturquoise: 1, mediumvioletred: 1, midnightblue: 1,
|
|
484
|
+
mintcream: 1, mistyrose: 1, moccasin: 1, navajowhite: 1, navy: 1, oldlace: 1,
|
|
485
|
+
olive: 1, olivedrab: 1, orange: 1, orangered: 1, orchid: 1, palegoldenrod: 1,
|
|
486
|
+
palegreen: 1, paleturquoise: 1, palevioletred: 1, papayawhip: 1, peachpuff: 1,
|
|
487
|
+
peru: 1, pink: 1, plum: 1, powderblue: 1, purple: 1, rebeccapurple: 1, red: 1,
|
|
488
|
+
rosybrown: 1, royalblue: 1, saddlebrown: 1, salmon: 1, sandybrown: 1, seagreen: 1,
|
|
489
|
+
seashell: 1, sienna: 1, silver: 1, skyblue: 1, slateblue: 1, slategray: 1,
|
|
490
|
+
slategrey: 1, snow: 1, springgreen: 1, steelblue: 1, tan: 1, teal: 1, thistle: 1,
|
|
491
|
+
tomato: 1, turquoise: 1, violet: 1, wheat: 1, white: 1, whitesmoke: 1, yellow: 1,
|
|
492
|
+
yellowgreen: 1, transparent: 1,
|
|
493
|
+
}
|
|
494
|
+
var HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
|
|
495
|
+
var FUNCTIONAL_COLOR_RE = /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\\((?:[0-9a-z%.,\\s/+-]+)\\)$/i
|
|
496
|
+
var COLOR_KEY_WORDS = {
|
|
497
|
+
color: 1, colors: 1, colour: 1, colours: 1, shade: 1, shades: 1, hue: 1, hues: 1,
|
|
498
|
+
tint: 1, tints: 1, tone: 1, tones: 1, swatch: 1, swatches: 1,
|
|
499
|
+
}
|
|
500
|
+
function splitKeyWords(key) {
|
|
501
|
+
return key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
502
|
+
}
|
|
503
|
+
function keyIndicatesColor(key) {
|
|
504
|
+
var normalized = key.toLowerCase()
|
|
505
|
+
if (/colou?r/.test(normalized)) return true
|
|
506
|
+
return splitKeyWords(key).some(function(word) { return !!COLOR_KEY_WORDS[word] })
|
|
507
|
+
}
|
|
508
|
+
function resolvePropertyColor(key, value) {
|
|
509
|
+
var v = value.trim()
|
|
510
|
+
if (!v) return ''
|
|
511
|
+
if (HEX_COLOR_RE.test(v) || FUNCTIONAL_COLOR_RE.test(v)) return v
|
|
512
|
+
if (CSS_NAMED_COLORS[v.toLowerCase()] && keyIndicatesColor(key)) return v
|
|
513
|
+
return ''
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
var DATA_IMAGE_URI_RE = /^data:image\\//i
|
|
517
|
+
var ABSOLUTE_URL_RE = /^(?:https?:)?\\/\\//i
|
|
518
|
+
var IMAGE_EXTENSION_RE = /\\.(?:png|jpe?g|gif|webp|svg|avif|bmp|ico|tiff?|heic|heif)$/i
|
|
519
|
+
var IMAGE_KEY_WORDS = {
|
|
520
|
+
image: 1, images: 1, img: 1, imgs: 1, photo: 1, photos: 1, picture: 1, pictures: 1,
|
|
521
|
+
pic: 1, pics: 1, thumbnail: 1, thumbnails: 1, thumb: 1, thumbs: 1, icon: 1, icons: 1,
|
|
522
|
+
logo: 1, logos: 1, avatar: 1, avatars: 1, banner: 1, banners: 1, cover: 1, covers: 1,
|
|
523
|
+
gallery: 1, screenshot: 1, screenshots: 1, poster: 1, posters: 1, artwork: 1,
|
|
524
|
+
graphic: 1, graphics: 1, illustration: 1, wallpaper: 1, preview: 1, snapshot: 1,
|
|
525
|
+
media: 1, visual: 1, visuals: 1,
|
|
526
|
+
}
|
|
527
|
+
function keyIndicatesImage(key) {
|
|
528
|
+
var normalized = key.toLowerCase()
|
|
529
|
+
if (/(?:image|img|photo|picture|thumb|icon|logo|avatar|banner|gallery|screenshot|poster|artwork|graphic|wallpaper)/.test(normalized)) return true
|
|
530
|
+
return splitKeyWords(key).some(function(word) { return !!IMAGE_KEY_WORDS[word] })
|
|
531
|
+
}
|
|
532
|
+
function stripUrlQueryAndHash(url) {
|
|
533
|
+
return url.split(/[?#]/)[0]
|
|
534
|
+
}
|
|
535
|
+
function resolvePropertyImage(key, value) {
|
|
536
|
+
var v = value.trim()
|
|
537
|
+
if (!v) return ''
|
|
538
|
+
if (DATA_IMAGE_URI_RE.test(v)) return v
|
|
539
|
+
var isAbsoluteUrl = ABSOLUTE_URL_RE.test(v)
|
|
540
|
+
var isRootRelative = v.charAt(0) === '/' && !isAbsoluteUrl
|
|
541
|
+
if (!isAbsoluteUrl && !isRootRelative) return ''
|
|
542
|
+
if (IMAGE_EXTENSION_RE.test(stripUrlQueryAndHash(v))) return v
|
|
543
|
+
if (isAbsoluteUrl && keyIndicatesImage(key)) return v
|
|
544
|
+
return ''
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Image wins over colour (a URL can never be a CSS colour, but checking image
|
|
548
|
+
// first keeps the precedence explicit), text is the fallback.
|
|
549
|
+
function classifyProductProperty(key, value) {
|
|
550
|
+
var imageUrl = resolvePropertyImage(key, value)
|
|
551
|
+
if (imageUrl) return { key: key, value: value, kind: 'image', color: '', imageUrl: imageUrl }
|
|
552
|
+
var color = resolvePropertyColor(key, value)
|
|
553
|
+
if (color) return { key: key, value: value, kind: 'color', color: color, imageUrl: '' }
|
|
554
|
+
return { key: key, value: value, kind: 'text', color: '', imageUrl: '' }
|
|
555
|
+
}
|
|
556
|
+
|
|
168
557
|
function transformEcommerceProducts(records, options) {
|
|
169
558
|
if (!Array.isArray(records)) return []
|
|
170
559
|
return records.map(function(record) { return buildEcommerceProduct(record, options) })
|
|
171
560
|
}
|
|
561
|
+
|
|
562
|
+
// Batched fetch of the purchasable variant rows for a set of product ids, keyed
|
|
563
|
+
// by product_id, so buildEcommerceProduct can attach product.variants without a
|
|
564
|
+
// per-product query. Best-effort: a store with no variants (or before the table
|
|
565
|
+
// exists) yields an empty map and flat products keep variants: [].
|
|
566
|
+
async function getVariantsMap(getClientFn, productIds) {
|
|
567
|
+
var map = {}
|
|
568
|
+
if (!Array.isArray(productIds) || productIds.length === 0) return map
|
|
569
|
+
var uniqueIds = []
|
|
570
|
+
var seenIds = {}
|
|
571
|
+
for (var u = 0; u < productIds.length; u++) {
|
|
572
|
+
var pid0 = productIds[u]
|
|
573
|
+
if (pid0 != null && !seenIds[pid0]) { seenIds[pid0] = true; uniqueIds.push(pid0) }
|
|
574
|
+
}
|
|
575
|
+
if (uniqueIds.length === 0) return map
|
|
576
|
+
var client
|
|
577
|
+
try {
|
|
578
|
+
client = getClientFn()
|
|
579
|
+
await client.connect()
|
|
580
|
+
var result = await client.query(
|
|
581
|
+
'SELECT id, product_id, options, price, quantity, image_url FROM teleport_product_variants WHERE product_id = ANY($1) ORDER BY position ASC, id ASC',
|
|
582
|
+
[uniqueIds]
|
|
583
|
+
)
|
|
584
|
+
if (result && result.rows) {
|
|
585
|
+
for (var r = 0; r < result.rows.length; r++) {
|
|
586
|
+
var row = result.rows[r]
|
|
587
|
+
var pid = row.product_id
|
|
588
|
+
if (!map[pid]) map[pid] = []
|
|
589
|
+
map[pid].push(row)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
// teleport_product_variants may not exist for a non-variant store.
|
|
594
|
+
} finally {
|
|
595
|
+
if (client) {
|
|
596
|
+
try { await client.end() } catch (e) { /* ignore */ }
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return map
|
|
600
|
+
}
|
|
172
601
|
`
|
|
173
602
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
1
2
|
import { generateSharedTransformationCode } from './shared-utils'
|
|
2
3
|
import { generateBlogPostTransformationCode } from './blog-post'
|
|
3
4
|
import { generateEcommerceProductTransformationCode } from './ecommerce-product'
|
|
@@ -48,7 +49,10 @@ export const detectTransformationType = (tableName: string): TransformationType
|
|
|
48
49
|
* to be included in the generated data source fetcher file.
|
|
49
50
|
* Returns empty string if no transformation is needed for this table.
|
|
50
51
|
*/
|
|
51
|
-
export const getTransformationCode = (
|
|
52
|
+
export const getTransformationCode = (
|
|
53
|
+
tableName: string,
|
|
54
|
+
categories?: UIDLEcommerceCategory[]
|
|
55
|
+
): string => {
|
|
52
56
|
const type = detectTransformationType(tableName)
|
|
53
57
|
if (!type) {
|
|
54
58
|
return ''
|
|
@@ -60,7 +64,7 @@ export const getTransformationCode = (tableName: string): string => {
|
|
|
60
64
|
case 'blog-post':
|
|
61
65
|
return shared + generateBlogPostTransformationCode()
|
|
62
66
|
case 'ecommerce-product':
|
|
63
|
-
return shared + generateEcommerceProductTransformationCode()
|
|
67
|
+
return shared + generateEcommerceProductTransformationCode(categories)
|
|
64
68
|
default:
|
|
65
69
|
return ''
|
|
66
70
|
}
|
|
@@ -101,6 +105,26 @@ export const getTransformWrapperCode = (tableName: string): string => {
|
|
|
101
105
|
|
|
102
106
|
const transformFn = type === 'blog-post' ? 'transformBlogPosts' : 'transformEcommerceProducts'
|
|
103
107
|
|
|
108
|
+
// Products additionally get their purchasable variant combinations attached in
|
|
109
|
+
// ONE batched query (keyed by product id). Blog posts have no such enrichment.
|
|
110
|
+
const variantEnrichment =
|
|
111
|
+
type === 'ecommerce-product'
|
|
112
|
+
? `
|
|
113
|
+
var variantsByProductId = {}
|
|
114
|
+
try {
|
|
115
|
+
var __variantPids = []
|
|
116
|
+
for (var __i = 0; __i < records.length; __i++) {
|
|
117
|
+
if (records[__i] && records[__i].id != null) __variantPids.push(records[__i].id)
|
|
118
|
+
}
|
|
119
|
+
variantsByProductId = await getVariantsMap(getClientFn, __variantPids)
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Variant enrichment is best-effort; flat products keep variants: [].
|
|
122
|
+
}`
|
|
123
|
+
: ''
|
|
124
|
+
|
|
125
|
+
const variantOption =
|
|
126
|
+
type === 'ecommerce-product' ? ', variantsByProductId: variantsByProductId' : ''
|
|
127
|
+
|
|
104
128
|
return `
|
|
105
129
|
async function transformRecords(records, getClientFn, reqQuery) {
|
|
106
130
|
var assetMap = {}
|
|
@@ -108,10 +132,10 @@ async function transformRecords(records, getClientFn, reqQuery) {
|
|
|
108
132
|
assetMap = await getAssetMap(getClientFn)
|
|
109
133
|
} catch (e) {
|
|
110
134
|
// Asset resolution is best-effort; continue without it
|
|
111
|
-
}
|
|
135
|
+
}${variantEnrichment}
|
|
112
136
|
var currentLanguage = (reqQuery && reqQuery.lang) || null
|
|
113
137
|
var mainLanguage = (reqQuery && reqQuery.mainLang) || null
|
|
114
|
-
var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage }
|
|
138
|
+
var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage${variantOption} }
|
|
115
139
|
return ${transformFn}(records, options)
|
|
116
140
|
}
|
|
117
141
|
`
|
package/src/utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
UIDLDataSource,
|
|
8
8
|
DataSourceType,
|
|
9
9
|
ChunkType,
|
|
10
|
+
UIDLEcommerceCategory,
|
|
10
11
|
} from '@teleporthq/teleport-types'
|
|
11
12
|
import * as types from '@babel/types'
|
|
12
13
|
import { ASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
@@ -189,7 +190,8 @@ export const extractDataSourceIntoNextAPIFolder = (
|
|
|
189
190
|
node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
|
|
190
191
|
dataSources: Record<string, UIDLDataSource>,
|
|
191
192
|
componentChunk: ChunkDefinition,
|
|
192
|
-
extractedResources: GeneratorOptions['extractedResources']
|
|
193
|
+
extractedResources: GeneratorOptions['extractedResources'],
|
|
194
|
+
categories?: UIDLEcommerceCategory[]
|
|
193
195
|
) => {
|
|
194
196
|
try {
|
|
195
197
|
// Validate node content structure
|
|
@@ -443,7 +445,7 @@ export default dataSourceModule.handler
|
|
|
443
445
|
// Generate fetcher code for API route (exports just the handler)
|
|
444
446
|
let fetcherCode: string
|
|
445
447
|
try {
|
|
446
|
-
fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '', true)
|
|
448
|
+
fetcherCode = generateDataSourceFetcherWithCore(dataSource, tableName || '', true, categories)
|
|
447
449
|
} catch (error) {
|
|
448
450
|
return
|
|
449
451
|
}
|
|
@@ -892,7 +894,8 @@ export const extractDataSourceIntoGetStaticProps = (
|
|
|
892
894
|
extractedResources: GeneratorOptions['extractedResources'],
|
|
893
895
|
dependencies: Record<string, any>,
|
|
894
896
|
dynamicRouteAttr?: string,
|
|
895
|
-
folderPath?: string[]
|
|
897
|
+
folderPath?: string[],
|
|
898
|
+
categories?: UIDLEcommerceCategory[]
|
|
896
899
|
): { success: boolean; chunk?: any } => {
|
|
897
900
|
try {
|
|
898
901
|
// Validate node content
|
|
@@ -1216,7 +1219,12 @@ export const extractDataSourceIntoGetStaticProps = (
|
|
|
1216
1219
|
// Generate fetcher code with core function
|
|
1217
1220
|
let fetcherCode: string
|
|
1218
1221
|
try {
|
|
1219
|
-
fetcherCode = generateDataSourceFetcherWithCore(
|
|
1222
|
+
fetcherCode = generateDataSourceFetcherWithCore(
|
|
1223
|
+
dataSource,
|
|
1224
|
+
tableName || '',
|
|
1225
|
+
false,
|
|
1226
|
+
categories
|
|
1227
|
+
)
|
|
1220
1228
|
} catch (error) {
|
|
1221
1229
|
// tslint:disable-next-line:no-console
|
|
1222
1230
|
console.error(`Failed to generate fetcher for ${dataSourceType} (${dataSourceId}):`, error)
|