Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nextjs] Add Vercel KV support for editing cache #1530

Merged
merged 16 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
ambrauer marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { EditingDataMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';
/**
* For Vercel deployments: you can use Vercel KV, Upstash or a self hosted Redis storage for editing cache.
* For Vercel KV:
* import { VercelEditingDataCache } from '@sitecore-jss/sitecore-jss-nextjs/editing';
* const redisDataCache = new VercelEditingDataCache(
process.env.KV_REST_API_URL,
process.env.KV_REST_API_TOKEN
);
* Pass redisDataCache into EditingDataMiddleware constructor.
*/

/**
* This Next.js API route is used to handle Sitecore editor data storage and retrieval by key
Expand All @@ -20,6 +30,8 @@ export const config = {
};

// Wire up the EditingDataMiddleware handler
// For Vercel deployments: specify editingDataCache like:
// new EditingDataMiddleware({ editingDataCache: redisDataCache }).getHandler();
const handler = new EditingDataMiddleware().getHandler();

export default handler;
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@sitecore-jss/sitecore-jss": "^21.3.0-canary.1",
"@sitecore-jss/sitecore-jss-dev-tools": "^21.3.0-canary.1",
"@sitecore-jss/sitecore-jss-react": "^21.3.0-canary.1",
"@vercel/kv": "^0.2.1",
"node-html-parser": "^6.1.4",
"prop-types": "^15.8.1",
"regex-parser": "^2.2.11",
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/editing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export {
ServerlessEditingDataServiceConfig,
editingDataService,
} from './editing-data-service';
export { VercelEditingDataCache } from './vercel-editng-data-cache';
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, use } from 'chai';
import * as vercelKv from '@vercel/kv';
import sinon from 'sinon';
import { EditingData } from './editing-data';
import { VercelEditingDataCache } from './vercel-editng-data-cache';
import sinonChai from 'sinon-chai';

use(sinonChai);
const sandbox = sinon.createSandbox();

describe('vercel editing data cache', () => {
const setup = (key: string, value: Record<string, unknown> | null) => {
const kvStub: vercelKv.VercelKV = new vercelKv.VercelKV({
url: 'test',
token: 'test',
});
sandbox.stub(kvStub, 'set').resolves();
sandbox
.stub(kvStub, 'get')
.withArgs(key)
.resolves(value);
sandbox.stub(kvStub, 'expire').resolves();
sandbox.stub(vercelKv, 'createClient').returns(kvStub);
return kvStub;
};

afterEach(() => {
sandbox.restore();
});

it('should get entries from storage', async () => {
const key = 'top-secret';
const expectedResult: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
JSON.stringify(expectedResult);
setup(key, expectedResult);

const result = await new VercelEditingDataCache('test', 'tset').get(key);

expect(result as EditingData).to.deep.equal(expectedResult);
});

it('should return undefined on cache miss', async () => {
const key = 'no-such-key';
setup(key, null);
const result = await new VercelEditingDataCache('test', 'tset').get('no-such-key');
expect(result).to.deep.equal(undefined);
});

it('should put entries into storage', async () => {
const key = 'top-secret';
const entry: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
const kvStub = setup('key', {});

await new VercelEditingDataCache('test', 'tset').set(key, entry);

expect(kvStub.set).to.have.been.calledWith(key, JSON.stringify(entry));
});

it('should put entries into storage with ttl', async () => {
const key = 'top-secret';
const entry: EditingData = {
path: '/rome',
language: 'en',
layoutData: {
sitecore: {
route: null,
context: {},
},
},
dictionary: {},
};
const ttl = 148;
const kvStub = setup('key', {});

await new VercelEditingDataCache('test', 'tset', ttl).set(key, entry);

expect(kvStub.set).to.have.been.calledWith(key, JSON.stringify(entry));
expect(kvStub.expire).to.have.been.calledWith(key, ttl);
});

it('should throw if initialized without API URL and token', () => {
expect(() => new VercelEditingDataCache('', '')).to.throw();
});
});
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { VercelKV, createClient } from '@vercel/kv';
import { EditingDataCache } from './editing-data-cache';
import { EditingData } from './editing-data';
import { debug } from '@sitecore-jss/sitecore-jss';

/**
* Implementation of editing cache for Vercel deployments
* Uses Vercel KV database and client to store data
* Set TTL for cache data in constructor (default: 60 seconds)
*/
export class VercelEditingDataCache implements EditingDataCache {
protected redisCache: VercelKV;
private ttl;

/**
* @param {string} redisUrl KV endpoint URL. Usually stored in process.env.KV_REST_API_URL
* @param {string} redisToken KV endpoint tokem. Usually stored in process.env.KV_REST_API_TOKEN
* @param {string} [entryTtlSeconds] TTL of cache entries in second. Default: 60
*/
constructor(redisUrl: string | undefined, redisToken: string | undefined, entryTtlSeconds = 60) {
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
if (!redisUrl || !redisToken) {
throw Error(
'API URL or token are missing, ensure you have set the KV or Upstash storage correctly.'
);
}
this.ttl = entryTtlSeconds;
this.redisCache = createClient({
url: redisUrl,
token: redisToken,
});
}

set(key: string, editingData: EditingData): Promise<void> {
debug.editing(`Putting editing data for ${key} into redis storage...`);
return new Promise<void>((resolve, reject) => {
this.redisCache
.set(key, JSON.stringify(editingData))
.then(() => {
this.redisCache.expire(key, this.ttl).then(() => {
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
resolve();
});
})
.catch((err) => reject(err));
});
}

get(key: string): Promise<EditingData | undefined> {
debug.editing(`Getting editing data for ${key} from redis storage...`);
return new Promise<EditingData | undefined>((resolve, reject) => {
this.redisCache
.get(key)
.then((entry) => {
// We need to normalize the object we get from API then JSON-ify it, won't work otherwise
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
const result = (entry ? JSON.parse(JSON.stringify(entry)) : undefined) as EditingData;
resolve(result);
})
.catch((err) => reject(err));
});
}
}
31 changes: 30 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6105,6 +6105,7 @@ __metadata:
"@types/react-dom": ^18.0.10
"@types/sinon": ^10.0.13
"@types/sinon-chai": ^3.2.9
"@vercel/kv": ^0.2.1
"@wojtekmaj/enzyme-adapter-react-17": ^0.8.0
chai: ^4.3.7
chai-as-promised: ^7.1.1
Expand Down Expand Up @@ -7770,6 +7771,24 @@ __metadata:
languageName: node
linkType: hard

"@upstash/redis@npm:1.20.6":
version: 1.20.6
resolution: "@upstash/redis@npm:1.20.6"
dependencies:
isomorphic-fetch: ^3.0.0
checksum: e4bcf1d61b0fa3dec5b2b730831a0d6939416bdafe1af753479e53b035aa70be49ca96a6f3f52473d488ec896588a0134ee71b4c71f0caecb6454648f2e3bd29
languageName: node
linkType: hard

"@vercel/kv@npm:^0.2.1":
version: 0.2.1
resolution: "@vercel/kv@npm:0.2.1"
dependencies:
"@upstash/redis": 1.20.6
checksum: 156058160caf151ee0a64204d83d1d6fa3fe9c568de2cb9c89f2e809855fda9ec26976c99a1f1fa74043f1fb00102561909608c6ea8f2ab4399b9663561d00b4
languageName: node
linkType: hard

"@vue/compiler-core@npm:3.2.45":
version: 3.2.45
resolution: "@vue/compiler-core@npm:3.2.45"
Expand Down Expand Up @@ -17223,6 +17242,16 @@ __metadata:
languageName: node
linkType: hard

"isomorphic-fetch@npm:^3.0.0":
version: 3.0.0
resolution: "isomorphic-fetch@npm:3.0.0"
dependencies:
node-fetch: ^2.6.1
whatwg-fetch: ^3.4.1
checksum: e5ab79a56ce5af6ddd21265f59312ad9a4bc5a72cebc98b54797b42cb30441d5c5f8d17c5cd84a99e18101c8af6f90c081ecb8d12fd79e332be1778d58486d75
languageName: node
linkType: hard

"isstream@npm:~0.1.2":
version: 0.1.2
resolution: "isstream@npm:0.1.2"
Expand Down Expand Up @@ -27897,7 +27926,7 @@ __metadata:
languageName: node
linkType: hard

"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0":
"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1":
version: 3.6.2
resolution: "whatwg-fetch@npm:3.6.2"
checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed
Expand Down