@financial-times/cp-content-pipeline-schema 1.3.0 → 1.3.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 (40) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/lib/generated/index.d.ts +12 -3
  3. package/lib/helpers/decorateHeadshotUrl.d.ts +3 -0
  4. package/lib/helpers/decorateHeadshotUrl.js +39 -0
  5. package/lib/helpers/decorateHeadshotUrl.js.map +1 -0
  6. package/lib/model/Concept.js +44 -14
  7. package/lib/model/Concept.js.map +1 -1
  8. package/lib/model/Concept.test.js +2 -2
  9. package/lib/model/Concept.test.js.map +1 -1
  10. package/lib/model/Picture.js +2 -0
  11. package/lib/model/Picture.js.map +1 -1
  12. package/lib/model/Topper.d.ts +1 -1
  13. package/lib/model/Topper.js +18 -11
  14. package/lib/model/Topper.js.map +1 -1
  15. package/lib/model/schemas/capi/article.d.ts +5 -0
  16. package/lib/model/schemas/capi/audio.d.ts +5 -0
  17. package/lib/model/schemas/capi/base-schema.d.ts +32 -0
  18. package/lib/model/schemas/capi/base-schema.js +1 -0
  19. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  20. package/lib/model/schemas/capi/content-package.d.ts +5 -0
  21. package/lib/model/schemas/capi/live-blog-package.d.ts +5 -0
  22. package/lib/model/schemas/capi/placeholder.d.ts +5 -0
  23. package/lib/resolvers/content-tree/references/ImageSet.js +1 -1
  24. package/lib/resolvers/content-tree/references/ImageSet.js.map +1 -1
  25. package/lib/resolvers/index.d.ts +3 -2
  26. package/lib/resolvers/topper.d.ts +3 -2
  27. package/lib/resolvers/topper.js +2 -3
  28. package/lib/resolvers/topper.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/generated/index.ts +9 -3
  31. package/src/helpers/decorateHeadshotUrl.ts +47 -0
  32. package/src/model/Concept.test.ts +2 -4
  33. package/src/model/Concept.ts +22 -17
  34. package/src/model/Picture.ts +3 -0
  35. package/src/model/Topper.ts +26 -12
  36. package/src/model/schemas/capi/base-schema.ts +1 -0
  37. package/src/resolvers/content-tree/references/ImageSet.ts +1 -1
  38. package/src/resolvers/topper.ts +2 -3
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/typedefs/topper.graphql +3 -0
@@ -88,11 +88,9 @@ describe('Concept model', () => {
88
88
  })
89
89
 
90
90
  describe('headshot()', () => {
91
- it('throws an error if the concept is not for an author', async () => {
91
+ it('returns null if the concept is not for an author', async () => {
92
92
  const model = new Concept(topic, context)
93
- await expect(model.headshot()).rejects.toThrowError(
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 () => {
@@ -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
- if (!this.isAuthor()) {
169
- throw new Error('Cannot request headshot for a non-Author concept')
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 peopleData = await this.context.dataSources.capi.getPerson(
174
- this.uuid()
175
- )
176
-
177
- if (!peopleData || !peopleData._imageUrl) return null
178
-
179
- return imageServiceUrl({
180
- url: peopleData._imageUrl,
181
- systemCode: this.#systemCode,
182
- width: args?.width || 150,
183
- dpr: args?.dpr ?? undefined,
184
- })
185
- } catch (err) {
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
  }
@@ -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'
@@ -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
- if (this.capiResponse.isColumn()) {
258
- return this.capiResponse.getAuthors()[0] ?? null
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
- const headshot = this.capiResponse.mainImage()
284
- if (!headshot) {
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: headshot.url(),
303
+ url: headshotUrl,
290
304
  systemCode: this.#systemCode,
291
305
  width: args.width ?? undefined,
292
306
  dpr: args.dpr ?? undefined,
@@ -7,6 +7,7 @@ const Concept = z.object({
7
7
  prefLabel: z.string(),
8
8
  type: z.string().optional(),
9
9
  types: z.string().array(),
10
+ headshot: z.string().optional(),
10
11
  })
11
12
 
12
13
  export const Annotation = Concept.extend({
@@ -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
  },
@@ -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
- async headshot(topper, args) {
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