@financial-times/cp-content-pipeline-schema 2.15.0 → 3.0.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 +39 -0
- package/lib/datasources/capi.d.ts +2 -2
- package/lib/datasources/capi.js +4 -2
- package/lib/datasources/capi.js.map +1 -1
- package/lib/datasources/capi.test.js +2 -2
- package/lib/fixtures/capiObject.d.ts +2 -2
- package/lib/fixtures/capiObject.js +2 -0
- package/lib/fixtures/capiObject.js.map +1 -1
- package/lib/fixtures/capiPerson.d.ts +1 -1
- package/lib/generated/index.d.ts +296 -131
- package/lib/helpers/decorateHeadshotUrl.d.ts +1 -2
- package/lib/helpers/decorateHeadshotUrl.js +2 -3
- package/lib/helpers/decorateHeadshotUrl.js.map +1 -1
- package/lib/model/Byline.d.ts +8 -10
- package/lib/model/Byline.js +34 -33
- package/lib/model/Byline.js.map +1 -1
- package/lib/model/Byline.test.js +105 -52
- package/lib/model/Byline.test.js.map +1 -1
- package/lib/model/CapiResponse.d.ts +12 -16
- package/lib/model/CapiResponse.js +38 -41
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/CapiResponse.test.js +7 -18
- package/lib/model/CapiResponse.test.js.map +1 -1
- package/lib/model/Clip.d.ts +1 -1
- package/lib/model/Concept.d.ts +1 -1
- package/lib/model/Concept.js +1 -2
- package/lib/model/Concept.js.map +1 -1
- package/lib/model/FlourishSource.d.ts +1 -1
- package/lib/model/FlourishSource.js.map +1 -1
- package/lib/model/Image.d.ts +1 -1
- package/lib/model/LeadFlourish.test.js +1 -0
- package/lib/model/LeadFlourish.test.js.map +1 -1
- package/lib/model/Person.d.ts +6 -12
- package/lib/model/Person.js +39 -66
- package/lib/model/Person.js.map +1 -1
- package/lib/model/Person.test.js +7 -60
- package/lib/model/Person.test.js.map +1 -1
- package/lib/model/Picture.d.ts +1 -1
- package/lib/model/RichText.d.ts +1 -2
- package/lib/model/Topper.d.ts +1 -1
- package/lib/model/Topper.js +10 -8
- package/lib/model/Topper.js.map +1 -1
- package/lib/model/Topper.test.js +9 -10
- package/lib/model/Topper.test.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +7 -4
- package/lib/model/schemas/capi/article.js +1 -0
- package/lib/model/schemas/capi/article.js.map +1 -1
- package/lib/model/schemas/capi/audio.d.ts +7 -4
- package/lib/model/schemas/capi/audio.js +1 -0
- package/lib/model/schemas/capi/audio.js.map +1 -1
- package/lib/model/schemas/capi/base-schema.d.ts +14 -123
- package/lib/model/schemas/capi/base-schema.js +7 -6
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +10 -5
- package/lib/model/schemas/capi/content-package.js +2 -0
- package/lib/model/schemas/capi/content-package.js.map +1 -1
- package/lib/model/schemas/capi/index.d.ts +41 -22
- package/lib/model/schemas/capi/internal-content.d.ts +24 -0
- package/lib/model/schemas/capi/internal-content.js +3 -0
- package/lib/model/schemas/capi/internal-content.js.map +1 -0
- package/lib/model/schemas/capi/live-blog-package.d.ts +7 -4
- package/lib/model/schemas/capi/live-blog-package.js +1 -0
- package/lib/model/schemas/capi/live-blog-package.js.map +1 -1
- package/lib/model/schemas/capi/placeholder.d.ts +7 -4
- package/lib/model/schemas/capi/placeholder.js +1 -0
- package/lib/model/schemas/capi/placeholder.js.map +1 -1
- package/lib/model/schemas/capi/video.d.ts +10 -5
- package/lib/model/schemas/capi/video.js +2 -0
- package/lib/model/schemas/capi/video.js.map +1 -1
- package/lib/resolvers/concept.d.ts +37 -2
- package/lib/resolvers/concept.js +17 -10
- package/lib/resolvers/concept.js.map +1 -1
- package/lib/resolvers/content-tree/Workarounds.d.ts +19 -11
- package/lib/resolvers/content-tree/references/Author.d.ts +4 -0
- package/lib/resolvers/content-tree/references/Author.js +14 -0
- package/lib/resolvers/content-tree/references/Author.js.map +1 -0
- package/lib/resolvers/content-tree/references/ClipSet.d.ts +1 -1
- package/lib/resolvers/content-tree/references/ClipSet.js +1 -1
- package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -1
- package/lib/resolvers/content-tree/references/Flourish.d.ts +1 -1
- package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
- package/lib/resolvers/content-tree/references/index.d.ts +4 -2
- package/lib/resolvers/content-tree/references/index.js +4 -1
- package/lib/resolvers/content-tree/references/index.js.map +1 -1
- package/lib/resolvers/content-tree/updateTreeWithReferenceIds.d.ts +3 -3
- package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js +2 -3
- package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js.map +1 -1
- package/lib/resolvers/content.d.ts +27 -18
- package/lib/resolvers/content.js +4 -2
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/image.d.ts +12 -12
- package/lib/resolvers/index.d.ts +89 -42
- package/lib/resolvers/leadFlourish.d.ts +2 -1
- package/lib/resolvers/leadFlourish.js +1 -0
- package/lib/resolvers/leadFlourish.js.map +1 -1
- package/lib/resolvers/person.d.ts +1 -1
- package/lib/resolvers/person.js +1 -1
- package/lib/resolvers/person.js.map +1 -1
- package/lib/resolvers/picture.d.ts +4 -4
- package/lib/resolvers/richText.d.ts +1 -1
- package/lib/resolvers/teaser.d.ts +1 -1
- package/lib/resolvers/topper.d.ts +3 -3
- package/package.json +1 -1
- package/queries/article.graphql +35 -13
- package/src/datasources/capi.test.ts +3 -3
- package/src/datasources/capi.ts +5 -3
- package/src/fixtures/capiObject.ts +4 -2
- package/src/fixtures/capiPerson.ts +1 -1
- package/src/generated/index.ts +321 -132
- package/src/helpers/decorateHeadshotUrl.ts +2 -2
- package/src/model/Byline.test.ts +136 -55
- package/src/model/Byline.ts +49 -39
- package/src/model/CapiResponse.test.ts +9 -25
- package/src/model/CapiResponse.ts +83 -56
- package/src/model/Clip.ts +1 -1
- package/src/model/Concept.ts +3 -3
- package/src/model/FlourishSource.ts +1 -1
- package/src/model/Image.test.ts +1 -1
- package/src/model/Image.ts +1 -1
- package/src/model/LeadFlourish.test.ts +1 -0
- package/src/model/Person.test.ts +11 -62
- package/src/model/Person.ts +47 -51
- package/src/model/Picture.test.ts +1 -1
- package/src/model/Picture.ts +1 -1
- package/src/model/Topper.test.ts +22 -18
- package/src/model/Topper.ts +10 -9
- package/src/model/__snapshots__/Byline.test.ts.snap +166 -27
- package/src/model/schemas/capi/article.ts +1 -0
- package/src/model/schemas/capi/audio.ts +1 -0
- package/src/model/schemas/capi/base-schema.ts +5 -4
- package/src/model/schemas/capi/content-package.ts +2 -0
- package/src/model/schemas/capi/internal-content.ts +45 -0
- package/src/model/schemas/capi/live-blog-package.ts +1 -0
- package/src/model/schemas/capi/placeholder.ts +1 -0
- package/src/model/schemas/capi/video.ts +2 -0
- package/src/resolvers/concept.ts +29 -12
- package/src/resolvers/content-tree/Workarounds.ts +39 -20
- package/src/resolvers/content-tree/references/Author.ts +18 -0
- package/src/resolvers/content-tree/references/ClipSet.ts +6 -8
- package/src/resolvers/content-tree/references/ImageSet.ts +1 -1
- package/src/resolvers/content-tree/references/ScrollyImage.ts +1 -1
- package/src/resolvers/content-tree/references/index.ts +6 -1
- package/src/resolvers/content-tree/updateTreeWithReferenceIds.ts +6 -8
- package/src/resolvers/content.ts +4 -2
- package/src/resolvers/leadFlourish.ts +1 -0
- package/src/resolvers/person.ts +1 -1
- package/src/types/n-display-metadata.d.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/clip.graphql +2 -2
- package/typedefs/concept.graphql +64 -2
- package/typedefs/content.graphql +63 -39
- package/typedefs/image.graphql +12 -12
- package/typedefs/leadFlourish.graphql +32 -0
- package/typedefs/person.graphql +2 -2
- package/typedefs/picture.graphql +6 -6
- package/typedefs/references/author.graphql +7 -0
- package/typedefs/references/clipSet.graphql +14 -2
- package/typedefs/references/tweet.graphql +1 -1
- package/typedefs/teaser.graphql +10 -10
- package/src/types/internal-content.d.ts +0 -55
- package/typedefs/leadFlouish.graphql +0 -29
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ContentTypeSchemas,
|
|
4
4
|
MainImage,
|
|
5
5
|
ClipSet,
|
|
6
|
-
} from '
|
|
6
|
+
} from './schemas/capi/internal-content'
|
|
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'
|
|
@@ -13,7 +13,7 @@ import type { QueryContext } from '..'
|
|
|
13
13
|
import sortBy from 'lodash.sortby'
|
|
14
14
|
|
|
15
15
|
import { CAPIImage } from './Image'
|
|
16
|
-
import { Concept } from './Concept'
|
|
16
|
+
import { Concept, predicates } from './Concept'
|
|
17
17
|
import { Person } from './Person'
|
|
18
18
|
import isError from '../helpers/isError'
|
|
19
19
|
import { uuidFromUrl } from '../helpers/metadata'
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '../resolvers/literal-union'
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
|
+
ContentAnnotationsArgs,
|
|
33
34
|
ContentPackageContainsArgs,
|
|
34
35
|
ContentUrlArgs,
|
|
35
36
|
Media,
|
|
@@ -161,18 +162,21 @@ export class CapiResponse {
|
|
|
161
162
|
packageContainer
|
|
162
163
|
)
|
|
163
164
|
}
|
|
165
|
+
|
|
164
166
|
body() {
|
|
165
167
|
if (this.type() === 'Video' && 'transcript' in this.capiData) {
|
|
166
168
|
return new RichText('transcript', this.capiData.transcript ?? null, this)
|
|
167
169
|
}
|
|
168
170
|
return new RichText('bodyXML', this.bodyXML(), this)
|
|
169
171
|
}
|
|
172
|
+
|
|
170
173
|
bodyXML(): string | null {
|
|
171
174
|
if ('bodyXML' in this.capiData) {
|
|
172
175
|
return this.capiData.bodyXML
|
|
173
176
|
}
|
|
174
177
|
return null
|
|
175
178
|
}
|
|
179
|
+
|
|
176
180
|
embeds(): (ImageSet | ClipSet)[] {
|
|
177
181
|
if ('embeds' in this.capiData && this.capiData.embeds)
|
|
178
182
|
return this.capiData.embeds
|
|
@@ -184,41 +188,43 @@ export class CapiResponse {
|
|
|
184
188
|
return null
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
byline({ vanity }: Partial<ContentUrlArgs>) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!bylineText) return null
|
|
191
|
-
const authorUrlMapping = this.getAuthorUrlMapping()
|
|
191
|
+
async byline({ vanity }: Partial<ContentUrlArgs>) {
|
|
192
|
+
if (!this.capiData.byline) return null
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
const byline = new Byline(
|
|
195
|
+
this.capiData.byline,
|
|
195
196
|
Boolean(vanity),
|
|
196
|
-
|
|
197
|
-
this
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
rawByline(): string | null {
|
|
201
|
-
if ('byline' in this.capiData && this.capiData.byline) {
|
|
202
|
-
return this.capiData.byline
|
|
203
|
-
}
|
|
197
|
+
this.annotations({ byPredicate: predicates.hasAuthor }),
|
|
198
|
+
this
|
|
199
|
+
)
|
|
204
200
|
|
|
205
|
-
return
|
|
201
|
+
return byline.buildBylineTree()
|
|
206
202
|
}
|
|
203
|
+
|
|
207
204
|
#rawAnnotations() {
|
|
208
205
|
return this.capiData.annotations
|
|
209
206
|
}
|
|
210
207
|
|
|
211
|
-
annotations(): Concept[] {
|
|
212
|
-
|
|
208
|
+
annotations({ byPredicate }: ContentAnnotationsArgs = {}): Concept[] {
|
|
209
|
+
const concepts = sortBy(this.#rawAnnotations(), ['predicate', 'id']).map(
|
|
213
210
|
(concept) => new Concept(concept, this.context)
|
|
214
211
|
)
|
|
212
|
+
|
|
213
|
+
if (byPredicate) {
|
|
214
|
+
return concepts.filter((concept) => concept.predicate() === byPredicate)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return concepts
|
|
215
218
|
}
|
|
219
|
+
|
|
216
220
|
types() {
|
|
217
221
|
return this.capiData.types
|
|
218
222
|
}
|
|
223
|
+
|
|
219
224
|
url({ relative, vanity }: Partial<ContentUrlArgs>) {
|
|
220
225
|
return relative ? this.relativeUrl(vanity) : this.absoluteUrl(vanity)
|
|
221
226
|
}
|
|
227
|
+
|
|
222
228
|
async absoluteUrl(vanity?: boolean | null) {
|
|
223
229
|
const url =
|
|
224
230
|
this.capiData.webUrl ??
|
|
@@ -243,43 +249,65 @@ export class CapiResponse {
|
|
|
243
249
|
}
|
|
244
250
|
return url
|
|
245
251
|
}
|
|
252
|
+
|
|
246
253
|
leadImages() {
|
|
247
254
|
if ('leadImages' in this.capiData) return this.capiData.leadImages
|
|
248
255
|
return []
|
|
249
256
|
}
|
|
257
|
+
|
|
250
258
|
topper() {
|
|
251
259
|
if ('topper' in this.capiData) return this.capiData.topper
|
|
252
260
|
return null
|
|
253
261
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
|
|
263
|
+
async authors(): Promise<Person[]> {
|
|
264
|
+
const authors = this.annotations({ byPredicate: predicates.hasAuthor })
|
|
265
|
+
|
|
266
|
+
return Promise.all(
|
|
267
|
+
authors.map((author) =>
|
|
268
|
+
this.context.dataSources.capi.getPerson(author.uuid())
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async primaryAuthor(): Promise<Person | null> {
|
|
274
|
+
const primaryAuthor = this.annotations({
|
|
275
|
+
byPredicate: predicates.hasAuthor,
|
|
276
|
+
})[0]
|
|
277
|
+
|
|
278
|
+
if (primaryAuthor) {
|
|
279
|
+
return this.context.dataSources.capi.getPerson(primaryAuthor.uuid())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return null
|
|
259
283
|
}
|
|
284
|
+
|
|
260
285
|
accessLevel(): LiteralUnionScalarValues<typeof AccessLevel> {
|
|
261
286
|
return this.capiData.accessLevel ?? 'subscribed'
|
|
262
287
|
}
|
|
288
|
+
|
|
263
289
|
editorialDesk() {
|
|
264
290
|
return this.capiData.editorialDesk ?? null
|
|
265
291
|
}
|
|
292
|
+
|
|
266
293
|
canBeSyndicated(): LiteralUnionScalarValues<typeof CanBeSyndicated> {
|
|
267
294
|
return this.capiData.canBeSyndicated
|
|
268
295
|
}
|
|
296
|
+
|
|
269
297
|
instantAlertConcept() {
|
|
270
|
-
const authors = this.
|
|
271
|
-
const authorConcept = authors.length === 1 ? authors[0] : null
|
|
298
|
+
const authors = this.annotations({ byPredicate: predicates.hasAuthor })
|
|
272
299
|
const displayConcept = this.getDisplayConcept()
|
|
273
300
|
|
|
274
|
-
if ((this.
|
|
275
|
-
return
|
|
301
|
+
if ((this.isColumn() || this.isAlphaville()) && authors[0]) {
|
|
302
|
+
return authors[0]
|
|
276
303
|
} else if (displayConcept?.isOrganisation()) {
|
|
277
304
|
return displayConcept
|
|
278
305
|
}
|
|
279
306
|
|
|
280
307
|
return null
|
|
281
308
|
}
|
|
282
|
-
|
|
309
|
+
|
|
310
|
+
originatingParty() {
|
|
283
311
|
return this.capiData.canBeDistributed === 'no' &&
|
|
284
312
|
this.annotations().some((annotation) => {
|
|
285
313
|
annotation.id() ===
|
|
@@ -288,10 +316,12 @@ export class CapiResponse {
|
|
|
288
316
|
? 'Reuters'
|
|
289
317
|
: 'FT'
|
|
290
318
|
}
|
|
319
|
+
|
|
291
320
|
summary() {
|
|
292
321
|
if ('summary' in this.capiData) return this.capiData.summary
|
|
293
322
|
return null
|
|
294
323
|
}
|
|
324
|
+
|
|
295
325
|
title() {
|
|
296
326
|
// CI-2038 HACK to remove "Comment:" prefix from live blog post titles
|
|
297
327
|
// as this is as the sole indicator that this is an opinion post
|
|
@@ -303,9 +333,11 @@ export class CapiResponse {
|
|
|
303
333
|
}
|
|
304
334
|
return this.capiData.title
|
|
305
335
|
}
|
|
336
|
+
|
|
306
337
|
id() {
|
|
307
338
|
return uuidFromUrl(this.capiData.id)
|
|
308
339
|
}
|
|
340
|
+
|
|
309
341
|
standfirst(): string | null {
|
|
310
342
|
if ('standfirst' in this.capiData && this.capiData.standfirst) {
|
|
311
343
|
return this.capiData.standfirst
|
|
@@ -313,18 +345,33 @@ export class CapiResponse {
|
|
|
313
345
|
|
|
314
346
|
return null
|
|
315
347
|
}
|
|
348
|
+
|
|
316
349
|
publishedDate() {
|
|
350
|
+
// WARNING:20240815:RB: "publishedDate" is only updated for editorial
|
|
351
|
+
// publishes where they explictly check a box to update it. If you need
|
|
352
|
+
// all editorial changes you may be after "lastModified" instead.
|
|
317
353
|
return this.capiData.publishedDate
|
|
318
354
|
}
|
|
355
|
+
|
|
319
356
|
publishedTimestamp() {
|
|
357
|
+
// WARNING:20240815:RB: "publishedTimestamp" is only updated for editorial
|
|
358
|
+
// publishes where they explictly check a box to update it. If you need
|
|
359
|
+
// all editorial changes you may be after "modifiedTimestamp" instead.
|
|
320
360
|
return new Date(this.capiData.publishedDate).getTime()
|
|
321
361
|
}
|
|
362
|
+
|
|
322
363
|
firstPublishedDate() {
|
|
323
364
|
return this.capiData.firstPublishedDate || this.capiData.publishedDate
|
|
324
365
|
}
|
|
366
|
+
|
|
367
|
+
modifiedTimestamp() {
|
|
368
|
+
return new Date(this.capiData.lastModified).getTime()
|
|
369
|
+
}
|
|
370
|
+
|
|
325
371
|
publishReference() {
|
|
326
|
-
return this.capiData.publishReference
|
|
372
|
+
return this.capiData.publishReference ?? null
|
|
327
373
|
}
|
|
374
|
+
|
|
328
375
|
alternativeTitle() {
|
|
329
376
|
return this.capiData.alternativeTitles ?? null
|
|
330
377
|
}
|
|
@@ -417,7 +464,7 @@ export class CapiResponse {
|
|
|
417
464
|
|
|
418
465
|
async metaPrefixText() {
|
|
419
466
|
const meta = await this.#teaserMetadata()
|
|
420
|
-
return meta.prefixText
|
|
467
|
+
return meta.prefixText ?? null
|
|
421
468
|
}
|
|
422
469
|
|
|
423
470
|
teaser() {
|
|
@@ -493,23 +540,6 @@ export class CapiResponse {
|
|
|
493
540
|
return cloneDeep(this.capiData)
|
|
494
541
|
}
|
|
495
542
|
|
|
496
|
-
#createStreamLink(streamUuid: string) {
|
|
497
|
-
return `https://www.ft.com/stream/${streamUuid}`
|
|
498
|
-
}
|
|
499
|
-
getAuthorUrlMapping() {
|
|
500
|
-
const authorAnnotations = this.getAuthors()
|
|
501
|
-
return authorAnnotations.reduce((mapping, author: Concept) => {
|
|
502
|
-
if (author.prefLabel()) {
|
|
503
|
-
const nameWithCurlyApostrophes = author.prefLabel().replace("'", '’')
|
|
504
|
-
mapping.set(
|
|
505
|
-
nameWithCurlyApostrophes,
|
|
506
|
-
this.#createStreamLink(author.uuid())
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
return mapping
|
|
510
|
-
}, new Map<string, string>())
|
|
511
|
-
}
|
|
512
|
-
|
|
513
543
|
getBrandConcept() {
|
|
514
544
|
return this.annotations().find((annotation: Concept) =>
|
|
515
545
|
annotation.isBrand()
|
|
@@ -546,14 +576,11 @@ export class CapiResponse {
|
|
|
546
576
|
)
|
|
547
577
|
}
|
|
548
578
|
|
|
549
|
-
getAuthors() {
|
|
550
|
-
return this.annotations().filter((annotation: Concept) =>
|
|
551
|
-
annotation.isAuthor()
|
|
552
|
-
)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
579
|
isColumn() {
|
|
556
|
-
return
|
|
580
|
+
return (
|
|
581
|
+
this.isOpinion() &&
|
|
582
|
+
this.annotations({ byPredicate: predicates.hasAuthor }).length === 1
|
|
583
|
+
)
|
|
557
584
|
}
|
|
558
585
|
|
|
559
586
|
isOpinion() {
|
package/src/model/Clip.ts
CHANGED
package/src/model/Concept.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Annotation } from '
|
|
1
|
+
import type { Annotation } from './schemas/capi/internal-content'
|
|
2
2
|
import type { QueryContext } from '..'
|
|
3
3
|
import { uuidFromUrl } from '../helpers/metadata'
|
|
4
4
|
import isError from '../helpers/isError'
|
|
@@ -6,7 +6,6 @@ import conceptIds from '@financial-times/n-concept-ids'
|
|
|
6
6
|
|
|
7
7
|
const CAPI_ID_PREFIX = /^https?:\/\/(?:www|api)\.ft\.com\/things?\//
|
|
8
8
|
const BASE_URL = 'https://www.ft.com/stream/'
|
|
9
|
-
const FT_URL = 'https://www.ft.com'
|
|
10
9
|
|
|
11
10
|
type URLArguments = {
|
|
12
11
|
vanity?: boolean | null
|
|
@@ -163,8 +162,9 @@ export class Concept {
|
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
if (args?.relative) {
|
|
166
|
-
|
|
165
|
+
return new URL(url).pathname
|
|
167
166
|
}
|
|
167
|
+
|
|
168
168
|
return url
|
|
169
169
|
}
|
|
170
170
|
}
|
package/src/model/Image.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CAPIImage } from './Image'
|
|
2
2
|
import { jest } from '@jest/globals'
|
|
3
3
|
import type { QueryContext } from '..'
|
|
4
|
-
import { Image } from '
|
|
4
|
+
import { Image } from './schemas/capi/internal-content'
|
|
5
5
|
import { OrigamiImageDataSource } from '../datasources/origami-image'
|
|
6
6
|
import { MAX_IMAGE_WIDTH } from '../helpers/imageService'
|
|
7
7
|
import type { ImageSource } from '../generated'
|
package/src/model/Image.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { DataSources } from '../datasources'
|
|
|
4
4
|
import type {
|
|
5
5
|
Image as InternalContentImage,
|
|
6
6
|
LeadImage,
|
|
7
|
-
} from '
|
|
7
|
+
} from './schemas/capi/internal-content'
|
|
8
8
|
import type { QueryContext } from '..'
|
|
9
9
|
import {
|
|
10
10
|
LiteralUnionScalarValues,
|
|
@@ -28,6 +28,7 @@ describe('LeadFlourish', () => {
|
|
|
28
28
|
clonedBase.topper = {
|
|
29
29
|
layout: 'flourish',
|
|
30
30
|
layoutWidth: 'full-width',
|
|
31
|
+
backgroundColour: 'paper',
|
|
31
32
|
}
|
|
32
33
|
const capiResponse = new CapiResponse(clonedBase, context)
|
|
33
34
|
leadFlourish = new LeadFlourish(capiResponse, context)
|
package/src/model/Person.test.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { Person } from './Person'
|
|
2
2
|
import { jest } from '@jest/globals'
|
|
3
3
|
import type { QueryContext } from '..'
|
|
4
|
-
import { Concept } from './Concept'
|
|
5
|
-
import { CapiResponse } from './CapiResponse'
|
|
6
|
-
import { baseCapiObject } from '../fixtures/capiObject'
|
|
7
4
|
import { capiPerson } from '../fixtures/capiPerson'
|
|
8
|
-
import cloneDeep from 'clone-deep'
|
|
9
5
|
import { URLManagementDataSource } from '../datasources/url-management'
|
|
10
6
|
import { CapiDataSource } from '../datasources/capi'
|
|
11
7
|
import { Logger } from '@dotcom-reliability-kit/logger'
|
|
8
|
+
import { CapiPerson } from './schemas/capi/internal-content'
|
|
12
9
|
|
|
13
10
|
const vanityMock = jest.fn<URLManagementDataSource['get']>()
|
|
14
11
|
const getPersonMock = jest.fn<CapiDataSource['getPerson']>()
|
|
@@ -22,86 +19,38 @@ const context = {
|
|
|
22
19
|
logger: new Logger(),
|
|
23
20
|
} as unknown as QueryContext
|
|
24
21
|
|
|
25
|
-
const topic = {
|
|
26
|
-
apiUrl: 'http://api.ft.com/things/6b32f2c1-da43-4e19-80b9-8aef4ab640d7',
|
|
27
|
-
directType: 'http://www.ft.com/ontology/Topic',
|
|
28
|
-
id: 'http://api.ft.com/things/6b32f2c1-da43-4e19-80b9-8aef4ab640d7',
|
|
29
|
-
predicate: 'http://www.ft.com/ontology/annotation/about',
|
|
30
|
-
prefLabel: 'Technology sector',
|
|
31
|
-
type: 'TOPIC',
|
|
32
|
-
types: [
|
|
33
|
-
'http://www.ft.com/ontology/core/Thing',
|
|
34
|
-
'http://www.ft.com/ontology/concept/Concept',
|
|
35
|
-
'http://www.ft.com/ontology/Topic',
|
|
36
|
-
],
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const author = {
|
|
40
|
-
apiUrl: 'http://api.ft.com/people/4076f4fd-723b-4ce5-9934-fb29416554fa',
|
|
41
|
-
directType: 'http://www.ft.com/ontology/person/Person',
|
|
42
|
-
id: 'http://api.ft.com/things/4076f4fd-723b-4ce5-9934-fb29416554fa',
|
|
43
|
-
predicate: 'http://www.ft.com/ontology/annotation/hasAuthor',
|
|
44
|
-
prefLabel: 'Robert Shrimsley',
|
|
45
|
-
type: 'PERSON',
|
|
46
|
-
types: [
|
|
47
|
-
'http://www.ft.com/ontology/core/Thing',
|
|
48
|
-
'http://www.ft.com/ontology/concept/Concept',
|
|
49
|
-
'http://www.ft.com/ontology/person/Person',
|
|
50
|
-
],
|
|
51
|
-
}
|
|
52
22
|
describe('Person model', () => {
|
|
53
|
-
let clonedBase: typeof baseCapiObject
|
|
54
|
-
let capiResponse: CapiResponse
|
|
55
|
-
let topicConcept: Concept
|
|
56
|
-
let authorConcept: Concept
|
|
57
|
-
|
|
58
23
|
beforeEach(() => {
|
|
59
24
|
jest.resetAllMocks()
|
|
60
|
-
|
|
61
|
-
clonedBase = cloneDeep(baseCapiObject)
|
|
62
|
-
capiResponse = new CapiResponse(clonedBase, context)
|
|
63
|
-
topicConcept = new Concept(topic, context)
|
|
64
|
-
authorConcept = new Concept(author, context)
|
|
65
25
|
})
|
|
66
26
|
|
|
67
|
-
describe('headshot
|
|
68
|
-
it('returns null if the concept is not for an author', async () => {
|
|
69
|
-
const model = new Person(topicConcept, capiResponse, context)
|
|
70
|
-
expect(await model.headshot()).toEqual(null)
|
|
71
|
-
})
|
|
72
|
-
|
|
27
|
+
describe('headshot', () => {
|
|
73
28
|
it('returns the headshot for an author', async () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(await model.headshot()).toEqual(
|
|
29
|
+
const model = new Person(capiPerson as CapiPerson, context)
|
|
30
|
+
expect(await model.headshot({})).toEqual(
|
|
77
31
|
'https://www.ft.com/__origami/service/image/v2/images/raw/https%3A%2F%2Fd1e00ek4ebabms.cloudfront.net%2Fproduction%2Fuploaded-files%2Ffthead-v1_robert-shrimsley-cc467908-15d6-474d-94d8-171594ceabb9.png?source=image-test&fit=scale-down&quality=highest&width=150&dpr=1'
|
|
78
32
|
)
|
|
79
33
|
})
|
|
80
34
|
|
|
81
35
|
it('can resize headshot and provide high resolution version', async () => {
|
|
82
|
-
|
|
83
|
-
const model = new Person(authorConcept, capiResponse, context)
|
|
36
|
+
const model = new Person(capiPerson as CapiPerson, context)
|
|
84
37
|
expect(await model.headshot({ width: 300, dpr: 2 })).toEqual(
|
|
85
38
|
'https://www.ft.com/__origami/service/image/v2/images/raw/https%3A%2F%2Fd1e00ek4ebabms.cloudfront.net%2Fproduction%2Fuploaded-files%2Ffthead-v1_robert-shrimsley-cc467908-15d6-474d-94d8-171594ceabb9.png?source=image-test&fit=scale-down&quality=highest&width=300&dpr=2'
|
|
86
39
|
)
|
|
87
40
|
})
|
|
88
41
|
|
|
89
|
-
it('returns null if there is an error getting the person data', async () => {
|
|
90
|
-
getPersonMock.mockRejectedValue(new Error('error getting person'))
|
|
91
|
-
const model = new Person(authorConcept, capiResponse, context)
|
|
92
|
-
expect(await model.headshot({ width: 300, dpr: 2 })).toEqual(null)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
42
|
it("returns null if the author doesn't have a headshot", async () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
43
|
+
const model = new Person(
|
|
44
|
+
{ ...capiPerson, _imageUrl: undefined } as CapiPerson,
|
|
45
|
+
context
|
|
46
|
+
)
|
|
47
|
+
expect(await model.headshot({})).toEqual(null)
|
|
99
48
|
})
|
|
100
49
|
})
|
|
101
50
|
|
|
102
51
|
describe('streamPage()', () => {
|
|
103
52
|
it('returns the stream URL', () => {
|
|
104
|
-
const model = new Person(
|
|
53
|
+
const model = new Person(capiPerson as CapiPerson, context)
|
|
105
54
|
expect(model.streamPage()).toEqual(
|
|
106
55
|
'https://www.ft.com/stream/4076f4fd-723b-4ce5-9934-fb29416554fa'
|
|
107
56
|
)
|
package/src/model/Person.ts
CHANGED
|
@@ -1,37 +1,54 @@
|
|
|
1
1
|
import type { QueryContext } from '..'
|
|
2
|
-
import { Concept } from './Concept'
|
|
3
|
-
import { CapiResponse } from './CapiResponse'
|
|
4
2
|
import imageServiceUrl from '../helpers/imageService'
|
|
5
|
-
import
|
|
6
|
-
import decorateHeadshotUrl, { UUID_REGEX } from '../helpers/decorateHeadshotUrl'
|
|
3
|
+
import decorateHeadshotUrl from '../helpers/decorateHeadshotUrl'
|
|
7
4
|
import { OperationalError } from '@dotcom-reliability-kit/errors'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const predicates = {
|
|
15
|
-
hasAuthor: 'http://www.ft.com/ontology/annotation/hasAuthor',
|
|
16
|
-
} as const
|
|
5
|
+
import { CapiPerson as CapiPersonSchema } from './schemas/capi/base-schema'
|
|
6
|
+
import { CapiPerson } from './schemas/capi/internal-content'
|
|
7
|
+
import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
|
|
8
|
+
import { PersonHeadshotArgs } from '../generated'
|
|
9
|
+
import { uuidFromUrl } from '../helpers/metadata'
|
|
17
10
|
|
|
18
11
|
export class Person {
|
|
19
12
|
#systemCode: string
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
static fromJson(data: unknown, context: QueryContext): Person {
|
|
15
|
+
const result = CapiPersonSchema.safeParse(data)
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
context.logger.warn({
|
|
18
|
+
event: 'RECOVERABLE_ERROR',
|
|
19
|
+
error: new OperationalError({
|
|
20
|
+
message:
|
|
21
|
+
'The data received from the CAPI data source does not match our data source schema. It is likely that our schema will require updating to handle all possible responses from CAPI.',
|
|
22
|
+
code: 'CAPI_SCHEMA_VALIDATION_FAILURE',
|
|
23
|
+
schemaError: flattenFormattedZodIssues(result.error.format(), data),
|
|
24
|
+
contentId: (data as { id?: string }).id,
|
|
25
|
+
contentType: 'Person',
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
28
|
+
context.metrics?.count(
|
|
29
|
+
`graphql.datasource.CapiDataSource.Person.validation.failure.count`,
|
|
30
|
+
1
|
|
31
|
+
)
|
|
32
|
+
} else {
|
|
33
|
+
context.metrics?.count(
|
|
34
|
+
`graphql.datasource.CapiDataSource.Person.validation.success.count`,
|
|
35
|
+
1
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Person(data as CapiPerson, context)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
constructor(private person: CapiPerson, private context: QueryContext) {
|
|
26
43
|
this.#systemCode = context.systemCode ?? 'cp-content-pipeline'
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
prefLabel() {
|
|
30
|
-
return this.
|
|
47
|
+
return this.person.prefLabel
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
uuid() {
|
|
34
|
-
return this.
|
|
51
|
+
return uuidFromUrl(this.person.id)
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
streamPage() {
|
|
@@ -40,40 +57,19 @@ export class Person {
|
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
apiUrl() {
|
|
43
|
-
return this.
|
|
60
|
+
return this.person.apiUrl
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
63
|
+
headshot(args: PersonHeadshotArgs) {
|
|
64
|
+
const url = decorateHeadshotUrl(this.person)
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return url
|
|
58
|
-
? imageServiceUrl({
|
|
59
|
-
url,
|
|
60
|
-
systemCode: this.#systemCode,
|
|
61
|
-
width: args?.width || 150,
|
|
62
|
-
dpr: args?.dpr ?? undefined,
|
|
63
|
-
})
|
|
64
|
-
: null
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (isError(error)) {
|
|
67
|
-
this.context.logger.warn({
|
|
68
|
-
event: 'RECOVERABLE_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
|
-
}),
|
|
66
|
+
return url
|
|
67
|
+
? imageServiceUrl({
|
|
68
|
+
url,
|
|
69
|
+
systemCode: this.#systemCode,
|
|
70
|
+
width: args.width || 150,
|
|
71
|
+
dpr: args.dpr ?? undefined,
|
|
74
72
|
})
|
|
75
|
-
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
73
|
+
: null
|
|
78
74
|
}
|
|
79
75
|
}
|
package/src/model/Picture.ts
CHANGED