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

feat: adds initial support for dictionary APIs #13

Merged
merged 8 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@
}
],
"require-await": "error",
"@typescript-eslint/require-await": "error",
"import/no-duplicates": "error"
},
"settings": {
Expand All @@ -132,8 +131,7 @@
"dist",
"node_modules",
"examples",
"**/*.d.ts",
"jest*.config.ts"
"**/*.d.ts"
]
},
"jest": {
Expand Down
190 changes: 190 additions & 0 deletions src/momento-redis-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import EventEmitter from 'stream';
import {
CacheClient,
CacheDelete,
CacheDictionaryFetch,
CacheDictionaryGetField,
CacheDictionaryGetFields,
CacheDictionaryRemoveFields,
CacheDictionarySetFields,
CacheGet,
CacheItemGetTtl,
CacheSet,
Expand Down Expand Up @@ -87,6 +92,40 @@ export interface MomentoIORedis {

del(...args: [...keys: RedisKey[]]): Promise<number>;

hget(key: RedisKey, field: string | Buffer): Promise<string | null>;

hmget(
...args: [key: RedisKey, ...fields: (string | Buffer)[]]
): Promise<(string | null)[]>;

hgetall(key: RedisKey): Promise<Record<string, string>>;

hset(key: RedisKey, object: object): Promise<number>;

hset(
key: RedisKey,
map: Map<string | Buffer | number, string | Buffer | number>
): Promise<number>;

hset(
...args: [key: RedisKey, ...fieldValues: (string | Buffer | number)[]]
): Promise<number>;

hmset(key: RedisKey, object: object): Promise<'OK'>;

hmset(
key: RedisKey,
map: Map<string | Buffer | number, string | Buffer | number>
): Promise<'OK'>;

hmset(
...args: [key: RedisKey, ...fieldValues: (string | Buffer | number)[]]
): Promise<'OK'>;

hdel(
...args: [key: RedisKey, ...fields: (string | Buffer)[]]
): Promise<number>;

quit(): Promise<'OK'>;
}

Expand Down Expand Up @@ -302,6 +341,157 @@ export class MomentoRedisAdapter
return null;
}

async hset(
...args: [
RedisKey,
(
| object
| Map<string | Buffer | number, string | Buffer | number>
| string
| Buffer
| number
),
...Array<string | Buffer | number>
]
): Promise<number> {
let fieldsToSet: Map<string | Uint8Array, string | Uint8Array> = new Map();
let dictionaryName = String(args[0]);
if (typeof args[1] === 'object') {
if (args[1] instanceof Map) {
for (const [key, value] of args[1]) {
fieldsToSet.set(String(key), String(value));
}
} else {
dictionaryName = String(args[0]);
fieldsToSet = new Map<string | Uint8Array, string | Uint8Array>(
Object.entries(args[1])
);
}
} else {
for (let i = 1; i < args.length; i += 2) {
fieldsToSet.set(String(args[i]), String(args[i + 1]));
}
}

const rsp = await this.momentoClient.dictionarySetFields(
this.cacheName,
dictionaryName,
fieldsToSet
);

if (rsp instanceof CacheDictionarySetFields.Success) {
return fieldsToSet.size;
} else if (rsp instanceof CacheDictionarySetFields.Error) {
this.emitError('hset', rsp.message(), rsp.errorCode());
return 0;
} else {
this.emitError('hset', 'unexpected-response ' + typeof rsp);
return 0;
}
}

async hmset(
...args: [
RedisKey,
(
| object
| Map<string | Buffer | number, string | Buffer | number>
| string
| Buffer
| number
),
...Array<string | Buffer | number>
]
): Promise<'OK'> {
await this.hset(...args);
return 'OK';
}

async hmget(
...args: [key: RedisKey, ...fields: (string | Buffer)[]]
): Promise<(string | null)[]> {
const fields: string[] = [];
for (let i = 1; i < args.length; i++) {
fields.push(String(args[i]));
}
const rsp = await this.momentoClient.dictionaryGetFields(
this.cacheName,
String(args[0]),
fields
);
if (rsp instanceof CacheDictionaryGetFields.Hit) {
return Array.from(rsp.valueMap().values());
} else if (rsp instanceof CacheDictionaryGetFields.Miss) {
return [];
} else if (rsp instanceof CacheDictionaryGetFields.Error) {
this.emitError('hmget', rsp.message(), rsp.errorCode());
return [];
} else {
this.emitError('hmget', 'unexpected-response ' + typeof rsp);
return [];
}
}

async hget(key: RedisKey, field: string | Buffer): Promise<string | null> {
const rsp = await this.momentoClient.dictionaryGetField(
this.cacheName,
String(key),
field
);
if (rsp instanceof CacheDictionaryGetField.Hit) {
return rsp.valueString();
} else if (rsp instanceof CacheDictionaryGetField.Miss) {
return null;
} else if (rsp instanceof CacheDictionaryGetField.Error) {
this.emitError('hget', rsp.message(), rsp.errorCode());
return null;
} else {
this.emitError('hget', 'unexpected-response ' + typeof rsp);
return null;
}
}

async hgetall(key: RedisKey): Promise<Record<string, string>> {
const rsp = await this.momentoClient.dictionaryFetch(
this.cacheName,
String(key)
);
if (rsp instanceof CacheDictionaryFetch.Hit) {
return rsp.valueRecord();
} else if (rsp instanceof CacheDictionaryFetch.Miss) {
return {};
} else if (rsp instanceof CacheDictionaryFetch.Error) {
this.emitError('hgetall', rsp.message(), rsp.errorCode());
return {};
} else {
this.emitError('hgetall', 'unexpected-response ' + typeof rsp);
return {};
}
}

async hdel(
...args: [key: RedisKey, ...fields: (string | Buffer)[]]
): Promise<number> {
const fields: string[] = [];
for (let i = 1; i < args.length; i++) {
fields.push(String(args[i]));
}
const rsp = await this.momentoClient.dictionaryRemoveFields(
this.cacheName,
String(args[0]),
fields
);
if (rsp instanceof CacheDictionaryRemoveFields.Success) {
return fields.length;
} else if (rsp instanceof CacheDictionaryRemoveFields.Error) {
this.emitError('hdel', rsp.message(), rsp.errorCode());
return 0;
} else {
this.emitError('hdel', 'unexpected-response ' + typeof rsp);
return 0;
}
}

async ttl(key: RedisKey): Promise<number | null> {
const rsp = await this.momentoClient.itemGetTtl(this.cacheName, key);
if (rsp instanceof CacheItemGetTtl.Hit) {
Expand Down
139 changes: 139 additions & 0 deletions test/hash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {SetupIntegrationTest} from './integration-setup';
import {v4} from 'uuid';

const {client} = SetupIntegrationTest();

describe('hash', () => {
it('should return an empty object on a missing hash', async () => {
const key = v4();
const result = await client.hgetall(key);
expect(result).toEqual({});
});

it('should accept a string field and string value pair on hset', async () => {
const key = v4();
const field = v4();
const value = v4();
const result = await client.hset(key, field, value);
expect(result).toBe(1);

const getAllResult = await client.hgetall(key);
expect(getAllResult).toEqual({[field]: value});
});

it('should support hmset for backwards compatibility', async () => {
const key = v4();
const field = v4();
const value = v4();
const result = await client.hmset(key, field, value);
expect(result).toBe('OK');

const getAllResult = await client.hgetall(key);
expect(getAllResult).toEqual({[field]: value});
});

it('should accept a number field and string value pair on hset', async () => {
const key = v4();
const field = 42;
const value = v4();
const result = await client.hset(key, field, value);
expect(result).toBe(1);

const getResult = await client.hgetall(key);
expect(getResult).toEqual({[field]: value});
});

it('should accept a string field and buffer value pair on hset', async () => {
const key = v4();
const field = v4();
const value = Buffer.from(v4());
const result = await client.hset(key, field, value);
expect(result).toBe(1);

const getResult = await client.hgetall(key);
expect(getResult).toEqual({[field]: String(value)});
});

it('should accept a number field and buffer value pair on hset', async () => {
const key = v4();
const field = 42;
const value = Buffer.from(v4());
const result = await client.hset(key, field, value);
expect(result).toBe(1);

const getResult = await client.hgetall(key);
expect(getResult).toEqual({[field]: String(value)});
});

it('should accept a Record<string, string> object on hset', async () => {
const key = v4();
const field = v4();
const value = v4();
const field2 = v4();
const value2 = v4();
const obj = {[field]: value, [field2]: value2};
const result = await client.hset(key, obj);
expect(result).toBe(2);

const getAllResult = await client.hgetall(key);
expect(getAllResult).toEqual(obj);

const getResult = await client.hget(key, field);
expect(getResult).toEqual(value);

const mgetResult = await client.hmget(key, field, field2);
expect(mgetResult).toEqual([value, value2]);
});

it('should accept a Map<string, string> instance on hset', async () => {
const key = v4();
const field = v4();
const value = v4();
const field2 = v4();
const value2 = v4();
const map = new Map<string, string>([
[field, value],
[field2, value2],
]);
const result = await client.hset(key, map);
expect(result).toBe(2);

const getResult = await client.hgetall(key);
// eslint-disable-next-line node/no-unsupported-features/es-builtins
expect(getResult).toEqual(Object.fromEntries(map));
});
it('should accept a generic object input on hset', async () => {
const key = v4();
const field = v4();
const value = v4();
const field2 = v4();
const value2 = v4();
const objectToSet = {
[field]: value,
[field2]: value2,
};
const result = await client.hset(key, objectToSet);
expect(result).toBe(2);

const getResult = await client.hgetall(key);
expect(getResult).toEqual(objectToSet);
});

it('should support deleting fields', async () => {
const key = v4();
const field = v4();
const value = v4();
const field2 = v4();
const value2 = v4();
const objectToSet = {
[field]: value,
[field2]: value2,
};
const result = await client.hset(key, objectToSet);
expect(result).toBe(2);

await client.hdel(key, field2);
const getResult = await client.hgetall(key);
expect(getResult[value2]).toBeUndefined();
});
});
Loading