@financial-times/cp-content-pipeline-schema 3.0.4 → 3.1.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.
- package/CHANGELOG.md +14 -0
- package/lib/generated/index.d.ts +33 -4
- package/lib/helpers/flatten-formatted-zod-errors.d.ts +1 -1
- package/lib/helpers/flatten-formatted-zod-errors.js +1 -0
- package/lib/helpers/flatten-formatted-zod-errors.js.map +1 -1
- package/lib/model/CapiResponse.d.ts +3 -2
- package/lib/model/CapiResponse.js +64 -22
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/Concept.js +5 -1
- package/lib/model/Concept.js.map +1 -1
- package/lib/model/Concept.test.js +5 -0
- package/lib/model/Concept.test.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +222 -0
- package/lib/model/schemas/capi/audio.d.ts +194 -0
- package/lib/model/schemas/capi/base-schema.d.ts +420 -0
- package/lib/model/schemas/capi/base-schema.js +25 -2
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +194 -0
- package/lib/model/schemas/capi/custom-code-component.d.ts +1906 -0
- package/lib/model/schemas/capi/custom-code-component.js +17 -0
- package/lib/model/schemas/capi/custom-code-component.js.map +1 -0
- package/lib/model/schemas/capi/index.d.ts +2947 -16
- package/lib/model/schemas/capi/index.js +3 -0
- package/lib/model/schemas/capi/index.js.map +1 -1
- package/lib/model/schemas/capi/internal-content.d.ts +5 -2
- package/lib/model/schemas/capi/live-blog-package.d.ts +222 -0
- package/lib/model/schemas/capi/placeholder.d.ts +222 -0
- package/lib/model/schemas/capi/video.d.ts +194 -0
- package/lib/resolvers/content-tree/Workarounds.d.ts +5 -1
- package/lib/resolvers/content-tree/references/CustomCodeComponent.d.ts +13 -0
- package/lib/resolvers/content-tree/references/CustomCodeComponent.js +58 -0
- package/lib/resolvers/content-tree/references/CustomCodeComponent.js.map +1 -0
- package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
- package/lib/resolvers/content-tree/references/index.d.ts +3 -1
- package/lib/resolvers/content-tree/references/index.js +3 -0
- package/lib/resolvers/content-tree/references/index.js.map +1 -1
- package/lib/resolvers/content-tree/tagMappings.js +27 -2
- package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
- package/lib/resolvers/content-tree/tagMappings.test.js +19 -0
- package/lib/resolvers/content-tree/tagMappings.test.js.map +1 -1
- package/lib/resolvers/content.d.ts +8 -8
- package/lib/resolvers/content.js +4 -0
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/index.d.ts +10 -9
- package/lib/resolvers/scalars.d.ts +2 -2
- package/lib/resolvers/scalars.js +1 -0
- package/lib/resolvers/scalars.js.map +1 -1
- package/package.json +2 -2
- package/queries/article.graphql +24 -0
- package/src/generated/index.ts +34 -3
- package/src/helpers/flatten-formatted-zod-errors.ts +2 -1
- package/src/model/CapiResponse.ts +74 -29
- package/src/model/Concept.test.ts +8 -0
- package/src/model/Concept.ts +7 -1
- package/src/model/schemas/capi/base-schema.ts +27 -1
- package/src/model/schemas/capi/custom-code-component.ts +20 -0
- package/src/model/schemas/capi/index.ts +3 -0
- package/src/model/schemas/capi/internal-content.ts +7 -0
- package/src/resolvers/content-tree/Workarounds.ts +7 -1
- package/src/resolvers/content-tree/references/CustomCodeComponent.ts +78 -0
- package/src/resolvers/content-tree/references/index.ts +5 -0
- package/src/resolvers/content-tree/tagMappings.test.ts +29 -0
- package/src/resolvers/content-tree/tagMappings.ts +46 -4
- package/src/resolvers/content.ts +7 -0
- package/src/resolvers/scalars.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/content.graphql +1 -1
- package/typedefs/references/CustomCodeComponent.graphql +17 -0
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ContentTypeSchemas,
|
|
4
4
|
MainImage,
|
|
5
5
|
ClipSet,
|
|
6
|
+
CustomCodeComponentReference,
|
|
6
7
|
} from './schemas/capi/internal-content'
|
|
7
8
|
import conceptIds from '@financial-times/n-concept-ids'
|
|
8
9
|
import metadata from '@financial-times/n-display-metadata'
|
|
@@ -177,7 +178,7 @@ export class CapiResponse {
|
|
|
177
178
|
return null
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
embeds(): (ImageSet | ClipSet)[] {
|
|
181
|
+
embeds(): (ImageSet | ClipSet | CustomCodeComponentReference)[] {
|
|
181
182
|
if ('embeds' in this.capiData && this.capiData.embeds)
|
|
182
183
|
return this.capiData.embeds
|
|
183
184
|
return []
|
|
@@ -189,16 +190,19 @@ export class CapiResponse {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
async byline({ vanity }: Partial<ContentUrlArgs>) {
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
if ('byline' in this.capiData) {
|
|
194
|
+
if (!this.capiData.byline) return null
|
|
195
|
+
|
|
196
|
+
const byline = new Byline(
|
|
197
|
+
this.capiData.byline,
|
|
198
|
+
Boolean(vanity),
|
|
199
|
+
this.annotations({ byPredicate: predicates.hasAuthor }),
|
|
200
|
+
this
|
|
201
|
+
)
|
|
200
202
|
|
|
201
|
-
|
|
203
|
+
return byline.buildBylineTree()
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
#rawAnnotations() {
|
|
@@ -225,9 +229,16 @@ export class CapiResponse {
|
|
|
225
229
|
return relative ? this.relativeUrl(vanity) : this.absoluteUrl(vanity)
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
webUrl() {
|
|
233
|
+
if ('webUrl' in this.capiData) {
|
|
234
|
+
return this.capiData.webUrl
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
async absoluteUrl(vanity?: boolean | null) {
|
|
229
240
|
const url =
|
|
230
|
-
this.
|
|
241
|
+
this.webUrl() ??
|
|
231
242
|
this.capiData.canonicalWebUrl ??
|
|
232
243
|
`https://www.ft.com/content/${this.id()}`
|
|
233
244
|
|
|
@@ -297,11 +308,17 @@ export class CapiResponse {
|
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
accessLevel(): LiteralUnionScalarValues<typeof AccessLevel> {
|
|
300
|
-
|
|
311
|
+
if ('accessLevel' in this.capiData) {
|
|
312
|
+
return this.capiData.accessLevel ?? 'subscribed'
|
|
313
|
+
}
|
|
314
|
+
return 'subscribed'
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
editorialDesk() {
|
|
304
|
-
|
|
318
|
+
if ('editorialDesk' in this.capiData) {
|
|
319
|
+
return this.capiData.editorialDesk ?? null
|
|
320
|
+
}
|
|
321
|
+
return null
|
|
305
322
|
}
|
|
306
323
|
|
|
307
324
|
canBeSyndicated(): LiteralUnionScalarValues<typeof CanBeSyndicated> {
|
|
@@ -337,6 +354,9 @@ export class CapiResponse {
|
|
|
337
354
|
}
|
|
338
355
|
|
|
339
356
|
title() {
|
|
357
|
+
if (!('title' in this.capiData)) {
|
|
358
|
+
return ''
|
|
359
|
+
}
|
|
340
360
|
// CI-2038 HACK to remove "Comment:" prefix from live blog post titles
|
|
341
361
|
// as this is as the sole indicator that this is an opinion post
|
|
342
362
|
// while we're waiting for the annotation data to be in CAPI
|
|
@@ -379,9 +399,11 @@ export class CapiResponse {
|
|
|
379
399
|
}
|
|
380
400
|
|
|
381
401
|
modifiedTimestamp() {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
402
|
+
if ('lastModified' in this.capiData && this.capiData.lastModified) {
|
|
403
|
+
return new Date(this.capiData.lastModified).getTime()
|
|
404
|
+
} else {
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
385
407
|
}
|
|
386
408
|
|
|
387
409
|
publishReference() {
|
|
@@ -389,7 +411,10 @@ export class CapiResponse {
|
|
|
389
411
|
}
|
|
390
412
|
|
|
391
413
|
alternativeTitle() {
|
|
392
|
-
|
|
414
|
+
if ('alternativeTitles' in this.capiData) {
|
|
415
|
+
return this.capiData.alternativeTitles ?? null
|
|
416
|
+
}
|
|
417
|
+
return null
|
|
393
418
|
}
|
|
394
419
|
|
|
395
420
|
alternativeStandfirst() {
|
|
@@ -401,9 +426,12 @@ export class CapiResponse {
|
|
|
401
426
|
}
|
|
402
427
|
|
|
403
428
|
overrideTitle(title: string) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
429
|
+
if ('title' in this.capiData) {
|
|
430
|
+
const clone = cloneDeep(this.capiData)
|
|
431
|
+
clone.title = title
|
|
432
|
+
return new CapiResponse(clone, this.context, this.packageContainer)
|
|
433
|
+
}
|
|
434
|
+
return this
|
|
407
435
|
}
|
|
408
436
|
|
|
409
437
|
async containedIn() {
|
|
@@ -526,17 +554,27 @@ export class CapiResponse {
|
|
|
526
554
|
}
|
|
527
555
|
|
|
528
556
|
mainImage() {
|
|
529
|
-
|
|
557
|
+
if ('mainImage' in this.capiData) {
|
|
558
|
+
return this.#createCAPIImage(this.capiData.mainImage)
|
|
559
|
+
}
|
|
560
|
+
return null
|
|
530
561
|
}
|
|
531
562
|
|
|
532
563
|
teaserImage() {
|
|
533
564
|
// Some content published via methode uses promotionalImage for teasers
|
|
534
|
-
|
|
535
|
-
'alternativeImages' in this.capiData
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
565
|
+
if (
|
|
566
|
+
'alternativeImages' in this.capiData &&
|
|
567
|
+
this.capiData.alternativeImages?.promotionalImage
|
|
568
|
+
) {
|
|
569
|
+
return this.#createCAPIImage(
|
|
570
|
+
this.capiData.alternativeImages.promotionalImage
|
|
571
|
+
)
|
|
572
|
+
} else {
|
|
573
|
+
if ('mainImage' in this.capiData) {
|
|
574
|
+
return this.#createCAPIImage(this.capiData.mainImage)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null
|
|
540
578
|
}
|
|
541
579
|
|
|
542
580
|
#createCAPIImage(image: MainImage | undefined) {
|
|
@@ -604,6 +642,7 @@ export class CapiResponse {
|
|
|
604
642
|
// while we're waiting for the annotation data to be in CAPI
|
|
605
643
|
if (
|
|
606
644
|
this.type() === 'LiveBlogPost' &&
|
|
645
|
+
'title' in this.capiData &&
|
|
607
646
|
this.capiData.title.startsWith('Comment:')
|
|
608
647
|
) {
|
|
609
648
|
return true
|
|
@@ -618,7 +657,10 @@ export class CapiResponse {
|
|
|
618
657
|
}
|
|
619
658
|
|
|
620
659
|
isCentralBanking() {
|
|
621
|
-
|
|
660
|
+
if ('editorialDesk' in this.capiData) {
|
|
661
|
+
return this.capiData.editorialDesk === '/FT/Professional/Central Banking'
|
|
662
|
+
}
|
|
663
|
+
return false
|
|
622
664
|
}
|
|
623
665
|
|
|
624
666
|
isPlaceholder() {
|
|
@@ -799,6 +841,9 @@ export class CapiResponse {
|
|
|
799
841
|
}
|
|
800
842
|
|
|
801
843
|
isPartnerContent(): boolean {
|
|
802
|
-
|
|
844
|
+
if ('editorialDesk' in this.capiData) {
|
|
845
|
+
return this.capiData.editorialDesk === '/FT/Commercial/PartnerContent'
|
|
846
|
+
}
|
|
847
|
+
return false
|
|
803
848
|
}
|
|
804
849
|
}
|
|
@@ -73,5 +73,13 @@ describe('Concept model', () => {
|
|
|
73
73
|
'/vanity-url'
|
|
74
74
|
)
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
it('returns the full vanity URL when vanity=true and relative=true but vanity URL host is not www.ft.com', async () => {
|
|
78
|
+
const model = new Concept(topic, context)
|
|
79
|
+
vanityMock.mockResolvedValue('https://professional-monetary-policy-radar.ft.com')
|
|
80
|
+
expect(await model.url({ vanity: true, relative: true })).toEqual(
|
|
81
|
+
'https://professional-monetary-policy-radar.ft.com'
|
|
82
|
+
)
|
|
83
|
+
})
|
|
76
84
|
})
|
|
77
85
|
})
|
package/src/model/Concept.ts
CHANGED
|
@@ -162,7 +162,13 @@ export class Concept {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
if (args?.relative) {
|
|
165
|
-
|
|
165
|
+
const urlObject = new URL(url)
|
|
166
|
+
|
|
167
|
+
if (urlObject.host === 'www.ft.com') {
|
|
168
|
+
return urlObject.pathname
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return url
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
return url
|
|
@@ -183,6 +183,9 @@ export const baseMetadataSchema = z.object({
|
|
|
183
183
|
annotations: Annotation.array(),
|
|
184
184
|
webUrl: z.string().optional(),
|
|
185
185
|
canonicalWebUrl: z.string().optional(),
|
|
186
|
+
apiUrl: z.string().optional(),
|
|
187
|
+
path: z.string(),
|
|
188
|
+
versionRange: z.string(),
|
|
186
189
|
type: z.string().optional(),
|
|
187
190
|
types: z.string().array(),
|
|
188
191
|
standout: z
|
|
@@ -267,11 +270,34 @@ export const baseContentSchema = z.object({
|
|
|
267
270
|
transcript: z.string().optional(),
|
|
268
271
|
})
|
|
269
272
|
|
|
273
|
+
export const CustomCodeComponentReference = baseMetadataSchema
|
|
274
|
+
.pick({
|
|
275
|
+
id: true,
|
|
276
|
+
canonicalWebUrl: true,
|
|
277
|
+
publishedDate: true,
|
|
278
|
+
firstPublishedDate: true,
|
|
279
|
+
publishReference: true,
|
|
280
|
+
canBeSyndicated: true,
|
|
281
|
+
canBeDistributed: true,
|
|
282
|
+
})
|
|
283
|
+
.merge(baseContentSchema.pick({ bodyXML: true }))
|
|
284
|
+
.merge(
|
|
285
|
+
z.object({
|
|
286
|
+
apiUrl: z.string(),
|
|
287
|
+
type: z.literal('http://www.ft.com/ontology/content/CustomCodeComponent'),
|
|
288
|
+
path: z.string(),
|
|
289
|
+
versionRange: z.string(),
|
|
290
|
+
attributes: z.record(z.union([z.string(), z.boolean(), z.undefined()])),
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
|
|
270
294
|
export const baseMediaSchema = z.object({
|
|
271
295
|
mainImage: MainImage.optional(),
|
|
272
296
|
leadImages: LeadImage.array().optional(),
|
|
273
297
|
alternativeImages: AlternativeImage.optional(),
|
|
274
298
|
leadFlourish: LeadFlourish.optional(),
|
|
275
|
-
embeds: z
|
|
299
|
+
embeds: z
|
|
300
|
+
.array(z.union([ImageSet, ClipSet, CustomCodeComponentReference]))
|
|
301
|
+
.optional(),
|
|
276
302
|
dataSource: DataSource.array().optional(),
|
|
277
303
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
baseMetadataSchema,
|
|
3
|
+
baseMediaSchema,
|
|
4
|
+
CustomCodeComponentReference,
|
|
5
|
+
} from './base-schema'
|
|
6
|
+
|
|
7
|
+
const customCodeComponentMetadataSchema = baseMetadataSchema.pick({
|
|
8
|
+
annotations: true,
|
|
9
|
+
types: true,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const customCodeComponentMediaSchema = baseMediaSchema.pick({
|
|
13
|
+
embeds: true,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const customCodeComponentSchema = CustomCodeComponentReference.omit({
|
|
17
|
+
type: true,
|
|
18
|
+
})
|
|
19
|
+
.merge(customCodeComponentMetadataSchema)
|
|
20
|
+
.merge(customCodeComponentMediaSchema)
|
|
@@ -6,6 +6,7 @@ import { audioSchema } from './audio'
|
|
|
6
6
|
import { videoSchema } from './video'
|
|
7
7
|
import { LiteralUnionScalarValues } from '../../../resolvers/literal-union'
|
|
8
8
|
import { ContentType } from '../../../resolvers/scalars'
|
|
9
|
+
import { customCodeComponentSchema } from './custom-code-component'
|
|
9
10
|
|
|
10
11
|
export const schemas = (
|
|
11
12
|
contentType: LiteralUnionScalarValues<typeof ContentType>
|
|
@@ -24,6 +25,8 @@ export const schemas = (
|
|
|
24
25
|
case 'Video':
|
|
25
26
|
case 'MediaResource':
|
|
26
27
|
return videoSchema
|
|
28
|
+
case 'CustomCodeComponent':
|
|
29
|
+
return customCodeComponentSchema
|
|
27
30
|
default:
|
|
28
31
|
return articleSchema
|
|
29
32
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ClipSet,
|
|
9
9
|
MainImage,
|
|
10
10
|
LeadImage,
|
|
11
|
+
CustomCodeComponentReference,
|
|
11
12
|
} from './base-schema'
|
|
12
13
|
|
|
13
14
|
import { articleSchema } from './article'
|
|
@@ -16,6 +17,7 @@ import { placeholderSchema } from './placeholder'
|
|
|
16
17
|
import { liveBlogPackageSchema } from './live-blog-package'
|
|
17
18
|
import { audioSchema } from './audio'
|
|
18
19
|
import { videoSchema } from './video'
|
|
20
|
+
import { customCodeComponentSchema } from './custom-code-component'
|
|
19
21
|
|
|
20
22
|
import { contentPackageSchema } from './content-package'
|
|
21
23
|
|
|
@@ -25,6 +27,7 @@ type LiveBlogPackage = z.infer<typeof liveBlogPackageSchema>
|
|
|
25
27
|
type ContentPackage = z.infer<typeof contentPackageSchema>
|
|
26
28
|
type Audio = z.infer<typeof audioSchema>
|
|
27
29
|
type Video = z.infer<typeof videoSchema>
|
|
30
|
+
type CustomCodeComponent = z.infer<typeof customCodeComponentSchema>
|
|
28
31
|
|
|
29
32
|
export type CapiPerson = z.infer<typeof CapiPerson>
|
|
30
33
|
|
|
@@ -35,6 +38,9 @@ export type ImageSet = z.infer<typeof ImageSet>
|
|
|
35
38
|
export type MainImage = z.infer<typeof MainImage>
|
|
36
39
|
export type Clip = z.infer<typeof Clip>
|
|
37
40
|
export type ClipSet = z.infer<typeof ClipSet>
|
|
41
|
+
export type CustomCodeComponentReference = z.infer<
|
|
42
|
+
typeof CustomCodeComponentReference
|
|
43
|
+
>
|
|
38
44
|
|
|
39
45
|
export type ContentTypeSchemas =
|
|
40
46
|
| Article
|
|
@@ -43,3 +49,4 @@ export type ContentTypeSchemas =
|
|
|
43
49
|
| ContentPackage
|
|
44
50
|
| Audio
|
|
45
51
|
| Video
|
|
52
|
+
| CustomCodeComponent
|
|
@@ -75,6 +75,11 @@ export interface Video extends ContentTree.Node {
|
|
|
75
75
|
embedded: boolean
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export interface CccFallbackText extends ContentTree.Node {
|
|
79
|
+
type: 'ccc-fallback-text'
|
|
80
|
+
children: ContentTree.Paragraph[]
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
export interface OldClip extends ContentTree.Node {
|
|
79
84
|
type: 'clip'
|
|
80
85
|
url: string
|
|
@@ -218,11 +223,12 @@ export type AnyNode =
|
|
|
218
223
|
| ContentTree.Blockquote
|
|
219
224
|
| Pullquote
|
|
220
225
|
| ContentTree.ImageSet
|
|
221
|
-
| ContentTree.CustomCodeComponent
|
|
222
226
|
| ContentTree.Root
|
|
223
227
|
| Body
|
|
224
228
|
| ClipSet
|
|
225
229
|
| OldClip
|
|
230
|
+
| ContentTree.CustomCodeComponent
|
|
231
|
+
| CccFallbackText
|
|
226
232
|
| Recommended
|
|
227
233
|
| ContentTree.Tweet
|
|
228
234
|
| Flourish
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { uuidFromUrl } from '../../../helpers/metadata'
|
|
2
|
+
import { CustomCodeComponentResolvers } from '../../../generated'
|
|
3
|
+
import { ReferenceWithCAPIData } from '.'
|
|
4
|
+
import { ContentTree } from '@financial-times/content-tree'
|
|
5
|
+
import { RichText } from '../../../model/RichText'
|
|
6
|
+
import { CapiResponse } from '../../../model/CapiResponse'
|
|
7
|
+
import type { QueryContext } from '../../..'
|
|
8
|
+
import isError from '../../../helpers/isError'
|
|
9
|
+
import { OperationalError } from '@dotcom-reliability-kit/errors'
|
|
10
|
+
|
|
11
|
+
async function getReferencesFromBody(
|
|
12
|
+
parent: ReferenceWithCAPIData<ContentTree.CustomCodeComponent>,
|
|
13
|
+
context: QueryContext
|
|
14
|
+
) {
|
|
15
|
+
const customCodeComponentEmbedData = getCustomCodeComponentEmbedsData(parent)
|
|
16
|
+
|
|
17
|
+
if (!customCodeComponentEmbedData) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let customCodeComponentContentData: CapiResponse
|
|
22
|
+
try {
|
|
23
|
+
customCodeComponentContentData = await context.dataSources.capi.getContent(
|
|
24
|
+
uuidFromUrl(customCodeComponentEmbedData.apiUrl)
|
|
25
|
+
)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (isError(error)) {
|
|
28
|
+
context.logger.warn({
|
|
29
|
+
event: 'RECOVERABLE_ERROR',
|
|
30
|
+
error: new OperationalError({
|
|
31
|
+
code: 'CUSTOM_CODE_COMPONENT_CONTENT_ERROR',
|
|
32
|
+
message: `Error fetching content for CustomCodeComponent`,
|
|
33
|
+
cccData: parent,
|
|
34
|
+
cause: error,
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const bodyXML = customCodeComponentContentData?.bodyXML() ?? `<body></body>`
|
|
42
|
+
// The CAPI Data will be used by the RichText to create the references when appropriate.
|
|
43
|
+
return new RichText('bodyXML', bodyXML, customCodeComponentContentData)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getCustomCodeComponentEmbedsData(
|
|
47
|
+
parent: ReferenceWithCAPIData<ContentTree.CustomCodeComponent>
|
|
48
|
+
) {
|
|
49
|
+
const customCodeComponentEmbeds = parent.contentApiData
|
|
50
|
+
?.embeds()
|
|
51
|
+
?.filter(
|
|
52
|
+
(embedded) =>
|
|
53
|
+
embedded.type ===
|
|
54
|
+
'http://www.ft.com/ontology/content/CustomCodeComponent'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const uuid = uuidFromUrl(parent.reference.id)
|
|
58
|
+
|
|
59
|
+
if (!uuid) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return customCodeComponentEmbeds?.find(
|
|
64
|
+
(embed) => uuidFromUrl(embed.id) === uuid
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const CustomCodeComponent = {
|
|
69
|
+
id: (parent) => uuidFromUrl(parent.reference.id),
|
|
70
|
+
type: (parent) => parent.reference.type,
|
|
71
|
+
layoutWidth: (parent) => parent.reference.layoutWidth ?? 'in-line',
|
|
72
|
+
path: (parent) => getCustomCodeComponentEmbedsData(parent)?.path ?? '',
|
|
73
|
+
versionRange: (parent) =>
|
|
74
|
+
getCustomCodeComponentEmbedsData(parent)?.versionRange ?? 'latest',
|
|
75
|
+
attributes: (parent) =>
|
|
76
|
+
getCustomCodeComponentEmbedsData(parent)?.attributes ?? {},
|
|
77
|
+
body: async (parent, _, context) => getReferencesFromBody(parent, context),
|
|
78
|
+
} satisfies CustomCodeComponentResolvers
|
|
@@ -6,6 +6,7 @@ import { Reference } from './Reference'
|
|
|
6
6
|
import { Tweet } from './Tweet'
|
|
7
7
|
import { ImageSet } from './ImageSet'
|
|
8
8
|
import { ClipSet, Accessibility, Caption } from './ClipSet'
|
|
9
|
+
import { CustomCodeComponent } from './CustomCodeComponent'
|
|
9
10
|
import { Video } from './Video'
|
|
10
11
|
import { Flourish, FlourishFallback } from './Flourish'
|
|
11
12
|
import { Recommended } from './Recommended'
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
FlourishResolvers,
|
|
17
18
|
ImageSetResolvers,
|
|
18
19
|
ClipSetResolvers,
|
|
20
|
+
CustomCodeComponentResolvers,
|
|
19
21
|
LayoutImageResolvers,
|
|
20
22
|
RawImageResolvers,
|
|
21
23
|
RecommendedResolvers,
|
|
@@ -41,6 +43,7 @@ export const resolvers: {
|
|
|
41
43
|
Tweet: TweetResolvers
|
|
42
44
|
ImageSet: ImageSetResolvers
|
|
43
45
|
ClipSet: ClipSetResolvers
|
|
46
|
+
CustomCodeComponent: CustomCodeComponentResolvers
|
|
44
47
|
VideoReference: VideoReferenceResolvers
|
|
45
48
|
Flourish: FlourishResolvers
|
|
46
49
|
Recommended: RecommendedResolvers
|
|
@@ -57,6 +60,7 @@ export const resolvers: {
|
|
|
57
60
|
Tweet,
|
|
58
61
|
ImageSet,
|
|
59
62
|
ClipSet,
|
|
63
|
+
CustomCodeComponent,
|
|
60
64
|
VideoReference: Video,
|
|
61
65
|
Flourish,
|
|
62
66
|
Recommended,
|
|
@@ -76,6 +80,7 @@ export const mapNodeToReference = {
|
|
|
76
80
|
'image-set': 'ImageSet',
|
|
77
81
|
'clip-set': 'ClipSet',
|
|
78
82
|
clip: 'ClipSet',
|
|
83
|
+
'custom-code-component': 'CustomCodeComponent',
|
|
79
84
|
video: 'VideoReference',
|
|
80
85
|
recommended: 'Recommended',
|
|
81
86
|
'layout-image': 'LayoutImage',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AnyNode, OldClip } from './Workarounds'
|
|
2
2
|
import tagMappings, { getBooleanAttributeValue } from './tagMappings'
|
|
3
|
+
import { ContentTree } from '@financial-times/content-tree'
|
|
3
4
|
import { load } from 'cheerio'
|
|
4
5
|
|
|
5
6
|
function expectNotArray<T>(thing: T | T[]): asserts thing is T {
|
|
@@ -59,3 +60,31 @@ describe('tagMappings test', () => {
|
|
|
59
60
|
expect(mapping.posterCredits).toBe('Reuters')
|
|
60
61
|
})
|
|
61
62
|
})
|
|
63
|
+
|
|
64
|
+
it('custom-code-component mapping test', () => {
|
|
65
|
+
const bodyXML = `
|
|
66
|
+
<body>
|
|
67
|
+
<ft-content
|
|
68
|
+
type=\"http://www.ft.com/ontology/content/CustomCodeComponent\"
|
|
69
|
+
url=\"http://api.ft.com/content/1111111-1111-1111-1111-111111111111\"
|
|
70
|
+
data-layout-width=\"in-line\"
|
|
71
|
+
embedded=\"true\"
|
|
72
|
+
>
|
|
73
|
+
</ft-content>
|
|
74
|
+
</body>
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
const selector =
|
|
78
|
+
'ft-content[type="http://www.ft.com/ontology/content/CustomCodeComponent"]'
|
|
79
|
+
const $el = load(bodyXML)(selector)
|
|
80
|
+
const mapping = tagMappings[selector]!($el, () => [])
|
|
81
|
+
|
|
82
|
+
expectNodeType<ContentTree.CustomCodeComponent>(
|
|
83
|
+
mapping,
|
|
84
|
+
'custom-code-component'
|
|
85
|
+
)
|
|
86
|
+
expect(mapping.id).toBe(
|
|
87
|
+
'http://api.ft.com/content/1111111-1111-1111-1111-111111111111'
|
|
88
|
+
)
|
|
89
|
+
expect(mapping.layoutWidth).toBe('in-line')
|
|
90
|
+
})
|
|
@@ -88,7 +88,12 @@ const validScrollytellingOption = <
|
|
|
88
88
|
const articleTagMappings = (capiData?: CapiResponse): TagMappings => ({
|
|
89
89
|
'body > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]:first-child':
|
|
90
90
|
($el) => ({
|
|
91
|
-
|
|
91
|
+
// CustomCodeComponent uses an inner bodyXML parsed as a RichText
|
|
92
|
+
// The rule about main-image should not apply to CustomCodeComponent.
|
|
93
|
+
type:
|
|
94
|
+
capiData?.type() === 'CustomCodeComponent' || capiData?.topperHasImage()
|
|
95
|
+
? 'image-set'
|
|
96
|
+
: 'main-image',
|
|
92
97
|
id: $el.attr('url') || '',
|
|
93
98
|
}),
|
|
94
99
|
'body > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]:not(:first-child),:not(body) > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]':
|
|
@@ -174,7 +179,33 @@ const commonTagMappings: TagMappings = {
|
|
|
174
179
|
type: 'tweet',
|
|
175
180
|
id: $el.attr('href') || '',
|
|
176
181
|
}),
|
|
177
|
-
|
|
182
|
+
'[data-asset-type="custom-code-component-fallback-text"]': (
|
|
183
|
+
$el,
|
|
184
|
+
traverse,
|
|
185
|
+
context
|
|
186
|
+
) => ({
|
|
187
|
+
type: 'ccc-fallback-text',
|
|
188
|
+
children: everyChildIsType(
|
|
189
|
+
'paragraph',
|
|
190
|
+
traverse(),
|
|
191
|
+
'ccc-fallback-text',
|
|
192
|
+
context
|
|
193
|
+
),
|
|
194
|
+
}),
|
|
195
|
+
'ft-content[type="http://www.ft.com/ontology/content/CustomCodeComponent"]': (
|
|
196
|
+
$el
|
|
197
|
+
) => {
|
|
198
|
+
const layoutWidth = $el.attr('data-layout-width')
|
|
199
|
+
return {
|
|
200
|
+
type: 'custom-code-component',
|
|
201
|
+
id: $el.attr('url') || '',
|
|
202
|
+
layoutWidth:
|
|
203
|
+
layoutWidth &&
|
|
204
|
+
['in-line', 'mid-grid', 'full-grid', 'full-bleed'].includes(layoutWidth)
|
|
205
|
+
? (layoutWidth as 'in-line' | 'mid-grid' | 'full-grid' | 'full-bleed')
|
|
206
|
+
: 'in-line',
|
|
207
|
+
}
|
|
208
|
+
},
|
|
178
209
|
'h4,h5,h6': ($el, traverse, context) => ({
|
|
179
210
|
type: 'heading',
|
|
180
211
|
level: 'label',
|
|
@@ -269,8 +300,19 @@ const commonTagMappings: TagMappings = {
|
|
|
269
300
|
//TODO: this is a bit gross??
|
|
270
301
|
const isValidWidth = (
|
|
271
302
|
str: string
|
|
272
|
-
): str is
|
|
273
|
-
|
|
303
|
+
): str is
|
|
304
|
+
| 'inset-left'
|
|
305
|
+
| 'full-grid'
|
|
306
|
+
| 'full-width'
|
|
307
|
+
| 'in-line'
|
|
308
|
+
| 'mid-grid' => {
|
|
309
|
+
return [
|
|
310
|
+
'inset-left',
|
|
311
|
+
'full-grid',
|
|
312
|
+
'full-width',
|
|
313
|
+
'in-line',
|
|
314
|
+
'mid-grid',
|
|
315
|
+
].includes(str)
|
|
274
316
|
}
|
|
275
317
|
const isValidName = (str: string): str is 'auto' | 'card' | 'timeline' => {
|
|
276
318
|
return ['auto', 'card', 'timeline'].includes(str)
|
package/src/resolvers/content.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
IndicatorsResolvers,
|
|
16
16
|
MediaResolvers,
|
|
17
17
|
} from '../generated'
|
|
18
|
+
import { BaseError } from '@dotcom-reliability-kit/errors'
|
|
18
19
|
|
|
19
20
|
const contentResolvers: ContentResolvers = {
|
|
20
21
|
accessLevel: (parent) => parent.accessLevel(),
|
|
@@ -58,6 +59,12 @@ const resolvers = {
|
|
|
58
59
|
return 'Video'
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
if (type === 'CustomCodeComponent') {
|
|
63
|
+
throw new BaseError(
|
|
64
|
+
'CustomCodeComponent is not supported as a root content type'
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
return type
|
|
62
69
|
},
|
|
63
70
|
...contentResolvers,
|
package/src/resolvers/scalars.ts
CHANGED
|
@@ -128,7 +128,8 @@ export const ContentType = new LiteralUnionScalar<
|
|
|
128
128
|
'LiveBlogPost',
|
|
129
129
|
'ContentPackage',
|
|
130
130
|
'Content',
|
|
131
|
-
'MediaResource'
|
|
131
|
+
'MediaResource',
|
|
132
|
+
'CustomCodeComponent'
|
|
132
133
|
]
|
|
133
134
|
>({
|
|
134
135
|
name: 'ContentType',
|
|
@@ -142,6 +143,7 @@ export const ContentType = new LiteralUnionScalar<
|
|
|
142
143
|
'ContentPackage',
|
|
143
144
|
'Content',
|
|
144
145
|
'MediaResource',
|
|
146
|
+
'CustomCodeComponent',
|
|
145
147
|
],
|
|
146
148
|
})
|
|
147
149
|
|