Skip to content

Commit

Permalink
TTL support
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Jan 21, 2022
1 parent 5f19642 commit 7fefb12
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 14 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $ npm install --save quickmongo
- Dot notation support
- Key-Value like interface
- Easy to use
- TTL (temporary storage) supported

# Example

Expand Down Expand Up @@ -61,6 +62,14 @@ async function doStuff() {
// remove item
await db.pull("userInfo.items", "Sword");
// -> { difficulty: 'Easy', items: ['Watch'], balance: 1000 }

// set the data and automatically delete it after 1 minute
await db.set("foo", "bar", 60); // 60 seconds = 1 minute

// fetch the temporary data after a minute
setTimeout(async () => {
await db.get("foo"); // null
}, 60_000);
}
```

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quickmongo",
"version": "5.1.0",
"version": "5.1.1",
"description": "Quick Mongodb wrapper for beginners that provides key-value based interface.",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
63 changes: 52 additions & 11 deletions src/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,19 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
*/
public async getRaw(key: string): Promise<DocType<T>> {
this.__readyCheck();
return await this.model.findOne({
const doc = await this.model.findOne({
ID: Util.getKey(key)
});

// return null if the doc has expired
// mongodb task runs every 60 seconds therefore expired docs may exist during that timeout
// this check fixes that issue and returns null if the doc has expired
// letting mongodb take care of data deletion in the background
if (!doc || (doc.expireAt && doc.expireAt.getTime() - Date.now() <= 0)) {
return null;
}

return doc;
}

/**
Expand All @@ -187,16 +197,33 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
* Set item in the database
* @param {string} key The key
* @param {any} value The value
* @param {?number} [expireAfterSeconds=-1] if specified, quickmongo deletes this data after specified seconds.
* Leave it blank or set it to `-1` to make it permanent.
* <warn>Data may still persist for a minute even after the data is supposed to be expired!</warn>
* Data may persist for a minute even after expiration due to the nature of mongodb. QuickMongo makes sure to never return expired
* documents even if it's not deleted.
* @returns {Promise<any>}
* @example // permanent
* await db.set("foo", "bar");
*
* // delete the record after 1 minute
* await db.set("foo", "bar", 60); // time in seconds (60 seconds = 1 minute)
*/
public async set(key: string, value: T | unknown): Promise<T> {
public async set(key: string, value: T | unknown, expireAfterSeconds = -1): Promise<T> {
this.__readyCheck();
if (!key.includes(".")) {
await this.model.findOneAndUpdate(
{
ID: key
},
{ $set: { data: value } },
{
$set: Util.shouldExpire(expireAfterSeconds)
? {
data: value,
expireAt: Util.createDuration(expireAfterSeconds * 1000)
}
: { data: value }
},
{ upsert: true }
);

Expand All @@ -205,10 +232,18 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
const keyMetadata = Util.getKeyMetadata(key);
const existing = await this.model.findOne({ ID: keyMetadata.master });
if (!existing) {
await this.model.create({
ID: keyMetadata.master,
data: _.set({}, keyMetadata.target, value)
});
await this.model.create(
Util.shouldExpire(expireAfterSeconds)
? {
ID: keyMetadata.master,
data: _.set({}, keyMetadata.target, value),
expireAt: Util.createDuration(expireAfterSeconds * 1000)
}
: {
ID: keyMetadata.master,
data: _.set({}, keyMetadata.target, value)
}
);

return await this.get(key);
}
Expand All @@ -219,9 +254,14 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
const newData = _.set(prev, keyMetadata.target, value);

await existing.updateOne({
$set: {
data: newData
}
$set: Util.shouldExpire(expireAfterSeconds)
? {
data: newData,
expireAt: Util.createDuration(expireAfterSeconds * 1000)
}
: {
data: newData
}
});

return await this.get(keyMetadata.master);
Expand Down Expand Up @@ -364,12 +404,13 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
this.__readyCheck();
const everything = await this.model.find();
let arb = everything
.filter((x) => !(x.expireAt && x.expireAt.getTime() - Date.now() <= 0))
.map((m) => ({
ID: m.ID,
data: this.__formatData(m)
}))
.filter((doc, idx) => {
if (options?.filter) return options.filter({ ID: doc.ID, data: doc.data }, idx);
if (options?.filter) return options.filter(doc, idx);
return true;
}) as AllData<T>[];

Expand Down
17 changes: 17 additions & 0 deletions src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ export class Util extends null {
target: child.join(".")
};
}

/**
* Utility to validate duration
* @param {number} dur The duration
* @returns {boolean}
*/
public static shouldExpire(dur: number) {
if (typeof dur !== "number") return false;
if (dur > Infinity || dur <= 0 || Number.isNaN(dur)) return false;
return true;
}

public static createDuration(dur: number) {
if (!Util.shouldExpire(dur)) return null;
const duration = new Date(Number(BigInt(Date.now()) + 1000n));
return duration;
}
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface CollectionInterface<T = unknown> {
data: T;
createdAt: Date;
updatedAt: Date;
expireAt?: Date;
}

export const docSchema = new mongoose.Schema<CollectionInterface>(
Expand All @@ -17,6 +18,11 @@ export const docSchema = new mongoose.Schema<CollectionInterface>(
data: {
type: mongoose.SchemaTypes.Mixed,
required: false
},
expireAt: {
type: mongoose.SchemaTypes.Date,
required: false,
default: null
}
},
{
Expand All @@ -26,5 +32,6 @@ export const docSchema = new mongoose.Schema<CollectionInterface>(

export default function modelSchema<T = unknown>(connection: mongoose.Connection, modelName = "JSON") {
const model = connection.model<CollectionInterface<T>>(modelName, docSchema);
model.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }).catch(() => null);
return model;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES6",
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
Expand Down
2 changes: 1 addition & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export default defineConfig({
minify: true,
skipNodeModulesBundle: true,
sourcemap: false,
target: "ES6"
target: "ES2020"
});

0 comments on commit 7fefb12

Please sign in to comment.