@financial-times/cp-content-pipeline-schema 1.3.0 → 1.3.2
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 +16 -0
- package/lib/generated/index.d.ts +12 -3
- package/lib/helpers/decorateHeadshotUrl.d.ts +3 -0
- package/lib/helpers/decorateHeadshotUrl.js +39 -0
- package/lib/helpers/decorateHeadshotUrl.js.map +1 -0
- package/lib/model/Concept.js +44 -14
- package/lib/model/Concept.js.map +1 -1
- package/lib/model/Concept.test.js +2 -2
- package/lib/model/Concept.test.js.map +1 -1
- package/lib/model/Picture.js +2 -0
- package/lib/model/Picture.js.map +1 -1
- package/lib/model/Topper.d.ts +1 -1
- package/lib/model/Topper.js +18 -11
- package/lib/model/Topper.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +5 -0
- package/lib/model/schemas/capi/audio.d.ts +5 -0
- package/lib/model/schemas/capi/base-schema.d.ts +32 -0
- package/lib/model/schemas/capi/base-schema.js +1 -0
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +5 -0
- package/lib/model/schemas/capi/live-blog-package.d.ts +5 -0
- package/lib/model/schemas/capi/placeholder.d.ts +5 -0
- package/lib/resolvers/content-tree/references/ImageSet.js +1 -1
- package/lib/resolvers/content-tree/references/ImageSet.js.map +1 -1
- package/lib/resolvers/content-tree/tagMappings.js +2 -2
- package/lib/resolvers/index.d.ts +3 -2
- package/lib/resolvers/topper.d.ts +3 -2
- package/lib/resolvers/topper.js +2 -3
- package/lib/resolvers/topper.js.map +1 -1
- package/package.json +1 -1
- package/src/generated/index.ts +9 -3
- package/src/helpers/decorateHeadshotUrl.ts +47 -0
- package/src/model/Concept.test.ts +2 -4
- package/src/model/Concept.ts +22 -17
- package/src/model/Picture.ts +3 -0
- package/src/model/Topper.ts +26 -12
- package/src/model/schemas/capi/base-schema.ts +1 -0
- package/src/resolvers/content-tree/references/ImageSet.ts +1 -1
- package/src/resolvers/content-tree/tagMappings.ts +2 -2
- package/src/resolvers/topper.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/topper.graphql +3 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CapiPerson } from '../types/internal-content'
|
|
2
|
+
|
|
3
|
+
export const UUID_REGEX =
|
|
4
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-9][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i
|
|
5
|
+
|
|
6
|
+
const IMAGESET_REGEX = /fthead(?:-v\d)?\:[^?]+/
|
|
7
|
+
|
|
8
|
+
export default function decorateHeadshotUrl(person: CapiPerson) {
|
|
9
|
+
const url = person?._imageUrl
|
|
10
|
+
|
|
11
|
+
if (!url) return null
|
|
12
|
+
|
|
13
|
+
if (isCloudfrontUrl(url)) {
|
|
14
|
+
return url
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ftcmsMatch = executeFtcmsMatch(url)
|
|
18
|
+
|
|
19
|
+
if (ftcmsMatch) {
|
|
20
|
+
return `ftcms:${ftcmsMatch.pop()}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ftheadSchemeMatch = executeFtheadMatch(url)
|
|
24
|
+
|
|
25
|
+
if (ftheadSchemeMatch) {
|
|
26
|
+
return ftheadSchemeMatch.pop()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCloudfrontUrl(url: string) {
|
|
31
|
+
const AWS_CLOUDFRONT_HOSTNAME = 'd1e00ek4ebabms.cloudfront.net'
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const urlHostname = new URL(url).hostname
|
|
35
|
+
return urlHostname && urlHostname === AWS_CLOUDFRONT_HOSTNAME
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function executeFtcmsMatch(url: string) {
|
|
42
|
+
return UUID_REGEX.exec(url)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function executeFtheadMatch(url: string) {
|
|
46
|
+
return IMAGESET_REGEX.exec(url)
|
|
47
|
+
}
|
|
@@ -88,11 +88,9 @@ describe('Concept model', () => {
|
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
describe('headshot()', () => {
|
|
91
|
-
it('
|
|
91
|
+
it('returns null if the concept is not for an author', async () => {
|
|
92
92
|
const model = new Concept(topic, context)
|
|
93
|
-
await
|
|
94
|
-
'Cannot request headshot for a non-Author concept'
|
|
95
|
-
)
|
|
93
|
+
expect(await model.headshot()).toEqual(null)
|
|
96
94
|
})
|
|
97
95
|
|
|
98
96
|
it('returns the headshot for an author', async () => {
|
package/src/model/Concept.ts
CHANGED
|
@@ -5,6 +5,7 @@ import imageServiceUrl from '../helpers/imageService'
|
|
|
5
5
|
import isError from '../helpers/isError'
|
|
6
6
|
import { logRecoverableError } from '@dotcom-reliability-kit/log-error'
|
|
7
7
|
import conceptIds from '@financial-times/n-concept-ids'
|
|
8
|
+
import decorateHeadshotUrl, { UUID_REGEX } from '../helpers/decorateHeadshotUrl'
|
|
8
9
|
|
|
9
10
|
const CAPI_ID_PREFIX = /^https?:\/\/(?:www|api)\.ft\.com\/things?\//
|
|
10
11
|
const BASE_URL = 'https://www.ft.com/stream/'
|
|
@@ -165,26 +166,30 @@ export class Concept {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
async headshot(args?: HeadshotArguments) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const uuid = this.apiUrl().match(UUID_REGEX)?.[0]
|
|
170
|
+
|
|
171
|
+
if (!this.isAuthor() || !uuid) return null
|
|
171
172
|
|
|
172
173
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
const person = await this.context.dataSources.capi.getPerson(uuid)
|
|
175
|
+
const url = decorateHeadshotUrl(person)
|
|
176
|
+
|
|
177
|
+
return url
|
|
178
|
+
? imageServiceUrl({
|
|
179
|
+
url,
|
|
180
|
+
systemCode: this.#systemCode,
|
|
181
|
+
width: args?.width || 150,
|
|
182
|
+
dpr: args?.dpr ?? undefined,
|
|
183
|
+
})
|
|
184
|
+
: null
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isError(error)) {
|
|
187
|
+
logRecoverableError({
|
|
188
|
+
logger: this.context.logger,
|
|
189
|
+
error,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
186
192
|
return null
|
|
187
|
-
//TODO: log operational error here
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
}
|
package/src/model/Picture.ts
CHANGED
|
@@ -30,6 +30,9 @@ export class Picture {
|
|
|
30
30
|
const images = this.images()
|
|
31
31
|
const standard =
|
|
32
32
|
images.find((image) => image.format() === 'standard-inline') ?? images[0]
|
|
33
|
+
|
|
34
|
+
// this assert will never fail, as a Picture model won't be constructed if
|
|
35
|
+
// the imageset is empty. it's here for Typescript's benefit
|
|
33
36
|
assertDefined(
|
|
34
37
|
standard,
|
|
35
38
|
'Picture must contain at least one image to display'
|
package/src/model/Topper.ts
CHANGED
|
@@ -132,10 +132,6 @@ export class Topper {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
backgroundColour(): TopperBackgroundColourValues {
|
|
135
|
-
if (this.capiResponse.isAlphaville()) {
|
|
136
|
-
return 'matisse'
|
|
137
|
-
}
|
|
138
|
-
|
|
139
135
|
if (
|
|
140
136
|
this.capiResponse.type() === 'ContentPackage' &&
|
|
141
137
|
this.capiResponse.design()
|
|
@@ -158,6 +154,10 @@ export class Topper {
|
|
|
158
154
|
return this.capiResponse.isContainedInPackage() ? 'wheat' : 'sky'
|
|
159
155
|
}
|
|
160
156
|
|
|
157
|
+
if (this.capiResponse.isAlphaville()) {
|
|
158
|
+
return 'matisse'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
161
|
if (type === 'PodcastTopper') {
|
|
162
162
|
return 'slate'
|
|
163
163
|
}
|
|
@@ -254,11 +254,11 @@ export class Topper {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
columnist() {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
const authors = this.capiResponse.getAuthors()
|
|
258
|
+
const isOpinionOrColumn =
|
|
259
|
+
this.type() === 'OpinionTopper' || this.capiResponse.isColumn()
|
|
260
260
|
|
|
261
|
-
return null
|
|
261
|
+
return isOpinionOrColumn && authors.length ? authors[0] : null
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
brandConcept() {
|
|
@@ -279,14 +279,28 @@ export class Topper {
|
|
|
279
279
|
return this.capiResponse.design()
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
headshot(args: HeadshotArguments) {
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
async headshot(args: HeadshotArguments) {
|
|
283
|
+
let headshotUrl: string | undefined
|
|
284
|
+
|
|
285
|
+
if (this.capiResponse.isPodcast()) {
|
|
286
|
+
headshotUrl = this.capiResponse.mainImage()?.url()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.capiResponse.isOpinion()) {
|
|
290
|
+
const authors = this.capiResponse.getAuthors()
|
|
291
|
+
const headshotUrls: (string | null)[] = await Promise.all(
|
|
292
|
+
authors.map(async (author) => await author.headshot(args))
|
|
293
|
+
).then((res) => res.filter(Boolean))
|
|
294
|
+
|
|
295
|
+
headshotUrl = headshotUrls[0] ?? undefined
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!headshotUrl) {
|
|
285
299
|
return null
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
return imageServiceUrl({
|
|
289
|
-
url:
|
|
303
|
+
url: headshotUrl,
|
|
290
304
|
systemCode: this.#systemCode,
|
|
291
305
|
width: args.width ?? undefined,
|
|
292
306
|
dpr: args.dpr ?? undefined,
|
|
@@ -14,7 +14,7 @@ export const ImageSet = {
|
|
|
14
14
|
uuidFromUrl(embed.id) === uuidFromUrl(parent.reference.id)
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
return imageSet && imageSet.members
|
|
17
|
+
return imageSet && imageSet.members && imageSet.members.length > 0
|
|
18
18
|
? new Picture(imageSet, isLiveBlog, context)
|
|
19
19
|
: null
|
|
20
20
|
},
|
|
@@ -306,7 +306,7 @@ const commonTagMappings: TagMappings = {
|
|
|
306
306
|
'.n-content-layout img': ($el) => ({
|
|
307
307
|
type: 'layout-image',
|
|
308
308
|
id: $el.attr('src') || '',
|
|
309
|
-
caption: $el.attr('
|
|
309
|
+
caption: $el.attr('longdesc') || '',
|
|
310
310
|
alt: $el.attr('alt') || '',
|
|
311
311
|
credit: $el.attr('data-copyright') || '',
|
|
312
312
|
}),
|
|
@@ -479,7 +479,7 @@ const commonTagMappings: TagMappings = {
|
|
|
479
479
|
// HACK needs to come after LayoutImage. TODO sort by selector specificity
|
|
480
480
|
img: ($el) => ({
|
|
481
481
|
type: 'raw-image',
|
|
482
|
-
caption: $el.attr('
|
|
482
|
+
caption: $el.attr('longdesc') || '',
|
|
483
483
|
credit: $el.attr('data-copyright') || '',
|
|
484
484
|
alt: $el.attr('alt') || '',
|
|
485
485
|
url: $el.attr('src') || '',
|
package/src/resolvers/topper.ts
CHANGED
|
@@ -16,6 +16,7 @@ const resolvers = {
|
|
|
16
16
|
backgroundColour: (topper) => topper.backgroundColour(),
|
|
17
17
|
displayConcept: (topper) => topper.displayConcept(),
|
|
18
18
|
followButtonVariant: (topper) => topper.followButtonVariant(),
|
|
19
|
+
genreConcept: (topper) => topper.genreConcept(),
|
|
19
20
|
},
|
|
20
21
|
|
|
21
22
|
TopperWithImages: {
|
|
@@ -38,9 +39,7 @@ const resolvers = {
|
|
|
38
39
|
},
|
|
39
40
|
|
|
40
41
|
OpinionTopper: {
|
|
41
|
-
|
|
42
|
-
return topper.columnist()?.headshot(args) || null
|
|
43
|
-
},
|
|
42
|
+
headshot: (topper, args) => topper.headshot(args),
|
|
44
43
|
columnist: (topper) => topper.columnist(),
|
|
45
44
|
},
|
|
46
45
|
|