@financial-times/cp-content-pipeline-schema 2.3.0 → 2.4.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 (47) 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 -1
  4. package/lib/model/CapiResponse.js +11 -5
  5. package/lib/model/CapiResponse.js.map +1 -1
  6. package/lib/model/Concept.test.js +0 -9
  7. package/lib/model/Concept.test.js.map +1 -1
  8. package/lib/model/RichText.d.ts +1 -1
  9. package/lib/model/RichText.js +20 -34
  10. package/lib/model/RichText.js.map +1 -1
  11. package/lib/model/RichText.test.js +0 -9
  12. package/lib/model/RichText.test.js.map +1 -1
  13. package/lib/resolvers/content-tree/bodyXMLToTree.d.ts +4 -3
  14. package/lib/resolvers/content-tree/bodyXMLToTree.js +2 -2
  15. package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
  16. package/lib/resolvers/content-tree/bodyXMLToTree.test.js +20 -32
  17. package/lib/resolvers/content-tree/bodyXMLToTree.test.js.map +1 -1
  18. package/lib/resolvers/content-tree/nodePredicates.d.ts +6 -13
  19. package/lib/resolvers/content-tree/nodePredicates.js +33 -33
  20. package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
  21. package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
  22. package/lib/resolvers/content-tree/tagMappings.d.ts +2 -1
  23. package/lib/resolvers/content-tree/tagMappings.js +60 -60
  24. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  25. package/lib/resolvers/content.d.ts +1 -0
  26. package/lib/resolvers/content.js +1 -0
  27. package/lib/resolvers/content.js.map +1 -1
  28. package/lib/resolvers/index.d.ts +1 -0
  29. package/package.json +1 -2
  30. package/queries/article.graphql +2 -1
  31. package/src/generated/index.ts +2 -0
  32. package/src/model/CapiResponse.ts +14 -7
  33. package/src/model/Concept.test.ts +0 -9
  34. package/src/model/RichText.test.ts +0 -10
  35. package/src/model/RichText.ts +27 -39
  36. package/src/resolvers/content-tree/bodyXMLToTree.test.ts +22 -32
  37. package/src/resolvers/content-tree/bodyXMLToTree.ts +11 -6
  38. package/src/resolvers/content-tree/nodePredicates.ts +39 -45
  39. package/src/resolvers/content-tree/tagMappings.ts +102 -60
  40. package/src/resolvers/content.ts +1 -0
  41. package/tsconfig.tsbuildinfo +1 -1
  42. package/typedefs/content.graphql +2 -1
  43. package/__mocks__/worker_threads.ts +0 -3
  44. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.d.ts +0 -9
  45. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.js +0 -18
  46. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.js.map +0 -1
  47. package/src/resolvers/content-tree/bodyXMLToTreeWorker.ts +0 -31
@@ -602,7 +602,7 @@ export class CapiResponse {
602
602
  : null
603
603
  }
604
604
 
605
- async liveBlogPosts(includePinned = true): Promise<CapiResponse[] | []> {
605
+ async liveBlogPosts(): Promise<CapiResponse[] | []> {
606
606
  const contains = await this.contains()
607
607
 
608
608
  if (contains && contains.length) {
@@ -614,17 +614,24 @@ export class CapiResponse {
614
614
  new Date(a.publishedDate()).getTime()
615
615
  )
616
616
 
617
- const pinnedPostId =
618
- 'pinnedPosts' in this.capiData && this.capiData?.pinnedPosts[0]
619
-
620
- return includePinned || !pinnedPostId
621
- ? liveBlogPosts
622
- : liveBlogPosts.filter((post) => post.id() !== pinnedPostId)
617
+ return liveBlogPosts
623
618
  }
624
619
 
625
620
  return []
626
621
  }
627
622
 
623
+ isPinned() {
624
+ if (
625
+ this.packageContainer &&
626
+ 'pinnedPosts' in this.packageContainer.capiData
627
+ ) {
628
+ const pinnedPosts = this.packageContainer?.capiData.pinnedPosts || []
629
+ const pinnedPostId = pinnedPosts[0]
630
+ return pinnedPostId === this.id()
631
+ }
632
+ return false
633
+ }
634
+
628
635
  async pinnedPost(): Promise<CapiResponse | null> {
629
636
  if ('pinnedPosts' in this.capiData) {
630
637
  const pinnedPosts = this.capiData.pinnedPosts || []
@@ -6,15 +6,6 @@ import { URLManagementDataSource } from '../datasources/url-management'
6
6
  import { CapiDataSource } from '../datasources/capi'
7
7
  import { Logger } from '@dotcom-reliability-kit/logger'
8
8
 
9
- jest.mock('@dotcom-reliability-kit/logger', () => ({
10
- Logger: jest.fn(() => ({
11
- debug: jest.fn(),
12
- error: jest.fn(),
13
- fatal: jest.fn(),
14
- info: jest.fn(),
15
- warn: jest.fn(),
16
- })),
17
- }))
18
9
  const vanityMock = jest.fn<URLManagementDataSource['get']>()
19
10
  const getPersonMock = jest.fn<CapiDataSource['getPerson']>()
20
11
  const context = {
@@ -1,16 +1,6 @@
1
1
  import { baseCapiObject } from '../fixtures/capiObject'
2
2
  import { RichText } from '../model/RichText'
3
3
 
4
- jest.mock('@dotcom-reliability-kit/logger', () => ({
5
- Logger: jest.fn(() => ({
6
- debug: jest.fn(),
7
- error: jest.fn(),
8
- fatal: jest.fn(),
9
- info: jest.fn(),
10
- warn: jest.fn(),
11
- })),
12
- }))
13
-
14
4
  describe('RichText resolver', () => {
15
5
  it('should transform bodyXML to an AST', async () => {
16
6
  const model = new RichText('bodyXML', baseCapiObject.bodyXML)
@@ -1,26 +1,16 @@
1
- import Piscina from 'piscina'
2
-
3
- import type { QueryContext } from '..'
4
- import bodyXMLToTreeWorker from '../resolvers/content-tree/bodyXMLToTreeWorker'
1
+ import bodyXMLToTree from '../resolvers/content-tree/bodyXMLToTree'
5
2
  import extractText from '../resolvers/content-tree/extractText'
6
- import type { PredicateError } from '../resolvers/content-tree/nodePredicates'
7
3
  import updateTreeWithReferenceIds from '../resolvers/content-tree/updateTreeWithReferenceIds'
8
4
  import { LiteralUnionScalarValues } from '../resolvers/literal-union'
9
5
  import { RichTextSource } from '../resolvers/scalars'
10
6
  import { CapiResponse } from './CapiResponse'
11
- import { OperationalError } from '@dotcom-reliability-kit/errors'
7
+ import {
8
+ commonTagMappings,
9
+ articleTagMappings,
10
+ liveBlogPostTagMappings,
11
+ } from '../resolvers/content-tree/tagMappings'
12
12
  import { ContentTree } from '@financial-times/content-tree'
13
-
14
- let piscina: Piscina | undefined
15
- // Don't run the thread pool during tests as it creates loads of threads that
16
- // aren't properly closed and leaves the process hanging. We do our best to
17
- // keep the same behaviour when running on the main thread vs a worker thread,
18
- // with the main difference being the lack of logs on the main thread.
19
- if (process.env.NODE_ENV !== 'test') {
20
- piscina = new Piscina({
21
- filename: require.resolve('../resolvers/content-tree/bodyXMLToTreeWorker'),
22
- })
23
- }
13
+ import { QueryContext } from '..'
24
14
 
25
15
  export class RichText {
26
16
  constructor(
@@ -34,29 +24,27 @@ export class RichText {
34
24
  }
35
25
 
36
26
  async structured(context?: QueryContext) {
37
- const args = {
38
- xml: this.value ?? '',
39
- responseMetadata: this.contentApiData
40
- ? {
41
- isLiveBlogPost: this.contentApiData.type() === 'LiveBlogPost',
42
- topperHasImage: this.contentApiData.topperHasImage(),
43
- }
44
- : undefined,
45
- }
46
- // forward errors from the worker threads to the logger
47
- piscina?.on('message', ({ event, error, ...predError }: PredicateError) => {
48
- const { stack } = error
49
- const opError = new OperationalError(predError)
50
- opError.stack = stack
51
- context?.logger.error({ event, error: opError })
27
+ const tree = await new Promise<ContentTree.Body>((resolve, reject) => {
28
+ // bodyXMLToTree is synchronous and slow. scheduling it in a setImmediate
29
+ // prevents it from blocking the event loop, so the app can still handle
30
+ // requests, and it won't skew resolver timing metrics by blocking the
31
+ // timers for quicker, asynchronous resolvers from finishing.
32
+
33
+ const tagMappings =
34
+ this.contentApiData?.type() === 'LiveBlogPost'
35
+ ? { ...commonTagMappings, ...liveBlogPostTagMappings }
36
+ : {
37
+ ...commonTagMappings,
38
+ ...articleTagMappings(this.contentApiData),
39
+ }
40
+ setImmediate(() => {
41
+ try {
42
+ resolve(bodyXMLToTree(this.value ?? '', tagMappings, context))
43
+ } catch (error) {
44
+ reject(error)
45
+ }
46
+ })
52
47
  })
53
- // bodyXMLToTree is synchronous and slow. Offload its processing to a
54
- // worker thread so that it does not block the event loop. This allows the
55
- // app to continue to handle other requests whilst processing an expensive
56
- // one, as well as not skewing timing metrics.
57
- const tree: ContentTree.Body = piscina
58
- ? await piscina.run(args)
59
- : bodyXMLToTreeWorker(args)
60
48
 
61
49
  const { tree: treeWithReferences, references } = updateTreeWithReferenceIds(
62
50
  tree,
@@ -1,16 +1,20 @@
1
- import { parentPort } from 'worker_threads'
2
1
  import { ContentTree } from '@financial-times/content-tree'
3
2
  import bodyXMLToTree, { TagMappings } from './bodyXMLToTree'
4
3
  import tags from './tagMappings'
4
+ import { Logger } from '@dotcom-reliability-kit/logger'
5
+ import { QueryContext } from '../..'
5
6
 
6
- jest.mock('worker_threads')
7
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
8
- const mockPostMessage = jest.mocked(parentPort)!.postMessage
7
+ const mockLogger = new Logger()
8
+ const mockLogError = jest.spyOn(mockLogger, 'error')
9
+
10
+ const mockContext = {
11
+ logger: mockLogger,
12
+ } as QueryContext
9
13
 
10
14
  describe('bodyXMLToTree', () => {
11
15
  it('converts XML to tree', () => {
12
16
  const xml = `<body><p>Hello world</p></body>`
13
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
17
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
14
18
  Object {
15
19
  "children": Array [
16
20
  Object {
@@ -216,14 +220,14 @@ describe('bodyXMLToTree', () => {
216
220
  "version": 1,
217
221
  }
218
222
  `)
219
- expect(mockPostMessage).not.toBeCalled()
223
+ expect(mockLogError).not.toBeCalled()
220
224
  })
221
225
 
222
226
  it('should handle heading and slots', () => {
223
227
  const xml =
224
228
  '<body><div class="n-content-layout"><h3></h3><div class="n-content-layout__slot"></div><div class="n-content-layout__slot"></div></body>'
225
229
 
226
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
230
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
227
231
  Object {
228
232
  "children": Array [
229
233
  Object {
@@ -251,14 +255,14 @@ describe('bodyXMLToTree', () => {
251
255
  "version": 1,
252
256
  }
253
257
  `)
254
- expect(mockPostMessage).not.toBeCalled()
258
+ expect(mockLogError).not.toBeCalled()
255
259
  })
256
260
 
257
261
  it('should log an error on unexpected child after heading', () => {
258
262
  const xml =
259
263
  '<body><div class="n-content-layout"><h3></h3><div class="n-content-layout__slot"></div><p></p></body>'
260
264
 
261
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
265
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
262
266
  Object {
263
267
  "children": Array [
264
268
  Object {
@@ -286,19 +290,12 @@ describe('bodyXMLToTree', () => {
286
290
  "version": 1,
287
291
  }
288
292
  `)
289
- expect(mockPostMessage).toBeCalled()
290
- expect(mockPostMessage.mock.lastCall).toMatchInlineSnapshot(`
293
+ expect(mockLogError).toBeCalled()
294
+ expect(mockLogError.mock.lastCall).toMatchInlineSnapshot(`
291
295
  Array [
292
296
  Object {
293
- "actual": Array [
294
- "layout-slot",
295
- "paragraph",
296
- ],
297
- "code": "BODY_XML_UNEXPECTED_STRUCTURE",
298
- "error": [Error],
297
+ "error": [OperationalError: Unexpected children types for layout],
299
298
  "event": "RECOVERABLE_ERROR",
300
- "expected": "layout-slot",
301
- "message": "Unexpected children types for layout",
302
299
  },
303
300
  ]
304
301
  `)
@@ -308,7 +305,7 @@ describe('bodyXMLToTree', () => {
308
305
  const xml =
309
306
  '<body><div class="n-content-layout"><div class="n-content-layout__slot"></div><p></p></body>'
310
307
 
311
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
308
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
312
309
  Object {
313
310
  "children": Array [
314
311
  Object {
@@ -331,19 +328,12 @@ describe('bodyXMLToTree', () => {
331
328
  "version": 1,
332
329
  }
333
330
  `)
334
- expect(mockPostMessage).toBeCalled()
335
- expect(mockPostMessage.mock.lastCall).toMatchInlineSnapshot(`
331
+ expect(mockLogError).toBeCalled()
332
+ expect(mockLogError.mock.lastCall).toMatchInlineSnapshot(`
336
333
  Array [
337
334
  Object {
338
- "actual": Array [
339
- "layout-slot",
340
- "paragraph",
341
- ],
342
- "code": "BODY_XML_UNEXPECTED_STRUCTURE",
343
- "error": [Error],
335
+ "error": [OperationalError: Unexpected children types for layout],
344
336
  "event": "RECOVERABLE_ERROR",
345
- "expected": "layout-slot",
346
- "message": "Unexpected children types for layout",
347
337
  },
348
338
  ]
349
339
  `)
@@ -355,7 +345,7 @@ describe('bodyXMLToTree', () => {
355
345
  const xml =
356
346
  '<table class="data-table" data-table-collapse-rownum="" data-table-layout-largescreen="auto" data-table-layout-smallscreen="auto" data-table-theme="auto"><caption>Nulla iaculis tempus augue</caption><thead><tr><th data-column-hidden="none" data-column-sortable="false" data-column-type="string">libero mollis</th><th data-column-hidden="none" data-column-sortable="false" data-column-type="string">pretium nunc</th><th data-column-hidden="none" data-column-sortable="false" data-column-type="string">euismod nunc</th></tr></thead><tbody><tr><td>Aenean </td><td>14134</td><td>dfdsfd</td></tr><tr><td>lobortis </td><td>3434</td><td>fdsf dsf </td></tr><tr><td>volutpat </td><td>234234</td><td>sd fsd</td></tr><tr><td>vitae </td><td>2423</td><td>s fsdf</td></tr><tr><td>elementumus</td><td>23423</td><td>f sdf</td></tr></tbody><tfoot><tr><td colspan="1000">Aenean sodales sapien</td></tr></tfoot></table>'
357
347
 
358
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
348
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
359
349
  Object {
360
350
  "children": Array [
361
351
  Object {
@@ -614,7 +604,7 @@ describe('bodyXMLToTree', () => {
614
604
  const xml =
615
605
  '<table class="data-table" id="U1140244733565W0C"><caption>Emerging markets outlook for 2017</caption><tbody><tr><td colspan="2"><p>Brazil</p><p>Brazilian shares were the best-performing asset globally over the 12 months to the end of January, returning 121 per cent, according to data from BofA Merrill Lynch. Brazilian stocks did very well through January, but investors including Lazard and Eastspring have scaled back their exposure after strong growth last year.</p></td></tr></tbody></table>'
616
606
 
617
- expect(bodyXMLToTree(xml, tags)).toMatchInlineSnapshot(`
607
+ expect(bodyXMLToTree(xml, tags, mockContext)).toMatchInlineSnapshot(`
618
608
  Object {
619
609
  "children": Array [
620
610
  Object {
@@ -1,8 +1,9 @@
1
- import type { ContentTree } from '@financial-times/content-tree'
2
1
  import * as cheerio from 'cheerio'
3
- import { isTag, isText } from 'domhandler'
4
2
 
3
+ import type { ContentTree } from '@financial-times/content-tree'
5
4
  import { AnyNode } from './Workarounds'
5
+ import { QueryContext } from '../..'
6
+ import { isTag, isText } from 'domhandler'
6
7
 
7
8
  function isNode(node: ContentTree.Node | undefined): node is ContentTree.Node {
8
9
  return Boolean(node && 'type' in node)
@@ -10,7 +11,8 @@ function isNode(node: ContentTree.Node | undefined): node is ContentTree.Node {
10
11
 
11
12
  type ContentTreeTransform = (
12
13
  $el: cheerio.Cheerio<any>,
13
- traverse: () => AnyNode[]
14
+ traverse: () => AnyNode[],
15
+ context?: QueryContext
14
16
  ) => AnyNode | AnyNode[]
15
17
 
16
18
  export type TagMappings = Record<string, ContentTreeTransform>
@@ -21,7 +23,8 @@ export type TagMappings = Record<string, ContentTreeTransform>
21
23
  */
22
24
  export default function bodyXMLToTree(
23
25
  xml: string,
24
- tagMappings: TagMappings
26
+ tagMappings: TagMappings,
27
+ context?: QueryContext
25
28
  ): ContentTree.Body {
26
29
  const $ = cheerio.load(xml)
27
30
 
@@ -41,8 +44,10 @@ export default function bodyXMLToTree(
41
44
 
42
45
  if (matchedSelector) {
43
46
  const contentTreeTransform = tagMappings[matchedSelector]
44
- return contentTreeTransform($(node), () =>
45
- flattenAndTraverseChildren(node.children)
47
+ return contentTreeTransform(
48
+ $(node),
49
+ () => flattenAndTraverseChildren(node.children),
50
+ context
46
51
  )
47
52
  }
48
53
 
@@ -1,6 +1,6 @@
1
- import { parentPort } from 'worker_threads'
2
-
3
- import type { AnyNode } from './Workarounds'
1
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
2
+ import { AnyNode } from './Workarounds'
3
+ import { QueryContext } from '../..'
4
4
 
5
5
  type ValuesOfTuple<Tuple extends readonly string[]> = Tuple[number]
6
6
 
@@ -19,39 +19,24 @@ export const phrasingTypes = [
19
19
  'link',
20
20
  ] as const
21
21
 
22
- export interface PredicateError {
23
- event: string
24
- code: string
25
- message: string
26
- expected: AnyNode['type'] | readonly AnyNode['type'][]
27
- actual: AnyNode['type'][]
28
- error: Error
29
- }
30
-
31
- // reliability-kit's logger creates a worker thread under the hood but we can't
32
- // access this thread directly from these worker threads. Instead let's send
33
- // all our recoverable errors to the piscina pool managing us.
34
- const postLoggerError = (error: Omit<PredicateError, 'error'>) => {
35
- // stacks will only copy over if they're in an Error object
36
- const errorWithStack = { ...error, error: new Error() }
37
- parentPort?.postMessage(errorWithStack)
38
- }
39
-
40
22
  export const findChildOftype = <NodeType extends AnyNode>(
41
23
  type: NodeType['type'],
42
24
  nodes: AnyNode[],
43
- parentType: AnyNode['type']
25
+ parentType: AnyNode['type'],
26
+ context?: QueryContext
44
27
  ): NodeType | undefined => {
45
28
  const predicate = (node: AnyNode): node is NodeType => node.type === type
46
29
  const child = nodes.find(predicate)
47
30
 
48
31
  if (!child) {
49
- postLoggerError({
32
+ context?.logger.error({
50
33
  event: 'RECOVERABLE_ERROR',
51
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
52
- message: `Didn't find expected child type in ${parentType}`,
53
- expected: type,
54
- actual: nodes.map((node) => node.type),
34
+ error: new OperationalError({
35
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
36
+ message: `Didn't find expected child type in ${parentType}`,
37
+ expected: type,
38
+ actual: nodes.map((node) => node.type),
39
+ }),
55
40
  })
56
41
  }
57
42
 
@@ -61,18 +46,21 @@ export const findChildOftype = <NodeType extends AnyNode>(
61
46
  export const everyChildIsType = <NodeType extends AnyNode>(
62
47
  type: NodeType['type'],
63
48
  nodes: AnyNode[],
64
- parentType: AnyNode['type']
49
+ parentType: AnyNode['type'],
50
+ context?: QueryContext
65
51
  ): NodeType[] => {
66
52
  const predicate = (node: AnyNode): node is NodeType => node.type === type
67
53
  const allChildrenAreType = nodes.every(predicate)
68
54
 
69
55
  if (!allChildrenAreType) {
70
- postLoggerError({
56
+ context?.logger.error({
71
57
  event: 'RECOVERABLE_ERROR',
72
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
73
- message: `Unexpected children types for ${parentType}`,
74
- expected: type,
75
- actual: nodes.map((node) => node.type),
58
+ error: new OperationalError({
59
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
60
+ message: `Unexpected children types for ${parentType}`,
61
+ expected: type,
62
+ actual: nodes.map((node) => node.type),
63
+ }),
76
64
  })
77
65
  }
78
66
 
@@ -82,19 +70,22 @@ export const everyChildIsType = <NodeType extends AnyNode>(
82
70
  export const childrenOfTypes = <Types extends readonly AnyNode['type'][]>(
83
71
  types: Types,
84
72
  nodes: AnyNode[],
85
- parentType: AnyNode['type']
73
+ parentType: AnyNode['type'],
74
+ context?: QueryContext
86
75
  ): NodeOfType<ValuesOfTuple<Types>>[] => {
87
76
  const predicate = (node: AnyNode): node is NodeOfType<ValuesOfTuple<Types>> =>
88
77
  types.includes(node.type)
89
78
  const allChildrenAreType = nodes.every(predicate)
90
79
 
91
80
  if (!allChildrenAreType) {
92
- postLoggerError({
81
+ context?.logger.error({
93
82
  event: 'RECOVERABLE_ERROR',
94
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
95
- message: `Unexpected ordered children types for ${parentType}`,
96
- expected: types,
97
- actual: nodes.map((node) => node.type),
83
+ error: new OperationalError({
84
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
85
+ message: `Unexpected ordered children types for ${parentType}`,
86
+ expected: types,
87
+ actual: nodes.map((node) => node.type),
88
+ }),
98
89
  })
99
90
  }
100
91
 
@@ -119,18 +110,21 @@ export const childrenOfOrderedTypes = <
119
110
  >(
120
111
  types: Types,
121
112
  nodes: AnyNode[],
122
- parentType: AnyNode['type']
113
+ parentType: AnyNode['type'],
114
+ context?: QueryContext
123
115
  ): NodesOfTypes<Types> => {
124
116
  if (nodesAreOrderedTypes(types, nodes)) {
125
117
  return nodes
126
118
  }
127
119
 
128
- postLoggerError({
120
+ context?.logger.error({
129
121
  event: 'RECOVERABLE_ERROR',
130
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
131
- message: `Unexpected children types for ${parentType}`,
132
- expected: types,
133
- actual: nodes.map((node) => node.type),
122
+ error: new OperationalError({
123
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
124
+ message: `Unexpected children types for ${parentType}`,
125
+ expected: types,
126
+ actual: nodes.map((node) => node.type),
127
+ }),
134
128
  })
135
129
 
136
130
  return nodes as unknown as NodesOfTypes<Types>