@teleporthq/teleport-plugin-next-data-source 0.43.74 → 0.43.75
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__/_helpers/pagination-widget-fixture.ts +8 -1
- package/__tests__/blog-adjacent-arranged.test.ts +176 -0
- package/__tests__/blog-post-contents.test.ts +212 -0
- package/__tests__/blog-post-page-enrichment.test.ts +28 -5
- package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
- package/__tests__/blog-post-reading-time.test.ts +55 -0
- package/__tests__/browser-row-policy.test.ts +5 -1
- package/__tests__/content-preset-wiring.test.ts +108 -0
- package/__tests__/fixed-list-sort.test.ts +183 -0
- package/__tests__/pagination-filter-groups.test.ts +186 -0
- package/__tests__/plain-list-sort.test.ts +215 -0
- package/__tests__/product-price-sort.ts +47 -19
- package/__tests__/rest-api-key-header.test.ts +18 -0
- package/__tests__/search-url-sync.test.ts +34 -15
- package/__tests__/shared-resource-prefetch.test.ts +65 -0
- 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/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts +19 -7
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +49 -13
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +137 -118
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +0 -9
- package/dist/cjs/product-price-sort.d.ts.map +1 -1
- package/dist/cjs/product-price-sort.js +1 -13
- package/dist/cjs/product-price-sort.js.map +1 -1
- package/dist/cjs/row-order.d.ts +19 -0
- package/dist/cjs/row-order.d.ts.map +1 -0
- package/dist/cjs/row-order.js +45 -0
- package/dist/cjs/row-order.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +10 -0
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +22 -1
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-comments.js +1 -1
- package/dist/cjs/transformations/blog-comments.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +14 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/cjs/transformations/heading-anchors.js +34 -10
- package/dist/cjs/transformations/heading-anchors.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +24 -12
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +59 -11
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +52 -3
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -2
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/filter-utils.d.ts +19 -7
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +46 -12
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +140 -121
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +0 -9
- package/dist/esm/product-price-sort.d.ts.map +1 -1
- package/dist/esm/product-price-sort.js +0 -11
- package/dist/esm/product-price-sort.js.map +1 -1
- package/dist/esm/row-order.d.ts +19 -0
- package/dist/esm/row-order.d.ts.map +1 -0
- package/dist/esm/row-order.js +40 -0
- package/dist/esm/row-order.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +10 -0
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +20 -0
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/esm/transformations/blog-comments.d.ts +1 -1
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
- package/dist/esm/transformations/blog-comments.js +1 -1
- package/dist/esm/transformations/blog-comments.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +14 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/heading-anchors.d.ts +32 -8
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/esm/transformations/heading-anchors.js +33 -9
- package/dist/esm/transformations/heading-anchors.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +24 -12
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +58 -11
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +52 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/fetchers/postgresql.ts +11 -15
- package/src/fetchers/rest-api.ts +3 -1
- package/src/fetchers/teleport.ts +12 -16
- package/src/fetchers/utils/browser-row-policy.ts +10 -20
- package/src/filter-utils.ts +102 -28
- package/src/pagination-plugin.ts +211 -175
- package/src/product-price-sort.ts +0 -11
- package/src/row-order.ts +54 -0
- package/src/sort-utils.ts +25 -0
- package/src/transformations/blog-adjacent-posts.ts +89 -3
- package/src/transformations/blog-comments.ts +5 -5
- package/src/transformations/blog-post.ts +64 -13
- package/src/transformations/heading-anchors.ts +79 -16
- package/src/transformations/index.ts +80 -13
- package/src/utils.ts +74 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ContentTables, TableAccess } from '@teleporthq/teleport-shared'
|
|
2
|
+
import {
|
|
3
|
+
BROWSER_READABLE_TABLES,
|
|
4
|
+
BROWSER_ROW_POLICIES as DATA_API_ROW_POLICIES,
|
|
5
|
+
} from '../../teleport-plugin-next-workflows/src/data-api-route-generator'
|
|
6
|
+
import {
|
|
7
|
+
BROWSER_ROW_POLICIES,
|
|
8
|
+
isViewDependentTable,
|
|
9
|
+
} from '../src/fetchers/utils/browser-row-policy'
|
|
10
|
+
import {
|
|
11
|
+
buildProductTransformOptions,
|
|
12
|
+
contentSettingsFor,
|
|
13
|
+
detectTransformationType,
|
|
14
|
+
getTransformWrapperCode,
|
|
15
|
+
getTransformationCode,
|
|
16
|
+
} from '../src/transformations'
|
|
17
|
+
import { generateBlogContextFileContent } from '../../teleport-project-generator-next/src/blog/blog-context-generator'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every content preset in the registry is wired through every generator that
|
|
21
|
+
* tells a content table apart from a custom one. A preset registered in
|
|
22
|
+
* teleport-shared but unknown to one of these would publish its drafts, or
|
|
23
|
+
* serve its rows raw — so each check runs per preset, and a new one fails
|
|
24
|
+
* here before it fails on a site.
|
|
25
|
+
*/
|
|
26
|
+
describe('content preset wiring in the generators', () => {
|
|
27
|
+
it.each(ContentTables.CONTENT_TABLES.map((tables) => [tables.key, tables]))(
|
|
28
|
+
'the "%s" preset is recognised everywhere',
|
|
29
|
+
(_key, tables: ContentTables.ContentTables) => {
|
|
30
|
+
// Visitors are served published rows only, through both route families.
|
|
31
|
+
expect(BROWSER_ROW_POLICIES[tables.posts]).toBe(
|
|
32
|
+
ContentTables.CONTENT_POSTS_VISITOR_ROW_POLICY
|
|
33
|
+
)
|
|
34
|
+
expect(DATA_API_ROW_POLICIES[tables.posts]).toBe(
|
|
35
|
+
ContentTables.CONTENT_POSTS_VISITOR_ROW_POLICY
|
|
36
|
+
)
|
|
37
|
+
expect(BROWSER_READABLE_TABLES).toContain(tables.posts)
|
|
38
|
+
expect(isViewDependentTable(tables.posts, TableAccess.PROTECTED_TABLES)).toBe(true)
|
|
39
|
+
|
|
40
|
+
// The rows go through the post view model, whose SQL names THIS preset's tables.
|
|
41
|
+
expect(detectTransformationType(tables.posts)).toBe('blog-post')
|
|
42
|
+
expect(detectTransformationType(`public.${tables.posts}`)).toBe('blog-post')
|
|
43
|
+
const code = getTransformationCode(tables.posts)
|
|
44
|
+
expect(code).toContain(`SELECT * FROM ${tables.posts} WHERE id = ANY($1)`)
|
|
45
|
+
expect(code).toContain(`SELECT p.* FROM ${tables.posts} p,`)
|
|
46
|
+
if (tables.comments) {
|
|
47
|
+
expect(code).toContain(`FROM ${tables.comments} c`)
|
|
48
|
+
// A browser never lists comments: the table carries the commenters' emails.
|
|
49
|
+
expect(TableAccess.CUSTOMER_RECORD_TABLES).toContain(tables.comments)
|
|
50
|
+
expect(BROWSER_READABLE_TABLES).not.toContain(tables.comments)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
it('leaves a custom table that only sounds like posts alone', () => {
|
|
56
|
+
expect(detectTransformationType('blog_posts')).toBeNull()
|
|
57
|
+
expect(BROWSER_ROW_POLICIES.blog_posts).toBeUndefined()
|
|
58
|
+
expect(BROWSER_READABLE_TABLES).not.toContain('blog_posts')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('bakes each preset\u2019s own settings into its transform, never another\u2019s', () => {
|
|
62
|
+
const blogTaxonomy = [{ id: 'b1', name: 'Blog cat', slug: 'blog-cat' }]
|
|
63
|
+
const helpTaxonomy = [{ id: 'h1', name: 'Help cat', slug: 'help-cat' }]
|
|
64
|
+
const options = buildProductTransformOptions({
|
|
65
|
+
blogSettings: { categories: blogTaxonomy as never, headingAnchors: false, comments: true },
|
|
66
|
+
helpCenterSettings: {
|
|
67
|
+
categories: helpTaxonomy as never,
|
|
68
|
+
headingAnchors: true,
|
|
69
|
+
comments: false,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
expect(contentSettingsFor(options, 'teleport_blog_posts')).toEqual({
|
|
73
|
+
categories: blogTaxonomy,
|
|
74
|
+
headingAnchors: false,
|
|
75
|
+
comments: true,
|
|
76
|
+
})
|
|
77
|
+
expect(contentSettingsFor(options, 'teleport_help_articles')).toEqual({
|
|
78
|
+
categories: helpTaxonomy,
|
|
79
|
+
headingAnchors: true,
|
|
80
|
+
comments: false,
|
|
81
|
+
})
|
|
82
|
+
expect(contentSettingsFor(options, 'teleport_products')).toEqual({})
|
|
83
|
+
// The Blog's legacy fields still answer for a caller that set only them.
|
|
84
|
+
expect(contentSettingsFor({ blogComments: true }, 'teleport_blog_posts').comments).toBe(true)
|
|
85
|
+
|
|
86
|
+
// The article transform carries the Help Center's taxonomy and anchors, not the Blog's.
|
|
87
|
+
const helpCode = getTransformationCode('teleport_help_articles', options)
|
|
88
|
+
expect(helpCode).toContain('Help cat')
|
|
89
|
+
expect(helpCode).not.toContain('Blog cat')
|
|
90
|
+
expect(helpCode).toContain('var BLOG_HEADING_ANCHORS = true')
|
|
91
|
+
// Comments are a Blog feature: the article wrapper never queries a comments table.
|
|
92
|
+
expect(getTransformWrapperCode('teleport_help_articles', options)).not.toContain(
|
|
93
|
+
'getBlogCommentsMap('
|
|
94
|
+
)
|
|
95
|
+
expect(getTransformWrapperCode('teleport_blog_posts', options)).toContain('getBlogCommentsMap(')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('exposes both taxonomies from the one generated context module', () => {
|
|
99
|
+
const code = generateBlogContextFileContent(
|
|
100
|
+
{ categories: [{ id: 'b1', name: 'Blog cat', slug: 'blog-cat' }] as never },
|
|
101
|
+
{ categories: [{ id: 'h1', name: 'Help cat', slug: 'help-cat' }] as never }
|
|
102
|
+
)
|
|
103
|
+
expect(code).toContain('const BLOG_CATEGORIES = [{"id":"b1"')
|
|
104
|
+
expect(code).toContain('const HELP_CATEGORIES = [{"id":"h1"')
|
|
105
|
+
expect(code).toContain('export const useBlogCategories = ()')
|
|
106
|
+
expect(code).toContain('export const useHelpCategories = ()')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkType,
|
|
5
|
+
FileType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
ChunkDefinition,
|
|
8
|
+
UIDLDataCacheConfig,
|
|
9
|
+
} from '@teleporthq/teleport-types'
|
|
10
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
11
|
+
import { readFixedSort } from '../src/sort-utils'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A list whose sort the editor FIXED (a blog listing's newest first: a static
|
|
15
|
+
* column, a static direction) is a static sort. Read as dynamic, it made the
|
|
16
|
+
* list skip the server-side first page — `initialData` is left out for a
|
|
17
|
+
* dynamic sort, since that prefetch ran without one — so the pre-rendered
|
|
18
|
+
* HTML of every blog and help center listing carried no posts, and the
|
|
19
|
+
* browser fetched page 1 a second time. Only a sort bound to state is dynamic.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const CACHE: UIDLDataCacheConfig = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
ttlSeconds: 60,
|
|
25
|
+
client: true,
|
|
26
|
+
server: true,
|
|
27
|
+
versionScope: 'ds1:posts',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const element = (name: string, attributes: types.JSXAttribute[] = []) =>
|
|
31
|
+
types.jsxElement(
|
|
32
|
+
types.jsxOpeningElement(types.jsxIdentifier(name), attributes, true),
|
|
33
|
+
null,
|
|
34
|
+
[],
|
|
35
|
+
true
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const makeDataProviderJSX = (): types.JSXElement =>
|
|
39
|
+
element('DataProvider', [
|
|
40
|
+
types.jsxAttribute(
|
|
41
|
+
types.jsxIdentifier('name'),
|
|
42
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
43
|
+
),
|
|
44
|
+
types.jsxAttribute(
|
|
45
|
+
types.jsxIdentifier('renderSuccess'),
|
|
46
|
+
types.jsxExpressionContainer(
|
|
47
|
+
types.arrowFunctionExpression(
|
|
48
|
+
[types.identifier('items')],
|
|
49
|
+
element('Repeater', [
|
|
50
|
+
types.jsxAttribute(
|
|
51
|
+
types.jsxIdentifier('renderItem'),
|
|
52
|
+
types.jsxExpressionContainer(
|
|
53
|
+
types.arrowFunctionExpression([types.identifier('post')], element('div'))
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
const makeComponentChunk = (): ChunkDefinition => ({
|
|
63
|
+
name: 'jsx-component',
|
|
64
|
+
type: ChunkType.AST,
|
|
65
|
+
fileType: FileType.JS,
|
|
66
|
+
linkAfter: [],
|
|
67
|
+
content: types.variableDeclaration('const', [
|
|
68
|
+
types.variableDeclarator(
|
|
69
|
+
types.identifier('PostsList'),
|
|
70
|
+
types.arrowFunctionExpression(
|
|
71
|
+
[types.identifier('props')],
|
|
72
|
+
types.blockStatement([types.returnStatement(makeDataProviderJSX())])
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
]),
|
|
76
|
+
meta: {},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
type SortKind = 'fixed' | 'state' | 'none'
|
|
80
|
+
|
|
81
|
+
const SORTS: Record<SortKind, Record<string, unknown>> = {
|
|
82
|
+
fixed: {
|
|
83
|
+
sort: { type: 'static', content: 'created_at' },
|
|
84
|
+
sortDirection: { type: 'static', content: 'desc' },
|
|
85
|
+
},
|
|
86
|
+
state: {
|
|
87
|
+
sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
|
|
88
|
+
sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
|
|
89
|
+
},
|
|
90
|
+
none: {},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const runPlugin = async (sort: SortKind, cache?: UIDLDataCacheConfig): Promise<string> => {
|
|
94
|
+
const chunk = makeComponentChunk()
|
|
95
|
+
const structure: ComponentStructure = {
|
|
96
|
+
uidl: {
|
|
97
|
+
name: 'PostsList',
|
|
98
|
+
node: {
|
|
99
|
+
type: 'data-source-list',
|
|
100
|
+
content: {
|
|
101
|
+
renderPropIdentifier: 'items',
|
|
102
|
+
resourceDefinition: {
|
|
103
|
+
dataSourceId: 'ds1',
|
|
104
|
+
tableName: 'posts',
|
|
105
|
+
dataSourceType: 'postgresql',
|
|
106
|
+
},
|
|
107
|
+
resource: { params: { queryColumns: { content: ['title'] } } },
|
|
108
|
+
nodes: {
|
|
109
|
+
success: {
|
|
110
|
+
type: 'cms-list-repeater',
|
|
111
|
+
content: {
|
|
112
|
+
renderPropIdentifier: 'post',
|
|
113
|
+
paginated: true,
|
|
114
|
+
perPage: 20,
|
|
115
|
+
searchEnabled: true,
|
|
116
|
+
searchDebounce: 300,
|
|
117
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
118
|
+
...(cache ? { cache } : {}),
|
|
119
|
+
...SORTS[sort],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
chunks: [chunk],
|
|
127
|
+
dependencies: {},
|
|
128
|
+
options: {
|
|
129
|
+
dataSources: {
|
|
130
|
+
ds1: {
|
|
131
|
+
id: 'ds1',
|
|
132
|
+
name: 'Content',
|
|
133
|
+
type: 'postgresql',
|
|
134
|
+
config: { host: 'h', database: 'd', user: 'u', password: 'p' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
extractedResources: {},
|
|
138
|
+
},
|
|
139
|
+
} as never
|
|
140
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
141
|
+
return generator(chunk.content as types.Node).code.replace(/\s+/g, ' ')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe('a list sort the editor fixed', () => {
|
|
145
|
+
it('keeps the server-side first page and asks for the sort itself', async () => {
|
|
146
|
+
const code = await runPlugin('fixed')
|
|
147
|
+
expect(code).toContain('initialData=')
|
|
148
|
+
expect(code).toContain('sorts: JSON.stringify([{ field: "created_at", order: "desc" }])')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('may peek at the browser cache on mount, as an unsorted list may', async () => {
|
|
152
|
+
expect(await runPlugin('fixed', CACHE)).toContain('ds_0_cached')
|
|
153
|
+
expect(await runPlugin('none', CACHE)).toContain('ds_0_cached')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('leaves a sort bound to state dynamic: no first page from the server, no peek', async () => {
|
|
157
|
+
const code = await runPlugin('state', CACHE)
|
|
158
|
+
expect(code).not.toContain('initialData=')
|
|
159
|
+
expect(code).not.toContain('ds_0_cached')
|
|
160
|
+
expect(code).toContain('field: sortBy.split("-")[0]')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('readFixedSort', () => {
|
|
165
|
+
it('reads a static column and direction, ascending by default', () => {
|
|
166
|
+
expect(
|
|
167
|
+
readFixedSort({ type: 'static', content: 'created_at' }, { type: 'static', content: 'desc' })
|
|
168
|
+
).toEqual({ field: 'created_at', order: 'desc' })
|
|
169
|
+
expect(readFixedSort({ type: 'static', content: 'title' }, undefined)).toEqual({
|
|
170
|
+
field: 'title',
|
|
171
|
+
order: 'asc',
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('refuses anything bound to state, and an empty column', () => {
|
|
176
|
+
expect(readFixedSort({ type: 'expr', content: 'sortBy' }, undefined)).toBeUndefined()
|
|
177
|
+
expect(
|
|
178
|
+
readFixedSort({ type: 'static', content: 'title' }, { type: 'expr', content: 'direction' })
|
|
179
|
+
).toBeUndefined()
|
|
180
|
+
expect(readFixedSort({ type: 'static', content: ' ' }, undefined)).toBeUndefined()
|
|
181
|
+
expect(readFixedSort(undefined, undefined)).toBeUndefined()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkDefinition,
|
|
5
|
+
ChunkType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
FileType,
|
|
8
|
+
} from '@teleporthq/teleport-types'
|
|
9
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
10
|
+
import {
|
|
11
|
+
makeUidlNode,
|
|
12
|
+
makeWidgetComponentChunk,
|
|
13
|
+
type RepeaterOptions,
|
|
14
|
+
} from './_helpers/pagination-widget-fixture'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A list filter can say "Category is Commercial OR Narrative" — an "any of"
|
|
18
|
+
* group. The paginated and searchable lists used to flatten that group into a
|
|
19
|
+
* flat list the fetcher joined with AND, so the published site showed the rows
|
|
20
|
+
* that were BOTH Commercial and Narrative: none. The group now reaches the
|
|
21
|
+
* fetcher as a group, in the browser fetch and in the build-time prefetch
|
|
22
|
+
* alike, and the fetcher's own `normalizeFilterTree` honours its operator
|
|
23
|
+
* (`filter-groups.test.ts`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const condition = (source: string, destination: unknown, operand = '=') => ({
|
|
27
|
+
type: 'condition',
|
|
28
|
+
source,
|
|
29
|
+
destination,
|
|
30
|
+
operand,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const group = (operator: 'and' | 'or', children: unknown[]) => ({
|
|
34
|
+
type: 'group',
|
|
35
|
+
operator,
|
|
36
|
+
children,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const stateRef = (id: string) => ({ type: 'dynamic', content: { referenceType: 'state', id } })
|
|
40
|
+
|
|
41
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
42
|
+
|
|
43
|
+
// The page's `getStaticProps` as the main data-source plugin leaves it: a
|
|
44
|
+
// `Promise.all` of prefetches and a `props` object, which the pagination plugin
|
|
45
|
+
// extends with the paginated list's first page and its count.
|
|
46
|
+
const makeGetStaticPropsChunk = (): ChunkDefinition => ({
|
|
47
|
+
name: 'getStaticProps',
|
|
48
|
+
type: ChunkType.AST,
|
|
49
|
+
fileType: FileType.JS,
|
|
50
|
+
linkAfter: ['jsx-component'],
|
|
51
|
+
meta: {},
|
|
52
|
+
content: types.exportNamedDeclaration(
|
|
53
|
+
types.functionDeclaration(
|
|
54
|
+
types.identifier('getStaticProps'),
|
|
55
|
+
[types.identifier('context')],
|
|
56
|
+
types.blockStatement([
|
|
57
|
+
types.tryStatement(
|
|
58
|
+
types.blockStatement([
|
|
59
|
+
types.variableDeclaration('const', [
|
|
60
|
+
types.variableDeclarator(
|
|
61
|
+
types.arrayPattern([]),
|
|
62
|
+
types.awaitExpression(
|
|
63
|
+
types.callExpression(
|
|
64
|
+
types.memberExpression(types.identifier('Promise'), types.identifier('all')),
|
|
65
|
+
[types.arrayExpression([])]
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
),
|
|
69
|
+
]),
|
|
70
|
+
types.returnStatement(
|
|
71
|
+
types.objectExpression([
|
|
72
|
+
types.objectProperty(types.identifier('props'), types.objectExpression([])),
|
|
73
|
+
])
|
|
74
|
+
),
|
|
75
|
+
]),
|
|
76
|
+
types.catchClause(
|
|
77
|
+
types.identifier('error'),
|
|
78
|
+
types.blockStatement([
|
|
79
|
+
types.returnStatement(
|
|
80
|
+
types.objectExpression([
|
|
81
|
+
types.objectProperty(types.identifier('props'), types.objectExpression([])),
|
|
82
|
+
])
|
|
83
|
+
),
|
|
84
|
+
])
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
]),
|
|
88
|
+
false,
|
|
89
|
+
true
|
|
90
|
+
)
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const runPlugin = async (repeater: RepeaterOptions) => {
|
|
95
|
+
const chunk = makeWidgetComponentChunk({ controls: ['previous', 'next'] })
|
|
96
|
+
const staticProps = makeGetStaticPropsChunk()
|
|
97
|
+
const structure: ComponentStructure = {
|
|
98
|
+
uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
|
|
99
|
+
chunks: [chunk, staticProps],
|
|
100
|
+
dependencies: {},
|
|
101
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
102
|
+
} as never
|
|
103
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
104
|
+
return {
|
|
105
|
+
component: collapse(generator(chunk.content as types.Node).code),
|
|
106
|
+
staticProps: collapse(generator(staticProps.content as types.Node).code),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const OR_GROUP = [
|
|
111
|
+
group('or', [condition('Category', 'Commercial'), condition('Category', 'Narrative')]),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const expectOrGroup = (code: string) => {
|
|
115
|
+
expect(code).toContain('type: "group"')
|
|
116
|
+
expect(code).toContain('operator: "or"')
|
|
117
|
+
expect(code).toContain('source: "Category", destination: "Commercial", operand: "="')
|
|
118
|
+
expect(code).toContain('source: "Category", destination: "Narrative", operand: "="')
|
|
119
|
+
// The two conditions sit INSIDE the group's children, never beside it.
|
|
120
|
+
expect(code).toMatch(/children: \[[^\]]*Commercial[^\]]*Narrative[^\]]*\]/)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('pagination plugin — filter groups', () => {
|
|
124
|
+
it('a paginated list keeps an or-group as an or-group in its browser fetch', async () => {
|
|
125
|
+
const { component } = await runPlugin({ paginated: true, filters: OR_GROUP })
|
|
126
|
+
expectOrGroup(component)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('a searchable list keeps it too', async () => {
|
|
130
|
+
const { component } = await runPlugin({
|
|
131
|
+
paginated: true,
|
|
132
|
+
searchEnabled: true,
|
|
133
|
+
filters: OR_GROUP,
|
|
134
|
+
})
|
|
135
|
+
expectOrGroup(component)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('the build-time prefetch and its count carry the same group', async () => {
|
|
139
|
+
const { staticProps } = await runPlugin({ paginated: true, filters: OR_GROUP })
|
|
140
|
+
expect(staticProps).toContain('page: 1')
|
|
141
|
+
expectOrGroup(staticProps)
|
|
142
|
+
// Once for the first page, once for the count.
|
|
143
|
+
expect(staticProps.match(/operator: "or"/g)).toHaveLength(2)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('the runtime drop of empty destinations lets a group through and prunes inside it', async () => {
|
|
147
|
+
const { component } = await runPlugin({
|
|
148
|
+
paginated: true,
|
|
149
|
+
filters: [
|
|
150
|
+
group('or', [
|
|
151
|
+
condition('Category', 'Commercial'),
|
|
152
|
+
condition('Category', stateRef('picked')),
|
|
153
|
+
]),
|
|
154
|
+
],
|
|
155
|
+
})
|
|
156
|
+
// Every level is filtered; the predicate keeps groups themselves.
|
|
157
|
+
expect(component).toContain('__f.type === "group" ||')
|
|
158
|
+
expect(component).toMatch(/children: \[.*\]\.filter\(__f =>/)
|
|
159
|
+
// The state-bound leaf is still a dependency of the fetch params.
|
|
160
|
+
expect(component).toContain('picked')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('the prefetch prunes a dynamic leaf but keeps the group around its static sibling', async () => {
|
|
164
|
+
const { staticProps } = await runPlugin({
|
|
165
|
+
paginated: true,
|
|
166
|
+
filters: [
|
|
167
|
+
group('or', [
|
|
168
|
+
condition('Category', 'Commercial'),
|
|
169
|
+
condition('Category', stateRef('picked')),
|
|
170
|
+
]),
|
|
171
|
+
],
|
|
172
|
+
})
|
|
173
|
+
expect(staticProps).toContain('operator: "or"')
|
|
174
|
+
expect(staticProps).toContain('destination: "Commercial"')
|
|
175
|
+
expect(staticProps).not.toContain('picked')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('a legacy flat condition list still works', async () => {
|
|
179
|
+
const { component } = await runPlugin({
|
|
180
|
+
paginated: true,
|
|
181
|
+
filters: [{ source: 'Category', destination: 'Commercial', operand: '=' }],
|
|
182
|
+
})
|
|
183
|
+
expect(component).toContain('source: "Category", destination: "Commercial", operand: "="')
|
|
184
|
+
expect(component).not.toContain('type: "group"')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A PLAIN list — no pagination, no search — fetches with the sort its
|
|
13
|
+
* repeater carries, like every other list: the three paginated / searchable
|
|
14
|
+
* kinds rebuild their params with it, while a plain list's params were kept
|
|
15
|
+
* as they came and the sort never reached the fetch, so the rows arrived in
|
|
16
|
+
* the table's own order whatever the editor said.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const element = (name: string, attributes: types.JSXAttribute[] = []) =>
|
|
20
|
+
types.jsxElement(
|
|
21
|
+
types.jsxOpeningElement(types.jsxIdentifier(name), attributes, true),
|
|
22
|
+
null,
|
|
23
|
+
[],
|
|
24
|
+
true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const makeDataProviderJSX = (): types.JSXElement =>
|
|
28
|
+
element('DataProvider', [
|
|
29
|
+
types.jsxAttribute(
|
|
30
|
+
types.jsxIdentifier('name'),
|
|
31
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
32
|
+
),
|
|
33
|
+
types.jsxAttribute(
|
|
34
|
+
types.jsxIdentifier('renderSuccess'),
|
|
35
|
+
types.jsxExpressionContainer(
|
|
36
|
+
types.arrowFunctionExpression(
|
|
37
|
+
[types.identifier('items')],
|
|
38
|
+
element('Repeater', [
|
|
39
|
+
types.jsxAttribute(
|
|
40
|
+
types.jsxIdentifier('renderItem'),
|
|
41
|
+
types.jsxExpressionContainer(
|
|
42
|
+
types.arrowFunctionExpression([types.identifier('post')], element('div'))
|
|
43
|
+
)
|
|
44
|
+
),
|
|
45
|
+
])
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const makeComponentChunk = (): ChunkDefinition => ({
|
|
52
|
+
name: 'jsx-component',
|
|
53
|
+
type: ChunkType.AST,
|
|
54
|
+
fileType: FileType.JS,
|
|
55
|
+
linkAfter: [],
|
|
56
|
+
content: types.variableDeclaration('const', [
|
|
57
|
+
types.variableDeclarator(
|
|
58
|
+
types.identifier('PostsList'),
|
|
59
|
+
types.arrowFunctionExpression(
|
|
60
|
+
[types.identifier('props')],
|
|
61
|
+
types.blockStatement([types.returnStatement(makeDataProviderJSX())])
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
]),
|
|
65
|
+
meta: {},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
type SortKind = 'fixed' | 'state' | 'none'
|
|
69
|
+
|
|
70
|
+
const SORTS: Record<SortKind, Record<string, unknown>> = {
|
|
71
|
+
fixed: {
|
|
72
|
+
sort: { type: 'static', content: 'title' },
|
|
73
|
+
sortDirection: { type: 'static', content: 'asc' },
|
|
74
|
+
},
|
|
75
|
+
state: {
|
|
76
|
+
sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
|
|
77
|
+
sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
|
|
78
|
+
},
|
|
79
|
+
none: {},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const runPlugin = async (
|
|
83
|
+
sort: SortKind,
|
|
84
|
+
options: { legacySorts?: boolean; params?: boolean; prefetched?: boolean } = {}
|
|
85
|
+
): Promise<string> => {
|
|
86
|
+
const chunk = makeComponentChunk()
|
|
87
|
+
if (options.prefetched) {
|
|
88
|
+
// A page's list, seeded with its server prefetch.
|
|
89
|
+
const provider = (
|
|
90
|
+
(chunk.content as types.VariableDeclaration).declarations[0]
|
|
91
|
+
.init as types.ArrowFunctionExpression
|
|
92
|
+
).body as types.BlockStatement
|
|
93
|
+
const jsx = (provider.body[0] as types.ReturnStatement).argument as types.JSXElement
|
|
94
|
+
jsx.openingElement.attributes.push(
|
|
95
|
+
types.jsxAttribute(
|
|
96
|
+
types.jsxIdentifier('initialData'),
|
|
97
|
+
types.jsxExpressionContainer(
|
|
98
|
+
types.memberExpression(types.identifier('props'), types.identifier('items'))
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if (options.params) {
|
|
104
|
+
// The params the data-source plugin writes for a plain list: its filters.
|
|
105
|
+
const provider = (
|
|
106
|
+
(chunk.content as types.VariableDeclaration).declarations[0]
|
|
107
|
+
.init as types.ArrowFunctionExpression
|
|
108
|
+
).body as types.BlockStatement
|
|
109
|
+
const jsx = (provider.body[0] as types.ReturnStatement).argument as types.JSXElement
|
|
110
|
+
jsx.openingElement.attributes.push(
|
|
111
|
+
types.jsxAttribute(
|
|
112
|
+
types.jsxIdentifier('params'),
|
|
113
|
+
types.jsxExpressionContainer(
|
|
114
|
+
types.objectExpression([
|
|
115
|
+
types.objectProperty(types.identifier('filters'), types.stringLiteral('[]')),
|
|
116
|
+
])
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
const structure: ComponentStructure = {
|
|
122
|
+
uidl: {
|
|
123
|
+
name: 'PostsList',
|
|
124
|
+
node: {
|
|
125
|
+
type: 'data-source-list',
|
|
126
|
+
content: {
|
|
127
|
+
renderPropIdentifier: 'items',
|
|
128
|
+
resourceDefinition: {
|
|
129
|
+
dataSourceId: 'ds1',
|
|
130
|
+
tableName: 'posts',
|
|
131
|
+
dataSourceType: 'postgresql',
|
|
132
|
+
},
|
|
133
|
+
resource: {
|
|
134
|
+
params: {
|
|
135
|
+
queryColumns: { content: ['title'] },
|
|
136
|
+
...(options.legacySorts
|
|
137
|
+
? { sorts: { content: [{ field: 'created_at', order: 'desc' }] } }
|
|
138
|
+
: {}),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
nodes: {
|
|
142
|
+
success: {
|
|
143
|
+
type: 'cms-list-repeater',
|
|
144
|
+
content: {
|
|
145
|
+
renderPropIdentifier: 'post',
|
|
146
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
147
|
+
...SORTS[sort],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
chunks: [chunk],
|
|
155
|
+
dependencies: {},
|
|
156
|
+
options: {
|
|
157
|
+
dataSources: {
|
|
158
|
+
ds1: {
|
|
159
|
+
id: 'ds1',
|
|
160
|
+
name: 'Content',
|
|
161
|
+
type: 'postgresql',
|
|
162
|
+
config: { host: 'h', database: 'd', user: 'u', password: 'p' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
extractedResources: {},
|
|
166
|
+
},
|
|
167
|
+
} as never
|
|
168
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
169
|
+
return generator(chunk.content as types.Node).code
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const squash = (code: string) => code.replace(/\s+/g, ' ')
|
|
173
|
+
|
|
174
|
+
describe('a plain list fetches with its repeater’s sort', () => {
|
|
175
|
+
it('sends a sort the editor fixed', async () => {
|
|
176
|
+
const code = squash(await runPlugin('fixed'))
|
|
177
|
+
expect(code).toContain(
|
|
178
|
+
'params={useMemo(() => ({ sorts: JSON.stringify([{ field: "title", order: "asc" }]) }), [])}'
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('sends a sort bound to state, and refetches when the state changes', async () => {
|
|
183
|
+
const code = squash(await runPlugin('state'))
|
|
184
|
+
expect(code).toContain(
|
|
185
|
+
'sorts: JSON.stringify([{ field: sortBy.split("-")[0], order: sortBy.split("-")[1] }])'
|
|
186
|
+
)
|
|
187
|
+
expect(code).toMatch(/\}\), \[sortBy\]\)\}/)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('adds the sort beside the params the list already had', async () => {
|
|
191
|
+
const code = squash(await runPlugin('fixed', { params: true }))
|
|
192
|
+
expect(code).toContain(
|
|
193
|
+
'params={useMemo(() => ({ filters: "[]", sorts: JSON.stringify([{ field: "title", order: "asc" }]) }), [])}'
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('keeps the resource’s own sorts first, as every other list does', async () => {
|
|
198
|
+
const code = squash(await runPlugin('fixed', { legacySorts: true }))
|
|
199
|
+
expect(code).toContain('sorts: JSON.stringify([{ field: "created_at", order: "desc" }])')
|
|
200
|
+
expect(code).not.toContain('field: "title"')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('drops the prefetched rows for a sort bound to state, which the server cannot know', async () => {
|
|
204
|
+
expect(squash(await runPlugin('state', { prefetched: true }))).not.toContain('initialData')
|
|
205
|
+
expect(squash(await runPlugin('fixed', { prefetched: true }))).toContain(
|
|
206
|
+
'initialData={props.items}'
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('leaves a list without a sort as it was', async () => {
|
|
211
|
+
const code = squash(await runPlugin('none'))
|
|
212
|
+
expect(code).not.toContain('sorts')
|
|
213
|
+
expect(code).not.toContain('params=')
|
|
214
|
+
})
|
|
215
|
+
})
|