@financial-times/cp-content-pipeline-schema 3.3.2 → 3.5.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 +30 -0
- package/lib/datasources/capi.d.ts +0 -2
- package/lib/datasources/capi.js +10 -23
- package/lib/datasources/capi.js.map +1 -1
- package/lib/datasources/instrumented.js +4 -1
- package/lib/datasources/instrumented.js.map +1 -1
- package/lib/datasources/origami-image.d.ts +0 -2
- package/lib/datasources/origami-image.js +5 -19
- package/lib/datasources/origami-image.js.map +1 -1
- package/lib/datasources/twitter.d.ts +0 -2
- package/lib/datasources/twitter.js +2 -16
- package/lib/datasources/twitter.js.map +1 -1
- package/lib/datasources/url-management.js +5 -3
- package/lib/datasources/url-management.js.map +1 -1
- package/lib/fixtures/dummyContext.js +1 -1
- package/lib/generated/index.d.ts +72 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js.map +1 -1
- package/lib/model/Byline.js +11 -14
- package/lib/model/Byline.js.map +1 -1
- package/lib/model/CapiList.js +2 -0
- package/lib/model/CapiList.js.map +1 -1
- package/lib/model/CapiResponse.d.ts +7 -3
- package/lib/model/CapiResponse.js +125 -57
- package/lib/model/CapiResponse.js.map +1 -1
- package/lib/model/CapiResponse.test.js +92 -7
- package/lib/model/CapiResponse.test.js.map +1 -1
- package/lib/model/Clip.js +2 -0
- package/lib/model/Clip.js.map +1 -1
- package/lib/model/Concept.js +4 -10
- package/lib/model/Concept.js.map +1 -1
- package/lib/model/FlourishSource.js +5 -10
- package/lib/model/FlourishSource.js.map +1 -1
- package/lib/model/Image.js +10 -20
- package/lib/model/Image.js.map +1 -1
- package/lib/model/LeadFlourish.js +4 -10
- package/lib/model/LeadFlourish.js.map +1 -1
- package/lib/model/Person.js +5 -16
- package/lib/model/Person.js.map +1 -1
- package/lib/model/Picture.js +3 -0
- package/lib/model/Picture.js.map +1 -1
- package/lib/model/RichText.js +3 -0
- package/lib/model/RichText.js.map +1 -1
- package/lib/model/Topper.js +5 -16
- package/lib/model/Topper.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +5 -0
- package/lib/model/schemas/capi/audio.d.ts +3 -0
- package/lib/model/schemas/capi/base-schema.d.ts +8 -0
- package/lib/model/schemas/capi/base-schema.js +1 -0
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +3 -0
- package/lib/model/schemas/capi/custom-code-component.d.ts +5 -0
- package/lib/model/schemas/capi/index.d.ts +24 -0
- package/lib/model/schemas/capi/internal-content.d.ts +1 -1
- package/lib/model/schemas/capi/live-blog-package.d.ts +5 -0
- package/lib/model/schemas/capi/placeholder.d.ts +5 -0
- package/lib/model/schemas/capi/video.d.ts +3 -0
- package/lib/resolvers/content-tree/references/ClipSet.d.ts +1 -0
- package/lib/resolvers/content-tree/references/ClipSet.js +1 -0
- package/lib/resolvers/content-tree/references/ClipSet.js.map +1 -1
- package/lib/resolvers/content-tree/references/RawImage.js +2 -0
- package/lib/resolvers/content-tree/references/RawImage.js.map +1 -1
- package/lib/resolvers/content.d.ts +26 -1
- package/lib/resolvers/content.js +21 -1
- package/lib/resolvers/content.js.map +1 -1
- package/lib/resolvers/index.d.ts +26 -1
- package/lib/resolvers/literal-union.js +1 -0
- package/lib/resolvers/literal-union.js.map +1 -1
- package/lib/types/connection.d.ts +21 -0
- package/lib/types/connection.js +5 -0
- package/lib/types/connection.js.map +1 -0
- package/package.json +1 -1
- package/queries/article.graphql +9 -2
- package/src/datasources/capi.ts +1 -14
- package/src/datasources/origami-image.ts +1 -13
- package/src/datasources/twitter.ts +1 -14
- package/src/fixtures/dummyContext.ts +1 -1
- package/src/generated/index.ts +74 -1
- package/src/index.ts +1 -0
- package/src/model/CapiResponse.test.ts +137 -7
- package/src/model/CapiResponse.ts +129 -37
- package/src/model/schemas/capi/base-schema.ts +1 -0
- package/src/model/schemas/capi/internal-content.ts +1 -1
- package/src/resolvers/content-tree/references/ClipSet.ts +1 -0
- package/src/resolvers/content.ts +31 -1
- package/src/types/connection.ts +28 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/content.graphql +40 -2
- package/typedefs/references/clipSet.graphql +3 -0
- package/lib/helpers/timeout-error.d.ts +0 -6
- package/lib/helpers/timeout-error.js +0 -15
- package/lib/helpers/timeout-error.js.map +0 -1
- package/src/helpers/timeout-error.ts +0 -13
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
MainImage,
|
|
5
5
|
ClipSet,
|
|
6
6
|
CustomCodeComponentReference,
|
|
7
|
+
LiveBlogPackage,
|
|
7
8
|
} from './schemas/capi/internal-content'
|
|
8
9
|
import conceptIds from '@financial-times/n-concept-ids'
|
|
9
10
|
import metadata from '@financial-times/n-display-metadata'
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
ContentAnnotationsArgs,
|
|
35
36
|
ContentPackageContainsArgs,
|
|
36
37
|
ContentUrlArgs,
|
|
38
|
+
LiveBlogPackageLiveBlogPostsArgs,
|
|
37
39
|
Media,
|
|
38
40
|
TableOfContents,
|
|
39
41
|
} from '../generated'
|
|
@@ -42,6 +44,7 @@ import { z } from 'zod'
|
|
|
42
44
|
import { Byline } from './Byline'
|
|
43
45
|
import { RichText } from './RichText'
|
|
44
46
|
import flattenFormattedZodIssues from '../helpers/flatten-formatted-zod-errors'
|
|
47
|
+
import { Connection, ConnectionArguments } from '../types/connection'
|
|
45
48
|
|
|
46
49
|
type Design = {
|
|
47
50
|
theme: LiteralUnionScalarValues<typeof PackageDesign>
|
|
@@ -708,21 +711,54 @@ export class CapiResponse {
|
|
|
708
711
|
surroundingArticles,
|
|
709
712
|
fromId,
|
|
710
713
|
}: Partial<ContentPackageContainsArgs> = {}): Promise<CapiResponse[] | null> {
|
|
714
|
+
let edges
|
|
715
|
+
if (fromId && surroundingArticles) {
|
|
716
|
+
const afterId = await this.containsConnection({
|
|
717
|
+
after: fromId,
|
|
718
|
+
first: surroundingArticles,
|
|
719
|
+
})
|
|
720
|
+
const beforeAndIncludingId = await this.containsConnection({
|
|
721
|
+
before: afterId.pageInfo.startCursor,
|
|
722
|
+
last: surroundingArticles + 1,
|
|
723
|
+
})
|
|
724
|
+
edges = afterId.edges.concat(beforeAndIncludingId.edges)
|
|
725
|
+
} else {
|
|
726
|
+
const connection = await this.containsConnection()
|
|
727
|
+
edges = connection.edges
|
|
728
|
+
}
|
|
729
|
+
return edges.map((edge) => edge.node)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async containsConnection(
|
|
733
|
+
{ before, after, first, last }: ConnectionArguments = {},
|
|
734
|
+
transformer?: (
|
|
735
|
+
contains: LiveBlogPackage['contains']
|
|
736
|
+
) => LiveBlogPackage['contains']
|
|
737
|
+
): Promise<Connection<CapiResponse>> {
|
|
711
738
|
if ('contains' in this.capiData) {
|
|
712
|
-
const
|
|
713
|
-
|
|
739
|
+
const { contains: containsUntransformed } = this.capiData
|
|
740
|
+
const contains = transformer
|
|
741
|
+
? transformer(containsUntransformed)
|
|
742
|
+
: containsUntransformed
|
|
743
|
+
|
|
744
|
+
const beforeIndex = before
|
|
745
|
+
? contains.findLastIndex(
|
|
746
|
+
(post) => post.id === `http://api.ft.com/things/${before}`
|
|
747
|
+
)
|
|
748
|
+
: -1
|
|
749
|
+
const afterIndex = after
|
|
750
|
+
? contains.findIndex(
|
|
751
|
+
(post) => post.id === `http://api.ft.com/things/${after}`
|
|
752
|
+
)
|
|
753
|
+
: -1
|
|
754
|
+
const cursoredContains = contains.slice(
|
|
755
|
+
afterIndex >= 0 ? afterIndex + 1 : undefined,
|
|
756
|
+
beforeIndex >= 0 ? beforeIndex : undefined
|
|
714
757
|
)
|
|
715
|
-
|
|
716
|
-
const contains =
|
|
717
|
-
fromId && surroundingArticles
|
|
718
|
-
? this.capiData.contains.slice(
|
|
719
|
-
Math.max(0, fromIndex - surroundingArticles),
|
|
720
|
-
fromIndex + surroundingArticles
|
|
721
|
-
)
|
|
722
|
-
: this.capiData.contains
|
|
758
|
+
const slicedContains = cursoredContains.slice(-(last ?? 0), first)
|
|
723
759
|
|
|
724
760
|
const results = await Promise.allSettled(
|
|
725
|
-
|
|
761
|
+
slicedContains.map(({ id }) =>
|
|
726
762
|
this.context.dataSources.capi.getContent(uuidFromUrl(id), this)
|
|
727
763
|
)
|
|
728
764
|
)
|
|
@@ -741,17 +777,49 @@ export class CapiResponse {
|
|
|
741
777
|
cause: new AggregateError(failed.map((result) => result.reason)),
|
|
742
778
|
}),
|
|
743
779
|
})
|
|
780
|
+
|
|
781
|
+
if (
|
|
782
|
+
failed.some(
|
|
783
|
+
(result) =>
|
|
784
|
+
(result.reason as OperationalError).code === 'FETCH_TIMEOUT_ERROR'
|
|
785
|
+
)
|
|
786
|
+
) {
|
|
787
|
+
this.context.contentTimedOut = true
|
|
788
|
+
}
|
|
744
789
|
}
|
|
745
790
|
|
|
746
|
-
|
|
791
|
+
const edges = results
|
|
747
792
|
.filter(
|
|
748
793
|
(result): result is PromiseFulfilledResult<CapiResponse> =>
|
|
749
794
|
result.status === 'fulfilled'
|
|
750
795
|
)
|
|
751
|
-
.map((result) =>
|
|
796
|
+
.map((result) => {
|
|
797
|
+
const node = result.value
|
|
798
|
+
return { node, cursor: node.id() }
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
edges,
|
|
803
|
+
pageInfo: {
|
|
804
|
+
hasPreviousPage: last
|
|
805
|
+
? cursoredContains.length > last
|
|
806
|
+
: afterIndex > 0,
|
|
807
|
+
hasNextPage: first
|
|
808
|
+
? cursoredContains.length > first
|
|
809
|
+
: beforeIndex > 0,
|
|
810
|
+
startCursor: edges[0]?.cursor,
|
|
811
|
+
endCursor: edges[edges.length - 1]?.cursor,
|
|
812
|
+
},
|
|
813
|
+
}
|
|
752
814
|
}
|
|
753
815
|
|
|
754
|
-
return
|
|
816
|
+
return {
|
|
817
|
+
edges: [],
|
|
818
|
+
pageInfo: {
|
|
819
|
+
hasPreviousPage: false,
|
|
820
|
+
hasNextPage: false,
|
|
821
|
+
},
|
|
822
|
+
}
|
|
755
823
|
}
|
|
756
824
|
|
|
757
825
|
containsLength() {
|
|
@@ -768,25 +836,37 @@ export class CapiResponse {
|
|
|
768
836
|
: null
|
|
769
837
|
}
|
|
770
838
|
|
|
771
|
-
|
|
772
|
-
|
|
839
|
+
private handleLiveBlogPosts(liveBlogPosts: LiveBlogPackage['contains']) {
|
|
840
|
+
this.context.addSurrogateKeys(
|
|
841
|
+
liveBlogPosts.map((article) => ({
|
|
842
|
+
prefix: 'contentPipelineArticle',
|
|
843
|
+
id: article.id,
|
|
844
|
+
}))
|
|
845
|
+
)
|
|
773
846
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
contains.map((article) => ({
|
|
777
|
-
prefix: 'contentPipelineArticle',
|
|
778
|
-
id: article.id(),
|
|
779
|
-
}))
|
|
780
|
-
)
|
|
847
|
+
return [...liveBlogPosts].reverse()
|
|
848
|
+
}
|
|
781
849
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
850
|
+
async liveBlogPosts(
|
|
851
|
+
args?: LiveBlogPackageLiveBlogPostsArgs
|
|
852
|
+
): Promise<CapiResponse[] | []> {
|
|
853
|
+
const containsConnections = await this.containsConnection(
|
|
854
|
+
{
|
|
855
|
+
first: args?.count ?? undefined,
|
|
856
|
+
},
|
|
857
|
+
this.handleLiveBlogPosts.bind(this)
|
|
858
|
+
)
|
|
859
|
+
const contains = containsConnections.edges.map((edge) => edge.node)
|
|
785
860
|
|
|
786
|
-
|
|
787
|
-
|
|
861
|
+
return contains.sort(
|
|
862
|
+
(a, b) => b.publishedTimestamp() - a.publishedTimestamp()
|
|
863
|
+
)
|
|
864
|
+
}
|
|
788
865
|
|
|
789
|
-
|
|
866
|
+
async liveBlogPostsConnection(
|
|
867
|
+
args: ConnectionArguments
|
|
868
|
+
): Promise<Connection<CapiResponse>> {
|
|
869
|
+
return this.containsConnection(args, this.handleLiveBlogPosts.bind(this))
|
|
790
870
|
}
|
|
791
871
|
|
|
792
872
|
isPinned() {
|
|
@@ -803,16 +883,28 @@ export class CapiResponse {
|
|
|
803
883
|
|
|
804
884
|
async pinnedPost(): Promise<CapiResponse | null> {
|
|
805
885
|
if ('pinnedPosts' in this.capiData) {
|
|
806
|
-
const pinnedPosts = this.capiData.pinnedPosts
|
|
807
|
-
const liveBlogPosts = await this.liveBlogPosts()
|
|
808
|
-
|
|
886
|
+
const pinnedPosts = this.capiData.pinnedPosts
|
|
809
887
|
const pinnedPostId = pinnedPosts[0]
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
888
|
+
if (!pinnedPostId) {
|
|
889
|
+
return null
|
|
890
|
+
}
|
|
813
891
|
|
|
814
|
-
|
|
815
|
-
return
|
|
892
|
+
try {
|
|
893
|
+
return await this.context.dataSources.capi.getContent(
|
|
894
|
+
uuidFromUrl(pinnedPostId),
|
|
895
|
+
this
|
|
896
|
+
)
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (isError(error)) {
|
|
899
|
+
this.context.logger.warn({
|
|
900
|
+
event: 'RECOVERABLE_ERROR',
|
|
901
|
+
error: new OperationalError({
|
|
902
|
+
code: 'PACKAGE_PINNED_POST_ERROR',
|
|
903
|
+
message: `Failed to fetch the pinned post ${pinnedPostId} contained in the package ${this.id()}`,
|
|
904
|
+
cause: error,
|
|
905
|
+
}),
|
|
906
|
+
})
|
|
907
|
+
}
|
|
816
908
|
}
|
|
817
909
|
}
|
|
818
910
|
return null
|
|
@@ -123,6 +123,7 @@ export const ClipSet = z.object({
|
|
|
123
123
|
dataCopyright: z.string().optional(),
|
|
124
124
|
description: z.string().optional(),
|
|
125
125
|
displayTitle: z.string().optional(),
|
|
126
|
+
systemTitle: z.string().optional(),
|
|
126
127
|
contentWarning: z.string().array().optional(),
|
|
127
128
|
noAudio: z.boolean().optional(),
|
|
128
129
|
source: z.string().optional(),
|
|
@@ -23,7 +23,7 @@ import { contentPackageSchema } from './content-package'
|
|
|
23
23
|
|
|
24
24
|
export type Article = z.infer<typeof articleSchema>
|
|
25
25
|
type Placeholder = z.infer<typeof placeholderSchema>
|
|
26
|
-
type LiveBlogPackage = z.infer<typeof liveBlogPackageSchema>
|
|
26
|
+
export type LiveBlogPackage = z.infer<typeof liveBlogPackageSchema>
|
|
27
27
|
type ContentPackage = z.infer<typeof contentPackageSchema>
|
|
28
28
|
type Audio = z.infer<typeof audioSchema>
|
|
29
29
|
type Video = z.infer<typeof videoSchema>
|
|
@@ -67,6 +67,7 @@ export const ClipSet = {
|
|
|
67
67
|
(parent.reference as OldClipContentTree).credits ??
|
|
68
68
|
getClipSet(parent)?.dataCopyright,
|
|
69
69
|
displayTitle: (parent) => getClipSet(parent)?.displayTitle || null,
|
|
70
|
+
systemTitle: (parent) => getClipSet(parent)?.systemTitle || null,
|
|
70
71
|
contentWarning: (parent) => getClipSet(parent)?.contentWarning || null,
|
|
71
72
|
source: (parent) => getClipSet(parent)?.source || null,
|
|
72
73
|
subtitle: (parent) => getClipSet(parent)?.subtitle || null,
|
package/src/resolvers/content.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
AudioResolvers,
|
|
6
6
|
ContentPackageResolvers,
|
|
7
7
|
ContentResolvers,
|
|
8
|
+
ContentConnectionResolvers,
|
|
9
|
+
ContentEdgeResolvers,
|
|
10
|
+
PageInfoResolvers,
|
|
8
11
|
LiveBlogPackageResolvers,
|
|
9
12
|
LiveBlogPostResolvers,
|
|
10
13
|
VideoResolvers,
|
|
@@ -70,6 +73,23 @@ const resolvers = {
|
|
|
70
73
|
...contentResolvers,
|
|
71
74
|
},
|
|
72
75
|
|
|
76
|
+
ContentConnection: {
|
|
77
|
+
edges: (parent) => parent.edges,
|
|
78
|
+
pageInfo: (parent) => parent.pageInfo,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
ContentEdge: {
|
|
82
|
+
node: (parent) => parent.node,
|
|
83
|
+
cursor: (parent) => parent.cursor,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
PageInfo: {
|
|
87
|
+
hasPreviousPage: (parent) => parent.hasPreviousPage,
|
|
88
|
+
hasNextPage: (parent) => parent.hasNextPage,
|
|
89
|
+
startCursor: (parent) => parent.startCursor ?? null,
|
|
90
|
+
endCursor: (parent) => parent.endCursor ?? null,
|
|
91
|
+
},
|
|
92
|
+
|
|
73
93
|
Article: {
|
|
74
94
|
...contentResolvers,
|
|
75
95
|
containedIn: (parent) => parent.containedIn(),
|
|
@@ -101,7 +121,14 @@ const resolvers = {
|
|
|
101
121
|
|
|
102
122
|
LiveBlogPackage: {
|
|
103
123
|
...contentResolvers,
|
|
104
|
-
liveBlogPosts: (parent) => parent.liveBlogPosts(),
|
|
124
|
+
liveBlogPosts: (parent, args) => parent.liveBlogPosts(args),
|
|
125
|
+
liveBlogPostsConnection: (parent, args) =>
|
|
126
|
+
parent.liveBlogPostsConnection({
|
|
127
|
+
first: args.first ?? undefined,
|
|
128
|
+
after: args.after ?? undefined,
|
|
129
|
+
last: args.last ?? undefined,
|
|
130
|
+
before: args.before ?? undefined,
|
|
131
|
+
}),
|
|
105
132
|
pinnedPost: (parent) => parent.pinnedPost(),
|
|
106
133
|
realtime: (parent) => parent.realtime(),
|
|
107
134
|
},
|
|
@@ -161,6 +188,9 @@ const resolvers = {
|
|
|
161
188
|
Placeholder: PlaceholderResolvers
|
|
162
189
|
LiveBlogPost: LiveBlogPostResolvers
|
|
163
190
|
Content: ContentResolvers
|
|
191
|
+
ContentConnection: ContentConnectionResolvers
|
|
192
|
+
ContentEdge: ContentEdgeResolvers
|
|
193
|
+
PageInfo: PageInfoResolvers
|
|
164
194
|
LiveBlogPackage: LiveBlogPackageResolvers
|
|
165
195
|
ContentPackage: ContentPackageResolvers
|
|
166
196
|
Audio: AudioResolvers
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Types to aid the implementation of a Connection type, as specified in
|
|
2
|
+
// https://relay.dev/graphql/connections.htm
|
|
3
|
+
|
|
4
|
+
export type ConnectionCursor = string
|
|
5
|
+
|
|
6
|
+
export interface ConnectionPageInfo {
|
|
7
|
+
hasPreviousPage: boolean
|
|
8
|
+
hasNextPage: boolean
|
|
9
|
+
startCursor?: ConnectionCursor
|
|
10
|
+
endCursor?: ConnectionCursor
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConnectionEdge<T> {
|
|
14
|
+
node: T
|
|
15
|
+
cursor: ConnectionCursor
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Connection<T> {
|
|
19
|
+
edges: ConnectionEdge<T>[]
|
|
20
|
+
pageInfo: ConnectionPageInfo
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConnectionArguments {
|
|
24
|
+
first?: number
|
|
25
|
+
after?: ConnectionCursor
|
|
26
|
+
last?: number
|
|
27
|
+
before?: ConnectionCursor
|
|
28
|
+
}
|