@financial-times/cp-content-pipeline-schema 3.4.0 → 3.5.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 (76) hide show
  1. package/CHANGELOG.md +23 -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 +69 -1
  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.js +5 -10
  33. package/lib/model/FlourishSource.js.map +1 -1
  34. package/lib/model/Image.js +10 -20
  35. package/lib/model/Image.js.map +1 -1
  36. package/lib/model/LeadFlourish.js +4 -10
  37. package/lib/model/LeadFlourish.js.map +1 -1
  38. package/lib/model/Person.js +5 -16
  39. package/lib/model/Person.js.map +1 -1
  40. package/lib/model/Picture.js +3 -0
  41. package/lib/model/Picture.js.map +1 -1
  42. package/lib/model/RichText.js +3 -0
  43. package/lib/model/RichText.js.map +1 -1
  44. package/lib/model/Topper.js +5 -16
  45. package/lib/model/Topper.js.map +1 -1
  46. package/lib/model/schemas/capi/internal-content.d.ts +1 -1
  47. package/lib/resolvers/content-tree/references/RawImage.js +2 -0
  48. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  49. package/lib/resolvers/content.d.ts +26 -1
  50. package/lib/resolvers/content.js +21 -1
  51. package/lib/resolvers/content.js.map +1 -1
  52. package/lib/resolvers/index.d.ts +26 -1
  53. package/lib/resolvers/literal-union.js +1 -0
  54. package/lib/resolvers/literal-union.js.map +1 -1
  55. package/lib/types/connection.d.ts +21 -0
  56. package/lib/types/connection.js +5 -0
  57. package/lib/types/connection.js.map +1 -0
  58. package/package.json +1 -1
  59. package/queries/article.graphql +8 -2
  60. package/src/datasources/capi.ts +1 -14
  61. package/src/datasources/origami-image.ts +1 -13
  62. package/src/datasources/twitter.ts +1 -14
  63. package/src/fixtures/dummyContext.ts +1 -1
  64. package/src/generated/index.ts +71 -1
  65. package/src/index.ts +1 -0
  66. package/src/model/CapiResponse.test.ts +137 -7
  67. package/src/model/CapiResponse.ts +129 -37
  68. package/src/model/schemas/capi/internal-content.ts +1 -1
  69. package/src/resolvers/content.ts +31 -1
  70. package/src/types/connection.ts +28 -0
  71. package/tsconfig.tsbuildinfo +1 -1
  72. package/typedefs/content.graphql +40 -2
  73. package/lib/helpers/timeout-error.d.ts +0 -6
  74. package/lib/helpers/timeout-error.js +0 -15
  75. package/lib/helpers/timeout-error.js.map +0 -1
  76. package/src/helpers/timeout-error.ts +0 -13
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // Types to aid the implementation of a Connection type, as specified in
3
+ // https://relay.dev/graphql/connections.htm
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/types/connection.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,4CAA4C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/cp-content-pipeline-schema",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -665,7 +665,7 @@ fragment ArticleFields on Content {
665
665
  }
666
666
  }
667
667
  ... on LiveBlogPackage {
668
- liveBlogPosts {
668
+ liveBlogPosts(count: $liveBlogPostCount) {
669
669
  ...Content
670
670
  url
671
671
  ... on LiveBlogPost {
@@ -678,6 +678,11 @@ fragment ArticleFields on Content {
678
678
  }
679
679
  }
680
680
  }
681
+ liveBlogPostsConnection(first: $liveBlogPostCount) {
682
+ pageInfo {
683
+ hasNextPage
684
+ }
685
+ }
681
686
  pinnedPost {
682
687
  ...PinnedPost
683
688
  }
@@ -696,7 +701,7 @@ fragment ArticleFields on Content {
696
701
  }
697
702
  }
698
703
 
699
- query Article($uuid: String!, $useVanities: Boolean!) {
704
+ query Article($uuid: String!, $useVanities: Boolean!, $liveBlogPostCount: Int) {
700
705
  content(uuid: $uuid) {
701
706
  ...ArticleFields
702
707
  }
@@ -705,6 +710,7 @@ query Article($uuid: String!, $useVanities: Boolean!) {
705
710
  query ArticleFromJSON(
706
711
  $content: JSON!
707
712
  $useVanities: Boolean!
713
+ $liveBlogPostCount: Int
708
714
  $uuid: String = null
709
715
  ) {
710
716
  contentFromJSON(content: $content) {
@@ -6,7 +6,6 @@ import {
6
6
  PrefixingKeyValueCache,
7
7
  } from '@apollo/utils.keyvaluecache'
8
8
  import { CapiList } from '../model/CapiList'
9
- import TimeoutError from '../helpers/timeout-error'
10
9
  import { Person } from '../model/Person'
11
10
 
12
11
  const REQUEST_TIMEOUT = process.env.CAPI_DATASOURCE_REQUEST_TIMEOUT
@@ -38,20 +37,13 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
38
37
  return 'up-ica'
39
38
  }
40
39
 
41
- abortController = new AbortController()
42
- timeout: ReturnType<typeof setTimeout> | undefined = undefined
43
-
44
40
  nextNotificationLink?: string
45
41
 
46
42
  override willSendRequest(path: string, request: AugmentedRequest) {
47
43
  super.willSendRequest(path, request)
48
44
 
45
+ request.signal = AbortSignal.timeout(REQUEST_TIMEOUT)
49
46
  request.headers['x-api-key'] = this.capiKey
50
- request.signal = this.abortController.signal
51
- this.timeout = setTimeout(
52
- () => this.abortController.abort(new TimeoutError(REQUEST_TIMEOUT)),
53
- REQUEST_TIMEOUT
54
- )
55
47
  }
56
48
 
57
49
  async getContent(
@@ -71,11 +63,6 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
71
63
  this.context.contentRequestedOnce = true
72
64
  this.calls.push(uuid)
73
65
 
74
- if (this.timeout) {
75
- clearTimeout(this.timeout)
76
- this.timeout = undefined
77
- }
78
-
79
66
  return CapiResponse.fromJSON(content, this.context, packageContainer)
80
67
  }
81
68
 
@@ -1,4 +1,3 @@
1
- import TimeoutError from '../helpers/timeout-error'
2
1
  import { InstrumentedRESTDataSource } from './instrumented'
3
2
  import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
4
3
 
@@ -12,9 +11,6 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
12
11
  return 'origami-image-service-v2'
13
12
  }
14
13
 
15
- abortController = new AbortController()
16
- timeout: ReturnType<typeof setTimeout> | undefined = undefined
17
-
18
14
  imageMetadataCacheTTL = process.env.IMAGE_METADATA_CACHE_TTL
19
15
  ? parseInt(process.env.IMAGE_METADATA_CACHE_TTL)
20
16
  : 60 * 60 * 24 * 7 // 1 week
@@ -22,11 +18,7 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
22
18
  override willSendRequest(path: string, request: AugmentedRequest) {
23
19
  super.willSendRequest(path, request)
24
20
 
25
- request.signal = this.abortController.signal
26
- this.timeout = setTimeout(
27
- () => this.abortController.abort(new TimeoutError(REQUEST_TIMEOUT)),
28
- REQUEST_TIMEOUT
29
- )
21
+ request.signal = AbortSignal.timeout(REQUEST_TIMEOUT)
30
22
  }
31
23
 
32
24
  protected cacheOptionsFor(): CacheOptions {
@@ -42,10 +34,6 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
42
34
 
43
35
  this.calls.push(url)
44
36
 
45
- if (this.timeout) {
46
- clearTimeout(this.timeout)
47
- this.timeout = undefined
48
- }
49
37
  return imageMetadata
50
38
  }
51
39
  }
@@ -2,7 +2,6 @@ import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
2
2
  import * as z from 'zod'
3
3
 
4
4
  import { InstrumentedRESTDataSource } from './instrumented'
5
- import TimeoutError from '../helpers/timeout-error'
6
5
 
7
6
  const REQUEST_TIMEOUT = process.env.TWITTER_DATASOURCE_REQUEST_TIMEOUT
8
7
  ? parseInt(process.env.TWITTER_DATASOURCE_REQUEST_TIMEOUT)
@@ -16,17 +15,10 @@ export class TwitterDataSource extends InstrumentedRESTDataSource {
16
15
  return 'twitter-oembed-api'
17
16
  }
18
17
 
19
- abortController = new AbortController()
20
- timeout: ReturnType<typeof setTimeout> | undefined = undefined
21
-
22
18
  override willSendRequest(path: string, request: AugmentedRequest) {
23
19
  super.willSendRequest(path, request)
24
20
 
25
- request.signal = this.abortController.signal
26
- this.timeout = setTimeout(
27
- () => this.abortController.abort(new TimeoutError(REQUEST_TIMEOUT)),
28
- REQUEST_TIMEOUT
29
- )
21
+ request.signal = AbortSignal.timeout(REQUEST_TIMEOUT)
30
22
  }
31
23
 
32
24
  async getTweet(tweetUrl: string) {
@@ -34,11 +26,6 @@ export class TwitterDataSource extends InstrumentedRESTDataSource {
34
26
 
35
27
  this.calls.push(tweetUrl)
36
28
 
37
- if (this.timeout) {
38
- clearTimeout(this.timeout)
39
- this.timeout = undefined
40
- }
41
-
42
29
  return Tweet.parse(tweet)
43
30
  }
44
31
 
@@ -23,7 +23,7 @@ export default {
23
23
  ...baseCapiObject,
24
24
  id,
25
25
  publishedDate: new Date(
26
- now + parseInt(id.slice(-1), 10)
26
+ now + parseInt(id.slice(-2), 10)
27
27
  ).toISOString(),
28
28
  }),
29
29
  { addSurrogateKeys } as QueryContext
@@ -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']>;
@@ -1120,6 +1134,8 @@ export type LiveBlogPackage = Content & {
1120
1134
  readonly instantAlertConcept?: Maybe<Concept>;
1121
1135
  /** The child articles of this live blog package. */
1122
1136
  readonly liveBlogPosts?: Maybe<ReadonlyArray<Maybe<Content>>>;
1137
+ /** The child articles of this live blog package following the [Connections](https://relay.dev/graphql/connections.htm) spec. */
1138
+ readonly liveBlogPostsConnection?: Maybe<ContentConnection>;
1123
1139
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1124
1140
  readonly mainImage?: Maybe<Image>;
1125
1141
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
@@ -1161,6 +1177,19 @@ export type LiveBlogPackageBylineArgs = {
1161
1177
  };
1162
1178
 
1163
1179
 
1180
+ export type LiveBlogPackageLiveBlogPostsArgs = {
1181
+ count?: InputMaybe<Scalars['Int']['input']>;
1182
+ };
1183
+
1184
+
1185
+ export type LiveBlogPackageLiveBlogPostsConnectionArgs = {
1186
+ after?: InputMaybe<Scalars['String']['input']>;
1187
+ before?: InputMaybe<Scalars['String']['input']>;
1188
+ first?: InputMaybe<Scalars['Int']['input']>;
1189
+ last?: InputMaybe<Scalars['Int']['input']>;
1190
+ };
1191
+
1192
+
1164
1193
  export type LiveBlogPackageUrlArgs = {
1165
1194
  relative?: InputMaybe<Scalars['Boolean']['input']>;
1166
1195
  vanity?: InputMaybe<Scalars['Boolean']['input']>;
@@ -1312,6 +1341,17 @@ export type OpinionTopperHeadshotArgs = {
1312
1341
  width?: InputMaybe<Scalars['Int']['input']>;
1313
1342
  };
1314
1343
 
1344
+ export type PageInfo = {
1345
+ /** The cursor for the last edge. */
1346
+ readonly endCursor?: Maybe<Scalars['String']['output']>;
1347
+ /** Whether a subsequent page of edges is available on request. */
1348
+ readonly hasNextPage: Scalars['Boolean']['output'];
1349
+ /** Whether a previous page of edges is available on request. */
1350
+ readonly hasPreviousPage: Scalars['Boolean']['output'];
1351
+ /** The cursor for the first edge. */
1352
+ readonly startCursor?: Maybe<Scalars['String']['output']>;
1353
+ };
1354
+
1315
1355
  export type PartnerContentTopper = Topper & TopperWithImages & TopperWithTheme & {
1316
1356
  /** Whether the topper should have a background box. */
1317
1357
  readonly backgroundBox?: Maybe<Scalars['Boolean']['output']>;
@@ -1976,6 +2016,8 @@ export type ResolversTypes = ResolversObject<{
1976
2016
  Concept: ResolverTypeWrapper<ConceptModel>;
1977
2017
  ConceptInterface: ResolverTypeWrapper<ConceptModel>;
1978
2018
  Content: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Content']>;
2019
+ ContentConnection: ResolverTypeWrapper<Omit<ContentConnection, 'edges'> & { edges: ReadonlyArray<Maybe<ResolversTypes['ContentEdge']>> }>;
2020
+ ContentEdge: ResolverTypeWrapper<Omit<ContentEdge, 'node'> & { node: Maybe<ResolversTypes['Content']> }>;
1979
2021
  ContentPackage: ResolverTypeWrapper<CapiResponse>;
1980
2022
  ContentType: ResolverTypeWrapper<Scalars['ContentType']['output']>;
1981
2023
  CustomCodeComponent: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.CustomCodeComponent>>;
@@ -2019,6 +2061,7 @@ export type ResolversTypes = ResolversObject<{
2019
2061
  Mutation: ResolverTypeWrapper<{}>;
2020
2062
  OpinionTopper: ResolverTypeWrapper<TopperModel>;
2021
2063
  PackageDesign: ResolverTypeWrapper<Scalars['PackageDesign']['output']>;
2064
+ PageInfo: ResolverTypeWrapper<PageInfo>;
2022
2065
  PartnerContentTopper: ResolverTypeWrapper<TopperModel>;
2023
2066
  Person: ResolverTypeWrapper<PersonModel>;
2024
2067
  Picture: ResolverTypeWrapper<PictureModel>;
@@ -2075,6 +2118,8 @@ export type ResolversParentTypes = ResolversObject<{
2075
2118
  Concept: ConceptModel;
2076
2119
  ConceptInterface: ConceptModel;
2077
2120
  Content: ResolversInterfaceTypes<ResolversParentTypes>['Content'];
2121
+ ContentConnection: Omit<ContentConnection, 'edges'> & { edges: ReadonlyArray<Maybe<ResolversParentTypes['ContentEdge']>> };
2122
+ ContentEdge: Omit<ContentEdge, 'node'> & { node: Maybe<ResolversParentTypes['Content']> };
2078
2123
  ContentPackage: CapiResponse;
2079
2124
  ContentType: Scalars['ContentType']['output'];
2080
2125
  CustomCodeComponent: ReferenceWithCAPIData<ContentTree.CustomCodeComponent>;
@@ -2118,6 +2163,7 @@ export type ResolversParentTypes = ResolversObject<{
2118
2163
  Mutation: {};
2119
2164
  OpinionTopper: TopperModel;
2120
2165
  PackageDesign: Scalars['PackageDesign']['output'];
2166
+ PageInfo: PageInfo;
2121
2167
  PartnerContentTopper: TopperModel;
2122
2168
  Person: PersonModel;
2123
2169
  Picture: PictureModel;
@@ -2396,6 +2442,18 @@ export type ContentResolvers<ContextType = QueryContext, ParentType extends Reso
2396
2442
  url: Resolver<ResolversTypes['String'], ParentType, ContextType, Partial<ContentUrlArgs>>;
2397
2443
  }>;
2398
2444
 
2445
+ export type ContentConnectionResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentConnection'] = ResolversParentTypes['ContentConnection']> = ResolversObject<{
2446
+ edges: Resolver<ReadonlyArray<Maybe<ResolversTypes['ContentEdge']>>, ParentType, ContextType>;
2447
+ pageInfo: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
2448
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2449
+ }>;
2450
+
2451
+ export type ContentEdgeResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentEdge'] = ResolversParentTypes['ContentEdge']> = ResolversObject<{
2452
+ cursor: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2453
+ node: Resolver<Maybe<ResolversTypes['Content']>, ParentType, ContextType>;
2454
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2455
+ }>;
2456
+
2399
2457
  export type ContentPackageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ContentPackage'] = ResolversParentTypes['ContentPackage']> = ResolversObject<{
2400
2458
  accessLevel: Resolver<Maybe<ResolversTypes['AccessLevel']>, ParentType, ContextType>;
2401
2459
  altStandfirst: Resolver<Maybe<ResolversTypes['AltStandfirst']>, ParentType, ContextType>;
@@ -2749,7 +2807,8 @@ export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType exte
2749
2807
  firstPublishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2750
2808
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2751
2809
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2752
- liveBlogPosts: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Content']>>>, ParentType, ContextType>;
2810
+ liveBlogPosts: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Content']>>>, ParentType, ContextType, Partial<LiveBlogPackageLiveBlogPostsArgs>>;
2811
+ liveBlogPostsConnection: Resolver<Maybe<ResolversTypes['ContentConnection']>, ParentType, ContextType, Partial<LiveBlogPackageLiveBlogPostsConnectionArgs>>;
2753
2812
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2754
2813
  modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2755
2814
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2843,6 +2902,14 @@ export interface PackageDesignScalarConfig extends GraphQLScalarTypeConfig<Resol
2843
2902
  name: 'PackageDesign';
2844
2903
  }
2845
2904
 
2905
+ export type PageInfoResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['PageInfo'] = ResolversParentTypes['PageInfo']> = ResolversObject<{
2906
+ endCursor: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2907
+ hasNextPage: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2908
+ hasPreviousPage: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2909
+ startCursor: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2910
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2911
+ }>;
2912
+
2846
2913
  export type PartnerContentTopperResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['PartnerContentTopper'] = ResolversParentTypes['PartnerContentTopper']> = ResolversObject<{
2847
2914
  backgroundBox: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2848
2915
  backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
@@ -3183,6 +3250,8 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3183
3250
  Concept: ConceptResolvers<ContextType>;
3184
3251
  ConceptInterface: ConceptInterfaceResolvers<ContextType>;
3185
3252
  Content: ContentResolvers<ContextType>;
3253
+ ContentConnection: ContentConnectionResolvers<ContextType>;
3254
+ ContentEdge: ContentEdgeResolvers<ContextType>;
3186
3255
  ContentPackage: ContentPackageResolvers<ContextType>;
3187
3256
  ContentType: GraphQLScalarType;
3188
3257
  CustomCodeComponent: CustomCodeComponentResolvers<ContextType>;
@@ -3223,6 +3292,7 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3223
3292
  Mutation: MutationResolvers<ContextType>;
3224
3293
  OpinionTopper: OpinionTopperResolvers<ContextType>;
3225
3294
  PackageDesign: GraphQLScalarType;
3295
+ PageInfo: PageInfoResolvers<ContextType>;
3226
3296
  PartnerContentTopper: PartnerContentTopperResolvers<ContextType>;
3227
3297
  Person: PersonResolvers<ContextType>;
3228
3298
  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()