@financial-times/cp-content-pipeline-schema 2.14.2 → 2.15.1

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 (119) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/lib/datasources/capi.d.ts +2 -0
  3. package/lib/datasources/capi.js +17 -1
  4. package/lib/datasources/capi.js.map +1 -1
  5. package/lib/datasources/capi.test.js +6 -1
  6. package/lib/datasources/capi.test.js.map +1 -1
  7. package/lib/datasources/instrumented.js.map +1 -1
  8. package/lib/datasources/origami-image.js.map +1 -1
  9. package/lib/datasources/twitter.js.map +1 -1
  10. package/lib/datasources/url-management.js.map +1 -1
  11. package/lib/fixtures/dummyContext.js.map +1 -1
  12. package/lib/generated/index.d.ts +78 -16
  13. package/lib/helpers/decorateHeadshotUrl.js +1 -1
  14. package/lib/helpers/decorateHeadshotUrl.js.map +1 -1
  15. package/lib/helpers/flatten-formatted-zod-errors.d.ts +6 -0
  16. package/lib/helpers/flatten-formatted-zod-errors.js +39 -0
  17. package/lib/helpers/flatten-formatted-zod-errors.js.map +1 -0
  18. package/lib/helpers/imageService.js +1 -1
  19. package/lib/helpers/imageService.js.map +1 -1
  20. package/lib/helpers/isError.js +1 -1
  21. package/lib/helpers/isError.js.map +1 -1
  22. package/lib/helpers/metadata.js +1 -2
  23. package/lib/helpers/metadata.js.map +1 -1
  24. package/lib/index.d.ts +5 -1
  25. package/lib/index.js.map +1 -1
  26. package/lib/model/Byline.js +1 -1
  27. package/lib/model/Byline.js.map +1 -1
  28. package/lib/model/CapiList.d.ts +17 -0
  29. package/lib/model/CapiList.js +87 -0
  30. package/lib/model/CapiList.js.map +1 -0
  31. package/lib/model/CapiResponse.d.ts +1 -1
  32. package/lib/model/CapiResponse.js +8 -39
  33. package/lib/model/CapiResponse.js.map +1 -1
  34. package/lib/model/Clip.js.map +1 -1
  35. package/lib/model/Concept.js.map +1 -1
  36. package/lib/model/FlourishSource.js.map +1 -1
  37. package/lib/model/Image.d.ts +2 -34
  38. package/lib/model/Image.js +1 -1
  39. package/lib/model/Image.js.map +1 -1
  40. package/lib/model/Person.js.map +1 -1
  41. package/lib/model/Picture.js.map +1 -1
  42. package/lib/model/RichText.d.ts +1 -1
  43. package/lib/model/RichText.js.map +1 -1
  44. package/lib/model/Topper.d.ts +2 -2
  45. package/lib/model/Topper.js.map +1 -1
  46. package/lib/model/schemas/capi/article.d.ts +3 -3
  47. package/lib/model/schemas/capi/audio.d.ts +3 -3
  48. package/lib/model/schemas/capi/base-schema.d.ts +3 -3
  49. package/lib/model/schemas/capi/base-schema.js +1 -1
  50. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  51. package/lib/model/schemas/capi/content-package.d.ts +3 -3
  52. package/lib/model/schemas/capi/index.d.ts +15 -15
  53. package/lib/model/schemas/capi/index.js.map +1 -1
  54. package/lib/model/schemas/capi/list.d.ts +48 -0
  55. package/lib/model/schemas/capi/list.js +35 -0
  56. package/lib/model/schemas/capi/list.js.map +1 -0
  57. package/lib/model/schemas/capi/live-blog-package.d.ts +3 -3
  58. package/lib/model/schemas/capi/placeholder.d.ts +3 -3
  59. package/lib/model/schemas/capi/video.d.ts +3 -3
  60. package/lib/resolvers/clip.d.ts +5 -5
  61. package/lib/resolvers/content-tree/bodyXMLToTree.js +1 -1
  62. package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
  63. package/lib/resolvers/content-tree/extractText.js +1 -1
  64. package/lib/resolvers/content-tree/extractText.js.map +1 -1
  65. package/lib/resolvers/content-tree/nodePredicates.d.ts +3 -3
  66. package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
  67. package/lib/resolvers/content-tree/references/ClipSet.d.ts +2 -2
  68. package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -1
  69. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  70. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  71. package/lib/resolvers/content-tree/references/Recommended.js.map +1 -1
  72. package/lib/resolvers/content-tree/references/Reference.d.ts +2 -2
  73. package/lib/resolvers/content-tree/references/Reference.js.map +1 -1
  74. package/lib/resolvers/content-tree/references/Tweet.js.map +1 -1
  75. package/lib/resolvers/content-tree/references/Video.js.map +1 -1
  76. package/lib/resolvers/content-tree/tagMappings.js +5 -5
  77. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  78. package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js +1 -1
  79. package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js.map +1 -1
  80. package/lib/resolvers/content.d.ts +16 -16
  81. package/lib/resolvers/content.js.map +1 -1
  82. package/lib/resolvers/core.d.ts +1 -0
  83. package/lib/resolvers/core.js +27 -21
  84. package/lib/resolvers/core.js.map +1 -1
  85. package/lib/resolvers/image.js.map +1 -1
  86. package/lib/resolvers/index.d.ts +56 -44
  87. package/lib/resolvers/index.js +2 -0
  88. package/lib/resolvers/index.js.map +1 -1
  89. package/lib/resolvers/list.d.ts +14 -0
  90. package/lib/resolvers/list.js +16 -0
  91. package/lib/resolvers/list.js.map +1 -0
  92. package/lib/resolvers/literal-union.js.map +1 -1
  93. package/lib/resolvers/meta-link.js.map +1 -1
  94. package/lib/resolvers/richText.d.ts +5 -5
  95. package/lib/resolvers/scalars.d.ts +4 -0
  96. package/lib/resolvers/scalars.js +18 -1
  97. package/lib/resolvers/scalars.js.map +1 -1
  98. package/lib/types/cache.js +1 -2
  99. package/lib/types/cache.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/datasources/capi.test.ts +6 -1
  102. package/src/datasources/capi.ts +19 -1
  103. package/src/fixtures/dummyContext.ts +1 -1
  104. package/src/generated/index.ts +78 -16
  105. package/src/helpers/flatten-formatted-zod-errors.ts +67 -0
  106. package/src/index.ts +3 -1
  107. package/src/model/CapiList.ts +103 -0
  108. package/src/model/CapiResponse.ts +8 -70
  109. package/src/model/schemas/capi/base-schema.ts +1 -1
  110. package/src/model/schemas/capi/list.ts +36 -0
  111. package/src/resolvers/core.ts +29 -22
  112. package/src/resolvers/index.ts +2 -0
  113. package/src/resolvers/list.ts +15 -0
  114. package/src/resolvers/scalars.ts +30 -0
  115. package/tsconfig.tsbuildinfo +1 -1
  116. package/typedefs/content.graphql +8 -8
  117. package/typedefs/core.graphql +3 -0
  118. package/typedefs/list.graphql +20 -0
  119. package/typedefs/scalars.graphql +2 -0
@@ -6,6 +6,7 @@ import {
6
6
  KeyValueCache,
7
7
  PrefixingKeyValueCache,
8
8
  } from '@apollo/utils.keyvaluecache'
9
+ import { CapiList } from '../model/CapiList'
9
10
  import TimeoutError from '../helpers/timeout-error'
10
11
 
11
12
  const REQUEST_TIMEOUT = process.env.CAPI_DATASOURCE_REQUEST_TIMEOUT
@@ -57,7 +58,12 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
57
58
  uuid: string,
58
59
  packageContainer?: CapiResponse
59
60
  ): Promise<CapiResponse> {
60
- this.context.addSurrogateKeys([uuid])
61
+ this.context.addSurrogateKeys([
62
+ {
63
+ prefix: 'contentPipelineArticle',
64
+ id: uuid,
65
+ },
66
+ ])
61
67
 
62
68
  const content = await this.get(`internalcontent/${uuid}`, {
63
69
  cacheOptions: { ttl: this.articleCacheTTL },
@@ -79,6 +85,18 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
79
85
  })
80
86
  }
81
87
 
88
+ async getList(uuid: string): Promise<CapiList> {
89
+ this.context.addSurrogateKeys([
90
+ {
91
+ prefix: 'contentPipelineList',
92
+ id: uuid,
93
+ },
94
+ ])
95
+
96
+ const list = await this.get(`lists/${uuid}`)
97
+ return CapiList.fromJSON(list, this.context)
98
+ }
99
+
82
100
  // replicates the logic implicit in PrefixingKeyValueCache to
83
101
  // construct a cache key for an article used to purge Redis
84
102
  async getHTTPCacheKeyForContent(uuid: string) {
@@ -4,7 +4,7 @@ import cloneDeep from 'clone-deep'
4
4
  import { QueryContext } from '..'
5
5
  const now = Date.now()
6
6
 
7
- const addSurrogateKeys = (ids: string[]) => {
7
+ const addSurrogateKeys = (ids: {prefix: string, id: string}[]) => {
8
8
  const keys = []
9
9
  keys.push(...ids)
10
10
  }
@@ -1,6 +1,7 @@
1
1
  import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
2
2
  import type { Concept as ConceptModel } from '../model/Concept';
3
3
  import type { Person as PersonModel } from '../model/Person';
4
+ import type { CapiList } from '../model/CapiList';
4
5
  import type { CapiResponse } from '../model/CapiResponse';
5
6
  import type { Image as ImageModel } from '../model/Image';
6
7
  import type { Clip as ClipModel } from '../model/Clip';
@@ -37,6 +38,8 @@ export type Scalars = {
37
38
  ImageFormat: { input: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; output: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; }
38
39
  ImageType: { input: 'image' | 'graphic'; output: 'image' | 'graphic'; }
39
40
  JSON: { input: any; output: any; }
41
+ LayoutHint: { input: 'standaloneimage' | 'landscape' | 'bigstory' | 'assassination'; output: 'standaloneimage' | 'landscape' | 'bigstory' | 'assassination'; }
42
+ ListType: { input: 'OpinionAnalysis' | 'Promotional' | 'Recommended' | 'TopStories' | 'TopStoriesBeta' | 'KeyDevelopments'; output: 'OpinionAnalysis' | 'Promotional' | 'Recommended' | 'TopStories' | 'TopStoriesBeta' | 'KeyDevelopments'; }
40
43
  PackageDesign: { input: 'special-report' | 'extra' | 'basic' | 'extra-wide'; output: 'special-report' | 'extra' | 'basic' | 'extra-wide'; }
41
44
  RichTextSource: { input: 'standfirst' | 'summary' | 'bodyXML' | 'transcript'; output: 'standfirst' | 'summary' | 'bodyXML' | 'transcript'; }
42
45
  TopperBackgroundColour: { input: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; output: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; }
@@ -95,7 +98,7 @@ export type Article = Content & {
95
98
  /** The party that originated the article, eg. 'FT'. */
96
99
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
97
100
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
98
- readonly publishReference: Scalars['String']['output'];
101
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
99
102
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
100
103
  readonly publishedDate: Scalars['String']['output'];
101
104
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -161,7 +164,7 @@ export type Audio = Content & {
161
164
  /** The party that originated the article, eg. 'FT'. */
162
165
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
163
166
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
164
- readonly publishReference: Scalars['String']['output'];
167
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
165
168
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
166
169
  readonly publishedDate: Scalars['String']['output'];
167
170
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -372,7 +375,7 @@ export type Content = {
372
375
  /** The party that originated the article, eg. 'FT'. */
373
376
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
374
377
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
375
- readonly publishReference: Scalars['String']['output'];
378
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
376
379
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
377
380
  readonly publishedDate: Scalars['String']['output'];
378
381
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -440,7 +443,7 @@ export type ContentPackage = Content & {
440
443
  /** The party that originated the article, eg. 'FT'. */
441
444
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
442
445
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
443
- readonly publishReference: Scalars['String']['output'];
446
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
444
447
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
445
448
  readonly publishedDate: Scalars['String']['output'];
446
449
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -942,6 +945,27 @@ export type LeadFlourish = {
942
945
  readonly type?: Maybe<Scalars['String']['output']>;
943
946
  };
944
947
 
948
+ export type List = {
949
+ /** The upstream API URL for the list */
950
+ readonly apiUrl: Scalars['String']['output'];
951
+ /** The UUID of the list */
952
+ readonly id: Scalars['String']['output'];
953
+ /** The content items referenced by this list */
954
+ readonly items: ReadonlyArray<Content>;
955
+ /** Used by ft.com to determine the visual style to use on the website */
956
+ readonly layoutHint?: Maybe<Scalars['LayoutHint']['output']>;
957
+ /** The type of the list, e.g. Recommended */
958
+ readonly listType: Scalars['ListType']['output'];
959
+ /** Array of publication IDs this list pertains to, e.g. 88fdde6c-2aa4-4f78-af02-9f680097cfd6 for FT Pink */
960
+ readonly publication?: Maybe<ReadonlyArray<Scalars['String']['output']>>;
961
+ /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
962
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
963
+ /** The date when this list was published */
964
+ readonly publishedDate: Scalars['String']['output'];
965
+ /** The title of the list */
966
+ readonly title: Scalars['String']['output'];
967
+ };
968
+
945
969
  export type LiveBlogPackage = Content & {
946
970
  /** A scalar representing the access level of the article, eg. 'free'. */
947
971
  readonly accessLevel?: Maybe<Scalars['AccessLevel']['output']>;
@@ -980,7 +1004,7 @@ export type LiveBlogPackage = Content & {
980
1004
  /** The pinned article of this live blog package. */
981
1005
  readonly pinnedPost?: Maybe<Content>;
982
1006
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
983
- readonly publishReference: Scalars['String']['output'];
1007
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
984
1008
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
985
1009
  readonly publishedDate: Scalars['String']['output'];
986
1010
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -1058,7 +1082,7 @@ export type LiveBlogPost = Content & {
1058
1082
  /** The party that originated the article, eg. 'FT'. */
1059
1083
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1060
1084
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
1061
- readonly publishReference: Scalars['String']['output'];
1085
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
1062
1086
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
1063
1087
  readonly publishedDate: Scalars['String']['output'];
1064
1088
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -1288,7 +1312,7 @@ export type Placeholder = Content & {
1288
1312
  /** The party that originated the article, eg. 'FT'. */
1289
1313
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1290
1314
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
1291
- readonly publishReference: Scalars['String']['output'];
1315
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
1292
1316
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
1293
1317
  readonly publishedDate: Scalars['String']['output'];
1294
1318
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -1359,6 +1383,8 @@ export type Query = {
1359
1383
  readonly content: Content;
1360
1384
  /** The article content as resolved from passed-in content-api json data. */
1361
1385
  readonly contentFromJSON: Content;
1386
+ /** Resolve a content list from a uuid. */
1387
+ readonly list: List;
1362
1388
  /** The version of the schema, eg. 2.0.0. */
1363
1389
  readonly version: Scalars['String']['output'];
1364
1390
  };
@@ -1373,6 +1399,11 @@ export type QueryContentFromJsonArgs = {
1373
1399
  content: Scalars['JSON']['input'];
1374
1400
  };
1375
1401
 
1402
+
1403
+ export type QueryListArgs = {
1404
+ uuid: Scalars['String']['input'];
1405
+ };
1406
+
1376
1407
  export type RawImage = Reference & {
1377
1408
  /** The raw image object. */
1378
1409
  readonly image: Image;
@@ -1619,7 +1650,7 @@ export type Video = Content & {
1619
1650
  /** The party that originated the article, eg. 'FT'. */
1620
1651
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1621
1652
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
1622
- readonly publishReference: Scalars['String']['output'];
1653
+ readonly publishReference?: Maybe<Scalars['String']['output']>;
1623
1654
  /** The ISO string date the article was published, eg. '2024-04-03T10:35:52.443Z'. */
1624
1655
  readonly publishedDate: Scalars['String']['output'];
1625
1656
  /** The number of milliseconds since the unix epoch the article was published, eg '1712140552443'. */
@@ -1793,8 +1824,11 @@ export type ResolversTypes = ResolversObject<{
1793
1824
  Indicators: ResolverTypeWrapper<Indicators>;
1794
1825
  Int: ResolverTypeWrapper<Scalars['Int']['output']>;
1795
1826
  JSON: ResolverTypeWrapper<Scalars['JSON']['output']>;
1827
+ LayoutHint: ResolverTypeWrapper<Scalars['LayoutHint']['output']>;
1796
1828
  LayoutImage: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.LayoutImage>>;
1797
1829
  LeadFlourish: ResolverTypeWrapper<LeadFlourishModel>;
1830
+ List: ResolverTypeWrapper<CapiList>;
1831
+ ListType: ResolverTypeWrapper<Scalars['ListType']['output']>;
1798
1832
  LiveBlogPackage: ResolverTypeWrapper<CapiResponse>;
1799
1833
  LiveBlogPost: ResolverTypeWrapper<CapiResponse>;
1800
1834
  MainImage: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.ImageSet>>;
@@ -1884,8 +1918,11 @@ export type ResolversParentTypes = ResolversObject<{
1884
1918
  Indicators: Indicators;
1885
1919
  Int: Scalars['Int']['output'];
1886
1920
  JSON: Scalars['JSON']['output'];
1921
+ LayoutHint: Scalars['LayoutHint']['output'];
1887
1922
  LayoutImage: ReferenceWithCAPIData<ContentTree.LayoutImage>;
1888
1923
  LeadFlourish: LeadFlourishModel;
1924
+ List: CapiList;
1925
+ ListType: Scalars['ListType']['output'];
1889
1926
  LiveBlogPackage: CapiResponse;
1890
1927
  LiveBlogPost: CapiResponse;
1891
1928
  MainImage: ReferenceWithCAPIData<ContentTree.ImageSet>;
@@ -1965,7 +2002,7 @@ export type ArticleResolvers<ContextType = QueryContext, ParentType extends Reso
1965
2002
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
1966
2003
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
1967
2004
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1968
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2005
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1969
2006
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1970
2007
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
1971
2008
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -1995,7 +2032,7 @@ export type AudioResolvers<ContextType = QueryContext, ParentType extends Resolv
1995
2032
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
1996
2033
  media: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Media']>>>, ParentType, ContextType>;
1997
2034
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1998
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2035
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1999
2036
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2000
2037
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2001
2038
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2120,7 +2157,7 @@ export type ContentResolvers<ContextType = QueryContext, ParentType extends Reso
2120
2157
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2121
2158
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2122
2159
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2123
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2160
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2124
2161
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2125
2162
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2126
2163
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2150,7 +2187,7 @@ export type ContentPackageResolvers<ContextType = QueryContext, ParentType exten
2150
2187
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2151
2188
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2152
2189
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2153
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2190
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2154
2191
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2155
2192
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2156
2193
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2426,6 +2463,10 @@ export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes
2426
2463
  name: 'JSON';
2427
2464
  }
2428
2465
 
2466
+ export interface LayoutHintScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['LayoutHint'], any> {
2467
+ name: 'LayoutHint';
2468
+ }
2469
+
2429
2470
  export type LayoutImageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LayoutImage'] = ResolversParentTypes['LayoutImage']> = ResolversObject<{
2430
2471
  picture: Resolver<Maybe<ResolversTypes['Picture']>, ParentType, ContextType>;
2431
2472
  type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2440,6 +2481,23 @@ export type LeadFlourishResolvers<ContextType = QueryContext, ParentType extends
2440
2481
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2441
2482
  }>;
2442
2483
 
2484
+ export type ListResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['List'] = ResolversParentTypes['List']> = ResolversObject<{
2485
+ apiUrl: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2486
+ id: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2487
+ items: Resolver<ReadonlyArray<ResolversTypes['Content']>, ParentType, ContextType>;
2488
+ layoutHint: Resolver<Maybe<ResolversTypes['LayoutHint']>, ParentType, ContextType>;
2489
+ listType: Resolver<ResolversTypes['ListType'], ParentType, ContextType>;
2490
+ publication: Resolver<Maybe<ReadonlyArray<ResolversTypes['String']>>, ParentType, ContextType>;
2491
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2492
+ publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2493
+ title: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2494
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2495
+ }>;
2496
+
2497
+ export interface ListTypeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['ListType'], any> {
2498
+ name: 'ListType';
2499
+ }
2500
+
2443
2501
  export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LiveBlogPackage'] = ResolversParentTypes['LiveBlogPackage']> = ResolversObject<{
2444
2502
  accessLevel: Resolver<Maybe<ResolversTypes['AccessLevel']>, ParentType, ContextType>;
2445
2503
  altStandfirst: Resolver<Maybe<ResolversTypes['AltStandfirst']>, ParentType, ContextType>;
@@ -2459,7 +2517,7 @@ export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType exte
2459
2517
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2460
2518
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2461
2519
  pinnedPost: Resolver<Maybe<ResolversTypes['Content']>, ParentType, ContextType>;
2462
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2520
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2463
2521
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2464
2522
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2465
2523
  realtime: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
@@ -2493,7 +2551,7 @@ export type LiveBlogPostResolvers<ContextType = QueryContext, ParentType extends
2493
2551
  isPinned: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2494
2552
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2495
2553
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2496
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2554
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2497
2555
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2498
2556
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2499
2557
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2628,7 +2686,7 @@ export type PlaceholderResolvers<ContextType = QueryContext, ParentType extends
2628
2686
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2629
2687
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2630
2688
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2631
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2689
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2632
2690
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2633
2691
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2634
2692
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2660,6 +2718,7 @@ export type PodcastTopperResolvers<ContextType = QueryContext, ParentType extend
2660
2718
  export type QueryResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = ResolversObject<{
2661
2719
  content: Resolver<ResolversTypes['Content'], ParentType, ContextType, RequireFields<QueryContentArgs, 'uuid'>>;
2662
2720
  contentFromJSON: Resolver<ResolversTypes['Content'], ParentType, ContextType, RequireFields<QueryContentFromJsonArgs, 'content'>>;
2721
+ list: Resolver<ResolversTypes['List'], ParentType, ContextType, RequireFields<QueryListArgs, 'uuid'>>;
2663
2722
  version: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2664
2723
  }>;
2665
2724
 
@@ -2830,7 +2889,7 @@ export type VideoResolvers<ContextType = QueryContext, ParentType extends Resolv
2830
2889
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2831
2890
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2832
2891
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2833
- publishReference: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2892
+ publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2834
2893
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2835
2894
  publishedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2836
2895
  standfirst: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2892,8 +2951,11 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
2892
2951
  ImageWide: ImageWideResolvers<ContextType>;
2893
2952
  Indicators: IndicatorsResolvers<ContextType>;
2894
2953
  JSON: GraphQLScalarType;
2954
+ LayoutHint: GraphQLScalarType;
2895
2955
  LayoutImage: LayoutImageResolvers<ContextType>;
2896
2956
  LeadFlourish: LeadFlourishResolvers<ContextType>;
2957
+ List: ListResolvers<ContextType>;
2958
+ ListType: GraphQLScalarType;
2897
2959
  LiveBlogPackage: LiveBlogPackageResolvers<ContextType>;
2898
2960
  LiveBlogPost: LiveBlogPostResolvers<ContextType>;
2899
2961
  MainImage: MainImageResolvers<ContextType>;
@@ -0,0 +1,67 @@
1
+ function isPlainObject(object: unknown): object is Record<string, unknown> {
2
+ if (object && typeof object === 'object') {
3
+ return true
4
+ }
5
+
6
+ return false
7
+ }
8
+
9
+ type FormattedZodIssues = {
10
+ _errors: string[]
11
+ [key: string]: FormattedZodIssues | string[]
12
+ }
13
+
14
+ export default function flattenFormattedZodIssues(
15
+ issues: FormattedZodIssues,
16
+ object: unknown,
17
+ path: string[] = [],
18
+ flattened: string[] = []
19
+ ): string[] {
20
+ if (issues._errors.length > 0) {
21
+ const literalErrors = issues._errors.filter((error) =>
22
+ error.startsWith('Invalid literal value')
23
+ )
24
+ const otherErrors = issues._errors.filter(
25
+ (error) => !error.startsWith('Invalid literal value')
26
+ )
27
+
28
+ if (literalErrors.length > 0) {
29
+ flattened.push(
30
+ `${path.join(
31
+ '.'
32
+ )}: Invalid literal value, received "${object}", expected one of ` +
33
+ literalErrors
34
+ .flatMap((error) => {
35
+ const match = error.match(/expected (".+")$/)
36
+ if (match && match[1]) {
37
+ return [match[1]]
38
+ }
39
+
40
+ return []
41
+ })
42
+ .join()
43
+ )
44
+ }
45
+
46
+ flattened.push(...otherErrors.map((error) => `${path.join('.')}: ${error}`))
47
+ }
48
+
49
+ for (const [key, value] of Object.entries(issues)) {
50
+ if (
51
+ key !== '_errors' &&
52
+ !Array.isArray(value) &&
53
+ object &&
54
+ (Array.isArray(object) || isPlainObject(object))
55
+ ) {
56
+ const isArrayKey = !Number.isNaN(parseInt(key))
57
+ flattenFormattedZodIssues(
58
+ value,
59
+ Array.isArray(object) ? object[parseInt(key)] : object[key],
60
+ [...path, isArrayKey ? '[]' : key],
61
+ flattened
62
+ )
63
+ }
64
+ }
65
+
66
+ return flattened
67
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,8 @@ export type BodyXMLToTreeError = {
17
17
  actual: AnyNode['type'] | AnyNode['type'][]
18
18
  }
19
19
 
20
+ export type SurrogateKey = { prefix: string; id: string }
21
+
20
22
  export interface QueryContext {
21
23
  redisAdapter: KeyValueCache
22
24
  metrics: Metrics
@@ -30,7 +32,7 @@ export interface QueryContext {
30
32
  schema: string
31
33
  }
32
34
  aggregatedErrors?: { bodyXMLToTree: BodyXMLToTreeError[] }
33
- addSurrogateKeys(keys: string[]): void
35
+ addSurrogateKeys(keys: SurrogateKey[]): void
34
36
  }
35
37
 
36
38
  export const articleDocumentQuery = readFileSync(
@@ -0,0 +1,103 @@
1
+ import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
2
+ import { QueryContext } from '..'
3
+ import { uuidFromUrl } from '../helpers/metadata'
4
+ import { List, listSchema } from './schemas/capi/list'
5
+ import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
6
+
7
+ export class CapiList {
8
+ static fromJSON(list: unknown, context: QueryContext): CapiList {
9
+ const result = listSchema.safeParse(list)
10
+
11
+ if (!result.success) {
12
+ context.logger.warn({
13
+ event: 'RECOVERABLE_ERROR',
14
+ error: new OperationalError({
15
+ message:
16
+ 'The data received from the CAPI data source does not match our data source schema. It is likely that our schema will require updating to handle all possible responses from CAPI.',
17
+ code: 'CAPI_SCHEMA_VALIDATION_FAILURE',
18
+ schemaError: flattenFormattedZodIssues(result.error.format(), list),
19
+ contentId: (list as { id?: string }).id,
20
+ contentType: 'List',
21
+ }),
22
+ })
23
+ context.metrics?.count(
24
+ `graphql.datasource.CapiDataSource.List.validation.failure.count`,
25
+ 1
26
+ )
27
+ } else {
28
+ context.metrics?.count(
29
+ `graphql.datasource.CapiDataSource.List.validation.success.count`,
30
+ 1
31
+ )
32
+ }
33
+
34
+ return new CapiList(list as List, context)
35
+ }
36
+
37
+ constructor(private list: List, private context: QueryContext) {}
38
+
39
+ async items() {
40
+ if (!this.list.items) return []
41
+
42
+ const items = await Promise.all(
43
+ this.list.items.map(({ id }) =>
44
+ this.context.dataSources.capi
45
+ .getContent(uuidFromUrl(id))
46
+ .catch((error) => {
47
+ if (error instanceof BaseError) {
48
+ if (error.data.upstreamStatusCode === 404) {
49
+ this.context.logger.warn({
50
+ event: 'RECOVERABLE_ERROR',
51
+ error: new OperationalError({
52
+ code: 'LIST_CONTENT_NOT_FOUND',
53
+ message: 'List content not found in Content API',
54
+ uuid: uuidFromUrl(error.data.upstreamUrl),
55
+ cause: error,
56
+ }),
57
+ })
58
+
59
+ return null
60
+ }
61
+ }
62
+
63
+ throw error
64
+ })
65
+ )
66
+ )
67
+
68
+ // filter out missing list items to match n-lists-client behaviour
69
+ return items.filter((item) => item !== null)
70
+ }
71
+
72
+ id() {
73
+ return uuidFromUrl(this.list.id)
74
+ }
75
+
76
+ apiUrl() {
77
+ return this.list.apiUrl
78
+ }
79
+
80
+ title() {
81
+ return this.list.title
82
+ }
83
+
84
+ listType() {
85
+ return this.list.listType
86
+ }
87
+
88
+ publishedDate() {
89
+ return this.list.publishedDate
90
+ }
91
+
92
+ layoutHint() {
93
+ return this.list.layoutHint ?? null
94
+ }
95
+
96
+ publication() {
97
+ return this.list.publication ?? null
98
+ }
99
+
100
+ publishReference() {
101
+ return this.list.publishReference ?? null
102
+ }
103
+ }
@@ -39,25 +39,13 @@ import { Topper } from './Topper'
39
39
  import { z } from 'zod'
40
40
  import { Byline } from './Byline'
41
41
  import { RichText } from './RichText'
42
-
43
- function isPlainObject(object: unknown): object is Record<string, unknown> {
44
- if (object && typeof object === 'object') {
45
- return true
46
- }
47
-
48
- return false
49
- }
42
+ import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
50
43
 
51
44
  type Design = {
52
45
  theme: LiteralUnionScalarValues<typeof PackageDesign>
53
46
  layout?: 'default' | 'wide'
54
47
  }
55
48
 
56
- type FormattedZodIssues = {
57
- _errors: string[]
58
- [key: string]: FormattedZodIssues | string[]
59
- }
60
-
61
49
  type ZodInputValue =
62
50
  | ZodInputObject
63
51
  | ZodInputValue[]
@@ -71,61 +59,6 @@ type ZodInputObject = {
71
59
  [key: string]: ZodInputValue
72
60
  }
73
61
 
74
- function flattenFormattedZodIssues(
75
- issues: FormattedZodIssues,
76
- object: unknown,
77
- path: string[] = [],
78
- flattened: string[] = []
79
- ): string[] {
80
- if (issues._errors.length > 0) {
81
- const literalErrors = issues._errors.filter((error) =>
82
- error.startsWith('Invalid literal value')
83
- )
84
- const otherErrors = issues._errors.filter(
85
- (error) => !error.startsWith('Invalid literal value')
86
- )
87
-
88
- if (literalErrors.length > 0) {
89
- flattened.push(
90
- `${path.join(
91
- '.'
92
- )}: Invalid literal value, received "${object}", expected one of ` +
93
- literalErrors
94
- .flatMap((error) => {
95
- const match = error.match(/expected (".+")$/)
96
- if (match && match[1]) {
97
- return [match[1]]
98
- }
99
-
100
- return []
101
- })
102
- .join()
103
- )
104
- }
105
-
106
- flattened.push(...otherErrors.map((error) => `${path.join('.')}: ${error}`))
107
- }
108
-
109
- for (const [key, value] of Object.entries(issues)) {
110
- if (
111
- key !== '_errors' &&
112
- !Array.isArray(value) &&
113
- object &&
114
- (Array.isArray(object) || isPlainObject(object))
115
- ) {
116
- const isArrayKey = !Number.isNaN(parseInt(key))
117
- flattenFormattedZodIssues(
118
- value,
119
- Array.isArray(object) ? object[parseInt(key)] : object[key],
120
- [...path, isArrayKey ? '[]' : key],
121
- flattened
122
- )
123
- }
124
- }
125
-
126
- return flattened
127
- }
128
-
129
62
  function getContentType(
130
63
  content: BaselineContent,
131
64
  validate?: true
@@ -390,7 +323,7 @@ export class CapiResponse {
390
323
  return this.capiData.firstPublishedDate || this.capiData.publishedDate
391
324
  }
392
325
  publishReference() {
393
- return this.capiData.publishReference
326
+ return this.capiData.publishReference ?? null
394
327
  }
395
328
  alternativeTitle() {
396
329
  return this.capiData.alternativeTitles ?? null
@@ -730,7 +663,12 @@ export class CapiResponse {
730
663
  const contains = await this.contains()
731
664
 
732
665
  if (contains && contains.length) {
733
- this.context.addSurrogateKeys(contains.map((article) => article.id()))
666
+ this.context.addSurrogateKeys(
667
+ contains.map((article) => ({
668
+ prefix: 'contentPipelineArticle',
669
+ id: article.id(),
670
+ }))
671
+ )
734
672
 
735
673
  const liveBlogPosts = contains.sort(
736
674
  (a, b) => b.publishedTimestamp() - a.publishedTimestamp()
@@ -194,7 +194,7 @@ export const baseMetadataSchema = z.object({
194
194
  .optional(),
195
195
  publishedDate: z.string(),
196
196
  firstPublishedDate: z.string(),
197
- publishReference: z.string(),
197
+ publishReference: z.string().optional(),
198
198
  realtime: z.boolean(),
199
199
  editorialDesk: z.string().optional(),
200
200
  accessLevel: z