@financial-times/cp-content-pipeline-schema 2.7.0 → 2.9.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 (95) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/datasources/capi.d.ts +1 -1
  3. package/lib/datasources/capi.js +14 -39
  4. package/lib/datasources/capi.js.map +1 -1
  5. package/lib/datasources/instrumented.d.ts +4 -1
  6. package/lib/datasources/instrumented.js +16 -16
  7. package/lib/datasources/instrumented.js.map +1 -1
  8. package/lib/datasources/origami-image.d.ts +1 -1
  9. package/lib/datasources/origami-image.js +7 -21
  10. package/lib/datasources/origami-image.js.map +1 -1
  11. package/lib/datasources/twitter.d.ts +1 -1
  12. package/lib/datasources/twitter.js +7 -21
  13. package/lib/datasources/twitter.js.map +1 -1
  14. package/lib/generated/index.d.ts +24 -0
  15. package/lib/model/CapiResponse.d.ts +2 -0
  16. package/lib/model/CapiResponse.js +40 -4
  17. package/lib/model/CapiResponse.js.map +1 -1
  18. package/lib/model/Concept.d.ts +0 -2
  19. package/lib/model/Concept.js +1 -57
  20. package/lib/model/Concept.js.map +1 -1
  21. package/lib/model/Concept.test.js +0 -40
  22. package/lib/model/Concept.test.js.map +1 -1
  23. package/lib/model/Image.js +8 -3
  24. package/lib/model/Image.js.map +1 -1
  25. package/lib/model/Person.d.ts +21 -0
  26. package/lib/model/Person.js +106 -0
  27. package/lib/model/Person.js.map +1 -0
  28. package/lib/model/Person.test.d.ts +1 -0
  29. package/lib/model/Person.test.js +96 -0
  30. package/lib/model/Person.test.js.map +1 -0
  31. package/lib/model/Topper.d.ts +2 -1
  32. package/lib/model/Topper.js +18 -16
  33. package/lib/model/Topper.js.map +1 -1
  34. package/lib/model/Topper.test.js +29 -0
  35. package/lib/model/Topper.test.js.map +1 -1
  36. package/lib/model/schemas/capi/base-schema.d.ts +3 -0
  37. package/lib/model/schemas/capi/base-schema.js +1 -0
  38. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  39. package/lib/resolvers/content-tree/bodyXMLToTree.js +1 -1
  40. package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
  41. package/lib/resolvers/content-tree/bodyXMLToTree.test.js +7 -7
  42. package/lib/resolvers/content-tree/bodyXMLToTree.test.js.map +1 -1
  43. package/lib/resolvers/content-tree/references/Flourish.js +7 -2
  44. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  45. package/lib/resolvers/content-tree/references/RawImage.js +7 -2
  46. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  47. package/lib/resolvers/content-tree/references/Recommended.js +1 -1
  48. package/lib/resolvers/content-tree/references/Recommended.js.map +1 -1
  49. package/lib/resolvers/content-tree/references/Tweet.js +7 -2
  50. package/lib/resolvers/content-tree/references/Tweet.js.map +1 -1
  51. package/lib/resolvers/content-tree/references/Video.js +15 -2
  52. package/lib/resolvers/content-tree/references/Video.js.map +1 -1
  53. package/lib/resolvers/content.d.ts +1 -0
  54. package/lib/resolvers/content.js +1 -0
  55. package/lib/resolvers/content.js.map +1 -1
  56. package/lib/resolvers/core.js +16 -1
  57. package/lib/resolvers/core.js.map +1 -1
  58. package/lib/resolvers/index.d.ts +9 -3
  59. package/lib/resolvers/index.js +2 -0
  60. package/lib/resolvers/index.js.map +1 -1
  61. package/lib/resolvers/person.d.ts +8 -0
  62. package/lib/resolvers/person.js +11 -0
  63. package/lib/resolvers/person.js.map +1 -0
  64. package/lib/resolvers/topper.d.ts +3 -3
  65. package/package.json +5 -2
  66. package/queries/article.graphql +9 -0
  67. package/src/datasources/capi.ts +16 -44
  68. package/src/datasources/instrumented.ts +29 -31
  69. package/src/datasources/origami-image.ts +11 -25
  70. package/src/datasources/twitter.ts +10 -24
  71. package/src/generated/index.ts +28 -0
  72. package/src/model/CapiResponse.ts +51 -6
  73. package/src/model/Concept.test.ts +0 -49
  74. package/src/model/Concept.ts +1 -32
  75. package/src/model/Image.ts +9 -4
  76. package/src/model/Person.test.ts +110 -0
  77. package/src/model/Person.ts +79 -0
  78. package/src/model/Topper.test.ts +37 -0
  79. package/src/model/Topper.ts +18 -23
  80. package/src/model/schemas/capi/base-schema.ts +1 -0
  81. package/src/resolvers/content-tree/bodyXMLToTree.test.ts +7 -7
  82. package/src/resolvers/content-tree/bodyXMLToTree.ts +1 -1
  83. package/src/resolvers/content-tree/references/Flourish.ts +7 -2
  84. package/src/resolvers/content-tree/references/RawImage.ts +7 -2
  85. package/src/resolvers/content-tree/references/Recommended.ts +1 -1
  86. package/src/resolvers/content-tree/references/Tweet.ts +7 -2
  87. package/src/resolvers/content-tree/references/Video.ts +18 -4
  88. package/src/resolvers/content.ts +1 -0
  89. package/src/resolvers/core.ts +18 -1
  90. package/src/resolvers/index.ts +2 -0
  91. package/src/resolvers/person.ts +12 -0
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/typedefs/content.graphql +1 -0
  94. package/typedefs/person.graphql +5 -0
  95. package/typedefs/topper.graphql +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/resolvers/core.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAmB;AACnB,gDAAuB;AAEvB,wDAAoD;AAEpD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC,CACxE,CAAA;AAEY,QAAA,OAAO,GAAG,WAAW,CAAC,OAAO,CAAA;AAE1C,MAAM,SAAS,GAAG;IAChB,KAAK,EAAE;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,eAAO;QACtB,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO;YAC5B,OAAO,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACvD,CAAC;QACD,eAAe,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO;YACrC,OAAO,2BAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAChD,CAAC;KACF;IACD,QAAQ,EAAE;QACR,oBAAoB,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;YAC/C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAC9C,qDAAqD,IAAI,CAAC,IAAI,EAAE,CACjE,CAAA;YAED,IAAI,MAAM,EAAE;gBACV,OAAO,qBAAqB,IAAI,CAAC,IAAI,EAAE,CAAA;aACxC;iBAAM,IAAI,MAAM,KAAK,KAAK,EAAE;gBAC3B,OAAO,WAAW,IAAI,CAAC,IAAI,SAAS,CAAA;aACrC;iBAAM;gBACL,OAAO,iBAAiB,CAAA;aACzB;QACH,CAAC;KACF;CAIF,CAAA;AAED,kBAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/resolvers/core.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAmB;AACnB,gDAAuB;AAEvB,wDAAoD;AACpD,2DAAqE;AAErE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC,CACxE,CAAA;AAEY,QAAA,OAAO,GAAG,WAAW,CAAC,OAAO,CAAA;AAE1C,MAAM,SAAS,GAAG;IAChB,KAAK,EAAE;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,eAAO;QACtB,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO;YAC5B,IAAI;gBACF,OAAO,MAAM,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;aAC5D;YAAC,OAAO,KAAK,EAAE;gBACd,IACE,KAAK,YAAY,kBAAS;oBAC1B,KAAK,CAAC,IAAI,CAAC,kBAAkB,KAAK,GAAG,EACrC;oBACA,MAAM,IAAI,kBAAS,CAAC;wBAClB,IAAI,EAAE,mBAAmB;wBACzB,OAAO,EAAE,WAAW,IAAI,CAAC,IAAI,2BAA2B;wBACxD,UAAU,EAAE,GAAG;wBACf,gBAAgB,EAAE,CAAC,QAAQ,CAAC;qBAC7B,CAAC,CAAA;iBACH;gBAED,MAAM,KAAK,CAAA;aACZ;QACH,CAAC;QACD,eAAe,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO;YACrC,OAAO,2BAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAChD,CAAC;KACF;IACD,QAAQ,EAAE;QACR,oBAAoB,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;YAC/C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAC9C,qDAAqD,IAAI,CAAC,IAAI,EAAE,CACjE,CAAA;YAED,IAAI,MAAM,EAAE;gBACV,OAAO,qBAAqB,IAAI,CAAC,IAAI,EAAE,CAAA;aACxC;iBAAM,IAAI,MAAM,KAAK,KAAK,EAAE;gBAC3B,OAAO,WAAW,IAAI,CAAC,IAAI,SAAS,CAAA;aACrC;iBAAM;gBACL,OAAO,iBAAiB,CAAA;aACzB;QACH,CAAC;KACF;CAIF,CAAA;AAED,kBAAe,SAAS,CAAA"}
@@ -1,4 +1,9 @@
1
1
  declare const resolvers: {
2
+ Person: {
3
+ headshot: (parent: import("../model/Person").Person) => Promise<string | null>;
4
+ prefLabel: (parent: import("../model/Person").Person) => string;
5
+ streamPage: (parent: import("../model/Person").Person) => string;
6
+ };
2
7
  Topper: {
3
8
  __resolveType: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper", import("../model/Topper").Topper, import("..").QueryContext>;
4
9
  backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
@@ -23,11 +28,11 @@ declare const resolvers: {
23
28
  genreConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
24
29
  };
25
30
  TopperWithHeadshot: {
26
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").TopperWithHeadshotHeadshotArgs>) => Promise<string | null>;
31
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").TopperWithHeadshotHeadshotArgs>) => string | Promise<string | null> | null;
27
32
  };
28
33
  PodcastTopper: {
29
34
  brandConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
30
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => Promise<string | null>;
35
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => string | Promise<string | null> | null;
31
36
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
32
37
  layout: (topper: import("../model/Topper").Topper) => string;
33
38
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
@@ -42,7 +47,7 @@ declare const resolvers: {
42
47
  textShadow: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
43
48
  };
44
49
  OpinionTopper: {
45
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>) => Promise<string | null>;
50
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>) => string | Promise<string | null> | null;
46
51
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
47
52
  layout: (topper: import("../model/Topper").Topper) => string;
48
53
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
@@ -537,6 +542,7 @@ declare const resolvers: {
537
542
  isOpinion: boolean;
538
543
  };
539
544
  isPinned: (parent: import("../model/CapiResponse").CapiResponse) => boolean;
545
+ authors: (parent: import("../model/CapiResponse").CapiResponse) => import("../model/Person").Person[] | null;
540
546
  __resolveType?: import("../generated").TypeResolveFn<"Article" | "Placeholder" | "Video" | "Audio" | "LiveBlogPackage" | "LiveBlogPost" | "ContentPackage", import("../model/CapiResponse").CapiResponse, import("..").QueryContext> | undefined;
541
547
  accessLevel: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"premium" | "subscribed" | "registered" | "free">>, import("../model/CapiResponse").CapiResponse, import("..").QueryContext, {}>;
542
548
  altStandfirst: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<import("../generated").AltStandfirst>>, import("../model/CapiResponse").CapiResponse, import("..").QueryContext, {}>;
@@ -14,6 +14,7 @@ const richText_1 = __importDefault(require("./richText"));
14
14
  const scalars_1 = __importDefault(require("./scalars"));
15
15
  const teaser_1 = __importDefault(require("./teaser"));
16
16
  const topper_1 = __importDefault(require("./topper"));
17
+ const person_1 = __importDefault(require("./person"));
17
18
  const references_1 = require("./content-tree/references");
18
19
  const resolvers = {
19
20
  ...concept_1.default,
@@ -28,6 +29,7 @@ const resolvers = {
28
29
  ...scalars_1.default,
29
30
  ...teaser_1.default,
30
31
  ...topper_1.default,
32
+ ...person_1.default,
31
33
  };
32
34
  exports.default = resolvers;
33
35
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":";;;;;AAAA,wDAA8C;AAC9C,wDAA8C;AAC9C,kDAAwC;AACxC,oDAA0C;AAC1C,kDAAwC;AACxC,4DAAiD;AACjD,wDAA8C;AAC9C,0DAAgD;AAChD,wDAA8C;AAC9C,sDAA4C;AAC5C,sDAA4C;AAC5C,0DAAmE;AAGnE,MAAM,SAAS,GAAG;IAChB,GAAG,iBAAO;IACV,GAAG,iBAAO;IACV,GAAG,cAAI;IACP,GAAG,eAAK;IACR,GAAG,cAAI;IACP,GAAG,mBAAQ;IACX,GAAG,iBAAO;IACV,GAAG,sBAAU;IACb,GAAG,kBAAQ;IACX,GAAG,iBAAO;IACV,GAAG,gBAAM;IACT,GAAG,gBAAM;CACU,CAAA;AAErB,kBAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resolvers/index.ts"],"names":[],"mappings":";;;;;AAAA,wDAA8C;AAC9C,wDAA8C;AAC9C,kDAAwC;AACxC,oDAA0C;AAC1C,kDAAwC;AACxC,4DAAiD;AACjD,wDAA8C;AAC9C,0DAAgD;AAChD,wDAA8C;AAC9C,sDAA4C;AAC5C,sDAA4C;AAC5C,sDAA4C;AAC5C,0DAAmE;AAGnE,MAAM,SAAS,GAAG;IAChB,GAAG,iBAAO;IACV,GAAG,iBAAO;IACV,GAAG,cAAI;IACP,GAAG,eAAK;IACR,GAAG,cAAI;IACP,GAAG,mBAAQ;IACX,GAAG,iBAAO;IACV,GAAG,sBAAU;IACb,GAAG,kBAAQ;IACX,GAAG,iBAAO;IACV,GAAG,gBAAM;IACT,GAAG,gBAAM;IACT,GAAG,gBAAM;CACU,CAAA;AAErB,kBAAe,SAAS,CAAA"}
@@ -0,0 +1,8 @@
1
+ declare const resolvers: {
2
+ Person: {
3
+ headshot: (parent: import("../model/Person").Person) => Promise<string | null>;
4
+ prefLabel: (parent: import("../model/Person").Person) => string;
5
+ streamPage: (parent: import("../model/Person").Person) => string;
6
+ };
7
+ };
8
+ export default resolvers;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const resolvers = {
4
+ Person: {
5
+ headshot: (parent) => parent.headshot(),
6
+ prefLabel: (parent) => parent.prefLabel(),
7
+ streamPage: (parent) => parent.streamPage(),
8
+ },
9
+ };
10
+ exports.default = resolvers;
11
+ //# sourceMappingURL=person.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"person.js","sourceRoot":"","sources":["../../src/resolvers/person.ts"],"names":[],"mappings":";;AACA,MAAM,SAAS,GAAG;IAChB,MAAM,EAAE;QACN,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;QACvC,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;QACzC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;KAC5C;CAGF,CAAA;AAED,kBAAe,SAAS,CAAA"}
@@ -23,11 +23,11 @@ declare const resolvers: {
23
23
  genreConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
24
24
  };
25
25
  TopperWithHeadshot: {
26
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").TopperWithHeadshotHeadshotArgs>) => Promise<string | null>;
26
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").TopperWithHeadshotHeadshotArgs>) => string | Promise<string | null> | null;
27
27
  };
28
28
  PodcastTopper: {
29
29
  brandConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
30
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => Promise<string | null>;
30
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => string | Promise<string | null> | null;
31
31
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
32
32
  layout: (topper: import("../model/Topper").Topper) => string;
33
33
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
@@ -42,7 +42,7 @@ declare const resolvers: {
42
42
  textShadow: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
43
43
  };
44
44
  OpinionTopper: {
45
- headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>) => Promise<string | null>;
45
+ headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>) => string | Promise<string | null> | null;
46
46
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
47
47
  layout: (topper: import("../model/Topper").Topper) => string;
48
48
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/cp-content-pipeline-schema",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@apollo/datasource-rest": "^6.2.2",
22
22
  "@apollo/utils.keyvaluecache": "^1.0.1",
23
- "@dotcom-reliability-kit/errors": "^2.0.0",
23
+ "@dotcom-reliability-kit/fetch-error-handler": "^0.2.3",
24
24
  "@dotcom-reliability-kit/log-error": "^2.0.0",
25
25
  "@dotcom-reliability-kit/serialize-request": "^2.0.0",
26
26
  "@financial-times/n-concept-ids": "^2.1.0",
@@ -50,6 +50,9 @@
50
50
  "@types/lodash.sortby": "^4.7.7",
51
51
  "type-fest": "^3.13.1"
52
52
  },
53
+ "peerDependencies": {
54
+ "@dotcom-reliability-kit/errors": "^3.1.0"
55
+ },
53
56
  "engines": {
54
57
  "node": "18.x"
55
58
  }
@@ -75,6 +75,12 @@ fragment Intro on RichText {
75
75
  }
76
76
  }
77
77
 
78
+ fragment Person on Person {
79
+ headshot(dpr: 2, width: 150)
80
+ prefLabel
81
+ streamPage
82
+ }
83
+
78
84
  fragment Topper on Topper {
79
85
  __typename
80
86
  headline
@@ -590,6 +596,9 @@ fragment ArticleFields on Content {
590
596
  ...Content
591
597
  url
592
598
  ... on LiveBlogPost {
599
+ authors {
600
+ ...Person
601
+ }
593
602
  isPinned
594
603
  indicators {
595
604
  ...Indicators
@@ -2,10 +2,6 @@ import { CapiResponse } from '../model/CapiResponse'
2
2
  import { InstrumentedRESTDataSource } from './instrumented'
3
3
  import { CapiPerson } from '../types/internal-content'
4
4
  import { AugmentedRequest } from '@apollo/datasource-rest'
5
- import {
6
- OperationalError,
7
- UpstreamServiceError,
8
- } from '@dotcom-reliability-kit/errors'
9
5
 
10
6
  const REQUEST_TIMEOUT = 5000 // 5 seconds
11
7
 
@@ -19,7 +15,7 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
19
15
  ? parseInt(process.env.PEOPLE_CACHE_TTL)
20
16
  : 600 // 10 minutes
21
17
 
22
- backendSystemCodes = ['up-ica']
18
+ backendSystemCode = 'up-ica'
23
19
 
24
20
  abortController = new AbortController()
25
21
  timeout: ReturnType<typeof setTimeout> | undefined = undefined
@@ -41,47 +37,23 @@ export class CapiDataSource extends InstrumentedRESTDataSource {
41
37
  uuid: string,
42
38
  packageContainer?: CapiResponse
43
39
  ): Promise<CapiResponse> {
44
- try {
45
- const content = await this.get(`internalcontent/${uuid}`, {
46
- cacheOptions: { ttl: this.articleCacheTTL },
47
- })
48
- this.context.contentRequestedOnce = true
49
- this.calls.push(uuid)
50
-
51
- if (this.timeout) {
52
- clearTimeout(this.timeout)
53
- this.timeout = undefined
54
- }
55
-
56
- return CapiResponse.fromJSON(content, this.context, packageContainer)
57
- } catch (error) {
58
- if (error instanceof Error && error.name === 'AbortError') {
59
- throw new UpstreamServiceError({
60
- code: 'CAPI_DATASOURCE_TIMEOUT',
61
- message: `Request to Internal Content API took longer than ${REQUEST_TIMEOUT}ms, and so has been aborted.`,
62
- statusCode: 408,
63
- relatesToSystems: this.backendSystemCodes,
64
- })
65
- }
66
-
67
- throw error
40
+ const content = await this.get(`internalcontent/${uuid}`, {
41
+ cacheOptions: { ttl: this.articleCacheTTL },
42
+ })
43
+ this.context.contentRequestedOnce = true
44
+ this.calls.push(uuid)
45
+
46
+ if (this.timeout) {
47
+ clearTimeout(this.timeout)
48
+ this.timeout = undefined
68
49
  }
69
- }
70
50
 
71
- async getPerson(uuid: string): Promise<CapiPerson> {
72
- try {
73
- return await this.get(`people/${uuid}`, {
74
- cacheOptions: { ttl: this.peopleCacheTTL },
75
- })
76
- } catch (error) {
77
- if (error instanceof Error) {
78
- throw new OperationalError({
79
- cause: error,
80
- code: 'CAPI_DATASOURCE_PERSON_ERROR',
81
- })
82
- }
51
+ return CapiResponse.fromJSON(content, this.context, packageContainer)
52
+ }
83
53
 
84
- throw error
85
- }
54
+ getPerson(uuid: string): Promise<CapiPerson> {
55
+ return this.get(`people/${uuid}`, {
56
+ cacheOptions: { ttl: this.peopleCacheTTL },
57
+ })
86
58
  }
87
59
  }
@@ -6,19 +6,22 @@ import {
6
6
  AugmentedRequest,
7
7
  } from '@apollo/datasource-rest'
8
8
  import { PrefixingKeyValueCache } from '@apollo/utils.keyvaluecache'
9
- import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
9
+ import { createFetchErrorHandler } from '@dotcom-reliability-kit/fetch-error-handler'
10
+ import type { FetchErrorHandler } from '@dotcom-reliability-kit/fetch-error-handler/lib/create-handler'
10
11
  import { QueryContext } from '..'
11
12
  import { isContextableCache } from '../types/cache'
12
13
  import { BaseDataSource, BaseDataSourceOptions } from './base'
14
+ import { BaseError } from '@dotcom-reliability-kit/errors'
13
15
 
14
16
  export class InstrumentedRESTDataSource
15
17
  extends RESTDataSource
16
18
  implements BaseDataSource
17
19
  {
18
20
  startTime?: bigint
19
- backendSystemCodes: string[] = []
21
+ backendSystemCode: string | undefined
20
22
  context: QueryContext
21
23
  calls: string[] = []
24
+ errorHandler: FetchErrorHandler
22
25
 
23
26
  constructor({ cache, context }: BaseDataSourceOptions) {
24
27
  const wrappedCache = new PrefixingKeyValueCache(
@@ -28,6 +31,11 @@ export class InstrumentedRESTDataSource
28
31
 
29
32
  super({
30
33
  cache: wrappedCache,
34
+ fetch: (url, init) => this.errorHandler(global.fetch(url, init)),
35
+ })
36
+
37
+ this.errorHandler = createFetchErrorHandler({
38
+ upstreamSystemCode: this.backendSystemCode,
31
39
  })
32
40
 
33
41
  // okay _now_ we can use `this`
@@ -59,6 +67,18 @@ export class InstrumentedRESTDataSource
59
67
  // eslint-disable-next-line @typescript-eslint/no-empty-function
60
68
  async throwIfResponseIsError() {}
61
69
 
70
+ logResponseMetrics(status: number, duration: bigint): void {
71
+ this.context.metrics?.count(
72
+ `graphql.datasource.${this.constructor.name}.response.${status}.count`,
73
+ 1
74
+ )
75
+
76
+ this.context.metrics?.count(
77
+ `graphql.datasource.${this.constructor.name}.response.${status}.time`,
78
+ Number(duration)
79
+ )
80
+ }
81
+
62
82
  async fetch<TResult>(
63
83
  path: string,
64
84
  incomingRequest?: DataSourceRequest<CacheOptions>
@@ -74,40 +94,18 @@ export class InstrumentedRESTDataSource
74
94
  const result = await super.fetch<TResult>(path, incomingRequest)
75
95
  const duration = (process.hrtime.bigint() - startTime) / BigInt(1e6)
76
96
 
77
- this.context.metrics?.count(
78
- `graphql.datasource.${this.constructor.name}.response.${result.response.status}.count`,
79
- 1
80
- )
81
-
82
- this.context.metrics?.count(
83
- `graphql.datasource.${this.constructor.name}.response.${result.response.status}.time`,
84
- Number(duration)
85
- )
86
-
87
- if (!result.response.ok) {
88
- throw new UpstreamServiceError({
89
- message: `${result.response.status}: ${result.response.statusText} from ${this.constructor.name}`,
90
- statusCode: result.response.status,
91
- relatesToSystems: this.backendSystemCodes,
92
- url: result.response.url,
93
- body: result.parsedBody,
94
- })
95
- }
97
+ this.logResponseMetrics(result.response.status, duration)
96
98
 
97
99
  return result
98
100
  } catch (error) {
99
- if (error instanceof Error && error.name === 'AbortError') {
101
+ if (error instanceof BaseError) {
102
+ const status =
103
+ error.code === 'FETCH_ABORT_ERROR'
104
+ ? 408
105
+ : Number(error.data.upstreamStatusCode ?? 0)
100
106
  const duration = (process.hrtime.bigint() - startTime) / BigInt(1e6)
101
107
 
102
- this.context.metrics?.count(
103
- `graphql.datasource.${this.constructor.name}.response.408.count`,
104
- 1
105
- )
106
-
107
- this.context.metrics?.count(
108
- `graphql.datasource.${this.constructor.name}.response.408.time`,
109
- Number(duration)
110
- )
108
+ this.logResponseMetrics(status, duration)
111
109
  }
112
110
 
113
111
  throw error
@@ -1,4 +1,3 @@
1
- import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
2
1
  import { InstrumentedRESTDataSource } from './instrumented'
3
2
  import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
4
3
 
@@ -6,7 +5,7 @@ const REQUEST_TIMEOUT = 5000 // 5 seconds
6
5
 
7
6
  export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
8
7
  baseURL = 'https://www.ft.com/__origami/service/image/v2/'
9
- backendSystemCodes = ['origami-image-service-v2']
8
+ backendSystemCode = 'origami-image-service-v2'
10
9
 
11
10
  abortController = new AbortController()
12
11
  timeout: ReturnType<typeof setTimeout> | undefined = undefined
@@ -32,29 +31,16 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
32
31
  async getImageMetadata(
33
32
  url: string
34
33
  ): Promise<{ width: number; height: number }> {
35
- try {
36
- const imageMetadata = await this.get(
37
- `images/metadata/${encodeURIComponent(url)}?source=next`
38
- )
39
-
40
- this.calls.push(url)
41
-
42
- if (this.timeout) {
43
- clearTimeout(this.timeout)
44
- this.timeout = undefined
45
- }
46
- return imageMetadata
47
- } catch (error) {
48
- if (error instanceof Error && error.name === 'AbortError') {
49
- throw new UpstreamServiceError({
50
- code: 'ORIGAMI_DATASOURCE_TIMEOUT',
51
- message: `Request to Image Service took longer than ${REQUEST_TIMEOUT}ms, and so has been aborted.`,
52
- statusCode: 408,
53
- relatesToSystems: this.backendSystemCodes,
54
- })
55
- }
56
-
57
- throw error
34
+ const imageMetadata = await this.get(
35
+ `images/metadata/${encodeURIComponent(url)}?source=next`
36
+ )
37
+
38
+ this.calls.push(url)
39
+
40
+ if (this.timeout) {
41
+ clearTimeout(this.timeout)
42
+ this.timeout = undefined
58
43
  }
44
+ return imageMetadata
59
45
  }
60
46
  }
@@ -1,11 +1,10 @@
1
1
  import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
2
- import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
3
2
  import { InstrumentedRESTDataSource } from './instrumented'
4
3
 
5
4
  const REQUEST_TIMEOUT = 5000 // 5 seconds
6
5
  export class TwitterDataSource extends InstrumentedRESTDataSource {
7
6
  baseURL = 'https://publish.twitter.com'
8
- backendSystemCodes = ['twitter-oembed-api']
7
+ backendSystemCode = 'twitter-oembed-api'
9
8
 
10
9
  abortController = new AbortController()
11
10
  timeout: ReturnType<typeof setTimeout> | undefined = undefined
@@ -21,29 +20,16 @@ export class TwitterDataSource extends InstrumentedRESTDataSource {
21
20
  }
22
21
 
23
22
  async getTweet(tweetUrl: string) {
24
- try {
25
- const tweet = await this.get(`oembed?url=${tweetUrl}&omit_script=true`)
26
-
27
- this.calls.push(tweetUrl)
28
-
29
- if (this.timeout) {
30
- clearTimeout(this.timeout)
31
- this.timeout = undefined
32
- }
33
-
34
- return tweet
35
- } catch (error) {
36
- if (error instanceof Error && error.name === 'AbortError') {
37
- throw new UpstreamServiceError({
38
- code: 'TWITTER_DATASOURCE_TIMEOUT',
39
- message: `Request to Twitter API took longer than ${REQUEST_TIMEOUT}ms, and so has been aborted.`,
40
- statusCode: 408,
41
- relatesToSystems: this.backendSystemCodes,
42
- })
43
- }
44
-
45
- throw error
23
+ const tweet = await this.get(`oembed?url=${tweetUrl}&omit_script=true`)
24
+
25
+ this.calls.push(tweetUrl)
26
+
27
+ if (this.timeout) {
28
+ clearTimeout(this.timeout)
29
+ this.timeout = undefined
46
30
  }
31
+
32
+ return tweet
47
33
  }
48
34
 
49
35
  cacheOptionsFor(): CacheOptions {
@@ -1,5 +1,6 @@
1
1
  import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
2
2
  import type { Concept as ConceptModel } from '../model/Concept';
3
+ import type { Person as PersonModel } from '../model/Person';
3
4
  import type { CapiResponse } from '../model/CapiResponse';
4
5
  import type { Image as ImageModel } from '../model/Image';
5
6
  import type { Clip as ClipModel } from '../model/Clip';
@@ -990,6 +991,7 @@ export type LiveBlogPost = Content & {
990
991
  readonly altTitle?: Maybe<AltTitle>;
991
992
  /** An array of concepts related to the article, eg. organisations or topics. */
992
993
  readonly annotations?: Maybe<ReadonlyArray<Maybe<Concept>>>;
994
+ readonly authors?: Maybe<ReadonlyArray<Maybe<Person>>>;
993
995
  /** An abstract syntax tree of the article content. */
994
996
  readonly body?: Maybe<RichText>;
995
997
  /** The raw string of the XML as returned from ContentAPI. */
@@ -1108,6 +1110,19 @@ export type OpinionTopper = Topper & TopperWithHeadshot & TopperWithTheme & {
1108
1110
 
1109
1111
 
1110
1112
  export type OpinionTopperHeadshotArgs = {
1113
+ dpr?: InputMaybe<Scalars['Int']['input']>;
1114
+ url?: InputMaybe<Scalars['String']['input']>;
1115
+ width?: InputMaybe<Scalars['Int']['input']>;
1116
+ };
1117
+
1118
+ export type Person = {
1119
+ readonly headshot?: Maybe<Scalars['String']['output']>;
1120
+ readonly prefLabel?: Maybe<Scalars['String']['output']>;
1121
+ readonly streamPage?: Maybe<Scalars['String']['output']>;
1122
+ };
1123
+
1124
+
1125
+ export type PersonHeadshotArgs = {
1111
1126
  dpr?: InputMaybe<Scalars['Int']['input']>;
1112
1127
  width?: InputMaybe<Scalars['Int']['input']>;
1113
1128
  };
@@ -1277,6 +1292,7 @@ export type PodcastTopper = Topper & TopperWithBrand & TopperWithHeadshot & Topp
1277
1292
 
1278
1293
  export type PodcastTopperHeadshotArgs = {
1279
1294
  dpr?: InputMaybe<Scalars['Int']['input']>;
1295
+ url?: InputMaybe<Scalars['String']['input']>;
1280
1296
  width?: InputMaybe<Scalars['Int']['input']>;
1281
1297
  };
1282
1298
 
@@ -1454,6 +1470,7 @@ export type TopperWithHeadshot = {
1454
1470
 
1455
1471
  export type TopperWithHeadshotHeadshotArgs = {
1456
1472
  dpr?: InputMaybe<Scalars['Int']['input']>;
1473
+ url?: InputMaybe<Scalars['String']['input']>;
1457
1474
  width?: InputMaybe<Scalars['Int']['input']>;
1458
1475
  };
1459
1476
 
@@ -1697,6 +1714,7 @@ export type ResolversTypes = ResolversObject<{
1697
1714
  Mutation: ResolverTypeWrapper<{}>;
1698
1715
  OpinionTopper: ResolverTypeWrapper<TopperModel>;
1699
1716
  PackageDesign: ResolverTypeWrapper<Scalars['PackageDesign']['output']>;
1717
+ Person: ResolverTypeWrapper<PersonModel>;
1700
1718
  Picture: ResolverTypeWrapper<PictureModel>;
1701
1719
  PictureFullBleed: ResolverTypeWrapper<PictureModel>;
1702
1720
  PictureInline: ResolverTypeWrapper<PictureModel>;
@@ -1783,6 +1801,7 @@ export type ResolversParentTypes = ResolversObject<{
1783
1801
  Mutation: {};
1784
1802
  OpinionTopper: TopperModel;
1785
1803
  PackageDesign: Scalars['PackageDesign']['output'];
1804
+ Person: PersonModel;
1786
1805
  Picture: PictureModel;
1787
1806
  PictureFullBleed: PictureModel;
1788
1807
  PictureInline: PictureModel;
@@ -2341,6 +2360,7 @@ export type LiveBlogPostResolvers<ContextType = QueryContext, ParentType extends
2341
2360
  altStandfirst: Resolver<Maybe<ResolversTypes['AltStandfirst']>, ParentType, ContextType>;
2342
2361
  altTitle: Resolver<Maybe<ResolversTypes['AltTitle']>, ParentType, ContextType>;
2343
2362
  annotations: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Concept']>>>, ParentType, ContextType>;
2363
+ authors: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Person']>>>, ParentType, ContextType>;
2344
2364
  body: Resolver<Maybe<ResolversTypes['RichText']>, ParentType, ContextType>;
2345
2365
  bodyXML: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2346
2366
  byline: Resolver<Maybe<ResolversTypes['StructuredContent']>, ParentType, ContextType, Partial<LiveBlogPostBylineArgs>>;
@@ -2409,6 +2429,13 @@ export interface PackageDesignScalarConfig extends GraphQLScalarTypeConfig<Resol
2409
2429
  name: 'PackageDesign';
2410
2430
  }
2411
2431
 
2432
+ export type PersonResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Person'] = ResolversParentTypes['Person']> = ResolversObject<{
2433
+ headshot: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, Partial<PersonHeadshotArgs>>;
2434
+ prefLabel: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2435
+ streamPage: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2436
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2437
+ }>;
2438
+
2412
2439
  export type PictureResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Picture'] = ResolversParentTypes['Picture']> = ResolversObject<{
2413
2440
  __resolveType?: TypeResolveFn<'PictureFullBleed' | 'PictureInline' | 'PictureStandard', ParentType, ContextType>;
2414
2441
  alt: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2725,6 +2752,7 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
2725
2752
  Mutation: MutationResolvers<ContextType>;
2726
2753
  OpinionTopper: OpinionTopperResolvers<ContextType>;
2727
2754
  PackageDesign: GraphQLScalarType;
2755
+ Person: PersonResolvers<ContextType>;
2728
2756
  Picture: PictureResolvers<ContextType>;
2729
2757
  PictureFullBleed: PictureFullBleedResolvers<ContextType>;
2730
2758
  PictureInline: PictureInlineResolvers<ContextType>;