@financial-times/cp-content-pipeline-schema 3.5.0 → 3.5.2

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 (52) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/generated/index.d.ts +4 -21
  3. package/lib/model/FlourishSource.d.ts +12 -5
  4. package/lib/model/FlourishSource.js +32 -28
  5. package/lib/model/FlourishSource.js.map +1 -1
  6. package/lib/model/FlourishSource.test.js +5 -5
  7. package/lib/model/FlourishSource.test.js.map +1 -1
  8. package/lib/model/Image.js +11 -20
  9. package/lib/model/Image.js.map +1 -1
  10. package/lib/model/Image.test.js +24 -54
  11. package/lib/model/Image.test.js.map +1 -1
  12. package/lib/model/LeadFlourish.d.ts +1 -1
  13. package/lib/model/LeadFlourish.js +4 -1
  14. package/lib/model/LeadFlourish.js.map +1 -1
  15. package/lib/model/LeadFlourish.test.js +5 -7
  16. package/lib/model/LeadFlourish.test.js.map +1 -1
  17. package/lib/model/schemas/capi/article.d.ts +3 -3
  18. package/lib/model/schemas/capi/audio.d.ts +5 -5
  19. package/lib/model/schemas/capi/base-schema.d.ts +5 -5
  20. package/lib/model/schemas/capi/base-schema.js +1 -1
  21. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  22. package/lib/model/schemas/capi/content-package.d.ts +3 -3
  23. package/lib/model/schemas/capi/custom-code-component.d.ts +3 -3
  24. package/lib/model/schemas/capi/index.d.ts +20 -20
  25. package/lib/model/schemas/capi/live-blog-package.d.ts +3 -3
  26. package/lib/model/schemas/capi/placeholder.d.ts +3 -3
  27. package/lib/model/schemas/capi/video.d.ts +3 -3
  28. package/lib/resolvers/content-tree/references/Flourish.d.ts +9 -19
  29. package/lib/resolvers/content-tree/references/Flourish.js +10 -32
  30. package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
  31. package/lib/resolvers/content-tree/references/Flourish.test.js +3 -3
  32. package/lib/resolvers/content-tree/references/Flourish.test.js.map +1 -1
  33. package/lib/resolvers/content-tree/references/index.d.ts +2 -2
  34. package/lib/resolvers/content-tree/references/index.js +1 -1
  35. package/lib/resolvers/content-tree/references/index.js.map +1 -1
  36. package/lib/resolvers/index.d.ts +1 -2
  37. package/lib/resolvers/leadFlourish.d.ts +1 -1
  38. package/package.json +1 -1
  39. package/src/generated/index.ts +4 -23
  40. package/src/model/FlourishSource.test.ts +5 -5
  41. package/src/model/FlourishSource.ts +57 -39
  42. package/src/model/Image.test.ts +37 -60
  43. package/src/model/Image.ts +14 -22
  44. package/src/model/LeadFlourish.test.ts +6 -9
  45. package/src/model/LeadFlourish.ts +5 -1
  46. package/src/model/schemas/capi/base-schema.ts +1 -1
  47. package/src/resolvers/content-tree/references/Flourish.test.ts +4 -3
  48. package/src/resolvers/content-tree/references/Flourish.ts +16 -32
  49. package/src/resolvers/content-tree/references/index.ts +4 -4
  50. package/tsconfig.tsbuildinfo +1 -1
  51. package/typedefs/leadFlourish.graphql +1 -1
  52. package/typedefs/references/flourish.graphql +1 -11
@@ -1,6 +1,6 @@
1
1
  import type { ContentTree } from '@financial-times/content-tree';
2
2
  import type { CapiResponse } from '../../../model/CapiResponse';
3
- import { FlourishResolvers, ImageSetResolvers, ClipSetResolvers, CustomCodeComponentResolvers, LayoutImageResolvers, RawImageResolvers, RecommendedResolvers, ReferenceResolvers, ScrollyImageResolvers, TweetResolvers, VideoReferenceResolvers, CaptionResolvers, AccessibilityResolvers, FlourishFallbackResolvers, AuthorReferenceResolvers } from '../../../generated';
3
+ import { FlourishResolvers, ImageSetResolvers, ClipSetResolvers, CustomCodeComponentResolvers, LayoutImageResolvers, RawImageResolvers, RecommendedResolvers, ReferenceResolvers, ScrollyImageResolvers, TweetResolvers, VideoReferenceResolvers, CaptionResolvers, AccessibilityResolvers, AuthorReferenceResolvers, FlourishSourceResolvers } from '../../../generated';
4
4
  export type ReferenceWithCAPIData<ReferenceType = ContentTree.Node> = {
5
5
  reference: ReferenceType;
6
6
  contentApiData?: CapiResponse;
@@ -13,6 +13,7 @@ export declare const resolvers: {
13
13
  CustomCodeComponent: CustomCodeComponentResolvers;
14
14
  VideoReference: VideoReferenceResolvers;
15
15
  Flourish: FlourishResolvers;
16
+ FlourishSource: FlourishSourceResolvers;
16
17
  Recommended: RecommendedResolvers;
17
18
  LayoutImage: LayoutImageResolvers;
18
19
  RawImage: RawImageResolvers;
@@ -20,7 +21,6 @@ export declare const resolvers: {
20
21
  MainImage: ImageSetResolvers;
21
22
  Caption: CaptionResolvers;
22
23
  Accessibility: AccessibilityResolvers;
23
- FlourishFallback: FlourishFallbackResolvers;
24
24
  AuthorReference: AuthorReferenceResolvers;
25
25
  };
26
26
  export declare const mapNodeToReference: {
@@ -21,6 +21,7 @@ exports.resolvers = {
21
21
  CustomCodeComponent: CustomCodeComponent_1.CustomCodeComponent,
22
22
  VideoReference: Video_1.Video,
23
23
  Flourish: Flourish_1.Flourish,
24
+ FlourishSource: Flourish_1.FlourishSource,
24
25
  Recommended: Recommended_1.Recommended,
25
26
  LayoutImage: LayoutImage_1.LayoutImage,
26
27
  RawImage: RawImage_1.RawImage,
@@ -28,7 +29,6 @@ exports.resolvers = {
28
29
  MainImage: ImageSet_1.ImageSet,
29
30
  Caption: ClipSet_1.Caption,
30
31
  Accessibility: ClipSet_1.Accessibility,
31
- FlourishFallback: Flourish_1.FlourishFallback,
32
32
  AuthorReference: Author_1.Author,
33
33
  };
34
34
  exports.mapNodeToReference = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/resolvers/content-tree/references/index.ts"],"names":[],"mappings":";;;AAGA,2CAAuC;AAEvC,mCAA+B;AAC/B,yCAAqC;AACrC,uCAA2D;AAC3D,+DAA2D;AAC3D,mCAA+B;AAC/B,yCAAuD;AACvD,+CAA2C;AAC3C,+CAA2C;AAC3C,yCAAqC;AACrC,iDAA6C;AAmB7C,qCAAiC;AAOpB,QAAA,SAAS,GAiBlB;IACF,SAAS,EAAT,qBAAS;IACT,KAAK,EAAL,aAAK;IACL,QAAQ,EAAR,mBAAQ;IACR,OAAO,EAAP,iBAAO;IACP,mBAAmB,EAAnB,yCAAmB;IACnB,cAAc,EAAE,aAAK;IACrB,QAAQ,EAAR,mBAAQ;IACR,WAAW,EAAX,yBAAW;IACX,WAAW,EAAX,yBAAW;IACX,QAAQ,EAAR,mBAAQ;IACR,YAAY,EAAZ,2BAAY;IACZ,SAAS,EAAE,mBAAQ;IACnB,OAAO,EAAP,iBAAO;IACP,aAAa,EAAb,uBAAa;IACb,gBAAgB,EAAhB,2BAAgB;IAChB,eAAe,EAAE,eAAM;CACxB,CAAA;AAEY,QAAA,kBAAkB,GAAG;IAChC,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,OAAO;IACd,WAAW,EAAE,UAAU;IACvB,UAAU,EAAE,SAAS;IACrB,IAAI,EAAE,SAAS;IACf,uBAAuB,EAAE,qBAAqB;IAC9C,KAAK,EAAE,gBAAgB;IACvB,WAAW,EAAE,aAAa;IAC1B,cAAc,EAAE,aAAa;IAC7B,eAAe,EAAE,cAAc;IAC/B,WAAW,EAAE,UAAU;IACvB,YAAY,EAAE,WAAW;IACzB,gBAAgB,EAAE,UAAU;IAC5B,MAAM,EAAE,iBAAiB;CACkD,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/resolvers/content-tree/references/index.ts"],"names":[],"mappings":";;;AAGA,2CAAuC;AAEvC,mCAA+B;AAC/B,yCAAqC;AACrC,uCAA2D;AAC3D,+DAA2D;AAC3D,mCAA+B;AAC/B,yCAAqD;AACrD,+CAA2C;AAC3C,+CAA2C;AAC3C,yCAAqC;AACrC,iDAA6C;AAmB7C,qCAAiC;AAOpB,QAAA,SAAS,GAiBlB;IACF,SAAS,EAAT,qBAAS;IACT,KAAK,EAAL,aAAK;IACL,QAAQ,EAAR,mBAAQ;IACR,OAAO,EAAP,iBAAO;IACP,mBAAmB,EAAnB,yCAAmB;IACnB,cAAc,EAAE,aAAK;IACrB,QAAQ,EAAR,mBAAQ;IACR,cAAc,EAAd,yBAAc;IACd,WAAW,EAAX,yBAAW;IACX,WAAW,EAAX,yBAAW;IACX,QAAQ,EAAR,mBAAQ;IACR,YAAY,EAAZ,2BAAY;IACZ,SAAS,EAAE,mBAAQ;IACnB,OAAO,EAAP,iBAAO;IACP,aAAa,EAAb,uBAAa;IACb,eAAe,EAAE,eAAM;CACxB,CAAA;AAEY,QAAA,kBAAkB,GAAG;IAChC,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,OAAO;IACd,WAAW,EAAE,UAAU;IACvB,UAAU,EAAE,SAAS;IACrB,IAAI,EAAE,SAAS;IACf,uBAAuB,EAAE,qBAAqB;IAC9C,KAAK,EAAE,gBAAgB;IACvB,WAAW,EAAE,aAAa;IAC1B,cAAc,EAAE,aAAa;IAC7B,eAAe,EAAE,cAAc;IAC/B,WAAW,EAAE,UAAU;IACvB,YAAY,EAAE,WAAW;IACzB,gBAAgB,EAAE,UAAU;IAC5B,MAAM,EAAE,iBAAiB;CACkD,CAAA"}
@@ -14,7 +14,7 @@ declare const resolvers: {
14
14
  type: (flourish: import("../model/LeadFlourish").LeadFlourish) => string;
15
15
  id: (flourish: import("../model/LeadFlourish").LeadFlourish) => string | null;
16
16
  description: (flourish: import("../model/LeadFlourish").LeadFlourish) => string;
17
- fallbackImage: (flourish: import("../model/LeadFlourish").LeadFlourish) => Promise<import("../model/FlourishSource").FlourishSource>;
17
+ fallbackImage: (flourish: import("../model/LeadFlourish").LeadFlourish) => Promise<import("../model/FlourishSource").FlourishSource | null>;
18
18
  };
19
19
  FlourishSource: {
20
20
  id: (parent: import("../model/FlourishSource").FlourishSource) => string;
@@ -290,7 +290,6 @@ declare const resolvers: {
290
290
  MainImage: import("../generated").ImageSetResolvers;
291
291
  Caption: import("../generated").CaptionResolvers;
292
292
  Accessibility: import("../generated").AccessibilityResolvers;
293
- FlourishFallback: import("../generated").FlourishFallbackResolvers;
294
293
  AuthorReference: import("../generated").AuthorReferenceResolvers;
295
294
  Picture: {
296
295
  __resolveType: import("../generated").TypeResolveFn<"PictureFullBleed" | "PictureInline" | "PictureStandard", import("../model/Picture").Picture, import("..").QueryContext>;
@@ -3,7 +3,7 @@ declare const resolvers: {
3
3
  type: (flourish: import("../model/LeadFlourish").LeadFlourish) => string;
4
4
  id: (flourish: import("../model/LeadFlourish").LeadFlourish) => string | null;
5
5
  description: (flourish: import("../model/LeadFlourish").LeadFlourish) => string;
6
- fallbackImage: (flourish: import("../model/LeadFlourish").LeadFlourish) => Promise<import("../model/FlourishSource").FlourishSource>;
6
+ fallbackImage: (flourish: import("../model/LeadFlourish").LeadFlourish) => Promise<import("../model/FlourishSource").FlourishSource | null>;
7
7
  };
8
8
  FlourishSource: {
9
9
  id: (parent: import("../model/FlourishSource").FlourishSource) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/cp-content-pipeline-schema",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -694,20 +694,11 @@ export type Design = {
694
694
 
695
695
  export type Flourish = Reference & {
696
696
  /** The fallback image to be used if the flourish graphics cannot be loaded. */
697
- readonly fallbackImage?: Maybe<FlourishFallback>;
697
+ readonly fallbackImage?: Maybe<FlourishSource>;
698
698
  /** The type of the reference, eg. 'flourish'. */
699
699
  readonly type: Scalars['String']['output'];
700
700
  };
701
701
 
702
- export type FlourishFallback = {
703
- /** The height in pixels of the fallback image. */
704
- readonly height?: Maybe<Scalars['Int']['output']>;
705
- /** The url of the fallback image. */
706
- readonly url?: Maybe<Scalars['String']['output']>;
707
- /** The width in pixels of the fallback image. */
708
- readonly width?: Maybe<Scalars['Int']['output']>;
709
- };
710
-
711
702
  export type FlourishSource = {
712
703
  /** The format of the source, eg. 'standard'. */
713
704
  readonly format: Scalars['ImageFormat']['output'];
@@ -1075,7 +1066,7 @@ export type LayoutImage = Reference & {
1075
1066
  export type LeadFlourish = {
1076
1067
  /** The description of the Flourish chart. */
1077
1068
  readonly description?: Maybe<Scalars['String']['output']>;
1078
- readonly fallbackImage: FlourishSource;
1069
+ readonly fallbackImage?: Maybe<FlourishSource>;
1079
1070
  /** The id of the Flourish chart. */
1080
1071
  readonly id?: Maybe<Scalars['String']['output']>;
1081
1072
  /** The type of the chart, eg. 'visualisation'. */
@@ -2026,7 +2017,6 @@ export type ResolversTypes = ResolversObject<{
2026
2017
  Design: ResolverTypeWrapper<Design>;
2027
2018
  Float: ResolverTypeWrapper<Scalars['Float']['output']>;
2028
2019
  Flourish: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.Flourish>>;
2029
- FlourishFallback: ResolverTypeWrapper<FlourishFallback>;
2030
2020
  FlourishSource: ResolverTypeWrapper<FlourishSourceModel>;
2031
2021
  FollowButtonVariant: ResolverTypeWrapper<Scalars['FollowButtonVariant']['output']>;
2032
2022
  FullBleedTopper: ResolverTypeWrapper<TopperModel>;
@@ -2128,7 +2118,6 @@ export type ResolversParentTypes = ResolversObject<{
2128
2118
  Design: Design;
2129
2119
  Float: Scalars['Float']['output'];
2130
2120
  Flourish: ReferenceWithCAPIData<ContentTree.Flourish>;
2131
- FlourishFallback: FlourishFallback;
2132
2121
  FlourishSource: FlourishSourceModel;
2133
2122
  FollowButtonVariant: Scalars['FollowButtonVariant']['output'];
2134
2123
  FullBleedTopper: TopperModel;
@@ -2543,18 +2532,11 @@ export type DesignResolvers<ContextType = QueryContext, ParentType extends Resol
2543
2532
  }>;
2544
2533
 
2545
2534
  export type FlourishResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Flourish'] = ResolversParentTypes['Flourish']> = ResolversObject<{
2546
- fallbackImage: Resolver<Maybe<ResolversTypes['FlourishFallback']>, ParentType, ContextType>;
2535
+ fallbackImage: Resolver<Maybe<ResolversTypes['FlourishSource']>, ParentType, ContextType>;
2547
2536
  type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2548
2537
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2549
2538
  }>;
2550
2539
 
2551
- export type FlourishFallbackResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['FlourishFallback'] = ResolversParentTypes['FlourishFallback']> = ResolversObject<{
2552
- height: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2553
- url: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2554
- width: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2555
- __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2556
- }>;
2557
-
2558
2540
  export type FlourishSourceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['FlourishSource'] = ResolversParentTypes['FlourishSource']> = ResolversObject<{
2559
2541
  format: Resolver<ResolversTypes['ImageFormat'], ParentType, ContextType>;
2560
2542
  height: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
@@ -2769,7 +2751,7 @@ export type LayoutImageResolvers<ContextType = QueryContext, ParentType extends
2769
2751
 
2770
2752
  export type LeadFlourishResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LeadFlourish'] = ResolversParentTypes['LeadFlourish']> = ResolversObject<{
2771
2753
  description: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2772
- fallbackImage: Resolver<ResolversTypes['FlourishSource'], ParentType, ContextType>;
2754
+ fallbackImage: Resolver<Maybe<ResolversTypes['FlourishSource']>, ParentType, ContextType>;
2773
2755
  id: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2774
2756
  type: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2775
2757
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -3259,7 +3241,6 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
3259
3241
  DeepPortraitTopper: DeepPortraitTopperResolvers<ContextType>;
3260
3242
  Design: DesignResolvers<ContextType>;
3261
3243
  Flourish: FlourishResolvers<ContextType>;
3262
- FlourishFallback: FlourishFallbackResolvers<ContextType>;
3263
3244
  FlourishSource: FlourishSourceResolvers<ContextType>;
3264
3245
  FollowButtonVariant: GraphQLScalarType;
3265
3246
  FullBleedTopper: FullBleedTopperResolvers<ContextType>;
@@ -45,7 +45,7 @@ describe('FlourishSource', () => {
45
45
 
46
46
  it('should return the correct flourishUrl', () => {
47
47
  expect(flourishSource.flourishUrl()).toBe(
48
- 'https://public.flourish.studio/test-type/1234/thumbnail?cacheBuster=5717196'
48
+ 'https://public.flourish.studio/test-type/1234/thumbnail?cacheBuster=952866'
49
49
  )
50
50
  })
51
51
 
@@ -70,9 +70,9 @@ describe('FlourishSource', () => {
70
70
  expect(decodeURIComponent(url)).toMatch(flourishImageUrlRegex)
71
71
  })
72
72
 
73
- it('should bust the cache of the fallback image every five minutes', async () => {
74
- const oldCacheBuster = '5717196'
75
- const newCacheBuster = '5717197'
73
+ it('should bust the cache of the fallback image every thirty minutes', async () => {
74
+ const oldCacheBuster = '952866'
75
+ const newCacheBuster = '952867'
76
76
 
77
77
  expect(flourishSource.flourishUrl()).toBe(
78
78
  `https://public.flourish.studio/test-type/1234/thumbnail?cacheBuster=${oldCacheBuster}`
@@ -85,7 +85,7 @@ describe('FlourishSource', () => {
85
85
  )
86
86
 
87
87
  newFlourishSource = new FlourishSource(mockFlourishData, context)
88
- jest.spyOn(Date, 'now').mockImplementation(() => 1715159100000) // 8 May 2024 09:05:00
88
+ jest.spyOn(Date, 'now').mockImplementation(() => 1715160840000) // 8 May 2024 09:34:00
89
89
  expect(newFlourishSource.flourishUrl()).toBe(
90
90
  `https://public.flourish.studio/test-type/1234/thumbnail?cacheBuster=${newCacheBuster}`
91
91
  )
@@ -8,19 +8,37 @@ type ImageMetadata = {
8
8
  height: number
9
9
  }
10
10
 
11
- type FlourishData =
12
- | {
13
- id: string
14
- type?: string
15
- description?: string
16
- }
17
- | null
18
- | undefined
11
+ type FlourishData = {
12
+ id: string
13
+ type?: string
14
+ description?: string
15
+ width?: number
16
+ height?: number
17
+ }
18
+
19
+ const DEFAULT_WIDTH = 2626
20
+ const DEFAULT_HEIGHT = 1459
19
21
 
20
22
  export class FlourishSource {
21
23
  #systemCode: string
22
24
  private imageMetadata?: ImageMetadata
23
25
 
26
+ static async createWithMetadata(data: FlourishData, context: QueryContext) {
27
+ const { width, height } = await FlourishSource.getImageMetadata(
28
+ data,
29
+ context
30
+ )
31
+
32
+ return new FlourishSource(
33
+ {
34
+ ...data,
35
+ width,
36
+ height,
37
+ },
38
+ context
39
+ )
40
+ }
41
+
24
42
  constructor(
25
43
  private flourishData: FlourishData,
26
44
  private context: QueryContext
@@ -29,11 +47,11 @@ export class FlourishSource {
29
47
  }
30
48
 
31
49
  id() {
32
- return this.flourishData?.id || ''
50
+ return this.flourishData.id
33
51
  }
34
52
 
35
53
  type() {
36
- return this.flourishData?.type || 'flourish'
54
+ return this.flourishData.type ?? 'flourish'
37
55
  }
38
56
 
39
57
  format() {
@@ -41,63 +59,63 @@ export class FlourishSource {
41
59
  }
42
60
 
43
61
  flourishUrl() {
44
- // Timestamp rounded to the nearest 5 minutes to bust the cache every five minutes
45
- const cacheBusterTimestamp = Math.floor(Date.now() / (5 * 60 * 1000))
46
-
47
- return `https://public.flourish.studio/${this.type()}/${this.id()}/thumbnail?cacheBuster=${cacheBusterTimestamp}`
62
+ return FlourishSource.fallbackUrl(this.flourishData)
48
63
  }
49
64
 
50
- private async getImageMetadata(): Promise<ImageMetadata> {
51
- const DEFAULT_WIDTH = 2626
52
- const DEFAULT_HEIGHT = 1459
65
+ private static fallbackUrl(data: FlourishData) {
66
+ // Timestamp rounded to the nearest 30 minutes to bust the cache every half an hour
67
+ const cacheBusterTimestamp = Math.floor(Date.now() / (30 * 60 * 1000))
53
68
 
54
- if (this.imageMetadata) {
55
- return this.imageMetadata
56
- }
69
+ return `https://public.flourish.studio/${data.type ?? 'flourish'}/${
70
+ data.id
71
+ }/thumbnail?cacheBuster=${cacheBusterTimestamp}`
72
+ }
73
+
74
+ static async getImageMetadata(
75
+ data: FlourishData,
76
+ context: QueryContext
77
+ ) {
78
+ const url = FlourishSource.fallbackUrl(data)
57
79
 
58
80
  try {
59
- const metadata = await this.context.dataSources.origami.getImageMetadata(
60
- this.flourishUrl()
61
- )
62
- this.imageMetadata = {
81
+ const metadata = await context.dataSources.origami.getImageMetadata(url)
82
+
83
+ return {
63
84
  width: metadata?.width || DEFAULT_WIDTH,
64
85
  height: metadata?.height || DEFAULT_HEIGHT,
65
86
  }
66
87
  } catch (error) {
67
88
  if (isError(error)) {
68
- this.context.logger.warn({
89
+ context.logger.warn({
69
90
  event: 'RECOVERABLE_ERROR',
70
91
  error: new OperationalError({
71
92
  code: 'FLOURISH_IMAGE_METADATA_ERROR',
72
- message: `Error getting image dimensions for Flourish fallback image ${this.flourishUrl()}`,
93
+ message: `Error getting image dimensions for Flourish fallback image ${url}`,
73
94
  cause: error,
74
95
  }),
75
96
  })
76
97
  }
77
- this.imageMetadata = {
98
+
99
+ return {
78
100
  width: DEFAULT_WIDTH,
79
101
  height: DEFAULT_HEIGHT,
80
102
  }
81
103
  }
82
- return this.imageMetadata
83
104
  }
84
- async width() {
85
- const imageMetadata = await this.getImageMetadata()
86
- return imageMetadata.width
105
+
106
+ width() {
107
+ return this.flourishData.width ?? DEFAULT_WIDTH
87
108
  }
88
109
 
89
- async height() {
90
- const imageMetadata = await this.getImageMetadata()
91
- return imageMetadata.height
110
+ height() {
111
+ return this.flourishData.height ?? DEFAULT_HEIGHT
92
112
  }
93
113
 
94
- async url() {
95
- const width = await this.width()
96
- const imageServiceWrappedUrl = imageServiceUrl({
114
+ url() {
115
+ return imageServiceUrl({
97
116
  url: this.flourishUrl(),
98
117
  systemCode: 'cp-content-pipeline',
99
- width,
118
+ width: this.width(),
100
119
  })
101
- return imageServiceWrappedUrl
102
120
  }
103
121
  }
@@ -1,10 +1,8 @@
1
1
  import { CAPIImage } from './Image'
2
- import { jest } from '@jest/globals'
2
+ import logger from '@dotcom-reliability-kit/logger'
3
3
  import type { QueryContext } from '..'
4
4
  import { Image } from './schemas/capi/internal-content'
5
- import { OrigamiImageDataSource } from '../datasources/origami-image'
6
5
  import { MAX_IMAGE_WIDTH } from '../helpers/imageService'
7
- import type { ImageSource } from '../generated'
8
6
 
9
7
  describe('Image', () => {
10
8
  const mockImage = {
@@ -13,6 +11,8 @@ describe('Image', () => {
13
11
  title: 'title',
14
12
  description: 'description',
15
13
  binaryUrl: 'cloudfront.com/image',
14
+ pixelWidth: 5000,
15
+ pixelHeight: 1000,
16
16
  }
17
17
 
18
18
  const graphic = {
@@ -20,34 +20,17 @@ describe('Image', () => {
20
20
  type: 'http://www.ft.com/ontology/content/Graphic' as const,
21
21
  }
22
22
 
23
- const getImageMetadata = jest.fn<OrigamiImageDataSource['getImageMetadata']>()
24
-
25
23
  const context = {
26
- dataSources: {
27
- origami: {
28
- getImageMetadata,
29
- },
30
- capi: {},
31
- vanityUrls: {},
32
- },
24
+ logger,
33
25
  systemCode: 'image-test',
34
26
  } as unknown as QueryContext
35
27
 
36
- beforeEach(() => {
37
- getImageMetadata.mockReset()
38
- })
39
-
40
28
  describe('sourceSet', () => {
41
29
  describe('An image that is provided with a high resolution', () => {
42
- let sourceSet: ImageSource[]
43
-
44
- beforeAll(async () => {
45
- getImageMetadata.mockResolvedValue({ width: 5000, height: 1000 })
30
+ it('transforms the URL to use the Origami Image Service', async () => {
46
31
  const model = new CAPIImage(mockImage, context)
47
- sourceSet = await model.sourceSet({ width: 1000 })
48
- })
32
+ const sourceSet = await model.sourceSet({ width: 1000 })
49
33
 
50
- it('transforms the URL to use the Origami Image Service', () => {
51
34
  expect(
52
35
  sourceSet.every((source) =>
53
36
  source.url.startsWith(
@@ -57,7 +40,10 @@ describe('Image', () => {
57
40
  ).toBeTruthy()
58
41
  })
59
42
 
60
- it('resizes the image to the requested width', () => {
43
+ it('resizes the image to the requested width', async () => {
44
+ const model = new CAPIImage(mockImage, context)
45
+ const sourceSet = await model.sourceSet({ width: 1000 })
46
+
61
47
  expect(
62
48
  sourceSet.every(
63
49
  (source) =>
@@ -66,7 +52,9 @@ describe('Image', () => {
66
52
  ).toBeTruthy()
67
53
  })
68
54
 
69
- it('includes sourceSet for all the possible resolutions we can display the image at, given the requested width and the original source width', () => {
55
+ it('includes sourceSet for all the possible resolutions we can display the image at, given the requested width and the original source width', async () => {
56
+ const model = new CAPIImage(mockImage, context)
57
+ const sourceSet = await model.sourceSet({ width: 1000 })
70
58
  expect(sourceSet.length).toEqual(5)
71
59
  expect(sourceSet[0]?.dpr).toEqual(1)
72
60
  expect(sourceSet[0]?.url).toMatch(/dpr=1/)
@@ -77,8 +65,10 @@ describe('Image', () => {
77
65
 
78
66
  describe("An image that that isn't wide enough to generate high-resolution sourceSet for", () => {
79
67
  it('returns an array with a single resolution iamge service UR', async () => {
80
- getImageMetadata.mockResolvedValue({ width: 1500, height: 500 })
81
- const model = new CAPIImage(mockImage, context)
68
+ const model = new CAPIImage(
69
+ { ...mockImage, pixelWidth: 1500, pixelHeight: 500 },
70
+ context
71
+ )
82
72
  const sourceSet = await model.sourceSet({ width: 1000 })
83
73
  expect(sourceSet.length).toEqual(1)
84
74
  expect(sourceSet[0]).toEqual({
@@ -91,8 +81,10 @@ describe('Image', () => {
91
81
 
92
82
  describe('An image that that is smaller than the width that was requested', () => {
93
83
  it('returns the image at the same width as the original', async () => {
94
- getImageMetadata.mockResolvedValue({ width: 500, height: 500 })
95
- const model = new CAPIImage(mockImage, context)
84
+ const model = new CAPIImage(
85
+ { ...mockImage, pixelWidth: 500, pixelHeight: 500 },
86
+ context
87
+ )
96
88
  const sourceSet = await model.sourceSet({ width: 1000 })
97
89
  expect(sourceSet.length).toEqual(1)
98
90
  expect(sourceSet[0]).toEqual({
@@ -105,8 +97,10 @@ describe('Image', () => {
105
97
 
106
98
  describe('An image that that is bigger than the maximum width that was requested', () => {
107
99
  it('returns the image at the same width as the maximum width', async () => {
108
- getImageMetadata.mockResolvedValue({ width: 4000, height: 4000 })
109
- const model = new CAPIImage(mockImage, context)
100
+ const model = new CAPIImage(
101
+ { ...mockImage, pixelWidth: 4000, pixelHeight: 4000 },
102
+ context
103
+ )
110
104
  const sourceSet = await model.sourceSet({ width: MAX_IMAGE_WIDTH })
111
105
  expect(sourceSet.length).toEqual(1)
112
106
  expect(sourceSet[0]?.width).toEqual(MAX_IMAGE_WIDTH)
@@ -115,8 +109,10 @@ describe('Image', () => {
115
109
 
116
110
  describe('A large image with the maxDpr argumet passed', () => {
117
111
  it('only returns sourceSet up to and including the maxDpr', async () => {
118
- getImageMetadata.mockResolvedValue({ width: 5000, height: 500 })
119
- const model = new CAPIImage(mockImage, context)
112
+ const model = new CAPIImage(
113
+ { ...mockImage, pixelWidth: 5000, pixelHeight: 500 },
114
+ context
115
+ )
120
116
  const sourceSet = await model.sourceSet({ width: 1000, maxDpr: 2 })
121
117
  expect(sourceSet.length).toEqual(2)
122
118
  expect(sourceSet[0]?.dpr).toEqual(1)
@@ -137,26 +133,6 @@ describe('Image', () => {
137
133
  await expect(
138
134
  model.sourceSet({ width: 1000, maxDpr: 2 })
139
135
  ).rejects.toThrow('not-a-uuid is not a valid Content API Image ID')
140
- expect(getImageMetadata).not.toHaveBeenCalled()
141
- })
142
- })
143
-
144
- describe('When the image service fails to return metadata', () => {
145
- let sourceSet: ImageSource[]
146
- beforeEach(async () => {
147
- getImageMetadata.mockRejectedValue(null)
148
- const model = new CAPIImage(mockImage, context)
149
- sourceSet = await model.sourceSet({ width: 3000 })
150
- })
151
- it('passes the requested width to the image service', async () => {
152
- expect(sourceSet.length).toEqual(1)
153
- expect(sourceSet[0]?.width).toEqual(3000)
154
- expect(sourceSet[0]?.url).toMatch(/width=3000/)
155
- })
156
- it('falls back to a single DPR source', async () => {
157
- expect(sourceSet.length).toEqual(1)
158
- expect(sourceSet[0]?.dpr).toEqual(1)
159
- expect(sourceSet[0]?.url).toMatch(/dpr=1/)
160
136
  })
161
137
  })
162
138
  })
@@ -235,17 +211,18 @@ describe('Image', () => {
235
211
  })
236
212
 
237
213
  describe('width/height', () => {
238
- it('returns the original width and height from the image service metadata', async () => {
239
- getImageMetadata.mockResolvedValue({ width: 100, height: 200 })
214
+ it('returns the original width and height from capi data', async () => {
240
215
  const model = new CAPIImage(mockImage, context)
241
216
  const { width, height } = (await model.dimensions()) || {}
242
- expect(width).toEqual(100)
243
- expect(height).toEqual(200)
217
+ expect(width).toEqual(5000)
218
+ expect(height).toEqual(1000)
244
219
  })
245
220
 
246
- it('returns null if the image service metadata call fails', async () => {
247
- getImageMetadata.mockRejectedValue(null)
248
- const model = new CAPIImage(mockImage, context)
221
+ it('returns null if no width and height in capi data', async () => {
222
+ const model = new CAPIImage(
223
+ { ...mockImage, pixelHeight: undefined, pixelWidth: undefined },
224
+ context
225
+ )
249
226
  const dimensions = await model.dimensions()
250
227
  expect(dimensions).toEqual(null)
251
228
  })
@@ -11,8 +11,7 @@ import {
11
11
  validLiteralUnionValue,
12
12
  } from '../resolvers/literal-union'
13
13
  import { ImageFormat, ImageType } from '../resolvers/scalars'
14
- import isError from '../helpers/isError'
15
- import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
14
+ import { BaseError } from '@dotcom-reliability-kit/errors'
16
15
  import type { ImageSource } from '../generated'
17
16
 
18
17
  export type ImageSourceArgs = {
@@ -126,7 +125,7 @@ export class CAPIImage implements Image {
126
125
  args.maxDpr || Infinity,
127
126
  Math.floor(dimensions.width / maxAllowedWidth)
128
127
  )
129
- : 1
128
+ : 2
130
129
 
131
130
  const resolutions = [...Array(maxDpr + 1).keys()].slice(1)
132
131
 
@@ -157,25 +156,18 @@ export class CAPIImage implements Image {
157
156
  height: this.capiImage.pixelHeight,
158
157
  }
159
158
  }
160
- try {
161
- const imageMetadata = await this.#dataSources.origami.getImageMetadata(
162
- this.capiImage.binaryUrl
163
- )
164
- return imageMetadata
165
- } catch (error) {
166
- if (isError(error)) {
167
- this.context.logger.warn({
168
- event: 'RECOVERABLE_ERROR',
169
- error: new OperationalError({
170
- code: 'IMAGE_DIMENSIONS_ERROR',
171
- message: `Failed to get dimensions for ${this.capiImage.binaryUrl}`,
172
- url: this.capiImage.binaryUrl,
173
- cause: error,
174
- }),
175
- })
176
- }
177
- return null
178
- }
159
+
160
+ // HACK:KB:20241029 graphics and images in layouts don't have pixelWidth/
161
+ // pixelHeight because of reasons. we want to avoid calling Image Service
162
+ // to improve performance. skip calling Image Service for the time being
163
+ this.context.logger.warn({
164
+ event: 'SKIPPING_IMAGE_DIMENSIONS',
165
+ message: 'Not calling Image Service to get graphics dimensions',
166
+ url: this.capiImage.binaryUrl,
167
+ type: this.type(),
168
+ })
169
+
170
+ return null
179
171
  }
180
172
 
181
173
  credit() {
@@ -7,18 +7,13 @@ import cloneDeep from 'clone-deep'
7
7
 
8
8
  const context = {} as unknown as QueryContext
9
9
 
10
- jest.mock('./FlourishSource', () => {
11
- return {
12
- FlourishSource: jest.fn().mockImplementation(() => ({})),
13
- }
14
- })
15
-
16
10
  const leadFlourishData = {
17
11
  id: 'test-id',
18
12
  description: 'test-description',
19
13
  type: 'test-type',
20
14
  }
21
15
 
16
+
22
17
  describe('LeadFlourish', () => {
23
18
  let leadFlourish: LeadFlourish
24
19
 
@@ -30,15 +25,17 @@ describe('LeadFlourish', () => {
30
25
  layoutWidth: 'full-width',
31
26
  backgroundColour: 'paper',
32
27
  }
28
+ jest.spyOn(FlourishSource, 'getImageMetadata').mockResolvedValue({
29
+ width: 800,
30
+ height: 600
31
+ })
33
32
  const capiResponse = new CapiResponse(clonedBase, context)
34
33
  leadFlourish = new LeadFlourish(capiResponse, context)
35
34
  })
36
35
 
37
36
  describe('fallbackImage', () => {
38
37
  it('should instantiate a new FlourishSource', async () => {
39
- await leadFlourish.fallbackImage()
40
-
41
- expect(FlourishSource).toHaveBeenCalledWith(leadFlourishData, context)
38
+ await expect(leadFlourish.fallbackImage()).resolves.toBeInstanceOf(FlourishSource)
42
39
  })
43
40
  })
44
41
 
@@ -13,7 +13,11 @@ export class LeadFlourish {
13
13
 
14
14
  async fallbackImage() {
15
15
  const flourishData = this.capiResponse.leadFlourish()
16
- return new FlourishSource(flourishData, this.context)
16
+ if (!flourishData) {
17
+ return null
18
+ }
19
+
20
+ return FlourishSource.createWithMetadata(flourishData, this.context)
17
21
  }
18
22
 
19
23
  id() {
@@ -147,7 +147,7 @@ export const ClipSet = z.object({
147
147
  const DataSource = z.object({
148
148
  binaryUrl: z.string(),
149
149
  duration: z.number(),
150
- filesize: z.number(),
150
+ filesize: z.number().optional(),
151
151
  mediaType: z.string(),
152
152
  })
153
153