@financial-times/cp-content-pipeline-schema 2.5.1 → 2.5.3

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 (136) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/datasources/instrumented.js.map +1 -1
  3. package/lib/generated/index.d.ts +668 -907
  4. package/lib/helpers/imageService.d.ts +2 -1
  5. package/lib/model/Byline.js +1 -1
  6. package/lib/model/Byline.js.map +1 -1
  7. package/lib/model/CapiResponse.d.ts +14 -4
  8. package/lib/model/CapiResponse.js +19 -4
  9. package/lib/model/CapiResponse.js.map +1 -1
  10. package/lib/model/Clip.d.ts +3 -2
  11. package/lib/model/Clip.js +7 -5
  12. package/lib/model/Clip.js.map +1 -1
  13. package/lib/model/Concept.d.ts +3 -13
  14. package/lib/model/Concept.js +10 -10
  15. package/lib/model/Concept.js.map +1 -1
  16. package/lib/model/Image.d.ts +1 -5
  17. package/lib/model/Image.js.map +1 -1
  18. package/lib/model/Image.test.js.map +1 -1
  19. package/lib/model/RichText.d.ts +1 -1
  20. package/lib/model/RichText.js.map +1 -1
  21. package/lib/model/Topper.d.ts +4 -7
  22. package/lib/model/Topper.js +3 -3
  23. package/lib/model/Topper.js.map +1 -1
  24. package/lib/model/schemas/capi/base-schema.d.ts +0 -119
  25. package/lib/model/schemas/capi/base-schema.js +7 -7
  26. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  27. package/lib/model/schemas/capi/content-package.d.ts +18 -0
  28. package/lib/model/schemas/capi/content-package.js +1 -0
  29. package/lib/model/schemas/capi/content-package.js.map +1 -1
  30. package/lib/resolvers/clip.d.ts +19 -1
  31. package/lib/resolvers/clip.js +10 -0
  32. package/lib/resolvers/clip.js.map +1 -1
  33. package/lib/resolvers/content-tree/nodePredicates.d.ts +0 -5
  34. package/lib/resolvers/content-tree/nodePredicates.js +1 -26
  35. package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
  36. package/lib/resolvers/content-tree/references/ClipSet.d.ts +13 -0
  37. package/lib/resolvers/content-tree/references/ClipSet.js +12 -1
  38. package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -1
  39. package/lib/resolvers/content-tree/references/Flourish.d.ts +6 -0
  40. package/lib/resolvers/content-tree/references/Flourish.js +15 -1
  41. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  42. package/lib/resolvers/content-tree/references/ImageSet.d.ts +1 -0
  43. package/lib/resolvers/content-tree/references/ImageSet.js +3 -0
  44. package/lib/resolvers/content-tree/references/ImageSet.js.map +1 -1
  45. package/lib/resolvers/content-tree/references/LayoutImage.d.ts +1 -0
  46. package/lib/resolvers/content-tree/references/LayoutImage.js +3 -0
  47. package/lib/resolvers/content-tree/references/LayoutImage.js.map +1 -1
  48. package/lib/resolvers/content-tree/references/RawImage.d.ts +1 -0
  49. package/lib/resolvers/content-tree/references/RawImage.js +3 -0
  50. package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
  51. package/lib/resolvers/content-tree/references/Recommended.d.ts +1 -0
  52. package/lib/resolvers/content-tree/references/Recommended.js +3 -0
  53. package/lib/resolvers/content-tree/references/Recommended.js.map +1 -1
  54. package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
  55. package/lib/resolvers/content-tree/references/Reference.js +1 -3
  56. package/lib/resolvers/content-tree/references/Reference.js.map +1 -1
  57. package/lib/resolvers/content-tree/references/ScrollyImage.d.ts +1 -0
  58. package/lib/resolvers/content-tree/references/ScrollyImage.js +3 -0
  59. package/lib/resolvers/content-tree/references/ScrollyImage.js.map +1 -1
  60. package/lib/resolvers/content-tree/references/Tweet.d.ts +1 -0
  61. package/lib/resolvers/content-tree/references/Tweet.js +3 -0
  62. package/lib/resolvers/content-tree/references/Tweet.js.map +1 -1
  63. package/lib/resolvers/content-tree/references/Video.d.ts +2 -1
  64. package/lib/resolvers/content-tree/references/Video.js +4 -3
  65. package/lib/resolvers/content-tree/references/Video.js.map +1 -1
  66. package/lib/resolvers/content-tree/references/index.d.ts +5 -1
  67. package/lib/resolvers/content-tree/references/index.js +4 -0
  68. package/lib/resolvers/content-tree/references/index.js.map +1 -1
  69. package/lib/resolvers/content-tree/tagMappings.test.js +6 -9
  70. package/lib/resolvers/content-tree/tagMappings.test.js.map +1 -1
  71. package/lib/resolvers/content.d.ts +242 -33
  72. package/lib/resolvers/content.js +66 -31
  73. package/lib/resolvers/content.js.map +1 -1
  74. package/lib/resolvers/image.d.ts +142 -12
  75. package/lib/resolvers/image.js +45 -10
  76. package/lib/resolvers/image.js.map +1 -1
  77. package/lib/resolvers/index.d.ts +597 -61
  78. package/lib/resolvers/picture.d.ts +41 -8
  79. package/lib/resolvers/picture.js +19 -21
  80. package/lib/resolvers/picture.js.map +1 -1
  81. package/lib/resolvers/richText.d.ts +14 -2
  82. package/lib/resolvers/richText.js +8 -6
  83. package/lib/resolvers/richText.js.map +1 -1
  84. package/lib/resolvers/scalars.d.ts +2 -1
  85. package/lib/resolvers/scalars.js +5 -3
  86. package/lib/resolvers/scalars.js.map +1 -1
  87. package/lib/resolvers/teaser.d.ts +5 -0
  88. package/lib/resolvers/teaser.js +6 -1
  89. package/lib/resolvers/teaser.js.map +1 -1
  90. package/lib/resolvers/topper.d.ts +127 -7
  91. package/lib/resolvers/topper.js +64 -6
  92. package/lib/resolvers/topper.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/datasources/instrumented.ts +0 -1
  95. package/src/generated/index.ts +668 -668
  96. package/src/helpers/imageService.ts +1 -1
  97. package/src/model/Byline.ts +1 -1
  98. package/src/model/CapiResponse.ts +32 -7
  99. package/src/model/Clip.ts +11 -8
  100. package/src/model/Concept.ts +5 -9
  101. package/src/model/Image.test.ts +1 -6
  102. package/src/model/Image.ts +1 -6
  103. package/src/model/RichText.ts +1 -1
  104. package/src/model/Topper.ts +5 -9
  105. package/src/model/schemas/capi/base-schema.ts +3 -3
  106. package/src/model/schemas/capi/content-package.ts +1 -0
  107. package/src/resolvers/clip.ts +13 -2
  108. package/src/resolvers/content-tree/nodePredicates.ts +0 -42
  109. package/src/resolvers/content-tree/references/ClipSet.ts +22 -2
  110. package/src/resolvers/content-tree/references/Flourish.ts +20 -1
  111. package/src/resolvers/content-tree/references/ImageSet.ts +4 -0
  112. package/src/resolvers/content-tree/references/LayoutImage.ts +4 -0
  113. package/src/resolvers/content-tree/references/RawImage.ts +4 -0
  114. package/src/resolvers/content-tree/references/Recommended.ts +4 -0
  115. package/src/resolvers/content-tree/references/Reference.ts +1 -3
  116. package/src/resolvers/content-tree/references/ScrollyImage.ts +4 -0
  117. package/src/resolvers/content-tree/references/Tweet.ts +4 -0
  118. package/src/resolvers/content-tree/references/Video.ts +5 -3
  119. package/src/resolvers/content-tree/references/index.ts +13 -4
  120. package/src/resolvers/content-tree/tagMappings.test.ts +6 -10
  121. package/src/resolvers/content.ts +88 -38
  122. package/src/resolvers/image.ts +75 -5
  123. package/src/resolvers/picture.ts +35 -23
  124. package/src/resolvers/richText.ts +13 -8
  125. package/src/resolvers/scalars.ts +3 -1
  126. package/src/resolvers/teaser.ts +6 -2
  127. package/src/resolvers/topper.ts +86 -6
  128. package/tsconfig.tsbuildinfo +1 -1
  129. package/lib/fixtures/clipSet.d.ts +0 -2
  130. package/lib/fixtures/clipSet.js +0 -70
  131. package/lib/fixtures/clipSet.js.map +0 -1
  132. package/lib/helpers/kebabCaseToPascalCase.d.ts +0 -1
  133. package/lib/helpers/kebabCaseToPascalCase.js +0 -7
  134. package/lib/helpers/kebabCaseToPascalCase.js.map +0 -1
  135. package/src/fixtures/clipSet.ts +0 -72
  136. package/src/helpers/kebabCaseToPascalCase.ts +0 -5
@@ -2,7 +2,7 @@ const DEFAULT_IMAGE_SERVICE_WIDTH = 700
2
2
  const DEFAULT_IMAGE_SERVICE_DPR = 1
3
3
  export const MAX_IMAGE_WIDTH = 3840
4
4
 
5
- export type ImageServiceUrlArguments = {
5
+ type ImageServiceUrlArguments = {
6
6
  url: string
7
7
  systemCode: string
8
8
  width?: number
@@ -17,7 +17,7 @@ export class Byline {
17
17
  const split = this.#splitBylineByName(bylineWithCorrectApostrophes, [
18
18
  ...this.authorUrlMapping.keys(),
19
19
  ])
20
- return await this.#buildUrlTree(this.authorUrlMapping, split)
20
+ return this.#buildUrlTree(this.authorUrlMapping, split)
21
21
  }
22
22
 
23
23
  #splitBylineByName(byline: string, names: string[]) {
@@ -28,9 +28,16 @@ import {
28
28
  validLiteralUnionValue,
29
29
  } from '../resolvers/literal-union'
30
30
 
31
- import { ContentPackageContainsArgs, Media } from '../generated'
31
+ import {
32
+ ContentPackageContainsArgs,
33
+ ContentUrlArgs,
34
+ Media,
35
+ TableOfContents,
36
+ } from '../generated'
32
37
  import { Topper } from './Topper'
33
38
  import { z } from 'zod'
39
+ import { Byline } from './Byline'
40
+ import { RichText } from './RichText'
34
41
 
35
42
  function isPlainObject(object: unknown): object is Record<string, unknown> {
36
43
  if (object && typeof object === 'object') {
@@ -214,7 +221,9 @@ export class CapiResponse {
214
221
  packageContainer
215
222
  )
216
223
  }
217
-
224
+ body() {
225
+ return new RichText('bodyXML', this.bodyXML(), this)
226
+ }
218
227
  bodyXML(): string | null {
219
228
  if ('bodyXML' in this.capiData) {
220
229
  return this.capiData.bodyXML
@@ -226,7 +235,20 @@ export class CapiResponse {
226
235
  return this.capiData.embeds
227
236
  return []
228
237
  }
229
- byline(): string | null {
238
+ byline({ vanity }: Partial<ContentUrlArgs>) {
239
+ const bylineText = this.rawByline()
240
+
241
+ if (!bylineText) return null
242
+ const authorUrlMapping = this.getAuthorUrlMapping()
243
+
244
+ return new Byline(
245
+ bylineText,
246
+ Boolean(vanity),
247
+ authorUrlMapping,
248
+ this.context
249
+ ).buildBylineTree()
250
+ }
251
+ rawByline(): string | null {
230
252
  if ('byline' in this.capiData && this.capiData.byline) {
231
253
  return this.capiData.byline
232
254
  }
@@ -245,7 +267,10 @@ export class CapiResponse {
245
267
  types() {
246
268
  return this.capiData.types
247
269
  }
248
- async url(vanity?: boolean | null) {
270
+ url({ relative, vanity }: Partial<ContentUrlArgs>) {
271
+ return relative ? this.relativeUrl(vanity) : this.absoluteUrl(vanity)
272
+ }
273
+ async absoluteUrl(vanity?: boolean | null) {
249
274
  const url =
250
275
  this.capiData.webUrl ??
251
276
  this.capiData.canonicalWebUrl ??
@@ -449,7 +474,7 @@ export class CapiResponse {
449
474
 
450
475
  async relativeUrl(vanity?: boolean | null) {
451
476
  const RELATIVE_URL_REGEX = /https?:\/\/www.ft.com/
452
- const url = await this.url(vanity)
477
+ const url = await this.absoluteUrl(vanity)
453
478
  return url.replace(RELATIVE_URL_REGEX, '')
454
479
  }
455
480
 
@@ -635,9 +660,9 @@ export class CapiResponse {
635
660
  return null
636
661
  }
637
662
 
638
- async tableOfContents() {
663
+ async tableOfContents(): Promise<TableOfContents | null> {
639
664
  return 'tableOfContents' in this.capiData
640
- ? this.capiData.tableOfContents
665
+ ? this.capiData.tableOfContents ?? null
641
666
  : null
642
667
  }
643
668
 
package/src/model/Clip.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import { ClipFormat } from '../resolvers/scalars'
8
8
  import imageServiceUrl from '../helpers/imageService'
9
9
 
10
- export type ClipSource = {
10
+ type ClipSource = {
11
11
  binaryUrl: string
12
12
  mediaType: string
13
13
  audioCodec?: string
@@ -17,7 +17,7 @@ export type ClipSource = {
17
17
  videoCodec?: string
18
18
  }
19
19
 
20
- export interface ClipVideo {
20
+ interface ClipVideo {
21
21
  id(): string
22
22
  type(): string
23
23
  format(): LiteralUnionScalarValues<typeof ClipFormat>
@@ -79,11 +79,14 @@ export class Clip implements ClipVideo {
79
79
  'full-grid': 1200,
80
80
  }
81
81
 
82
- const url = this.clip.poster?.members[0].binaryUrl;
83
- return url ? imageServiceUrl({
84
- url,
85
- systemCode: 'cp-content-pipeline',
86
- width: this.layout && mapSizes[this.layout] ? mapSizes[this.layout] : 700,
87
- }) : ''
82
+ const url = this.clip.poster?.members[0].binaryUrl
83
+ return url
84
+ ? imageServiceUrl({
85
+ url,
86
+ systemCode: 'cp-content-pipeline',
87
+ width:
88
+ this.layout && mapSizes[this.layout] ? mapSizes[this.layout] : 700,
89
+ })
90
+ : ''
88
91
  }
89
92
  }
@@ -5,21 +5,17 @@ import imageServiceUrl from '../helpers/imageService'
5
5
  import isError from '../helpers/isError'
6
6
  import conceptIds from '@financial-times/n-concept-ids'
7
7
  import decorateHeadshotUrl, { UUID_REGEX } from '../helpers/decorateHeadshotUrl'
8
+ import type { TopperWithHeadshotHeadshotArgs } from '../generated'
8
9
 
9
10
  const CAPI_ID_PREFIX = /^https?:\/\/(?:www|api)\.ft\.com\/things?\//
10
11
  const BASE_URL = 'https://www.ft.com/stream/'
11
12
  const FT_URL = 'https://www.ft.com'
12
13
 
13
- export type URLArguments = {
14
+ type URLArguments = {
14
15
  vanity?: boolean | null
15
16
  relative?: boolean | null
16
17
  }
17
18
 
18
- export type HeadshotArguments = {
19
- width?: number | null
20
- dpr?: number | null
21
- }
22
-
23
19
  export const predicates = {
24
20
  hasAuthor: 'http://www.ft.com/ontology/annotation/hasAuthor',
25
21
  isClassifiedBy: 'http://www.ft.com/ontology/classification/isClassifiedBy',
@@ -30,14 +26,14 @@ export const predicates = {
30
26
  about: 'http://www.ft.com/ontology/annotation/about',
31
27
  } as const
32
28
 
33
- export const types = {
29
+ const types = {
34
30
  brand: 'http://www.ft.com/ontology/product/Brand',
35
31
  genre: 'http://www.ft.com/ontology/Genre',
36
32
  organisation: 'http://www.ft.com/ontology/organisation/Organisation',
37
33
  publicCompany: 'http://www.ft.com/ontology/company/PublicCompany',
38
34
  }
39
35
 
40
- export const packageBrands = [
36
+ const packageBrands = [
41
37
  'specialReport',
42
38
  'ftSeries',
43
39
  'ftGuides',
@@ -175,7 +171,7 @@ export class Concept {
175
171
  return url
176
172
  }
177
173
 
178
- async headshot(args?: HeadshotArguments) {
174
+ async headshot(args?: TopperWithHeadshotHeadshotArgs) {
179
175
  const uuid = this.apiUrl().match(UUID_REGEX)?.[0]
180
176
 
181
177
  if (!this.isAuthor() || !uuid) return null
@@ -4,12 +4,7 @@ import type { QueryContext } from '..'
4
4
  import { Image } from '../types/internal-content'
5
5
  import { OrigamiImageDataSource } from '../datasources/origami-image'
6
6
  import { MAX_IMAGE_WIDTH } from '../helpers/imageService'
7
- //TODO:AG:20220908 Replace this with generated schema types
8
- type ImageSource = {
9
- url: string
10
- width?: number
11
- dpr?: number
12
- }
7
+ import type { ImageSource } from '../generated'
13
8
 
14
9
  describe('Image', () => {
15
10
  const mockImage = {
@@ -13,12 +13,7 @@ import {
13
13
  import { ImageFormat, ImageType } from '../resolvers/scalars'
14
14
  import isError from '../helpers/isError'
15
15
  import { BaseError } from '@dotcom-reliability-kit/errors'
16
-
17
- export type ImageSource = {
18
- url: string
19
- width?: number
20
- dpr?: number
21
- }
16
+ import type { ImageSource } from '../generated'
22
17
 
23
18
  export type ImageSourceArgs = {
24
19
  width?: number | null
@@ -14,7 +14,7 @@ import { QueryContext } from '..'
14
14
 
15
15
  export class RichText {
16
16
  constructor(
17
- private source: LiteralUnionScalarValues<typeof RichTextSource>,
17
+ public source: LiteralUnionScalarValues<typeof RichTextSource>,
18
18
  private value: string | null,
19
19
  private contentApiData?: CapiResponse
20
20
  ) {}
@@ -8,6 +8,7 @@ import {
8
8
  import { TopperBackgroundColour } from '../resolvers/scalars'
9
9
  import { RichText } from './RichText'
10
10
  import imageServiceUrl from '../helpers/imageService'
11
+ import type { TopperWithHeadshotHeadshotArgs } from '../generated'
11
12
 
12
13
  type TopperType =
13
14
  | 'DeepPortraitTopper'
@@ -19,11 +20,6 @@ type TopperType =
19
20
  | 'BrandedTopper'
20
21
  | 'BasicTopper'
21
22
 
22
- export type HeadshotArguments = {
23
- width?: number | null
24
- dpr?: number | null
25
- }
26
-
27
23
  type TopperBackgroundColourValues = LiteralUnionScalarValues<
28
24
  typeof TopperBackgroundColour
29
25
  >
@@ -207,11 +203,11 @@ export class Topper {
207
203
  }
208
204
 
209
205
  backgroundBox() {
210
- return this.capiResponse.topper()?.backgroundBox
206
+ return this.capiResponse.topper()?.backgroundBox ?? null
211
207
  }
212
208
 
213
209
  textShadow() {
214
- return this.capiResponse.topper()?.textShadow
210
+ return this.capiResponse.topper()?.textShadow ?? null
215
211
  }
216
212
 
217
213
  displayConcept() {
@@ -303,7 +299,7 @@ export class Topper {
303
299
  return this.capiResponse.design()
304
300
  }
305
301
 
306
- async headshot(args: HeadshotArguments) {
302
+ async headshot(args: TopperWithHeadshotHeadshotArgs) {
307
303
  let headshotUrl: string | undefined
308
304
 
309
305
  if (this.capiResponse.isPodcast()) {
@@ -313,7 +309,7 @@ export class Topper {
313
309
  if (this.capiResponse.isOpinion()) {
314
310
  const authors = this.capiResponse.getAuthors()
315
311
  const headshotUrls: (string | null)[] = await Promise.all(
316
- authors.map(async (author) => await author.headshot(args))
312
+ authors.map(async (author) => author.headshot(args))
317
313
  ).then((res) => res.filter(Boolean))
318
314
 
319
315
  headshotUrl = headshotUrls[0] ?? undefined
@@ -14,7 +14,7 @@ export const Annotation = Concept.extend({
14
14
  predicate: z.string(),
15
15
  })
16
16
 
17
- export const Topper = z.object({
17
+ const Topper = z.object({
18
18
  headline: z.string().optional(),
19
19
  standfirst: z.string().optional(),
20
20
  backgroundColour: z.string(),
@@ -84,7 +84,7 @@ export const LeadImage = z.object({
84
84
  image: Image,
85
85
  })
86
86
 
87
- export const AlternativeImage = z.object({
87
+ const AlternativeImage = z.object({
88
88
  promotionalImage: BaseImage,
89
89
  })
90
90
 
@@ -136,7 +136,7 @@ export const ClipSet = z.object({
136
136
  .optional(),
137
137
  })
138
138
 
139
- export const DataSource = z.object({
139
+ const DataSource = z.object({
140
140
  binaryUrl: z.string(),
141
141
  duration: z.number(),
142
142
  filesize: z.number(),
@@ -38,6 +38,7 @@ const contentPackageMetadataSchema = baseMetadataSchema.pick({
38
38
  canBeDistributed: true,
39
39
  topper: true,
40
40
  comments: true,
41
+ tableOfContents: true,
41
42
  })
42
43
 
43
44
  const contentPackageMediaSchema = baseMediaSchema.pick({
@@ -1,4 +1,4 @@
1
- import { ClipResolvers } from '../generated'
1
+ import { ClipResolvers, ClipSourceResolvers } from '../generated'
2
2
 
3
3
  const resolvers = {
4
4
  Clip: {
@@ -7,7 +7,18 @@ const resolvers = {
7
7
  type: (clip) => clip.type(),
8
8
  format: (clip) => clip.format(),
9
9
  poster: (clip) => clip.poster(),
10
+ id: (clip) => clip.id(),
10
11
  },
11
- } satisfies { Clip: ClipResolvers }
12
+
13
+ ClipSource: {
14
+ audioCodec: (parent) => parent.audioCodec ?? null,
15
+ binaryUrl: (parent) => parent.binaryUrl,
16
+ duration: (parent) => parent.duration ?? null,
17
+ mediaType: (parent) => parent.mediaType,
18
+ pixelWidth: (parent) => parent.pixelWidth ?? null,
19
+ pixelHeight: (parent) => parent.pixelHeight ?? null,
20
+ videoCodec: (parent) => parent.videoCodec ?? null,
21
+ },
22
+ } satisfies { Clip: ClipResolvers; ClipSource: ClipSourceResolvers }
12
23
 
13
24
  export default resolvers
@@ -6,10 +6,6 @@ type ValuesOfTuple<Tuple extends readonly string[]> = Tuple[number]
6
6
 
7
7
  type NodeOfType<Type extends AnyNode['type']> = Extract<AnyNode, { type: Type }>
8
8
 
9
- type NodesOfTypes<Types extends readonly AnyNode['type'][]> = {
10
- -readonly [key in keyof Types]: NodeOfType<Types[key]>
11
- }
12
-
13
9
  export const phrasingTypes = [
14
10
  'text',
15
11
  'break',
@@ -91,41 +87,3 @@ export const childrenOfTypes = <Types extends readonly AnyNode['type'][]>(
91
87
 
92
88
  return nodes as NodeOfType<ValuesOfTuple<Types>>[]
93
89
  }
94
-
95
- export const nodesAreOrderedTypes = <Types extends readonly AnyNode['type'][]>(
96
- types: Types,
97
- nodes: readonly AnyNode[]
98
- ): nodes is NodesOfTypes<Types> => {
99
- for (const [index, node] of nodes.entries()) {
100
- if (node.type !== types[index]) {
101
- return false
102
- }
103
- }
104
-
105
- return true
106
- }
107
-
108
- export const childrenOfOrderedTypes = <
109
- Types extends readonly AnyNode['type'][]
110
- >(
111
- types: Types,
112
- nodes: AnyNode[],
113
- parentType: AnyNode['type'],
114
- context?: QueryContext
115
- ): NodesOfTypes<Types> => {
116
- if (nodesAreOrderedTypes(types, nodes)) {
117
- return nodes
118
- }
119
-
120
- context?.logger.error({
121
- event: 'RECOVERABLE_ERROR',
122
- error: new OperationalError({
123
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
124
- message: `Unexpected children types for ${parentType}`,
125
- expected: types,
126
- actual: nodes.map((node) => node.type),
127
- }),
128
- })
129
-
130
- return nodes as unknown as NodesOfTypes<Types>
131
- }
@@ -6,7 +6,11 @@ import type {
6
6
  import { uuidFromUrl } from '../../../helpers/metadata'
7
7
  import { Clip } from '../../../model/Clip'
8
8
  import { RichText } from '../../../model/RichText'
9
- import { ClipSetResolvers } from '../../../generated'
9
+ import {
10
+ AccessibilityResolvers,
11
+ CaptionResolvers,
12
+ ClipSetResolvers,
13
+ } from '../../../generated'
10
14
  import { ReferenceWithCAPIData } from '.'
11
15
 
12
16
  function getClipSet(
@@ -73,7 +77,23 @@ export const ClipSet = {
73
77
  const clipSet = getClipSet(parent)
74
78
 
75
79
  return clipSet && clipSet.members && clipSet.members.length > 0
76
- ? clipSet.members.map((clip) => new Clip(clip, parent.reference.dataLayout))
80
+ ? clipSet.members.map(
81
+ (clip) => new Clip(clip, parent.reference.dataLayout)
82
+ )
77
83
  : null
78
84
  },
85
+
86
+ type() {
87
+ return 'clip-set'
88
+ },
79
89
  } satisfies ClipSetResolvers
90
+
91
+ export const Accessibility = {
92
+ captions: (parent) => parent.captions ?? null,
93
+ transcript: (parent) => parent.transcript,
94
+ } satisfies AccessibilityResolvers
95
+
96
+ export const Caption = {
97
+ mediaType: (parent) => parent.mediaType ?? null,
98
+ url: (parent) => parent.url ?? null,
99
+ } satisfies CaptionResolvers
@@ -1,5 +1,8 @@
1
1
  import imageServiceUrl from '../../../helpers/imageService'
2
- import { FlourishResolvers } from '../../../generated'
2
+ import {
3
+ FlourishFallbackResolvers,
4
+ FlourishResolvers,
5
+ } from '../../../generated'
3
6
  import { QueryContext } from '../../..'
4
7
  import isError from '../../../helpers/isError'
5
8
 
@@ -35,8 +38,24 @@ export const Flourish = {
35
38
  height,
36
39
  }
37
40
  },
41
+
42
+ type(parent) {
43
+ return parent.reference.type
44
+ },
38
45
  } satisfies FlourishResolvers
39
46
 
47
+ export const FlourishFallback = {
48
+ async height(parent) {
49
+ return parent.height ?? null
50
+ },
51
+ async width(parent) {
52
+ return parent.width ?? null
53
+ },
54
+ async url(parent) {
55
+ return parent.url ?? null
56
+ },
57
+ } satisfies FlourishFallbackResolvers
58
+
40
59
  type ImageMetadata = {
41
60
  width: number
42
61
  height: number
@@ -22,4 +22,8 @@ export const ImageSet = {
22
22
  ? new Picture(imageSet, isLiveBlog, context)
23
23
  : null
24
24
  },
25
+
26
+ type(parent) {
27
+ return parent.reference.type
28
+ },
25
29
  } satisfies ImageSetResolvers
@@ -27,4 +27,8 @@ export const LayoutImage = {
27
27
  context
28
28
  )
29
29
  },
30
+
31
+ type(parent) {
32
+ return parent.reference.type
33
+ },
30
34
  } satisfies LayoutImageResolvers
@@ -96,4 +96,8 @@ export const RawImage = {
96
96
  image(parent, _args, context): Image {
97
97
  return new RawImageModel(parent.reference, context)
98
98
  },
99
+
100
+ type(parent) {
101
+ return parent.reference.type
102
+ },
99
103
  } satisfies RawImageResolvers
@@ -28,4 +28,8 @@ export const Recommended = {
28
28
  return null
29
29
  }
30
30
  },
31
+
32
+ type(parent) {
33
+ return parent.reference.type
34
+ },
31
35
  } satisfies RecommendedResolvers
@@ -18,7 +18,5 @@ export const Reference = {
18
18
  validNodes: Object.keys(mapNodeToReference),
19
19
  })
20
20
  },
21
- type(parent) {
22
- return parent.reference.type
23
- },
21
+ type: (parent) => parent.reference.type,
24
22
  } satisfies ReferenceResolvers
@@ -20,4 +20,8 @@ export const ScrollyImage = {
20
20
 
21
21
  return imageSet ? new Picture(imageSet, isLiveBlog, context) : null
22
22
  },
23
+
24
+ type(parent) {
25
+ return parent.reference.type
26
+ },
23
27
  } satisfies ScrollyImageResolvers
@@ -18,4 +18,8 @@ export const Tweet = {
18
18
  return null
19
19
  }
20
20
  },
21
+
22
+ type(parent) {
23
+ return parent.reference.type
24
+ },
21
25
  } satisfies TweetResolvers
@@ -3,9 +3,7 @@ import { VideoReferenceResolvers } from '../../../generated'
3
3
 
4
4
  export const Video = {
5
5
  // This will override the original id from the parent that is a URL.
6
- id(parent) {
7
- return uuidFromUrl(parent.reference.id)
8
- },
6
+ id: (parent) => uuidFromUrl(parent.reference.id),
9
7
 
10
8
  async title(parent, _args, context) {
11
9
  const capiResponse = await context.dataSources.capi.getContent(
@@ -13,4 +11,8 @@ export const Video = {
13
11
  )
14
12
  return capiResponse.title()
15
13
  },
14
+
15
+ type(parent) {
16
+ return parent.reference.type
17
+ },
16
18
  } satisfies VideoReferenceResolvers
@@ -1,5 +1,3 @@
1
- import { IResolvers } from '@graphql-tools/utils'
2
-
3
1
  import type { ContentTree } from '@financial-times/content-tree'
4
2
  import type { CapiResponse } from '../../../model/CapiResponse'
5
3
 
@@ -7,9 +5,9 @@ import { Reference } from './Reference'
7
5
 
8
6
  import { Tweet } from './Tweet'
9
7
  import { ImageSet } from './ImageSet'
10
- import { ClipSet } from './ClipSet'
8
+ import { ClipSet, Accessibility, Caption } from './ClipSet'
11
9
  import { Video } from './Video'
12
- import { Flourish } from './Flourish'
10
+ import { Flourish, FlourishFallback } from './Flourish'
13
11
  import { Recommended } from './Recommended'
14
12
  import { LayoutImage } from './LayoutImage'
15
13
  import { RawImage } from './RawImage'
@@ -25,6 +23,9 @@ import {
25
23
  ScrollyImageResolvers,
26
24
  TweetResolvers,
27
25
  VideoReferenceResolvers,
26
+ CaptionResolvers,
27
+ AccessibilityResolvers,
28
+ FlourishFallbackResolvers,
28
29
  } from '../../../generated'
29
30
  import { AnyNode } from '../Workarounds'
30
31
 
@@ -44,6 +45,10 @@ export const resolvers: {
44
45
  LayoutImage: LayoutImageResolvers
45
46
  RawImage: RawImageResolvers
46
47
  ScrollyImage: ScrollyImageResolvers
48
+ MainImage: ImageSetResolvers
49
+ Caption: CaptionResolvers
50
+ Accessibility: AccessibilityResolvers
51
+ FlourishFallback: FlourishFallbackResolvers
47
52
  } = {
48
53
  Reference,
49
54
  Tweet,
@@ -55,6 +60,10 @@ export const resolvers: {
55
60
  LayoutImage,
56
61
  RawImage,
57
62
  ScrollyImage,
63
+ MainImage: ImageSet,
64
+ Caption,
65
+ Accessibility,
66
+ FlourishFallback,
58
67
  }
59
68
 
60
69
  export const mapNodeToReference = {
@@ -1,6 +1,6 @@
1
1
  import { AnyNode, OldClip } from './Workarounds'
2
2
  import tagMappings, { getBooleanAttributeValue } from './tagMappings'
3
- import cheerio from 'cheerio'
3
+ import { load } from 'cheerio'
4
4
 
5
5
  function expectNotArray<T>(thing: T | T[]): asserts thing is T {
6
6
  expect(thing).not.toBeInstanceOf(Array)
@@ -16,17 +16,13 @@ function expectNodeType<T extends AnyNode>(
16
16
 
17
17
  describe('tagMappings test', () => {
18
18
  it('getBooleanAttributeValue attrubute', () => {
19
- let $el = cheerio.load('<ft-content autoplay></ft-content>')('ft-content')
19
+ let $el = load('<ft-content autoplay></ft-content>')('ft-content')
20
20
  expect(getBooleanAttributeValue($el, 'autoplay')).toBe(true)
21
- $el = cheerio.load('<ft-content autoplay="true"></ft-content>')(
22
- 'ft-content'
23
- )
21
+ $el = load('<ft-content autoplay="true"></ft-content>')('ft-content')
24
22
  expect(getBooleanAttributeValue($el, 'autoplay')).toBe(true)
25
- $el = cheerio.load('<ft-content autoplay="false"></ft-content>')(
26
- 'ft-content'
27
- )
23
+ $el = load('<ft-content autoplay="false"></ft-content>')('ft-content')
28
24
  expect(getBooleanAttributeValue($el, 'autoplay')).toBe(false)
29
- $el = cheerio.load('<ft-content></ft-content>')('ft-content')
25
+ $el = load('<ft-content></ft-content>')('ft-content')
30
26
  expect(getBooleanAttributeValue($el, 'autoplay')).toBe(false)
31
27
  })
32
28
 
@@ -43,7 +39,7 @@ describe('tagMappings test', () => {
43
39
  <body>`
44
40
  const selector =
45
41
  'ft-content[type="http://www.ft.com/ontology/content/clip"]'
46
- const $el = cheerio.load(bodyXML)(selector)
42
+ const $el = load(bodyXML)(selector)
47
43
  const mapping = tagMappings[selector]($el, () => [])
48
44
 
49
45
  expectNodeType<OldClip>(mapping, 'clip')