feat: Created Filter feature and redesign Database UI - #29
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces a QueryEngine utility for dynamic backend query building (filtering, sorting, pagination) and integrates it into data retrieval endpoints. The frontend is updated with an enhanced RowDetailDrawer component featuring structured read-only field display and a revamped Database page with client-side filtering/sorting UI. Landing page styling is modernized with a darker theme and glow effects. Changes
Sequence DiagramsequenceDiagram
participant Client as Client (Browser)
participant DB as Database.jsx
participant API as Backend API
participant QE as QueryEngine
participant Mongo as MongoDB
Client->>DB: User applies filters/sort/pagination
activate DB
DB->>DB: Update queryParams state
DB->>API: fetchData with query string<br/>(filters, sort, page, limit)
deactivate DB
activate API
API->>QE: new QueryEngine(Model.find(), queryParams)
activate QE
QE->>QE: filter() - parse operators (_gt, _gte, etc)
QE->>QE: sort() - build sort expressions
QE->>QE: paginate() - calculate skip/limit
QE->>Mongo: Execute chained query with lean()
deactivate QE
Mongo-->>API: Return filtered/sorted/paginated results
deactivate API
API-->>DB: JSON response
activate DB
DB->>DB: Update data state
DB->>Client: Render table with fetched records
deactivate DB
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @yash-pouranik, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly upgrades the data retrieval capabilities of the backend by implementing a flexible query engine for filtering, sorting, and pagination. Concurrently, it revamps the frontend database user interface to provide a more intuitive and responsive experience for managing collections and records, alongside a visual refresh of the landing page. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/Database.jsx (1)
118-161:⚠️ Potential issue | 🟠 MajorEvery filter keystroke triggers an API call — "Apply Queries" button is ineffective.
fetchDatais auseCallbackthat depends onqueryParams(line 152). TheuseEffecton line 155 depends onfetchData. So every change toqueryParams— including every character typed in a filter value input (line 473) or every operator/field selection — recreatesfetchDataand triggers the effect, firing an API request immediately.The "Apply Queries" button (line 506) only resets page to 1, but the fetch already happened on each keystroke.
To fix this, separate the "draft" filter state from the "committed" query state. Only update
queryParamswhen "Apply Queries" is clicked:Suggested approach
+ // Draft state for the filter menu (not triggering fetches) + const [draftFilters, setDraftFilters] = useState([]); + const [draftSort, setDraftSort] = useState('-createdAt'); // In the Apply button handler: onClick={() => { setShowFilterMenu(false); - setQueryParams(p => ({ ...p, page: 1 })); + setQueryParams(p => ({ ...p, page: 1, sort: draftSort, filters: draftFilters })); }}Use
draftFilters/draftSortin the filter menu inputs, and sync them fromqueryParamswhen the menu opens.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/Database.jsx` around lines 118 - 161, fetchData is being re-created on every keystroke because it depends on queryParams, which causes the useEffect watching fetchData to fire immediately; fix by introducing separate "draft" filter/sort state for the filter UI (e.g., draftFilters, draftSort) and a distinct committed/applied query state used for fetching (e.g., appliedQueryParams or committedQueryParams). Bind filter inputs to draftFilters/draftSort and initialize/sync them from queryParams when the filter menu opens, then update queryParams (setQueryParams or setAppliedQueryParams) only when the "Apply Queries" button is clicked; change fetchData to depend on appliedQueryParams (not the draft) so typing no longer triggers API calls, and ensure the useEffect that calls fetchData depends on fetchData/appliedQueryParams accordingly.
🧹 Nitpick comments (3)
backend/utils/queryEngine.js (1)
9-10: Minor: suppress linter warning onforEachcallback return value.The
deleteexpression returns a boolean, which triggers the BiomeuseIterableCallbackReturnwarning. Use a block body to discard the return.Proposed fix
- excludedFields.forEach(el => delete queryObj[el]); + excludedFields.forEach(el => { delete queryObj[el]; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/utils/queryEngine.js` around lines 9 - 10, The forEach callback currently uses a concise arrow that returns the boolean from the delete expression (excludedFields.forEach(el => delete queryObj[el])), triggering the linter; change the callback to use a block body that discards the return value — e.g., replace the arrow with a function body that calls delete queryObj[el]; (or use a for...of loop) so the callback does not return the delete result; update the code around excludedFields and queryObj accordingly.frontend/src/pages/LandingPage/style.css (1)
44-47: Rename keyframe to kebab-case:pulse-glow.Stylelint flags
pulseGlowas violating thekeyframes-name-patternrule (kebab-case expected).Proposed fix
-@keyframes pulseGlow { +@keyframes pulse-glow { 0% { transform: translateX(-50%) scale(1); opacity: 0.8; } 100% { transform: translateX(-50%) scale(1.1); opacity: 1; } }Also update the reference on line 41:
- animation: pulseGlow 8s ease-in-out infinite alternate; + animation: pulse-glow 8s ease-in-out infinite alternate;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/LandingPage/style.css` around lines 44 - 47, Rename the `@keyframes` identifier from "pulseGlow" to kebab-case "pulse-glow" and update all usages that reference it (e.g., animation, animation-name, or shorthand animation properties that currently use pulseGlow) to "pulse-glow" so the keyframes-name-pattern rule is satisfied; ensure the `@keyframes` declaration and every CSS rule that applies the animation are updated consistently.frontend/src/components/RowDetailDrawer.jsx (1)
6-14: Only bind the Escape listener while the drawer is open.This currently registers a global keydown handler even when closed. Not incorrect, but unnecessary global work.
Suggested patch
useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (e) => { - if (e.key === "Escape" && isOpen) { + if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/RowDetailDrawer.jsx` around lines 6 - 14, The Escape key handler is being attached globally even when the drawer is closed; update the useEffect that declares handleKeyDown so it only adds the window "keydown" listener when isOpen is true and removes it when isOpen becomes false or on unmount: inside the useEffect for isOpen/onClose, guard with if (!isOpen) return (or only call addEventListener when isOpen), and ensure the cleanup always removes the same handleKeyDown reference so onClose and handleKeyDown are referenced correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/utils/queryEngine.js`:
- Around line 54-62: In paginate(), prevent unbounded requests by clamping the
parsed limit from this.queryString.limit to a safe maximum (e.g., MAX_LIMIT)
before computing skip; parse the incoming limit as an integer, ensure it's at
least 1, then set limit = Math.min(parsedLimit, MAX_LIMIT) (or a default if
missing/invalid), compute skip and apply this.query =
this.query.skip(skip).limit(limit); add a clear constant like MAX_LIMIT near
paginate() for easy adjustment and reference.
- Around line 7-31: The filter() method builds mongoQuery from this.queryString
and currently copies keys directly into mongoQuery before calling
this.query.find(mongoQuery), which permits NoSQL injection; fix by
sanitizing/whitelisting keys: either derive an allowedFields list from the
model/schema used by this class (e.g., check against this.model.schema.paths or
an explicit allowedFields array) and only copy keys that exist there, or at
minimum drop any key that begins with '$' or contains prototype pollution names
like '__proto__' or 'constructor' before adding to mongoQuery; ensure
special-suffix handlers (keys ending with _gt/_gte/_lt/_lte) also respect the
same whitelist/sanitization before setting field operators.
- Around line 12-29: The loop that builds mongoQuery in
backend/utils/queryEngine.js uses req.query string values directly (e.g., in the
_gt/_gte/_lt/_lte branches), causing lexicographic comparisons; update the logic
in that loop (the for...in over queryObj that constructs mongoQuery) so that
when handling comparison suffixes (_gt, _gte, _lt, _lte) you attempt to coerce
the string value to a Number (e.g., parseFloat and check !isNaN) and use the
numeric value in the $gt/$gte/$lt/$lte operators when valid; otherwise fall back
to the original string. Keep the coercion local to those branches (do not change
non-comparison keys) and preserve existing merging behavior (mongoQuery[field] =
{ ...mongoQuery[field], $gt: ... } etc.).
In `@frontend/src/components/RowDetailDrawer.jsx`:
- Around line 133-139: Guard the date rendering in the RowDetailDrawer JSX: when
field.type === "Date" compute const d = new Date(record[field.key]) and check d
is valid (e.g. !isNaN(d.getTime())) before calling d.toLocaleString(); if
invalid or missing, return the safe fallback string ("Empty") instead of letting
"Invalid Date" render. Update the value prop used in the input (and the other
similar occurrences in this file) to use this validity check rather than only
testing record[field.key].
- Around line 141-147: The render branch in RowDetailDrawer.jsx treats
null/objects incorrectly because the condition `typeof record[field.key] ===
"object"` and the value `JSON.stringify(record[field.key] ?? {}, null, 2)`
coerce null/undefined to `{}`; change the condition to explicitly handle
objects/arrays (e.g., use `field.type === "Object" || field.type === "Array" ||
Array.isArray(record[field.key]) || (record[field.key] !== null && typeof
record[field.key] === "object")`) and update the textarea value to only
JSON.stringify when the actual value is an object/array (e.g.,
`value={isObjectOrArray ? JSON.stringify(record[field.key], null, 2) :
(record[field.key] ?? "")}`) so null/undefined/empty values render as empty
string instead of `{}`.
- Around line 38-81: The drawer currently renders as a plain div (the element
with className "drawer-panel glass-panel") and the close button
(onClick={onClose} className="btn-icon") is icon-only without an accessible
name; add proper dialog semantics by giving the panel role="dialog" and
aria-modal="true", add an aria-labelledby that references the header title (give
the h2 a stable id), ensure the close button gets an accessible name (e.g.,
aria-label="Close" or a visually-hidden label), and implement basic focus
management when isOpen changes (move focus into the dialog — e.g., to the header
or the panel via tabIndex and focus — and restore focus on close) plus handle
Escape key to call onClose to meet expected modal behavior.
In `@frontend/src/pages/Database.jsx`:
- Around line 388-393: The inline style on the filter-menu glass-panel sets an
excessive zIndex of 9000000000; update the style for the <div
className="filter-menu glass-panel"> to use a sensible z-index (e.g., 1000) or
reference your app's existing z-index scale variable instead of the huge literal
so stacking behavior remains correct and cross-browser safe.
- Around line 436-439: The handler mutates objects inside queryParams.filters
via newFilters[idx].field which is a shallow-array copy only; instead create a
new filter object for the modified index and replace it in the copied array
before calling setQueryParams. In the onChange handlers that update
queryParams.filters (the handlers using idx, setQueryParams, and
queryParams.filters), do: clone the filters array, clone the filter object at
idx (e.g., const updated = { ...filters[idx], field: e.target.value }), assign
updated into the cloned array, then call setQueryParams(p => ({ ...p, filters:
clonedArray })) so no existing state objects are mutated.
---
Outside diff comments:
In `@frontend/src/pages/Database.jsx`:
- Around line 118-161: fetchData is being re-created on every keystroke because
it depends on queryParams, which causes the useEffect watching fetchData to fire
immediately; fix by introducing separate "draft" filter/sort state for the
filter UI (e.g., draftFilters, draftSort) and a distinct committed/applied query
state used for fetching (e.g., appliedQueryParams or committedQueryParams). Bind
filter inputs to draftFilters/draftSort and initialize/sync them from
queryParams when the filter menu opens, then update queryParams (setQueryParams
or setAppliedQueryParams) only when the "Apply Queries" button is clicked;
change fetchData to depend on appliedQueryParams (not the draft) so typing no
longer triggers API calls, and ensure the useEffect that calls fetchData depends
on fetchData/appliedQueryParams accordingly.
---
Nitpick comments:
In `@backend/utils/queryEngine.js`:
- Around line 9-10: The forEach callback currently uses a concise arrow that
returns the boolean from the delete expression (excludedFields.forEach(el =>
delete queryObj[el])), triggering the linter; change the callback to use a block
body that discards the return value — e.g., replace the arrow with a function
body that calls delete queryObj[el]; (or use a for...of loop) so the callback
does not return the delete result; update the code around excludedFields and
queryObj accordingly.
In `@frontend/src/components/RowDetailDrawer.jsx`:
- Around line 6-14: The Escape key handler is being attached globally even when
the drawer is closed; update the useEffect that declares handleKeyDown so it
only adds the window "keydown" listener when isOpen is true and removes it when
isOpen becomes false or on unmount: inside the useEffect for isOpen/onClose,
guard with if (!isOpen) return (or only call addEventListener when isOpen), and
ensure the cleanup always removes the same handleKeyDown reference so onClose
and handleKeyDown are referenced correctly.
In `@frontend/src/pages/LandingPage/style.css`:
- Around line 44-47: Rename the `@keyframes` identifier from "pulseGlow" to
kebab-case "pulse-glow" and update all usages that reference it (e.g.,
animation, animation-name, or shorthand animation properties that currently use
pulseGlow) to "pulse-glow" so the keyframes-name-pattern rule is satisfied;
ensure the `@keyframes` declaration and every CSS rule that applies the animation
are updated consistently.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/controllers/data.controller.jsbackend/controllers/project.controller.jsbackend/utils/queryEngine.jsdiff_output.txtfrontend/src/components/CollectionTable.jsxfrontend/src/components/DatabaseSidebar.jsxfrontend/src/components/RowDetailDrawer.jsxfrontend/src/pages/Database.jsxfrontend/src/pages/LandingPage/index.jsxfrontend/src/pages/LandingPage/style.cssnext.txt
💤 Files with no reviewable changes (2)
- next.txt
- diff_output.txt
| 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); |
There was a problem hiding this comment.
User-supplied query keys are passed directly into MongoDB filter — potential NoSQL injection vector.
An attacker can craft query parameters like $where=... or __proto__ that flow straight into mongoQuery and then into Model.find(). While Mongoose provides some protections, the else branch on line 27 passes arbitrary keys unvalidated. You should either whitelist allowed field names (e.g., check against the collection's schema model) or at minimum reject keys starting with $.
Proposed safeguard
for (const key in queryObj) {
+ // Strip any keys that could be Mongo operators
+ const baseField = key.replace(/_(gt|gte|lt|lte)$/, '');
+ if (baseField.startsWith('$') || baseField.startsWith('__')) continue;
+
if (key.endsWith('_gt')) {🧰 Tools
🪛 Biome (2.4.4)
[error] 10-10: This callback passed to forEach() iterable method should not return a value.
(lint/suspicious/useIterableCallbackReturn)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/utils/queryEngine.js` around lines 7 - 31, The filter() method builds
mongoQuery from this.queryString and currently copies keys directly into
mongoQuery before calling this.query.find(mongoQuery), which permits NoSQL
injection; fix by sanitizing/whitelisting keys: either derive an allowedFields
list from the model/schema used by this class (e.g., check against
this.model.schema.paths or an explicit allowedFields array) and only copy keys
that exist there, or at minimum drop any key that begins with '$' or contains
prototype pollution names like '__proto__' or 'constructor' before adding to
mongoQuery; ensure special-suffix handlers (keys ending with _gt/_gte/_lt/_lte)
also respect the same whitelist/sanitization before setting field operators.
| 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]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Query values are always strings — numeric comparisons will silently misbehave.
req.query values are always strings. When you build { price: { $gt: "5" } }, MongoDB performs a lexicographic comparison, not a numeric one. This means price_gt=5 won't correctly filter numeric fields (e.g., "50" is less than "5" lexicographically).
You need to coerce values to their appropriate types. At minimum, attempt numeric parsing for comparison operators:
Proposed fix
+ const coerce = (val) => {
+ if (!isNaN(val) && val.trim() !== '') return Number(val);
+ // Could also handle date parsing here
+ return val;
+ };
+
const mongoQuery = {};
for (const key in queryObj) {
if (key.endsWith('_gt')) {
const field = key.replace(/_gt$/, '');
- mongoQuery[field] = { ...mongoQuery[field], $gt: queryObj[key] };
+ mongoQuery[field] = { ...mongoQuery[field], $gt: coerce(queryObj[key]) };
} else if (key.endsWith('_gte')) {
const field = key.replace(/_gte$/, '');
- mongoQuery[field] = { ...mongoQuery[field], $gte: queryObj[key] };
+ mongoQuery[field] = { ...mongoQuery[field], $gte: coerce(queryObj[key]) };
} else if (key.endsWith('_lt')) {
const field = key.replace(/_lt$/, '');
- mongoQuery[field] = { ...mongoQuery[field], $lt: queryObj[key] };
+ mongoQuery[field] = { ...mongoQuery[field], $lt: coerce(queryObj[key]) };
} else if (key.endsWith('_lte')) {
const field = key.replace(/_lte$/, '');
- mongoQuery[field] = { ...mongoQuery[field], $lte: queryObj[key] };
+ mongoQuery[field] = { ...mongoQuery[field], $lte: coerce(queryObj[key]) };
} else {
mongoQuery[key] = queryObj[key];
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/utils/queryEngine.js` around lines 12 - 29, The loop that builds
mongoQuery in backend/utils/queryEngine.js uses req.query string values directly
(e.g., in the _gt/_gte/_lt/_lte branches), causing lexicographic comparisons;
update the logic in that loop (the for...in over queryObj that constructs
mongoQuery) so that when handling comparison suffixes (_gt, _gte, _lt, _lte) you
attempt to coerce the string value to a Number (e.g., parseFloat and check
!isNaN) and use the numeric value in the $gt/$gte/$lt/$lte operators when valid;
otherwise fall back to the original string. Keep the coercion local to those
branches (do not change non-comparison keys) and preserve existing merging
behavior (mongoQuery[field] = { ...mongoQuery[field], $gt: ... } etc.).
| 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; | ||
| } |
There was a problem hiding this comment.
No upper bound on limit — allows unbounded data retrieval.
A client can pass limit=10000000 and dump the entire collection in one request, causing potential memory exhaustion and slow responses. Clamp to a reasonable maximum.
Proposed fix
paginate() {
+ const MAX_LIMIT = 500;
const page = parseInt(this.queryString.page, 10) || 1;
- const limit = parseInt(this.queryString.limit, 10) || 100;
+ const limit = Math.min(parseInt(this.queryString.limit, 10) || 100, MAX_LIMIT);
const skip = (page - 1) * limit;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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; | |
| } | |
| paginate() { | |
| const MAX_LIMIT = 500; | |
| const page = parseInt(this.queryString.page, 10) || 1; | |
| const limit = Math.min(parseInt(this.queryString.limit, 10) || 100, MAX_LIMIT); | |
| const skip = (page - 1) * limit; | |
| this.query = this.query.skip(skip).limit(limit); | |
| return this; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/utils/queryEngine.js` around lines 54 - 62, In paginate(), prevent
unbounded requests by clamping the parsed limit from this.queryString.limit to a
safe maximum (e.g., MAX_LIMIT) before computing skip; parse the incoming limit
as an integer, ensure it's at least 1, then set limit = Math.min(parsedLimit,
MAX_LIMIT) (or a default if missing/invalid), compute skip and apply this.query
= this.query.skip(skip).limit(limit); add a clear constant like MAX_LIMIT near
paginate() for easy adjustment and reference.
| <div | ||
| className="drawer-panel glass-panel" | ||
| style={{ | ||
| position: "fixed", | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| width: isWideForm ? "600px" : "450px", | ||
| maxWidth: "100%", | ||
| zIndex: 1000, | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| transform: isOpen ? "translateX(0)" : "translateX(100%)", | ||
| animation: "slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)", | ||
| borderLeft: "1px solid var(--color-border)", | ||
| background: "var(--color-bg-card)", | ||
| boxShadow: "-10px 0 30px rgba(0,0,0,0.3)" | ||
| }} | ||
| > | ||
| {/* Header */} | ||
| <div className="drawer-header" style={{ | ||
| padding: "1.5rem", | ||
| borderBottom: "1px solid var(--color-border)", | ||
| display: "flex", | ||
| justifyContent: "space-between", | ||
| alignItems: "center" | ||
| }}> | ||
| <div> | ||
| <h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}> | ||
| <FileText size={20} className="text-primary" /> | ||
| Record Details | ||
| </h2> | ||
| <p style={{ fontSize: "0.85rem", color: "var(--color-text-muted)", margin: "4px 0 0 0", fontFamily: "monospace" }}> | ||
| ID: {record._id} | ||
| </p> | ||
| </div> | ||
| <button | ||
| onClick={onClose} | ||
| className="btn-icon" | ||
| style={{ color: "var(--color-text-muted)" }} | ||
| > | ||
| <X size={20} /> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Add proper dialog accessibility semantics and labeling.
Line 38 renders a modal-like panel without dialog semantics/focus context, and Line 74 has an icon-only close button without an accessible name. This makes keyboard/screen-reader usage fragile.
Suggested patch
- <div
- className="drawer-panel glass-panel"
+ <div
+ className="drawer-panel glass-panel"
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="row-detail-drawer-title"
+ tabIndex={-1}
style={{
@@
- <h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}>
+ <h2 id="row-detail-drawer-title" style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}>
@@
<button
onClick={onClose}
className="btn-icon"
+ aria-label="Close record details"
style={{ color: "var(--color-text-muted)" }}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| className="drawer-panel glass-panel" | |
| style={{ | |
| position: "fixed", | |
| top: 0, | |
| right: 0, | |
| bottom: 0, | |
| width: isWideForm ? "600px" : "450px", | |
| maxWidth: "100%", | |
| zIndex: 1000, | |
| display: "flex", | |
| flexDirection: "column", | |
| transform: isOpen ? "translateX(0)" : "translateX(100%)", | |
| animation: "slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)", | |
| borderLeft: "1px solid var(--color-border)", | |
| background: "var(--color-bg-card)", | |
| boxShadow: "-10px 0 30px rgba(0,0,0,0.3)" | |
| }} | |
| > | |
| {/* Header */} | |
| <div className="drawer-header" style={{ | |
| padding: "1.5rem", | |
| borderBottom: "1px solid var(--color-border)", | |
| display: "flex", | |
| justifyContent: "space-between", | |
| alignItems: "center" | |
| }}> | |
| <div> | |
| <h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}> | |
| <FileText size={20} className="text-primary" /> | |
| Record Details | |
| </h2> | |
| <p style={{ fontSize: "0.85rem", color: "var(--color-text-muted)", margin: "4px 0 0 0", fontFamily: "monospace" }}> | |
| ID: {record._id} | |
| </p> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="btn-icon" | |
| style={{ color: "var(--color-text-muted)" }} | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div | |
| className="drawer-panel glass-panel" | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="row-detail-drawer-title" | |
| tabIndex={-1} | |
| style={{ | |
| position: "fixed", | |
| top: 0, | |
| right: 0, | |
| bottom: 0, | |
| width: isWideForm ? "600px" : "450px", | |
| maxWidth: "100%", | |
| zIndex: 1000, | |
| display: "flex", | |
| flexDirection: "column", | |
| transform: isOpen ? "translateX(0)" : "translateX(100%)", | |
| animation: "slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)", | |
| borderLeft: "1px solid var(--color-border)", | |
| background: "var(--color-bg-card)", | |
| boxShadow: "-10px 0 30px rgba(0,0,0,0.3)" | |
| }} | |
| > | |
| {/* Header */} | |
| <div className="drawer-header" style={{ | |
| padding: "1.5rem", | |
| borderBottom: "1px solid var(--color-border)", | |
| display: "flex", | |
| justifyContent: "space-between", | |
| alignItems: "center" | |
| }}> | |
| <div> | |
| <h2 id="row-detail-drawer-title" style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}> | |
| <FileText size={20} className="text-primary" /> | |
| Record Details | |
| </h2> | |
| <p style={{ fontSize: "0.85rem", color: "var(--color-text-muted)", margin: "4px 0 0 0", fontFamily: "monospace" }}> | |
| ID: {record._id} | |
| </p> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="btn-icon" | |
| aria-label="Close record details" | |
| style={{ color: "var(--color-text-muted)" }} | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/RowDetailDrawer.jsx` around lines 38 - 81, The drawer
currently renders as a plain div (the element with className "drawer-panel
glass-panel") and the close button (onClick={onClose} className="btn-icon") is
icon-only without an accessible name; add proper dialog semantics by giving the
panel role="dialog" and aria-modal="true", add an aria-labelledby that
references the header title (give the h2 a stable id), ensure the close button
gets an accessible name (e.g., aria-label="Close" or a visually-hidden label),
and implement basic focus management when isOpen changes (move focus into the
dialog — e.g., to the header or the panel via tabIndex and focus — and restore
focus on close) plus handle Escape key to call onClose to meet expected modal
behavior.
| ) : field.type === "Date" ? ( | ||
| <input | ||
| type="text" | ||
| className="form-input" | ||
| readOnly | ||
| value={record[field.key] ? new Date(record[field.key]).toLocaleString() : "Empty"} | ||
| style={{ cursor: "default" }} |
There was a problem hiding this comment.
Guard date formatting to avoid exposing "Invalid Date" in the UI.
Lines 138/185/191 directly format new Date(...). Invalid values will render "Invalid Date" to users instead of a safe fallback.
Suggested patch
export default function RowDetailDrawer({ isOpen, onClose, record, fields = [], onEdit }) {
+ const formatDate = (value) => {
+ const d = new Date(value);
+ return Number.isNaN(d.getTime()) ? "Empty" : d.toLocaleString();
+ };
@@
- value={record[field.key] ? new Date(record[field.key]).toLocaleString() : "Empty"}
+ value={record[field.key] ? formatDate(record[field.key]) : "Empty"}
@@
- <span style={{ color: "var(--color-text-main)" }}>{new Date(record.createdAt).toLocaleString()}</span>
+ <span style={{ color: "var(--color-text-main)" }}>{formatDate(record.createdAt)}</span>
@@
- <span style={{ color: "var(--color-text-main)" }}>{new Date(record.updatedAt).toLocaleString()}</span>
+ <span style={{ color: "var(--color-text-main)" }}>{formatDate(record.updatedAt)}</span>Also applies to: 182-191
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/RowDetailDrawer.jsx` around lines 133 - 139, Guard
the date rendering in the RowDetailDrawer JSX: when field.type === "Date"
compute const d = new Date(record[field.key]) and check d is valid (e.g.
!isNaN(d.getTime())) before calling d.toLocaleString(); if invalid or missing,
return the safe fallback string ("Empty") instead of letting "Invalid Date"
render. Update the value prop used in the input (and the other similar
occurrences in this file) to use this validity check rather than only testing
record[field.key].
| ) : field.type === "Object" || field.type === "Array" || typeof record[field.key] === "object" ? ( | ||
| <textarea | ||
| className="form-input custom-scrollbar" | ||
| readOnly | ||
| rows={4} | ||
| value={JSON.stringify(record[field.key] ?? {}, null, 2)} | ||
| style={{ cursor: "default", resize: "vertical", fontFamily: "monospace", fontSize: "0.85rem", color: "#3ECF8E" }} |
There was a problem hiding this comment.
Avoid rendering null/empty object fields as {} by default.
Line 141 treats null as an object, and Line 146 coerces empty values to {}. That misrepresents missing data.
Suggested patch
- ) : field.type === "Object" || field.type === "Array" || typeof record[field.key] === "object" ? (
+ ) : field.type === "Object" || field.type === "Array" || (record[field.key] !== null && typeof record[field.key] === "object") ? (
<textarea
@@
- value={JSON.stringify(record[field.key] ?? {}, null, 2)}
+ value={record[field.key] == null ? "Empty" : JSON.stringify(record[field.key], null, 2)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ) : field.type === "Object" || field.type === "Array" || typeof record[field.key] === "object" ? ( | |
| <textarea | |
| className="form-input custom-scrollbar" | |
| readOnly | |
| rows={4} | |
| value={JSON.stringify(record[field.key] ?? {}, null, 2)} | |
| style={{ cursor: "default", resize: "vertical", fontFamily: "monospace", fontSize: "0.85rem", color: "#3ECF8E" }} | |
| ) : field.type === "Object" || field.type === "Array" || (record[field.key] !== null && typeof record[field.key] === "object") ? ( | |
| <textarea | |
| className="form-input custom-scrollbar" | |
| readOnly | |
| rows={4} | |
| value={record[field.key] == null ? "Empty" : JSON.stringify(record[field.key], null, 2)} | |
| style={{ cursor: "default", resize: "vertical", fontFamily: "monospace", fontSize: "0.85rem", color: "#3ECF8E" }} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/RowDetailDrawer.jsx` around lines 141 - 147, The
render branch in RowDetailDrawer.jsx treats null/objects incorrectly because the
condition `typeof record[field.key] === "object"` and the value
`JSON.stringify(record[field.key] ?? {}, null, 2)` coerce null/undefined to
`{}`; change the condition to explicitly handle objects/arrays (e.g., use
`field.type === "Object" || field.type === "Array" ||
Array.isArray(record[field.key]) || (record[field.key] !== null && typeof
record[field.key] === "object")`) and update the textarea value to only
JSON.stringify when the actual value is an object/array (e.g.,
`value={isObjectOrArray ? JSON.stringify(record[field.key], null, 2) :
(record[field.key] ?? "")}`) so null/undefined/empty values render as empty
string instead of `{}`.
| <div className="filter-menu glass-panel" style={{ | ||
| position: 'absolute', right: 0, top: 'calc(100% + 10px)', width: '300px', | ||
| zIndex: 9000000000, padding: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem', | ||
| background: '#151515', border: '1px solid var(--color-border)', | ||
| boxShadow: '0 10px 30px rgba(0,0,0,0.7)', borderRadius: '8px' | ||
| }}> |
There was a problem hiding this comment.
zIndex: 9000000000 is excessive.
This value is unreasonable and may cause issues with z-index stacking on some browsers (values above 2^31 - 1 can overflow to negative on some engines). Use a sensible value like 1000 or match your existing z-index scale.
Proposed fix
- zIndex: 9000000000, padding: '1rem',
+ zIndex: 1000, padding: '1rem',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="filter-menu glass-panel" style={{ | |
| position: 'absolute', right: 0, top: 'calc(100% + 10px)', width: '300px', | |
| zIndex: 9000000000, padding: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem', | |
| background: '#151515', border: '1px solid var(--color-border)', | |
| boxShadow: '0 10px 30px rgba(0,0,0,0.7)', borderRadius: '8px' | |
| }}> | |
| <div className="filter-menu glass-panel" style={{ | |
| position: 'absolute', right: 0, top: 'calc(100% + 10px)', width: '300px', | |
| zIndex: 1000, padding: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem', | |
| background: '#151515', border: '1px solid var(--color-border)', | |
| boxShadow: '0 10px 30px rgba(0,0,0,0.7)', borderRadius: '8px' | |
| }}> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/Database.jsx` around lines 388 - 393, The inline style on
the filter-menu glass-panel sets an excessive zIndex of 9000000000; update the
style for the <div className="filter-menu glass-panel"> to use a sensible
z-index (e.g., 1000) or reference your app's existing z-index scale variable
instead of the huge literal so stacking behavior remains correct and
cross-browser safe.
| onChange={e => { | ||
| const newFilters = [...queryParams.filters]; | ||
| newFilters[idx].field = e.target.value; | ||
| setQueryParams(p => ({ ...p, filters: newFilters })); |
There was a problem hiding this comment.
Direct mutation of state objects — React anti-pattern.
[...queryParams.filters] creates a shallow copy of the array, but the objects inside are still the same references. Mutating newFilters[idx].field on line 438 mutates the object that's already in React state, bypassing React's change detection and potentially causing stale renders.
Create new objects for modified entries:
Proposed fix (apply to all three onChange handlers at lines 436, 452, 472)
onChange={e => {
- const newFilters = [...queryParams.filters];
- newFilters[idx].field = e.target.value;
+ const newFilters = queryParams.filters.map((f, i) =>
+ i === idx ? { ...f, field: e.target.value } : f
+ );
setQueryParams(p => ({ ...p, filters: newFilters }));
}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onChange={e => { | |
| const newFilters = [...queryParams.filters]; | |
| newFilters[idx].field = e.target.value; | |
| setQueryParams(p => ({ ...p, filters: newFilters })); | |
| onChange={e => { | |
| const newFilters = queryParams.filters.map((f, i) => | |
| i === idx ? { ...f, field: e.target.value } : f | |
| ); | |
| setQueryParams(p => ({ ...p, filters: newFilters })); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/Database.jsx` around lines 436 - 439, The handler mutates
objects inside queryParams.filters via newFilters[idx].field which is a
shallow-array copy only; instead create a new filter object for the modified
index and replace it in the copied array before calling setQueryParams. In the
onChange handlers that update queryParams.filters (the handlers using idx,
setQueryParams, and queryParams.filters), do: clone the filters array, clone the
filter object at idx (e.g., const updated = { ...filters[idx], field:
e.target.value }), assign updated into the cloned array, then call
setQueryParams(p => ({ ...p, filters: clonedArray })) so no existing state
objects are mutated.
There was a problem hiding this comment.
Code Review
This PR introduces a new QueryEngine for the backend and redesigns the Database UI on the frontend. While this is a significant upgrade, the filter method in backend/utils/queryEngine.js contains a high-severity NoSQL injection vulnerability. The method fails to validate query parameters before passing them to MongoDB's find method, potentially allowing attackers to inject arbitrary MongoDB operators. Besides this vulnerability, consider the following:
- Refactor the filter logic in
queryEngine.jsto improve code readability. - Refactor the query string creation in
Database.jsxto improve code readability. - Manage
z-indexvalues inDatabase.jsxusing CSS variables. - Move inline styles in components to separate CSS files to improve maintainability.
| <div className="filter-menu glass-panel" style={{ | ||
| position: 'absolute', right: 0, top: 'calc(100% + 10px)', width: '300px', | ||
| zIndex: 9000000000, padding: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem', | ||
| background: '#151515', border: '1px solid var(--color-border)', | ||
| boxShadow: '0 10px 30px rgba(0,0,0,0.7)', borderRadius: '8px' | ||
| }}> |
There was a problem hiding this comment.
Filter menu ke liye zIndex: 9000000000 ka istemal kiya gaya hai. Yeh ek bahut bada "magic number" hai aur z-index conflicts paida kar sakta hai. Isse maintain karna bhi mushkil hai.
General rule ke mutabik, itne bade z-index values se bachna chahiye. Behtar hoga ki aap ek chhota, apekshakrit value (jaise 1100) istemal karein aur sabhi z-index values ko CSS variables mein centralize karein. Isse code scalable aur maintainable rahega.
<div className="filter-menu glass-panel" style={{
position: 'absolute', right: 0, top: 'calc(100% + 10px)', width: '300px',
zIndex: 1100, padding: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem',
background: '#151515', border: '1px solid var(--color-border)',
boxShadow: '0 10px 30px rgba(0,0,0,0.7)', borderRadius: '8px'
}}>References
- A large magic number (9000000000) is used for z-index, which should be avoided in favor of smaller, managed values, preferably through CSS variables.
| 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; | ||
| } |
There was a problem hiding this comment.
The filter method is vulnerable to NoSQL injection. It iterates over the queryString object (derived from req.query) and directly assigns values to the mongoQuery object without validation. If an attacker provides an object containing MongoDB operators (e.g., ?field[$ne]=value), Express's query parser (typically qs) will parse this into a nested object, which is then passed directly to Mongoose's find() method. This allows attackers to bypass intended filters or perform unauthorized data retrieval. You should ensure that all filter values are primitives (strings, numbers, or booleans) or use a sanitization library like mongo-sanitize.
To prevent NoSQL injection, ensure that the value is a primitive.
filter() {
const queryObj = { ...this.queryString };
const excludedFields = ['page', 'sort', 'limit', 'fields'];
excludedFields.forEach(el => delete queryObj[el]);
const mongoQuery = {};
for (const key in queryObj) {
// Prevent NoSQL injection by ensuring the value is a primitive
if (typeof queryObj[key] === 'object') continue;
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;
}| <div | ||
| className="drawer-panel glass-panel" | ||
| style={{ | ||
| position: "fixed", | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| width: isWideForm ? "600px" : "450px", | ||
| maxWidth: "100%", | ||
| zIndex: 1000, | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| transform: isOpen ? "translateX(0)" : "translateX(100%)", | ||
| animation: "slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)", | ||
| borderLeft: "1px solid var(--color-border)", | ||
| background: "var(--color-bg-card)", | ||
| boxShadow: "-10px 0 30px rgba(0,0,0,0.3)" | ||
| }} |
There was a problem hiding this comment.
Is component mein bahut saare inline styles ka istemal kiya gaya hai. Isse component ko padhna aur maintain karna mushkil ho jaata hai, aur yeh styling ko component logic ke saath mix kar deta hai.
Behtar hoga ki in styles ko ek alag CSS file mein move kiya jaaye aur CSS classes ka istemal kiya jaaye. Aap CSS Modules ya kisi CSS-in-JS library (jaise styled-components) ka bhi istemal kar sakte hain. Isse code saaf aur organized rahega.
| // Build query string from state | ||
| let queryStr = `?page=${queryParams.page}&limit=${queryParams.limit}`; | ||
| if (queryParams.sort) { | ||
| queryStr += `&sort=${queryParams.sort}`; | ||
| } | ||
|
|
||
| // Append advanced filters | ||
| queryParams.filters.forEach(filter => { | ||
| if (filter.field && filter.operator && filter.value !== '') { | ||
| if (filter.operator === '=') { | ||
| queryStr += `&${filter.field}=${encodeURIComponent(filter.value)}`; | ||
| } else { | ||
| queryStr += `&${filter.field}${filter.operator}=${encodeURIComponent(filter.value)}`; | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
Query string ko manually string concatenation se banaya ja raha hai. Isse code padhna thoda mushkil ho jaata hai aur URL encoding mein galtiyan ho sakti hain.
URLSearchParams ka istemal karke isse aur saaf aur surakshit banaya ja sakta hai. Yeh automatically values ko encode kar deta hai aur code ko zyada readable banata hai.
// Build query string from state
const params = new URLSearchParams({
page: queryParams.page,
limit: queryParams.limit,
});
if (queryParams.sort) {
params.append('sort', queryParams.sort);
}
// Append advanced filters
queryParams.filters.forEach(filter => {
if (filter.field && filter.operator && filter.value !== '') {
const key = filter.operator === '=' ? filter.field : `${filter.field}${filter.operator}`;
params.append(key, filter.value);
}
});
const queryStr = `?${params.toString()}`;
Pull Request: feat: Created Filter feature and redesign Database UI
Overview
Is PR mein urBackend ke data retrieval system ko upgrade kiya gaya hai aur database management interface ko ek naya, clean look diya gaya hai.
Key Changes
1. Backend: Query Engine v1 Implementation
_gt,_lt,_gte,_lte) ko support karta hai.page,limit, aurskiplogic add kiya gaya hai.sort=field:orderformat mein sort karne ki capability add ki gayi hai.queryEngine.js) mein move kiya gaya hai.2. Frontend: Database UI Redesign
Database.jsxaurDatabaseSidebarko redesign kiya gaya hai naye navigation flow ke liye.CollectionTable.jsxko update kiya gaya hai naye filters aur action buttons ke saath.Testing Steps
GET /api/data/:collectionName?field_gt=valuetest karein.page=1&limit=10pass karke response size verify karein.Summary by CodeRabbit
Release Notes
New Features
Style
Chores