@financial-times/cp-content-pipeline-schema 2.14.1 → 2.15.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.
- package/CHANGELOG.md +16 -0
- package/lib/datasources/capi.d.ts +2 -0
- package/lib/datasources/capi.js +17 -1
- package/lib/datasources/capi.js.map +1 -1
- package/lib/datasources/capi.test.js +6 -1
- package/lib/datasources/capi.test.js.map +1 -1
- package/lib/datasources/instrumented.d.ts +1 -1
- package/lib/datasources/instrumented.js.map +1 -1
- package/lib/datasources/origami-image.js.map +1 -1
- package/lib/datasources/twitter.js.map +1 -1
- package/lib/datasources/url-management.js.map +1 -1
- package/lib/fixtures/dummyContext.js.map +1 -1
- package/lib/generated/index.d.ts +62 -0
- package/lib/helpers/decorateHeadshotUrl.js +1 -1
- package/lib/helpers/decorateHeadshotUrl.js.map +1 -1
- package/lib/helpers/flatten-formatted-zod-errors.d.ts +6 -0
- package/lib/helpers/flatten-formatted-zod-errors.js +39 -0
- package/lib/helpers/flatten-formatted-zod-errors.js.map +1 -0
- package/lib/helpers/imageService.js +1 -1
- package/lib/helpers/imageService.js.map +1 -1
- package/lib/helpers/isError.js +1 -1
- package/lib/helpers/isError.js.map +1 -1
- package/lib/helpers/metadata.js +1 -2
- package/lib/helpers/metadata.js.map +1 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js.map +1 -1
- package/lib/model/Byline.js +1 -1
- package/lib/model/Byline.js.map +1 -1
- package/lib/model/CapiList.d.ts +17 -0
- package/lib/model/CapiList.js +87 -0
- package/lib/model/CapiList.js.map +1 -0
- package/lib/model/CapiResponse.js +7 -38
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/Clip.js.map +1 -1
- package/lib/model/Concept.js.map +1 -1
- package/lib/model/FlourishSource.js.map +1 -1
- package/lib/model/Image.d.ts +2 -34
- package/lib/model/Image.js +1 -1
- package/lib/model/Image.js.map +1 -1
- package/lib/model/Person.js.map +1 -1
- package/lib/model/Picture.js.map +1 -1
- package/lib/model/RichText.d.ts +1 -1
- package/lib/model/RichText.js.map +1 -1
- package/lib/model/Topper.d.ts +2 -2
- package/lib/model/Topper.js.map +1 -1
- package/lib/model/schemas/capi/index.js.map +1 -1
- package/lib/model/schemas/capi/list.d.ts +48 -0
- package/lib/model/schemas/capi/list.js +35 -0
- package/lib/model/schemas/capi/list.js.map +1 -0
- package/lib/resolvers/clip.d.ts +5 -5
- package/lib/resolvers/content-tree/bodyXMLToTree.js +1 -1
- package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
- package/lib/resolvers/content-tree/extractText.js +1 -1
- package/lib/resolvers/content-tree/extractText.js.map +1 -1
- package/lib/resolvers/content-tree/nodePredicates.d.ts +3 -3
- package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
- package/lib/resolvers/content-tree/references/ClipSet.d.ts +2 -2
- package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -1
- package/lib/resolvers/content-tree/references/Flourish.js.map +1 -1
- package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
- package/lib/resolvers/content-tree/references/Recommended.js.map +1 -1
- package/lib/resolvers/content-tree/references/Reference.d.ts +2 -2
- package/lib/resolvers/content-tree/references/Reference.js.map +1 -1
- package/lib/resolvers/content-tree/references/Tweet.js.map +1 -1
- package/lib/resolvers/content-tree/references/Video.js.map +1 -1
- package/lib/resolvers/content-tree/tagMappings.js +5 -5
- package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
- package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js +1 -1
- package/lib/resolvers/content-tree/updateTreeWithReferenceIds.js.map +1 -1
- package/lib/resolvers/content.d.ts +8 -8
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/core.d.ts +1 -0
- package/lib/resolvers/core.js +27 -21
- package/lib/resolvers/core.js.map +1 -1
- package/lib/resolvers/image.js.map +1 -1
- package/lib/resolvers/index.d.ts +48 -36
- package/lib/resolvers/index.js +2 -0
- package/lib/resolvers/index.js.map +1 -1
- package/lib/resolvers/list.d.ts +14 -0
- package/lib/resolvers/list.js +16 -0
- package/lib/resolvers/list.js.map +1 -0
- package/lib/resolvers/literal-union.js.map +1 -1
- package/lib/resolvers/meta-link.js.map +1 -1
- package/lib/resolvers/richText.d.ts +5 -5
- package/lib/resolvers/scalars.d.ts +4 -0
- package/lib/resolvers/scalars.js +18 -1
- package/lib/resolvers/scalars.js.map +1 -1
- package/lib/types/cache.js +1 -2
- package/lib/types/cache.js.map +1 -1
- package/package.json +5 -5
- package/src/datasources/capi.test.ts +6 -1
- package/src/datasources/capi.ts +19 -1
- package/src/datasources/instrumented.ts +4 -2
- package/src/fixtures/dummyContext.ts +1 -1
- package/src/generated/index.ts +62 -0
- package/src/helpers/flatten-formatted-zod-errors.ts +67 -0
- package/src/index.ts +3 -1
- package/src/model/CapiList.ts +103 -0
- package/src/model/CapiResponse.ts +7 -69
- package/src/model/schemas/capi/list.ts +36 -0
- package/src/resolvers/core.ts +29 -22
- package/src/resolvers/index.ts +2 -0
- package/src/resolvers/list.ts +15 -0
- package/src/resolvers/scalars.ts +30 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/core.graphql +3 -0
- package/typedefs/list.graphql +20 -0
- package/typedefs/scalars.graphql +2 -0
package/src/generated/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
|
|
2
2
|
import type { Concept as ConceptModel } from '../model/Concept';
|
|
3
3
|
import type { Person as PersonModel } from '../model/Person';
|
|
4
|
+
import type { CapiList } from '../model/CapiList';
|
|
4
5
|
import type { CapiResponse } from '../model/CapiResponse';
|
|
5
6
|
import type { Image as ImageModel } from '../model/Image';
|
|
6
7
|
import type { Clip as ClipModel } from '../model/Clip';
|
|
@@ -37,6 +38,8 @@ export type Scalars = {
|
|
|
37
38
|
ImageFormat: { input: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; output: 'standard' | 'standard-inline' | 'desktop' | 'mobile' | 'wide' | 'square' | 'square-ftedit' | 'portrait' | 'landscape'; }
|
|
38
39
|
ImageType: { input: 'image' | 'graphic'; output: 'image' | 'graphic'; }
|
|
39
40
|
JSON: { input: any; output: any; }
|
|
41
|
+
LayoutHint: { input: 'standaloneimage' | 'landscape' | 'bigstory' | 'assassination'; output: 'standaloneimage' | 'landscape' | 'bigstory' | 'assassination'; }
|
|
42
|
+
ListType: { input: 'OpinionAnalysis' | 'Promotional' | 'Recommended' | 'TopStories' | 'TopStoriesBeta' | 'KeyDevelopments'; output: 'OpinionAnalysis' | 'Promotional' | 'Recommended' | 'TopStories' | 'TopStoriesBeta' | 'KeyDevelopments'; }
|
|
40
43
|
PackageDesign: { input: 'special-report' | 'extra' | 'basic' | 'extra-wide'; output: 'special-report' | 'extra' | 'basic' | 'extra-wide'; }
|
|
41
44
|
RichTextSource: { input: 'standfirst' | 'summary' | 'bodyXML' | 'transcript'; output: 'standfirst' | 'summary' | 'bodyXML' | 'transcript'; }
|
|
42
45
|
TopperBackgroundColour: { input: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; output: 'paper' | 'wheat' | 'white' | 'black' | 'claret' | 'oxford' | 'slate' | 'crimson' | 'sky' | 'matisse'; }
|
|
@@ -942,6 +945,27 @@ export type LeadFlourish = {
|
|
|
942
945
|
readonly type?: Maybe<Scalars['String']['output']>;
|
|
943
946
|
};
|
|
944
947
|
|
|
948
|
+
export type List = {
|
|
949
|
+
/** The upstream API URL for the list */
|
|
950
|
+
readonly apiUrl: Scalars['String']['output'];
|
|
951
|
+
/** The UUID of the list */
|
|
952
|
+
readonly id: Scalars['String']['output'];
|
|
953
|
+
/** The content items referenced by this list */
|
|
954
|
+
readonly items: ReadonlyArray<Content>;
|
|
955
|
+
/** Used by ft.com to determine the visual style to use on the website */
|
|
956
|
+
readonly layoutHint?: Maybe<Scalars['LayoutHint']['output']>;
|
|
957
|
+
/** The type of the list, e.g. Recommended */
|
|
958
|
+
readonly listType: Scalars['ListType']['output'];
|
|
959
|
+
/** Array of publication IDs this list pertains to, e.g. 88fdde6c-2aa4-4f78-af02-9f680097cfd6 for FT Pink */
|
|
960
|
+
readonly publication?: Maybe<ReadonlyArray<Scalars['String']['output']>>;
|
|
961
|
+
/** A string ID used to trace content publishes through the pipeline, e.g. 'republish_tid_JNhUrBjdXo'. */
|
|
962
|
+
readonly publishReference?: Maybe<Scalars['String']['output']>;
|
|
963
|
+
/** The date when this list was published */
|
|
964
|
+
readonly publishedDate: Scalars['String']['output'];
|
|
965
|
+
/** The title of the list */
|
|
966
|
+
readonly title: Scalars['String']['output'];
|
|
967
|
+
};
|
|
968
|
+
|
|
945
969
|
export type LiveBlogPackage = Content & {
|
|
946
970
|
/** A scalar representing the access level of the article, eg. 'free'. */
|
|
947
971
|
readonly accessLevel?: Maybe<Scalars['AccessLevel']['output']>;
|
|
@@ -1359,6 +1383,8 @@ export type Query = {
|
|
|
1359
1383
|
readonly content: Content;
|
|
1360
1384
|
/** The article content as resolved from passed-in content-api json data. */
|
|
1361
1385
|
readonly contentFromJSON: Content;
|
|
1386
|
+
/** Resolve a content list from a uuid. */
|
|
1387
|
+
readonly list: List;
|
|
1362
1388
|
/** The version of the schema, eg. 2.0.0. */
|
|
1363
1389
|
readonly version: Scalars['String']['output'];
|
|
1364
1390
|
};
|
|
@@ -1373,6 +1399,11 @@ export type QueryContentFromJsonArgs = {
|
|
|
1373
1399
|
content: Scalars['JSON']['input'];
|
|
1374
1400
|
};
|
|
1375
1401
|
|
|
1402
|
+
|
|
1403
|
+
export type QueryListArgs = {
|
|
1404
|
+
uuid: Scalars['String']['input'];
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1376
1407
|
export type RawImage = Reference & {
|
|
1377
1408
|
/** The raw image object. */
|
|
1378
1409
|
readonly image: Image;
|
|
@@ -1793,8 +1824,11 @@ export type ResolversTypes = ResolversObject<{
|
|
|
1793
1824
|
Indicators: ResolverTypeWrapper<Indicators>;
|
|
1794
1825
|
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
|
|
1795
1826
|
JSON: ResolverTypeWrapper<Scalars['JSON']['output']>;
|
|
1827
|
+
LayoutHint: ResolverTypeWrapper<Scalars['LayoutHint']['output']>;
|
|
1796
1828
|
LayoutImage: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.LayoutImage>>;
|
|
1797
1829
|
LeadFlourish: ResolverTypeWrapper<LeadFlourishModel>;
|
|
1830
|
+
List: ResolverTypeWrapper<CapiList>;
|
|
1831
|
+
ListType: ResolverTypeWrapper<Scalars['ListType']['output']>;
|
|
1798
1832
|
LiveBlogPackage: ResolverTypeWrapper<CapiResponse>;
|
|
1799
1833
|
LiveBlogPost: ResolverTypeWrapper<CapiResponse>;
|
|
1800
1834
|
MainImage: ResolverTypeWrapper<ReferenceWithCAPIData<ContentTree.ImageSet>>;
|
|
@@ -1884,8 +1918,11 @@ export type ResolversParentTypes = ResolversObject<{
|
|
|
1884
1918
|
Indicators: Indicators;
|
|
1885
1919
|
Int: Scalars['Int']['output'];
|
|
1886
1920
|
JSON: Scalars['JSON']['output'];
|
|
1921
|
+
LayoutHint: Scalars['LayoutHint']['output'];
|
|
1887
1922
|
LayoutImage: ReferenceWithCAPIData<ContentTree.LayoutImage>;
|
|
1888
1923
|
LeadFlourish: LeadFlourishModel;
|
|
1924
|
+
List: CapiList;
|
|
1925
|
+
ListType: Scalars['ListType']['output'];
|
|
1889
1926
|
LiveBlogPackage: CapiResponse;
|
|
1890
1927
|
LiveBlogPost: CapiResponse;
|
|
1891
1928
|
MainImage: ReferenceWithCAPIData<ContentTree.ImageSet>;
|
|
@@ -2426,6 +2463,10 @@ export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes
|
|
|
2426
2463
|
name: 'JSON';
|
|
2427
2464
|
}
|
|
2428
2465
|
|
|
2466
|
+
export interface LayoutHintScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['LayoutHint'], any> {
|
|
2467
|
+
name: 'LayoutHint';
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2429
2470
|
export type LayoutImageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LayoutImage'] = ResolversParentTypes['LayoutImage']> = ResolversObject<{
|
|
2430
2471
|
picture: Resolver<Maybe<ResolversTypes['Picture']>, ParentType, ContextType>;
|
|
2431
2472
|
type: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
@@ -2440,6 +2481,23 @@ export type LeadFlourishResolvers<ContextType = QueryContext, ParentType extends
|
|
|
2440
2481
|
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
|
2441
2482
|
}>;
|
|
2442
2483
|
|
|
2484
|
+
export type ListResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['List'] = ResolversParentTypes['List']> = ResolversObject<{
|
|
2485
|
+
apiUrl: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2486
|
+
id: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2487
|
+
items: Resolver<ReadonlyArray<ResolversTypes['Content']>, ParentType, ContextType>;
|
|
2488
|
+
layoutHint: Resolver<Maybe<ResolversTypes['LayoutHint']>, ParentType, ContextType>;
|
|
2489
|
+
listType: Resolver<ResolversTypes['ListType'], ParentType, ContextType>;
|
|
2490
|
+
publication: Resolver<Maybe<ReadonlyArray<ResolversTypes['String']>>, ParentType, ContextType>;
|
|
2491
|
+
publishReference: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
2492
|
+
publishedDate: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2493
|
+
title: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2494
|
+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
|
2495
|
+
}>;
|
|
2496
|
+
|
|
2497
|
+
export interface ListTypeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['ListType'], any> {
|
|
2498
|
+
name: 'ListType';
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2443
2501
|
export type LiveBlogPackageResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['LiveBlogPackage'] = ResolversParentTypes['LiveBlogPackage']> = ResolversObject<{
|
|
2444
2502
|
accessLevel: Resolver<Maybe<ResolversTypes['AccessLevel']>, ParentType, ContextType>;
|
|
2445
2503
|
altStandfirst: Resolver<Maybe<ResolversTypes['AltStandfirst']>, ParentType, ContextType>;
|
|
@@ -2660,6 +2718,7 @@ export type PodcastTopperResolvers<ContextType = QueryContext, ParentType extend
|
|
|
2660
2718
|
export type QueryResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = ResolversObject<{
|
|
2661
2719
|
content: Resolver<ResolversTypes['Content'], ParentType, ContextType, RequireFields<QueryContentArgs, 'uuid'>>;
|
|
2662
2720
|
contentFromJSON: Resolver<ResolversTypes['Content'], ParentType, ContextType, RequireFields<QueryContentFromJsonArgs, 'content'>>;
|
|
2721
|
+
list: Resolver<ResolversTypes['List'], ParentType, ContextType, RequireFields<QueryListArgs, 'uuid'>>;
|
|
2663
2722
|
version: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2664
2723
|
}>;
|
|
2665
2724
|
|
|
@@ -2892,8 +2951,11 @@ export type Resolvers<ContextType = QueryContext> = ResolversObject<{
|
|
|
2892
2951
|
ImageWide: ImageWideResolvers<ContextType>;
|
|
2893
2952
|
Indicators: IndicatorsResolvers<ContextType>;
|
|
2894
2953
|
JSON: GraphQLScalarType;
|
|
2954
|
+
LayoutHint: GraphQLScalarType;
|
|
2895
2955
|
LayoutImage: LayoutImageResolvers<ContextType>;
|
|
2896
2956
|
LeadFlourish: LeadFlourishResolvers<ContextType>;
|
|
2957
|
+
List: ListResolvers<ContextType>;
|
|
2958
|
+
ListType: GraphQLScalarType;
|
|
2897
2959
|
LiveBlogPackage: LiveBlogPackageResolvers<ContextType>;
|
|
2898
2960
|
LiveBlogPost: LiveBlogPostResolvers<ContextType>;
|
|
2899
2961
|
MainImage: MainImageResolvers<ContextType>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function isPlainObject(object: unknown): object is Record<string, unknown> {
|
|
2
|
+
if (object && typeof object === 'object') {
|
|
3
|
+
return true
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type FormattedZodIssues = {
|
|
10
|
+
_errors: string[]
|
|
11
|
+
[key: string]: FormattedZodIssues | string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function flattenFormattedZodIssues(
|
|
15
|
+
issues: FormattedZodIssues,
|
|
16
|
+
object: unknown,
|
|
17
|
+
path: string[] = [],
|
|
18
|
+
flattened: string[] = []
|
|
19
|
+
): string[] {
|
|
20
|
+
if (issues._errors.length > 0) {
|
|
21
|
+
const literalErrors = issues._errors.filter((error) =>
|
|
22
|
+
error.startsWith('Invalid literal value')
|
|
23
|
+
)
|
|
24
|
+
const otherErrors = issues._errors.filter(
|
|
25
|
+
(error) => !error.startsWith('Invalid literal value')
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if (literalErrors.length > 0) {
|
|
29
|
+
flattened.push(
|
|
30
|
+
`${path.join(
|
|
31
|
+
'.'
|
|
32
|
+
)}: Invalid literal value, received "${object}", expected one of ` +
|
|
33
|
+
literalErrors
|
|
34
|
+
.flatMap((error) => {
|
|
35
|
+
const match = error.match(/expected (".+")$/)
|
|
36
|
+
if (match && match[1]) {
|
|
37
|
+
return [match[1]]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return []
|
|
41
|
+
})
|
|
42
|
+
.join()
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
flattened.push(...otherErrors.map((error) => `${path.join('.')}: ${error}`))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(issues)) {
|
|
50
|
+
if (
|
|
51
|
+
key !== '_errors' &&
|
|
52
|
+
!Array.isArray(value) &&
|
|
53
|
+
object &&
|
|
54
|
+
(Array.isArray(object) || isPlainObject(object))
|
|
55
|
+
) {
|
|
56
|
+
const isArrayKey = !Number.isNaN(parseInt(key))
|
|
57
|
+
flattenFormattedZodIssues(
|
|
58
|
+
value,
|
|
59
|
+
Array.isArray(object) ? object[parseInt(key)] : object[key],
|
|
60
|
+
[...path, isArrayKey ? '[]' : key],
|
|
61
|
+
flattened
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return flattened
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type BodyXMLToTreeError = {
|
|
|
17
17
|
actual: AnyNode['type'] | AnyNode['type'][]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export type SurrogateKey = { prefix: string; id: string }
|
|
21
|
+
|
|
20
22
|
export interface QueryContext {
|
|
21
23
|
redisAdapter: KeyValueCache
|
|
22
24
|
metrics: Metrics
|
|
@@ -30,7 +32,7 @@ export interface QueryContext {
|
|
|
30
32
|
schema: string
|
|
31
33
|
}
|
|
32
34
|
aggregatedErrors?: { bodyXMLToTree: BodyXMLToTreeError[] }
|
|
33
|
-
addSurrogateKeys(keys:
|
|
35
|
+
addSurrogateKeys(keys: SurrogateKey[]): void
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export const articleDocumentQuery = readFileSync(
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
|
|
2
|
+
import { QueryContext } from '..'
|
|
3
|
+
import { uuidFromUrl } from '../helpers/metadata'
|
|
4
|
+
import { List, listSchema } from './schemas/capi/list'
|
|
5
|
+
import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
|
|
6
|
+
|
|
7
|
+
export class CapiList {
|
|
8
|
+
static fromJSON(list: unknown, context: QueryContext): CapiList {
|
|
9
|
+
const result = listSchema.safeParse(list)
|
|
10
|
+
|
|
11
|
+
if (!result.success) {
|
|
12
|
+
context.logger.warn({
|
|
13
|
+
event: 'RECOVERABLE_ERROR',
|
|
14
|
+
error: new OperationalError({
|
|
15
|
+
message:
|
|
16
|
+
'The data received from the CAPI data source does not match our data source schema. It is likely that our schema will require updating to handle all possible responses from CAPI.',
|
|
17
|
+
code: 'CAPI_SCHEMA_VALIDATION_FAILURE',
|
|
18
|
+
schemaError: flattenFormattedZodIssues(result.error.format(), list),
|
|
19
|
+
contentId: (list as { id?: string }).id,
|
|
20
|
+
contentType: 'List',
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
context.metrics?.count(
|
|
24
|
+
`graphql.datasource.CapiDataSource.List.validation.failure.count`,
|
|
25
|
+
1
|
|
26
|
+
)
|
|
27
|
+
} else {
|
|
28
|
+
context.metrics?.count(
|
|
29
|
+
`graphql.datasource.CapiDataSource.List.validation.success.count`,
|
|
30
|
+
1
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new CapiList(list as List, context)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(private list: List, private context: QueryContext) {}
|
|
38
|
+
|
|
39
|
+
async items() {
|
|
40
|
+
if (!this.list.items) return []
|
|
41
|
+
|
|
42
|
+
const items = await Promise.all(
|
|
43
|
+
this.list.items.map(({ id }) =>
|
|
44
|
+
this.context.dataSources.capi
|
|
45
|
+
.getContent(uuidFromUrl(id))
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
if (error instanceof BaseError) {
|
|
48
|
+
if (error.data.upstreamStatusCode === 404) {
|
|
49
|
+
this.context.logger.warn({
|
|
50
|
+
event: 'RECOVERABLE_ERROR',
|
|
51
|
+
error: new OperationalError({
|
|
52
|
+
code: 'LIST_CONTENT_NOT_FOUND',
|
|
53
|
+
message: 'List content not found in Content API',
|
|
54
|
+
uuid: uuidFromUrl(error.data.upstreamUrl),
|
|
55
|
+
cause: error,
|
|
56
|
+
}),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw error
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// filter out missing list items to match n-lists-client behaviour
|
|
69
|
+
return items.filter((item) => item !== null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
id() {
|
|
73
|
+
return uuidFromUrl(this.list.id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
apiUrl() {
|
|
77
|
+
return this.list.apiUrl
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
title() {
|
|
81
|
+
return this.list.title
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
listType() {
|
|
85
|
+
return this.list.listType
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
publishedDate() {
|
|
89
|
+
return this.list.publishedDate
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
layoutHint() {
|
|
93
|
+
return this.list.layoutHint ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
publication() {
|
|
97
|
+
return this.list.publication ?? null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
publishReference() {
|
|
101
|
+
return this.list.publishReference ?? null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -39,25 +39,13 @@ import { Topper } from './Topper'
|
|
|
39
39
|
import { z } from 'zod'
|
|
40
40
|
import { Byline } from './Byline'
|
|
41
41
|
import { RichText } from './RichText'
|
|
42
|
-
|
|
43
|
-
function isPlainObject(object: unknown): object is Record<string, unknown> {
|
|
44
|
-
if (object && typeof object === 'object') {
|
|
45
|
-
return true
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
42
|
+
import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
|
|
50
43
|
|
|
51
44
|
type Design = {
|
|
52
45
|
theme: LiteralUnionScalarValues<typeof PackageDesign>
|
|
53
46
|
layout?: 'default' | 'wide'
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
type FormattedZodIssues = {
|
|
57
|
-
_errors: string[]
|
|
58
|
-
[key: string]: FormattedZodIssues | string[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
49
|
type ZodInputValue =
|
|
62
50
|
| ZodInputObject
|
|
63
51
|
| ZodInputValue[]
|
|
@@ -71,61 +59,6 @@ type ZodInputObject = {
|
|
|
71
59
|
[key: string]: ZodInputValue
|
|
72
60
|
}
|
|
73
61
|
|
|
74
|
-
function flattenFormattedZodIssues(
|
|
75
|
-
issues: FormattedZodIssues,
|
|
76
|
-
object: unknown,
|
|
77
|
-
path: string[] = [],
|
|
78
|
-
flattened: string[] = []
|
|
79
|
-
): string[] {
|
|
80
|
-
if (issues._errors.length > 0) {
|
|
81
|
-
const literalErrors = issues._errors.filter((error) =>
|
|
82
|
-
error.startsWith('Invalid literal value')
|
|
83
|
-
)
|
|
84
|
-
const otherErrors = issues._errors.filter(
|
|
85
|
-
(error) => !error.startsWith('Invalid literal value')
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
if (literalErrors.length > 0) {
|
|
89
|
-
flattened.push(
|
|
90
|
-
`${path.join(
|
|
91
|
-
'.'
|
|
92
|
-
)}: Invalid literal value, received "${object}", expected one of ` +
|
|
93
|
-
literalErrors
|
|
94
|
-
.flatMap((error) => {
|
|
95
|
-
const match = error.match(/expected (".+")$/)
|
|
96
|
-
if (match && match[1]) {
|
|
97
|
-
return [match[1]]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return []
|
|
101
|
-
})
|
|
102
|
-
.join()
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
flattened.push(...otherErrors.map((error) => `${path.join('.')}: ${error}`))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const [key, value] of Object.entries(issues)) {
|
|
110
|
-
if (
|
|
111
|
-
key !== '_errors' &&
|
|
112
|
-
!Array.isArray(value) &&
|
|
113
|
-
object &&
|
|
114
|
-
(Array.isArray(object) || isPlainObject(object))
|
|
115
|
-
) {
|
|
116
|
-
const isArrayKey = !Number.isNaN(parseInt(key))
|
|
117
|
-
flattenFormattedZodIssues(
|
|
118
|
-
value,
|
|
119
|
-
Array.isArray(object) ? object[parseInt(key)] : object[key],
|
|
120
|
-
[...path, isArrayKey ? '[]' : key],
|
|
121
|
-
flattened
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return flattened
|
|
127
|
-
}
|
|
128
|
-
|
|
129
62
|
function getContentType(
|
|
130
63
|
content: BaselineContent,
|
|
131
64
|
validate?: true
|
|
@@ -730,7 +663,12 @@ export class CapiResponse {
|
|
|
730
663
|
const contains = await this.contains()
|
|
731
664
|
|
|
732
665
|
if (contains && contains.length) {
|
|
733
|
-
this.context.addSurrogateKeys(
|
|
666
|
+
this.context.addSurrogateKeys(
|
|
667
|
+
contains.map((article) => ({
|
|
668
|
+
prefix: 'contentPipelineArticle',
|
|
669
|
+
id: article.id(),
|
|
670
|
+
}))
|
|
671
|
+
)
|
|
734
672
|
|
|
735
673
|
const liveBlogPosts = contains.sort(
|
|
736
674
|
(a, b) => b.publishedTimestamp() - a.publishedTimestamp()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const listSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
apiUrl: z.string(),
|
|
6
|
+
title: z.string(),
|
|
7
|
+
items: z
|
|
8
|
+
.array(
|
|
9
|
+
z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
apiUrl: z.string(),
|
|
12
|
+
})
|
|
13
|
+
)
|
|
14
|
+
.optional(),
|
|
15
|
+
listType: z.union([
|
|
16
|
+
z.literal('OpinionAnalysis'),
|
|
17
|
+
z.literal('Promotional'),
|
|
18
|
+
z.literal('Recommended'),
|
|
19
|
+
z.literal('TopStories'),
|
|
20
|
+
z.literal('TopStoriesBeta'),
|
|
21
|
+
z.literal('KeyDevelopments'),
|
|
22
|
+
]),
|
|
23
|
+
publishedDate: z.string(),
|
|
24
|
+
layoutHint: z
|
|
25
|
+
.union([
|
|
26
|
+
z.literal('standaloneimage'),
|
|
27
|
+
z.literal('landscape'),
|
|
28
|
+
z.literal('bigstory'),
|
|
29
|
+
z.literal('assassination'),
|
|
30
|
+
])
|
|
31
|
+
.optional(),
|
|
32
|
+
publication: z.array(z.string()).optional(),
|
|
33
|
+
publishReference: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export type List = z.input<typeof listSchema>
|
package/src/resolvers/core.ts
CHANGED
|
@@ -10,6 +10,31 @@ const packageJson = JSON.parse(
|
|
|
10
10
|
|
|
11
11
|
export const version = packageJson.version
|
|
12
12
|
|
|
13
|
+
function handleContentError(error: unknown, uuid: string): Error | unknown {
|
|
14
|
+
if (error instanceof BaseError) {
|
|
15
|
+
if (error.data.upstreamStatusCode === 404) {
|
|
16
|
+
return new HttpError({
|
|
17
|
+
code: 'CONTENT_NOT_FOUND',
|
|
18
|
+
message: 'Content not found in Content API',
|
|
19
|
+
cause: error,
|
|
20
|
+
uuid,
|
|
21
|
+
statusCode: 404,
|
|
22
|
+
relatesToSystems: ['up-ica'],
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
if (error.code === 'UNEXPECTED_CONTENT_TYPE') {
|
|
26
|
+
throw new HttpError({
|
|
27
|
+
code: error.code,
|
|
28
|
+
cause: error,
|
|
29
|
+
message: `Requested a content type we don't handle`,
|
|
30
|
+
statusCode: 404,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return error
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
const resolvers = {
|
|
14
39
|
Query: {
|
|
15
40
|
version: () => version,
|
|
@@ -17,30 +42,12 @@ const resolvers = {
|
|
|
17
42
|
try {
|
|
18
43
|
return await context.dataSources.capi.getContent(args.uuid)
|
|
19
44
|
} catch (error) {
|
|
20
|
-
|
|
21
|
-
if (error.data.upstreamStatusCode === 404) {
|
|
22
|
-
throw new HttpError({
|
|
23
|
-
code: 'CONTENT_NOT_FOUND',
|
|
24
|
-
message: 'Content not found in Content API',
|
|
25
|
-
cause: error,
|
|
26
|
-
uuid: args.uuid,
|
|
27
|
-
statusCode: 404,
|
|
28
|
-
relatesToSystems: ['up-ica'],
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
if (error.code === 'UNEXPECTED_CONTENT_TYPE') {
|
|
32
|
-
throw new HttpError({
|
|
33
|
-
code: error.code,
|
|
34
|
-
cause: error,
|
|
35
|
-
message: `Requested a content type we don't handle`,
|
|
36
|
-
statusCode: 404,
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
throw error
|
|
45
|
+
throw handleContentError(error, args.uuid)
|
|
42
46
|
}
|
|
43
47
|
},
|
|
48
|
+
list(_, args, context) {
|
|
49
|
+
return context.dataSources.capi.getList(args.uuid)
|
|
50
|
+
},
|
|
44
51
|
contentFromJSON(_, { content }, context) {
|
|
45
52
|
return CapiResponse.fromJSON(content, context)
|
|
46
53
|
},
|
package/src/resolvers/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { default as teaser } from './teaser'
|
|
|
11
11
|
import { default as topper } from './topper'
|
|
12
12
|
import { default as person } from './person'
|
|
13
13
|
import { default as leadFlourish } from './leadFlourish'
|
|
14
|
+
import { default as list } from './list'
|
|
14
15
|
import { resolvers as references } from './content-tree/references'
|
|
15
16
|
import { Resolvers } from '../generated'
|
|
16
17
|
|
|
@@ -29,6 +30,7 @@ const resolvers = {
|
|
|
29
30
|
...topper,
|
|
30
31
|
...person,
|
|
31
32
|
...leadFlourish,
|
|
33
|
+
...list,
|
|
32
34
|
} satisfies Resolvers
|
|
33
35
|
|
|
34
36
|
export default resolvers
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ListResolvers } from '../generated'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
List: {
|
|
5
|
+
id: (parent) => parent.id(),
|
|
6
|
+
apiUrl: (parent) => parent.apiUrl(),
|
|
7
|
+
title: (parent) => parent.title(),
|
|
8
|
+
items: (parent) => parent.items(),
|
|
9
|
+
listType: (parent) => parent.listType(),
|
|
10
|
+
publishedDate: (parent) => parent.publishedDate(),
|
|
11
|
+
layoutHint: (parent) => parent.layoutHint(),
|
|
12
|
+
publication: (parent) => parent.publication(),
|
|
13
|
+
publishReference: (parent) => parent.publishReference(),
|
|
14
|
+
},
|
|
15
|
+
} satisfies { List: ListResolvers }
|
package/src/resolvers/scalars.ts
CHANGED
|
@@ -145,6 +145,34 @@ export const ContentType = new LiteralUnionScalar<
|
|
|
145
145
|
],
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
+
export const ListType = new LiteralUnionScalar<
|
|
149
|
+
[
|
|
150
|
+
'OpinionAnalysis',
|
|
151
|
+
'Promotional',
|
|
152
|
+
'Recommended',
|
|
153
|
+
'TopStories',
|
|
154
|
+
'TopStoriesBeta',
|
|
155
|
+
'KeyDevelopments'
|
|
156
|
+
]
|
|
157
|
+
>({
|
|
158
|
+
name: 'ListType',
|
|
159
|
+
values: [
|
|
160
|
+
'OpinionAnalysis',
|
|
161
|
+
'Promotional',
|
|
162
|
+
'Recommended',
|
|
163
|
+
'TopStories',
|
|
164
|
+
'TopStoriesBeta',
|
|
165
|
+
'KeyDevelopments',
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export const LayoutHint = new LiteralUnionScalar<
|
|
170
|
+
['standaloneimage', 'landscape', 'bigstory', 'assassination']
|
|
171
|
+
>({
|
|
172
|
+
name: 'LayoutHint',
|
|
173
|
+
values: ['standaloneimage', 'landscape', 'bigstory', 'assassination'],
|
|
174
|
+
})
|
|
175
|
+
|
|
148
176
|
const resolvers = {
|
|
149
177
|
ClipFormat,
|
|
150
178
|
ImageFormat,
|
|
@@ -156,6 +184,8 @@ const resolvers = {
|
|
|
156
184
|
ImageType,
|
|
157
185
|
RichTextSource,
|
|
158
186
|
ContentType,
|
|
187
|
+
ListType,
|
|
188
|
+
LayoutHint,
|
|
159
189
|
JSON: new GraphQLScalarType({ name: 'JSON' }),
|
|
160
190
|
}
|
|
161
191
|
|