@financial-times/cp-content-pipeline-schema 3.15.2 → 3.17.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.
- package/CHANGELOG.md +17 -0
- package/lib/generated/index.d.ts +19 -2
- package/lib/resolvers/content-tree/Workarounds.d.ts +2 -2
- package/lib/resolvers/content-tree/nodePredicates.d.ts +1 -1
- package/lib/resolvers/content-tree/nodePredicates.js +1 -0
- package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
- package/lib/resolvers/content-tree/references/Flourish.test.js +3 -3
- package/lib/resolvers/content-tree/references/Flourish.test.js.map +1 -1
- package/lib/resolvers/content-tree/references/RecommendedList.d.ts +5 -0
- package/lib/resolvers/content-tree/references/RecommendedList.js +41 -0
- package/lib/resolvers/content-tree/references/RecommendedList.js.map +1 -0
- package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
- package/lib/resolvers/content-tree/references/index.d.ts +3 -1
- package/lib/resolvers/content-tree/references/index.js +3 -0
- package/lib/resolvers/content-tree/references/index.js.map +1 -1
- package/lib/resolvers/content-tree/tagMappings.js +33 -6
- package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
- package/lib/resolvers/index.d.ts +2 -0
- package/lib/resolvers/teaser.d.ts +1 -0
- package/lib/resolvers/teaser.js +1 -0
- package/lib/resolvers/teaser.js.map +1 -1
- package/package.json +2 -2
- package/queries/article.graphql +9 -0
- package/src/generated/index.ts +21 -2
- package/src/resolvers/content-tree/Workarounds.ts +2 -0
- package/src/resolvers/content-tree/nodePredicates.ts +1 -0
- package/src/resolvers/content-tree/references/Flourish.test.ts +3 -3
- package/src/resolvers/content-tree/references/RecommendedList.ts +52 -0
- package/src/resolvers/content-tree/references/index.ts +5 -0
- package/src/resolvers/content-tree/tagMappings.ts +40 -6
- package/src/resolvers/teaser.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/references/recommendedList.graphql +7 -0
- package/typedefs/teaser.graphql +3 -0
package/src/generated/index.ts
CHANGED
|
@@ -1620,6 +1620,13 @@ export type Recommended = Reference & {
|
|
|
1620
1620
|
readonly type: Scalars['String']['output'];
|
|
1621
1621
|
};
|
|
1622
1622
|
|
|
1623
|
+
export type RecommendedList = Reference & {
|
|
1624
|
+
/** Recommended references contain teaser thumbnails of related articles. */
|
|
1625
|
+
readonly teasers: ReadonlyArray<Teaser>;
|
|
1626
|
+
/** The type of the reference, eg. 'recommended'. */
|
|
1627
|
+
readonly type: Scalars['String']['output'];
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1623
1630
|
export type Reference = {
|
|
1624
1631
|
/** The type of the reference, eg. 'image-set'. References are additional data that cannot be loaded by content-tree. They will be appended to content-tree nodes during resolution. */
|
|
1625
1632
|
readonly type: Scalars['String']['output'];
|
|
@@ -1690,6 +1697,8 @@ export type TableOfContents = {
|
|
|
1690
1697
|
};
|
|
1691
1698
|
|
|
1692
1699
|
export type Teaser = {
|
|
1700
|
+
/** The label to show if sponsored content. */
|
|
1701
|
+
readonly clientName?: Maybe<Scalars['String']['output']>;
|
|
1693
1702
|
/** The date the content of the teaser was first published. */
|
|
1694
1703
|
readonly firstPublishedDate: Scalars['String']['output'];
|
|
1695
1704
|
/** The unique identifier of the teaser, a uuid or a url. */
|
|
@@ -1997,7 +2006,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = R
|
|
|
1997
2006
|
Content: ( ContentModel ) | ( ContentModel ) | ( ContentModel ) | ( ContentModel ) | ( ContentModel ) | ( ContentModel ) | ( ContentModel );
|
|
1998
2007
|
Image: ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel ) | ( ImageModel );
|
|
1999
2008
|
Picture: ( PictureModel ) | ( PictureModel ) | ( PictureModel );
|
|
2000
|
-
Reference: ( ReferenceWithCAPIData<AuthorNode> ) | ( ReferenceWithCAPIData<ClipSetNode|OldClipNode> ) | ( ReferenceWithCAPIData<ContentTree.CustomCodeComponent> ) | ( ReferenceWithCAPIData<ContentTree.Flourish> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<ContentTree.LayoutImage> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<RawImageNode> ) | ( ReferenceWithCAPIData<ContentTree.Recommended> ) | ( ReferenceWithCAPIData<ContentTree.ScrollyImage> ) | ( ReferenceWithCAPIData<ContentTree.Tweet> ) | ( ReferenceWithCAPIData<ContentTree.Video> );
|
|
2009
|
+
Reference: ( ReferenceWithCAPIData<AuthorNode> ) | ( ReferenceWithCAPIData<ClipSetNode|OldClipNode> ) | ( ReferenceWithCAPIData<ContentTree.CustomCodeComponent> ) | ( ReferenceWithCAPIData<ContentTree.Flourish> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<ContentTree.LayoutImage> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<RawImageNode> ) | ( ReferenceWithCAPIData<ContentTree.Recommended> ) | ( ReferenceWithCAPIData<ContentTree.RecommendedList> ) | ( ReferenceWithCAPIData<ContentTree.ScrollyImage> ) | ( ReferenceWithCAPIData<ContentTree.Tweet> ) | ( ReferenceWithCAPIData<ContentTree.Video> );
|
|
2001
2010
|
Topper: ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel );
|
|
2002
2011
|
TopperWithBrand: ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel );
|
|
2003
2012
|
TopperWithHeadshot: ( TopperModel ) | ( TopperModel );
|
|
@@ -2085,6 +2094,7 @@ export type ResolversTypes = ResolversObject<{
|
|
|
2085
2094
|
Query: ResolverTypeWrapper<{}>;
|
|
2086
2095
|
RawImage: ResolverTypeWrapper<ReferenceWithCAPIData<RawImageNode>>;
|
|
2087
2096
|
Recommended: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.Recommended>>;
|
|
2097
|
+
RecommendedList: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.RecommendedList>>;
|
|
2088
2098
|
Reference: ResolverTypeWrapper<ReferenceWithCAPIData>;
|
|
2089
2099
|
RichText: ResolverTypeWrapper<RichTextModel>;
|
|
2090
2100
|
RichTextSource: ResolverTypeWrapper<Scalars['RichTextSource']['output']>;
|
|
@@ -2187,6 +2197,7 @@ export type ResolversParentTypes = ResolversObject<{
|
|
|
2187
2197
|
Query: {};
|
|
2188
2198
|
RawImage: ReferenceWithCAPIData<RawImageNode>;
|
|
2189
2199
|
Recommended: ReferenceWithCAPIData<ContentTree.Recommended>;
|
|
2200
|
+
RecommendedList: ReferenceWithCAPIData<ContentTree.RecommendedList>;
|
|
2190
2201
|
Reference: ReferenceWithCAPIData;
|
|
2191
2202
|
RichText: RichTextModel;
|
|
2192
2203
|
RichTextSource: Scalars['RichTextSource']['output'];
|
|
@@ -3062,8 +3073,14 @@ export type RecommendedResolvers<ContextType = QueryContext, ParentType extends
|
|
|
3062
3073
|
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
|
3063
3074
|
}>;
|
|
3064
3075
|
|
|
3076
|
+
export type RecommendedListResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['RecommendedList'] = ResolversParentTypes['RecommendedList']> = ResolversObject<{
|
|
3077
|
+
teasers: Resolver<ReadonlyArray<ResolversTypes['Teaser']>, ParentType, ContextType>;
|
|
3078
|
+
type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
3079
|
+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
|
3080
|
+
}>;
|
|
3081
|
+
|
|
3065
3082
|
export type ReferenceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Reference'] = ResolversParentTypes['Reference']> = ResolversObject<{
|
|
3066
|
-
__resolveType?: TypeResolveFn<'AuthorReference' | 'ClipSet' | 'CustomCodeComponent' | 'Flourish' | 'ImageSet' | 'LayoutImage' | 'MainImage' | 'RawImage' | 'Recommended' | 'ScrollyImage' | 'Tweet' | 'VideoReference', ParentType, ContextType>;
|
|
3083
|
+
__resolveType?: TypeResolveFn<'AuthorReference' | 'ClipSet' | 'CustomCodeComponent' | 'Flourish' | 'ImageSet' | 'LayoutImage' | 'MainImage' | 'RawImage' | 'Recommended' | 'RecommendedList' | 'ScrollyImage' | 'Tweet' | 'VideoReference', ParentType, ContextType>;
|
|
3067
3084
|
type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
3068
3085
|
}>;
|
|
3069
3086
|
|
|
@@ -3116,6 +3133,7 @@ export type TableOfContentsResolvers<ContextType = QueryContext, ParentType exte
|
|
|
3116
3133
|
}>;
|
|
3117
3134
|
|
|
3118
3135
|
export type TeaserResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Teaser'] = ResolversParentTypes['Teaser']> = ResolversObject<{
|
|
3136
|
+
clientName: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
3119
3137
|
firstPublishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
3120
3138
|
id: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
3121
3139
|
image: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
|
|
@@ -3323,6 +3341,7 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
|
|
|
3323
3341
|
Query: QueryResolvers<ContextType>;
|
|
3324
3342
|
RawImage: RawImageResolvers<ContextType>;
|
|
3325
3343
|
Recommended: RecommendedResolvers<ContextType>;
|
|
3344
|
+
RecommendedList: RecommendedListResolvers<ContextType>;
|
|
3326
3345
|
Reference: ReferenceResolvers<ContextType>;
|
|
3327
3346
|
RichText: RichTextResolvers<ContextType>;
|
|
3328
3347
|
RichTextSource: GraphQLScalarType;
|
|
@@ -200,6 +200,7 @@ export type BodyBlock =
|
|
|
200
200
|
| ContentTree.Tweet
|
|
201
201
|
| ContentTree.Video
|
|
202
202
|
| ContentTree.YoutubeVideo
|
|
203
|
+
| ContentTree.RecommendedList
|
|
203
204
|
|
|
204
205
|
export type AnyNode =
|
|
205
206
|
| ContentTree.Text
|
|
@@ -248,3 +249,4 @@ export type AnyNode =
|
|
|
248
249
|
| Byline
|
|
249
250
|
| Author
|
|
250
251
|
| Cite
|
|
252
|
+
| ContentTree.RecommendedList
|
|
@@ -26,7 +26,7 @@ describe('Flourish Model', () => {
|
|
|
26
26
|
type: 'flourish',
|
|
27
27
|
id: 'example-id',
|
|
28
28
|
flourishType: 'example-type',
|
|
29
|
-
layoutWidth: 'grid',
|
|
29
|
+
layoutWidth: 'full-grid',
|
|
30
30
|
timestamp: '2022-01-06T14:41:01.102Z',
|
|
31
31
|
description: 'Example description',
|
|
32
32
|
}
|
|
@@ -53,7 +53,7 @@ describe('Flourish Model', () => {
|
|
|
53
53
|
type: 'flourish',
|
|
54
54
|
id: 'example-id',
|
|
55
55
|
flourishType: 'example-type',
|
|
56
|
-
layoutWidth: 'grid',
|
|
56
|
+
layoutWidth: 'full-grid',
|
|
57
57
|
timestamp: '2022-01-06T14:41:01.102Z',
|
|
58
58
|
description: 'Example description',
|
|
59
59
|
}
|
|
@@ -77,7 +77,7 @@ describe('Flourish Model', () => {
|
|
|
77
77
|
type: 'flourish',
|
|
78
78
|
id: 'example-id',
|
|
79
79
|
flourishType: 'example-type',
|
|
80
|
-
layoutWidth: 'grid',
|
|
80
|
+
layoutWidth: 'full-grid',
|
|
81
81
|
timestamp: '2022-01-06T14:41:01.102Z',
|
|
82
82
|
description: 'Example description',
|
|
83
83
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { uuidFromUrl } from '../../../helpers/metadata'
|
|
2
|
+
import { RecommendedListResolvers } from '../../../generated'
|
|
3
|
+
import { OperationalError } from '@dotcom-reliability-kit/errors'
|
|
4
|
+
import { ContentTree } from '@financial-times/content-tree'
|
|
5
|
+
import { Logger } from '@dotcom-reliability-kit/logger'
|
|
6
|
+
import { CapiDataSource } from '../../../datasources/capi'
|
|
7
|
+
|
|
8
|
+
function getTeaser(context: {
|
|
9
|
+
logger: Logger
|
|
10
|
+
dataSources: { capi: CapiDataSource }
|
|
11
|
+
}) {
|
|
12
|
+
return async function (child: ContentTree.Recommended) {
|
|
13
|
+
try {
|
|
14
|
+
let content = await context.dataSources.capi.getContent(
|
|
15
|
+
uuidFromUrl(child.id)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if (!content) {
|
|
19
|
+
throw new Error('Invalid content from recommended article teaser')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (child.teaserTitleOverride) {
|
|
23
|
+
content = content.overrideTitle(child.teaserTitleOverride)
|
|
24
|
+
}
|
|
25
|
+
return content
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
context.logger.warn({
|
|
29
|
+
event: 'RECOVERABLE_ERROR',
|
|
30
|
+
error: new OperationalError({
|
|
31
|
+
code: 'RECOMMENDED_TEASER_ERROR',
|
|
32
|
+
message: `Couldn't load teaser for recommended article ${child.id}`,
|
|
33
|
+
cause: error,
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const RecommendedList = {
|
|
42
|
+
async teasers(parent, _args, context) {
|
|
43
|
+
const responses = await Promise.all(
|
|
44
|
+
parent.reference.children.map(getTeaser(context))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return responses.filter((response) => response !== undefined)
|
|
48
|
+
},
|
|
49
|
+
type(parent) {
|
|
50
|
+
return parent.reference.type
|
|
51
|
+
},
|
|
52
|
+
} satisfies RecommendedListResolvers
|
|
@@ -10,6 +10,7 @@ import { CustomCodeComponent } from './CustomCodeComponent'
|
|
|
10
10
|
import { Video } from './Video'
|
|
11
11
|
import { Flourish, FlourishSource } from './Flourish'
|
|
12
12
|
import { Recommended } from './Recommended'
|
|
13
|
+
import { RecommendedList } from './RecommendedList'
|
|
13
14
|
import { LayoutImage } from './LayoutImage'
|
|
14
15
|
import { RawImage } from './RawImage'
|
|
15
16
|
import { ScrollyImage } from './ScrollyImage'
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
LayoutImageResolvers,
|
|
22
23
|
RawImageResolvers,
|
|
23
24
|
RecommendedResolvers,
|
|
25
|
+
RecommendedListResolvers,
|
|
24
26
|
ReferenceResolvers,
|
|
25
27
|
ScrollyImageResolvers,
|
|
26
28
|
TweetResolvers,
|
|
@@ -48,6 +50,7 @@ export const resolvers: {
|
|
|
48
50
|
Flourish: FlourishResolvers
|
|
49
51
|
FlourishSource: FlourishSourceResolvers
|
|
50
52
|
Recommended: RecommendedResolvers
|
|
53
|
+
RecommendedList: RecommendedListResolvers
|
|
51
54
|
LayoutImage: LayoutImageResolvers
|
|
52
55
|
RawImage: RawImageResolvers
|
|
53
56
|
ScrollyImage: ScrollyImageResolvers
|
|
@@ -65,6 +68,7 @@ export const resolvers: {
|
|
|
65
68
|
Flourish,
|
|
66
69
|
FlourishSource,
|
|
67
70
|
Recommended,
|
|
71
|
+
RecommendedList,
|
|
68
72
|
LayoutImage,
|
|
69
73
|
RawImage,
|
|
70
74
|
ScrollyImage,
|
|
@@ -83,6 +87,7 @@ export const mapNodeToReference = {
|
|
|
83
87
|
'custom-code-component': 'CustomCodeComponent',
|
|
84
88
|
video: 'VideoReference',
|
|
85
89
|
recommended: 'Recommended',
|
|
90
|
+
'recommended-list': 'RecommendedList',
|
|
86
91
|
'layout-image': 'LayoutImage',
|
|
87
92
|
'scrolly-image': 'ScrollyImage',
|
|
88
93
|
'raw-image': 'RawImage',
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TableChildren,
|
|
10
10
|
TableColumnSettings,
|
|
11
11
|
TableFooter,
|
|
12
|
+
Recommended,
|
|
12
13
|
} from './Workarounds'
|
|
13
14
|
|
|
14
15
|
import {
|
|
@@ -119,12 +120,24 @@ const commonTagMappings: TagMappings = {
|
|
|
119
120
|
version: 1,
|
|
120
121
|
children: childrenOfTypes(bodyBlockTypes, traverse(), 'body'),
|
|
121
122
|
}),
|
|
122
|
-
'a:not([data-asset-type])': ($el, traverse, context) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
'a:not([data-asset-type])': ($el, traverse, context) => {
|
|
124
|
+
const isValidStyleType = (
|
|
125
|
+
value?: string
|
|
126
|
+
): value is ContentTree.Link['styleType'] =>
|
|
127
|
+
// The data-anchor-style attribute could be used to serve different variations of styles
|
|
128
|
+
// from the standard link. Currently, only 'onward-journey' is supported.
|
|
129
|
+
!!value && ['onward-journey'].includes(value)
|
|
130
|
+
|
|
131
|
+
const styleType = $el.attr('data-anchor-style') || ''
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
type: 'link',
|
|
135
|
+
url: $el.attr('href') || '',
|
|
136
|
+
title: '',
|
|
137
|
+
children: childrenOfTypes(phrasingTypes, traverse(), 'link', context),
|
|
138
|
+
...(isValidStyleType(styleType) && { styleType }),
|
|
139
|
+
}
|
|
140
|
+
},
|
|
128
141
|
strong: ($el, traverse, context) => ({
|
|
129
142
|
type: 'strong',
|
|
130
143
|
children: childrenOfTypes(phrasingTypes, traverse(), 'strong', context),
|
|
@@ -291,6 +304,27 @@ const commonTagMappings: TagMappings = {
|
|
|
291
304
|
teaserTitleOverride: $firstLink.text(),
|
|
292
305
|
}
|
|
293
306
|
},
|
|
307
|
+
'recommended-list': ($el) => {
|
|
308
|
+
const title = $el.find('recommended-title').first().text()
|
|
309
|
+
const items = $el
|
|
310
|
+
.find('ft-content')
|
|
311
|
+
.toArray()
|
|
312
|
+
.map((item) => {
|
|
313
|
+
const $item = $el.find(item)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: $item.attr('url') || '',
|
|
317
|
+
type: 'recommended',
|
|
318
|
+
teaserTitleOverride: $item.text(),
|
|
319
|
+
}
|
|
320
|
+
}) as Recommended[]
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
type: 'recommended-list',
|
|
324
|
+
heading: title || 'Related content',
|
|
325
|
+
children: items,
|
|
326
|
+
}
|
|
327
|
+
},
|
|
294
328
|
'big-number': ($el) => {
|
|
295
329
|
const $number = $el.find('big-number-headline').first()
|
|
296
330
|
const $description = $el.find('big-number-intro').first()
|
package/src/resolvers/teaser.ts
CHANGED
|
@@ -33,6 +33,7 @@ const resolvers = {
|
|
|
33
33
|
publishedDate: (parent) => parent.publishedDate(),
|
|
34
34
|
theme: (parent) => parent.design().theme,
|
|
35
35
|
title: (parent) => parent.title(),
|
|
36
|
+
clientName: (parent) => parent.clientName?.(),
|
|
36
37
|
},
|
|
37
38
|
TeaserIndicators: {
|
|
38
39
|
accessLevel: (parent) => parent.accessLevel,
|