@financial-times/cp-content-pipeline-schema 3.0.2 → 3.0.4

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.
@@ -96,7 +96,7 @@ export type Article = Content & {
96
96
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
97
97
  readonly mainImage?: Maybe<Image>;
98
98
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
99
- readonly modifiedTimestamp: Scalars['Float']['output'];
99
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
100
100
  /** The party that originated the article, eg. 'FT'. */
101
101
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
102
102
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -169,7 +169,7 @@ export type Audio = Content & {
169
169
  /** An array of media objects containing the url, the duration, the filesize and the media type. */
170
170
  readonly media?: Maybe<ReadonlyArray<Maybe<Media>>>;
171
171
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
172
- readonly modifiedTimestamp: Scalars['Float']['output'];
172
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
173
173
  /** The party that originated the article, eg. 'FT'. */
174
174
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
175
175
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -466,7 +466,7 @@ export type Content = {
466
466
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
467
467
  readonly mainImage?: Maybe<Image>;
468
468
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
469
- readonly modifiedTimestamp: Scalars['Float']['output'];
469
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
470
470
  /** The party that originated the article, eg. 'FT'. */
471
471
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
472
472
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -541,7 +541,7 @@ export type ContentPackage = Content & {
541
541
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
542
542
  readonly mainImage?: Maybe<Image>;
543
543
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
544
- readonly modifiedTimestamp: Scalars['Float']['output'];
544
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
545
545
  /** The party that originated the article, eg. 'FT'. */
546
546
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
547
547
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -1096,7 +1096,7 @@ export type LiveBlogPackage = Content & {
1096
1096
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1097
1097
  readonly mainImage?: Maybe<Image>;
1098
1098
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
1099
- readonly modifiedTimestamp: Scalars['Float']['output'];
1099
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
1100
1100
  /** The party that originated the article, eg. 'FT'. */
1101
1101
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1102
1102
  /** The pinned article of this live blog package. */
@@ -1182,7 +1182,7 @@ export type LiveBlogPost = Content & {
1182
1182
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1183
1183
  readonly mainImage?: Maybe<Image>;
1184
1184
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
1185
- readonly modifiedTimestamp: Scalars['Float']['output'];
1185
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
1186
1186
  /** The party that originated the article, eg. 'FT'. */
1187
1187
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1188
1188
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -1420,7 +1420,7 @@ export type Placeholder = Content & {
1420
1420
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1421
1421
  readonly mainImage?: Maybe<Image>;
1422
1422
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
1423
- readonly modifiedTimestamp: Scalars['Float']['output'];
1423
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
1424
1424
  /** The party that originated the article, eg. 'FT'. */
1425
1425
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1426
1426
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -1783,7 +1783,7 @@ export type Video = Content & {
1783
1783
  /** An image object containing the url and the caption, to be displayed usually before the article content. */
1784
1784
  readonly mainImage?: Maybe<Image>;
1785
1785
  /** The number of milliseconds since the unix epoch the article was last changed, eg '1712140552443'. */
1786
- readonly modifiedTimestamp: Scalars['Float']['output'];
1786
+ readonly modifiedTimestamp?: Maybe<Scalars['Float']['output']>;
1787
1787
  /** The party that originated the article, eg. 'FT'. */
1788
1788
  readonly originatingParty?: Maybe<Scalars['String']['output']>;
1789
1789
  /** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
@@ -2152,7 +2152,7 @@ export type ArticleResolvers<ContextType = QueryContext, ParentType extends Reso
2152
2152
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2153
2153
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2154
2154
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2155
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2155
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2156
2156
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2157
2157
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2158
2158
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2183,7 +2183,7 @@ export type AudioResolvers<ContextType = QueryContext, ParentType extends Resolv
2183
2183
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2184
2184
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2185
2185
  media: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Media']>>>, ParentType, ContextType>;
2186
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2186
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2187
2187
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2188
2188
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2189
2189
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2342,7 +2342,7 @@ export type ContentResolvers<ContextType = QueryContext, ParentType extends Reso
2342
2342
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2343
2343
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2344
2344
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2345
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2345
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2346
2346
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2347
2347
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2348
2348
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2373,7 +2373,7 @@ export type ContentPackageResolvers<ContextType = QueryContext, ParentType exten
2373
2373
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2374
2374
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2375
2375
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2376
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2376
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2377
2377
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2378
2378
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2379
2379
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2698,7 +2698,7 @@ export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType exte
2698
2698
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2699
2699
  liveBlogPosts: Resolver<Maybe<ReadonlyArray<Maybe<ResolversTypes['Content']>>>, ParentType, ContextType>;
2700
2700
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2701
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2701
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2702
2702
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2703
2703
  pinnedPost: Resolver<Maybe<ResolversTypes['Content']>, ParentType, ContextType>;
2704
2704
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2734,7 +2734,7 @@ export type LiveBlogPostResolvers<ContextType = QueryContext, ParentType extends
2734
2734
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2735
2735
  isPinned: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2736
2736
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2737
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2737
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2738
2738
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2739
2739
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2740
2740
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2870,7 +2870,7 @@ export type PlaceholderResolvers<ContextType = QueryContext, ParentType extends
2870
2870
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
2871
2871
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2872
2872
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
2873
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
2873
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
2874
2874
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2875
2875
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2876
2876
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -3085,7 +3085,7 @@ export type VideoResolvers<ContextType = QueryContext, ParentType extends Resolv
3085
3085
  id: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
3086
3086
  instantAlertConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
3087
3087
  mainImage: Resolver<Maybe<ResolversTypes['Image']>, ParentType, ContextType>;
3088
- modifiedTimestamp: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
3088
+ modifiedTimestamp: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
3089
3089
  originatingParty: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
3090
3090
  publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
3091
3091
  publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -263,11 +263,25 @@ export class CapiResponse {
263
263
  async authors(): Promise<Person[]> {
264
264
  const authors = this.annotations({ byPredicate: predicates.hasAuthor })
265
265
 
266
- return Promise.all(
267
- authors.map((author) =>
268
- this.context.dataSources.capi.getPerson(author.uuid())
269
- )
270
- )
266
+ const getPerson = async (author: Concept) => {
267
+ try {
268
+ return await this.context.dataSources.capi.getPerson(author.uuid())
269
+ } catch (error) {
270
+ if (isError(error)) {
271
+ this.context.logger.warn({
272
+ event: 'RECOVERABLE_ERROR',
273
+ error: new OperationalError({
274
+ message: `Error fetching CAPI Person for author ${author.prefLabel()}`,
275
+ code: 'AUTHOR_PERSON_FETCH_ERROR',
276
+ cause: error,
277
+ }),
278
+ })
279
+ }
280
+ return Person.fromConcept(author, this.context)
281
+ }
282
+ }
283
+
284
+ return Promise.all(authors.map(getPerson))
271
285
  }
272
286
 
273
287
  async primaryAuthor(): Promise<Person | null> {
@@ -365,7 +379,9 @@ export class CapiResponse {
365
379
  }
366
380
 
367
381
  modifiedTimestamp() {
368
- return new Date(this.capiData.lastModified).getTime()
382
+ return this.capiData.lastModified
383
+ ? new Date(this.capiData.lastModified).getTime()
384
+ : null
369
385
  }
370
386
 
371
387
  publishReference() {
@@ -7,6 +7,7 @@ import { CapiPerson } from './schemas/capi/internal-content'
7
7
  import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
8
8
  import { PersonHeadshotArgs } from '../generated'
9
9
  import { uuidFromUrl } from '../helpers/metadata'
10
+ import { Concept } from './Concept'
10
11
 
11
12
  export class Person {
12
13
  #systemCode: string
@@ -39,6 +40,24 @@ export class Person {
39
40
  return new Person(data as CapiPerson, context)
40
41
  }
41
42
 
43
+ // The concepts returned in the annotations contain *most* of the fields
44
+ // returned by the Person API so we can use one to create a simple Person.
45
+ // Useful as a fallback if the Person API fails.
46
+ static fromConcept(concept: Concept, context: QueryContext): Person {
47
+ const personShim: CapiPerson = {
48
+ id: concept.id(),
49
+ apiUrl: concept.apiUrl(),
50
+ prefLabel: concept.prefLabel(),
51
+ types: concept.types(),
52
+ directType: concept.directType(),
53
+ // there's no labels equivalent in Concept but we don't use this field
54
+ // anyway
55
+ labels: [],
56
+ }
57
+
58
+ return new Person(personShim, context)
59
+ }
60
+
42
61
  constructor(private person: CapiPerson, private context: QueryContext) {
43
62
  this.#systemCode = context.systemCode ?? 'cp-content-pipeline'
44
63
  }
@@ -11,6 +11,8 @@ import imageServiceUrl from '../helpers/imageService'
11
11
  import { LeadFlourish } from './LeadFlourish'
12
12
  import type { TopperWithHeadshotHeadshotArgs } from '../generated'
13
13
  import { predicates } from './Concept'
14
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
15
+ import isError from '../helpers/isError'
14
16
 
15
17
  type TopperType =
16
18
  | 'DeepPortraitTopper'
@@ -346,7 +348,22 @@ export class Topper {
346
348
  }
347
349
 
348
350
  if (this.capiResponse.isOpinion()) {
349
- return (await this.capiResponse.primaryAuthor())?.headshot(args) ?? null
351
+ try {
352
+ const primaryAuthor = await this.capiResponse.primaryAuthor()
353
+ return primaryAuthor?.headshot(args) ?? null
354
+ } catch (error) {
355
+ if (isError(error)) {
356
+ this.context.logger.warn({
357
+ event: 'RECOVERABLE_ERROR',
358
+ error: new OperationalError({
359
+ message: 'Error fetching CAPI Person for headshot URL for topper',
360
+ code: 'HEADSHOT_PERSON_FETCH_ERROR',
361
+ cause: error,
362
+ }),
363
+ })
364
+ }
365
+ return null
366
+ }
350
367
  }
351
368
 
352
369
  return null
@@ -195,7 +195,7 @@ export const baseMetadataSchema = z.object({
195
195
  publishedDate: z.string(),
196
196
  firstPublishedDate: z.string(),
197
197
  publishReference: z.string().optional(),
198
- lastModified: z.string(),
198
+ lastModified: z.string().optional(),
199
199
  realtime: z.boolean(),
200
200
  editorialDesk: z.string().optional(),
201
201
  accessLevel: z