Skip to content

Commit

Permalink
Merge pull request #211 from garrett-green/feature/120-add-expireAt
Browse files Browse the repository at this point in the history
Add 'expireAt' To Set TTL With Date Object
  • Loading branch information
guyroyse authored Oct 27, 2023
2 parents 31d7da0 + 96a3d5c commit 9872e6d
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ export class Client {
this.#validateRedisOpen()
await this.redis.expire(key, ttl)
}

/** @internal */
async expireAt(key: string, timestamp: Date) {
this.#validateRedisOpen()
await this.redis.expireAt(key, timestamp)
}

/** @internal */
async get(key: string): Promise<string | null> {
Expand Down
33 changes: 33 additions & 0 deletions lib/repository/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,39 @@ export class Repository {
)
}

/**
* Use Date object to set the {@link Entity}'s time to live. If the {@link Entity}
* is not found, does nothing.
*
* @param id The ID of the {@link Entity} to set an expiration date for.
* @param expirationDate The time the data should expire.
*/
async expireAt(id: string, expirationDate: Date): Promise<void>;

/**
* Use Date object to set the {@link Entity | Entities} in Redis with the given
* ids. If a particular {@link Entity} is not found, does nothing.
*
* @param ids The IDs of the {@link Entity | Entities} to set an expiration date for.
* @param expirationDate The time the data should expire.
*/
async expireAt(ids: string[], expirationDate: Date): Promise<void>;

async expireAt(idOrIds: string | string[], expirationDate: Date) {
const ids = typeof idOrIds === 'string' ? [idOrIds] : idOrIds;
if (Date.now() >= expirationDate.getTime()) {
throw new Error(
`${expirationDate.toString()} is invalid. Expiration date must be in the future.`
);
}
await Promise.all(
ids.map((id) => {
const key = this.makeKey(id);
return this.client.expireAt(key, expirationDate);
})
);
}

/**
* Kicks off the process of building a query. Requires that RediSearch (and optionally
* RedisJSON) be installed on your instance of Redis.
Expand Down
47 changes: 47 additions & 0 deletions spec/unit/client/client-expireAt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import '../../helpers/custom-matchers'

import { redis } from '../helpers/mock-redis'

import { Client } from '$lib/client'
import { RedisOmError } from '$lib/error'

const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);

describe("Client", () => {

let client: Client

beforeEach(() => { client = new Client() })

describe("#expire", () => {
describe("when called on an open client", () => {
beforeEach(async () => {
await client.open()
})

it("passes the command to redis", async () => {
await client.expireAt('foo', tomorrow)
expect(redis.expireAt).toHaveBeenCalledWith('foo', tomorrow)
})
})

describe("when called on a closed client", () => {
beforeEach(async () => {
await client.open()
await client.close()
})

it("errors when called on a closed client", () =>
expect(async () => await client.expireAt('foo', tomorrow))
.rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open."))
})

it("errors when called on a new client", async () =>
expect(async () => await client.expireAt('foo', tomorrow))
.rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open."))
})
})
1 change: 1 addition & 0 deletions spec/unit/helpers/mock-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ client.dropIndex = vi.fn()
client.search = vi.fn()
client.unlink = vi.fn()
client.expire = vi.fn()
client.expireAt = vi.fn()
client.get = vi.fn()
client.set = vi.fn()
client.hgetall = vi.fn()
Expand Down
1 change: 1 addition & 0 deletions spec/unit/helpers/mock-redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const redis = {
set: vi.fn(),
hGetAll: vi.fn(),
expire: vi.fn(),
expireAt: vi.fn(),
sendCommand: vi.fn(),
unlink: vi.fn(),
multi: vi.fn().mockImplementation(() => multi)
Expand Down
66 changes: 66 additions & 0 deletions spec/unit/repository/repository-expireAt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import '../helpers/mock-client';

import { Client } from '$lib/client';
import { Repository } from '$lib/repository';
import { Schema } from '$lib/schema';

const simpleSchema = new Schema('SimpleEntity', {}, { dataStructure: 'HASH' });

const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);

describe('Repository', () => {
describe('#expireAt', () => {
let client: Client;
let repository: Repository;

beforeAll(() => {
client = new Client();
});
beforeEach(() => {
repository = new Repository(simpleSchema, client);
});

it('expires a single entity', async () => {
await repository.expireAt('foo', tomorrow);
expect(client.expireAt).toHaveBeenCalledWith(
'SimpleEntity:foo',
tomorrow
);
});

it('expires multiple entities', async () => {
await repository.expireAt(['foo', 'bar', 'baz'], tomorrow);
expect(client.expireAt).toHaveBeenNthCalledWith(
1,
'SimpleEntity:foo',
tomorrow
);
expect(client.expireAt).toHaveBeenNthCalledWith(
2,
'SimpleEntity:bar',
tomorrow
);
expect(client.expireAt).toHaveBeenNthCalledWith(
3,
'SimpleEntity:baz',
tomorrow
);
});

it('throws an error when provided invalid/past date', async () => {
let caughtError: any;
await repository.expireAt('foo', yesterday).catch((error) => {
caughtError = error;
});
expect(client.expireAt).toHaveBeenCalledTimes(0);
expect(caughtError).toBeDefined();
expect(caughtError!.message).toEqual(
`${yesterday.toString()} is invalid. Expiration date must be in the future.`
);
});
});
});

0 comments on commit 9872e6d

Please sign in to comment.