@financial-times/cp-content-pipeline-schema 1.1.0 → 1.2.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 (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/generated/index.d.ts +2 -0
  3. package/lib/model/CapiResponse.d.ts +2 -0
  4. package/lib/model/CapiResponse.js +17 -3
  5. package/lib/model/CapiResponse.js.map +1 -1
  6. package/lib/model/Picture.d.ts +2 -1
  7. package/lib/model/Picture.js +4 -2
  8. package/lib/model/Picture.js.map +1 -1
  9. package/lib/model/Picture.test.js +3 -2
  10. package/lib/model/Picture.test.js.map +1 -1
  11. package/lib/model/schemas/capi/article.d.ts +29 -26
  12. package/lib/model/schemas/capi/article.js +1 -0
  13. package/lib/model/schemas/capi/article.js.map +1 -1
  14. package/lib/model/schemas/capi/audio.d.ts +9 -6
  15. package/lib/model/schemas/capi/audio.js +1 -0
  16. package/lib/model/schemas/capi/audio.js.map +1 -1
  17. package/lib/model/schemas/capi/base-schema.d.ts +32 -26
  18. package/lib/model/schemas/capi/base-schema.js +15 -7
  19. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  20. package/lib/model/schemas/capi/content-package.d.ts +29 -26
  21. package/lib/model/schemas/capi/content-package.js +1 -0
  22. package/lib/model/schemas/capi/content-package.js.map +1 -1
  23. package/lib/model/schemas/capi/index.d.ts +1 -0
  24. package/lib/model/schemas/capi/live-blog-package.d.ts +32 -26
  25. package/lib/model/schemas/capi/live-blog-package.js +2 -0
  26. package/lib/model/schemas/capi/live-blog-package.js.map +1 -1
  27. package/lib/model/schemas/capi/placeholder.d.ts +29 -26
  28. package/lib/model/schemas/capi/placeholder.js +1 -0
  29. package/lib/model/schemas/capi/placeholder.js.map +1 -1
  30. package/lib/model/schemas/capi/video.d.ts +1 -0
  31. package/lib/resolvers/content-tree/references/ImageSet.js +2 -1
  32. package/lib/resolvers/content-tree/references/ImageSet.js.map +1 -1
  33. package/lib/resolvers/content-tree/references/LayoutImage.js +2 -1
  34. package/lib/resolvers/content-tree/references/LayoutImage.js.map +1 -1
  35. package/lib/resolvers/content-tree/references/ScrollyImage.js +2 -1
  36. package/lib/resolvers/content-tree/references/ScrollyImage.js.map +1 -1
  37. package/lib/resolvers/content.d.ts +1 -0
  38. package/lib/resolvers/content.js +1 -0
  39. package/lib/resolvers/content.js.map +1 -1
  40. package/lib/resolvers/index.d.ts +1 -0
  41. package/package.json +1 -1
  42. package/src/generated/index.ts +2 -0
  43. package/src/model/CapiResponse.ts +25 -3
  44. package/src/model/Picture.test.ts +8 -2
  45. package/src/model/Picture.ts +9 -2
  46. package/src/model/schemas/capi/article.ts +1 -0
  47. package/src/model/schemas/capi/audio.ts +1 -0
  48. package/src/model/schemas/capi/base-schema.ts +23 -15
  49. package/src/model/schemas/capi/content-package.ts +1 -0
  50. package/src/model/schemas/capi/live-blog-package.ts +2 -0
  51. package/src/model/schemas/capi/placeholder.ts +1 -0
  52. package/src/resolvers/content-tree/references/ImageSet.ts +3 -1
  53. package/src/resolvers/content-tree/references/LayoutImage.ts +3 -0
  54. package/src/resolvers/content-tree/references/ScrollyImage.ts +3 -1
  55. package/src/resolvers/content.ts +1 -0
  56. package/tsconfig.tsbuildinfo +1 -1
  57. package/typedefs/content.graphql +1 -0
@@ -203,7 +203,11 @@ export class CapiResponse {
203
203
  return this.capiData.types
204
204
  }
205
205
  async url(vanity?: boolean | null) {
206
- const url = this.capiData.webUrl
206
+ const url =
207
+ this.capiData.webUrl ??
208
+ this.capiData.canonicalWebUrl ??
209
+ `https://www.ft.com/content/${this.id()}`
210
+
207
211
  if (vanity) {
208
212
  try {
209
213
  const vanityUrl = await this.context.dataSources.vanityUrls.get(url)
@@ -231,7 +235,7 @@ export class CapiResponse {
231
235
  return null
232
236
  }
233
237
  accessLevel(): LiteralUnionScalarValues<typeof AccessLevel> {
234
- return this.capiData.accessLevel
238
+ return this.capiData.accessLevel ?? 'subscribed'
235
239
  }
236
240
  canBeSyndicated(): LiteralUnionScalarValues<typeof CanBeSyndicated> {
237
241
  return this.capiData.canBeSyndicated
@@ -533,6 +537,23 @@ export class CapiResponse {
533
537
  return []
534
538
  }
535
539
 
540
+ async pinnedPost(): Promise<CapiResponse | null> {
541
+ if ('pinnedPosts' in this.capiData) {
542
+ const pinnedPosts = this.capiData.pinnedPosts || []
543
+ const liveBlogPosts = await this.liveBlogPosts()
544
+
545
+ const pinnedPostId = pinnedPosts[0]
546
+ const pinnedPostIndex = liveBlogPosts.findIndex(
547
+ (post) => post.id() === pinnedPostId
548
+ )
549
+
550
+ if (pinnedPostIndex !== -1) {
551
+ return liveBlogPosts.splice(pinnedPostIndex, 1)[0]
552
+ }
553
+ }
554
+ return null
555
+ }
556
+
536
557
  isContainedInPackage(): boolean {
537
558
  return 'containedIn' in this.capiData
538
559
  ? (this.capiData.containedIn?.length ?? 0) > 0
@@ -551,8 +572,9 @@ export class CapiResponse {
551
572
 
552
573
  commentsEnabled(): boolean {
553
574
  if ('comments' in this.capiData) {
554
- return this.capiData.comments.enabled
575
+ return this.capiData.comments?.enabled ?? false
555
576
  }
577
+
556
578
  return false
557
579
  }
558
580
 
@@ -2,6 +2,8 @@ import { QueryContext } from '..'
2
2
  import { ImageSet } from '../types/internal-content'
3
3
  import { Picture } from './Picture'
4
4
 
5
+ const isLiveBlog = false
6
+
5
7
  describe('Picture model', () => {
6
8
  const mockImageSet = {
7
9
  apiUrl: 'https://api.ft.com/content/image-set',
@@ -35,7 +37,7 @@ describe('Picture model', () => {
35
37
 
36
38
  describe('finding the standard image', () => {
37
39
  it('uses the StandardInline image if it exists', () => {
38
- const picture = new Picture(mockImageSet, mockContext)
40
+ const picture = new Picture(mockImageSet, isLiveBlog, mockContext)
39
41
  expect(picture.standard().url()).toEqual(
40
42
  'https://cloudfront.com/standard-inline.jpg'
41
43
  )
@@ -73,7 +75,11 @@ describe('Picture model', () => {
73
75
  type: 'http://www.ft.com/ontology/content/ImageSet',
74
76
  } as ImageSet
75
77
 
76
- const picture = new Picture(mockWithoutStandardInline, mockContext)
78
+ const picture = new Picture(
79
+ mockWithoutStandardInline,
80
+ isLiveBlog,
81
+ mockContext
82
+ )
77
83
  expect(picture.standard().url()).toEqual(
78
84
  'https://cloudfront.com/desktop.jpg'
79
85
  )
@@ -14,7 +14,11 @@ function assertDefined<T>(
14
14
  }
15
15
 
16
16
  export class Picture {
17
- constructor(private imageSet: ImageSet, private context: QueryContext) {}
17
+ constructor(
18
+ private imageSet: ImageSet,
19
+ private isLiveBlog: boolean,
20
+ private context: QueryContext
21
+ ) {}
18
22
 
19
23
  images(): Image[] {
20
24
  return this.imageSet.members.map(
@@ -36,7 +40,10 @@ export class Picture {
36
40
 
37
41
  async layoutWidth(): Promise<LayoutWidth> {
38
42
  //TODO: actually work out the types
39
- if (this.imageSet.members.length === 3) return 'full-grid'
43
+
44
+ if (!this.isLiveBlog && this.imageSet.members.length === 3) {
45
+ return 'full-grid'
46
+ }
40
47
 
41
48
  const dimensions = await this.standard().dimensions()
42
49
 
@@ -17,6 +17,7 @@ const articleMetadataSchema = baseMetadataSchema.pick({
17
17
  id: true,
18
18
  annotations: true,
19
19
  webUrl: true,
20
+ canonicalWebUrl: true,
20
21
  type: true,
21
22
  types: true,
22
23
  standout: true,
@@ -14,6 +14,7 @@ const audioMetadataSchema = baseMetadataSchema.pick({
14
14
  id: true,
15
15
  annotations: true,
16
16
  webUrl: true,
17
+ canonicalWebUrl: true,
17
18
  types: true,
18
19
  publishedDate: true,
19
20
  firstPublishedDate: true,
@@ -120,23 +120,28 @@ export const CapiPerson = z.object({
120
120
  export const baseMetadataSchema = z.object({
121
121
  id: z.string(),
122
122
  annotations: Annotation.array(),
123
- webUrl: z.string(),
123
+ webUrl: z.string().optional(),
124
+ canonicalWebUrl: z.string().optional(),
124
125
  type: z.string(),
125
126
  types: z.string().array(),
126
- standout: z.object({
127
- editorsChoice: z.boolean(),
128
- exclusive: z.boolean(),
129
- scoop: z.boolean(),
130
- }),
127
+ standout: z
128
+ .object({
129
+ editorsChoice: z.boolean(),
130
+ exclusive: z.boolean(),
131
+ scoop: z.boolean(),
132
+ })
133
+ .optional(),
131
134
  publishedDate: z.string(),
132
135
  firstPublishedDate: z.string(),
133
136
  realtime: z.boolean(),
134
- accessLevel: z.union([
135
- z.literal('premium'),
136
- z.literal('subscribed'),
137
- z.literal('registered'),
138
- z.literal('free'),
139
- ]),
137
+ accessLevel: z
138
+ .union([
139
+ z.literal('premium'),
140
+ z.literal('subscribed'),
141
+ z.literal('registered'),
142
+ z.literal('free'),
143
+ ])
144
+ .optional(),
140
145
  canBeSyndicated: z.union([
141
146
  z.literal('yes'),
142
147
  z.literal('no'),
@@ -145,9 +150,11 @@ export const baseMetadataSchema = z.object({
145
150
  z.literal('unknown'),
146
151
  ]),
147
152
  topper: Topper.optional(),
148
- comments: z.object({
149
- enabled: z.boolean(),
150
- }),
153
+ comments: z
154
+ .object({
155
+ enabled: z.boolean(),
156
+ })
157
+ .optional(),
151
158
  containedIn: z
152
159
  .object({ apiUrl: z.string(), id: z.string() })
153
160
  .array()
@@ -175,6 +182,7 @@ export const baseContentSchema = z.object({
175
182
  promotionalStandfirst: z.string().optional(),
176
183
  })
177
184
  .optional(),
185
+ pinnedPosts: z.string().array(),
178
186
  contains: z
179
187
  .object({
180
188
  id: z.string(),
@@ -26,6 +26,7 @@ const contentPackageMetadataSchema = baseMetadataSchema.pick({
26
26
  id: true,
27
27
  annotations: true,
28
28
  webUrl: true,
29
+ canonicalWebUrl: true,
29
30
  type: true,
30
31
  types: true,
31
32
  standout: true,
@@ -10,6 +10,7 @@ const liveBlogPackageContentSchema = baseContentSchema.pick({
10
10
  summary: true,
11
11
  alternativeTitles: true,
12
12
  contains: true,
13
+ pinnedPosts: true,
13
14
  realtime: true,
14
15
  })
15
16
 
@@ -17,6 +18,7 @@ const liveBlogPackageMetadataSchema = baseMetadataSchema.pick({
17
18
  id: true,
18
19
  annotations: true,
19
20
  webUrl: true,
21
+ canonicalWebUrl: true,
20
22
  type: true,
21
23
  types: true,
22
24
  standout: true,
@@ -17,6 +17,7 @@ const placeholderMetadataSchema = baseMetadataSchema.pick({
17
17
  id: true,
18
18
  annotations: true,
19
19
  webUrl: true,
20
+ canonicalWebUrl: true,
20
21
  type: true,
21
22
  types: true,
22
23
  standout: true,
@@ -5,6 +5,8 @@ import { ImageSetResolvers } from '../../../generated'
5
5
 
6
6
  export const ImageSet = {
7
7
  picture(parent, _args, context) {
8
+ const isLiveBlog = parent.contentApiData?.type() === 'LiveBlogPost'
9
+
8
10
  const imageSet = parent.contentApiData
9
11
  ?.embeds()
10
12
  ?.find(
@@ -12,6 +14,6 @@ export const ImageSet = {
12
14
  uuidFromUrl(embed.id) === uuidFromUrl(parent.reference.id)
13
15
  )
14
16
 
15
- return imageSet ? new Picture(imageSet, context) : null
17
+ return imageSet ? new Picture(imageSet, isLiveBlog, context) : null
16
18
  },
17
19
  } satisfies ImageSetResolvers
@@ -3,6 +3,8 @@ import { LayoutImageResolvers } from '../../../generated'
3
3
 
4
4
  export const LayoutImage = {
5
5
  picture(parent, _args, context): Picture {
6
+ const isLiveBlog = parent.contentApiData?.type() === 'LiveBlogPost'
7
+
6
8
  return new Picture(
7
9
  {
8
10
  id: 'layout-imageset',
@@ -21,6 +23,7 @@ export const LayoutImage = {
21
23
  },
22
24
  ],
23
25
  },
26
+ isLiveBlog,
24
27
  context
25
28
  )
26
29
  },
@@ -5,6 +5,8 @@ import { ScrollyImageResolvers } from '../../../generated'
5
5
 
6
6
  export const ScrollyImage = {
7
7
  picture(parent, _args, context) {
8
+ const isLiveBlog = parent.contentApiData?.type() === 'LiveBlogPost'
9
+
8
10
  const imageSet = parent.contentApiData
9
11
  ?.embeds()
10
12
  ?.find(
@@ -12,6 +14,6 @@ export const ScrollyImage = {
12
14
  uuidFromUrl(embed.id) === uuidFromUrl(parent.reference.id)
13
15
  )
14
16
 
15
- return imageSet ? new Picture(imageSet, context) : null
17
+ return imageSet ? new Picture(imageSet, isLiveBlog, context) : null
16
18
  },
17
19
  } satisfies ScrollyImageResolvers
@@ -69,6 +69,7 @@ const resolvers = {
69
69
 
70
70
  LiveBlogPackage: {
71
71
  liveBlogPosts: (parent) => parent.liveBlogPosts(),
72
+ pinnedPost: (parent) => parent.pinnedPost(),
72
73
  realtime: (parent) => parent.realtime(),
73
74
  },
74
75