@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.
- package/CHANGELOG.md +18 -0
- package/lib/generated/index.d.ts +4 -21
- package/lib/model/FlourishSource.d.ts +12 -5
- package/lib/model/FlourishSource.js +32 -28
- package/lib/model/FlourishSource.js.map +1 -1
- package/lib/model/FlourishSource.test.js +5 -5
- package/lib/model/FlourishSource.test.js.map +1 -1
- package/lib/model/Image.js +11 -20
- package/lib/model/Image.js.map +1 -1
- package/lib/model/Image.test.js +24 -54
- package/lib/model/Image.test.js.map +1 -1
- package/lib/model/LeadFlourish.d.ts +1 -1
- package/lib/model/LeadFlourish.js +4 -1
- package/lib/model/LeadFlourish.js.map +1 -1
- package/lib/model/LeadFlourish.test.js +5 -7
- package/lib/model/LeadFlourish.test.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +3 -3
- package/lib/model/schemas/capi/audio.d.ts +5 -5
- package/lib/model/schemas/capi/base-schema.d.ts +5 -5
- package/lib/model/schemas/capi/base-schema.js +1 -1
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +3 -3
- package/lib/model/schemas/capi/custom-code-component.d.ts +3 -3
- package/lib/model/schemas/capi/index.d.ts +20 -20
- package/lib/model/schemas/capi/live-blog-package.d.ts +3 -3
- package/lib/model/schemas/capi/placeholder.d.ts +3 -3
- package/lib/model/schemas/capi/video.d.ts +3 -3
- package/lib/resolvers/content-tree/references/Flourish.d.ts +9 -19
- package/lib/resolvers/content-tree/references/Flourish.js +10 -32
- package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
- package/lib/resolvers/content-tree/references/Flourish.test.js +3 -3
- package/lib/resolvers/content-tree/references/Flourish.test.js.map +1 -1
- package/lib/resolvers/content-tree/references/index.d.ts +2 -2
- package/lib/resolvers/content-tree/references/index.js +1 -1
- package/lib/resolvers/content-tree/references/index.js.map +1 -1
- package/lib/resolvers/index.d.ts +1 -2
- package/lib/resolvers/leadFlourish.d.ts +1 -1
- package/package.json +1 -1
- package/src/generated/index.ts +4 -23
- package/src/model/FlourishSource.test.ts +5 -5
- package/src/model/FlourishSource.ts +57 -39
- package/src/model/Image.test.ts +37 -60
- package/src/model/Image.ts +14 -22
- package/src/model/LeadFlourish.test.ts +6 -9
- package/src/model/LeadFlourish.ts +5 -1
- package/src/model/schemas/capi/base-schema.ts +1 -1
- package/src/resolvers/content-tree/references/Flourish.test.ts +4 -3
- package/src/resolvers/content-tree/references/Flourish.ts +16 -32
- package/src/resolvers/content-tree/references/index.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/leadFlourish.graphql +1 -1
- 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,
|
|
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,
|
|
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"}
|
package/lib/resolvers/index.d.ts
CHANGED
|
@@ -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
package/src/generated/index.ts
CHANGED
|
@@ -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<
|
|
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
|
|
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['
|
|
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']
|
|
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=
|
|
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
|
|
74
|
-
const oldCacheBuster = '
|
|
75
|
-
const newCacheBuster = '
|
|
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(() =>
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
50
|
+
return this.flourishData.id
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
type() {
|
|
36
|
-
return this.flourishData
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
const
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
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 ${
|
|
93
|
+
message: `Error getting image dimensions for Flourish fallback image ${url}`,
|
|
73
94
|
cause: error,
|
|
74
95
|
}),
|
|
75
96
|
})
|
|
76
97
|
}
|
|
77
|
-
|
|
98
|
+
|
|
99
|
+
return {
|
|
78
100
|
width: DEFAULT_WIDTH,
|
|
79
101
|
height: DEFAULT_HEIGHT,
|
|
80
102
|
}
|
|
81
103
|
}
|
|
82
|
-
return this.imageMetadata
|
|
83
104
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
105
|
+
|
|
106
|
+
width() {
|
|
107
|
+
return this.flourishData.width ?? DEFAULT_WIDTH
|
|
87
108
|
}
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return imageMetadata.height
|
|
110
|
+
height() {
|
|
111
|
+
return this.flourishData.height ?? DEFAULT_HEIGHT
|
|
92
112
|
}
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|
package/src/model/Image.test.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { CAPIImage } from './Image'
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
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(
|
|
243
|
-
expect(height).toEqual(
|
|
217
|
+
expect(width).toEqual(5000)
|
|
218
|
+
expect(height).toEqual(1000)
|
|
244
219
|
})
|
|
245
220
|
|
|
246
|
-
it('returns null if
|
|
247
|
-
|
|
248
|
-
|
|
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
|
})
|
package/src/model/Image.ts
CHANGED
|
@@ -11,8 +11,7 @@ import {
|
|
|
11
11
|
validLiteralUnionValue,
|
|
12
12
|
} from '../resolvers/literal-union'
|
|
13
13
|
import { ImageFormat, ImageType } from '../resolvers/scalars'
|
|
14
|
-
import
|
|
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
|
-
:
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
16
|
+
if (!flourishData) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return FlourishSource.createWithMetadata(flourishData, this.context)
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
id() {
|