@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.
- package/CHANGELOG.md +17 -0
- package/lib/datasources/capi.d.ts +1 -1
- package/lib/datasources/capi.js +8 -2
- package/lib/datasources/capi.js.map +1 -1
- package/lib/datasources/instrumented.d.ts +1 -1
- package/lib/datasources/instrumented.js +4 -1
- package/lib/datasources/instrumented.js.map +1 -1
- package/lib/datasources/origami-image.d.ts +1 -1
- package/lib/datasources/origami-image.js +8 -2
- package/lib/datasources/origami-image.js.map +1 -1
- package/lib/datasources/twitter.d.ts +1 -1
- package/lib/datasources/twitter.js +8 -2
- package/lib/datasources/twitter.js.map +1 -1
- package/lib/generated/index.d.ts +6 -3
- package/lib/helpers/timeout-error.d.ts +6 -0
- package/lib/helpers/timeout-error.js +15 -0
- package/lib/helpers/timeout-error.js.map +1 -0
- package/lib/model/CapiResponse.js +8 -1
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/Topper.js +2 -1
- package/lib/model/Topper.js.map +1 -1
- package/lib/resolvers/core.js +19 -8
- package/lib/resolvers/core.js.map +1 -1
- package/lib/resolvers/index.d.ts +1 -0
- package/lib/resolvers/topper.d.ts +1 -0
- package/lib/resolvers/topper.js +1 -0
- package/lib/resolvers/topper.js.map +1 -1
- package/package.json +1 -1
- package/queries/article.graphql +1 -0
- package/src/datasources/capi.ts +5 -2
- package/src/datasources/instrumented.ts +5 -2
- package/src/datasources/origami-image.ts +5 -2
- package/src/datasources/twitter.ts +5 -2
- package/src/generated/index.ts +6 -3
- package/src/helpers/timeout-error.ts +13 -0
- package/src/model/CapiResponse.ts +14 -8
- package/src/model/Topper.ts +2 -1
- package/src/resolvers/core.ts +19 -10
- package/src/resolvers/topper.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- 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 === '
|
|
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
|
|
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
|
|
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
|
}
|
package/src/generated/index.ts
CHANGED
|
@@ -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:
|
|
130
|
+
content: BaselineContent,
|
|
131
131
|
validate?: true
|
|
132
132
|
): LiteralUnionScalarValues<typeof ContentType>
|
|
133
133
|
function getContentType(
|
|
134
|
-
content:
|
|
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
|
-
|
|
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,
|
package/src/model/Topper.ts
CHANGED
package/src/resolvers/core.ts
CHANGED
|
@@ -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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
package/src/resolvers/topper.ts
CHANGED
|
@@ -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(),
|