@faststore/core 4.10.1-dev.1 → 4.10.1-dev.3
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/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +150 -160
- package/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/pages/[slug]/p.tsx +7 -2
- package/src/server/cms/index.ts +59 -7
- package/src/server/cms/pdp.ts +10 -5
- package/src/server/cms/plp.ts +10 -5
- package/src/server/content/promiseCache.ts +68 -0
- package/src/server/content/service.ts +71 -51
- package/test/server/cms/index.test.ts +102 -9
- package/test/server/content/promiseCache.test.ts +150 -0
- package/test/server/content/service.test.ts +277 -11
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
import type { ContentEntry, EntryPathParams } from '@vtex/client-cp'
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
|
+
import { clientCMS, cmsPageCache } from '../../../src/server/cms'
|
|
4
5
|
import { ContentService } from '../../../src/server/content/service'
|
|
5
|
-
import
|
|
6
|
-
ContentOptions,
|
|
7
|
-
ContentParams,
|
|
6
|
+
import {
|
|
7
|
+
type ContentOptions,
|
|
8
|
+
type ContentParams,
|
|
9
|
+
ContentSourceType,
|
|
8
10
|
} from '../../../src/server/content/types'
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
+
import { contentSource } from '../../../discovery.config'
|
|
12
|
+
|
|
13
|
+
type ContentSourceConfig = { type: string }
|
|
14
|
+
type DiscoveryConfigModule = {
|
|
15
|
+
default: Record<string, unknown>
|
|
16
|
+
contentSource: ContentSourceConfig
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
vi.mock('../../../discovery.config', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<DiscoveryConfigModule>()
|
|
21
|
+
const base = actual.default ?? actual
|
|
22
|
+
const mutableContentSource = {
|
|
23
|
+
...(base.contentSource as ContentSourceConfig),
|
|
24
|
+
}
|
|
11
25
|
|
|
12
|
-
vi.mock('../../../src/server/content/utils', async (importOriginal) => {
|
|
13
|
-
const actual = await importOriginal<typeof ContentUtils>()
|
|
14
26
|
return {
|
|
15
27
|
...actual,
|
|
16
|
-
|
|
28
|
+
default: { ...base, contentSource: mutableContentSource },
|
|
29
|
+
contentSource: mutableContentSource,
|
|
17
30
|
}
|
|
18
31
|
})
|
|
19
32
|
|
|
33
|
+
const DEFAULT_CONTENT_SOURCE_TYPE = contentSource.type
|
|
34
|
+
|
|
35
|
+
const useContentPlatform = () => {
|
|
36
|
+
contentSource.type = ContentSourceType.ContentPlatform
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const useHeadlessCms = () => {
|
|
40
|
+
contentSource.type = DEFAULT_CONTENT_SOURCE_TYPE
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
type ContentServiceInternals = {
|
|
21
44
|
createContentOptions: (params: ContentParams) => ContentOptions
|
|
22
45
|
convertOptionsToParams: (options: ContentOptions) => EntryPathParams
|
|
@@ -39,7 +62,17 @@ const entryData: Record<string, Record<string, unknown>> = {
|
|
|
39
62
|
},
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
const toEntry = (id: string) => ({
|
|
65
|
+
const toEntry = (id: string): ContentEntry => ({
|
|
66
|
+
id,
|
|
67
|
+
name: id,
|
|
68
|
+
account: 'example-account',
|
|
69
|
+
storeId: 'faststore',
|
|
70
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
71
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
72
|
+
createdBy: 'editor@example.com',
|
|
73
|
+
updatedBy: 'other@example.com',
|
|
74
|
+
slug: id,
|
|
75
|
+
})
|
|
43
76
|
|
|
44
77
|
function createFakeClientCP(pages: string[][]) {
|
|
45
78
|
return {
|
|
@@ -52,6 +85,8 @@ function createFakeClientCP(pages: string[][]) {
|
|
|
52
85
|
}),
|
|
53
86
|
getEntry: vi.fn(async ({ entryId }: EntryPathParams) => ({
|
|
54
87
|
...entryData[entryId as string],
|
|
88
|
+
createdBy: 'payload@example.com',
|
|
89
|
+
updatedBy: 'payload@example.com',
|
|
55
90
|
})),
|
|
56
91
|
previewEntryById: vi.fn(async ({ entryId }: EntryPathParams) => ({
|
|
57
92
|
...entryData[entryId as string],
|
|
@@ -163,16 +198,43 @@ describe('ContentService', () => {
|
|
|
163
198
|
const FIVE_MINUTES = 5 * 60 * 1000
|
|
164
199
|
|
|
165
200
|
beforeEach(() => {
|
|
166
|
-
|
|
201
|
+
useContentPlatform()
|
|
167
202
|
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
168
203
|
})
|
|
169
204
|
|
|
170
205
|
afterEach(() => {
|
|
171
|
-
|
|
206
|
+
useHeadlessCms()
|
|
172
207
|
vi.restoreAllMocks()
|
|
173
208
|
vi.useRealTimers()
|
|
174
209
|
})
|
|
175
210
|
|
|
211
|
+
it('keeps Content Platform author emails out of public page content', async () => {
|
|
212
|
+
const { service, clientCP } = createServiceWithClient()
|
|
213
|
+
clientCP.getEntryBySlug.mockResolvedValueOnce({
|
|
214
|
+
sections: [{ name: 'Direct' }],
|
|
215
|
+
createdBy: 'editor@example.com',
|
|
216
|
+
updatedBy: 'other@example.com',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const directPage = await service.getSingleContent({
|
|
220
|
+
contentType: 'pdp',
|
|
221
|
+
slug: 'product-slug',
|
|
222
|
+
})
|
|
223
|
+
const { data } = await service.getMultipleContent({ contentType: 'pdp' })
|
|
224
|
+
const listedPage = data[0]
|
|
225
|
+
|
|
226
|
+
expect(directPage.sections).toEqual([{ name: 'Direct' }])
|
|
227
|
+
expect(directPage).not.toHaveProperty('createdBy')
|
|
228
|
+
expect(directPage).not.toHaveProperty('updatedBy')
|
|
229
|
+
expect(listedPage.id).toBe('generic')
|
|
230
|
+
expect(listedPage.name).toBe('generic')
|
|
231
|
+
expect(listedPage.sections).toEqual([{ name: 'Generic' }])
|
|
232
|
+
expect(listedPage).not.toHaveProperty('createdBy')
|
|
233
|
+
expect(listedPage).not.toHaveProperty('updatedBy')
|
|
234
|
+
expect(listedPage).not.toHaveProperty('account')
|
|
235
|
+
expect(listedPage).not.toHaveProperty('storeId')
|
|
236
|
+
})
|
|
237
|
+
|
|
176
238
|
it('shares one list and entry fetch across concurrent PLPs with different slugs', async () => {
|
|
177
239
|
const { service, clientCP } = createServiceWithClient()
|
|
178
240
|
|
|
@@ -334,4 +396,208 @@ describe('ContentService', () => {
|
|
|
334
396
|
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
335
397
|
})
|
|
336
398
|
})
|
|
399
|
+
|
|
400
|
+
describe('Content Platform single entry cache', () => {
|
|
401
|
+
const FIVE_MINUTES = 5 * 60 * 1000
|
|
402
|
+
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
useContentPlatform()
|
|
405
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
afterEach(() => {
|
|
409
|
+
useHeadlessCms()
|
|
410
|
+
vi.restoreAllMocks()
|
|
411
|
+
vi.useRealTimers()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('shares one list and entry fetch across concurrent pages', async () => {
|
|
415
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
416
|
+
|
|
417
|
+
const results = await Promise.all(
|
|
418
|
+
Array.from({ length: 20 }, () =>
|
|
419
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
|
|
424
|
+
expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
|
|
425
|
+
for (const result of results) {
|
|
426
|
+
expect(result.sections).toEqual([{ name: 'Generic' }])
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('keeps separate entries per content type and locale', async () => {
|
|
431
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
432
|
+
|
|
433
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
434
|
+
await service.getSingleContent({ contentType: 'globalHeaderSections' })
|
|
435
|
+
await service.getSingleContent({
|
|
436
|
+
contentType: 'globalSections',
|
|
437
|
+
locale: 'en-US',
|
|
438
|
+
})
|
|
439
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
440
|
+
|
|
441
|
+
expect(
|
|
442
|
+
clientCP.listEntries.mock.calls.map(([params]) => [
|
|
443
|
+
params.contentType,
|
|
444
|
+
params.locale,
|
|
445
|
+
])
|
|
446
|
+
).toEqual([
|
|
447
|
+
['globalSections', undefined],
|
|
448
|
+
['globalHeaderSections', undefined],
|
|
449
|
+
['globalSections', 'en-US'],
|
|
450
|
+
])
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('caches an empty optional content type', async () => {
|
|
454
|
+
const { service, clientCP } = createServiceWithClient([[]])
|
|
455
|
+
|
|
456
|
+
const first = await service.getSingleContent({
|
|
457
|
+
contentType: 'globalFooterSections',
|
|
458
|
+
})
|
|
459
|
+
await service.getSingleContent({ contentType: 'globalFooterSections' })
|
|
460
|
+
|
|
461
|
+
expect(first).toEqual({})
|
|
462
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
|
|
463
|
+
expect(clientCP.getEntry).not.toHaveBeenCalled()
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('does not cache an empty required content type', async () => {
|
|
467
|
+
const { service, clientCP } = createServiceWithClient([[]])
|
|
468
|
+
|
|
469
|
+
const first = await service.getSingleContent({
|
|
470
|
+
contentType: 'globalSections',
|
|
471
|
+
})
|
|
472
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
473
|
+
|
|
474
|
+
expect(first).toEqual({})
|
|
475
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('picks up a required content type published after an empty lookup', async () => {
|
|
479
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
480
|
+
clientCP.listEntries.mockResolvedValueOnce({ entries: [], scroll: null })
|
|
481
|
+
|
|
482
|
+
const before = await service.getSingleContent({
|
|
483
|
+
contentType: 'globalSections',
|
|
484
|
+
})
|
|
485
|
+
const after = await service.getSingleContent({
|
|
486
|
+
contentType: 'globalSections',
|
|
487
|
+
})
|
|
488
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
489
|
+
|
|
490
|
+
expect(before).toEqual({})
|
|
491
|
+
expect(after.sections).toEqual([{ name: 'Generic' }])
|
|
492
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('does not cache lookups by slug', async () => {
|
|
496
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
497
|
+
|
|
498
|
+
await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
|
|
499
|
+
await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
|
|
500
|
+
|
|
501
|
+
expect(clientCP.getEntryBySlug).toHaveBeenCalledTimes(2)
|
|
502
|
+
expect(clientCP.listEntries).not.toHaveBeenCalled()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('bypasses the cache for branch previews', async () => {
|
|
506
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
507
|
+
const branchPreview: ContentParams = {
|
|
508
|
+
contentType: 'globalSections',
|
|
509
|
+
previewData: {
|
|
510
|
+
contentType: 'landingPage',
|
|
511
|
+
documentId: 'some-landing-page',
|
|
512
|
+
versionId: 'branch-1',
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
517
|
+
await service.getSingleContent(branchPreview)
|
|
518
|
+
await service.getSingleContent(branchPreview)
|
|
519
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
520
|
+
|
|
521
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
|
|
522
|
+
expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
|
|
523
|
+
expect(clientCP.previewEntryById).toHaveBeenCalledTimes(2)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('does not cache a multiple-entries error', async () => {
|
|
527
|
+
const { service, clientCP } = createServiceWithClient([
|
|
528
|
+
['generic', 'collection'],
|
|
529
|
+
])
|
|
530
|
+
|
|
531
|
+
await expect(
|
|
532
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
533
|
+
).rejects.toThrow()
|
|
534
|
+
await expect(
|
|
535
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
536
|
+
).rejects.toThrow()
|
|
537
|
+
|
|
538
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('fetches again after the TTL', async () => {
|
|
542
|
+
vi.useFakeTimers()
|
|
543
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
544
|
+
|
|
545
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
546
|
+
vi.advanceTimersByTime(FIVE_MINUTES)
|
|
547
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
548
|
+
|
|
549
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
describe('Headless CMS single content cache', () => {
|
|
554
|
+
const locator = {
|
|
555
|
+
contentType: 'globalSections',
|
|
556
|
+
documentId: 'doc-1',
|
|
557
|
+
versionId: 'version-1',
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
beforeEach(() => {
|
|
561
|
+
useHeadlessCms()
|
|
562
|
+
cmsPageCache.clear()
|
|
563
|
+
vi.spyOn(clientCMS, 'getCMSPage').mockResolvedValue({
|
|
564
|
+
id: 'doc-1',
|
|
565
|
+
name: 'doc-1',
|
|
566
|
+
status: 'published',
|
|
567
|
+
type: 'globalSections',
|
|
568
|
+
releaseId: 'release-1',
|
|
569
|
+
sections: [{ name: 'Children' }],
|
|
570
|
+
} as Awaited<ReturnType<typeof clientCMS.getCMSPage>>)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
afterEach(() => {
|
|
574
|
+
useHeadlessCms()
|
|
575
|
+
vi.restoreAllMocks()
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('reuses a published document fetched by locator', async () => {
|
|
579
|
+
const service = new ContentService()
|
|
580
|
+
|
|
581
|
+
await Promise.all([
|
|
582
|
+
service.getSingleContent(locator),
|
|
583
|
+
service.getSingleContent(locator),
|
|
584
|
+
])
|
|
585
|
+
await service.getSingleContent(locator)
|
|
586
|
+
|
|
587
|
+
expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(1)
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('fetches the document on every preview request', async () => {
|
|
591
|
+
const service = new ContentService()
|
|
592
|
+
const previewParams: ContentParams = {
|
|
593
|
+
...locator,
|
|
594
|
+
previewData: locator,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await service.getSingleContent(previewParams)
|
|
598
|
+
await service.getSingleContent(previewParams)
|
|
599
|
+
|
|
600
|
+
expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(2)
|
|
601
|
+
})
|
|
602
|
+
})
|
|
337
603
|
})
|