From 141a523346184aa420b71393250ed0a8e93c037c Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 25 Feb 2026 22:01:31 +0530 Subject: [PATCH] feat: redesign landing page and fix filter bugs --- backend/controllers/data.controller.js | 8 +- backend/controllers/project.controller.js | 8 +- backend/utils/queryEngine.js | 65 ++++ diff_output.txt | 261 ------------- frontend/src/components/CollectionTable.jsx | 2 +- frontend/src/components/DatabaseSidebar.jsx | 4 +- frontend/src/components/RowDetailDrawer.jsx | 407 ++++++++++++-------- frontend/src/pages/Database.jsx | 253 +++++++++++- frontend/src/pages/LandingPage/index.jsx | 82 ++-- frontend/src/pages/LandingPage/style.css | 337 ++++++++++------ next.txt | 231 ----------- 11 files changed, 844 insertions(+), 814 deletions(-) create mode 100644 backend/utils/queryEngine.js delete mode 100644 diff_output.txt delete mode 100644 next.txt diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js index 4a5c5df73..50ee2ec47 100644 --- a/backend/controllers/data.controller.js +++ b/backend/controllers/data.controller.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose'); const Project = require("../models/Project"); const { getConnection } = require("../utils/connection.manager"); const { getCompiledModel } = require("../utils/injectModel"); +const QueryEngine = require("../utils/queryEngine"); // Validate MongoDB ObjectId const isValidId = (id) => mongoose.Types.ObjectId.isValid(id); @@ -82,7 +83,12 @@ module.exports.getAllData = async (req, res) => { const connection = await getConnection(project._id); const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); - const data = await Model.find({}).limit(100).lean(); + const features = new QueryEngine(Model.find(), req.query) + .filter() + .sort() + .paginate(); + + const data = await features.query.lean(); console.timeEnd("getall") res.json(data); } catch (err) { diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 759b9077c..39a825c6e 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -10,6 +10,7 @@ const { encrypt } = require('../utils/encryption'); const { URL } = require('url'); const { getConnection } = require("../utils/connection.manager"); const { getCompiledModel } = require("../utils/injectModel") +const QueryEngine = require("../utils/queryEngine"); const { storageRegistry } = require("../utils/registry"); const { deleteProjectByApiKeyCache, setProjectById, getProjectById, deleteProjectById } = require("../services/redisCaching"); const { getPublicIp } = require("../utils/network"); @@ -332,7 +333,12 @@ module.exports.getData = async (req, res) => { // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); - const data = await model.find({}).limit(50); + const features = new QueryEngine(model.find(), req.query) + .filter() + .sort() + .paginate(); + + const data = await features.query.lean(); // let data = []; // if (collectionsList.length > 0) { diff --git a/backend/utils/queryEngine.js b/backend/utils/queryEngine.js new file mode 100644 index 000000000..6a65d94bd --- /dev/null +++ b/backend/utils/queryEngine.js @@ -0,0 +1,65 @@ +class QueryEngine { + constructor(query, queryString) { + this.query = query; + this.queryString = queryString; + } + + filter() { + const queryObj = { ...this.queryString }; + const excludedFields = ['page', 'sort', 'limit', 'fields']; + excludedFields.forEach(el => delete queryObj[el]); + + const mongoQuery = {}; + for (const key in queryObj) { + if (key.endsWith('_gt')) { + const field = key.replace(/_gt$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $gt: queryObj[key] }; + } else if (key.endsWith('_gte')) { + const field = key.replace(/_gte$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $gte: queryObj[key] }; + } else if (key.endsWith('_lt')) { + const field = key.replace(/_lt$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $lt: queryObj[key] }; + } else if (key.endsWith('_lte')) { + const field = key.replace(/_lte$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $lte: queryObj[key] }; + } else { + mongoQuery[key] = queryObj[key]; + } + } + + this.query = this.query.find(mongoQuery); + + return this; + } + + sort() { + if (this.queryString.sort) { + const sortBy = this.queryString.sort.split(',').map(item => { + const [field, order] = item.split(':'); + if (order && (order === '-1' || order.toLowerCase() === 'desc')) { + return `-${field}`; + } + return field; + }).join(' '); + + this.query = this.query.sort(sortBy); + } else { + this.query = this.query.sort('-createdAt'); + } + + return this; + } + + paginate() { + const page = parseInt(this.queryString.page, 10) || 1; + const limit = parseInt(this.queryString.limit, 10) || 100; + const skip = (page - 1) * limit; + + this.query = this.query.skip(skip).limit(limit); + + return this; + } +} + +module.exports = QueryEngine; diff --git a/diff_output.txt b/diff_output.txt deleted file mode 100644 index 7f832bb06..000000000 --- a/diff_output.txt +++ /dev/null @@ -1,261 +0,0 @@ -diff --git a/backend/app.js b/backend/app.js -index 25410ca6..d0a80b08 100644 ---- a/backend/app.js -+++ b/backend/app.js -@@ -38,18 +38,24 @@ if (process.env.NODE_ENV === 'development') { - - const adminCorsOptions = { - origin: function (origin, callback) { -- console.time("cors for admin chk") -- // Allow requests with no origin (like mobile apps or curl) -- if (!origin || adminWhitelist.indexOf(origin) !== -1) { -+ const start = process.hrtime.bigint(); -+ -+ const allowed = !origin || adminWhitelist.includes(origin); -+ -+ const end = process.hrtime.bigint(); -+ console.log("Pure CORS check time:", -+ Number(end - start) / 1e6, "ms"); -+ -+ if (allowed) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS for Admin access')); - } -- console.timeEnd("cors for admin chk") - }, - credentials: true - }; - -+ - if (process.env.NODE_ENV !== 'test') { - GC.garbageCollect(); - GC.storageGarbageCollect(); -diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js -index f64f52ed..4a5c5df7 100644 ---- a/backend/controllers/data.controller.js -+++ b/backend/controllers/data.controller.js -@@ -10,6 +10,7 @@ const isValidId = (id) => mongoose.Types.ObjectId.isValid(id); - // INSERT DATA - module.exports.insertData = async (req, res) => { - try { -+ console.time("insert data") - const { collectionName } = req.params; - const project = req.project; - -@@ -61,6 +62,7 @@ module.exports.insertData = async (req, res) => { - ); - } - -+ console.timeEnd("insert data") - res.status(201).json(result); - } catch (err) { - res.status(500).json({ error: err.message }); -diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js -index d447725c..759b9077 100644 ---- a/backend/controllers/project.controller.js -+++ b/backend/controllers/project.controller.js -@@ -262,6 +262,8 @@ module.exports.createCollection = async (req, res) => { - - await deleteProjectById(projectId); - await setProjectById(projectId, project); -+ await deleteProjectByApiKeyCache(project.apiKey); -+ await deleteProjectByApiKeyCache(project.apiKey); - // Safe Response - const projectObj = project.toObject(); - delete projectObj.apiKey; -@@ -345,6 +347,7 @@ module.exports.getData = async (req, res) => { - - module.exports.insertData = async (req, res) => { - try { -+ console.time("insert data") - const { projectId, collectionName } = req.params; - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); - if (!project) return res.status(404).json({ error: "Project not found." }); -@@ -377,6 +380,7 @@ module.exports.insertData = async (req, res) => { - project.databaseUsed = (project.databaseUsed || 0) + docSize; - } - await project.save(); -+ console.timeEnd("insert data") - - res.json(result); - } catch (err) { -diff --git a/backend/middleware/verifyApiKey.js b/backend/middleware/verifyApiKey.js -index 804dd9ff..bdee2ce5 100644 ---- a/backend/middleware/verifyApiKey.js -+++ b/backend/middleware/verifyApiKey.js -@@ -7,7 +7,6 @@ const { - - module.exports = async (req, res, next) => { - try { -- console.time("api chk middleware") - const apiKey = req.header('x-api-key'); - if (!apiKey) { - return res.status(401).json({ error: 'API key not found' }); -@@ -45,21 +44,26 @@ module.exports = async (req, res, next) => { - await setProjectByApiKeyCache(hashedApi, project); - } - -+ console.time("checking if owner is verified") - if (!project.owner.isVerified) { - return res.status(401).json({ - error: 'Owner not verified', - fix: 'Verify your account on https://urbackend.bitbros.in/dashboard' - }); - } -+ console.timeEnd("checking if owner is verified") - - // Ensure defaults are present (crucial for lean objects or cached POJOs) -+ console.time("setting defaults") - if (!project.resources) project.resources = {}; - if (!project.resources.db) project.resources.db = { isExternal: false }; - if (!project.resources.storage) project.resources.storage = { isExternal: false }; -+ console.timeEnd("setting defaults") - -+ console.time("setting project and hashed api key") - req.project = project; - req.hashedApiKey = hashedApi; -- console.timeEnd("api chk middleware") -+ console.timeEnd("setting project and hashed api key") - next(); - } catch (err) { - res.status(500).json({ error: err.message }); -diff --git a/backend/services/redisCaching.js b/backend/services/redisCaching.js -index 437f1aa1..7d378cae 100644 ---- a/backend/services/redisCaching.js -+++ b/backend/services/redisCaching.js -@@ -2,14 +2,21 @@ const redis = require("../config/redis"); - - async function setProjectByApiKeyCache(api, project) { - if (redis.status !== "ready") return; -+ - try { -+ console.time("cache stringify"); - const data = JSON.stringify(project); -+ console.timeEnd("cache stringify"); -+ -+ console.time("redis set"); - await redis.set( - `project:apikey:${api}`, - data, - 'EX', - 60 * 60 * 2 - ); -+ console.timeEnd("redis set"); -+ - } catch (err) { - console.log(err); - } -@@ -17,12 +24,20 @@ async function setProjectByApiKeyCache(api, project) { - - async function getProjectByApiKeyCache(api) { - if (redis.status !== "ready") return null; -+ - try { -+ console.time("redis get"); - const data = await redis.get(`project:apikey:${api}`); -+ console.timeEnd("redis get"); -+ - if (!data) return null; - -- const parsedData = JSON.parse(data) -+ console.time("cache parse"); -+ const parsedData = JSON.parse(data); -+ console.timeEnd("cache parse"); -+ - return parsedData; -+ - } catch (err) { - console.log(err); - return null; -@@ -38,37 +53,50 @@ async function deleteProjectByApiKeyCache(api) { - } - } - -- - async function setProjectById(id, project) { - if (redis.status !== "ready") return; -+ - try { -+ console.time("cache stringify by id"); - const data = JSON.stringify(project); -+ console.timeEnd("cache stringify by id"); -+ -+ console.time("redis set by id"); - await redis.set( - `project:id:${id}`, - data, - 'EX', - 60 * 60 * 2 - ); -+ console.timeEnd("redis set by id"); -+ - } catch (err) { - console.log(err); - } - } - -- - async function getProjectById(id) { - if (redis.status !== "ready") return null; -+ - try { -+ console.time("redis get by id"); - const data = await redis.get(`project:id:${id}`); -+ console.timeEnd("redis get by id"); -+ - if (!data) return null; -- const parsedData = JSON.parse(data) -+ -+ console.time("cache parse by id"); -+ const parsedData = JSON.parse(data); -+ console.timeEnd("cache parse by id"); -+ - return parsedData; -+ - } catch (err) { - console.log(err); - return null; - } - } - -- - async function deleteProjectById(id) { - if (redis.status !== "ready") return; - try { -@@ -78,7 +106,6 @@ async function deleteProjectById(id) { - } - } - -- - module.exports = { - setProjectByApiKeyCache, - getProjectByApiKeyCache, -@@ -86,4 +113,4 @@ module.exports = { - setProjectById, - getProjectById, - deleteProjectById --}; -+}; -\ No newline at end of file -diff --git a/backend/utils/api.js b/backend/utils/api.js -index 65fdd0ca..3b700c31 100644 ---- a/backend/utils/api.js -+++ b/backend/utils/api.js -@@ -15,11 +15,10 @@ function generateApiKey() { - - //api hashing - function hashApiKey(apikey) { -- // scrypt hashing: fast but brute focre resistant -- const salt = process.env.API_KEY_SALT; -- -- // derived key of 64 bytes -- return crypto.scryptSync(apikey, salt, 64).toString('hex'); -+ return crypto -+ .createHash("sha256") -+ .update(apikey) -+ .digest("hex"); - } - - module.exports = { generateApiKey, hashApiKey } -\ No newline at end of file diff --git a/frontend/src/components/CollectionTable.jsx b/frontend/src/components/CollectionTable.jsx index 4fe0dc709..7790a2d1f 100644 --- a/frontend/src/components/CollectionTable.jsx +++ b/frontend/src/components/CollectionTable.jsx @@ -131,7 +131,7 @@ export default function CollectionTable({ data, activeCollection, onDelete, onVi { id: "actions", header: "Actions", - size: 80, + size: 120, enableResizing: false, enableHiding: false, cell: (info) => ( diff --git a/frontend/src/components/DatabaseSidebar.jsx b/frontend/src/components/DatabaseSidebar.jsx index 716b5c5c7..24352c45d 100644 --- a/frontend/src/components/DatabaseSidebar.jsx +++ b/frontend/src/components/DatabaseSidebar.jsx @@ -96,8 +96,8 @@ export default function DatabaseSidebar({ /* Sidebar Styles - Scoped */ .db-sidebar { width: 280px; - background: var(--color-bg-sidebar); - border-right: 1px solid var(--color-border); + background: transparent; + border-right: none; display: flex; flex-direction: column; z-index: 100; diff --git a/frontend/src/components/RowDetailDrawer.jsx b/frontend/src/components/RowDetailDrawer.jsx index ad79f04be..e14e90f3a 100644 --- a/frontend/src/components/RowDetailDrawer.jsx +++ b/frontend/src/components/RowDetailDrawer.jsx @@ -1,164 +1,269 @@ -import React from "react"; -import { X, Calendar, Type, Hash, ToggleLeft, FileText, Link as LinkIcon, AlertCircle } from "lucide-react"; +import React, { useEffect } from "react"; +import { X, FileText, Edit2 } from "lucide-react"; -export default function RowDetailDrawer({ isOpen, onClose, record, fields }) { - if (!isOpen || !record) return null; - - const getIconForType = (type) => { - switch (type) { - case "String": return ; - case "Number": return ; - case "Boolean": return ; - case "Date": return ; - case "Object": return ; - case "Array": return ; - default: return ; - } +export default function RowDetailDrawer({ isOpen, onClose, record, fields = [], onEdit }) { + // Handle outside click to close + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === "Escape" && isOpen) { + onClose(); + } }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); - const renderValue = (value) => { - if (value === null || value === undefined) return Empty; + if (!isOpen || !record) return null; - if (typeof value === "boolean") { - return ( - - {String(value)} - - ); - } + // Determine grid columns based on field count + const isWideForm = fields.length > 8; - if (typeof value === "object") { - return
{JSON.stringify(value, null, 2)}
; - } + return ( + <> + {/* Backdrop */} +
- return String(value); - }; + {/* Drawer Panel */} +
+ {/* Header */} +
+
+

+ + Record Details +

+

+ ID: {record._id} +

+
+ +
- return ( - <> -
-
-
-
-
- -
-
-

Record Details

-

ID: {record._id}

-
-
- -
+ {/* Body */} +
+
+ {fields.map((field) => ( +
20)) ? "span 2" : "auto" + }} + > + -
-
- {fields.map((field) => ( -
-
-
- {getIconForType(field.type)} - {field.key} -
- {field.type} -
-
- {renderValue(record[field.key])} -
-
- ))} -
+ {/* Read Only Input Field matching AddRecordDrawer */} + {field.type === "Boolean" ? ( + + ) : field.type === "Date" ? ( + + ) : field.type === "Object" || field.type === "Array" || typeof record[field.key] === "object" ? ( +