Skip to content

Commit

Permalink
Merge pull request #823 from anchan828/add-middleware
Browse files Browse the repository at this point in the history
feat: add middleware
  • Loading branch information
anchan828 authored Jul 20, 2024
2 parents 06d4ec4 + 55cb5b1 commit f381935
Show file tree
Hide file tree
Showing 11 changed files with 512 additions and 104 deletions.
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,85 @@ export class ExampleService {
}
```

## Middleware

You can add middleware that is executed just before calling the cache method. It can be used as an interceptor to process the cache key or the value to be stored, or to define dependencies on the cache to manipulate other caches under certain conditions.

```ts
@Injectable()
export class ExampleService {
constructor(private readonly cacheService: CacheService) {}

public async update(userId: number, age: number): Promise<void> {
await this.cacheService.set(`users/${userId}`, age, 10, {
/**
* You can pass information to be processed under specific conditions used in middleware.
*/
source: { userId },
});
}
}

@CacheMiddleware({
/**
* The priority of the middleware. The higher the number, the low the priority.
*/
priority: 1,
})
class TestCacheMiddleware implements ICacheMiddleware {
constructor(private readonly cacheService: CacheService) {}
/**
* If you want to set a hook for set, implement the set method.
*/
async set(context: CacheContext<"set">): Promise<void> {
/**
* Change data
*/
context.key = `version-1/${context.key}`;
context.value = { data: context.value };
context.ttl = 1000;

/**
* Get the source passed from the set method
*/
const source = context.getSource<{ userId: number }>();

/**
* Manage other caches under certain conditions
*/
if (source?.userId === 1) {
this.cacheService.delete("another-cache-key");
}
}

/**
* You can define middleware for most methods.
*/
// ttl?(context: CacheContext<"ttl">): Promise<void>;
// delete?(context: CacheContext<"delete">): Promise<void>;
// mget?(context: CacheContext<"mget">): Promise<void>;
// mset?(context: CacheContext<"mset">): Promise<void>;
// mdel?(context: CacheContext<"mdel">): Promise<void>;
// hget?(context: CacheContext<"hget">): Promise<void>;
// hset?(context: CacheContext<"hset">): Promise<void>;
// hdel?(context: CacheContext<"hdel">): Promise<void>;
// hgetall?(context: CacheContext<"hgetall">): Promise<void>;
// hkeys?(context: CacheContext<"hkeys">): Promise<void>;
}

@Module({
imports: [CacheModule.register()],
prividers: [
/**
* Register middleware
*/
TestCacheMiddleware,
ExampleService,
],
})
export class AppModule {}
```

## Using In-memory

@anchan828/nest-cache has been extended to make more Redis commands available. In line with this, the memory store also provides compatibility features. Please use [@anchan828/nest-cache-manager-memory](https://www.npmjs.com/package/@anchan828/nest-cache-manager-memory) instead of the default memory store.
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@nestjs/core": "10.3.10",
"@nestjs/platform-express": "10.3.10",
"@nestjs/testing": "10.3.10",
"@golevelup/ts-jest": "0.5.0",
"@types/jest": "29.5.12",
"@types/lru-cache": "7.10.10",
"@types/node": "20.14.11",
Expand Down Expand Up @@ -67,4 +68,4 @@
"node": "20.15.1"
},
"packageManager": "[email protected]"
}
}
2 changes: 1 addition & 1 deletion packages/cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@
"extends": "../../package.json"
},
"packageManager": "[email protected]"
}
}
4 changes: 4 additions & 0 deletions packages/cache/src/cache.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export interface CacheModuleOptionsFactory<T = any> {
| CacheModuleOptions<T>
| CacheModuleOptions<T>[];
}

export interface CacheOptions<Source = any> {
source?: Source;
}
247 changes: 247 additions & 0 deletions packages/cache/src/cache.middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { CacheManager } from "@anchan828/nest-cache-common";
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { Provider } from "@nestjs/common";
import { Test } from "@nestjs/testing";
import { CacheContext, CacheMiddleware, ICacheMiddleware } from "./cache.middleware";
import { CacheModule } from "./cache.module";
import { CacheService } from "./cache.service";
describe("CacheMiddleware", () => {
async function createTestingModule(providers: Provider[]) {
return Test.createTestingModule({
imports: [CacheModule.register({})],
providers,
})
.compile()
.then((app) => app.init());
}

describe("priority", () => {
it("should be called in order of priority", async () => {
const mock = jest.fn();

@CacheMiddleware({ priority: 2 })
class TestCacheMiddleware1 implements ICacheMiddleware {
async ttl(context: CacheContext<"ttl">): Promise<void> {
mock(context, "priority 2");
}
}

@CacheMiddleware({ priority: 1 })
class TestCacheMiddleware2 implements ICacheMiddleware {
async ttl(context: CacheContext<"ttl">): Promise<void> {
mock(context, "priority 1");
}
}

const app = await createTestingModule([TestCacheMiddleware1, TestCacheMiddleware2]);

await app.get(CacheService).ttl("test");

expect(mock.mock.calls).toEqual([
[
{
key: "test",
getSource: expect.any(Function),
},
"priority 1",
],
[
{
key: "test",
getSource: expect.any(Function),
},
"priority 2",
],
]);
});
});

it("call all types", async () => {
const fn = jest.fn();

@CacheMiddleware()
class TestCacheMiddleware implements ICacheMiddleware {
async ttl(context: CacheContext<"ttl">): Promise<void> {
fn("ttl", context);
}

async get(context: CacheContext<"get">): Promise<void> {
fn("get", context);
}

async set(context: CacheContext<"set">): Promise<void> {
fn("set", context);
}

async delete(context: CacheContext<"delete">): Promise<void> {
fn("delete", context);
}

async mget(context: CacheContext<"mget">): Promise<void> {
fn("mget", context);
}

async mset(context: CacheContext<"mset">): Promise<void> {
fn("mset", context);
}

async mdel(context: CacheContext<"mdel">): Promise<void> {
fn("mdel", context);
}

async hget(context: CacheContext<"hget">): Promise<void> {
fn("hget", context);
}

async hset(context: CacheContext<"hset">): Promise<void> {
fn("hset", context);
}

async hdel(context: CacheContext<"hdel">): Promise<void> {
fn("hdel", context);
}

async hgetall(context: CacheContext<"hgetall">): Promise<void> {
fn("hgetall", context);
}

async hkeys(context: CacheContext<"hkeys">): Promise<void> {
fn("hkeys", context);
}
}

const app = await createTestingModule([TestCacheMiddleware]);
const cache = app.get(CacheService);
await cache.ttl("test");
await cache.get("test");
await cache.set("test", "value");
await cache.delete("test");
await cache.mget(["test"]);
await cache.mset({ test: "value" });
await cache.mdel(["test"]);
await cache.hget("test", "field");
await cache.hset("test", "field", "value");
await cache.hdel("test", ["field"]);
await cache.hgetall("test");
await cache.hkeys("test");

expect(fn.mock.calls).toEqual([
["ttl", { key: "test", getSource: expect.any(Function) }],
["get", { key: "test", getSource: expect.any(Function) }],
["set", { key: "test", value: "value", getSource: expect.any(Function) }],
["delete", { key: "test", getSource: expect.any(Function) }],
["mget", { keys: ["test"], getSource: expect.any(Function) }],
["mset", { record: { test: "value" }, getSource: expect.any(Function) }],
["mdel", { keys: ["test"], getSource: expect.any(Function) }],
["hget", { key: "test", field: "field", getSource: expect.any(Function) }],
["hset", { key: "test", field: "field", value: "value", getSource: expect.any(Function) }],
["hdel", { key: "test", fields: ["field"], getSource: expect.any(Function) }],
["hgetall", { key: "test", getSource: expect.any(Function) }],
["hkeys", { key: "test", getSource: expect.any(Function) }],
]);

await app.close();
});

it("should override context", async () => {
@CacheMiddleware()
class TestCacheMiddleware implements ICacheMiddleware {
async ttl(context: CacheContext<"ttl">): Promise<void> {
context.key = "test2";
}

async get(context: CacheContext<"get">): Promise<void> {
context.key = "test2";
}

async set(context: CacheContext<"set">): Promise<void> {
context.key = "test2";
context.value = "value2";
context.ttl = 10;
}

async delete(context: CacheContext<"delete">): Promise<void> {
context.key = "test2";
}

async mget(context: CacheContext<"mget">): Promise<void> {
context.keys = ["test2"];
}

async mset(context: CacheContext<"mset">): Promise<void> {
context.record = { test2: "value2" };
context.ttl = 10;
}

async mdel(context: CacheContext<"mdel">): Promise<void> {
context.keys = ["test2"];
}

async hget(context: CacheContext<"hget">): Promise<void> {
context.key = "test2";
context.field = "field2";
}

async hset(context: CacheContext<"hset">): Promise<void> {
context.key = "test2";
context.field = "field2";
context.value = "value2";
}

async hdel(context: CacheContext<"hdel">): Promise<void> {
context.key = "test2";
context.fields = ["field2"];
}

async hgetall(context: CacheContext<"hgetall">): Promise<void> {
context.key = "test2";
}

async hkeys(context: CacheContext<"hkeys">): Promise<void> {
context.key = "test2";
}
}

const app = await createTestingModule([TestCacheMiddleware]);
const cache = app.get(CacheService);
const manager = app.get<CacheManager>(CACHE_MANAGER);
const ttlSpy = jest.spyOn(manager, "ttl");
const getSpy = jest.spyOn(manager, "get");
const setSpy = jest.spyOn(manager, "set");
const delSpy = jest.spyOn(manager, "del");
const mgetSpy = jest.spyOn(manager, "mget");
const msetSpy = jest.spyOn(manager, "mset");
const mdelSpy = jest.spyOn(manager, "mdel");
const hgetSpy = jest.spyOn(manager, "hget");
const hsetSpy = jest.spyOn(manager, "hset");
const hdelSpy = jest.spyOn(manager, "hdel");
const hgetallSpy = jest.spyOn(manager, "hgetall");
const hkeysSpy = jest.spyOn(manager, "hkeys");

await cache.ttl("test");
await cache.get("test");
await cache.set("test", "value");
await cache.delete("test");
await cache.mget(["test"]);
await cache.mset({ test: "value" });
await cache.mdel(["test"]);
await cache.hget("test", "field");
await cache.hset("test", "field", "value");
await cache.hdel("test", ["field"]);
await cache.hgetall("test");
await cache.hkeys("test");

expect(ttlSpy.mock.calls).toEqual([["test2"]]);
expect(getSpy.mock.calls).toEqual([["test2"]]);
expect(setSpy.mock.calls).toEqual([["test2", "value2", 10]]);
expect(delSpy.mock.calls).toEqual([["test2"]]);
expect(mgetSpy.mock.calls).toEqual([["test2"]]);
expect(msetSpy.mock.calls).toEqual([[[["test2", "value2"]], 10]]);
expect(mdelSpy.mock.calls).toEqual([["test2"]]);
expect(hgetSpy.mock.calls).toEqual([["test2", "field2"]]);
expect(hsetSpy.mock.calls).toEqual([["test2", "field2", "value2"]]);
expect(hdelSpy.mock.calls).toEqual([["test2", "field2"]]);
expect(hgetallSpy.mock.calls).toEqual([["test2"]]);
expect(hkeysSpy.mock.calls).toEqual([["test2"]]);
});
});
Loading

0 comments on commit f381935

Please sign in to comment.