@financial-times/cp-content-pipeline-schema 3.7.2 → 3.8.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 (102) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/datasources/capi.d.ts +4 -4
  3. package/lib/datasources/capi.js +4 -4
  4. package/lib/datasources/capi.js.map +1 -1
  5. package/lib/datasources/capi.test.js +3 -3
  6. package/lib/datasources/capi.test.js.map +1 -1
  7. package/lib/fixtures/dummyContext.js +2 -2
  8. package/lib/fixtures/dummyContext.js.map +1 -1
  9. package/lib/generated/index.d.ts +41 -23
  10. package/lib/model/Byline.d.ts +2 -2
  11. package/lib/model/Byline.js.map +1 -1
  12. package/lib/model/{CapiResponse.d.ts → Content.d.ts} +13 -12
  13. package/lib/model/{CapiResponse.js → Content.js} +14 -7
  14. package/lib/model/Content.js.map +1 -0
  15. package/lib/model/{CapiResponse.test.js → Content.test.js} +56 -15
  16. package/lib/model/Content.test.js.map +1 -0
  17. package/lib/model/LeadFlourish.d.ts +2 -2
  18. package/lib/model/LeadFlourish.js.map +1 -1
  19. package/lib/model/LeadFlourish.test.js +3 -3
  20. package/lib/model/LeadFlourish.test.js.map +1 -1
  21. package/lib/model/{CapiList.d.ts → List.d.ts} +5 -5
  22. package/lib/model/{CapiList.js → List.js} +5 -5
  23. package/lib/model/List.js.map +1 -0
  24. package/lib/model/RichText.d.ts +2 -2
  25. package/lib/model/RichText.js.map +1 -1
  26. package/lib/model/Topper.d.ts +2 -2
  27. package/lib/model/Topper.js.map +1 -1
  28. package/lib/model/Topper.test.js +22 -22
  29. package/lib/model/Topper.test.js.map +1 -1
  30. package/lib/model/schemas/capi/article.d.ts +5 -1
  31. package/lib/model/schemas/capi/article.js +1 -0
  32. package/lib/model/schemas/capi/article.js.map +1 -1
  33. package/lib/model/schemas/capi/audio.d.ts +5 -1
  34. package/lib/model/schemas/capi/audio.js +1 -0
  35. package/lib/model/schemas/capi/audio.js.map +1 -1
  36. package/lib/model/schemas/capi/base-schema.d.ts +5 -0
  37. package/lib/model/schemas/capi/base-schema.js +1 -0
  38. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  39. package/lib/model/schemas/capi/content-package.d.ts +5 -1
  40. package/lib/model/schemas/capi/content-package.js +1 -0
  41. package/lib/model/schemas/capi/content-package.js.map +1 -1
  42. package/lib/model/schemas/capi/custom-code-component.d.ts +3 -0
  43. package/lib/model/schemas/capi/index.d.ts +28 -5
  44. package/lib/model/schemas/capi/live-blog-package.d.ts +5 -1
  45. package/lib/model/schemas/capi/live-blog-package.js +1 -0
  46. package/lib/model/schemas/capi/live-blog-package.js.map +1 -1
  47. package/lib/model/schemas/capi/placeholder.d.ts +5 -1
  48. package/lib/model/schemas/capi/placeholder.js +1 -0
  49. package/lib/model/schemas/capi/placeholder.js.map +1 -1
  50. package/lib/model/schemas/capi/video.d.ts +5 -1
  51. package/lib/model/schemas/capi/video.js +1 -0
  52. package/lib/model/schemas/capi/video.js.map +1 -1
  53. package/lib/resolvers/content-tree/references/CustomCodeComponent.js.map +1 -1
  54. package/lib/resolvers/content-tree/references/Recommended.d.ts +1 -1
  55. package/lib/resolvers/content-tree/references/index.d.ts +2 -2
  56. package/lib/resolvers/content-tree/tagMappings.d.ts +2 -2
  57. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  58. package/lib/resolvers/content-tree/updateTreeWithReferenceIds.d.ts +2 -2
  59. package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js.map +1 -1
  60. package/lib/resolvers/content.d.ts +251 -235
  61. package/lib/resolvers/content.js +27 -0
  62. package/lib/resolvers/content.js.map +1 -1
  63. package/lib/resolvers/core.d.ts +4 -4
  64. package/lib/resolvers/core.js +12 -2
  65. package/lib/resolvers/core.js.map +1 -1
  66. package/lib/resolvers/index.d.ts +276 -260
  67. package/lib/resolvers/list.d.ts +9 -9
  68. package/lib/resolvers/teaser.d.ts +13 -13
  69. package/package.json +1 -1
  70. package/queries/article.graphql +19 -0
  71. package/src/datasources/capi.test.ts +3 -3
  72. package/src/datasources/capi.ts +6 -9
  73. package/src/fixtures/dummyContext.ts +2 -2
  74. package/src/generated/index.ts +41 -23
  75. package/src/model/Byline.ts +2 -2
  76. package/src/model/{CapiResponse.test.ts → Content.test.ts} +65 -17
  77. package/src/model/{CapiResponse.ts → Content.ts} +24 -17
  78. package/src/model/LeadFlourish.test.ts +6 -5
  79. package/src/model/LeadFlourish.ts +2 -5
  80. package/src/model/{CapiList.ts → List.ts} +5 -5
  81. package/src/model/RichText.ts +2 -2
  82. package/src/model/Topper.test.ts +23 -26
  83. package/src/model/Topper.ts +2 -5
  84. package/src/model/schemas/capi/article.ts +1 -0
  85. package/src/model/schemas/capi/audio.ts +1 -0
  86. package/src/model/schemas/capi/base-schema.ts +1 -0
  87. package/src/model/schemas/capi/content-package.ts +1 -0
  88. package/src/model/schemas/capi/live-blog-package.ts +1 -0
  89. package/src/model/schemas/capi/placeholder.ts +1 -0
  90. package/src/model/schemas/capi/video.ts +1 -0
  91. package/src/resolvers/content-tree/references/CustomCodeComponent.ts +2 -2
  92. package/src/resolvers/content-tree/references/index.ts +2 -2
  93. package/src/resolvers/content-tree/tagMappings.ts +2 -2
  94. package/src/resolvers/content-tree/updateTreeWithReferenceIds.ts +2 -2
  95. package/src/resolvers/content.ts +27 -0
  96. package/src/resolvers/core.ts +12 -2
  97. package/tsconfig.tsbuildinfo +1 -1
  98. package/typedefs/content.graphql +19 -1
  99. package/lib/model/CapiList.js.map +0 -1
  100. package/lib/model/CapiResponse.js.map +0 -1
  101. package/lib/model/CapiResponse.test.js.map +0 -1
  102. /package/lib/model/{CapiResponse.test.d.ts → Content.test.d.ts} +0 -0
@@ -1,13 +1,13 @@
1
- import { CapiResponse } from './CapiResponse'
1
+ import { Content } from './Content'
2
2
  import { baseCapiObject } from '../fixtures/capiObject'
3
3
  import cloneDeep from 'clone-deep'
4
4
  import context from '../fixtures/dummyContext'
5
5
 
6
- describe('CAPI response', () => {
6
+ describe('Content', () => {
7
7
  describe('Content ID', () => {
8
8
  test('returns the UUID', () => {
9
9
  const article = cloneDeep(baseCapiObject)
10
- const capiResponse = new CapiResponse(article, context)
10
+ const capiResponse = new Content(article, context)
11
11
 
12
12
  expect(capiResponse.id()).toEqual('3071cae8-a27c-4ab4-a0d4-7bb00df1d477')
13
13
  })
@@ -17,7 +17,7 @@ describe('CAPI response', () => {
17
17
  test(`uses first entry in types if there's no type`, () => {
18
18
  const article = cloneDeep(baseCapiObject)
19
19
  delete article.type
20
- const capiResponseArticle = new CapiResponse(article, context)
20
+ const capiResponseArticle = new Content(article, context)
21
21
 
22
22
  expect(capiResponseArticle.type()).toEqual('Article')
23
23
  })
@@ -28,11 +28,8 @@ describe('CAPI response', () => {
28
28
  ...baseCapiObject,
29
29
  type: 'http://www.ft.com/ontology/content/LiveBlogPackage',
30
30
  })
31
- const capiResponseArticle = new CapiResponse(article, context)
32
- const capiResponseLiveBlogPackage = new CapiResponse(
33
- liveBlogPackage,
34
- context
35
- )
31
+ const capiResponseArticle = new Content(article, context)
32
+ const capiResponseLiveBlogPackage = new Content(liveBlogPackage, context)
36
33
 
37
34
  expect(capiResponseArticle.type()).toEqual('Article')
38
35
  expect(capiResponseLiveBlogPackage.type()).toEqual('LiveBlogPackage')
@@ -43,7 +40,7 @@ describe('CAPI response', () => {
43
40
  ...baseCapiObject,
44
41
  type: 'http://www.ft.com/ontology/content/Unknown',
45
42
  })
46
- const capiResponse = new CapiResponse(unknown, context)
43
+ const capiResponse = new Content(unknown, context)
47
44
  expect(() => capiResponse.type()).toThrowError(
48
45
  'Content type is invalid: http://www.ft.com/ontology/content/Unknown'
49
46
  )
@@ -53,7 +50,7 @@ describe('CAPI response', () => {
53
50
  describe('Publish timestamp', () => {
54
51
  test('generates a timestamp from the published date', () => {
55
52
  const article = cloneDeep(baseCapiObject)
56
- const capiResponse = new CapiResponse(article, context)
53
+ const capiResponse = new Content(article, context)
57
54
 
58
55
  expect(capiResponse.publishedTimestamp()).toEqual(1712052009935)
59
56
  })
@@ -62,7 +59,7 @@ describe('CAPI response', () => {
62
59
  describe('Modified timestamp', () => {
63
60
  test('generates a timestamp from the last modified date', () => {
64
61
  const article = cloneDeep(baseCapiObject)
65
- const capiResponse = new CapiResponse(article, context)
62
+ const capiResponse = new Content(article, context)
66
63
 
67
64
  expect(capiResponse.modifiedTimestamp()).toEqual(1712061296789)
68
65
  })
@@ -92,7 +89,7 @@ describe('CAPI response', () => {
92
89
  ],
93
90
  })
94
91
 
95
- const capiResponse = new CapiResponse(liveBlogPackage, context)
92
+ const capiResponse = new Content(liveBlogPackage, context)
96
93
  const liveBlogPosts = await capiResponse.liveBlogPosts()
97
94
  expect(liveBlogPosts[0]?.id()).toEqual(
98
95
  '00000000-0000-0000-0000-000000000002'
@@ -122,7 +119,7 @@ describe('CAPI response', () => {
122
119
  }
123
120
  }),
124
121
  })
125
- const capiResponse = new CapiResponse(liveBlogPackage, context)
122
+ const capiResponse = new Content(liveBlogPackage, context)
126
123
 
127
124
  test('returns all posts when no argument specified', async () => {
128
125
  const liveBlogPostsConnection =
@@ -245,7 +242,7 @@ describe('CAPI response', () => {
245
242
  'http://www.ft.com/thing/724b5e36-6d45-4cf1-b1c2-3f676b21f21b',
246
243
  ],
247
244
  })
248
- const capiResponse = new CapiResponse(article, context)
245
+ const capiResponse = new Content(article, context)
249
246
 
250
247
  expect(capiResponse.isPartnerContent()).toBe(true)
251
248
  })
@@ -254,7 +251,7 @@ describe('CAPI response', () => {
254
251
  ...baseCapiObject,
255
252
  publication: null,
256
253
  })
257
- const capiResponse = new CapiResponse(article, context)
254
+ const capiResponse = new Content(article, context)
258
255
 
259
256
  expect(capiResponse.isPartnerContent()).toBe(false)
260
257
  })
@@ -268,7 +265,7 @@ describe('CAPI response', () => {
268
265
  const timingOutContext = cloneDeep(context)
269
266
  timingOutContext.dataSources.capi.getPerson = () =>
270
267
  Promise.reject(new DOMException(undefined, 'TimeoutError'))
271
- const capiResponse = new CapiResponse(article, timingOutContext)
268
+ const capiResponse = new Content(article, timingOutContext)
272
269
 
273
270
  const authors = await capiResponse.authors()
274
271
  const primaryAuthor = await capiResponse.primaryAuthor()
@@ -277,4 +274,55 @@ describe('CAPI response', () => {
277
274
  expect(primaryAuthor?.['person'].id).toBe(authorAnnotation.id)
278
275
  })
279
276
  })
277
+
278
+ describe('isFTEdit', () => {
279
+ test('identifies articles that are part of an FT Edit edition', () => {
280
+ const article = cloneDeep({
281
+ ...baseCapiObject,
282
+ internalAnalyticsTags: 'ftedit',
283
+ })
284
+ const capiResponse = new Content(article, context)
285
+
286
+ expect(capiResponse.isFTEdit()).toBe(true)
287
+ })
288
+
289
+ test('identifies articles that are not part of an FT Edit edition', () => {
290
+ const article = cloneDeep({
291
+ ...baseCapiObject,
292
+ internalAnalyticsTags: 'test',
293
+ })
294
+ const capiResponse = new Content(article, context)
295
+
296
+ expect(capiResponse.isFTEdit()).toBe(false)
297
+ })
298
+
299
+ test('ignores typos', () => {
300
+ const article = cloneDeep({
301
+ ...baseCapiObject,
302
+ internalAnalyticsTags: 'ft edit',
303
+ })
304
+ const capiResponse = new Content(article, context)
305
+
306
+ expect(capiResponse.isFTEdit()).toBe(false)
307
+ })
308
+
309
+ test('handles multiple values', () => {
310
+ const article = cloneDeep({
311
+ ...baseCapiObject,
312
+ internalAnalyticsTags: 'test,ftedit,test2',
313
+ })
314
+ const capiResponse = new Content(article, context)
315
+
316
+ expect(capiResponse.isFTEdit()).toBe(true)
317
+ })
318
+
319
+ test('handles missing `internalAnalyticsTags` property', () => {
320
+ const article = cloneDeep({
321
+ ...baseCapiObject,
322
+ })
323
+ const capiResponse = new Content(article, context)
324
+
325
+ expect(capiResponse.isFTEdit()).toBe(false)
326
+ })
327
+ })
280
328
  })
@@ -100,18 +100,18 @@ const baselineContentSchema = z.object({
100
100
 
101
101
  type BaselineContent = z.infer<typeof baselineContentSchema>
102
102
 
103
- export class CapiResponse {
103
+ export class Content {
104
104
  constructor(
105
105
  private capiData: ContentTypeSchemas,
106
106
  private context: QueryContext,
107
- private packageContainer?: CapiResponse
107
+ private packageContainer?: Content
108
108
  ) {}
109
109
 
110
110
  static fromJSON(
111
111
  content: unknown,
112
112
  context: QueryContext,
113
- packageContainer?: CapiResponse
114
- ): CapiResponse {
113
+ packageContainer?: Content
114
+ ): Content {
115
115
  // if the content doesn't meet the baseline schema, something's gone
116
116
  // unexpectedly very wrong. throwing an non-operational error is the
117
117
  // right thing to do in that case, so we call .parse not .safeParse
@@ -160,11 +160,7 @@ export class CapiResponse {
160
160
 
161
161
  // we can "safely" cast content to the schema return types here. if the type is
162
162
  // incorrect, we've at least logged that error.
163
- return new CapiResponse(
164
- content as ContentTypeSchemas,
165
- context,
166
- packageContainer
167
- )
163
+ return new Content(content as ContentTypeSchemas, context, packageContainer)
168
164
  }
169
165
 
170
166
  body() {
@@ -456,7 +452,7 @@ export class CapiResponse {
456
452
  if ('title' in this.capiData) {
457
453
  const clone = cloneDeep(this.capiData)
458
454
  clone.title = title
459
- return new CapiResponse(clone, this.context, this.packageContainer)
455
+ return new Content(clone, this.context, this.packageContainer)
460
456
  }
461
457
  return this
462
458
  }
@@ -540,7 +536,7 @@ export class CapiResponse {
540
536
 
541
537
  teaser() {
542
538
  const clone = cloneDeep(this.capiData)
543
- return new CapiResponse(clone, this.context, this.packageContainer)
539
+ return new Content(clone, this.context, this.packageContainer)
544
540
  }
545
541
 
546
542
  isEditorsChoice() {
@@ -710,7 +706,7 @@ export class CapiResponse {
710
706
  async contains({
711
707
  surroundingArticles,
712
708
  fromId,
713
- }: Partial<ContentPackageContainsArgs> = {}): Promise<CapiResponse[] | null> {
709
+ }: Partial<ContentPackageContainsArgs> = {}): Promise<Content[] | null> {
714
710
  let edges
715
711
  if (fromId && surroundingArticles) {
716
712
  const afterId = await this.containsConnection({
@@ -734,7 +730,7 @@ export class CapiResponse {
734
730
  transformer?: (
735
731
  contains: LiveBlogPackage['contains']
736
732
  ) => LiveBlogPackage['contains']
737
- ): Promise<Connection<CapiResponse>> {
733
+ ): Promise<Connection<Content>> {
738
734
  if ('contains' in this.capiData) {
739
735
  const { contains: containsUntransformed } = this.capiData
740
736
  const contains = transformer
@@ -790,7 +786,7 @@ export class CapiResponse {
790
786
 
791
787
  const edges = results
792
788
  .filter(
793
- (result): result is PromiseFulfilledResult<CapiResponse> =>
789
+ (result): result is PromiseFulfilledResult<Content> =>
794
790
  result.status === 'fulfilled'
795
791
  )
796
792
  .map((result) => {
@@ -849,7 +845,7 @@ export class CapiResponse {
849
845
 
850
846
  async liveBlogPosts(
851
847
  args?: LiveBlogPackageLiveBlogPostsArgs
852
- ): Promise<CapiResponse[] | []> {
848
+ ): Promise<Content[] | []> {
853
849
  const containsConnections = await this.containsConnection(
854
850
  {
855
851
  first: args?.count ?? undefined,
@@ -865,7 +861,7 @@ export class CapiResponse {
865
861
 
866
862
  async liveBlogPostsConnection(
867
863
  args: ConnectionArguments
868
- ): Promise<Connection<CapiResponse>> {
864
+ ): Promise<Connection<Content>> {
869
865
  return this.containsConnection(args, this.handleLiveBlogPosts.bind(this))
870
866
  }
871
867
 
@@ -881,7 +877,7 @@ export class CapiResponse {
881
877
  return false
882
878
  }
883
879
 
884
- async pinnedPost(): Promise<CapiResponse | null> {
880
+ async pinnedPost(): Promise<Content | null> {
885
881
  if ('pinnedPosts' in this.capiData) {
886
882
  const pinnedPosts = this.capiData.pinnedPosts
887
883
  const pinnedPostId = pinnedPosts[0]
@@ -934,6 +930,17 @@ export class CapiResponse {
934
930
  return false
935
931
  }
936
932
 
933
+ isFTEdit(): boolean {
934
+ if (
935
+ 'internalAnalyticsTags' in this.capiData &&
936
+ this.capiData.internalAnalyticsTags
937
+ ) {
938
+ return this.capiData.internalAnalyticsTags.includes('ftedit')
939
+ }
940
+
941
+ return false
942
+ }
943
+
937
944
  design(): Design {
938
945
  if ('design' in this.capiData && this.capiData.design.theme) {
939
946
  return { theme: this.capiData.design.theme }
@@ -1,6 +1,6 @@
1
1
  import { LeadFlourish } from './LeadFlourish'
2
2
  import { FlourishSource } from './FlourishSource'
3
- import { CapiResponse } from './CapiResponse'
3
+ import { Content } from './Content'
4
4
  import { baseCapiObject } from '../fixtures/capiObject'
5
5
  import type { QueryContext } from '..'
6
6
  import cloneDeep from 'clone-deep'
@@ -13,7 +13,6 @@ const leadFlourishData = {
13
13
  type: 'test-type',
14
14
  }
15
15
 
16
-
17
16
  describe('LeadFlourish', () => {
18
17
  let leadFlourish: LeadFlourish
19
18
 
@@ -27,15 +26,17 @@ describe('LeadFlourish', () => {
27
26
  }
28
27
  jest.spyOn(FlourishSource, 'getImageMetadata').mockResolvedValue({
29
28
  width: 800,
30
- height: 600
29
+ height: 600,
31
30
  })
32
- const capiResponse = new CapiResponse(clonedBase, context)
31
+ const capiResponse = new Content(clonedBase, context)
33
32
  leadFlourish = new LeadFlourish(capiResponse, context)
34
33
  })
35
34
 
36
35
  describe('fallbackImage', () => {
37
36
  it('should instantiate a new FlourishSource', async () => {
38
- await expect(leadFlourish.fallbackImage()).resolves.toBeInstanceOf(FlourishSource)
37
+ await expect(leadFlourish.fallbackImage()).resolves.toBeInstanceOf(
38
+ FlourishSource
39
+ )
39
40
  })
40
41
  })
41
42
 
@@ -1,13 +1,10 @@
1
- import { CapiResponse } from './CapiResponse'
1
+ import { Content } from './Content'
2
2
  import type { QueryContext } from '..'
3
3
  import { FlourishSource } from './FlourishSource'
4
4
 
5
5
  export class LeadFlourish {
6
6
  #systemCode: string
7
- constructor(
8
- private capiResponse: CapiResponse,
9
- private context: QueryContext
10
- ) {
7
+ constructor(private capiResponse: Content, private context: QueryContext) {
11
8
  this.#systemCode = context.systemCode ?? 'cp-content-pipeline'
12
9
  }
13
10
 
@@ -1,11 +1,11 @@
1
1
  import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
2
2
  import { QueryContext } from '..'
3
3
  import { uuidFromUrl } from '../helpers/metadata'
4
- import { List, listSchema } from './schemas/capi/list'
4
+ import { List as ListSchema, listSchema } from './schemas/capi/list'
5
5
  import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
6
6
 
7
- export class CapiList {
8
- static fromJSON(list: unknown, context: QueryContext): CapiList {
7
+ export class List {
8
+ static fromJSON(list: unknown, context: QueryContext): List {
9
9
  const result = listSchema.safeParse(list)
10
10
 
11
11
  if (!result.success) {
@@ -31,10 +31,10 @@ export class CapiList {
31
31
  )
32
32
  }
33
33
 
34
- return new CapiList(list as List, context)
34
+ return new List(list as ListSchema, context)
35
35
  }
36
36
 
37
- constructor(private list: List, private context: QueryContext) {}
37
+ constructor(private list: ListSchema, private context: QueryContext) {}
38
38
 
39
39
  async items() {
40
40
  if (!this.list.items) return []
@@ -3,7 +3,7 @@ import extractText from '../resolvers/content-tree/extractText'
3
3
  import updateTreeWithReferenceIds from '../resolvers/content-tree/updateTreeWithReferenceIds'
4
4
  import { LiteralUnionScalarValues } from '../resolvers/literal-union'
5
5
  import { RichTextSource } from '../resolvers/scalars'
6
- import { CapiResponse } from './CapiResponse'
6
+ import { Content } from './Content'
7
7
  import {
8
8
  commonTagMappings,
9
9
  articleTagMappings,
@@ -16,7 +16,7 @@ export class RichText {
16
16
  constructor(
17
17
  public source: LiteralUnionScalarValues<typeof RichTextSource>,
18
18
  private value: string | null,
19
- private contentApiData?: CapiResponse
19
+ private contentApiData?: Content
20
20
  ) {}
21
21
 
22
22
  raw() {
@@ -1,7 +1,7 @@
1
1
  import { Topper } from './Topper'
2
2
  import { Person } from './Person'
3
3
  import { baseCapiObject } from '../fixtures/capiObject'
4
- import { CapiResponse } from './CapiResponse'
4
+ import { Content } from './Content'
5
5
  import { predicates } from './Concept'
6
6
  import cloneDeep from 'clone-deep'
7
7
  import conceptIds from '@financial-times/n-concept-ids'
@@ -18,7 +18,7 @@ const context = {} as unknown as QueryContext
18
18
  describe('produces the correct types', () => {
19
19
  it('defaults when there is no topper, is normal headline, is paper background', () => {
20
20
  const clonedBase = cloneDeep(baseCapiObject)
21
- const capiResponse = new CapiResponse(clonedBase, context)
21
+ const capiResponse = new Content(clonedBase, context)
22
22
  const topper = new Topper(capiResponse, context)
23
23
 
24
24
  const desired = 'BasicTopper'
@@ -47,7 +47,7 @@ describe('produces the correct types', () => {
47
47
 
48
48
  const clonedBase = cloneDeep(baseCapiObject)
49
49
  clonedBase.topper = splitTopper
50
- const capiResponse = new CapiResponse(clonedBase, context)
50
+ const capiResponse = new Content(clonedBase, context)
51
51
  const topper = new Topper(capiResponse, context)
52
52
 
53
53
  const desiredType = 'SplitTextTopper'
@@ -76,7 +76,7 @@ describe('produces the correct types', () => {
76
76
 
77
77
  const clonedBase = cloneDeep(baseCapiObject)
78
78
  clonedBase.topper = splitTopper
79
- const capiResponse = new CapiResponse(clonedBase, context)
79
+ const capiResponse = new Content(clonedBase, context)
80
80
  const topper = new Topper(capiResponse, context)
81
81
 
82
82
  const desired = 'FullBleedTopper'
@@ -105,7 +105,7 @@ describe('produces the correct types', () => {
105
105
 
106
106
  const clonedBase = cloneDeep(baseCapiObject)
107
107
  clonedBase.topper = splitTopper
108
- const capiResponse = new CapiResponse(clonedBase, context)
108
+ const capiResponse = new Content(clonedBase, context)
109
109
  const topper = new Topper(capiResponse, context)
110
110
 
111
111
  const desired = 'FullBleedTopper'
@@ -125,7 +125,7 @@ describe('produces the correct types', () => {
125
125
  type: '',
126
126
  types: [],
127
127
  })
128
- const capiResponse = new CapiResponse(clonedBase, context)
128
+ const capiResponse = new Content(clonedBase, context)
129
129
  const topper = new Topper(capiResponse, context)
130
130
 
131
131
  const desired = 'OpinionTopper'
@@ -155,7 +155,7 @@ describe('produces the correct types', () => {
155
155
  type: '',
156
156
  types: [],
157
157
  })
158
- const capiResponse = new CapiResponse(clonedBase, context)
158
+ const capiResponse = new Content(clonedBase, context)
159
159
  const topper = new Topper(capiResponse, context)
160
160
 
161
161
  const desired = 'BrandedTopper'
@@ -186,7 +186,7 @@ describe('produces the correct types', () => {
186
186
  layoutWidth: 'full-width',
187
187
  backgroundColour: 'paper',
188
188
  }
189
- const capiResponse = new CapiResponse(clonedBase, context)
189
+ const capiResponse = new Content(clonedBase, context)
190
190
  const topper = new Topper(capiResponse, context)
191
191
 
192
192
  const desired = 'TopperWithFlourish'
@@ -204,7 +204,7 @@ describe('produces the correct types', () => {
204
204
  it('live blog topper, is full bleed, large headline, paper background', () => {
205
205
  const clonedBase = cloneDeep(baseCapiObject)
206
206
  clonedBase.type = 'http://www.ft.com/ontology/content/LiveBlogPackage'
207
- const capiResponse = new CapiResponse(clonedBase, context)
207
+ const capiResponse = new Content(clonedBase, context)
208
208
  const topper = new Topper(capiResponse, context)
209
209
 
210
210
  const desired = 'FullBleedTopper'
@@ -235,7 +235,7 @@ describe('it retrieves the correct headline', () => {
235
235
  }
236
236
  clonedBase.topper = topperData
237
237
 
238
- const capiResponse = new CapiResponse(clonedBase, context)
238
+ const capiResponse = new Content(clonedBase, context)
239
239
  const topper = new Topper(capiResponse, context)
240
240
 
241
241
  const desired = topperData.headline
@@ -247,7 +247,7 @@ describe('it retrieves the correct headline', () => {
247
247
  it('when there is one headline', () => {
248
248
  const clonedBase = cloneDeep(baseCapiObject)
249
249
 
250
- const capiResponse = new CapiResponse(clonedBase, context)
250
+ const capiResponse = new Content(clonedBase, context)
251
251
  const topper = new Topper(capiResponse, context)
252
252
 
253
253
  const desired = clonedBase.title
@@ -268,7 +268,7 @@ describe('produces the correct intro', () => {
268
268
  clonedBase.standfirst =
269
269
  'Decision follows 10 days of pressure in wake of controversial mini-Budget'
270
270
 
271
- const capiResponse = new CapiResponse(clonedBase, context)
271
+ const capiResponse = new Content(clonedBase, context)
272
272
  const topper = new Topper(capiResponse, context)
273
273
 
274
274
  const desired = {
@@ -286,7 +286,7 @@ describe('produces the correct intro', () => {
286
286
  'Decision follows 10 days of pressure in wake of controversial mini-Budget'
287
287
  clonedBase.standfirst = standfirst
288
288
 
289
- const capiResponse = new CapiResponse(clonedBase, context)
289
+ const capiResponse = new Content(clonedBase, context)
290
290
  const topper = new Topper(capiResponse, context)
291
291
 
292
292
  const desired = {
@@ -302,7 +302,7 @@ describe('produces the correct intro', () => {
302
302
  describe('produces the correct layout', () => {
303
303
  it('defaults to branded if there is no layout', () => {
304
304
  const clonedBase = cloneDeep(baseCapiObject)
305
- const capiResponse = new CapiResponse(clonedBase, context)
305
+ const capiResponse = new Content(clonedBase, context)
306
306
  const topper = new Topper(capiResponse, context)
307
307
  const desired = 'branded'
308
308
  const actual = topper.layout()
@@ -335,7 +335,7 @@ describe('produces the correct background colour', () => {
335
335
 
336
336
  const clonedBase = cloneDeep(baseCapiObject)
337
337
  clonedBase.topper = splitTopper
338
- const capiResponse = new CapiResponse(clonedBase, context)
338
+ const capiResponse = new Content(clonedBase, context)
339
339
  const topper = new Topper(capiResponse, context)
340
340
 
341
341
  expect(topper.backgroundColour()).toEqual(expected)
@@ -371,10 +371,7 @@ describe('produces correct images', () => {
371
371
  },
372
372
  ],
373
373
  }
374
- const capiResponse = new CapiResponse(
375
- withLeadImagesAndDisplayMainImage,
376
- context
377
- )
374
+ const capiResponse = new Content(withLeadImagesAndDisplayMainImage, context)
378
375
  const topper = new Topper(capiResponse, context)
379
376
  const fallback = topper.fallbackImage()
380
377
  expect(fallback?.url()).toEqual(
@@ -383,7 +380,7 @@ describe('produces correct images', () => {
383
380
  })
384
381
  it('uses the main image as fallback if no lead images', () => {
385
382
  const clonedBase = cloneDeep(baseCapiObject)
386
- const capiResponse = new CapiResponse(clonedBase, context)
383
+ const capiResponse = new Content(clonedBase, context)
387
384
  const topper = new Topper(capiResponse, context)
388
385
  const fallback = topper.fallbackImage()
389
386
  expect(fallback?.url()).toEqual(
@@ -428,7 +425,7 @@ describe('produces the correct metadata tags', () => {
428
425
 
429
426
  it('uses the annotation with the predicate hasDisplayTag for the displayConcept', () => {
430
427
  const clonedBase = cloneDeep(baseCapiObject)
431
- const capiResponse = new CapiResponse(clonedBase, context)
428
+ const capiResponse = new Content(clonedBase, context)
432
429
  const topper = new Topper(capiResponse, context)
433
430
  const displayConcept = topper.displayConcept()
434
431
  expect(displayConcept?.prefLabel()).toEqual('UK politics & policy')
@@ -442,7 +439,7 @@ describe('produces the correct metadata tags', () => {
442
439
  ...clonedBase,
443
440
  annotations: [...clonedBase.annotations, opinionGenre, lexBrand],
444
441
  }
445
- const capiResponse = new CapiResponse(lexArticle, context)
442
+ const capiResponse = new Content(lexArticle, context)
446
443
  const topper = new Topper(capiResponse, context)
447
444
  const displayConcept = topper.displayConcept()
448
445
  expect(displayConcept?.prefLabel()).toEqual('Lex')
@@ -456,7 +453,7 @@ describe('produces the correct metadata tags', () => {
456
453
  ...clonedBase,
457
454
  annotations: [...clonedBase.annotations, opinionGenre],
458
455
  }
459
- const capiResponse = new CapiResponse(opinionArticle, context)
456
+ const capiResponse = new Content(opinionArticle, context)
460
457
  const topper = new Topper(capiResponse, context)
461
458
  const displayConcept = topper.displayConcept()
462
459
  expect(displayConcept?.prefLabel()).toEqual('UK politics & policy')
@@ -470,7 +467,7 @@ describe('produces the correct metadata tags', () => {
470
467
  ...clonedBase,
471
468
  annotations: [...clonedBase.annotations, lexBrand],
472
469
  }
473
- const capiResponse = new CapiResponse(opinionArticle, context)
470
+ const capiResponse = new Content(opinionArticle, context)
474
471
  const topper = new Topper(capiResponse, context)
475
472
  const displayConcept = topper.displayConcept()
476
473
  expect(displayConcept?.prefLabel()).toEqual('UK politics & policy')
@@ -481,11 +478,11 @@ describe('produces the correct metadata tags', () => {
481
478
 
482
479
  describe('headshot method', () => {
483
480
  let topper: Topper
484
- let capiResponse: CapiResponse
481
+ let capiResponse: Content
485
482
 
486
483
  beforeEach(() => {
487
484
  const clonedBase = cloneDeep(baseCapiObject)
488
- capiResponse = new CapiResponse(clonedBase, context)
485
+ capiResponse = new Content(clonedBase, context)
489
486
  topper = new Topper(capiResponse, context)
490
487
  })
491
488
 
@@ -1,4 +1,4 @@
1
- import { CapiResponse } from './CapiResponse'
1
+ import { Content } from './Content'
2
2
  import { CAPIImage } from './Image'
3
3
  import type { QueryContext } from '..'
4
4
  import {
@@ -44,10 +44,7 @@ function isDarkBackground(backgroundColour: string): boolean {
44
44
  export class Topper {
45
45
  #systemCode: string
46
46
 
47
- constructor(
48
- private capiResponse: CapiResponse,
49
- private context: QueryContext
50
- ) {
47
+ constructor(private capiResponse: Content, private context: QueryContext) {
51
48
  this.#systemCode = context.systemCode ?? 'cp-content-pipeline'
52
49
  }
53
50
 
@@ -34,6 +34,7 @@ const articleMetadataSchema = baseMetadataSchema.pick({
34
34
  containedIn: true,
35
35
  publication: true,
36
36
  clientName: true,
37
+ internalAnalyticsTags: true,
37
38
  })
38
39
 
39
40
  const articleMediaSchema = baseMediaSchema.pick({
@@ -24,6 +24,7 @@ const audioMetadataSchema = baseMetadataSchema.pick({
24
24
  editorialDesk: true,
25
25
  canBeSyndicated: true,
26
26
  canBeDistributed: true,
27
+ internalAnalyticsTags: true,
27
28
  })
28
29
 
29
30
  const audioMediaSchema = baseMediaSchema.pick({
@@ -235,6 +235,7 @@ export const baseMetadataSchema = z.object({
235
235
  .optional(),
236
236
  publication: z.array(z.string()).optional(),
237
237
  clientName: z.string().optional(),
238
+ internalAnalyticsTags: z.string().optional(),
238
239
  })
239
240
 
240
241
  export const baseContentSchema = z.object({
@@ -41,6 +41,7 @@ const contentPackageMetadataSchema = baseMetadataSchema.pick({
41
41
  topper: true,
42
42
  comments: true,
43
43
  tableOfContents: true,
44
+ internalAnalyticsTags: true,
44
45
  })
45
46
 
46
47
  const contentPackageMediaSchema = baseMediaSchema.pick({
@@ -31,6 +31,7 @@ const liveBlogPackageMetadataSchema = baseMetadataSchema.pick({
31
31
  canBeDistributed: true,
32
32
  topper: true,
33
33
  comments: true,
34
+ internalAnalyticsTags: true,
34
35
  })
35
36
 
36
37
  const liveBlogPackageMediaSchema = baseMediaSchema.pick({
@@ -32,6 +32,7 @@ const placeholderMetadataSchema = baseMetadataSchema.pick({
32
32
  topper: true,
33
33
  comments: true,
34
34
  containedIn: true,
35
+ internalAnalyticsTags: true,
35
36
  })
36
37
 
37
38
  const placeholderMediaSchema = baseMediaSchema.pick({
@@ -25,6 +25,7 @@ const videoMetadataSchema = baseMetadataSchema.pick({
25
25
  editorialDesk: true,
26
26
  canBeSyndicated: true,
27
27
  canBeDistributed: true,
28
+ internalAnalyticsTags: true,
28
29
  })
29
30
 
30
31
  const videoMediaSchema = baseMediaSchema.pick({