diff --git a/core/controllers/CacheController.ts b/core/controllers/CacheController.ts index 947cb7a..d26eab6 100644 --- a/core/controllers/CacheController.ts +++ b/core/controllers/CacheController.ts @@ -1,187 +1,78 @@ -//@ts-nocheck -import { Database } from "bun:sqlite"; -/** - * @class CacheController - * @description Haptas Cache Controller for storing and retrieving data from the cache based on sqlite - */ -export default class CacheController { - db: Database; - constructor() { - this.db = new Database(":memory:"); - this.db.exec("PRAGMA journal_mode = WAL;"); - } +interface CacheEntry { + value: T; + expires: number; +} +export default class CacheController { + private maxAge: number; + private cacheStore: Map>; + private maxSize: number; + private evictionPolicy: 'LRU' | 'FIFO'; + public keysQueue: string[]; - public clear(collection: string, key: string) { - try { - this.db.exec(`DELETE FROM ${collection} WHERE key='${key}'`); - return true; - } catch (error) { - return false; - } + constructor(maxAge: number = 1000 * 60 * 60 * 24, maxSize: number = 100, evictionPolicy: 'LRU' | 'FIFO' = 'LRU') { + this.maxAge = maxAge; + this.cacheStore = new Map(); + this.maxSize = maxSize; + this.evictionPolicy = evictionPolicy; + this.keysQueue = []; } - public getCache(collection: string, key: string) { - try { - // Fetch the entry with the TTL - const entry = this.db.prepare(`SELECT data FROM ${collection} WHERE key = ?`).get(key); - - if (!entry) { - // If entry does not exist, return null - return null; - } - - // Check if the entry has expired - const now = Date.now(); - //@ts-ignore - if (entry.ttl && entry.ttl < now) { - // Entry has expired, delete it - this.clear(collection, key); - return null; + + private evictIfNecessary() { + if (this.cacheStore.size >= this.maxSize) { + let keyToEvict; + if (this.evictionPolicy === 'LRU') { + keyToEvict = this.keysQueue.shift(); + } else { + keyToEvict = this.keysQueue.pop(); } - - // If entry is valid, update its TTL (if necessary) - // Assuming TTL needs to be refreshed on access, otherwise this can be omitted - if (entry.ttl) { - this.db - .prepare(`UPDATE ${collection} SET ttl = ? WHERE key = ?`) - .run(now + entry.ttl); + if (keyToEvict) { + this.cacheStore.delete(keyToEvict); } - //@ts-ignore - return { data: entry.data }; - } catch (error) { - console.log(error); - return { error: true, message: error }; } } - - public async update(collection: string, key: string, data: string) { - try { - this.db - .prepare(`UPDATE ${collection} SET data = '${data}' WHERE key='${key}'`) - .run(); - return this.db - .prepare(`SELECT data FROM ${collection} WHERE key='${key}'`) - .get(); - } catch (error) { - return false; - } + exists(key: string): boolean { + return this.cacheStore.has(key); } - public async list(collection: string, offset: number, limit: number, order: string) { - try { - return this.db - .prepare(`SELECT * FROM ${collection} LIMIT ${offset}, ${limit} ORDER BY ${order}`) - .all(); - } catch (error) { - return { error: true, message: error }; - } + set(key: string, value: T, maxAge: number = this.maxAge) { + const expires = Date.now() + maxAge; + this.evictIfNecessary(); + this.cacheStore.set(key, { value, expires }); + this.keysQueue.push(key); + return { value, expires }; } - public exists(collection: string, key: string) { - try { - this.db.prepare(`SELECT * FROM ${collection} WHERE key='${key}'`).get(); - return true; - } catch (error) { - return false; - } - } - public tableExists(collection: string) { - try { - return this.db - .query( - `SELECT name FROM sqlite_master WHERE type='table' AND name='${collection}'` - ) - .get(); - } catch (error) { - return { error: true, message: error }; - } - } - - public async updateCache(collection: string, key: string, data: string) { - try { - this.db - .prepare(`UPDATE ${collection} SET data = '${JSON.stringify(data)}' WHERE key='${key}'`) - .run(); - return this.db - .prepare(`SELECT data FROM ${collection} WHERE key='${key}'`) - .get(); - } catch (error) { - return false; - } - } - public async setCache(collection: string, key: string, data: string, ttl: number = 0) { - - try { - - if (!this.tableExists(collection)) { - this.db.prepare(`CREATE TABLE ${collection} (key TEXT, data TEXT, ttl INTEGER)`).run(); + get(key: string): T | null { + const cache = this.cacheStore.get(key); + if (cache) { + if (cache.expires > Date.now()) { + return cache.value; + } else { + console.log('Cache expired'); + this.cacheStore.delete(key); + const index = this.keysQueue.indexOf(key); + if (index > -1) { + this.keysQueue.splice(index, 1); + } } - this.db - .prepare( - `INSERT INTO ${collection} (key, data, ttl) VALUES ('${key}', '${JSON.stringify(data)}', ${ttl ? Date.now() + ttl : 0})` - ) - .run(); - return this.db - .prepare(`SELECT data FROM ${collection} WHERE key='${key}'`) - .get(); - } catch (error) { - console.log(error); - return { error: true, message: error }; - } - } - - public async deleteTable(collection: string) { - try { - this.db.prepare(`DROP TABLE ${collection}`).run(); - return true; - } catch (error) { - return false; } + return null; } - public async tables() { - try { - let t = this.db - .prepare(`SELECT * FROM sqlite_master WHERE type='table'`) - .all(); - } catch (error) { - return { error: true, message: error }; - } - } - - public async createTable(collection: string, fields: string[]) { - try { - this.db - .prepare( - `CREATE TABLE IF NOT EXISTS ${collection} (${fields.join(", ")})` - ) - .run(); - return true; - } catch (error) { - return false; - } + delete(key: string) { + this.cacheStore.delete(key); + const index = this.keysQueue.indexOf(key); + if (index > -1) { + this.keysQueue.splice(index, 1); + } } - public flatten(collection: string) { - // return an array of all the items in the collection - /** - * {items:[], totalPage: 0, totalItems: 0} - */ - if(!this.tableExists(collection)) return - let items = this.db.prepare(`SELECT * FROM ${collection}`).all() - let flattened: any = [] - items.forEach((item: any) => { - let data = JSON.parse(item.data) - flattened.push(data) - }) - - - return flattened + clear() { + this.cacheStore.clear(); + this.keysQueue = []; } - startExpirationCheck() { - setInterval(async () => { - const now = Date.now(); - await this.db.exec(`DELETE FROM cache WHERE ttl < ${now}`); - }, 60000); // Run every minute + getKeys() { + return Array.from(this.cacheStore.keys()); } -} \ No newline at end of file +} diff --git a/core/controllers/CrudManager.ts b/core/controllers/CrudManager.ts index 910474f..b1d8b10 100644 --- a/core/controllers/CrudManager.ts +++ b/core/controllers/CrudManager.ts @@ -12,50 +12,104 @@ let config = await import(process.cwd() + "/config.ts").then( (res) => res.default ); -function handleFiles(data:any){ - let files:any = [] - if(Array.isArray(data)){ - data.forEach((file:any)=>{ - if(!file.data) return false +const updateQueue = new Map(); +const lastUpdated = new Map(); + +function appendToQueue(data: any, cancelPrevious = true) { + if (cancelPrevious && updateQueue.has(data.id)) { + // If cancelPrevious is true, replace existing updates for the same ID + updateQueue.set(data.id, [data]); + } else { + if (updateQueue.has(data.id)) { + updateQueue.get(data.id)?.push(data); + } else { + console.log("Queue appended"); + updateQueue.set(data.id, [data]); + } + } + // Update the last updated timestamp + lastUpdated.set(data.id, Date.now()); +} + +function removeFromQueue(data: any) { + if (updateQueue.has(data.id)) { + const queue = updateQueue.get(data.id); + if (queue) { + const index = queue.findIndex((d: any) => d.key === data.key); + if (index > -1) { + queue.splice(index, 1); + } + } + } +} + +let count = 0; + +async function rollQueue(id: string, pb: Pocketbase) { + if (updateQueue.has(id)) { + const queue = updateQueue.get(id); + if (queue) { + const lastTime = lastUpdated.get(id) || 0; + if (Date.now() - lastTime < 1000) { + console.log(`Skipping rollQueue for ${id} due to recent update`); + return; + } + + count++; + for (const d of queue) { + await pb.admins.client.collection(d.collection).update(d.id, d.data); + console.log(`Queue rolled ${count} times`); + } + updateQueue.delete(id); + } + } +} + +function handleFiles(data: any) { + let files: any = []; + if (Array.isArray(data)) { + data.forEach((file: any) => { + if (!file.data) return false; const array = new Uint8Array(file.data); const blob = new Blob([array]); - let name = Math.random().toString(36).substring(7) + Date.now().toString(36) - let f = new File([blob], file.name || name, { - type: file.type || 'image/png' + let name = + Math.random().toString(36).substring(7) + Date.now().toString(36); + let f = new File([blob], file.name || name, { + type: file.type || "image/png", }); - - files.push(f) - - }) - return files - }else{ + + files.push(f); + }); + return files; + } else { const array = new Uint8Array(data.data); const blob = new Blob([array]); - let name = Math.random().toString(36).substring(7) + Date.now().toString(36) - let f = new File([blob],data.name || name, { - type: data.type || 'image/png' + let name = + Math.random().toString(36).substring(7) + Date.now().toString(36); + let f = new File([blob], data.name || name, { + type: data.type || "image/png", }); - return f + return f; } } function joinExpand(expand: Array) { - return expand.map((e, index) => { - if (index === expand.length - 1) { - return e; - } - return e + ","; - }).join(""); + return expand + .map((e, index) => { + if (index === expand.length - 1) { + return e; + } + return e + ","; + }) + .join(""); } function handle(item: any, returnable: Array) { let newRecord = {}; function recursiveObject(item: any) { switch (true) { - case item.emailVisibility === false && - item.email && - item.email !== null: + case item.emailVisibility === false && item.email && item.email !== null: delete item.email; break; case item.expand && item.expand !== null: @@ -79,10 +133,14 @@ function handle(item: any, returnable: Array) { }); } - if(item.emailVisibility === false && item.email && item.email !== null){ - delete item.email - }else if(item.emailVisibility === true && item.email && item.email !== null){ - newRecord['email'] = item.email + if (item.emailVisibility === false && item.email && item.email !== null) { + delete item.email; + } else if ( + item.emailVisibility === true && + item.email && + item.email !== null + ) { + newRecord["email"] = item.email; } Object.keys(item).forEach((key) => { @@ -93,7 +151,6 @@ function handle(item: any, returnable: Array) { }); return newRecord; - } function cannotUpdate(data: any, isSameUser: boolean) { @@ -108,46 +165,45 @@ function cannotUpdate(data: any, isSameUser: boolean) { "email", "verified", "validVerified", - "postr_plus", - "following", + "postr_plus", + "followers", "bio", "postr_subscriber_since", ]; - if(new TokenManager().decodeToken(data.token).id == data.id){ + if (new TokenManager().decodeToken(data.token).id == data.id) { for (const key in data.record) { if (cannotUpdate.includes(key)) { - return{ + return { ...new ErrorHandler(data).handle({ - code: ErrorCodes.OWNERSHIP_REQUIRED + code: ErrorCodes.OWNERSHIP_REQUIRED, }), key: data.key, - session: data.session - } + session: data.session, + }; } } - }else{ + } else { for (const key in data.record) { if (!others.includes(key)) { - return{ + return { ...new ErrorHandler(data).handle({ - code: ErrorCodes.OWNERSHIP_REQUIRED + code: ErrorCodes.OWNERSHIP_REQUIRED, }), key: data.key, - session: data.session - } + session: data.session, + }; } } } - return false + return false; } export default class CrudManager { pb: Pocketbase; Config: any; tokenManager: TokenManager; evt: EventEmitter; - subscriptions: Map; - Cache: CacheController; + subscriptions: Map; constructor(pb: any, Config: any, tokenManager: TokenManager) { this.pb = pb; this.Config = Config; @@ -155,129 +211,190 @@ export default class CrudManager { this.subscriptions = new Map(); this.evt = new EventEmitter(); this.Cache = new CacheController(); - this.worker = config.rules ? new Worker(new URL(process.cwd() + config.rules, import.meta.url)) : null + this.worker = config.rules + ? new Worker(new URL(process.cwd() + config.rules, import.meta.url)) + : null; + // Run rollQueue every 5 minutes + setInterval(() => { + if (updateQueue.size > 0) { + for (const key of updateQueue.keys()) { + rollQueue(key, this.pb); + } + console.log("Queue rolled"); + } else { + count = 0; + } + }, 300000); } - - public async create(data:{ - key: string, - expand:Array, - record:{ - [key:string]:any - }, - collection:string, - token:string, - id:string, - session:string - }){ - switch(true){ - case !data.key || !data.collection || !data.token || !data.id || !data.session || !data.record: + + public async create(data: { + key: string; + expand: Array; + record: { + [key: string]: any; + }; + collection: string; + token: string; + id: string; + session: string; + }) { + switch (true) { + case !data.key || + !data.collection || + !data.token || + !data.id || + !data.session || + !data.record: return { error: true, - message: 'key, collection, token, id, session, and record are required' - } - case !await this.tokenManager.isValid(data.token, true) || this.tokenManager.decodeToken(data.token).id !== data.id: - return {...new ErrorHandler(data).handle({code: ErrorCodes.INVALID_TOKEN}), key: data.key, session: data.session, isValid: false} + message: + "key, collection, token, id, session, and record are required", + }; + case !(await this.tokenManager.isValid(data.token, true)) || + this.tokenManager.decodeToken(data.token).id !== data.id: + return { + ...new ErrorHandler(data).handle({ code: ErrorCodes.INVALID_TOKEN }), + key: data.key, + session: data.session, + isValid: false, + }; default: - try { - let d = await this.pb.admins.client.collection(data.collection).create(data.record, { - ...(data.expand && { expand: joinExpand(data.expand) }), - }); - return {error: false, message: 'success', key: data.key, data: d, session: data.session} + try { + let d = await this.pb.admins.client + .collection(data.collection) + .create(data.record, { + ...(data.expand && { expand: joinExpand(data.expand) }), + }); + return { + error: false, + message: "success", + key: data.key, + data: d, + session: data.session, + }; } catch (error) { - console.log(error) - return {...new ErrorHandler(error).handle({code: ErrorCodes.AUTHORIZATION_FAILED}), key: data.key, session: data.session} + console.log(error); + return { + ...new ErrorHandler(error).handle({ + code: ErrorCodes.AUTHORIZATION_FAILED, + }), + key: data.key, + session: data.session, + }; } } } - public async read(data:{ - type?: string, - key?: string, - collection?: string, - token?: string, - id?: string, - expand?: Array, - session?: string, - cacheKey?: string, - isAdmin?: boolean, - }){ - switch(true){ - case !data.collection && !data.isAdmin || !data.token && !data.isAdmin || !data.id && !data.isAdmin || !data.session && !data.isAdmin : + public async read(data: { + type?: string; + key?: string; + collection?: string; + token?: string; + id?: string; + expand?: Array; + session?: string; + cacheKey?: string; + isAdmin?: boolean; + }) { + switch (true) { + case (!data.collection && !data.isAdmin) || + (!data.token && !data.isAdmin) || + (!data.id && !data.isAdmin) || + (!data.session && !data.isAdmin): return { error: true, - message: 'collection, token, id, session, and cacheKey are required', + message: "collection, token, id, session, and cacheKey are required", key: data.key, session: data.session, - isValid: false - } - case !data.token == process.env.HAPTA_ADMIN_KEY && !await this.tokenManager.isValid(data.token, true) || !data.token == process.env.HAPTA_ADMIN_KEY && this.tokenManager.decodeToken(data.token).id !== data.id: // bypass token check - - return {...new ErrorHandler(data).handle({code: ErrorCodes.INVALID_TOKEN}), key: data.key, session: data.session, isValid: false} + isValid: false, + }; + case (!data.token == process.env.HAPTA_ADMIN_KEY && + !(await this.tokenManager.isValid(data.token, true))) || + (!data.token == process.env.HAPTA_ADMIN_KEY && + this.tokenManager.decodeToken(data.token).id !== data.id): // bypass token check + return { + ...new ErrorHandler(data).handle({ code: ErrorCodes.INVALID_TOKEN }), + key: data.key, + session: data.session, + isValid: false, + }; default: - let existsinCache = this.Cache.exists(data.collection, data.cacheKey) - if(existsinCache){ - let d = this.Cache.getCache(data.collection, data.cacheKey) - if(d){ - return {error: false, message: 'success', key: data.key, data: JSON.parse(d.data), session: data.session} + let existsinCache = this.Cache.exists(data.cacheKey); + if (existsinCache) { + let d = this.Cache.get(data.cacheKey); + if (d) { + return { + error: false, + message: "success", + key: data.key, + data: d, + session: data.session, + }; } - } - let d = handle(await this.pb.admins.client.collection(data.collection).getOne(data.id, { - ...(data.expand && { expand: joinExpand(data.expand) }), - }), data.returnable) - this.Cache.setCache(data.collection, data.cacheKey, d, 3600) - setTimeout(()=>{ - this.Cache.clear(data.collection, data.cacheKey) - }, data.collection === 'users' ? 3600000 : 60000) // 1 hour for users, 1 minute for others - return {error: false, message: 'success', key: data.key, data: d, session: data.session} + } + let d = handle( + await this.pb.admins.client + .collection(data.collection) + .getOne(data.id, { + ...(data.expand && { expand: joinExpand(data.expand) }), + }), + data.returnable + ); + if(!this.Cache.exists(data.cacheKey)){ + this.Cache.set(data.cacheKey, d, new Date().getTime() + 3600) + } + return { + error: false, + message: "success", + key: data.key, + data: d, + session: data.session, + }; } - } - public async update(data: { - key: string, - data: { [key: string]: any }, - expand: Array, - collection: string, - sort: string, - filter: string, - token: string, - id: string, - session: string, - cacheKey: string - }) { + public async update(data: { + key: string; + data: { [key: string]: any }; + expand: Array; + collection: string; + sort: string; + skipDataUpdate: boolean; + immediatelyUpdate: boolean; + invalidateCache: string, + filter: string; + token: string; + id: string; + session: string; + cacheKey: string; + }) { let { key, token, session, id, cacheKey, collection } = data; - + // Check for required parameters - if (!key || !token || !session || !data.data || !collection || !id) { - console.log('Missing required parameters:', data); + if (!key || !token || !session || !data.data || !collection || !id) { + console.log("Missing required parameters:", data); return { error: true, - message: 'key, collection, token, id, session, returnable, sort, filter, limit, offset, expand, and cacheKey are required' + message: + "key, collection, token, id, session, returnable, sort, filter, limit, offset, expand, and cacheKey are required", }; } - + // Validate token - if (!await this.tokenManager.isValid(token, true)) { + if (!(await this.tokenManager.isValid(token, true))) { return { ...new ErrorHandler(data).handle({ code: ErrorCodes.INVALID_TOKEN }), key: key, session: session, - isValid: false + isValid: false, }; } - + try { // Check if the update is allowed if (cannotUpdate(data, true)) { - console.log('Cannot update data:', data); + console.log("Cannot update data:", data); return cannotUpdate(data, true); } - - let cache = this.Cache - if(cache.exists(collection, cacheKey)){ - cache.clear(collection, cacheKey) - } - if(collection === 'users' && cache.exists('users', `posts-${id}`)){ - cache.clear('users', `posts-${id}`) - } + for (let i in data.data) { if (data.data[i].isFile && data.data[i].file) { let files = handleFiles(data.data[i].file); @@ -285,85 +402,261 @@ export default class CrudManager { data.data[i] = files; } else { return { - ...new ErrorHandler(data).handle({ code: ErrorCodes.UPDATE_FAILED }), + ...new ErrorHandler(data).handle({ + code: ErrorCodes.UPDATE_FAILED, + }), key: data.key, session: data.session, message: "Invalid file type or size", - type: "update" + type: "update", }; } } - } + } + + let existsinCache = this.Cache.exists(cacheKey); + let final = null + if (existsinCache && !data.invalidateCache) { + let keys = this.Cache.getKeys(); + for (const key of keys) { + const cachedData = this.Cache.get(key); + if (cachedData && cachedData.collectionName === collection && cachedData.items) { + const itemId = cachedData.items.findIndex( + (item: { id: string }) => item.id === id + ); + if (itemId > -1 && cachedData.collectionName === collection) { + cachedData.items[itemId] = { + ...cachedData.items[itemId], + ...data.data, + } + } + final = cachedData + }else if(cachedData && cachedData.collectionName === collection && !cachedData.items && !data.invalidateCache){ + if(collection === "users"){ + let keys = this.Cache.getKeys(); + for (const key of keys) { + const cachedData = this.Cache.get(key); + if (cachedData && cachedData.collectionName === "posts" && cachedData.items) { + let dupdated = cachedData.items.map((item: any) => { + if (item.author === id) { + item.expand.author = { + ...item.expand.author, + ...data.data, + }; + } + return item; + }); + cachedData.items = dupdated; + console.log("Updating cache" + key) + this.Cache.set(key, cachedData, new Date().getTime() + 3600) + } + } + } + final = { + ...cachedData, + ...data.data + } + } + } + } - // Perform the update operation - let d = await this.pb.admins.client.collection(data.collection).update(id, data.data, { - ...(data.expand && { expand: joinExpand(data.expand) }), - }); - - d = handle(d, data.returnable); + if (data.invalidateCache) { + let keys = this.Cache.getKeys(); + let invalidateParts = data.invalidateCache.split("-"); + + for (const key of keys) { + let keyParts = key.split("-"); + let matches = true; + + // Ensure all parts of the invalidateCache string are in the key and in order + for (const part of invalidateParts) { + if (!keyParts.includes(part)) { + matches = false; + break; + } + } + + // Additionally, ensure the length and structure match + if (matches && keyParts.length >= invalidateParts.length) { + // Invalidate the cache key + this.Cache.delete(key); + console.log(`Cache key invalidated: ${key}`); + } else { + console.log(`Key: ${key} does not match invalidate criteria`); + } + } + } + + + if(final){ + console.log("Updating cache" + cacheKey) + this.Cache.set(cacheKey, final, new Date().getTime() + 3600) // + } + if(!data.skipDataUpdate && !data.immediatelyUpdate){ + appendToQueue(data); + } + else if(data.immediatelyUpdate){ + await this.pb.admins.client.collection(collection).update(id, data.data); + }else{ + console.log("Skipping data update") + } + return { error: false, - message: 'success', - key: key, - data: d, - session: session + message: "success", + key: key, + data: final, + session: session, }; - } catch (error) { + } catch (error) { + console.log(error); return { - ...new ErrorHandler(error).handle({ code: ErrorCodes.AUTHORIZATION_FAILED }), + ...new ErrorHandler(error).handle({ + code: ErrorCodes.AUTHORIZATION_FAILED, + }), key: key, - session: session + session: session, }; } } - - public async delete(data:{}){} - public async get(data:{ - key: string, - token:string, - data: { - returnable: Array, - collection: string, - sort: string, - filter: string, - limit: number, - offset: number, - id: string, - expand: Array, - cacheKey: string, - - }, - session:string - }){ - let {key, token, session} = data - let {returnable, collection, sort, filter, limit, offset, id, expand, cacheKey} = data.data - switch(true){ - case !key || !collection || !token || !id || !session || !data.data.hasOwnProperty("limit") || !data.data.hasOwnProperty('offset') : - console.log("Missing field " + (!key ? ' key' : !collection ? ' collection' : !token ? ' token' : !id ? ' id' : !session ? ' session' : !limit ? ' limit' : !offset ? ' offset' : ' returnable, sort, filter, limit, offset, expand, and cacheKey')) + + public async delete(data: {}) {} + public async get(data: { + key: string; + token: string; + data: { + returnable: Array; + collection: string; + sort: string; + filter: string; + refresh: boolean; + refreshEvery: number; + limit: number; + offset: number; + id: string; + expand: Array; + cacheKey: string; + }; + session: string; + }) { + let { key, token, session } = data; + let { + returnable, + collection, + sort, + filter, + limit, + offset, + id, + expand, + cacheKey, + } = data.data; + switch (true) { + case !key || + !collection || + !token || + !id || + !session || + !data.data.hasOwnProperty("limit") || + !data.data.hasOwnProperty("offset"): + console.log( + "Missing field " + + (!key + ? " key" + : !collection + ? " collection" + : !token + ? " token" + : !id + ? " id" + : !session + ? " session" + : !limit + ? " limit" + : !offset + ? " offset" + : " returnable, sort, filter, limit, offset, expand, and cacheKey") + ); return { error: true, - message: !key ? 'key is required' : !collection ? 'collection is required' : !token ? 'token is required' : !id ? 'id is required' : !session ? 'session is required' : !limit ? 'limit is required' : !offset ? 'offset is required' : 'returnable, sort, filter, limit, offset, expand, and cacheKey are required' + message: !key + ? "key is required" + : !collection + ? "collection is required" + : !token + ? "token is required" + : !id + ? "id is required" + : !session + ? "session is required" + : !limit + ? "limit is required" + : !offset + ? "offset is required" + : "returnable, sort, filter, limit, offset, expand, and cacheKey are required", + }; + case (!(await this.tokenManager.isValid(token, true)) && + !token == process.env.HAPTA_ADMIN_KEY) || + (this.tokenManager.decodeToken(token).id !== id && + !token == process.env.HAPTA_ADMIN_KEY): // bypass token check if token is the admin key + return { + ...new ErrorHandler(data).handle({ code: ErrorCodes.INVALID_TOKEN }), + key: key, + session: session, + isValid: false, + }; + default: + let existsinCache = this.Cache.exists(cacheKey); + if (existsinCache && !data.data.refresh) { + console.log("Cache exists") + let d = this.Cache.get(cacheKey); + if (d) { + return { + error: false, + message: "success", + key: key, + data: d, + session: session, + }; + } } - case !await this.tokenManager.isValid(token, true) && !token == process.env.HAPTA_ADMIN_KEY || this.tokenManager.decodeToken(token).id !== id&& !token == process.env.HAPTA_ADMIN_KEY: // bypass token check if token is the admin key - return {...new ErrorHandler(data).handle({code: ErrorCodes.INVALID_TOKEN}), key: key, session: session, isValid: false} - default: - let existsinCache = this.Cache.exists(collection, cacheKey) - if(existsinCache){ - let d = this.Cache.getCache(collection, cacheKey) - if(d){ - return {error: false, message: 'success', key: key, data: JSON.parse(d.data), session: session} + let d = handle( + await this.pb.admins.client + .collection(collection) + .getList(offset, limit, { + ...(sort && { sort: sort }), + ...(filter && { filter: filter }), + ...(expand && { expand: joinExpand(expand) }), + }), + returnable + ); + let keys = this.Cache.getKeys(); + for (const key of keys) { + const cachedData = this.Cache.get(key); + if (cachedData && cachedData.collectionName === collection && cachedData.items) { + let dupdated = d.items.map((item: any) => { + const itemId = cachedData.items.findIndex( + (i: { id: string }) => i.id === item.id + ); + if (itemId > -1) { + item = cachedData.items[itemId]; + } + return item; + }); + d.items = dupdated; } + } + d.collectionName = collection; + if(!this.Cache.exists(cacheKey)){ + this.Cache.set(cacheKey, d, new Date().getTime() + 3600) } - let d = handle(await this.pb.admins.client.collection(collection).getList(offset, limit, { - ...(sort && {sort: sort}), - ...(filter && {filter: filter}), - ...(expand && {expand: joinExpand(expand)}) - }), returnable) - this.Cache.setCache(collection, cacheKey, d, 3600) - setTimeout(()=>{ - this.Cache.clear(collection, cacheKey) - }, collection === 'users' ? 3600000 : 60000) // 1 hour for users, 1 minute for others - return {error: false, message: 'success', key: key, data: d, session: session} + return { + error: false, + message: "success", + key: key, + data: d, + session: session, + }; } - } + } } diff --git a/core/utils/jwt/JWT.ts b/core/utils/jwt/JWT.ts index b9a3fdb..f36971f 100644 --- a/core/utils/jwt/JWT.ts +++ b/core/utils/jwt/JWT.ts @@ -124,7 +124,7 @@ export class TokenManager { if (!signingKey) { return false; } - const currentTime = Math.floor(Date.now() / 1000); + const currentTime = Math.floor(Date.now() / 1000); if (decoded.exp && decoded.exp < currentTime) { console.log("Token has expired"); return false; diff --git a/server.ts b/server.ts index 71f667b..62f94cb 100644 --- a/server.ts +++ b/server.ts @@ -8,7 +8,7 @@ if(!fs.existsSync(process.cwd() + '/config.ts')){ console.log("⛔ Please create a config.ts file in the root directory") process.exit(1) } -globalThis.version = "1.0.3" +globalThis.version = "1.0.4" let config = await import(process.cwd() + '/config.ts').then((res) => res.default) import eventsource from 'eventsource' @@ -129,4 +129,4 @@ console.log(` Version: ${globalThis.version || "1.0.0"} Port: ${server.port} SSL: ${process.env.SSL_ENABLED == 'true' ? 'Enabled' : 'Disabled'} -`) +`) \ No newline at end of file