@financial-times/cp-content-pipeline-schema 1.2.4 → 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.
- package/CHANGELOG.md +22 -0
- package/lib/generated/index.d.ts +14 -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/Workarounds.d.ts +5 -1
- package/lib/resolvers/content-tree/nodePredicates.d.ts +3 -3
- 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 +16 -0
- package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
- package/lib/resolvers/content.d.ts +3 -0
- package/lib/resolvers/content.js +3 -0
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/index.d.ts +6 -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 +11 -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/Workarounds.ts +6 -0
- package/src/resolvers/content-tree/references/ImageSet.ts +1 -1
- package/src/resolvers/content-tree/tagMappings.ts +18 -0
- package/src/resolvers/content.ts +5 -0
- package/src/resolvers/topper.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/content.graphql +1 -0
- package/typedefs/topper.graphql +3 -0
|
@@ -6,6 +6,7 @@ declare const resolvers: {
|
|
|
6
6
|
backgroundColour: (topper: import("../model/Topper").Topper) => "paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse";
|
|
7
7
|
displayConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
8
8
|
followButtonVariant: (topper: import("../model/Topper").Topper) => "standard" | "inverse" | "opinion" | "alphaville" | "monochrome" | "inverse-monochrome";
|
|
9
|
+
genreConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
9
10
|
};
|
|
10
11
|
TopperWithImages: {
|
|
11
12
|
images: (topper: import("../model/Topper").Topper) => import("../model/Image").CAPIImage[];
|
|
@@ -20,10 +21,10 @@ declare const resolvers: {
|
|
|
20
21
|
genreConcept: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
21
22
|
};
|
|
22
23
|
PodcastTopper: {
|
|
23
|
-
headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => string | null
|
|
24
|
+
headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").PodcastTopperHeadshotArgs>) => Promise<string | null>;
|
|
24
25
|
};
|
|
25
26
|
OpinionTopper: {
|
|
26
|
-
headshot(topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>)
|
|
27
|
+
headshot: (topper: import("../model/Topper").Topper, args: Partial<import("../generated").OpinionTopperHeadshotArgs>) => Promise<string | null>;
|
|
27
28
|
columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
28
29
|
};
|
|
29
30
|
TopperWithPackage: {
|
package/lib/resolvers/topper.js
CHANGED
|
@@ -8,6 +8,7 @@ const resolvers = {
|
|
|
8
8
|
backgroundColour: (topper) => topper.backgroundColour(),
|
|
9
9
|
displayConcept: (topper) => topper.displayConcept(),
|
|
10
10
|
followButtonVariant: (topper) => topper.followButtonVariant(),
|
|
11
|
+
genreConcept: (topper) => topper.genreConcept(),
|
|
11
12
|
},
|
|
12
13
|
TopperWithImages: {
|
|
13
14
|
images: (topper) => topper.images(),
|
|
@@ -25,9 +26,7 @@ const resolvers = {
|
|
|
25
26
|
headshot: (topper, args) => topper.headshot(args),
|
|
26
27
|
},
|
|
27
28
|
OpinionTopper: {
|
|
28
|
-
|
|
29
|
-
return topper.columnist()?.headshot(args) || null;
|
|
30
|
-
},
|
|
29
|
+
headshot: (topper, args) => topper.headshot(args),
|
|
31
30
|
columnist: (topper) => topper.columnist(),
|
|
32
31
|
},
|
|
33
32
|
TopperWithPackage: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":";;AAUA,MAAM,SAAS,GAAG;IAChB,MAAM,EAAE;QACN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;QACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;QACvC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;QACjC,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;QACvD,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE;QACnD,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE;
|
|
1
|
+
{"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":";;AAUA,MAAM,SAAS,GAAG;IAChB,MAAM,EAAE;QACN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;QACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;QACvC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;QACjC,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;QACvD,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE;QACnD,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE;QAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;KAChD;IAED,gBAAgB,EAAE;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;KAClD;IAED,eAAe,EAAE;QACf,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,eAAe,EAAE;QACf,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;KAChD;IAED,aAAa,EAAE;QACb,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;KAClD;IAED,aAAa,EAAE;QACb,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;KAC1C;IAED,iBAAiB,EAAE;QACjB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK;KAC1C;CASF,CAAA;AAED,kBAAe,SAAS,CAAA"}
|
package/package.json
CHANGED
package/src/generated/index.ts
CHANGED
|
@@ -120,6 +120,7 @@ export type BasicTopper = Topper & {
|
|
|
120
120
|
readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
|
|
121
121
|
readonly displayConcept?: Maybe<Concept>;
|
|
122
122
|
readonly followButtonVariant?: Maybe<Scalars['FollowButtonVariant']['output']>;
|
|
123
|
+
readonly genreConcept?: Maybe<Concept>;
|
|
123
124
|
readonly headline: Scalars['String']['output'];
|
|
124
125
|
readonly intro?: Maybe<RichText>;
|
|
125
126
|
readonly textShadow?: Maybe<Scalars['Boolean']['output']>;
|
|
@@ -561,6 +562,7 @@ export type LiveBlogPost = Content & {
|
|
|
561
562
|
readonly byline?: Maybe<StructuredContent>;
|
|
562
563
|
readonly canBeSyndicated?: Maybe<Scalars['CanBeSyndicated']['output']>;
|
|
563
564
|
readonly commentsEnabled?: Maybe<Scalars['Boolean']['output']>;
|
|
565
|
+
readonly containedIn?: Maybe<Content>;
|
|
564
566
|
readonly design?: Maybe<Design>;
|
|
565
567
|
readonly firstPublishedDate: Scalars['String']['output'];
|
|
566
568
|
readonly id: Scalars['String']['output'];
|
|
@@ -612,6 +614,7 @@ export type OpinionTopper = Topper & TopperWithHeadshot & TopperWithTheme & {
|
|
|
612
614
|
readonly columnist?: Maybe<Concept>;
|
|
613
615
|
readonly displayConcept?: Maybe<Concept>;
|
|
614
616
|
readonly followButtonVariant?: Maybe<Scalars['FollowButtonVariant']['output']>;
|
|
617
|
+
readonly genreConcept?: Maybe<Concept>;
|
|
615
618
|
readonly headline: Scalars['String']['output'];
|
|
616
619
|
readonly headshot?: Maybe<Scalars['String']['output']>;
|
|
617
620
|
readonly intro?: Maybe<RichText>;
|
|
@@ -819,6 +822,7 @@ export type Topper = {
|
|
|
819
822
|
readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
|
|
820
823
|
readonly displayConcept?: Maybe<Concept>;
|
|
821
824
|
readonly followButtonVariant?: Maybe<Scalars['FollowButtonVariant']['output']>;
|
|
825
|
+
readonly genreConcept?: Maybe<Concept>;
|
|
822
826
|
readonly headline: Scalars['String']['output'];
|
|
823
827
|
readonly intro?: Maybe<RichText>;
|
|
824
828
|
readonly textShadow?: Maybe<Scalars['Boolean']['output']>;
|
|
@@ -973,7 +977,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = R
|
|
|
973
977
|
Image: ( ImageDesktop ) | ( ImageLandscape ) | ( ImageMobile ) | ( ImagePortrait ) | ( ImageSquare ) | ( ImageSquareFtEdit ) | ( ImageStandard ) | ( ImageStandardInline ) | ( ImageWide );
|
|
974
978
|
Picture: ( Omit<PictureFullBleed, 'fallbackImage' | 'images'> & { fallbackImage: RefType['Image'], images: ReadonlyArray<RefType['Image']> } ) | ( Omit<PictureInline, 'fallbackImage' | 'images'> & { fallbackImage: RefType['Image'], images: ReadonlyArray<RefType['Image']> } ) | ( Omit<PictureStandard, 'fallbackImage' | 'images'> & { fallbackImage: RefType['Image'], images: ReadonlyArray<RefType['Image']> } );
|
|
975
979
|
Reference: ( ReferenceWithCAPIData<ContentTree.Flourish> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<ContentTree.LayoutImage> ) | ( ReferenceWithCAPIData<ContentTree.ImageSet> ) | ( ReferenceWithCAPIData<RawImageNode> ) | ( ReferenceWithCAPIData<ContentTree.Recommended> ) | ( ReferenceWithCAPIData<ContentTree.ScrollyImage> ) | ( ReferenceWithCAPIData<ContentTree.Tweet> ) | ( ReferenceWithCAPIData<VideoNode> );
|
|
976
|
-
Topper: ( Omit<BasicTopper, 'displayConcept' | 'intro'> & { displayConcept?: Maybe<RefType['Concept']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, genreConcept?: Maybe<RefType['Concept']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepLandscapeTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepPortraitTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<FullBleedTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( TopperModel ) | ( TopperModel ) | ( Omit<SplitTextTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } );
|
|
980
|
+
Topper: ( Omit<BasicTopper, 'displayConcept' | 'genreConcept' | 'intro'> & { displayConcept?: Maybe<RefType['Concept']>, genreConcept?: Maybe<RefType['Concept']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, genreConcept?: Maybe<RefType['Concept']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepLandscapeTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepPortraitTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<FullBleedTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( TopperModel ) | ( TopperModel ) | ( Omit<SplitTextTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } );
|
|
977
981
|
TopperWithBrand: ( Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, genreConcept?: Maybe<RefType['Concept']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepLandscapeTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepPortraitTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<FullBleedTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( TopperModel ) | ( Omit<SplitTextTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } );
|
|
978
982
|
TopperWithHeadshot: ( TopperModel ) | ( TopperModel );
|
|
979
983
|
TopperWithImages: ( Omit<DeepLandscapeTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<DeepPortraitTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<FullBleedTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } ) | ( Omit<SplitTextTopper, 'brandConcept' | 'displayConcept' | 'fallbackImage' | 'genreConcept' | 'images' | 'intro'> & { brandConcept?: Maybe<RefType['Concept']>, displayConcept?: Maybe<RefType['Concept']>, fallbackImage?: Maybe<RefType['Image']>, genreConcept?: Maybe<RefType['Concept']>, images: ReadonlyArray<RefType['Image']>, intro?: Maybe<RefType['RichText']> } );
|
|
@@ -988,7 +992,7 @@ export type ResolversTypes = ResolversObject<{
|
|
|
988
992
|
AltTitle: ResolverTypeWrapper<AltTitle>;
|
|
989
993
|
Article: ResolverTypeWrapper<CapiResponse>;
|
|
990
994
|
Audio: ResolverTypeWrapper<CapiResponse>;
|
|
991
|
-
BasicTopper: ResolverTypeWrapper<Omit<BasicTopper, 'displayConcept' | 'intro'> & { displayConcept?: Maybe<ResolversTypes['Concept']>, intro?: Maybe<ResolversTypes['RichText']> }>;
|
|
995
|
+
BasicTopper: ResolverTypeWrapper<Omit<BasicTopper, 'displayConcept' | 'genreConcept' | 'intro'> & { displayConcept?: Maybe<ResolversTypes['Concept']>, genreConcept?: Maybe<ResolversTypes['Concept']>, intro?: Maybe<ResolversTypes['RichText']> }>;
|
|
992
996
|
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
|
|
993
997
|
BrandedTopper: ResolverTypeWrapper<Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<ResolversTypes['Concept']>, displayConcept?: Maybe<ResolversTypes['Concept']>, genreConcept?: Maybe<ResolversTypes['Concept']>, intro?: Maybe<ResolversTypes['RichText']> }>;
|
|
994
998
|
CanBeSyndicated: ResolverTypeWrapper<Scalars['CanBeSyndicated']['output']>;
|
|
@@ -1066,7 +1070,7 @@ export type ResolversParentTypes = ResolversObject<{
|
|
|
1066
1070
|
AltTitle: AltTitle;
|
|
1067
1071
|
Article: CapiResponse;
|
|
1068
1072
|
Audio: CapiResponse;
|
|
1069
|
-
BasicTopper: Omit<BasicTopper, 'displayConcept' | 'intro'> & { displayConcept?: Maybe<ResolversParentTypes['Concept']>, intro?: Maybe<ResolversParentTypes['RichText']> };
|
|
1073
|
+
BasicTopper: Omit<BasicTopper, 'displayConcept' | 'genreConcept' | 'intro'> & { displayConcept?: Maybe<ResolversParentTypes['Concept']>, genreConcept?: Maybe<ResolversParentTypes['Concept']>, intro?: Maybe<ResolversParentTypes['RichText']> };
|
|
1070
1074
|
Boolean: Scalars['Boolean']['output'];
|
|
1071
1075
|
BrandedTopper: Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<ResolversParentTypes['Concept']>, displayConcept?: Maybe<ResolversParentTypes['Concept']>, genreConcept?: Maybe<ResolversParentTypes['Concept']>, intro?: Maybe<ResolversParentTypes['RichText']> };
|
|
1072
1076
|
CanBeSyndicated: Scalars['CanBeSyndicated']['output'];
|
|
@@ -1206,6 +1210,7 @@ export type BasicTopperResolvers<ContextType = QueryContext, ParentType extends
|
|
|
1206
1210
|
backgroundColour?: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
|
|
1207
1211
|
displayConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1208
1212
|
followButtonVariant?: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
|
|
1213
|
+
genreConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1209
1214
|
headline?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
1210
1215
|
intro?: Resolver<Maybe<ResolversTypes['RichText']>, ParentType, ContextType>;
|
|
1211
1216
|
textShadow?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
|
@@ -1592,6 +1597,7 @@ export type LiveBlogPostResolvers<ContextType = QueryContext, ParentType extends
|
|
|
1592
1597
|
byline?: Resolver<Maybe<ResolversTypes['StructuredContent']>, ParentType, ContextType, Partial<LiveBlogPostBylineArgs>>;
|
|
1593
1598
|
canBeSyndicated?: Resolver<Maybe<ResolversTypes['CanBeSyndicated']>, ParentType, ContextType>;
|
|
1594
1599
|
commentsEnabled?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
|
1600
|
+
containedIn?: Resolver<Maybe<ResolversTypes['Content']>, ParentType, ContextType>;
|
|
1595
1601
|
design?: Resolver<Maybe<ResolversTypes['Design']>, ParentType, ContextType>;
|
|
1596
1602
|
firstPublishedDate?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
1597
1603
|
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
@@ -1630,6 +1636,7 @@ export type OpinionTopperResolvers<ContextType = QueryContext, ParentType extend
|
|
|
1630
1636
|
columnist?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1631
1637
|
displayConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1632
1638
|
followButtonVariant?: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
|
|
1639
|
+
genreConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1633
1640
|
headline?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
1634
1641
|
headshot?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, Partial<OpinionTopperHeadshotArgs>>;
|
|
1635
1642
|
intro?: Resolver<Maybe<ResolversTypes['RichText']>, ParentType, ContextType>;
|
|
@@ -1824,6 +1831,7 @@ export type TopperResolvers<ContextType = QueryContext, ParentType extends Resol
|
|
|
1824
1831
|
backgroundColour?: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
|
|
1825
1832
|
displayConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1826
1833
|
followButtonVariant?: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
|
|
1834
|
+
genreConcept?: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
1827
1835
|
headline?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
1828
1836
|
intro?: Resolver<Maybe<ResolversTypes['RichText']>, ParentType, ContextType>;
|
|
1829
1837
|
textShadow?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
|
@@ -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,
|
|
@@ -59,6 +59,11 @@ export interface Video extends ContentTree.Node {
|
|
|
59
59
|
embedded: boolean
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface YoutubeVideo extends ContentTree.Node {
|
|
63
|
+
type: 'youtube-video'
|
|
64
|
+
url: string
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export interface Clip extends ContentTree.Node {
|
|
63
68
|
type: 'clip'
|
|
64
69
|
url: string
|
|
@@ -158,6 +163,7 @@ export type AnyNode =
|
|
|
158
163
|
| TableRow
|
|
159
164
|
| TableCell
|
|
160
165
|
| Video
|
|
166
|
+
| YoutubeVideo
|
|
161
167
|
| Clip
|
|
162
168
|
| MainImage
|
|
163
169
|
| RawImage
|
|
@@ -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
|
},
|
|
@@ -179,6 +179,18 @@ const commonTagMappings: TagMappings = {
|
|
|
179
179
|
id: $el.attr('url') || '',
|
|
180
180
|
embedded: $el.attr('data-embedded') === 'true' || true,
|
|
181
181
|
}),
|
|
182
|
+
'a[data-asset-type="video"]': ($el) => {
|
|
183
|
+
const url = $el.attr('href')
|
|
184
|
+
|
|
185
|
+
if (url) {
|
|
186
|
+
return {
|
|
187
|
+
type: 'youtube-video',
|
|
188
|
+
url,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return []
|
|
193
|
+
},
|
|
182
194
|
'ft-content[type="http://www.ft.com/ontology/content/clip"]': ($el) => ({
|
|
183
195
|
type: 'clip',
|
|
184
196
|
url: $el.attr('href') || '',
|
|
@@ -207,6 +219,12 @@ const commonTagMappings: TagMappings = {
|
|
|
207
219
|
timestamp: $el.attr('data-time-stamp') || '',
|
|
208
220
|
}
|
|
209
221
|
},
|
|
222
|
+
'ft-related': ($el) => {
|
|
223
|
+
return {
|
|
224
|
+
id: $el.attr('url') ?? '',
|
|
225
|
+
type: 'recommended',
|
|
226
|
+
}
|
|
227
|
+
},
|
|
210
228
|
recommended: ($el) => {
|
|
211
229
|
const $firstLink = $el.find('ft-content').first()
|
|
212
230
|
const $heading = $el.find('recommended-title').first()
|
package/src/resolvers/content.ts
CHANGED
|
@@ -67,6 +67,10 @@ const resolvers = {
|
|
|
67
67
|
containedIn: (parent) => parent.containedIn(),
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
+
LiveBlogPost: {
|
|
71
|
+
containedIn: (parent) => parent.containedIn(),
|
|
72
|
+
},
|
|
73
|
+
|
|
70
74
|
LiveBlogPackage: {
|
|
71
75
|
liveBlogPosts: (parent) => parent.liveBlogPosts(),
|
|
72
76
|
pinnedPost: (parent) => parent.pinnedPost(),
|
|
@@ -87,6 +91,7 @@ const resolvers = {
|
|
|
87
91
|
} satisfies {
|
|
88
92
|
Article: ArticleResolvers
|
|
89
93
|
Placeholder: PlaceholderResolvers
|
|
94
|
+
LiveBlogPost: ArticleResolvers
|
|
90
95
|
Content: ContentResolvers
|
|
91
96
|
LiveBlogPackage: LiveBlogPackageResolvers
|
|
92
97
|
ContentPackage: ContentPackageResolvers
|
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
|
|