Skip to content

Commit 4c4543d

Browse files
Merge pull request #45 from DBB-Software/feat/PLATFORM-1640
feat: moved s3 strategy and fixed revalidating of cache
2 parents 6061354 + 448a108 commit 4c4543d

File tree

13 files changed

+6872
-4109
lines changed

13 files changed

+6872
-4109
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
},
1616
"plugins": [
1717
"@typescript-eslint",
18-
"prettier"
18+
"prettier",
19+
"jest"
1920
],
2021
"rules": {
2122
"semi": ["error", "never"],

jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
modulePathIgnorePatterns: ['<rootDir>/dist/']
6+
}

package-lock.json

Lines changed: 6402 additions & 4089 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"deployment"
2222
],
2323
"scripts": {
24-
"test": "echo \"Error: no test specified\" && exit 1",
24+
"test": "jest",
2525
"build": "tsc",
2626
"lint": "npx eslint \"./src/**/*.ts\"",
2727
"lint:fix": "eslint \"src/**/*.ts*\" --fix",
@@ -42,6 +42,7 @@
4242
"@semantic-release/git": "10.0.1",
4343
"@semantic-release/npm": "12.0.1",
4444
"@types/aws-lambda": "8.10.138",
45+
"@types/jest": "29.5.14",
4546
"@types/lodash": "4.17.13",
4647
"@types/node": "20.12.11",
4748
"@types/yargs": "17.0.32",
@@ -50,11 +51,14 @@
5051
"conventional-changelog-conventionalcommits": "8.0.0",
5152
"eslint": "8.57.0",
5253
"eslint-config-prettier": "9.1.0",
54+
"eslint-plugin-jest": "28.9.0",
5355
"eslint-plugin-prettier": "5.1.3",
5456
"husky": "9.1.4",
57+
"jest": "29.7.0",
5558
"lint-staged": "15.2.8",
5659
"prettier": "3.2.5",
5760
"semantic-release": "24.0.0",
61+
"ts-jest": "29.2.5",
5862
"typescript": "5.4.5"
5963
},
6064
"dependencies": {
@@ -66,8 +70,7 @@
6670
"@aws-sdk/client-sts": "3.590.0",
6771
"@aws-sdk/credential-providers": "3.590.0",
6872
"@aws-sdk/util-endpoints": "3.587.0",
69-
"@dbbs/next-cache-handler-core": "1.2.0",
70-
"@dbbs/next-cache-handler-s3": "1.2.0",
73+
"@dbbs/next-cache-handler-core": "1.3.0",
7174
"aws-cdk-lib": "2.144.0",
7275
"aws-sdk": "2.1635.0",
7376
"cdk-assets": "2.144.0",

src/cacheHandler/index.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { Cache } from '@dbbs/next-cache-handler-core'
2-
import { S3Cache } from '@dbbs/next-cache-handler-s3'
32
import getConfig from 'next/config'
43
import { CacheConfig } from '../types'
4+
import { S3Cache } from './strategy/s3'
55

66
const { serverRuntimeConfig } = getConfig() || {}
77
const config: CacheConfig | undefined = serverRuntimeConfig?.nextServerlessCacheConfig
88

9-
Cache.addCookies(config?.cacheCookies ?? [])
10-
Cache.addQueries(config?.cacheQueries ?? [])
11-
Cache.addNoCacheMatchers(config?.noCacheRoutes ?? [])
12-
13-
if (config?.enableDeviceSplit) {
14-
Cache.addDeviceSplit()
15-
}
16-
17-
Cache.setCacheStrategy(new S3Cache(process.env.STATIC_BUCKET_NAME!))
9+
Cache.setConfig({
10+
cacheCookies: config?.cacheCookies ?? [],
11+
cacheQueries: config?.cacheQueries ?? [],
12+
noCacheMatchers: config?.noCacheRoutes ?? [],
13+
enableDeviceSplit: config?.enableDeviceSplit,
14+
cache: new S3Cache(process.env.STATIC_BUCKET_NAME!)
15+
})
1816

1917
export default Cache
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { CacheEntry, CacheContext } from '@dbbs/next-cache-handler-core'
2+
import { S3Cache } from './s3'
3+
4+
const mockHtmlPage = '<p>My Page</p>'
5+
6+
export const mockCacheEntry = {
7+
value: {
8+
pageData: {},
9+
html: mockHtmlPage,
10+
kind: 'PAGE',
11+
postponed: undefined,
12+
headers: undefined,
13+
status: 200
14+
},
15+
lastModified: 100000
16+
} satisfies CacheEntry
17+
18+
const mockCacheContext: CacheContext = {
19+
isAppRouter: false,
20+
serverCacheDirPath: ''
21+
}
22+
23+
const mockBucketName = 'test-bucket'
24+
const cacheKey = 'test'
25+
const s3Cache = new S3Cache(mockBucketName)
26+
27+
const store = new Map()
28+
const mockGetObject = jest.fn().mockImplementation(async ({ Key }) => {
29+
const res = store.get(Key)
30+
return res
31+
? { Body: { transformToString: () => res.Body }, Metadata: res.Metadata }
32+
: { Body: undefined, Metadata: undefined }
33+
})
34+
const mockPutObject = jest
35+
.fn()
36+
.mockImplementation(async ({ Key, Body, Metadata }) => store.set(Key, { Body, Metadata }))
37+
const mockDeleteObject = jest.fn().mockImplementation(async ({ Key }) => store.delete(Key))
38+
const mockDeleteObjects = jest
39+
.fn()
40+
.mockImplementation(async ({ Delete: { Objects } }: { Delete: { Objects: { Key: string }[] } }) =>
41+
Objects.forEach(({ Key }) => store.delete(Key))
42+
)
43+
const mockGetObjectList = jest
44+
.fn()
45+
.mockImplementation(async () => ({ Contents: [...store.keys()].map((key) => ({ Key: key })) }))
46+
const mockGetObjectTagging = jest
47+
.fn()
48+
.mockImplementation(() => ({ TagSet: [{ Key: 'revalidateTag0', Value: cacheKey }] }))
49+
50+
jest.mock('@aws-sdk/client-s3', () => {
51+
return {
52+
S3: jest.fn().mockReturnValue({
53+
getObject: jest.fn((...params) => mockGetObject(...params)),
54+
putObject: jest.fn((...params) => mockPutObject(...params)),
55+
deleteObject: jest.fn((...params) => mockDeleteObject(...params)),
56+
deleteObjects: jest.fn((...params) => mockDeleteObjects(...params)),
57+
listObjectsV2: jest.fn((...params) => mockGetObjectList(...params)),
58+
getObjectTagging: jest.fn((...params) => mockGetObjectTagging(...params)),
59+
config: {}
60+
})
61+
}
62+
})
63+
64+
describe('S3Cache', () => {
65+
afterEach(() => {
66+
jest.clearAllMocks()
67+
})
68+
afterAll(() => {
69+
jest.restoreAllMocks()
70+
})
71+
72+
it('should set and read the cache for page router', async () => {
73+
await s3Cache.set(cacheKey, cacheKey, mockCacheEntry, mockCacheContext)
74+
expect(s3Cache.client.putObject).toHaveBeenCalledTimes(2)
75+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(1, {
76+
Bucket: mockBucketName,
77+
Key: `${cacheKey}/${cacheKey}.html`,
78+
Body: mockHtmlPage,
79+
ContentType: 'text/html'
80+
})
81+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(2, {
82+
Bucket: mockBucketName,
83+
Key: `${cacheKey}/${cacheKey}.json`,
84+
Body: JSON.stringify(mockCacheEntry.value.pageData),
85+
ContentType: 'application/json'
86+
})
87+
88+
const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext)
89+
expect(result).toEqual(mockCacheEntry.value.pageData)
90+
expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1)
91+
expect(s3Cache.client.getObject).toHaveBeenCalledWith({
92+
Bucket: mockBucketName,
93+
Key: `${cacheKey}/${cacheKey}.json`
94+
})
95+
})
96+
97+
it('should set and read the cache for app router', async () => {
98+
await s3Cache.set(cacheKey, cacheKey, mockCacheEntry, { ...mockCacheContext, isAppRouter: true })
99+
expect(s3Cache.client.putObject).toHaveBeenCalledTimes(2)
100+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(1, {
101+
Bucket: mockBucketName,
102+
Key: `${cacheKey}/${cacheKey}.html`,
103+
Body: mockHtmlPage,
104+
ContentType: 'text/html'
105+
})
106+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(2, {
107+
Bucket: mockBucketName,
108+
Key: `${cacheKey}/${cacheKey}.rsc`,
109+
Body: mockCacheEntry.value.pageData,
110+
ContentType: 'text/x-component'
111+
})
112+
113+
const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext)
114+
expect(result).toEqual(mockCacheEntry.value.pageData)
115+
expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1)
116+
expect(s3Cache.client.getObject).toHaveBeenCalledWith({
117+
Bucket: mockBucketName,
118+
Key: `${cacheKey}/${cacheKey}.json`
119+
})
120+
})
121+
122+
it('should delete cache value', async () => {
123+
await s3Cache.set(cacheKey, cacheKey, mockCacheEntry, mockCacheContext)
124+
expect(s3Cache.client.putObject).toHaveBeenCalledTimes(2)
125+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(1, {
126+
Bucket: mockBucketName,
127+
Key: `${cacheKey}/${cacheKey}.html`,
128+
Body: mockHtmlPage,
129+
ContentType: 'text/html'
130+
})
131+
expect(s3Cache.client.putObject).toHaveBeenNthCalledWith(2, {
132+
Bucket: mockBucketName,
133+
Key: `${cacheKey}/${cacheKey}.json`,
134+
Body: JSON.stringify(mockCacheEntry.value.pageData),
135+
ContentType: 'application/json'
136+
})
137+
138+
const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext)
139+
expect(result).toEqual(mockCacheEntry.value.pageData)
140+
expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1)
141+
expect(s3Cache.client.getObject).toHaveBeenCalledWith({
142+
Bucket: mockBucketName,
143+
Key: `${cacheKey}/${cacheKey}.json`
144+
})
145+
146+
await s3Cache.delete(cacheKey, cacheKey)
147+
const updatedResult = await s3Cache.get(cacheKey, cacheKey, mockCacheContext)
148+
expect(updatedResult).toBeNull()
149+
expect(s3Cache.client.deleteObjects).toHaveBeenCalledTimes(1)
150+
expect(s3Cache.client.deleteObjects).toHaveBeenNthCalledWith(1, {
151+
Bucket: mockBucketName,
152+
Delete: {
153+
Objects: [
154+
{ Key: `${cacheKey}/${cacheKey}.json` },
155+
{ Key: `${cacheKey}/${cacheKey}.html` },
156+
{ Key: `${cacheKey}/${cacheKey}.rsc` }
157+
]
158+
}
159+
})
160+
})
161+
162+
it('should revalidate cache by tag', async () => {
163+
const mockCacheEntryWithTags = { ...mockCacheEntry, tags: [cacheKey] }
164+
await s3Cache.set(cacheKey, cacheKey, mockCacheEntryWithTags, mockCacheContext)
165+
166+
expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toEqual(mockCacheEntryWithTags.value.pageData)
167+
168+
await s3Cache.revalidateTag(cacheKey, [])
169+
170+
expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toBeNull()
171+
})
172+
173+
it('should revalidate cache by path', async () => {
174+
await s3Cache.set(cacheKey, cacheKey, mockCacheEntry, mockCacheContext)
175+
176+
expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toEqual(mockCacheEntry.value.pageData)
177+
178+
await s3Cache.deleteAllByKeyMatch(cacheKey, '')
179+
expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toBeNull()
180+
})
181+
})

0 commit comments

Comments
 (0)