@financial-times/cp-content-pipeline-schema 3.4.0 → 3.5.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 (113) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/lib/datasources/capi.d.ts +0 -2
  3. package/lib/datasources/capi.js +10 -23
  4. package/lib/datasources/capi.js.map +1 -1
  5. package/lib/datasources/instrumented.js +4 -1
  6. package/lib/datasources/instrumented.js.map +1 -1
  7. package/lib/datasources/origami-image.d.ts +0 -2
  8. package/lib/datasources/origami-image.js +5 -19
  9. package/lib/datasources/origami-image.js.map +1 -1
  10. package/lib/datasources/twitter.d.ts +0 -2
  11. package/lib/datasources/twitter.js +2 -16
  12. package/lib/datasources/twitter.js.map +1 -1
  13. package/lib/datasources/url-management.js +5 -3
  14. package/lib/datasources/url-management.js.map +1 -1
  15. package/lib/fixtures/dummyContext.js +1 -1
  16. package/lib/generated/index.d.ts +73 -22
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.js.map +1 -1
  19. package/lib/model/Byline.js +11 -14
  20. package/lib/model/Byline.js.map +1 -1
  21. package/lib/model/CapiList.js +2 -0
  22. package/lib/model/CapiList.js.map +1 -1
  23. package/lib/model/CapiResponse.d.ts +7 -3
  24. package/lib/model/CapiResponse.js +125 -57
  25. package/lib/model/CapiResponse.js.map +1 -1
  26. package/lib/model/CapiResponse.test.js +92 -7
  27. package/lib/model/CapiResponse.test.js.map +1 -1
  28. package/lib/model/Clip.js +2 -0
  29. package/lib/model/Clip.js.map +1 -1
  30. package/lib/model/Concept.js +4 -10
  31. package/lib/model/Concept.js.map +1 -1
  32. package/lib/model/FlourishSource.d.ts +12 -5
  33. package/lib/model/FlourishSource.js +37 -38
  34. package/lib/model/FlourishSource.js.map +1 -1
  35. package/lib/model/FlourishSource.test.js +5 -5
  36. package/lib/model/FlourishSource.test.js.map +1 -1
  37. package/lib/model/Image.js +22 -20
  38. package/lib/model/Image.js.map +1 -1
  39. package/lib/model/LeadFlourish.d.ts +1 -1
  40. package/lib/model/LeadFlourish.js +8 -11
  41. package/lib/model/LeadFlourish.js.map +1 -1
  42. package/lib/model/LeadFlourish.test.js +5 -7
  43. package/lib/model/LeadFlourish.test.js.map +1 -1
  44. package/lib/model/Person.js +5 -16
  45. package/lib/model/Person.js.map +1 -1
  46. package/lib/model/Picture.js +3 -0
  47. package/lib/model/Picture.js.map +1 -1
  48. package/lib/model/RichText.js +3 -0
  49. package/lib/model/RichText.js.map +1 -1
  50. package/lib/model/Topper.js +5 -16
  51. package/lib/model/Topper.js.map +1 -1
  52. package/lib/model/schemas/capi/article.d.ts +3 -3
  53. package/lib/model/schemas/capi/audio.d.ts +5 -5
  54. package/lib/model/schemas/capi/base-schema.d.ts +5 -5
  55. package/lib/model/schemas/capi/base-schema.js +1 -1
  56. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  57. package/lib/model/schemas/capi/content-package.d.ts +3 -3
  58. package/lib/model/schemas/capi/custom-code-component.d.ts +3 -3
  59. package/lib/model/schemas/capi/index.d.ts +20 -20
  60. package/lib/model/schemas/capi/internal-content.d.ts +1 -1
  61. package/lib/model/schemas/capi/live-blog-package.d.ts +3 -3
  62. package/lib/model/schemas/capi/placeholder.d.ts +3 -3
  63. package/lib/model/schemas/capi/video.d.ts +3 -3
  64. package/lib/resolvers/content-tree/references/Flourish.d.ts +9 -19
  65. package/lib/resolvers/content-tree/references/Flourish.js +10 -32
  66. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  67. package/lib/resolvers/content-tree/references/Flourish.test.js +3 -3
  68. package/lib/resolvers/content-tree/references/Flourish.test.js.map +1 -1
  69. package/lib/resolvers/content-tree/references/RawImage.js +2 -0
  70. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  71. package/lib/resolvers/content-tree/references/index.d.ts +2 -2
  72. package/lib/resolvers/content-tree/references/index.js +1 -1
  73. package/lib/resolvers/content-tree/references/index.js.map +1 -1
  74. package/lib/resolvers/content.d.ts +26 -1
  75. package/lib/resolvers/content.js +21 -1
  76. package/lib/resolvers/content.js.map +1 -1
  77. package/lib/resolvers/index.d.ts +27 -3
  78. package/lib/resolvers/leadFlourish.d.ts +1 -1
  79. package/lib/resolvers/literal-union.js +1 -0
  80. package/lib/resolvers/literal-union.js.map +1 -1
  81. package/lib/types/connection.d.ts +21 -0
  82. package/lib/types/connection.js +5 -0
  83. package/lib/types/connection.js.map +1 -0
  84. package/package.json +1 -1
  85. package/queries/article.graphql +8 -2
  86. package/src/datasources/capi.ts +1 -14
  87. package/src/datasources/origami-image.ts +1 -13
  88. package/src/datasources/twitter.ts +1 -14
  89. package/src/fixtures/dummyContext.ts +1 -1
  90. package/src/generated/index.ts +75 -24
  91. package/src/index.ts +1 -0
  92. package/src/model/CapiResponse.test.ts +137 -7
  93. package/src/model/CapiResponse.ts +129 -37
  94. package/src/model/FlourishSource.test.ts +5 -5
  95. package/src/model/FlourishSource.ts +57 -39
  96. package/src/model/Image.ts +16 -0
  97. package/src/model/LeadFlourish.test.ts +6 -9
  98. package/src/model/LeadFlourish.ts +5 -1
  99. package/src/model/schemas/capi/base-schema.ts +1 -1
  100. package/src/model/schemas/capi/internal-content.ts +1 -1
  101. package/src/resolvers/content-tree/references/Flourish.test.ts +4 -3
  102. package/src/resolvers/content-tree/references/Flourish.ts +16 -32
  103. package/src/resolvers/content-tree/references/index.ts +4 -4
  104. package/src/resolvers/content.ts +31 -1
  105. package/src/types/connection.ts +28 -0
  106. package/tsconfig.tsbuildinfo +1 -1
  107. package/typedefs/content.graphql +40 -2
  108. package/typedefs/leadFlourish.graphql +1 -1
  109. package/typedefs/references/flourish.graphql +1 -11
  110. package/lib/helpers/timeout-error.d.ts +0 -6
  111. package/lib/helpers/timeout-error.js +0 -15
  112. package/lib/helpers/timeout-error.js.map +0 -1
  113. package/src/helpers/timeout-error.ts +0 -13
@@ -513,6 +513,20 @@ export type ContentUrlArgs = {
513
513
  vanity?: InputMaybe<Scalars['Boolean']['input']>;
514
514
  };
515
515
 
516
+ export type ContentConnection = {
517
+ /** A list of edges for the connection. */
518
+ readonly edges: ReadonlyArray<Maybe<ContentEdge>>;
519
+ /** Pagination data for the connection. */
520
+ readonly pageInfo: PageInfo;
521
+ };
522
+
523
+ export type ContentEdge = {
524
+ /** The opaque cursor for this edge that can be used for future requests. */
525
+ readonly cursor: Scalars['String']['output'];
526
+ /** The child article of this edge. */
527
+ readonly node?: Maybe<Content>;
528
+ };
529
+
516
530
  export type ContentPackage = Content & {
517
531
  /** A scalar representing the access level of the article, eg. 'free'. */
518
532
  readonly accessLevel?: Maybe<Scalars['AccessLevel']['output']>;
@@ -680,20 +694,11 @@ export type Design = {
680
694
 
681
695
  export type Flourish = Reference & {
682
696
  /** The fallback image to be used if the flourish graphics cannot be loaded. */
683
- readonly fallbackImage?: Maybe<FlourishFallback>;
697
+ readonly fallbackImage?: Maybe<FlourishSource>;
684
698
  /** The type of the reference, eg. 'flourish'. */
685
699
  readonly type: Scalars['String']['output'];
686
700
  };
687
701
 
688
- export type FlourishFallback = {
689
- /** The height in pixels of the fallback image. */
690
- readonly height?: Maybe<Scalars['Int']['output']>;
691
- /** The url of the fallback image. */
692
- readonly url?: Maybe<Scalars['String']['output']>;
693
- /** The width in pixels of the fallback image. */
694
- readonly width?: Maybe<Scalars['Int']['output']>;
695
- };
696
-
697
702
  export type FlourishSource = {
698
703
  /** The format of the source, eg. 'standard'. */
699
704
  readonly format: Scalars['ImageFormat']['output'];
@@ -1061,7 +1066,7 @@ export type LayoutImage = Reference & {
1061
1066
  export type LeadFlourish = {
1062
1067
  /** The description of the Flourish chart. */
1063
1068
  readonly description?: Maybe<Scalars['String']['output']>;
1064
- readonly fallbackImage: FlourishSource;
1069
+ readonly fallbackImage?: Maybe<FlourishSource>;
1065
1070
  /** The id of the Flourish chart. */
1066
1071
  readonly id?: Maybe<Scalars['String']['output']>;
1067
1072
  /** The type of the chart, eg. 'visualisation'. */
@@ -1120,6 +1125,8 @@ export type LiveBlogPackage = Content & {
1120
1125
  readonly instantAlertConcept?: Maybe<Concept>;
1121
1126
  /** The child articles of this live blog package. */
1122
1127
  readonly liveBlogPosts?: Maybe<ReadonlyArray<Maybe<Content>>>;
1128
+ /** The child articles of this live blog package following the [Connections](https://relay.dev/graphql/connections.htm) spec. */
1129
+ readonly liveBlogPostsConnection?: Maybe<ContentConnection>;
1123
1130
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1124
1131
  readonly mainImage?: Maybe<Image>;
1125
1132
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
@@ -1161,6 +1168,19 @@ export type LiveBlogPackageBylineArgs = {
1161
1168
  };
1162
1169
 
1163
1170
 
1171
+ export type LiveBlogPackageLiveBlogPostsArgs = {
1172
+ count?: InputMaybe<Scalars['Int']['input']>;
1173
+ };
1174
+
1175
+
1176
+ export type LiveBlogPackageLiveBlogPostsConnectionArgs = {
1177
+ after?: InputMaybe<Scalars['String']['input']>;
1178
+ before?: InputMaybe<Scalars['String']['input']>;
1179
+ first?: InputMaybe<Scalars['Int']['input']>;
1180
+ last?: InputMaybe<Scalars['Int']['input']>;
1181
+ };
1182
+
1183
+
1164
1184
  export type LiveBlogPackageUrlArgs = {
1165
1185
  relative?: InputMaybe<Scalars['Boolean']['input']>;
1166
1186
  vanity?: InputMaybe<Scalars['Boolean']['input']>;
@@ -1312,6 +1332,17 @@ export type OpinionTopperHeadshotArgs = {
1312
1332
  width?: InputMaybe<Scalars['Int']['input']>;
1313
1333
  };
1314
1334
 
1335
+ export type PageInfo = {
1336
+ /** The cursor for the last edge. */
1337
+ readonly endCursor?: Maybe<Scalars['String']['output']>;
1338
+ /** Whether a subsequent page of edges is available on request. */
1339
+ readonly hasNextPage: Scalars['Boolean']['output'];
1340
+ /** Whether a previous page of edges is available on request. */
1341
+ readonly hasPreviousPage: Scalars['Boolean']['output'];
1342
+ /** The cursor for the first edge. */
1343
+ readonly startCursor?: Maybe<Scalars['String']['output']>;
1344
+ };
1345
+
1315
1346
  export type PartnerContentTopper = Topper & TopperWithImages & TopperWithTheme & {
1316
1347
  /** Whether the topper should have a background box. */
1317
1348
  readonly backgroundBox?: Maybe<Scalars['Boolean']['output']>;
@@ -1976,6 +2007,8 @@ export type ResolversTypes = ResolversObject<{
1976
2007
  Concept: ResolverTypeWrapper<ConceptModel>;
1977
2008
  ConceptInterface: ResolverTypeWrapper<ConceptModel>;
1978
2009
  Content: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Content']>;
2010
+ ContentConnection: ResolverTypeWrapper<Omit<ContentConnection, 'edges'> & { edges: ReadonlyArray<Maybe<ResolversTypes['ContentEdge']>> }>;
2011
+ ContentEdge: ResolverTypeWrapper<Omit<ContentEdge, 'node'> & { node: Maybe<ResolversTypes['Content']> }>;
1979
2012
  ContentPackage: ResolverTypeWrapper<CapiResponse>;
1980
2013
  ContentType: ResolverTypeWrapper<Scalars['ContentType']['output']>;
1981
2014
  CustomCodeComponent: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.CustomCodeComponent>>;
@@ -1984,7 +2017,6 @@ export type ResolversTypes = ResolversObject<{
1984
2017
  Design: ResolverTypeWrapper<Design>;
1985
2018
  Float: ResolverTypeWrapper<Scalars['Float']['output']>;
1986
2019
  Flourish: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.Flourish>>;
1987
- FlourishFallback: ResolverTypeWrapper<FlourishFallback>;
1988
2020
  FlourishSource: ResolverTypeWrapper<FlourishSourceModel>;
1989
2021
  FollowButtonVariant: ResolverTypeWrapper<Scalars['FollowButtonVariant']['output']>;
1990
2022
  FullBleedTopper: ResolverTypeWrapper<TopperModel>;
@@ -2019,6 +2051,7 @@ export type ResolversTypes = ResolversObject<{
2019
2051
  Mutation: ResolverTypeWrapper<{}>;
2020
2052
  OpinionTopper: ResolverTypeWrapper<TopperModel>;
2021
2053
  PackageDesign: ResolverTypeWrapper<Scalars['PackageDesign']['output']>;
2054
+ PageInfo: ResolverTypeWrapper<PageInfo>;
2022
2055
  PartnerContentTopper: ResolverTypeWrapper<TopperModel>;
2023
2056
  Person: ResolverTypeWrapper<PersonModel>;
2024
2057
  Picture: ResolverTypeWrapper<PictureModel>;
@@ -2075,6 +2108,8 @@ export type ResolversParentTypes = ResolversObject<{
2075
2108
  Concept: ConceptModel;
2076
2109
  ConceptInterface: ConceptModel;
2077
2110
  Content: ResolversInterfaceTypes<ResolversParentTypes>['Content'];
2111
+ ContentConnection: Omit<ContentConnection, 'edges'> & { edges: ReadonlyArray<Maybe<ResolversParentTypes['ContentEdge']>> };
2112
+ ContentEdge: Omit<ContentEdge, 'node'> & { node: Maybe<ResolversParentTypes['Content']> };
2078
2113
  ContentPackage: CapiResponse;
2079
2114
  ContentType: Scalars['ContentType']['output'];
2080
2115
  CustomCodeComponent: ReferenceWithCAPIData<ContentTree.CustomCodeComponent>;
@@ -2083,7 +2118,6 @@ export type ResolversParentTypes = ResolversObject<{
2083
2118
  Design: Design;
2084
2119
  Float: Scalars['Float']['output'];
2085
2120
  Flourish: ReferenceWithCAPIData<ContentTree.Flourish>;
2086
- FlourishFallback: FlourishFallback;
2087
2121
  FlourishSource: FlourishSourceModel;
2088
2122
  FollowButtonVariant: Scalars['FollowButtonVariant']['output'];
2089
2123
  FullBleedTopper: TopperModel;
@@ -2118,6 +2152,7 @@ export type ResolversParentTypes = ResolversObject<{
2118
2152
  Mutation: {};
2119
2153
  OpinionTopper: TopperModel;
2120
2154
  PackageDesign: Scalars['PackageDesign']['output'];
2155
+ PageInfo: PageInfo;
2121
2156
  PartnerContentTopper: TopperModel;
2122
2157
  Person: PersonModel;
2123
2158
  Picture: PictureModel;
@@ -2396,6 +2431,18 @@ export type ContentResolvers<ContextType = QueryContext, ParentType extends Reso
2396
2431
  url: Resolver<ResolversTypes['String'], ParentType, ContextType, Partial<ContentUrlArgs>>;
2397
2432
  }>;
2398
2433
 
2434
+ export type ContentConnectionResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentConnection'] = ResolversParentTypes['ContentConnection']> = ResolversObject<{
2435
+ edges: Resolver<ReadonlyArray<Maybe<ResolversTypes['ContentEdge']>>, ParentType, ContextType>;
2436
+ pageInfo: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
2437
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2438
+ }>;
2439
+
2440
+ export type ContentEdgeResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentEdge'] = ResolversParentTypes['ContentEdge']> = ResolversObject<{
2441
+ cursor: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2442
+ node: Resolver<Maybe<ResolversTypes['Content']>, ParentType, ContextType>;
2443
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2444
+ }>;
2445
+
2399
2446
  export type ContentPackageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentPackage'] = ResolversParentTypes['ContentPackage']> = ResolversObject<{
2400
2447
  accessLevel: Resolver<Maybe<ResolversTypes['AccessLevel']>, ParentType, ContextType>;
2401
2448
  altStandfirst: Resolver<Maybe<ResolversTypes['AltStandfirst']>, ParentType, ContextType>;
@@ -2485,18 +2532,11 @@ export type DesignResolvers<ContextType = QueryContext, ParentType extends Resol
2485
2532
  }>;
2486
2533
 
2487
2534
  export type FlourishResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Flourish'] = ResolversParentTypes['Flourish']> = ResolversObject<{
2488
- fallbackImage: Resolver<Maybe<ResolversTypes['FlourishFallback']>, ParentType, ContextType>;
2535
+ fallbackImage: Resolver<Maybe<ResolversTypes['FlourishSource']>, ParentType, ContextType>;
2489
2536
  type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2490
2537
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2491
2538
  }>;
2492
2539
 
2493
- export type FlourishFallbackResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['FlourishFallback'] = ResolversParentTypes['FlourishFallback']> = ResolversObject<{
2494
- height: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2495
- url: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2496
- width: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2497
- __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2498
- }>;
2499
-
2500
2540
  export type FlourishSourceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['FlourishSource'] = ResolversParentTypes['FlourishSource']> = ResolversObject<{
2501
2541
  format: Resolver<ResolversTypes['ImageFormat'], ParentType, ContextType>;
2502
2542
  height: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
@@ -2711,7 +2751,7 @@ export type LayoutImageResolvers<ContextType = QueryContext, ParentType extends
2711
2751
 
2712
2752
  export type LeadFlourishResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LeadFlourish'] = ResolversParentTypes['LeadFlourish']> = ResolversObject<{
2713
2753
  description: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2714
- fallbackImage: Resolver<ResolversTypes['FlourishSource'], ParentType, ContextType>;
2754
+ fallbackImage: Resolver<Maybe<ResolversTypes['FlourishSource']>, ParentType, ContextType>;
2715
2755
  id: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2716
2756
  type: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2717
2757
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -2749,7 +2789,8 @@ export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType exte
2749
2789
  firstPublishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2750
2790
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2751
2791
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2752
- liveBlogPosts: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Content']>>>, ParentType, ContextType>;
2792
+ liveBlogPosts: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Content']>>>, ParentType, ContextType, Partial<LiveBlogPackageLiveBlogPostsArgs>>;
2793
+ liveBlogPostsConnection: Resolver<Maybe<ResolversTypes['ContentConnection']>, ParentType, ContextType, Partial<LiveBlogPackageLiveBlogPostsConnectionArgs>>;
2753
2794
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2754
2795
  modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2755
2796
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2843,6 +2884,14 @@ export interface PackageDesignScalarConfig extends GraphQLScalarTypeConfig<Resol
2843
2884
  name: 'PackageDesign';
2844
2885
  }
2845
2886
 
2887
+ export type PageInfoResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['PageInfo'] = ResolversParentTypes['PageInfo']> = ResolversObject<{
2888
+ endCursor: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2889
+ hasNextPage: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2890
+ hasPreviousPage: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2891
+ startCursor: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2892
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2893
+ }>;
2894
+
2846
2895
  export type PartnerContentTopperResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['PartnerContentTopper'] = ResolversParentTypes['PartnerContentTopper']> = ResolversObject<{
2847
2896
  backgroundBox: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2848
2897
  backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
@@ -3183,6 +3232,8 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3183
3232
  Concept: ConceptResolvers<ContextType>;
3184
3233
  ConceptInterface: ConceptInterfaceResolvers<ContextType>;
3185
3234
  Content: ContentResolvers<ContextType>;
3235
+ ContentConnection: ContentConnectionResolvers<ContextType>;
3236
+ ContentEdge: ContentEdgeResolvers<ContextType>;
3186
3237
  ContentPackage: ContentPackageResolvers<ContextType>;
3187
3238
  ContentType: GraphQLScalarType;
3188
3239
  CustomCodeComponent: CustomCodeComponentResolvers<ContextType>;
@@ -3190,7 +3241,6 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3190
3241
  DeepPortraitTopper: DeepPortraitTopperResolvers<ContextType>;
3191
3242
  Design: DesignResolvers<ContextType>;
3192
3243
  Flourish: FlourishResolvers<ContextType>;
3193
- FlourishFallback: FlourishFallbackResolvers<ContextType>;
3194
3244
  FlourishSource: FlourishSourceResolvers<ContextType>;
3195
3245
  FollowButtonVariant: GraphQLScalarType;
3196
3246
  FullBleedTopper: FullBleedTopperResolvers<ContextType>;
@@ -3223,6 +3273,7 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3223
3273
  Mutation: MutationResolvers<ContextType>;
3224
3274
  OpinionTopper: OpinionTopperResolvers<ContextType>;
3225
3275
  PackageDesign: GraphQLScalarType;
3276
+ PageInfo: PageInfoResolvers<ContextType>;
3226
3277
  PartnerContentTopper: PartnerContentTopperResolvers<ContextType>;
3227
3278
  Person: PersonResolvers<ContextType>;
3228
3279
  Picture: PictureResolvers<ContextType>;
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export interface QueryContext {
26
26
  systemCode?: string
27
27
  requestId?: string
28
28
  contentRequestedOnce?: boolean
29
+ contentTimedOut?: boolean
29
30
  logger: Logger
30
31
  versions: {
31
32
  api: string
@@ -2,7 +2,6 @@ import { CapiResponse } from './CapiResponse'
2
2
  import { baseCapiObject } from '../fixtures/capiObject'
3
3
  import cloneDeep from 'clone-deep'
4
4
  import context from '../fixtures/dummyContext'
5
- import TimeoutError from '../helpers/timeout-error'
6
5
 
7
6
  describe('CAPI response', () => {
8
7
  describe('Content ID', () => {
@@ -70,7 +69,7 @@ describe('CAPI response', () => {
70
69
  })
71
70
 
72
71
  describe('liveBlogPosts', () => {
73
- test('returns a resolved array of the articles contained sorted by most recently published', async () => {
72
+ test('returns a resolved array of the articles contained', async () => {
74
73
  const liveBlogPackage = cloneDeep({
75
74
  ...baseCapiObject,
76
75
  type: 'http://www.ft.com/ontology/content/LiveBlogPackage',
@@ -81,14 +80,14 @@ describe('CAPI response', () => {
81
80
  'http://api.ft.com/content/000000000-0000-0000-0000-000000000000',
82
81
  },
83
82
  {
84
- id: 'http://api.ft.com/things/000000000-0000-0000-0000-000000000002',
83
+ id: 'http://api.ft.com/things/000000000-0000-0000-0000-000000000001',
85
84
  apiUrl:
86
- 'http://api.ft.com/content/000000000-0000-0000-0000-000000000002',
85
+ 'http://api.ft.com/content/000000000-0000-0000-0000-000000000001',
87
86
  },
88
87
  {
89
- id: 'http://api.ft.com/things/000000000-0000-0000-0000-000000000001',
88
+ id: 'http://api.ft.com/things/000000000-0000-0000-0000-000000000002',
90
89
  apiUrl:
91
- 'http://api.ft.com/content/000000000-0000-0000-0000-000000000001',
90
+ 'http://api.ft.com/content/000000000-0000-0000-0000-000000000002',
92
91
  },
93
92
  ],
94
93
  })
@@ -107,6 +106,137 @@ describe('CAPI response', () => {
107
106
  })
108
107
  })
109
108
 
109
+ describe('liveBlogPostsConnection', () => {
110
+ const generateId = (i: number) =>
111
+ '00000000-0000-0000-0000-0000000000' + String(i).padStart(2, '0')
112
+
113
+ const postCount = 100
114
+ const liveBlogPackage = cloneDeep({
115
+ ...baseCapiObject,
116
+ type: 'http://www.ft.com/ontology/content/LiveBlogPackage',
117
+ contains: [...Array(postCount).keys()].map((i) => {
118
+ const id = generateId(postCount - 1 - i)
119
+ return {
120
+ id: 'http://api.ft.com/things/' + id,
121
+ apiUrl: 'http://api.ft.com/content/' + id,
122
+ }
123
+ }),
124
+ })
125
+ const capiResponse = new CapiResponse(liveBlogPackage, context)
126
+
127
+ test('returns all posts when no argument specified', async () => {
128
+ const liveBlogPostsConnection =
129
+ await capiResponse.liveBlogPostsConnection({})
130
+ expect(liveBlogPostsConnection.edges).toHaveLength(postCount)
131
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
132
+ expect(edge.node.id()).toEqual(generateId(i))
133
+ )
134
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
135
+ expect.objectContaining({
136
+ hasPreviousPage: false,
137
+ hasNextPage: false,
138
+ })
139
+ )
140
+ })
141
+
142
+ test('returns first x edges', async () => {
143
+ const firstCount = 20
144
+ const liveBlogPostsConnection =
145
+ await capiResponse.liveBlogPostsConnection({ first: firstCount })
146
+ expect(liveBlogPostsConnection.edges).toHaveLength(firstCount)
147
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
148
+ expect(edge.node.id()).toEqual(generateId(i))
149
+ )
150
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
151
+ expect.objectContaining({
152
+ hasPreviousPage: false,
153
+ hasNextPage: true,
154
+ })
155
+ )
156
+ })
157
+
158
+ test('returns last x edges', async () => {
159
+ const lastCount = 20
160
+ const liveBlogPostsConnection =
161
+ await capiResponse.liveBlogPostsConnection({ last: lastCount })
162
+ expect(liveBlogPostsConnection.edges).toHaveLength(lastCount)
163
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
164
+ expect(edge.node.id()).toEqual(generateId(i + (postCount - lastCount)))
165
+ )
166
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
167
+ expect.objectContaining({
168
+ hasPreviousPage: true,
169
+ hasNextPage: false,
170
+ })
171
+ )
172
+ })
173
+
174
+ test('returns edges after cursor', async () => {
175
+ const firstCount = 20
176
+ const startingLiveBlogPostsConnection =
177
+ await capiResponse.liveBlogPostsConnection({ first: firstCount })
178
+ const cursor = startingLiveBlogPostsConnection.pageInfo.endCursor
179
+ expect(cursor).toBeDefined()
180
+
181
+ const liveBlogPostsConnection =
182
+ await capiResponse.liveBlogPostsConnection({
183
+ first: firstCount,
184
+ after: cursor,
185
+ })
186
+ expect(liveBlogPostsConnection.edges).toHaveLength(firstCount)
187
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
188
+ expect(edge.node.id()).toEqual(generateId(i + firstCount))
189
+ )
190
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
191
+ expect.objectContaining({
192
+ hasPreviousPage: true,
193
+ hasNextPage: true,
194
+ })
195
+ )
196
+ })
197
+
198
+ test('returns edges before cursor', async () => {
199
+ const lastCount = 20
200
+ const endingLiveBlogPostsConnection =
201
+ await capiResponse.liveBlogPostsConnection({ last: lastCount })
202
+ const cursor = endingLiveBlogPostsConnection.pageInfo.startCursor
203
+ expect(cursor).toBeDefined()
204
+
205
+ const liveBlogPostsConnection =
206
+ await capiResponse.liveBlogPostsConnection({
207
+ last: lastCount,
208
+ before: cursor,
209
+ })
210
+ expect(liveBlogPostsConnection.edges).toHaveLength(lastCount)
211
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
212
+ expect(edge.node.id()).toEqual(
213
+ generateId(i + (postCount - lastCount * 2))
214
+ )
215
+ )
216
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
217
+ expect.objectContaining({
218
+ hasPreviousPage: true,
219
+ hasNextPage: true,
220
+ })
221
+ )
222
+ })
223
+
224
+ test('returns all edges when first is greater than total posts', async () => {
225
+ const liveBlogPostsConnection =
226
+ await capiResponse.liveBlogPostsConnection({ first: postCount + 1 })
227
+ expect(liveBlogPostsConnection.edges).toHaveLength(postCount)
228
+ liveBlogPostsConnection.edges.forEach((edge, i) =>
229
+ expect(edge.node.id()).toEqual(generateId(i))
230
+ )
231
+ expect(liveBlogPostsConnection.pageInfo).toEqual(
232
+ expect.objectContaining({
233
+ hasPreviousPage: false,
234
+ hasNextPage: false,
235
+ })
236
+ )
237
+ })
238
+ })
239
+
110
240
  describe('Partner content type articles', () => {
111
241
  test('should have `isPartnerContent` set correctly as `true` when correct publication id is set', () => {
112
242
  const article = cloneDeep({
@@ -137,7 +267,7 @@ describe('CAPI response', () => {
137
267
 
138
268
  const timingOutContext = cloneDeep(context)
139
269
  timingOutContext.dataSources.capi.getPerson = () =>
140
- Promise.reject(new TimeoutError(0))
270
+ Promise.reject(new DOMException(undefined, 'TimeoutError'))
141
271
  const capiResponse = new CapiResponse(article, timingOutContext)
142
272
 
143
273
  const authors = await capiResponse.authors()
@@ -4,6 +4,7 @@ import type {
4
4
  MainImage,
5
5
  ClipSet,
6
6
  CustomCodeComponentReference,
7
+ LiveBlogPackage,
7
8
  } from './schemas/capi/internal-content'
8
9
  import conceptIds from '@financial-times/n-concept-ids'
9
10
  import metadata from '@financial-times/n-display-metadata'
@@ -34,6 +35,7 @@ import {
34
35
  ContentAnnotationsArgs,
35
36
  ContentPackageContainsArgs,
36
37
  ContentUrlArgs,
38
+ LiveBlogPackageLiveBlogPostsArgs,
37
39
  Media,
38
40
  TableOfContents,
39
41
  } from '../generated'
@@ -42,6 +44,7 @@ import { z } from 'zod'
42
44
  import { Byline } from './Byline'
43
45
  import { RichText } from './RichText'
44
46
  import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
47
+ import { Connection, ConnectionArguments } from '../types/connection'
45
48
 
46
49
  type Design = {
47
50
  theme: LiteralUnionScalarValues<typeof PackageDesign>
@@ -708,21 +711,54 @@ export class CapiResponse {
708
711
  surroundingArticles,
709
712
  fromId,
710
713
  }: Partial<ContentPackageContainsArgs> = {}): Promise<CapiResponse[] | null> {
714
+ let edges
715
+ if (fromId && surroundingArticles) {
716
+ const afterId = await this.containsConnection({
717
+ after: fromId,
718
+ first: surroundingArticles,
719
+ })
720
+ const beforeAndIncludingId = await this.containsConnection({
721
+ before: afterId.pageInfo.startCursor,
722
+ last: surroundingArticles + 1,
723
+ })
724
+ edges = afterId.edges.concat(beforeAndIncludingId.edges)
725
+ } else {
726
+ const connection = await this.containsConnection()
727
+ edges = connection.edges
728
+ }
729
+ return edges.map((edge) => edge.node)
730
+ }
731
+
732
+ async containsConnection(
733
+ { before, after, first, last }: ConnectionArguments = {},
734
+ transformer?: (
735
+ contains: LiveBlogPackage['contains']
736
+ ) => LiveBlogPackage['contains']
737
+ ): Promise<Connection<CapiResponse>> {
711
738
  if ('contains' in this.capiData) {
712
- const fromIndex = this.capiData.contains.findIndex(
713
- (contains) => contains.id === `http://api.ft.com/things/${fromId}`
739
+ const { contains: containsUntransformed } = this.capiData
740
+ const contains = transformer
741
+ ? transformer(containsUntransformed)
742
+ : containsUntransformed
743
+
744
+ const beforeIndex = before
745
+ ? contains.findLastIndex(
746
+ (post) => post.id === `http://api.ft.com/things/${before}`
747
+ )
748
+ : -1
749
+ const afterIndex = after
750
+ ? contains.findIndex(
751
+ (post) => post.id === `http://api.ft.com/things/${after}`
752
+ )
753
+ : -1
754
+ const cursoredContains = contains.slice(
755
+ afterIndex >= 0 ? afterIndex + 1 : undefined,
756
+ beforeIndex >= 0 ? beforeIndex : undefined
714
757
  )
715
-
716
- const contains =
717
- fromId && surroundingArticles
718
- ? this.capiData.contains.slice(
719
- Math.max(0, fromIndex - surroundingArticles),
720
- fromIndex + surroundingArticles
721
- )
722
- : this.capiData.contains
758
+ const slicedContains = cursoredContains.slice(-(last ?? 0), first)
723
759
 
724
760
  const results = await Promise.allSettled(
725
- contains.map(({ id }) =>
761
+ slicedContains.map(({ id }) =>
726
762
  this.context.dataSources.capi.getContent(uuidFromUrl(id), this)
727
763
  )
728
764
  )
@@ -741,17 +777,49 @@ export class CapiResponse {
741
777
  cause: new AggregateError(failed.map((result) => result.reason)),
742
778
  }),
743
779
  })
780
+
781
+ if (
782
+ failed.some(
783
+ (result) =>
784
+ (result.reason as OperationalError).code === 'FETCH_TIMEOUT_ERROR'
785
+ )
786
+ ) {
787
+ this.context.contentTimedOut = true
788
+ }
744
789
  }
745
790
 
746
- return results
791
+ const edges = results
747
792
  .filter(
748
793
  (result): result is PromiseFulfilledResult<CapiResponse> =>
749
794
  result.status === 'fulfilled'
750
795
  )
751
- .map((result) => result.value)
796
+ .map((result) => {
797
+ const node = result.value
798
+ return { node, cursor: node.id() }
799
+ })
800
+
801
+ return {
802
+ edges,
803
+ pageInfo: {
804
+ hasPreviousPage: last
805
+ ? cursoredContains.length > last
806
+ : afterIndex > 0,
807
+ hasNextPage: first
808
+ ? cursoredContains.length > first
809
+ : beforeIndex > 0,
810
+ startCursor: edges[0]?.cursor,
811
+ endCursor: edges[edges.length - 1]?.cursor,
812
+ },
813
+ }
752
814
  }
753
815
 
754
- return null
816
+ return {
817
+ edges: [],
818
+ pageInfo: {
819
+ hasPreviousPage: false,
820
+ hasNextPage: false,
821
+ },
822
+ }
755
823
  }
756
824
 
757
825
  containsLength() {
@@ -768,25 +836,37 @@ export class CapiResponse {
768
836
  : null
769
837
  }
770
838
 
771
- async liveBlogPosts(): Promise<CapiResponse[] | []> {
772
- const contains = await this.contains()
839
+ private handleLiveBlogPosts(liveBlogPosts: LiveBlogPackage['contains']) {
840
+ this.context.addSurrogateKeys(
841
+ liveBlogPosts.map((article) => ({
842
+ prefix: 'contentPipelineArticle',
843
+ id: article.id,
844
+ }))
845
+ )
773
846
 
774
- if (contains && contains.length) {
775
- this.context.addSurrogateKeys(
776
- contains.map((article) => ({
777
- prefix: 'contentPipelineArticle',
778
- id: article.id(),
779
- }))
780
- )
847
+ return [...liveBlogPosts].reverse()
848
+ }
781
849
 
782
- const liveBlogPosts = contains.sort(
783
- (a, b) => b.publishedTimestamp() - a.publishedTimestamp()
784
- )
850
+ async liveBlogPosts(
851
+ args?: LiveBlogPackageLiveBlogPostsArgs
852
+ ): Promise<CapiResponse[] | []> {
853
+ const containsConnections = await this.containsConnection(
854
+ {
855
+ first: args?.count ?? undefined,
856
+ },
857
+ this.handleLiveBlogPosts.bind(this)
858
+ )
859
+ const contains = containsConnections.edges.map((edge) => edge.node)
785
860
 
786
- return liveBlogPosts
787
- }
861
+ return contains.sort(
862
+ (a, b) => b.publishedTimestamp() - a.publishedTimestamp()
863
+ )
864
+ }
788
865
 
789
- return []
866
+ async liveBlogPostsConnection(
867
+ args: ConnectionArguments
868
+ ): Promise<Connection<CapiResponse>> {
869
+ return this.containsConnection(args, this.handleLiveBlogPosts.bind(this))
790
870
  }
791
871
 
792
872
  isPinned() {
@@ -803,16 +883,28 @@ export class CapiResponse {
803
883
 
804
884
  async pinnedPost(): Promise<CapiResponse | null> {
805
885
  if ('pinnedPosts' in this.capiData) {
806
- const pinnedPosts = this.capiData.pinnedPosts || []
807
- const liveBlogPosts = await this.liveBlogPosts()
808
-
886
+ const pinnedPosts = this.capiData.pinnedPosts
809
887
  const pinnedPostId = pinnedPosts[0]
810
- const pinnedPostIndex = liveBlogPosts.findIndex(
811
- (post) => post.id() === pinnedPostId
812
- )
888
+ if (!pinnedPostId) {
889
+ return null
890
+ }
813
891
 
814
- if (pinnedPostIndex !== -1) {
815
- return liveBlogPosts.splice(pinnedPostIndex, 1)[0] ?? null
892
+ try {
893
+ return await this.context.dataSources.capi.getContent(
894
+ uuidFromUrl(pinnedPostId),
895
+ this
896
+ )
897
+ } catch (error) {
898
+ if (isError(error)) {
899
+ this.context.logger.warn({
900
+ event: 'RECOVERABLE_ERROR',
901
+ error: new OperationalError({
902
+ code: 'PACKAGE_PINNED_POST_ERROR',
903
+ message: `Failed to fetch the pinned post ${pinnedPostId} contained in the package ${this.id()}`,
904
+ cause: error,
905
+ }),
906
+ })
907
+ }
816
908
  }
817
909
  }
818
910
  return null