@financial-times/cp-content-pipeline-schema 2.0.3 → 2.2.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.
- package/CHANGELOG.md +25 -0
- package/lib/datasources/capi.d.ts +2 -3
- package/lib/datasources/capi.js +2 -2
- package/lib/datasources/capi.js.map +1 -1
- package/lib/datasources/instrumented.d.ts +4 -5
- package/lib/datasources/instrumented.js +33 -27
- package/lib/datasources/instrumented.js.map +1 -1
- package/lib/datasources/origami-image.d.ts +2 -3
- package/lib/datasources/origami-image.js +2 -2
- package/lib/datasources/origami-image.js.map +1 -1
- package/lib/datasources/twitter.d.ts +2 -2
- package/lib/datasources/twitter.js +2 -2
- package/lib/datasources/twitter.js.map +1 -1
- package/lib/generated/index.d.ts +2 -0
- package/lib/model/CapiResponse.js +14 -0
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/schemas/capi/base-schema.js +8 -4
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/resolvers/content.d.ts +3 -0
- package/lib/resolvers/content.js +5 -0
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/index.d.ts +3 -0
- package/package.json +2 -2
- package/queries/article.graphql +9 -0
- package/src/datasources/capi.ts +3 -5
- package/src/datasources/instrumented.ts +58 -44
- package/src/datasources/origami-image.ts +4 -8
- package/src/datasources/twitter.ts +4 -10
- package/src/generated/index.ts +2 -0
- package/src/model/CapiResponse.ts +16 -0
- package/src/model/schemas/capi/base-schema.ts +13 -9
- package/src/resolvers/content.ts +7 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/content.graphql +5 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
WillSendRequestOptions,
|
|
3
2
|
RESTDataSource,
|
|
4
|
-
|
|
3
|
+
CacheOptions,
|
|
4
|
+
DataSourceFetchResult,
|
|
5
|
+
DataSourceRequest,
|
|
6
|
+
AugmentedRequest,
|
|
5
7
|
} from '@apollo/datasource-rest'
|
|
6
|
-
import type { FetcherResponse } from '@apollo/utils.fetcher'
|
|
7
8
|
import { PrefixingKeyValueCache } from '@apollo/utils.keyvaluecache'
|
|
8
9
|
import { SerializedRequest } from '@dotcom-reliability-kit/serialize-request'
|
|
9
10
|
import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
|
|
@@ -40,69 +41,82 @@ export class InstrumentedRESTDataSource
|
|
|
40
41
|
return this.constructor.name.replace(/DataSource$/, '').toLowerCase()
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
override willSendRequest(request:
|
|
44
|
+
override willSendRequest(path: string, request: AugmentedRequest): void {
|
|
44
45
|
request.headers[
|
|
45
46
|
'user-agent'
|
|
46
47
|
] = `cp-content-pipeline-api/${this.context.versions.api} cp-content-pipeline-schema/${this.context.versions.schema} `
|
|
48
|
+
|
|
49
|
+
if (this.context.requestId && !this.context.contentRequestedOnce) {
|
|
50
|
+
request.headers['x-request-id'] = this.context.requestId
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// we implement our own error handling so skip the built-in GraphQLError
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
56
|
+
async throwIfResponseIsError() {}
|
|
57
|
+
|
|
58
|
+
async fetch<TResult>(
|
|
59
|
+
path: string,
|
|
60
|
+
incomingRequest?: DataSourceRequest<CacheOptions>
|
|
61
|
+
): Promise<DataSourceFetchResult<TResult>> {
|
|
62
|
+
const startTime = process.hrtime.bigint()
|
|
55
63
|
|
|
56
64
|
this.context.metrics?.count(
|
|
57
65
|
`graphql.datasource.${this.constructor.name}.request.count`,
|
|
58
66
|
1
|
|
59
67
|
)
|
|
68
|
+
|
|
60
69
|
this.context.logger.info({
|
|
61
70
|
event: 'REQUEST_DATASOURCE',
|
|
62
71
|
datasource: this.constructor.name,
|
|
63
72
|
request: {
|
|
64
73
|
id: this.context.requestId,
|
|
65
|
-
url:
|
|
66
|
-
method:
|
|
74
|
+
url: new URL(path, this.baseURL).href,
|
|
75
|
+
method: incomingRequest?.method,
|
|
67
76
|
} as SerializedRequest,
|
|
68
77
|
})
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
const result = await super.fetch<TResult>(path, incomingRequest)
|
|
81
|
+
const duration = (process.hrtime.bigint() - startTime) / BigInt(1e6)
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
this.context.metrics?.count(
|
|
84
|
+
`graphql.datasource.${this.constructor.name}.response.${result.response.status}.count`,
|
|
85
|
+
1
|
|
86
|
+
)
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// this.startTime will definitely have been set by willSendRequest, but no way of encoding that in types
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
-
const duration = (process.hrtime.bigint() - this.startTime!) / BigInt(1e6)
|
|
88
|
+
this.context.metrics?.count(
|
|
89
|
+
`graphql.datasource.${this.constructor.name}.response.${result.response.status}.time`,
|
|
90
|
+
Number(duration)
|
|
91
|
+
)
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
if (!result.response.ok) {
|
|
94
|
+
throw new UpstreamServiceError({
|
|
95
|
+
message: `${result.response.status}: ${result.response.statusText} from ${this.constructor.name}`,
|
|
96
|
+
statusCode: result.response.status,
|
|
97
|
+
relatesToSystems: this.backendSystemCodes,
|
|
98
|
+
url: result.response.url,
|
|
99
|
+
body: result.parsedBody,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
106
|
+
const duration = (process.hrtime.bigint() - startTime) / BigInt(1e6)
|
|
107
|
+
|
|
108
|
+
this.context.metrics?.count(
|
|
109
|
+
`graphql.datasource.${this.constructor.name}.response.408.count`,
|
|
110
|
+
1
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
this.context.metrics?.count(
|
|
114
|
+
`graphql.datasource.${this.constructor.name}.response.408.time`,
|
|
115
|
+
Number(duration)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
return super.didReceiveResponse(response, request)
|
|
96
|
-
} else {
|
|
97
|
-
const body = await this.parseBody(response)
|
|
98
|
-
|
|
99
|
-
throw new UpstreamServiceError({
|
|
100
|
-
message: `${response.status}: ${response.statusText} from ${this.constructor.name}`,
|
|
101
|
-
statusCode: response.status,
|
|
102
|
-
relatesToSystems: this.backendSystemCodes,
|
|
103
|
-
url: response.url,
|
|
104
|
-
body,
|
|
105
|
-
})
|
|
119
|
+
throw error
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
122
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
OperationalError,
|
|
4
|
-
UpstreamServiceError,
|
|
5
|
-
} from '@dotcom-reliability-kit/errors'
|
|
1
|
+
import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
|
|
6
2
|
import { InstrumentedRESTDataSource } from './instrumented'
|
|
7
|
-
import { CacheOptions } from '@apollo/datasource-rest
|
|
3
|
+
import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
|
|
8
4
|
|
|
9
5
|
const REQUEST_TIMEOUT = 5000 // 5 seconds
|
|
10
6
|
|
|
@@ -19,8 +15,8 @@ export class OrigamiImageDataSource extends InstrumentedRESTDataSource {
|
|
|
19
15
|
? parseInt(process.env.IMAGE_METADATA_CACHE_TTL)
|
|
20
16
|
: 60 * 60 * 24 * 7 // 1 week
|
|
21
17
|
|
|
22
|
-
override willSendRequest(request:
|
|
23
|
-
super.willSendRequest(request)
|
|
18
|
+
override willSendRequest(path: string, request: AugmentedRequest) {
|
|
19
|
+
super.willSendRequest(path, request)
|
|
24
20
|
|
|
25
21
|
request.signal = this.abortController.signal
|
|
26
22
|
this.timeout = setTimeout(
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
WillSendRequestOptions,
|
|
4
|
-
} from '@apollo/datasource-rest/dist/RESTDataSource'
|
|
5
|
-
import {
|
|
6
|
-
OperationalError,
|
|
7
|
-
UpstreamServiceError,
|
|
8
|
-
} from '@dotcom-reliability-kit/errors'
|
|
1
|
+
import { AugmentedRequest, CacheOptions } from '@apollo/datasource-rest'
|
|
2
|
+
import { UpstreamServiceError } from '@dotcom-reliability-kit/errors'
|
|
9
3
|
import { InstrumentedRESTDataSource } from './instrumented'
|
|
10
4
|
|
|
11
5
|
const REQUEST_TIMEOUT = 5000 // 5 seconds
|
|
@@ -16,8 +10,8 @@ export class TwitterDataSource extends InstrumentedRESTDataSource {
|
|
|
16
10
|
abortController = new AbortController()
|
|
17
11
|
timeout: ReturnType<typeof setTimeout> | undefined = undefined
|
|
18
12
|
|
|
19
|
-
override willSendRequest(request:
|
|
20
|
-
super.willSendRequest(request)
|
|
13
|
+
override willSendRequest(path: string, request: AugmentedRequest) {
|
|
14
|
+
super.willSendRequest(path, request)
|
|
21
15
|
|
|
22
16
|
request.signal = this.abortController.signal
|
|
23
17
|
this.timeout = setTimeout(
|
package/src/generated/index.ts
CHANGED
|
@@ -637,6 +637,7 @@ export type LiveBlogPost = Content & {
|
|
|
637
637
|
readonly editorialDesk?: Maybe<Scalars['String']['output']>;
|
|
638
638
|
readonly firstPublishedDate: Scalars['String']['output'];
|
|
639
639
|
readonly id: Scalars['ID']['output'];
|
|
640
|
+
readonly indicators?: Maybe<Indicators>;
|
|
640
641
|
readonly instantAlertConcept?: Maybe<Concept>;
|
|
641
642
|
readonly mainImage?: Maybe<Image>;
|
|
642
643
|
readonly originatingParty?: Maybe<Scalars['String']['output']>;
|
|
@@ -1780,6 +1781,7 @@ export type LiveBlogPostResolvers<ContextType = QueryContext, ParentType extends
|
|
|
1780
1781
|
editorialDesk?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
1781
1782
|
firstPublishedDate?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
1782
1783
|
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
|
1784
|
+
indicators?: Resolver<Maybe<ResolversTypes['Indicators']>, ParentType, ContextType>;
|
|
1783
1785
|
instantAlertConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1784
1786
|
mainImage?: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
|
|
1785
1787
|
originatingParty?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
@@ -270,6 +270,14 @@ export class CapiResponse {
|
|
|
270
270
|
return null
|
|
271
271
|
}
|
|
272
272
|
title() {
|
|
273
|
+
// CI-2038 HACK to remove "Comment:" prefix from live blog post titles
|
|
274
|
+
// as this is as the sole indicator that this is an opinion post
|
|
275
|
+
// while we're waiting for the annotation data to be in CAPI
|
|
276
|
+
if (this.type() === 'LiveBlogPost') {
|
|
277
|
+
if (this.capiData.title.startsWith('Comment:')) {
|
|
278
|
+
return this.capiData.title.replace('Comment:', '').trim()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
273
281
|
return this.capiData.title
|
|
274
282
|
}
|
|
275
283
|
id() {
|
|
@@ -507,6 +515,14 @@ export class CapiResponse {
|
|
|
507
515
|
}
|
|
508
516
|
|
|
509
517
|
isOpinion() {
|
|
518
|
+
// CI-2038 HACK to identify live blog opinion posts by the "Comment:" prefix in the title
|
|
519
|
+
// while we're waiting for the annotation data to be in CAPI
|
|
520
|
+
if (
|
|
521
|
+
this.type() === 'LiveBlogPost' &&
|
|
522
|
+
this.capiData.title.startsWith('Comment:')
|
|
523
|
+
) {
|
|
524
|
+
return true
|
|
525
|
+
}
|
|
510
526
|
return this.annotations().some((annotation: Concept) =>
|
|
511
527
|
annotation.isGenre('opinion')
|
|
512
528
|
)
|
|
@@ -121,15 +121,19 @@ export const ClipSet = z.object({
|
|
|
121
121
|
source: z.string().optional(),
|
|
122
122
|
subtitle: z.string().optional(),
|
|
123
123
|
publishedDate: z.string().optional(),
|
|
124
|
-
accessibility: z
|
|
125
|
-
|
|
126
|
-
z
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
accessibility: z
|
|
125
|
+
.object({
|
|
126
|
+
captions: z
|
|
127
|
+
.array(
|
|
128
|
+
z.object({
|
|
129
|
+
mediaType: z.string().optional(),
|
|
130
|
+
url: z.string().optional(),
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
.optional(),
|
|
134
|
+
transcript: z.string().optional(),
|
|
135
|
+
})
|
|
136
|
+
.optional(),
|
|
133
137
|
})
|
|
134
138
|
|
|
135
139
|
export const DataSource = z.object({
|
package/src/resolvers/content.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ContentPackageResolvers,
|
|
8
8
|
ContentResolvers,
|
|
9
9
|
LiveBlogPackageResolvers,
|
|
10
|
+
LiveBlogPostResolvers,
|
|
10
11
|
} from '../generated'
|
|
11
12
|
import { RichText } from '../model/RichText'
|
|
12
13
|
|
|
@@ -72,6 +73,11 @@ const resolvers = {
|
|
|
72
73
|
|
|
73
74
|
LiveBlogPost: {
|
|
74
75
|
containedIn: (parent) => parent.containedIn(),
|
|
76
|
+
indicators(parent) {
|
|
77
|
+
return {
|
|
78
|
+
isOpinion: parent.isOpinion(),
|
|
79
|
+
}
|
|
80
|
+
},
|
|
75
81
|
},
|
|
76
82
|
|
|
77
83
|
LiveBlogPackage: {
|
|
@@ -94,7 +100,7 @@ const resolvers = {
|
|
|
94
100
|
} satisfies {
|
|
95
101
|
Article: ArticleResolvers
|
|
96
102
|
Placeholder: PlaceholderResolvers
|
|
97
|
-
LiveBlogPost:
|
|
103
|
+
LiveBlogPost: LiveBlogPostResolvers
|
|
98
104
|
Content: ContentResolvers
|
|
99
105
|
LiveBlogPackage: LiveBlogPackageResolvers
|
|
100
106
|
ContentPackage: ContentPackageResolvers
|