Skip to content

Commit 7fefb12

Browse files
committed
TTL support
1 parent 5f19642 commit 7fefb12

File tree

7 files changed

+88
-14
lines changed

7 files changed

+88
-14
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ $ npm install --save quickmongo
1919
- Dot notation support
2020
- Key-Value like interface
2121
- Easy to use
22+
- TTL (temporary storage) supported
2223

2324
# Example
2425

@@ -61,6 +62,14 @@ async function doStuff() {
6162
// remove item
6263
await db.pull("userInfo.items", "Sword");
6364
// -> { difficulty: 'Easy', items: ['Watch'], balance: 1000 }
65+
66+
// set the data and automatically delete it after 1 minute
67+
await db.set("foo", "bar", 60); // 60 seconds = 1 minute
68+
69+
// fetch the temporary data after a minute
70+
setTimeout(async () => {
71+
await db.get("foo"); // null
72+
}, 60_000);
6473
}
6574
```
6675

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quickmongo",
3-
"version": "5.1.0",
3+
"version": "5.1.1",
44
"description": "Quick Mongodb wrapper for beginners that provides key-value based interface.",
55
"main": "dist/index.js",
66
"module": "dist/index.mjs",

src/Database.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,19 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
158158
*/
159159
public async getRaw(key: string): Promise<DocType<T>> {
160160
this.__readyCheck();
161-
return await this.model.findOne({
161+
const doc = await this.model.findOne({
162162
ID: Util.getKey(key)
163163
});
164+
165+
// return null if the doc has expired
166+
// mongodb task runs every 60 seconds therefore expired docs may exist during that timeout
167+
// this check fixes that issue and returns null if the doc has expired
168+
// letting mongodb take care of data deletion in the background
169+
if (!doc || (doc.expireAt && doc.expireAt.getTime() - Date.now() <= 0)) {
170+
return null;
171+
}
172+
173+
return doc;
164174
}
165175

166176
/**
@@ -187,16 +197,33 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
187197
* Set item in the database
188198
* @param {string} key The key
189199
* @param {any} value The value
200+
* @param {?number} [expireAfterSeconds=-1] if specified, quickmongo deletes this data after specified seconds.
201+
* Leave it blank or set it to `-1` to make it permanent.
202+
* <warn>Data may still persist for a minute even after the data is supposed to be expired!</warn>
203+
* Data may persist for a minute even after expiration due to the nature of mongodb. QuickMongo makes sure to never return expired
204+
* documents even if it's not deleted.
190205
* @returns {Promise<any>}
206+
* @example // permanent
207+
* await db.set("foo", "bar");
208+
*
209+
* // delete the record after 1 minute
210+
* await db.set("foo", "bar", 60); // time in seconds (60 seconds = 1 minute)
191211
*/
192-
public async set(key: string, value: T | unknown): Promise<T> {
212+
public async set(key: string, value: T | unknown, expireAfterSeconds = -1): Promise<T> {
193213
this.__readyCheck();
194214
if (!key.includes(".")) {
195215
await this.model.findOneAndUpdate(
196216
{
197217
ID: key
198218
},
199-
{ $set: { data: value } },
219+
{
220+
$set: Util.shouldExpire(expireAfterSeconds)
221+
? {
222+
data: value,
223+
expireAt: Util.createDuration(expireAfterSeconds * 1000)
224+
}
225+
: { data: value }
226+
},
200227
{ upsert: true }
201228
);
202229

@@ -205,10 +232,18 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
205232
const keyMetadata = Util.getKeyMetadata(key);
206233
const existing = await this.model.findOne({ ID: keyMetadata.master });
207234
if (!existing) {
208-
await this.model.create({
209-
ID: keyMetadata.master,
210-
data: _.set({}, keyMetadata.target, value)
211-
});
235+
await this.model.create(
236+
Util.shouldExpire(expireAfterSeconds)
237+
? {
238+
ID: keyMetadata.master,
239+
data: _.set({}, keyMetadata.target, value),
240+
expireAt: Util.createDuration(expireAfterSeconds * 1000)
241+
}
242+
: {
243+
ID: keyMetadata.master,
244+
data: _.set({}, keyMetadata.target, value)
245+
}
246+
);
212247

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

221256
await existing.updateOne({
222-
$set: {
223-
data: newData
224-
}
257+
$set: Util.shouldExpire(expireAfterSeconds)
258+
? {
259+
data: newData,
260+
expireAt: Util.createDuration(expireAfterSeconds * 1000)
261+
}
262+
: {
263+
data: newData
264+
}
225265
});
226266

227267
return await this.get(keyMetadata.master);
@@ -364,12 +404,13 @@ export class Database<T = unknown, PAR = unknown> extends TypedEmitter<QmEvents<
364404
this.__readyCheck();
365405
const everything = await this.model.find();
366406
let arb = everything
407+
.filter((x) => !(x.expireAt && x.expireAt.getTime() - Date.now() <= 0))
367408
.map((m) => ({
368409
ID: m.ID,
369410
data: this.__formatData(m)
370411
}))
371412
.filter((doc, idx) => {
372-
if (options?.filter) return options.filter({ ID: doc.ID, data: doc.data }, idx);
413+
if (options?.filter) return options.filter(doc, idx);
373414
return true;
374415
}) as AllData<T>[];
375416

src/Util.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ export class Util extends null {
6161
target: child.join(".")
6262
};
6363
}
64+
65+
/**
66+
* Utility to validate duration
67+
* @param {number} dur The duration
68+
* @returns {boolean}
69+
*/
70+
public static shouldExpire(dur: number) {
71+
if (typeof dur !== "number") return false;
72+
if (dur > Infinity || dur <= 0 || Number.isNaN(dur)) return false;
73+
return true;
74+
}
75+
76+
public static createDuration(dur: number) {
77+
if (!Util.shouldExpire(dur)) return null;
78+
const duration = new Date(Number(BigInt(Date.now()) + 1000n));
79+
return duration;
80+
}
6481
}
6582

6683
/**

src/collection.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface CollectionInterface<T = unknown> {
55
data: T;
66
createdAt: Date;
77
updatedAt: Date;
8+
expireAt?: Date;
89
}
910

1011
export const docSchema = new mongoose.Schema<CollectionInterface>(
@@ -17,6 +18,11 @@ export const docSchema = new mongoose.Schema<CollectionInterface>(
1718
data: {
1819
type: mongoose.SchemaTypes.Mixed,
1920
required: false
21+
},
22+
expireAt: {
23+
type: mongoose.SchemaTypes.Date,
24+
required: false,
25+
default: null
2026
}
2127
},
2228
{
@@ -26,5 +32,6 @@ export const docSchema = new mongoose.Schema<CollectionInterface>(
2632

2733
export default function modelSchema<T = unknown>(connection: mongoose.Connection, modelName = "JSON") {
2834
const model = connection.model<CollectionInterface<T>>(modelName, docSchema);
35+
model.collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }).catch(() => null);
2936
return model;
3037
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "ES6",
3+
"target": "ES2020",
44
"module": "commonjs",
55
"moduleResolution": "node",
66
"declaration": true,

tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export default defineConfig({
99
minify: true,
1010
skipNodeModulesBundle: true,
1111
sourcemap: false,
12-
target: "ES6"
12+
target: "ES2020"
1313
});

0 commit comments

Comments
 (0)