mirror of https://github.com/boxyhq/jackson.git
234 lines
6.3 KiB
TypeScript
234 lines
6.3 KiB
TypeScript
// This is an in-memory implementation to be used with testing and prototyping only
|
|
|
|
import { DatabaseDriver, DatabaseOption, Index, Encrypted, Records, SortOrder } from '../typings';
|
|
import * as dbutils from './utils';
|
|
|
|
class Mem implements DatabaseDriver {
|
|
private options: DatabaseOption;
|
|
private store: any;
|
|
private indexes: any;
|
|
private cleanup: any;
|
|
private ttlStore: any;
|
|
private ttlCleanup: any;
|
|
private timerId: any;
|
|
|
|
constructor(options: DatabaseOption) {
|
|
this.options = options;
|
|
}
|
|
|
|
async init(): Promise<Mem> {
|
|
this.store = {}; // map of key, value
|
|
this.indexes = {}; // map of key, Set
|
|
this.cleanup = {}; // map of indexes for cleanup when store key is deleted
|
|
this.ttlStore = {}; // map of key to ttl
|
|
|
|
if (this.options.ttl) {
|
|
this.ttlCleanup = async () => {
|
|
const now = Date.now();
|
|
for (const k in this.ttlStore) {
|
|
if (this.ttlStore[k].expiresAt < now) {
|
|
await this.delete(this.ttlStore[k].namespace, this.ttlStore[k].key);
|
|
}
|
|
}
|
|
|
|
if (this.options.ttl) {
|
|
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
|
}
|
|
};
|
|
|
|
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
async get(namespace: string, key: string): Promise<any> {
|
|
const res = this.store[dbutils.key(namespace, key)];
|
|
if (res) {
|
|
return res;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
async getAll(
|
|
namespace: string,
|
|
pageOffset?: number,
|
|
pageLimit?: number,
|
|
_?: string,
|
|
sortOrder?: SortOrder
|
|
): Promise<Records> {
|
|
const returnValue: string[] = [];
|
|
|
|
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
|
|
pageOffset,
|
|
pageLimit,
|
|
maxLimit: this.options.pageLimit!,
|
|
});
|
|
|
|
let take = limit;
|
|
let count = 0;
|
|
|
|
take! += skip!;
|
|
|
|
if (namespace) {
|
|
const index = dbutils.keyFromParts(dbutils.createdAtPrefix, namespace);
|
|
|
|
if (this.indexes[index] === undefined) {
|
|
return { data: [] };
|
|
}
|
|
|
|
const val: string[] = Array.from(this.indexes[index]);
|
|
const iterator: IterableIterator<string> = sortOrder === 'ASC' ? val.values() : val.reverse().values();
|
|
|
|
for (const value of iterator) {
|
|
if (count >= take!) {
|
|
break;
|
|
}
|
|
|
|
if (count >= skip!) {
|
|
returnValue.push(this.store[dbutils.keyFromParts(namespace, value)]);
|
|
}
|
|
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return { data: returnValue || [] };
|
|
}
|
|
|
|
async getByIndex(
|
|
namespace: string,
|
|
idx: Index,
|
|
pageOffset?: number,
|
|
pageLimit?: number,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_?: string,
|
|
sortOrder?: SortOrder
|
|
): Promise<Records> {
|
|
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
|
|
pageOffset,
|
|
pageLimit,
|
|
maxLimit: this.options.pageLimit!,
|
|
});
|
|
|
|
let take = limit;
|
|
|
|
let count = 0;
|
|
|
|
take! += skip!;
|
|
const dbKeys = Array.from((await this.indexes[dbutils.keyForIndex(namespace, idx)]) || []) as string[];
|
|
const iterator: IterableIterator<string> =
|
|
sortOrder === 'ASC' ? dbKeys.values() : dbKeys.reverse().values();
|
|
const ret: string[] = [];
|
|
for (const dbKey of iterator || []) {
|
|
if (count >= take!) {
|
|
break;
|
|
}
|
|
if (count >= skip!) {
|
|
ret.push(await this.get(namespace, dbKey));
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return { data: ret };
|
|
}
|
|
|
|
async put(namespace: string, key: string, val: Encrypted, ttl = 0, ...indexes: any[]): Promise<any> {
|
|
const k = dbutils.key(namespace, key);
|
|
|
|
this.store[k] = val;
|
|
|
|
if (ttl) {
|
|
this.ttlStore[k] = {
|
|
namespace,
|
|
key,
|
|
expiresAt: Date.now() + ttl * 1000,
|
|
};
|
|
}
|
|
// no ttl support for secondary indexes
|
|
for (const idx of indexes || []) {
|
|
const idxKey = dbutils.keyForIndex(namespace, idx);
|
|
let set = this.indexes[idxKey];
|
|
if (!set) {
|
|
set = new Set();
|
|
this.indexes[idxKey] = set;
|
|
}
|
|
|
|
set.add(key);
|
|
const cleanupKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
|
|
let cleanup = this.cleanup[cleanupKey];
|
|
if (!cleanup) {
|
|
cleanup = new Set();
|
|
this.cleanup[cleanupKey] = cleanup;
|
|
}
|
|
|
|
cleanup.add(idxKey);
|
|
}
|
|
let createdAtSet = this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)];
|
|
if (!createdAtSet) {
|
|
createdAtSet = new Set();
|
|
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)] = createdAtSet;
|
|
this.store['createdAt'] = new Date().toISOString();
|
|
createdAtSet.add(key);
|
|
} else {
|
|
if (!this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].has(key)) {
|
|
createdAtSet.add(key);
|
|
this.store['createdAt'] = new Date().toISOString();
|
|
}
|
|
}
|
|
|
|
let modifiedAtSet = this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)];
|
|
if (!modifiedAtSet) {
|
|
modifiedAtSet = new Set();
|
|
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)] = modifiedAtSet;
|
|
}
|
|
modifiedAtSet.add(key);
|
|
this.store['modifiedAt'] = new Date().toISOString();
|
|
}
|
|
|
|
async delete(namespace: string, key: string): Promise<any> {
|
|
const k = dbutils.key(namespace, key);
|
|
|
|
delete this.store[k];
|
|
|
|
const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
|
|
// delete secondary indexes and then the mapping of the seconary indexes
|
|
const dbKeys = this.cleanup[idxKey];
|
|
|
|
for (const dbKey of dbKeys || []) {
|
|
this.indexes[dbKey] && this.indexes[dbKey].delete(key);
|
|
}
|
|
if (this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)]) {
|
|
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].delete(key);
|
|
}
|
|
if (this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)]) {
|
|
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)].delete(key);
|
|
}
|
|
delete this.cleanup[idxKey];
|
|
delete this.ttlStore[k];
|
|
}
|
|
|
|
async deleteMany(namespace: string, keys: string[]): Promise<void> {
|
|
if (keys.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const key of keys) {
|
|
await this.delete(namespace, key);
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
// no-op
|
|
}
|
|
}
|
|
|
|
export default {
|
|
new: async (options: DatabaseOption) => {
|
|
return await new Mem(options).init();
|
|
},
|
|
};
|