@financial-times/cp-content-pipeline-schema 2.8.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 (55) hide show
  1. package/CHANGELOG.md +17 -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/model/CapiResponse.js +33 -4
  15. package/lib/model/CapiResponse.js.map +1 -1
  16. package/lib/model/Concept.js +1 -1
  17. package/lib/model/Concept.js.map +1 -1
  18. package/lib/model/Image.js +8 -3
  19. package/lib/model/Image.js.map +1 -1
  20. package/lib/model/Person.js +7 -2
  21. package/lib/model/Person.js.map +1 -1
  22. package/lib/resolvers/content-tree/bodyXMLToTree.js +1 -1
  23. package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
  24. package/lib/resolvers/content-tree/bodyXMLToTree.test.js +7 -7
  25. package/lib/resolvers/content-tree/bodyXMLToTree.test.js.map +1 -1
  26. package/lib/resolvers/content-tree/references/Flourish.js +7 -2
  27. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  28. package/lib/resolvers/content-tree/references/RawImage.js +7 -2
  29. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  30. package/lib/resolvers/content-tree/references/Recommended.js +1 -1
  31. package/lib/resolvers/content-tree/references/Recommended.js.map +1 -1
  32. package/lib/resolvers/content-tree/references/Tweet.js +7 -2
  33. package/lib/resolvers/content-tree/references/Tweet.js.map +1 -1
  34. package/lib/resolvers/content-tree/references/Video.js +15 -2
  35. package/lib/resolvers/content-tree/references/Video.js.map +1 -1
  36. package/lib/resolvers/core.js +16 -1
  37. package/lib/resolvers/core.js.map +1 -1
  38. package/package.json +5 -2
  39. package/src/datasources/capi.ts +16 -44
  40. package/src/datasources/instrumented.ts +29 -31
  41. package/src/datasources/origami-image.ts +11 -25
  42. package/src/datasources/twitter.ts +10 -24
  43. package/src/model/CapiResponse.ts +44 -6
  44. package/src/model/Concept.ts +1 -1
  45. package/src/model/Image.ts +9 -4
  46. package/src/model/Person.ts +7 -2
  47. package/src/resolvers/content-tree/bodyXMLToTree.test.ts +7 -7
  48. package/src/resolvers/content-tree/bodyXMLToTree.ts +1 -1
  49. package/src/resolvers/content-tree/references/Flourish.ts +7 -2
  50. package/src/resolvers/content-tree/references/RawImage.ts +7 -2
  51. package/src/resolvers/content-tree/references/Recommended.ts +1 -1
  52. package/src/resolvers/content-tree/references/Tweet.ts +7 -2
  53. package/src/resolvers/content-tree/references/Video.ts +18 -4
  54. package/src/resolvers/core.ts +18 -1
  55. package/tsconfig.tsbuildinfo +1 -1
@@ -188,7 +188,7 @@ export class CapiResponse {
188
188
  * Don't let this failure block this system from continuing to try and handle the request
189
189
  */
190
190
  if (!schemaResponse.success) {
191
- context.logger.error({
191
+ context.logger.warn({
192
192
  event: 'RECOVERABLE_ERROR',
193
193
  error: new OperationalError({
194
194
  message:
@@ -286,7 +286,7 @@ export class CapiResponse {
286
286
  return vanityUrl ?? url
287
287
  } catch (error) {
288
288
  if (isError(error)) {
289
- this.context.logger.error({
289
+ this.context.logger.warn({
290
290
  event: 'RECOVERABLE_ERROR',
291
291
  error,
292
292
  })
@@ -405,9 +405,24 @@ export class CapiResponse {
405
405
  'containedIn' in this.capiData && this.capiData.containedIn?.[0]?.id
406
406
 
407
407
  if (containerId) {
408
- container = await this.context.dataSources.capi.getContent(
409
- uuidFromUrl(containerId)
410
- )
408
+ try {
409
+ container = await this.context.dataSources.capi.getContent(
410
+ uuidFromUrl(containerId)
411
+ )
412
+ } catch (error) {
413
+ if (error instanceof Error) {
414
+ this.context.logger.warn({
415
+ event: 'RECOVERABLE_ERROR',
416
+ error: new OperationalError({
417
+ message: `Failed to fetch package container ${containerId} for content ${this.id()}`,
418
+ code: `PACKAGE_CONTAINEDIN_ERROR`,
419
+ cause: error,
420
+ }),
421
+ })
422
+ }
423
+
424
+ return null
425
+ }
411
426
  this.packageContainer = container
412
427
  }
413
428
  }
@@ -652,11 +667,34 @@ export class CapiResponse {
652
667
 
653
668
  this.context.addSurrogateKeys(contains.map((article) => article.id))
654
669
 
655
- return Promise.all(
670
+ const results = await Promise.allSettled(
656
671
  contains.map(({ id }) =>
657
672
  this.context.dataSources.capi.getContent(uuidFromUrl(id), this)
658
673
  )
659
674
  )
675
+
676
+ const failed = results.filter(
677
+ (result): result is PromiseRejectedResult =>
678
+ result.status === 'rejected'
679
+ )
680
+
681
+ if (failed.length) {
682
+ this.context.logger.warn({
683
+ event: 'RECOVERABLE_ERROR',
684
+ error: new OperationalError({
685
+ code: 'PACKAGE_CONTAINS_ERROR',
686
+ message: `Failed to fetch some of the content contained in the package ${this.id()}`,
687
+ cause: new AggregateError(failed.map((result) => result.reason)),
688
+ }),
689
+ })
690
+ }
691
+
692
+ return results
693
+ .filter(
694
+ (result): result is PromiseFulfilledResult<CapiResponse> =>
695
+ result.status === 'fulfilled'
696
+ )
697
+ .map((result) => result.value)
660
698
  }
661
699
 
662
700
  return null
@@ -154,7 +154,7 @@ export class Concept {
154
154
  }
155
155
  } catch (error) {
156
156
  if (isError(error)) {
157
- this.context.logger.error({
157
+ this.context.logger.warn({
158
158
  event: 'RECOVERABLE_ERROR',
159
159
  error,
160
160
  })
@@ -12,7 +12,7 @@ import {
12
12
  } from '../resolvers/literal-union'
13
13
  import { ImageFormat, ImageType } from '../resolvers/scalars'
14
14
  import isError from '../helpers/isError'
15
- import { BaseError } from '@dotcom-reliability-kit/errors'
15
+ import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
16
16
  import type { ImageSource } from '../generated'
17
17
 
18
18
  export type ImageSourceArgs = {
@@ -54,7 +54,7 @@ export class CAPIImage implements Image {
54
54
  }
55
55
 
56
56
  // we shouldn't be here. but just in case,
57
- this.context.logger.error({
57
+ this.context.logger.warn({
58
58
  event: 'RECOVERABLE_ERROR',
59
59
  error: new BaseError({
60
60
  code: 'INVALID_IMAGE_TYPE',
@@ -164,9 +164,14 @@ export class CAPIImage implements Image {
164
164
  return imageMetadata
165
165
  } catch (error) {
166
166
  if (isError(error)) {
167
- this.context.logger.error({
167
+ this.context.logger.warn({
168
168
  event: 'RECOVERABLE_ERROR',
169
- error,
169
+ error: new OperationalError({
170
+ code: 'IMAGE_DIMENSIONS_ERROR',
171
+ message: `Failed to get dimensions for ${this.capiImage.binaryUrl}`,
172
+ url: this.capiImage.binaryUrl,
173
+ cause: error,
174
+ }),
170
175
  })
171
176
  }
172
177
  return null
@@ -4,6 +4,7 @@ import { CapiResponse } from './CapiResponse'
4
4
  import imageServiceUrl from '../helpers/imageService'
5
5
  import isError from '../helpers/isError'
6
6
  import decorateHeadshotUrl, { UUID_REGEX } from '../helpers/decorateHeadshotUrl'
7
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
7
8
 
8
9
  type HeadshotArguments = {
9
10
  width?: number | null
@@ -63,9 +64,13 @@ export class Person {
63
64
  : null
64
65
  } catch (error) {
65
66
  if (isError(error)) {
66
- this.context.logger.error({
67
+ this.context.logger.warn({
67
68
  event: 'RECOVERABLE_ERROR',
68
- error,
69
+ error: new OperationalError({
70
+ message: `Error fetching CAPI Person for headshot URL for ${this.prefLabel()}`,
71
+ code: 'HEADSHOT_PERSON_FETCH_ERROR',
72
+ cause: error,
73
+ }),
69
74
  })
70
75
  }
71
76
  return null
@@ -6,7 +6,7 @@ import { QueryContext, BodyXMLToTreeError } from '../..'
6
6
  import { OperationalError } from '@dotcom-reliability-kit/errors'
7
7
 
8
8
  const mockLogger = new Logger()
9
- const mockLogError = jest.spyOn(mockLogger, 'error')
9
+ const mockLogWarn = jest.spyOn(mockLogger, 'warn')
10
10
 
11
11
  const mockContext = {
12
12
  logger: mockLogger,
@@ -233,7 +233,7 @@ describe('bodyXMLToTree', () => {
233
233
  "version": 1,
234
234
  }
235
235
  `)
236
- expect(mockLogError).not.toBeCalled()
236
+ expect(mockLogWarn).not.toBeCalled()
237
237
  })
238
238
 
239
239
  it('should handle heading and slots', () => {
@@ -268,7 +268,7 @@ describe('bodyXMLToTree', () => {
268
268
  "version": 1,
269
269
  }
270
270
  `)
271
- expect(mockLogError).not.toBeCalled()
271
+ expect(mockLogWarn).not.toBeCalled()
272
272
  })
273
273
 
274
274
  it('should log an error on unexpected child after heading', () => {
@@ -303,8 +303,8 @@ describe('bodyXMLToTree', () => {
303
303
  "version": 1,
304
304
  }
305
305
  `)
306
- expect(mockLogError).toBeCalled()
307
- expect(mockLogError.mock.lastCall).toMatchInlineSnapshot(`
306
+ expect(mockLogWarn).toBeCalled()
307
+ expect(mockLogWarn.mock.lastCall).toMatchInlineSnapshot(`
308
308
  Array [
309
309
  Object {
310
310
  "error": Object {
@@ -349,8 +349,8 @@ describe('bodyXMLToTree', () => {
349
349
  "version": 1,
350
350
  }
351
351
  `)
352
- expect(mockLogError).toBeCalled()
353
- expect(mockLogError.mock.lastCall).toMatchInlineSnapshot(`
352
+ expect(mockLogWarn).toBeCalled()
353
+ expect(mockLogWarn.mock.lastCall).toMatchInlineSnapshot(`
354
354
  Array [
355
355
  Object {
356
356
  "error": Object {
@@ -80,7 +80,7 @@ export default function bodyXMLToTree(
80
80
  function logErrors(context?: QueryContext) {
81
81
  if (!context?.aggregatedErrors?.bodyXMLToTree.length) return
82
82
 
83
- context.logger.error({
83
+ context.logger.warn({
84
84
  event: 'RECOVERABLE_ERROR',
85
85
  message: 'Unexpected structure in bodyXMLToTree',
86
86
  error: new OperationalError({
@@ -5,6 +5,7 @@ import {
5
5
  } from '../../../generated'
6
6
  import { QueryContext } from '../../..'
7
7
  import isError from '../../../helpers/isError'
8
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
8
9
 
9
10
  export const Flourish = {
10
11
  async fallbackImage(parent, _args, context) {
@@ -79,9 +80,13 @@ const getImageMetadata = async (
79
80
  }
80
81
  } catch (error) {
81
82
  if (isError(error)) {
82
- context.logger.error({
83
+ context.logger.warn({
83
84
  event: 'RECOVERABLE_ERROR',
84
- error,
85
+ error: new OperationalError({
86
+ code: 'FLOURISH_IMAGE_METADATA_ERROR',
87
+ message: `Error getting image dimensions for Flourish fallback image ${flourishUrl}`,
88
+ cause: error,
89
+ }),
85
90
  })
86
91
  }
87
92
  return {
@@ -4,6 +4,7 @@ import imageServiceUrl from '../../../helpers/imageService'
4
4
  import { RawImage as RawImageNode } from '../Workarounds'
5
5
  import { RawImageResolvers } from '../../../generated'
6
6
  import isError from '../../../helpers/isError'
7
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
7
8
 
8
9
  class RawImageModel implements Image {
9
10
  constructor(private rawImage: RawImageNode, private context: QueryContext) {}
@@ -50,9 +51,13 @@ class RawImageModel implements Image {
50
51
  return imageMetadata
51
52
  } catch (error) {
52
53
  if (isError(error)) {
53
- this.context.logger.error({
54
+ this.context.logger.warn({
54
55
  event: 'RECOVERABLE_ERROR',
55
- error,
56
+ error: new OperationalError({
57
+ code: 'RAW_IMAGE_DIMENSIONS_ERROR',
58
+ message: `Error getting dimensions for raw image ${this.url()}`,
59
+ cause: error,
60
+ }),
56
61
  })
57
62
  }
58
63
  return null
@@ -15,7 +15,7 @@ export const Recommended = {
15
15
  return content
16
16
  } catch (error) {
17
17
  if (error instanceof Error) {
18
- context.logger.error({
18
+ context.logger.warn({
19
19
  event: 'RECOVERABLE_ERROR',
20
20
  error: new OperationalError({
21
21
  code: 'RECOMMENDED_TEASER_ERROR',
@@ -1,3 +1,4 @@
1
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
1
2
  import { TweetResolvers } from '../../../generated'
2
3
  import isError from '../../../helpers/isError'
3
4
 
@@ -10,9 +11,13 @@ export const Tweet = {
10
11
  return tweet.html
11
12
  } catch (error) {
12
13
  if (isError(error)) {
13
- context.logger.error({
14
+ context.logger.warn({
14
15
  event: 'RECOVERABLE_ERROR',
15
- error,
16
+ error: new OperationalError({
17
+ code: 'TWEET_EMBED_ERROR',
18
+ message: `Failed to get HTML embed for tweet ${parent.reference.id}`,
19
+ cause: error,
20
+ }),
16
21
  })
17
22
  }
18
23
  return null
@@ -1,15 +1,29 @@
1
1
  import { uuidFromUrl } from '../../../helpers/metadata'
2
2
  import { VideoReferenceResolvers } from '../../../generated'
3
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
3
4
 
4
5
  export const Video = {
5
6
  // This will override the original id from the parent that is a URL.
6
7
  id: (parent) => uuidFromUrl(parent.reference.id),
7
8
 
8
9
  async title(parent, _args, context) {
9
- const capiResponse = await context.dataSources.capi.getContent(
10
- uuidFromUrl(parent.reference.id)
11
- )
12
- return capiResponse.title()
10
+ try {
11
+ const capiResponse = await context.dataSources.capi.getContent(
12
+ uuidFromUrl(parent.reference.id)
13
+ )
14
+
15
+ return capiResponse.title()
16
+ } catch (error) {
17
+ if (error instanceof Error) {
18
+ throw new OperationalError({
19
+ code: 'VIDEO_CONTENT_FETCH_ERROR',
20
+ message: `Error getting content object for video ${parent.reference.id}`,
21
+ cause: error,
22
+ })
23
+ }
24
+
25
+ throw error
26
+ }
13
27
  },
14
28
 
15
29
  type(parent) {
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { QueryResolvers, MutationResolvers } from '../generated'
4
4
  import { CapiResponse } from '../model/CapiResponse'
5
+ import { BaseError, HttpError } from '@dotcom-reliability-kit/errors'
5
6
 
6
7
  const packageJson = JSON.parse(
7
8
  fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf-8')
@@ -13,7 +14,23 @@ const resolvers = {
13
14
  Query: {
14
15
  version: () => version,
15
16
  async content(_, args, context) {
16
- return context.dataSources.capi.getContent(args.uuid)
17
+ try {
18
+ return await context.dataSources.capi.getContent(args.uuid)
19
+ } catch (error) {
20
+ if (
21
+ error instanceof BaseError &&
22
+ error.data.upstreamStatusCode === 404
23
+ ) {
24
+ throw new HttpError({
25
+ code: 'CONTENT_NOT_FOUND',
26
+ message: `Content ${args.uuid} not found in Content API`,
27
+ statusCode: 404,
28
+ relatesToSystems: ['up-ica'],
29
+ })
30
+ }
31
+
32
+ throw error
33
+ }
17
34
  },
18
35
  contentFromJSON(_, { content }, context) {
19
36
  return CapiResponse.fromJSON(content, context)