@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/generated/index.d.ts +19 -2
  3. package/lib/resolvers/content-tree/Workarounds.d.ts +2 -2
  4. package/lib/resolvers/content-tree/nodePredicates.d.ts +1 -1
  5. package/lib/resolvers/content-tree/nodePredicates.js +1 -0
  6. package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
  7. package/lib/resolvers/content-tree/references/Flourish.test.js +3 -3
  8. package/lib/resolvers/content-tree/references/Flourish.test.js.map +1 -1
  9. package/lib/resolvers/content-tree/references/RecommendedList.d.ts +5 -0
  10. package/lib/resolvers/content-tree/references/RecommendedList.js +41 -0
  11. package/lib/resolvers/content-tree/references/RecommendedList.js.map +1 -0
  12. package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
  13. package/lib/resolvers/content-tree/references/index.d.ts +3 -1
  14. package/lib/resolvers/content-tree/references/index.js +3 -0
  15. package/lib/resolvers/content-tree/references/index.js.map +1 -1
  16. package/lib/resolvers/content-tree/tagMappings.js +33 -6
  17. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  18. package/lib/resolvers/index.d.ts +2 -0
  19. package/lib/resolvers/teaser.d.ts +1 -0
  20. package/lib/resolvers/teaser.js +1 -0
  21. package/lib/resolvers/teaser.js.map +1 -1
  22. package/package.json +2 -2
  23. package/queries/article.graphql +9 -0
  24. package/src/generated/index.ts +21 -2
  25. package/src/resolvers/content-tree/Workarounds.ts +2 -0
  26. package/src/resolvers/content-tree/nodePredicates.ts +1 -0
  27. package/src/resolvers/content-tree/references/Flourish.test.ts +3 -3
  28. package/src/resolvers/content-tree/references/RecommendedList.ts +52 -0
  29. package/src/resolvers/content-tree/references/index.ts +5 -0
  30. package/src/resolvers/content-tree/tagMappings.ts +40 -6
  31. package/src/resolvers/teaser.ts +1 -0
  32. package/tsconfig.tsbuildinfo +1 -1
  33. package/typedefs/references/recommendedList.graphql +7 -0
  34. package/typedefs/teaser.graphql +3 -0
@@ -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
@@ -28,6 +28,7 @@ export const bodyBlockTypes = [
28
28
  'thematic-break',
29
29
  'table',
30
30
  'recommended',
31
+ 'recommended-list',
31
32
  'tweet',
32
33
  'video',
33
34
  'youtube-video',
@@ -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
- type: 'link',
124
- url: $el.attr('href') || '',
125
- title: '',
126
- children: childrenOfTypes(phrasingTypes, traverse(), 'link', context),
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()
@@ -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,