@financial-times/cp-content-pipeline-schema 2.13.1 → 2.14.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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/datasources/capi.d.ts +1 -1
  3. package/lib/datasources/capi.js +8 -2
  4. package/lib/datasources/capi.js.map +1 -1
  5. package/lib/datasources/instrumented.d.ts +1 -1
  6. package/lib/datasources/instrumented.js +4 -1
  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 +8 -2
  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 +8 -2
  13. package/lib/datasources/twitter.js.map +1 -1
  14. package/lib/generated/index.d.ts +6 -3
  15. package/lib/helpers/timeout-error.d.ts +6 -0
  16. package/lib/helpers/timeout-error.js +15 -0
  17. package/lib/helpers/timeout-error.js.map +1 -0
  18. package/lib/model/CapiResponse.js +8 -1
  19. package/lib/model/CapiResponse.js.map +1 -1
  20. package/lib/model/Topper.js +2 -1
  21. package/lib/model/Topper.js.map +1 -1
  22. package/lib/resolvers/core.js +19 -8
  23. package/lib/resolvers/core.js.map +1 -1
  24. package/lib/resolvers/index.d.ts +1 -0
  25. package/lib/resolvers/topper.d.ts +1 -0
  26. package/lib/resolvers/topper.js +1 -0
  27. package/lib/resolvers/topper.js.map +1 -1
  28. package/package.json +1 -1
  29. package/queries/article.graphql +1 -0
  30. package/src/datasources/capi.ts +5 -2
  31. package/src/datasources/instrumented.ts +5 -2
  32. package/src/datasources/origami-image.ts +5 -2
  33. package/src/datasources/twitter.ts +5 -2
  34. package/src/generated/index.ts +6 -3
  35. package/src/helpers/timeout-error.ts +13 -0
  36. package/src/model/CapiResponse.ts +14 -8
  37. package/src/model/Topper.ts +2 -1
  38. package/src/resolvers/core.ts +19 -10
  39. package/src/resolvers/topper.ts +1 -0
  40. package/tsconfig.tsbuildinfo +1 -1
  41. package/typedefs/topper.graphql +4 -1
@@ -18,11 +18,14 @@ export class InstrumentedRESTDataSource
18
18
  implements BaseDataSource
19
19
  {
20
20
  startTime?: bigint
21
- backendSystemCode: string | undefined
22
21
  context: QueryContext
23
22
  calls: string[] = []
24
23
  errorHandler: FetchErrorHandler
25
24
 
25
+ get backendSystemCode(): string | undefined {
26
+ return undefined
27
+ }
28
+
26
29
  constructor({ cache, context }: BaseDataSourceOptions) {
27
30
  const wrappedCache = new PrefixingKeyValueCache(
28
31
  isContextableCache(cache) ? cache.withContext(context) : cache,
@@ -100,7 +103,7 @@ export class InstrumentedRESTDataSource
100
103
  } catch (error) {
101
104
  if (error instanceof BaseError) {
102
105
  const status =
103
- error.code === 'FETCH_ABORT_ERROR'
106
+ error.code === 'FETCH_TIMEOUT_ERROR'
104
107
  ? 408
105
108
  : Number(error.data.upstreamStatusCode ?? 0)
106
109
  const duration = (process.hrtime.bigint() - startTime) / BigInt(1e6)
@@ -1,3 +1,4 @@
1
+ import TimeoutError from '../helpers/timeout-error'
1
2
  import { InstrumentedRESTDataSource } from './instrumented'
2
3
  import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
3
4
 
@@ -7,7 +8,9 @@ const REQUEST_TIMEOUT = process.env.ORIGAMI_DATASOURCE_REQUEST_TIMEOUT
7
8
 
8
9
  export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
9
10
  baseURL = 'https://www.ft.com/__origami/service/image/v2/'
10
- backendSystemCode = 'origami-image-service-v2'
11
+ get backendSystemCode() {
12
+ return 'origami-image-service-v2'
13
+ }
11
14
 
12
15
  abortController = new AbortController()
13
16
  timeout: ReturnType<typeof setTimeout> | undefined = undefined
@@ -21,7 +24,7 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
21
24
 
22
25
  request.signal = this.abortController.signal
23
26
  this.timeout = setTimeout(
24
- () => this.abortController.abort(),
27
+ () => this.abortController.abort(new TimeoutError(REQUEST_TIMEOUT)),
25
28
  REQUEST_TIMEOUT
26
29
  )
27
30
  }
@@ -1,5 +1,6 @@
1
1
  import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
2
2
  import { InstrumentedRESTDataSource } from './instrumented'
3
+ import TimeoutError from '../helpers/timeout-error'
3
4
 
4
5
  const REQUEST_TIMEOUT = process.env.TWITTER_DATASOURCE_REQUEST_TIMEOUT
5
6
  ? parseInt(process.env.TWITTER_DATASOURCE_REQUEST_TIMEOUT)
@@ -7,7 +8,9 @@ const REQUEST_TIMEOUT = process.env.TWITTER_DATASOURCE_REQUEST_TIMEOUT
7
8
 
8
9
  export class TwitterDataSource extends InstrumentedRESTDataSource {
9
10
  baseURL = 'https://publish.twitter.com'
10
- backendSystemCode = 'twitter-oembed-api'
11
+ get backendSystemCode() {
12
+ return 'twitter-oembed-api'
13
+ }
11
14
 
12
15
  abortController = new AbortController()
13
16
  timeout: ReturnType<typeof setTimeout> | undefined = undefined
@@ -17,7 +20,7 @@ export class TwitterDataSource extends InstrumentedRESTDataSource {
17
20
 
18
21
  request.signal = this.abortController.signal
19
22
  this.timeout = setTimeout(
20
- () => this.abortController.abort(),
23
+ () => this.abortController.abort(new TimeoutError(REQUEST_TIMEOUT)),
21
24
  REQUEST_TIMEOUT
22
25
  )
23
26
  }
@@ -1520,7 +1520,7 @@ export type TopperWithBrand = {
1520
1520
  readonly genreConcept?: Maybe<Concept>;
1521
1521
  };
1522
1522
 
1523
- export type TopperWithFlourish = Topper & {
1523
+ export type TopperWithFlourish = Topper & TopperWithTheme & {
1524
1524
  /** Whether the topper should have a background box. */
1525
1525
  readonly backgroundBox?: Maybe<Scalars['Boolean']['output']>;
1526
1526
  /** The background colour of the topper. */
@@ -1535,6 +1535,8 @@ export type TopperWithFlourish = Topper & {
1535
1535
  readonly headline: Scalars['String']['output'];
1536
1536
  /** An abstract syntax tree of the introduction text. */
1537
1537
  readonly intro?: Maybe<RichText>;
1538
+ /** Whether the headline should be large. */
1539
+ readonly isLargeHeadline?: Maybe<Scalars['Boolean']['output']>;
1538
1540
  /** The layout type of the topper, eg. 'flourish'. */
1539
1541
  readonly layout?: Maybe<Scalars['String']['output']>;
1540
1542
  /** The layout width of the topper, eg. 'full-grid'. */
@@ -1740,7 +1742,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = R
1740
1742
  TopperWithHeadshot: ( TopperModel ) | ( TopperModel );
1741
1743
  TopperWithImages: ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel );
1742
1744
  TopperWithPackage: ( TopperModel );
1743
- TopperWithTheme: ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel );
1745
+ TopperWithTheme: ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel ) | ( TopperModel );
1744
1746
  }>;
1745
1747
 
1746
1748
  /** Mapping between all available schema types and the resolvers types */
@@ -2775,6 +2777,7 @@ export type TopperWithFlourishResolvers<ContextType = QueryContext, ParentType e
2775
2777
  genreConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2776
2778
  headline: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2777
2779
  intro: Resolver<Maybe<ResolversTypes['RichText']>, ParentType, ContextType>;
2780
+ isLargeHeadline: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2778
2781
  layout: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2779
2782
  layoutWidth: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2780
2783
  leadFlourish: Resolver<Maybe<ResolversTypes['LeadFlourish']>, ParentType, ContextType>;
@@ -2799,7 +2802,7 @@ export type TopperWithPackageResolvers<ContextType = QueryContext, ParentType ex
2799
2802
  }>;
2800
2803
 
2801
2804
  export type TopperWithThemeResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['TopperWithTheme'] = ResolversParentTypes['TopperWithTheme']> = ResolversObject<{
2802
- __resolveType?: TypeResolveFn<'BrandedTopper' | 'DeepLandscapeTopper' | 'DeepPortraitTopper' | 'FullBleedTopper' | 'OpinionTopper' | 'PodcastTopper' | 'SplitTextTopper', ParentType, ContextType>;
2805
+ __resolveType?: TypeResolveFn<'BrandedTopper' | 'DeepLandscapeTopper' | 'DeepPortraitTopper' | 'FullBleedTopper' | 'OpinionTopper' | 'PodcastTopper' | 'SplitTextTopper' | 'TopperWithFlourish', ParentType, ContextType>;
2803
2806
  isLargeHeadline: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2804
2807
  layout: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2805
2808
  }>;
@@ -0,0 +1,13 @@
1
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
2
+
3
+ export default class TimeoutError extends OperationalError {
4
+ name = 'TimeoutError'
5
+ code = 'TIMEOUT_ERROR'
6
+
7
+ constructor(timeout: number) {
8
+ super({
9
+ message: `Missed the timeout of ${timeout}ms`,
10
+ timeout,
11
+ })
12
+ }
13
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  import conceptIds from '@financial-times/n-concept-ids'
8
8
  import metadata from '@financial-times/n-display-metadata'
9
9
  import cloneDeep from 'clone-deep'
10
- import { OperationalError } from '@dotcom-reliability-kit/errors'
10
+ import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
11
11
 
12
12
  import type { QueryContext } from '..'
13
13
  import sortBy from 'lodash.sortby'
@@ -127,24 +127,28 @@ function flattenFormattedZodIssues(
127
127
  }
128
128
 
129
129
  function getContentType(
130
- content: { type?: string; types: string[] },
130
+ content: BaselineContent,
131
131
  validate?: true
132
132
  ): LiteralUnionScalarValues<typeof ContentType>
133
133
  function getContentType(
134
- content: { type?: string; types: string[] },
134
+ content: BaselineContent,
135
135
  validate: false
136
136
  ): LiteralUnionScalarValues<typeof ContentType> | string
137
- function getContentType(
138
- content: { type?: string; types: string[] },
139
- validate = true
140
- ) {
137
+ function getContentType(content: BaselineContent, validate = true) {
141
138
  const TYPE_REGEX = /https?:\/\/www.ft.com\/ontology\/content\//
142
139
  const useType =
143
140
  'type' in content && content.type ? content.type : content.types[0]
144
141
  const type = useType?.replace(TYPE_REGEX, '')
145
142
 
146
143
  if (validate && !validLiteralUnionValue(type, ContentType.values)) {
147
- throw new Error('Content type is invalid: ' + useType)
144
+ // if we get a request for a content type we can't (and shouldn't)
145
+ // handle, such as Image, there's nothing else we can do, so throw
146
+ throw new BaseError({
147
+ message: `Content type is invalid: ${useType}`,
148
+ code: 'UNEXPECTED_CONTENT_TYPE',
149
+ type: useType,
150
+ id: content.id,
151
+ })
148
152
  }
149
153
 
150
154
  return type
@@ -156,6 +160,8 @@ const baselineContentSchema = z.object({
156
160
  types: z.array(z.string()),
157
161
  })
158
162
 
163
+ type BaselineContent = z.infer<typeof baselineContentSchema>
164
+
159
165
  export class CapiResponse {
160
166
  constructor(
161
167
  private capiData: ContentTypeSchemas,
@@ -138,7 +138,8 @@ export class Topper {
138
138
  type === 'SplitTextTopper' ||
139
139
  type === 'FullBleedTopper' ||
140
140
  type === 'DeepPortraitTopper' ||
141
- type === 'DeepLandscapeTopper'
141
+ type === 'DeepLandscapeTopper' ||
142
+ type === 'TopperWithFlourish'
142
143
  )
143
144
  }
144
145
 
@@ -17,16 +17,25 @@ const resolvers = {
17
17
  try {
18
18
  return await context.dataSources.capi.getContent(args.uuid)
19
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
- })
20
+ if (error instanceof BaseError) {
21
+ if (error.data.upstreamStatusCode === 404) {
22
+ throw new HttpError({
23
+ code: 'CONTENT_NOT_FOUND',
24
+ message: 'Content not found in Content API',
25
+ cause: error,
26
+ uuid: args.uuid,
27
+ statusCode: 404,
28
+ relatesToSystems: ['up-ica'],
29
+ })
30
+ }
31
+ if (error.code === 'UNEXPECTED_CONTENT_TYPE') {
32
+ throw new HttpError({
33
+ code: error.code,
34
+ cause: error,
35
+ message: `Requested a content type we don't handle`,
36
+ statusCode: 404,
37
+ })
38
+ }
30
39
  }
31
40
 
32
41
  throw error
@@ -105,6 +105,7 @@ const resolvers = {
105
105
 
106
106
  TopperWithFlourish: {
107
107
  ...topperResolvers,
108
+ isLargeHeadline: (topper) => topper.isLargeHeadline(),
108
109
  layout: (topper) => topper.layout(),
109
110
  layoutWidth: (topper) => topper.layoutWidth(),
110
111
  leadFlourish: (topper) => topper.leadFlourish(),