@financial-times/cp-content-pipeline-schema 1.4.4 → 1.5.0

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 (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/fixtures/capiObject.js +1 -1
  3. package/lib/fixtures/capiObject.js.map +1 -1
  4. package/lib/fixtures/clipSet.d.ts +2 -0
  5. package/lib/fixtures/clipSet.js +70 -0
  6. package/lib/fixtures/clipSet.js.map +1 -0
  7. package/lib/generated/index.d.ts +120 -3
  8. package/lib/model/CapiResponse.d.ts +2 -2
  9. package/lib/model/CapiResponse.js +1 -1
  10. package/lib/model/CapiResponse.js.map +1 -1
  11. package/lib/model/Clip.d.ts +28 -0
  12. package/lib/model/Clip.js +51 -0
  13. package/lib/model/Clip.js.map +1 -0
  14. package/lib/model/schemas/capi/article.d.ts +602 -9
  15. package/lib/model/schemas/capi/audio.d.ts +31 -31
  16. package/lib/model/schemas/capi/base-schema.d.ts +1355 -14
  17. package/lib/model/schemas/capi/base-schema.js +41 -2
  18. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  19. package/lib/model/schemas/capi/content-package.d.ts +3 -3
  20. package/lib/model/schemas/capi/live-blog-package.d.ts +602 -9
  21. package/lib/model/schemas/capi/placeholder.d.ts +602 -9
  22. package/lib/resolvers/clip.d.ts +10 -0
  23. package/lib/resolvers/clip.js +13 -0
  24. package/lib/resolvers/clip.js.map +1 -0
  25. package/lib/resolvers/content-tree/Workarounds.d.ts +10 -2
  26. package/lib/resolvers/content-tree/nodePredicates.d.ts +3 -3
  27. package/lib/resolvers/content-tree/references/ClipSet.d.ts +27 -0
  28. package/lib/resolvers/content-tree/references/ClipSet.js +37 -0
  29. package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -0
  30. package/lib/resolvers/content-tree/references/ImageSet.js +3 -2
  31. package/lib/resolvers/content-tree/references/ImageSet.js.map +1 -1
  32. package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
  33. package/lib/resolvers/content-tree/references/ScrollyImage.js +3 -2
  34. package/lib/resolvers/content-tree/references/ScrollyImage.js.map +1 -1
  35. package/lib/resolvers/content-tree/references/index.d.ts +4 -1
  36. package/lib/resolvers/content-tree/references/index.js +4 -0
  37. package/lib/resolvers/content-tree/references/index.js.map +1 -1
  38. package/lib/resolvers/content-tree/tagMappings.js +9 -0
  39. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  40. package/lib/resolvers/index.d.ts +9 -0
  41. package/lib/resolvers/index.js +2 -0
  42. package/lib/resolvers/index.js.map +1 -1
  43. package/lib/resolvers/scalars.d.ts +2 -0
  44. package/lib/resolvers/scalars.js +6 -1
  45. package/lib/resolvers/scalars.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/fixtures/capiObject.ts +1 -1
  48. package/src/fixtures/clipSet.ts +72 -0
  49. package/src/generated/index.ts +128 -3
  50. package/src/model/CapiResponse.ts +3 -2
  51. package/src/model/Clip.ts +75 -0
  52. package/src/model/__snapshots__/RichText.test.ts.snap +25 -0
  53. package/src/model/schemas/capi/base-schema.ts +45 -1
  54. package/src/resolvers/clip.ts +13 -0
  55. package/src/resolvers/content-tree/Workarounds.ts +13 -2
  56. package/src/resolvers/content-tree/references/ClipSet.ts +49 -0
  57. package/src/resolvers/content-tree/references/ImageSet.ts +9 -5
  58. package/src/resolvers/content-tree/references/ScrollyImage.ts +9 -5
  59. package/src/resolvers/content-tree/references/index.ts +6 -0
  60. package/src/resolvers/content-tree/tagMappings.ts +10 -0
  61. package/src/resolvers/index.ts +2 -0
  62. package/src/resolvers/scalars.ts +8 -0
  63. package/src/types/internal-content.d.ts +4 -0
  64. package/tsconfig.tsbuildinfo +1 -1
  65. package/typedefs/clip.graphql +28 -0
  66. package/typedefs/references/clipSet.graphql +19 -0
  67. package/typedefs/scalars.graphql +1 -0
@@ -0,0 +1,72 @@
1
+ import { ClipSet as ClipSetType } from '../types/internal-content'
2
+
3
+ export const ClipSet: ClipSetType = {
4
+ accessibility: {
5
+ captions: [
6
+ {
7
+ mediaType: 'text/vtt',
8
+ url: 'https://next-media-api.ft.com/captions/16862228218010.vtt',
9
+ },
10
+ ],
11
+ transcript:
12
+ '<p>Hydrogen is this magic molecule which is the lightest, the most energetic and the most abundant in the universe. ... </p>',
13
+ },
14
+ description:
15
+ "The global push for net zero carbon emissions is one of humanity's greatest challenges. ...",
16
+ displayTitle: 'Ukranians fight back',
17
+ contentWarning: ['graphic violence', 'animal cruelty'],
18
+ id: 'http://www.ft.com/thing/f17fe25b-cdea-4d5f-a6af-40e56e33e888',
19
+ members: [
20
+ {
21
+ format: 'standardInline',
22
+ id: 'https://api.ft.com/content/4d000e07-d679-4e3b-bd6d-afa3461243a8',
23
+ type: 'http://www.ft.com/ontology/content/Clip',
24
+ dataSource: [
25
+ {
26
+ audioCodec: 'mp3',
27
+ binaryUrl:
28
+ 'https://next-media-api.ft.com/renditions/16868569859480/0x0.mp3',
29
+ duration: 1057,
30
+ mediaType: 'audio/mpeg',
31
+ pixelHeight: 720,
32
+ pixelWidth: 1280,
33
+ videoCodec: 'h264',
34
+ },
35
+ ],
36
+ poster: {
37
+ id: 'https://api.ft.com/content/2f730c3c-1fb2-4131-9697-c534fe1ee488',
38
+ type: 'http://www.ft.com/ontology/content/ImageSet',
39
+ description: 'Picture for video',
40
+ members: [],
41
+ },
42
+ },
43
+ {
44
+ format: 'mobile',
45
+ id: 'https://api.ft.com/content/b529ba2b-d424-4f85-a93f-8413636dac82',
46
+ type: 'http://www.ft.com/ontology/content/Clip',
47
+ dataSource: [
48
+ {
49
+ audioCodec: 'mp3',
50
+ binaryUrl:
51
+ 'https://next-media-api.ft.com/renditions/16868569859480/0x0.mp3',
52
+ duration: 1057,
53
+ mediaType: 'audio/mpeg',
54
+ pixelHeight: 720,
55
+ pixelWidth: 1280,
56
+ videoCodec: 'h264',
57
+ },
58
+ ],
59
+ poster: {
60
+ id: 'https://api.ft.com/content/2f730c3c-1fb2-4131-9697-c534fe1ee488',
61
+ type: 'http://www.ft.com/ontology/content/ImageSet',
62
+ description: 'Picture for video',
63
+ members: [],
64
+ },
65
+ },
66
+ ],
67
+ noAudio: false,
68
+ publishedDate: '2023-06-14T03:59:47.543Z',
69
+ source: 'YouGov',
70
+ subtitle: 'Drone hits Moscow tower block',
71
+ type: 'http://www.ft.com/ontology/content/ClipSet',
72
+ }
@@ -2,12 +2,13 @@ import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } f
2
2
  import type { Concept as ConceptModel } from '../model/Concept';
3
3
  import type { CapiResponse } from '../model/CapiResponse';
4
4
  import type { Image as ImageModel } from '../model/Image';
5
+ import type { Clip as ClipModel } from '../model/Clip';
5
6
  import type { Picture as PictureModel } from '../model/Picture';
6
7
  import type { RichText as RichTextModel } from '../model/RichText';
7
8
  import type { Topper as TopperModel } from '../model/Topper';
8
9
  import type { ContentTree } from '@financial-times/content-tree';
9
10
  import type { ReferenceWithCAPIData } from '../resolvers/content-tree/references';
10
- import type { Video as VideoNode, RawImage as RawImageNode } from '../resolvers/content-tree/Workarounds';
11
+ import type { Video as VideoNode, ClipSet as ClipSetNode, OldClip as OldClipNode, RawImage as RawImageNode } from '../resolvers/content-tree/Workarounds';
11
12
  import type { QueryContext } from '../';
12
13
  export type Maybe<T> = T | null;
13
14
  export type InputMaybe<T> = Maybe<T>;
@@ -27,6 +28,7 @@ export type Scalars = {
27
28
  Float: { input: number; output: number; }
28
29
  AccessLevel: { input: 'premium' | 'subscribed' | 'registered' | 'free'; output: 'premium' | 'subscribed' | 'registered' | 'free'; }
29
30
  CanBeSyndicated: { input: 'yes' | 'no' | 'verify' | 'withContributorPayment' | 'unknown'; output: 'yes' | 'no' | 'verify' | 'withContributorPayment' | 'unknown'; }
31
+ ClipFormat: { input: 'standard-inline' | 'mobile'; output: 'standard-inline' | 'mobile'; }
30
32
  ContentType: { input: 'Article' | 'Placeholder' | 'Video' | 'Audio' | 'LiveBlogPackage' | 'LiveBlogPost' | 'ContentPackage' | 'Content' | 'MediaResource'; output: 'Article' | 'Placeholder' | 'Video' | 'Audio' | 'LiveBlogPackage' | 'LiveBlogPost' | 'ContentPackage' | 'Content' | 'MediaResource'; }
31
33
  FollowButtonVariant: { input: 'standard' | 'inverse' | 'opinion' | 'alphaville' | 'monochrome' | 'inverse-monochrome'; output: 'standard' | 'inverse' | 'opinion' | 'alphaville' | 'monochrome' | 'inverse-monochrome'; }
32
34
  ImageFormat: { input: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; output: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; }
@@ -37,6 +39,11 @@ export type Scalars = {
37
39
  TopperBackgroundColour: { input: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; output: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; }
38
40
  };
39
41
 
42
+ export type Accessibility = {
43
+ readonly captions?: Maybe<ReadonlyArray<Maybe<Caption>>>;
44
+ readonly transcript?: Maybe<Scalars['String']['output']>;
45
+ };
46
+
40
47
  export type AltStandfirst = {
41
48
  readonly promotionalStandfirst?: Maybe<Scalars['String']['output']>;
42
49
  };
@@ -142,6 +149,49 @@ export type BrandedTopper = Topper & TopperWithBrand & TopperWithTheme & {
142
149
  readonly textShadow?: Maybe<Scalars['Boolean']['output']>;
143
150
  };
144
151
 
152
+ export type Caption = {
153
+ readonly mediaType?: Maybe<Scalars['String']['output']>;
154
+ readonly url?: Maybe<Scalars['String']['output']>;
155
+ };
156
+
157
+ export type Clip = {
158
+ readonly dataSource: ReadonlyArray<ClipSource>;
159
+ readonly format?: Maybe<Scalars['ClipFormat']['output']>;
160
+ readonly id: Scalars['String']['output'];
161
+ readonly poster?: Maybe<Scalars['String']['output']>;
162
+ readonly type?: Maybe<Scalars['String']['output']>;
163
+ };
164
+
165
+ export type ClipSet = Reference & {
166
+ readonly accessibility?: Maybe<Accessibility>;
167
+ readonly autoplay?: Maybe<Scalars['Boolean']['output']>;
168
+ readonly caption?: Maybe<Scalars['String']['output']>;
169
+ readonly clips?: Maybe<ReadonlyArray<Maybe<Clip>>>;
170
+ readonly contentWarning?: Maybe<ReadonlyArray<Maybe<Scalars['String']['output']>>>;
171
+ readonly credits?: Maybe<Scalars['String']['output']>;
172
+ readonly dataLayout?: Maybe<Scalars['String']['output']>;
173
+ readonly description?: Maybe<Scalars['String']['output']>;
174
+ readonly displayTitle?: Maybe<Scalars['String']['output']>;
175
+ readonly id: Scalars['String']['output'];
176
+ readonly loop?: Maybe<Scalars['Boolean']['output']>;
177
+ readonly muted?: Maybe<Scalars['Boolean']['output']>;
178
+ readonly noAudio?: Maybe<Scalars['Boolean']['output']>;
179
+ readonly publishedDate?: Maybe<Scalars['String']['output']>;
180
+ readonly source?: Maybe<Scalars['String']['output']>;
181
+ readonly subtitle?: Maybe<Scalars['String']['output']>;
182
+ readonly type: Scalars['String']['output'];
183
+ };
184
+
185
+ export type ClipSource = {
186
+ readonly audioCodec?: Maybe<Scalars['String']['output']>;
187
+ readonly binaryUrl: Scalars['String']['output'];
188
+ readonly duration?: Maybe<Scalars['Int']['output']>;
189
+ readonly mediaType: Scalars['String']['output'];
190
+ readonly pixelHeight?: Maybe<Scalars['Int']['output']>;
191
+ readonly pixelWidth?: Maybe<Scalars['Int']['output']>;
192
+ readonly videoCodec?: Maybe<Scalars['String']['output']>;
193
+ };
194
+
145
195
  export type Concept = {
146
196
  readonly apiUrl?: Maybe<Scalars['String']['output']>;
147
197
  readonly directType?: Maybe<Scalars['String']['output']>;
@@ -990,7 +1040,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = R
990
1040
  Content: ( CapiResponse ) | ( CapiResponse ) | ( CapiResponse ) | ( CapiResponse ) | ( CapiResponse ) | ( CapiResponse ) | ( CapiResponse );
991
1041
  Image: ( ImageDesktop ) | ( ImageLandscape ) | ( ImageMobile ) | ( ImagePortrait ) | ( ImageSquare ) | ( ImageSquareFtEdit ) | ( ImageStandard ) | ( ImageStandardInline ) | ( ImageWide );
992
1042
  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']> } );
993
- 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> );
1043
+ Reference: ( ReferenceWithCAPIData<ClipSetNode|OldClipNode> ) | ( 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> );
994
1044
  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']> } );
995
1045
  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']> } );
996
1046
  TopperWithHeadshot: ( TopperModel ) | ( TopperModel );
@@ -1002,6 +1052,7 @@ export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = R
1002
1052
  /** Mapping between all available schema types and the resolvers types */
1003
1053
  export type ResolversTypes = ResolversObject<{
1004
1054
  AccessLevel: ResolverTypeWrapper<Scalars['AccessLevel']['output']>;
1055
+ Accessibility: ResolverTypeWrapper<Accessibility>;
1005
1056
  AltStandfirst: ResolverTypeWrapper<AltStandfirst>;
1006
1057
  AltTitle: ResolverTypeWrapper<AltTitle>;
1007
1058
  Article: ResolverTypeWrapper<CapiResponse>;
@@ -1010,6 +1061,11 @@ export type ResolversTypes = ResolversObject<{
1010
1061
  Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
1011
1062
  BrandedTopper: ResolverTypeWrapper<Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<ResolversTypes['Concept']>, displayConcept?: Maybe<ResolversTypes['Concept']>, genreConcept?: Maybe<ResolversTypes['Concept']>, intro?: Maybe<ResolversTypes['RichText']> }>;
1012
1063
  CanBeSyndicated: ResolverTypeWrapper<Scalars['CanBeSyndicated']['output']>;
1064
+ Caption: ResolverTypeWrapper<Caption>;
1065
+ Clip: ResolverTypeWrapper<ClipModel>;
1066
+ ClipFormat: ResolverTypeWrapper<Scalars['ClipFormat']['output']>;
1067
+ ClipSet: ResolverTypeWrapper<ReferenceWithCAPIData<ClipSetNode|OldClipNode>>;
1068
+ ClipSource: ResolverTypeWrapper<ClipSource>;
1013
1069
  Concept: ResolverTypeWrapper<ConceptModel>;
1014
1070
  Content: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Content']>;
1015
1071
  ContentPackage: ResolverTypeWrapper<CapiResponse>;
@@ -1081,6 +1137,7 @@ export type ResolversTypes = ResolversObject<{
1081
1137
  /** Mapping between all available schema types and the resolvers parents */
1082
1138
  export type ResolversParentTypes = ResolversObject<{
1083
1139
  AccessLevel: Scalars['AccessLevel']['output'];
1140
+ Accessibility: Accessibility;
1084
1141
  AltStandfirst: AltStandfirst;
1085
1142
  AltTitle: AltTitle;
1086
1143
  Article: CapiResponse;
@@ -1089,6 +1146,11 @@ export type ResolversParentTypes = ResolversObject<{
1089
1146
  Boolean: Scalars['Boolean']['output'];
1090
1147
  BrandedTopper: Omit<BrandedTopper, 'brandConcept' | 'displayConcept' | 'genreConcept' | 'intro'> & { brandConcept?: Maybe<ResolversParentTypes['Concept']>, displayConcept?: Maybe<ResolversParentTypes['Concept']>, genreConcept?: Maybe<ResolversParentTypes['Concept']>, intro?: Maybe<ResolversParentTypes['RichText']> };
1091
1148
  CanBeSyndicated: Scalars['CanBeSyndicated']['output'];
1149
+ Caption: Caption;
1150
+ Clip: ClipModel;
1151
+ ClipFormat: Scalars['ClipFormat']['output'];
1152
+ ClipSet: ReferenceWithCAPIData<ClipSetNode|OldClipNode>;
1153
+ ClipSource: ClipSource;
1092
1154
  Concept: ConceptModel;
1093
1155
  Content: ResolversInterfaceTypes<ResolversParentTypes>['Content'];
1094
1156
  ContentPackage: CapiResponse;
@@ -1161,6 +1223,12 @@ export interface AccessLevelScalarConfig extends GraphQLScalarTypeConfig<Resolve
1161
1223
  name: 'AccessLevel';
1162
1224
  }
1163
1225
 
1226
+ export type AccessibilityResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Accessibility'] = ResolversParentTypes['Accessibility']> = ResolversObject<{
1227
+ captions?: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Caption']>>>, ParentType, ContextType>;
1228
+ transcript?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1229
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1230
+ }>;
1231
+
1164
1232
  export type AltStandfirstResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['AltStandfirst'] = ResolversParentTypes['AltStandfirst']> = ResolversObject<{
1165
1233
  promotionalStandfirst?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1166
1234
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -1254,6 +1322,57 @@ export interface CanBeSyndicatedScalarConfig extends GraphQLScalarTypeConfig<Res
1254
1322
  name: 'CanBeSyndicated';
1255
1323
  }
1256
1324
 
1325
+ export type CaptionResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Caption'] = ResolversParentTypes['Caption']> = ResolversObject<{
1326
+ mediaType?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1327
+ url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1328
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1329
+ }>;
1330
+
1331
+ export type ClipResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Clip'] = ResolversParentTypes['Clip']> = ResolversObject<{
1332
+ dataSource?: Resolver<ReadonlyArray<ResolversTypes['ClipSource']>, ParentType, ContextType>;
1333
+ format?: Resolver<Maybe<ResolversTypes['ClipFormat']>, ParentType, ContextType>;
1334
+ id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1335
+ poster?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1336
+ type?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1337
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1338
+ }>;
1339
+
1340
+ export interface ClipFormatScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['ClipFormat'], any> {
1341
+ name: 'ClipFormat';
1342
+ }
1343
+
1344
+ export type ClipSetResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ClipSet'] = ResolversParentTypes['ClipSet']> = ResolversObject<{
1345
+ accessibility?: Resolver<Maybe<ResolversTypes['Accessibility']>, ParentType, ContextType>;
1346
+ autoplay?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
1347
+ caption?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1348
+ clips?: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Clip']>>>, ParentType, ContextType>;
1349
+ contentWarning?: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
1350
+ credits?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1351
+ dataLayout?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1352
+ description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1353
+ displayTitle?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1354
+ id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1355
+ loop?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
1356
+ muted?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
1357
+ noAudio?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
1358
+ publishedDate?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1359
+ source?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1360
+ subtitle?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1361
+ type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1362
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1363
+ }>;
1364
+
1365
+ export type ClipSourceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ClipSource'] = ResolversParentTypes['ClipSource']> = ResolversObject<{
1366
+ audioCodec?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1367
+ binaryUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1368
+ duration?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
1369
+ mediaType?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1370
+ pixelHeight?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
1371
+ pixelWidth?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
1372
+ videoCodec?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1373
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
1374
+ }>;
1375
+
1257
1376
  export type ConceptResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Concept'] = ResolversParentTypes['Concept']> = ResolversObject<{
1258
1377
  apiUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
1259
1378
  directType?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -1782,7 +1901,7 @@ export type RecommendedResolvers<ContextType = QueryContext, ParentType extends
1782
1901
  }>;
1783
1902
 
1784
1903
  export type ReferenceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Reference'] = ResolversParentTypes['Reference']> = ResolversObject<{
1785
- __resolveType?: TypeResolveFn<'Flourish' | 'ImageSet' | 'LayoutImage' | 'MainImage' | 'RawImage' | 'Recommended' | 'ScrollyImage' | 'Tweet' | 'VideoReference', ParentType, ContextType>;
1904
+ __resolveType?: TypeResolveFn<'ClipSet' | 'Flourish' | 'ImageSet' | 'LayoutImage' | 'MainImage' | 'RawImage' | 'Recommended' | 'ScrollyImage' | 'Tweet' | 'VideoReference', ParentType, ContextType>;
1786
1905
  type?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
1787
1906
  }>;
1788
1907
 
@@ -1936,6 +2055,7 @@ export type VideoReferenceResolvers<ContextType = QueryContext, ParentType exten
1936
2055
 
1937
2056
  export type Resolvers<ContextType = QueryContext> = ResolversObject<{
1938
2057
  AccessLevel?: GraphQLScalarType;
2058
+ Accessibility?: AccessibilityResolvers<ContextType>;
1939
2059
  AltStandfirst?: AltStandfirstResolvers<ContextType>;
1940
2060
  AltTitle?: AltTitleResolvers<ContextType>;
1941
2061
  Article?: ArticleResolvers<ContextType>;
@@ -1943,6 +2063,11 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
1943
2063
  BasicTopper?: BasicTopperResolvers<ContextType>;
1944
2064
  BrandedTopper?: BrandedTopperResolvers<ContextType>;
1945
2065
  CanBeSyndicated?: GraphQLScalarType;
2066
+ Caption?: CaptionResolvers<ContextType>;
2067
+ Clip?: ClipResolvers<ContextType>;
2068
+ ClipFormat?: GraphQLScalarType;
2069
+ ClipSet?: ClipSetResolvers<ContextType>;
2070
+ ClipSource?: ClipSourceResolvers<ContextType>;
1946
2071
  Concept?: ConceptResolvers<ContextType>;
1947
2072
  Content?: ContentResolvers<ContextType>;
1948
2073
  ContentPackage?: ContentPackageResolvers<ContextType>;
@@ -2,6 +2,7 @@ import type {
2
2
  ImageSet,
3
3
  ContentTypeSchemas,
4
4
  MainImage,
5
+ ClipSet,
5
6
  } from '../types/internal-content'
6
7
  import conceptIds from '@financial-times/n-concept-ids'
7
8
  import metadata from '@financial-times/n-display-metadata'
@@ -125,7 +126,7 @@ export class CapiResponse {
125
126
  /*
126
127
  * Check if the incoming data matches the types defined in our data source schema
127
128
  * Our data source schema should handle all possible response from CAPI
128
- * As there is no agreed schema provided by CAPI there is a chance it will out of date / incorrect at times
129
+ * As there is no agreed schema provided by CAPI there is a chance it will be out of date / incorrect at times
129
130
  * Manual updates will be required at times to keep it in sync with the responses we receive
130
131
  */
131
132
  const schemaResponse = model.schema().safeParse(content)
@@ -177,7 +178,7 @@ export class CapiResponse {
177
178
  }
178
179
  return null
179
180
  }
180
- embeds(): ImageSet[] {
181
+ embeds(): (ImageSet | ClipSet)[] {
181
182
  if ('embeds' in this.capiData && this.capiData.embeds)
182
183
  return this.capiData.embeds
183
184
  return []
@@ -0,0 +1,75 @@
1
+ import { uuidFromUrl } from '../helpers/metadata'
2
+ import { Clip as ClipMetadata } from '../types/internal-content'
3
+ import {
4
+ LiteralUnionScalarValues,
5
+ validLiteralUnionValue,
6
+ } from '../resolvers/literal-union'
7
+ import { ClipFormat } from '../resolvers/scalars'
8
+
9
+ export type ClipSource = {
10
+ binaryUrl: string
11
+ mediaType: string
12
+ audioCodec?: string
13
+ duration?: number
14
+ pixelHeight?: number
15
+ pixelWidth?: number
16
+ videoCodec?: string
17
+ }
18
+
19
+ export interface ClipVideo {
20
+ id(): string
21
+ type(): string
22
+ format(): LiteralUnionScalarValues<typeof ClipFormat>
23
+ poster(): string
24
+ dataSource(): ClipSource[]
25
+ }
26
+
27
+ export class Clip implements ClipVideo {
28
+ constructor(private clip: ClipMetadata) {}
29
+
30
+ type() {
31
+ return 'clip'
32
+ }
33
+
34
+ id() {
35
+ const uuid = uuidFromUrl(this.clip.id)
36
+ if (!uuid) {
37
+ throw new Error(`${this.clip.id} is not a valid Content API Clip ID`)
38
+ }
39
+ return uuid
40
+ }
41
+
42
+ format(): LiteralUnionScalarValues<typeof ClipFormat> {
43
+ if (this.clip.format) {
44
+ const capiFormat = this.clip.format
45
+
46
+ if (capiFormat === 'standardInline') {
47
+ return 'standard-inline'
48
+ }
49
+
50
+ if (validLiteralUnionValue(this.clip.format, ClipFormat.values)) {
51
+ return capiFormat
52
+ }
53
+ }
54
+
55
+ return 'standard-inline'
56
+ }
57
+
58
+ dataSource() {
59
+ // Order clip dataSource to have an order of video formats first then audio/mpeg, as the browser will first check if it can play the MIME type of the first source
60
+ const dataSource = this.clip.dataSource.slice().sort((a, b) => {
61
+ if (a.mediaType === 'video/mp4' && b.mediaType !== 'video/mp4') {
62
+ return -1
63
+ }
64
+ if (a.mediaType !== 'video/mp4' && b.mediaType === 'video/mp4') {
65
+ return 1
66
+ }
67
+ return 0
68
+ })
69
+ return dataSource as ClipSource[]
70
+ }
71
+
72
+ poster() {
73
+ return this.clip.poster?.members[0].binaryUrl ?? ''
74
+ }
75
+ }
@@ -96,6 +96,20 @@ Object {
96
96
  "type": "image-set",
97
97
  },
98
98
  },
99
+ Object {
100
+ "contentApiData": undefined,
101
+ "reference": Object {
102
+ "autoplay": true,
103
+ "data": Object {
104
+ "referenceIndex": 9,
105
+ },
106
+ "dataLayout": "in-line",
107
+ "id": "http://api-t.ft.com/content/f17fe25b-cdea-4d5f-a6af-40e56e33e888",
108
+ "loop": true,
109
+ "muted": true,
110
+ "type": "clip-set",
111
+ },
112
+ },
99
113
  ],
100
114
  "text": "Eliot After The Waste Land (Eliot Biographies, 2)by Robert Crawford, Jonathan Cape £25
101
115
 
@@ -905,6 +919,17 @@ Join our online book group on Facebook at FT Books Café",
905
919
  ],
906
920
  "type": "paragraph",
907
921
  },
922
+ Object {
923
+ "autoplay": true,
924
+ "data": Object {
925
+ "referenceIndex": 9,
926
+ },
927
+ "dataLayout": "in-line",
928
+ "id": "http://api-t.ft.com/content/f17fe25b-cdea-4d5f-a6af-40e56e33e888",
929
+ "loop": true,
930
+ "muted": true,
931
+ "type": "clip-set",
932
+ },
908
933
  Object {
909
934
  "children": Array [
910
935
  Object {
@@ -88,6 +88,50 @@ export const AlternativeImage = z.object({
88
88
  promotionalImage: BaseImage,
89
89
  })
90
90
 
91
+ const ClipSource = z.object({
92
+ audioCodec: z.string().optional(),
93
+ binaryUrl: z.string().optional(),
94
+ duration: z.number().optional(),
95
+ mediaType: z.string().optional(),
96
+ pixelHeight: z.number().optional(),
97
+ pixelWidth: z.number().optional(),
98
+ videoCodec: z.string().optional(),
99
+ })
100
+
101
+ export const Clip = z.object({
102
+ id: z.string(),
103
+ type: z.literal('http://www.ft.com/ontology/content/Clip'),
104
+ format: z
105
+ .union([z.literal('standardInline'), z.literal('mobile')])
106
+ .optional(),
107
+ dataSource: ClipSource.array(),
108
+ poster: ImageSet.optional(),
109
+ })
110
+
111
+ export const ClipSet = z.object({
112
+ id: z.string(),
113
+ type: z.literal('http://www.ft.com/ontology/content/ClipSet'),
114
+ members: Clip.array(),
115
+ caption: z.string().optional(),
116
+ dataCopyright: z.string().optional(),
117
+ description: z.string().optional(),
118
+ displayTitle: z.string().optional(),
119
+ contentWarning: z.string().array().optional(),
120
+ noAudio: z.boolean().optional(),
121
+ source: z.string().optional(),
122
+ subtitle: z.string().optional(),
123
+ publishedDate: z.string().optional(),
124
+ accessibility: z.object({
125
+ captions: z.array(
126
+ z.object({
127
+ mediaType: z.string(),
128
+ url: z.string(),
129
+ })
130
+ ),
131
+ transcript: z.string().optional(),
132
+ }),
133
+ })
134
+
91
135
  export const DataSource = z.object({
92
136
  binaryUrl: z.string(),
93
137
  duration: z.number(),
@@ -213,6 +257,6 @@ export const baseMediaSchema = z.object({
213
257
  mainImage: MainImage.optional(),
214
258
  leadImages: LeadImage.array().optional(),
215
259
  alternativeImages: AlternativeImage.optional(),
216
- embeds: ImageSet.array().optional(),
260
+ embeds: z.array(z.union([ImageSet, ClipSet])).optional(),
217
261
  dataSource: DataSource.array().optional(),
218
262
  })
@@ -0,0 +1,13 @@
1
+ import { ClipResolvers } from '../generated'
2
+
3
+ const resolvers = {
4
+ Clip: {
5
+ __resolveType: () => 'Clip',
6
+ dataSource: (clip) => clip.dataSource(),
7
+ type: (clip) => clip.type(),
8
+ format: (clip) => clip.format(),
9
+ poster: (clip) => clip.poster(),
10
+ },
11
+ } satisfies { Clip: ClipResolvers }
12
+
13
+ export default resolvers
@@ -68,7 +68,7 @@ export interface YoutubeVideo extends ContentTree.Node {
68
68
  url: string
69
69
  }
70
70
 
71
- export interface Clip extends ContentTree.Node {
71
+ export interface OldClip extends ContentTree.Node {
72
72
  type: 'clip'
73
73
  url: string
74
74
  autoplay: boolean
@@ -83,6 +83,16 @@ export interface Clip extends ContentTree.Node {
83
83
  credits: string
84
84
  }
85
85
 
86
+ // this type is used for bodyXML transformation ONLY in tagMappings
87
+ export interface ClipSet extends ContentTree.Node {
88
+ type: 'clip-set'
89
+ id: string
90
+ autoplay: boolean
91
+ loop: boolean
92
+ muted: boolean
93
+ dataLayout: 'in-line' | 'mid-grid' | 'full-grid'
94
+ }
95
+
86
96
  export type TableColumnSettings = {
87
97
  hideOnMobile: boolean
88
98
  sortable: boolean
@@ -148,6 +158,8 @@ export type AnyNode =
148
158
  | ContentTree.Blockquote
149
159
  | ContentTree.Pullquote
150
160
  | ContentTree.ImageSet
161
+ | ClipSet
162
+ | OldClip
151
163
  | ContentTree.Recommended
152
164
  | ContentTree.Tweet
153
165
  | ContentTree.Flourish
@@ -168,7 +180,6 @@ export type AnyNode =
168
180
  | TableCell
169
181
  | Video
170
182
  | YoutubeVideo
171
- | Clip
172
183
  | MainImage
173
184
  | MainImageRaw
174
185
  | RawImage
@@ -0,0 +1,49 @@
1
+ import type { ClipSet as CAPIClipSet } from '../../../types/internal-content'
2
+ import type { ClipSet as ClipSetWorkaroundContentTree, OldClip as OldClipContentTree } from '../Workarounds'
3
+ import { uuidFromUrl } from '../../../helpers/metadata'
4
+ import { Clip } from '../../../model/Clip'
5
+ import { ClipSetResolvers } from '../../../generated'
6
+ import { ReferenceWithCAPIData } from '.'
7
+
8
+ function getClipSet(
9
+ parent: ReferenceWithCAPIData<ClipSetWorkaroundContentTree|OldClipContentTree>
10
+ ) {
11
+ const clipSets = parent.contentApiData
12
+ ?.embeds()
13
+ ?.filter((embedded) => embedded.type.includes('ClipSet')) as
14
+ | CAPIClipSet[]
15
+ | []
16
+
17
+ const clipSet = clipSets?.find(
18
+ (embed: CAPIClipSet) =>
19
+ uuidFromUrl(embed.id) === uuidFromUrl((parent.reference as ClipSetWorkaroundContentTree).id)
20
+ )
21
+
22
+ return clipSet as CAPIClipSet
23
+ }
24
+
25
+ export const ClipSet = {
26
+ // amend id, caption, description, credits when v1 is no longer supported
27
+ id: (parent) => ((parent.reference as ClipSetWorkaroundContentTree).id ? uuidFromUrl((parent.reference as ClipSetWorkaroundContentTree).id) : ''),
28
+ autoplay: (parent) => parent.reference.autoplay,
29
+ noAudio: (parent) => getClipSet(parent)?.noAudio || null,
30
+ loop: (parent) => parent.reference.loop,
31
+ muted: (parent) => parent.reference.muted,
32
+ dataLayout: (parent) => parent.reference.dataLayout,
33
+ caption: (parent) => ((parent.reference as OldClipContentTree).caption ?? getClipSet(parent)?.caption),
34
+ accessibility: (parent) => getClipSet(parent)?.accessibility || null,
35
+ description: (parent) => ((parent.reference as OldClipContentTree).description ?? getClipSet(parent)?.description),
36
+ credits: (parent) => ((parent.reference as OldClipContentTree).credits ?? getClipSet(parent)?.dataCopyright),
37
+ displayTitle: (parent) => getClipSet(parent)?.displayTitle || null,
38
+ contentWarning: (parent) => getClipSet(parent)?.contentWarning || null,
39
+ source: (parent) => getClipSet(parent)?.source || null,
40
+ subtitle: (parent) => getClipSet(parent)?.subtitle || null,
41
+ publishedDate: (parent) => getClipSet(parent)?.publishedDate || null,
42
+ async clips(parent) {
43
+ const clipSet = getClipSet(parent)
44
+
45
+ return clipSet && clipSet.members && clipSet.members.length > 0
46
+ ? clipSet.members.map((clip) => new Clip(clip))
47
+ : null
48
+ },
49
+ } satisfies ClipSetResolvers
@@ -7,12 +7,16 @@ export const ImageSet = {
7
7
  async picture(parent, _args, context) {
8
8
  const isLiveBlog = parent.contentApiData?.type() === 'LiveBlogPost'
9
9
 
10
- const imageSet = parent.contentApiData
10
+ const imageSets = parent.contentApiData
11
11
  ?.embeds()
12
- ?.find(
13
- (embed: CAPIImageSet) =>
14
- uuidFromUrl(embed.id) === uuidFromUrl(parent.reference.id)
15
- )
12
+ ?.filter((embedded) => embedded.type.includes('ImageSet')) as
13
+ | CAPIImageSet[]
14
+ | []
15
+
16
+ const imageSet = imageSets?.find(
17
+ (embed: CAPIImageSet) =>
18
+ uuidFromUrl(embed.id) === uuidFromUrl(parent.reference.id)
19
+ )
16
20
 
17
21
  return imageSet && imageSet.members && imageSet.members.length > 0
18
22
  ? new Picture(imageSet, isLiveBlog, context)