@teleporthq/teleport-plugin-next-data-source 0.44.15 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/__tests__/_helpers/exposure.ts +29 -0
  2. package/__tests__/_helpers/first-page-runtime.ts +12 -2
  3. package/__tests__/blog-post-contents.test.ts +3 -1
  4. package/__tests__/blog-post-page-enrichment.test.ts +2 -1
  5. package/__tests__/blog-post-seo-fields.test.ts +124 -0
  6. package/__tests__/browser-row-policy.test.ts +65 -5
  7. package/__tests__/data-source-cache-wrapping.test.ts +48 -7
  8. package/__tests__/external-read-routes-guard.test.ts +991 -0
  9. package/__tests__/null-filter-where.test.ts +19 -2
  10. package/__tests__/range-control.test.ts +29 -0
  11. package/__tests__/search-relevance.test.ts +42 -6
  12. package/__tests__/table-read-route-protected-tables.test.ts +54 -4
  13. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  14. package/dist/cjs/count-fetchers.js +1 -1
  15. package/dist/cjs/count-fetchers.js.map +1 -1
  16. package/dist/cjs/data-source-fetchers.d.ts +4 -0
  17. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  18. package/dist/cjs/data-source-fetchers.js +97 -20
  19. package/dist/cjs/data-source-fetchers.js.map +1 -1
  20. package/dist/cjs/fetchers/postgresql.d.ts +3 -1
  21. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/postgresql.js +11 -3
  23. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  24. package/dist/cjs/fetchers/raw-query.d.ts +20 -1
  25. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/raw-query.js +37 -7
  27. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  28. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/redshift.js +2 -1
  30. package/dist/cjs/fetchers/redshift.js.map +1 -1
  31. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/teleport.js +5 -3
  33. package/dist/cjs/fetchers/teleport.js.map +1 -1
  34. package/dist/cjs/fetchers/utils/external-read-guard.d.ts +53 -0
  35. package/dist/cjs/fetchers/utils/external-read-guard.d.ts.map +1 -0
  36. package/dist/cjs/fetchers/utils/external-read-guard.js +126 -0
  37. package/dist/cjs/fetchers/utils/external-read-guard.js.map +1 -0
  38. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +76 -12
  39. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/utils/table-access-guard.js +123 -15
  41. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -1
  42. package/dist/cjs/index.js +1 -1
  43. package/dist/cjs/pagination-controls.d.ts +4 -1
  44. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  45. package/dist/cjs/pagination-controls.js +4 -1
  46. package/dist/cjs/pagination-controls.js.map +1 -1
  47. package/dist/cjs/pagination-plugin.js +1 -0
  48. package/dist/cjs/pagination-plugin.js.map +1 -1
  49. package/dist/cjs/range-control.d.ts +10 -5
  50. package/dist/cjs/range-control.d.ts.map +1 -1
  51. package/dist/cjs/range-control.js +23 -2
  52. package/dist/cjs/range-control.js.map +1 -1
  53. package/dist/cjs/transformations/blog-post.d.ts +5 -0
  54. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  55. package/dist/cjs/transformations/blog-post.js +6 -2
  56. package/dist/cjs/transformations/blog-post.js.map +1 -1
  57. package/dist/cjs/transformations/index.d.ts +18 -2
  58. package/dist/cjs/transformations/index.d.ts.map +1 -1
  59. package/dist/cjs/transformations/index.js +26 -1
  60. package/dist/cjs/transformations/index.js.map +1 -1
  61. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  62. package/dist/esm/count-fetchers.d.ts.map +1 -1
  63. package/dist/esm/count-fetchers.js +1 -1
  64. package/dist/esm/count-fetchers.js.map +1 -1
  65. package/dist/esm/data-source-fetchers.d.ts +4 -0
  66. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  67. package/dist/esm/data-source-fetchers.js +97 -20
  68. package/dist/esm/data-source-fetchers.js.map +1 -1
  69. package/dist/esm/fetchers/postgresql.d.ts +3 -1
  70. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  71. package/dist/esm/fetchers/postgresql.js +12 -4
  72. package/dist/esm/fetchers/postgresql.js.map +1 -1
  73. package/dist/esm/fetchers/raw-query.d.ts +20 -1
  74. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  75. package/dist/esm/fetchers/raw-query.js +38 -8
  76. package/dist/esm/fetchers/raw-query.js.map +1 -1
  77. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  78. package/dist/esm/fetchers/redshift.js +2 -1
  79. package/dist/esm/fetchers/redshift.js.map +1 -1
  80. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  81. package/dist/esm/fetchers/teleport.js +6 -4
  82. package/dist/esm/fetchers/teleport.js.map +1 -1
  83. package/dist/esm/fetchers/utils/external-read-guard.d.ts +53 -0
  84. package/dist/esm/fetchers/utils/external-read-guard.d.ts.map +1 -0
  85. package/dist/esm/fetchers/utils/external-read-guard.js +118 -0
  86. package/dist/esm/fetchers/utils/external-read-guard.js.map +1 -0
  87. package/dist/esm/fetchers/utils/table-access-guard.d.ts +76 -12
  88. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -1
  89. package/dist/esm/fetchers/utils/table-access-guard.js +119 -15
  90. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -1
  91. package/dist/esm/index.js +1 -1
  92. package/dist/esm/pagination-controls.d.ts +4 -1
  93. package/dist/esm/pagination-controls.d.ts.map +1 -1
  94. package/dist/esm/pagination-controls.js +4 -1
  95. package/dist/esm/pagination-controls.js.map +1 -1
  96. package/dist/esm/pagination-plugin.js +1 -0
  97. package/dist/esm/pagination-plugin.js.map +1 -1
  98. package/dist/esm/range-control.d.ts +10 -5
  99. package/dist/esm/range-control.d.ts.map +1 -1
  100. package/dist/esm/range-control.js +23 -2
  101. package/dist/esm/range-control.js.map +1 -1
  102. package/dist/esm/transformations/blog-post.d.ts +5 -0
  103. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  104. package/dist/esm/transformations/blog-post.js +6 -2
  105. package/dist/esm/transformations/blog-post.js.map +1 -1
  106. package/dist/esm/transformations/index.d.ts +18 -2
  107. package/dist/esm/transformations/index.d.ts.map +1 -1
  108. package/dist/esm/transformations/index.js +27 -2
  109. package/dist/esm/transformations/index.js.map +1 -1
  110. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +5 -5
  112. package/src/count-fetchers.ts +10 -2
  113. package/src/data-source-fetchers.ts +144 -23
  114. package/src/fetchers/postgresql.ts +38 -9
  115. package/src/fetchers/raw-query.ts +125 -13
  116. package/src/fetchers/redshift.ts +3 -0
  117. package/src/fetchers/teleport.ts +29 -8
  118. package/src/fetchers/utils/external-read-guard.ts +289 -0
  119. package/src/fetchers/utils/table-access-guard.ts +469 -20
  120. package/src/index.ts +1 -1
  121. package/src/pagination-controls.ts +4 -1
  122. package/src/pagination-plugin.ts +1 -0
  123. package/src/range-control.ts +40 -8
  124. package/src/transformations/blog-post.ts +107 -4
  125. package/src/transformations/index.ts +44 -1
@@ -24,8 +24,18 @@ export interface BlogPostTransformOptions {
24
24
  contents?: boolean
25
25
  /** Manual Order: Previous / Next walk the post's category as arranged — see `blog-adjacent-posts.ts`. */
26
26
  arranged?: boolean
27
+ /**
28
+ * Every site language's code, the main one included: what `slugByLocale`
29
+ * reads the `<lang>_slug` columns for (the head's hreflang links).
30
+ */
31
+ locales?: string[]
27
32
  }
28
33
 
34
+ /** How much of a body stands in for a missing meta description. */
35
+ const META_DESCRIPTION_LENGTH = 155
36
+ /** An edit this long after publishing is worth an "Updated on" line. */
37
+ const UPDATED_AFTER_MS = 24 * 60 * 60 * 1000
38
+
29
39
  /**
30
40
  * Generates JavaScript code for blog post data transformation.
31
41
  * Transforms raw snake_case database records into the camelCase shape
@@ -47,6 +57,67 @@ ${RichTextContentCodegen.generateLegacyRichTextNormalizerCode()}
47
57
  var BLOG_HEADING_ANCHORS = ${options.headingAnchors === true}
48
58
  var BLOG_CONTENTS = ${options.contents === true}
49
59
  var BLOG_PICTURE_STAND_IN = ${tables.pictureStandIn !== false}
60
+ var BLOG_LOCALES = ${JSON.stringify(options.locales ?? [])}
61
+ var BLOG_META_DESCRIPTION_LENGTH = ${META_DESCRIPTION_LENGTH}
62
+ var BLOG_UPDATED_AFTER_MS = ${UPDATED_AFTER_MS}
63
+
64
+ // The words a reader sees, for a count or a summary.
65
+ function blogPlainText(html) {
66
+ // A block's end is a word break; an inline tag (<em>, <a>) is not.
67
+ var text = String(html || '')
68
+ .replace(/<\\/(p|li|h[1-6]|div|details|summary|section|blockquote|tr|td|th)>|<br\\s*\\/?>/gi, ' ')
69
+ .replace(/<[^>]*>/g, '')
70
+ return decodeHtmlEntities(text).replace(/\\s+/g, ' ').trim()
71
+ }
72
+
73
+ // The opening of the body as a meta description: cut at a word, never mid-word.
74
+ function blogSummaryOf(text) {
75
+ if (!text) return null
76
+ if (text.length <= BLOG_META_DESCRIPTION_LENGTH) return text
77
+ var cut = text.slice(0, BLOG_META_DESCRIPTION_LENGTH)
78
+ var space = cut.lastIndexOf(' ')
79
+ return (space > 0 ? cut.slice(0, space) : cut).trim()
80
+ }
81
+
82
+ // A body's own H1 becomes an H2: the page's title is the one H1 a post page has.
83
+ function blogDemoteH1(html) {
84
+ if (typeof html !== 'string' || !/<h1[\\s>]/i.test(html)) return html
85
+ return html.replace(/<(\\/?)h1(\\s[^>]*)?>/gi, function(match, closing, attrs) {
86
+ return '<' + closing + 'h2' + (attrs || '') + '>'
87
+ })
88
+ }
89
+
90
+ // The FAQ pairs of a body, text only: each <details> of a .tq-faq section with
91
+ // a question in its <summary> and the rest as the answer. What the page's
92
+ // FAQPage structured data lists.
93
+ function blogFaqOf(html) {
94
+ var pairs = []
95
+ if (typeof html !== 'string' || html.indexOf('tq-faq') === -1) return pairs
96
+ var sections = html.match(/<section[^>]*class="[^"]*\\btq-faq\\b[^"]*"[^>]*>[\\s\\S]*?<\\/section>/gi) || []
97
+ for (var s = 0; s < sections.length; s++) {
98
+ var details = sections[s].match(/<details[\\s>][\\s\\S]*?<\\/details>/gi) || []
99
+ for (var d = 0; d < details.length; d++) {
100
+ var summary = details[d].match(/<summary[^>]*>([\\s\\S]*?)<\\/summary>/i)
101
+ var question = summary ? blogPlainText(summary[1]) : ''
102
+ var answer = blogPlainText(details[d].replace(/<summary[^>]*>[\\s\\S]*?<\\/summary>/i, ' ').replace(/<\\/?details[^>]*>/gi, ' '))
103
+ if (question && answer) pairs.push({ question: question, answer: answer })
104
+ }
105
+ }
106
+ return pairs
107
+ }
108
+
109
+ // The post's slug per site language, for the head's hreflang links: the
110
+ // translated column where it holds one, the main slug for the main language.
111
+ function blogSlugByLocale(record, mainLang) {
112
+ var out = {}
113
+ for (var l = 0; l < BLOG_LOCALES.length; l++) {
114
+ var locale = BLOG_LOCALES[l]
115
+ // The base column holds the main language; a translation its own copy.
116
+ var value = locale === mainLang ? record.slug : record[locale + '_slug']
117
+ if (typeof value === 'string' && value.trim()) out[locale] = value.trim()
118
+ }
119
+ return out
120
+ }
50
121
 
51
122
  // A post's reading time from its body, for one that stores none: its words at
52
123
  // about 200 a minute, at least one — what the editor's post form computes. A
@@ -71,11 +142,14 @@ function buildBlogPost(record, options) {
71
142
  var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''
72
143
  // A body stored before the rich-text contract (the studio's flat list lines,
73
144
  // the admin panel's no-break spaces) reads as stored HTML from here on.
74
- var content = normalizeLegacyRichTextHtml(
145
+ var content = blogDemoteH1(normalizeLegacyRichTextHtml(
75
146
  resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
76
- )
147
+ ))
77
148
  // Counted before the heading anchors add their marks.
149
+ var bodyText = blogPlainText(content)
150
+ var wordCount = bodyText ? bodyText.split(' ').length : 0
78
151
  var bodyReadingTime = blogReadingTimeOf(content)
152
+ var faq = blogFaqOf(content)
79
153
  // The Contents list links to the headings, so they get their ids for it too
80
154
  // — the # links only with the heading anchors on. Its sections ride on a
81
155
  // post page's own fetch only (options.details): a listing draws no list, and
@@ -100,8 +174,13 @@ function buildBlogPost(record, options) {
100
174
  mainLang,
101
175
  category
102
176
  )
103
- var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null
104
- var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null
177
+ // Never empty on the page: a post with no meta title is titled by its title,
178
+ // one with no meta description described by its excerpt, else by its opening.
179
+ var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || title || null
180
+ var metaDescription =
181
+ resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) ||
182
+ blogPlainText(resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang)) ||
183
+ blogSummaryOf(bodyText)
105
184
  var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null
106
185
  var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null
107
186
 
@@ -128,6 +207,10 @@ function buildBlogPost(record, options) {
128
207
  : null
129
208
  }
130
209
 
210
+ // The picture the head shares and the structured data names: the post's own,
211
+ // never the stand-in.
212
+ var socialImageUrl = featuredImageUrl && featuredImageUrl.indexOf('data:') !== 0 ? featuredImageUrl : null
213
+
131
214
  var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)
132
215
  var authorAvatarUrl = resolveAssetUrl(rawAuthorAvatar, assetMap)
133
216
 
@@ -143,6 +226,8 @@ function buildBlogPost(record, options) {
143
226
  // Tags - i18n-resolved then parsed as JSON array
144
227
  var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)
145
228
  var tags = parseJsonArray(rawTags)
229
+ var tagsText = tags.length > 0 ? tags.join(', ') : null
230
+ var primaryCategory = categories.length > 0 ? categories[0] : null
146
231
 
147
232
  // Gallery images - parse JSON array then resolve each asset URL
148
233
  var rawGalleryImages = pickFirst(record.gallery_images, record.galleryImages)
@@ -237,6 +322,14 @@ function buildBlogPost(record, options) {
237
322
 
238
323
  // Computed: created = Unix seconds from createdAt milliseconds
239
324
  var created = Math.floor(createdAt / 1000)
325
+ var publishedAtIso = publishedAt != null && status === 'published' ? new Date(publishedAt).toISOString() : null
326
+ var updatedAtIso = updatedAt != null ? new Date(updatedAt).toISOString() : null
327
+ // An edit a day or more after publishing is worth telling the reader: the
328
+ // date of it, else null — what the page's "Updated on" line shows and gates on.
329
+ var updatedAfterPublishIso =
330
+ publishedAt != null && updatedAt != null && updatedAt - publishedAt > BLOG_UPDATED_AFTER_MS
331
+ ? updatedAtIso
332
+ : null
240
333
 
241
334
  return {
242
335
  id: id,
@@ -265,6 +358,13 @@ function buildBlogPost(record, options) {
265
358
  metaTitle: metaTitle,
266
359
  metaDescription: metaDescription,
267
360
  readingTimeMinutes: readingTimeMinutes,
361
+ wordCount: wordCount,
362
+ tagsText: tagsText,
363
+ categoryName: primaryCategory ? primaryCategory.name : category,
364
+ categorySlug: primaryCategory ? primaryCategory.slug : null,
365
+ socialImageUrl: socialImageUrl,
366
+ slugByLocale: blogSlugByLocale(record, mainLang),
367
+ faq: faq,
268
368
  // The post's sections, what its page's Contents list links to — see
269
369
  // blogContentsSectionsOf; empty off a post page or under three sections.
270
370
  sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],
@@ -282,6 +382,9 @@ function buildBlogPost(record, options) {
282
382
  publishedAt: publishedAt,
283
383
  createdAt: createdAt,
284
384
  updatedAt: updatedAt,
385
+ publishedAtIso: publishedAtIso,
386
+ updatedAtIso: updatedAtIso,
387
+ updatedAfterPublishIso: updatedAfterPublishIso,
285
388
  created: created,
286
389
  // The post's place among its primary category's posts (Manual Order), or
287
390
  // null: what the category pages order by.
@@ -1,10 +1,18 @@
1
1
  import type {
2
+ DataReadExposure,
2
3
  GeneratorOptions,
4
+ ProjectDataExposure,
5
+ ProjectUIDL,
3
6
  UIDLBlogSettings,
4
7
  UIDLAuthentication,
5
8
  UIDLEcommerceCategory,
6
9
  } from '@teleporthq/teleport-types'
7
- import { ContentTables, StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
10
+ import {
11
+ ContentTables,
12
+ DataExposure,
13
+ StorefrontTax,
14
+ TableAccess,
15
+ } from '@teleporthq/teleport-shared'
8
16
  import { generateSharedTransformationCode } from './shared-utils'
9
17
  import { generateBlogPostTransformationCode } from './blog-post'
10
18
  import { generateCustomPageTransformationCode } from './custom-page'
@@ -69,6 +77,22 @@ export interface EntityTransformOptions extends EcommerceProductTransformOptions
69
77
  * `TableAccess.resolveTrustedReaderRoles`.
70
78
  */
71
79
  trustedReaderRoles?: string[]
80
+ /**
81
+ * Who may read each table from a browser, decided from the pages that bind
82
+ * it — see `DataExposure.resolveDataExposure`. Absent (no project around the
83
+ * fetcher), every read route serves the app's own server code only.
84
+ */
85
+ dataExposure?: ProjectDataExposure
86
+ /** The exposure of the one route being generated, resolved for its data source and table. */
87
+ routeExposure?: DataReadExposure
88
+ /**
89
+ * The project has authentication: the route's session reader checks each
90
+ * session against the account store (`utils/auth/session-revocation`).
91
+ * Absent, it does; false (no authentication), the reader is HEAD's.
92
+ */
93
+ sessionRevocation?: boolean
94
+ /** The route's path back to the project root; absent, two folders (`utils/data-sources/`, `pages/api/`). */
95
+ routeProjectRoot?: string
72
96
  }
73
97
 
74
98
  /**
@@ -136,6 +160,19 @@ export const contentSettingsFor = (
136
160
  return {}
137
161
  }
138
162
 
163
+ /**
164
+ * The exposure of a project plugin's caller, which hands over the project UIDL
165
+ * itself (its pages are in `root`); a page plugin's options carry it already
166
+ * (`GeneratorOptions.dataExposure`).
167
+ */
168
+ const projectDataExposure = (options: object): ProjectDataExposure | undefined => {
169
+ const uidl = options as Partial<ProjectUIDL>
170
+ if (!uidl.root || !uidl.root.stateDefinitions || !uidl.root.stateDefinitions.route) {
171
+ return undefined
172
+ }
173
+ return DataExposure.resolveDataExposure(uidl as ProjectUIDL)
174
+ }
175
+
139
176
  export const buildProductTransformOptions = (
140
177
  options: Pick<
141
178
  GeneratorOptions,
@@ -145,6 +182,7 @@ export const buildProductTransformOptions = (
145
182
  | 'bookingSettings'
146
183
  | 'helpCenterSettings'
147
184
  | 'auth'
185
+ | 'dataExposure'
148
186
  > &
149
187
  LocalizedProjectOptions & {
150
188
  /**
@@ -173,6 +211,8 @@ export const buildProductTransformOptions = (
173
211
  productPages: options.ecommerceSettings?.productPages,
174
212
  localization: resolveContentLocalization(options),
175
213
  trustedReaderRoles: TableAccess.resolveTrustedReaderRoles(options.auth || options.authentication),
214
+ dataExposure: options.dataExposure || projectDataExposure(options),
215
+ sessionRevocation: (options.auth || options.authentication)?.enabled === true,
176
216
  })
177
217
 
178
218
  export type TransformationType =
@@ -255,6 +295,9 @@ export const getTransformationCode = (
255
295
  headingAnchors: settings.headingAnchors,
256
296
  contents: settings.contents === true,
257
297
  arranged: settings.order === 'manual',
298
+ locales: options.localization
299
+ ? [options.localization.mainLocale, ...options.localization.secondaryLocales]
300
+ : [],
258
301
  })
259
302
  )
260
303
  }