From 8228eb197f9307aad7ddda408b53e3a6805a2f19 Mon Sep 17 00:00:00 2001 From: Garrett Green Date: Sun, 4 May 2025 15:56:52 -0500 Subject: [PATCH] Implemented repository.exists method --- lib/client/client.ts | 6 +++ lib/repository/repository.ts | 28 +++++++++++++ spec/unit/client/client-exists.spec.ts | 41 +++++++++++++++++++ spec/unit/helpers/mock-client.ts | 1 + spec/unit/helpers/mock-redis.ts | 1 + .../unit/repository/repository-exists.spec.ts | 37 +++++++++++++++++ 6 files changed, 114 insertions(+) create mode 100644 spec/unit/client/client-exists.spec.ts create mode 100644 spec/unit/repository/repository-exists.spec.ts diff --git a/lib/client/client.ts b/lib/client/client.ts index c6cb022b..db05ab22 100644 --- a/lib/client/client.ts +++ b/lib/client/client.ts @@ -165,6 +165,12 @@ export class Client { this.#validateRedisOpen() await this.redis.expireAt(key, timestamp) } + + /** @internal */ + async exists(...keys: string[]) { + this.#validateRedisOpen() + return this.redis.exists(keys); + } /** @internal */ async get(key: string): Promise { diff --git a/lib/repository/repository.ts b/lib/repository/repository.ts index 86a124c2..4113bb30 100644 --- a/lib/repository/repository.ts +++ b/lib/repository/repository.ts @@ -293,6 +293,34 @@ export class Repository> { ); } + /** + * Returns boolean representing existence of an {@link Entity} in Redis for the given id. + * + * @param id The ID of the {@link Entity} you wish to check for existence. + */ + async exists(id: string): Promise + + /** + * Returns boolean representing existence of {@link Entity | Entities} in Redis. Returns `true` if ALL provided IDs exist otherwise `false`. + * + * @param ids The IDs of the {@link Entity | Entities} you wish to check for existence. + */ + async exists(...ids: string[]): Promise + + /** + * Returns boolean representing existence of {@link Entity | Entities} in Redis. Returns `true` if ALL provided IDs exist otherwise `false`. + * + * @param ids The IDs of the {@link Entity | Entities} you wish to check for existence. + */ + async exists(ids: string[]): Promise + + async exists(idOrIds: string | string[]): Promise { + const keys = Array.isArray(idOrIds) ? this.makeKeys([...new Set(idOrIds).values()]) : this.makeKeys([idOrIds]); + const numberOfKeysThatExist = await this.client.exists(...keys); + return numberOfKeysThatExist === keys.length; + + } + /** * Kicks off the process of building a query. Requires that RediSearch (and optionally * RedisJSON) be installed on your instance of Redis. diff --git a/spec/unit/client/client-exists.spec.ts b/spec/unit/client/client-exists.spec.ts new file mode 100644 index 00000000..8a5dd012 --- /dev/null +++ b/spec/unit/client/client-exists.spec.ts @@ -0,0 +1,41 @@ +import '../../helpers/custom-matchers' + +import { redis } from '../helpers/mock-redis' + +import { Client } from '$lib/client' +import { RedisOmError } from '$lib/error' + +describe("Client", () => { + + let client: Client + + beforeEach(() => { client = new Client() }) + + describe("#exists", () => { + describe("when called on an open client", () => { + beforeEach(async () => { + await client.open() + }) + + it("passes the command to redis", async () => { + await client.exists('foo') + expect(redis.exists).toHaveBeenCalledWith(['foo']) + }) + }) + + 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.exists('foo')) + .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) + }) + + it("errors when called on a new client", async () => + expect(async () => await client.exists('foo')) + .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) + }) +}) diff --git a/spec/unit/helpers/mock-client.ts b/spec/unit/helpers/mock-client.ts index dfae5e0e..70ba55b4 100644 --- a/spec/unit/helpers/mock-client.ts +++ b/spec/unit/helpers/mock-client.ts @@ -16,6 +16,7 @@ client.search = vi.fn() client.unlink = vi.fn() client.expire = vi.fn() client.expireAt = vi.fn() +client.exists = vi.fn() client.get = vi.fn() client.set = vi.fn() client.hgetall = vi.fn() diff --git a/spec/unit/helpers/mock-redis.ts b/spec/unit/helpers/mock-redis.ts index 2dcc6c44..ee2852a2 100644 --- a/spec/unit/helpers/mock-redis.ts +++ b/spec/unit/helpers/mock-redis.ts @@ -21,6 +21,7 @@ export const redis = { hGetAll: vi.fn(), expire: vi.fn(), expireAt: vi.fn(), + exists: vi.fn(), sendCommand: vi.fn(), unlink: vi.fn(), multi: vi.fn().mockImplementation(() => multi) diff --git a/spec/unit/repository/repository-exists.spec.ts b/spec/unit/repository/repository-exists.spec.ts new file mode 100644 index 00000000..3fc2588b --- /dev/null +++ b/spec/unit/repository/repository-exists.spec.ts @@ -0,0 +1,37 @@ +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' }); + +describe('Repository', () => { + describe('#exists', () => { + let client: Client; + let repository: Repository; + + beforeAll(() => { + client = new Client(); + }); + beforeEach(() => { + repository = new Repository(simpleSchema, client); + }); + + it('checks existence of a single entity', async () => { + await repository.exists('foo'); + expect(client.exists).toHaveBeenCalledWith('SimpleEntity:foo'); + }); + + it('checks existence of multiple entities', async () => { + await repository.exists(['foo', 'bar', 'baz']); + expect(client.exists).toHaveBeenCalledWith('SimpleEntity:foo', 'SimpleEntity:bar', 'SimpleEntity:baz'); + }); + + it('removes duplicate keys prior to checking for existence of entities', async () => { + await repository.exists(['foo', 'bar', 'baz', 'bar']); + expect(client.exists).toHaveBeenCalledWith('SimpleEntity:foo', 'SimpleEntity:bar', 'SimpleEntity:baz'); + }); + + }); +});