Skip to content

feat: add resource search#933

Merged
mleonidas merged 5 commits intomainfrom
920-feat-improved-api-endpoint-for-listing-resources
Apr 8, 2026
Merged

feat: add resource search#933
mleonidas merged 5 commits intomainfrom
920-feat-improved-api-endpoint-for-listing-resources

Conversation

@mleonidas
Copy link
Copy Markdown
Collaborator

@mleonidas mleonidas commented Apr 8, 2026

  • adds a POST request to resources/search where you can have a more complex filter of the resources you would like to list

Summary by CodeRabbit

  • New Features
    • Added workspace-scoped POST /v1/workspaces/{workspaceId}/resources/search for resource discovery with full-text query, multi-value filters (kinds, provider IDs, versions, identifiers), metadata exact-match, pagination (limit/offset) and sorting (createdAt, updatedAt, name, kind).
    • Responses return paginated results (items, total, limit, offset) and surface validation errors for invalid requests.

* adds a POST request to resources/search where you can have a more
  complex filter of the resources you would like to list
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a workspace-scoped POST /v1/workspaces/{workspaceId}/resources/search endpoint and a new ListResourcesFilters schema; implements request validation, dynamic DB filtering, parallel total+fetch, sorting/pagination, OpenAPI/type entries, and route wiring returning paginated Resource results.

Changes

Cohort / File(s) Summary
OpenAPI root
apps/api/openapi/openapi.json
Added components/schemas/ListResourcesFilters and registered POST /v1/workspaces/{workspaceId}/resources/search operation with request/response definitions.
OpenAPI path & schemas
apps/api/openapi/paths/resources.jsonnet, apps/api/openapi/schemas/resources.jsonnet
Added path entry for the search endpoint and exported ListResourcesFilters schema (arrays for providerIds/versions/identifiers/kinds, metadata map, query, limit/offset, sortBy, order).
Runtime route
apps/api/src/routes/v1/workspaces/resources.ts
Added searchResources handler + route registration: validates paging, constructs workspace-scoped dynamic WHERE clauses (providerIds, versions, identifiers, ilike query with escaping, kinds IN, metadata exact matches), runs parallel count + fetch, applies dynamic sorting, maps rows to responses, returns paginated result.
Type definitions
apps/api/src/types/openapi.ts
Added components.schemas.ListResourcesFilters type and operations["searchResources"] (path param workspaceId, request body, 200/400 responses) to OpenAPI types.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as "API /v1/.../resources/search"
    participant DB

    Client->>API: POST /v1/workspaces/{workspaceId}/resources/search\n(body: ListResourcesFilters)
    API->>DB: Build filtered query (workspaceId + optional filters)
    API->>DB: Parallel: fetch rows (limit/offset) and count(total)
    DB-->>API: rows
    DB-->>API: total
    API->>API: Map rows -> Resource responses
    API-->>Client: 200 { items: Resource[], total, limit, offset }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jsbroks

Poem

🐰 I sniffed the schemas, filters in tow,
I hop through queries where wild results grow.
I count and page, then sort by name,
Map rows to carrots — tidy, not tame.
Hooray — the search is ready to show! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add resource search' clearly and directly describes the main change—adding a new resource search feature with a POST endpoint for advanced filtering.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 920-feat-improved-api-endpoint-for-listing-resources

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
apps/api/openapi/schemas/resources.jsonnet (1)

134-137: Consider adding a default value for sortBy.

When sortBy is omitted but order is provided (or defaults to asc), the sort behavior becomes implementation-dependent. Adding a default like createdAt would ensure predictable ordering for clients that don't explicitly specify sorting.

🔧 Suggested change
       sortBy: {
         type: 'string',
         enum: ['createdAt', 'updatedAt', 'name', 'kind'],
+        default: 'createdAt',
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/openapi/schemas/resources.jsonnet` around lines 134 - 137, The
schema for the sortBy property lacks a default, causing unpredictable sort
behavior when clients omit it; update the sortBy definition (the sortBy property
in resources.jsonnet) to include a default value (e.g., "createdAt") while
keeping the existing enum so that when clients don't provide sortBy the API will
default to predictable ordering.
apps/api/openapi/openapi.json (1)

1431-1436: Consider lowering default search page size for safer baseline performance.

On Line 1432, limit defaults to 500, which is much higher than most list endpoints in this spec. A lower default (e.g., 50) reduces expensive default queries and response payload size.

Suggested diff
                "limit": {
-                  "default": 500,
+                  "default": 50,
                   "maximum": 1000,
                   "minimum": 1,
                   "type": "integer"
                },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/openapi/openapi.json` around lines 1431 - 1436, The OpenAPI schema's
integer parameter "limit" currently has a default of 500 which is high; change
the "default" value on the "limit" schema to a lower safe baseline such as 50,
keeping the existing "minimum": 1 and "maximum": 1000 intact, and update any
nearby docs/comments or examples that assume the 500 default to reflect the new
default to avoid inconsistencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/workspaces/resources.ts`:
- Around line 367-373: The ILIKE patterns are built directly from user input
(query) in the or(...) call using ilike(schema.resource.name, `%${query}%`) and
ilike(schema.resource.identifier, `%${query}%`), which treats % and _ as SQL
wildcards; fix this by escaping backslashes, % and _ in the query before
interpolating (e.g., write a helper like escapeILikePattern(query) that replaces
"\"=>"\\" then "%"=>"\%" and "_"=>"\_"), then use the escaped value inside the
`%...%` pattern and ensure the query builder issues the ILIKE with the proper
escape character so ilike(schema.resource.name, `%${escaped}%`) and
ilike(schema.resource.identifier, `%${escaped}%`) perform literal substring
matches.
- Around line 354-355: Validate and sanitize the pagination inputs before
passing them into the query builder: for the variables declared as const limit =
body.limit and const offset = body.offset ensure they are numeric integers and
non‑negative (coerce/parse to integers or return defaults), reject or clamp
fractional/negative values (e.g., floor or error for fractions, Math.max(0,
...)), and enforce a sensible max for limit to prevent very large queries; then
pass the sanitized values to .limit() and .offset() instead of the raw body
values.
- Around line 382-405: The current ORDER BY uses only orderBy(orderFn(orderCol))
which is unstable when primary keys tie; update the query that builds the rows
to add a deterministic tiebreaker by including schema.resource.id as a secondary
sort using the same direction function (orderFn) so pagination is stable —
change the orderBy call that references orderFn(orderCol) to orderBy with two
clauses: the primary orderFn(orderCol) and a secondary
orderFn(schema.resource.id) (use the existing orderCol, orderFn identifiers and
the schema.resource.id symbol).

In `@apps/api/src/types/openapi.ts`:
- Around line 5206-5214: The OpenAPI schema for the search/list response
currently uses components["schemas"]["Resource"][] but actual responses from
toResourceResponse(...) (in apps/api/src/routes/v1/workspaces/resources.ts)
include the primary key id; update the OpenAPI source so the response items
reflect the real shape—either change the items type to the existing
ResourceResponse schema (if present) or add id:string|number to
components.schemas.Resource (or create a new ResourceResponse schema matching
toResourceResponse) so generated clients see the id property in search results.

---

Nitpick comments:
In `@apps/api/openapi/openapi.json`:
- Around line 1431-1436: The OpenAPI schema's integer parameter "limit"
currently has a default of 500 which is high; change the "default" value on the
"limit" schema to a lower safe baseline such as 50, keeping the existing
"minimum": 1 and "maximum": 1000 intact, and update any nearby docs/comments or
examples that assume the 500 default to reflect the new default to avoid
inconsistencies.

In `@apps/api/openapi/schemas/resources.jsonnet`:
- Around line 134-137: The schema for the sortBy property lacks a default,
causing unpredictable sort behavior when clients omit it; update the sortBy
definition (the sortBy property in resources.jsonnet) to include a default value
(e.g., "createdAt") while keeping the existing enum so that when clients don't
provide sortBy the API will default to predictable ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b185d8f-3f97-4237-976d-acfe448948ee

📥 Commits

Reviewing files that changed from the base of the PR and between c9fc990 and 7bcfd1e.

📒 Files selected for processing (6)
  • apps/api/openapi/openapi.json
  • apps/api/openapi/paths/resources.jsonnet
  • apps/api/openapi/schemas/resources.jsonnet
  • apps/api/src/routes/v1/workspaces/resources.ts
  • apps/api/src/types/openapi.ts
  • packages/workspace-engine-sdk/src/schema.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/workspaces/resources.ts`:
- Around line 354-368: Defaults for `limit` and `offset` should be applied
before the integer validation to avoid rejecting requests where the client
omitted them; update the code that reads `const limit = body.limit; const offset
= body.offset;` to assign defaults (limit = 500, offset = 0) when those
properties are undefined (or use nullish coalescing), then run the existing
Number.isInteger and >=0 checks against those normalized values (refer to the
`limit` and `offset` variables in this file and the validation block that calls
res.status(400)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0f81b0ba-1b65-426a-83e9-b4c0a6e415ea

📥 Commits

Reviewing files that changed from the base of the PR and between 7bcfd1e and 537cec9.

📒 Files selected for processing (4)
  • apps/api/openapi/openapi.json
  • apps/api/openapi/schemas/resources.jsonnet
  • apps/api/src/routes/v1/workspaces/resources.ts
  • apps/api/src/types/openapi.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/types/openapi.ts
  • apps/api/openapi/openapi.json

Comment on lines +369 to +391
const conditions = [eq(schema.resource.workspaceId, workspaceId)];

if (providerId != null)
conditions.push(eq(schema.resource.providerId, providerId));
if (version != null) conditions.push(eq(schema.resource.version, version));
if (identifier != null)
conditions.push(eq(schema.resource.identifier, identifier));
if (query != null) {
const escapedQuery = query.replace(/[%_\\]/g, "\\$&");
conditions.push(
or(
ilike(schema.resource.name, `%${escapedQuery}%`),
ilike(schema.resource.identifier, `%${escapedQuery}%`),
)!,
);
}
if (kinds != null && kinds.length > 0)
conditions.push(inArray(schema.resource.kind, kinds));
if (metadata != null) {
for (const [key, value] of Object.entries(metadata)) {
conditions.push(sql`${schema.resource.metadata}->>${key} = ${value}`);
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S/TS move is usually to build the array declaratively and filter out empty entries, instead of mutating with a bunch of push calls. Essential use consts as much as possible,

You can do it:

const escapedQuery =
  query != null ? query.replace(/[%_\\]/g, "\\$&") : null;

const conditions = [
  eq(schema.resource.workspaceId, workspaceId),

  providerId != null
    ? eq(schema.resource.providerId, providerId)
    : undefined,

  version != null
    ? eq(schema.resource.version, version)
    : undefined,

  identifier != null
    ? eq(schema.resource.identifier, identifier)
    : undefined,

  escapedQuery != null
    ? or(
        ilike(schema.resource.name, `%${escapedQuery}%`),
        ilike(schema.resource.identifier, `%${escapedQuery}%`),
      )
    : undefined,

  kinds?.length
    ? inArray(schema.resource.kind, kinds)
    : undefined,

  ...(metadata != null
    ? Object.entries(metadata).map(([key, value]) =>
        sql`${schema.resource.metadata}->>${key} = ${value}`
      )
    : []),
].filter((c): c is NonNullable<typeof c> => c != null);

Or in my imo

const when = <T>(condition: boolean, value: T) => (condition ? value : undefined);

const escapedQuery =
  query != null ? query.replace(/[%_\\]/g, "\\$&") : null;

const conditions = [
  eq(schema.resource.workspaceId, workspaceId),
  when(providerId != null, eq(schema.resource.providerId, providerId)),
  when(version != null, eq(schema.resource.version, version)),
  when(identifier != null, eq(schema.resource.identifier, identifier)),
  when(
    escapedQuery != null,
    or(
      ilike(schema.resource.name, `%${escapedQuery}%`),
      ilike(schema.resource.identifier, `%${escapedQuery}%`),
    )
  ),
  when(!!kinds?.length, inArray(schema.resource.kind, kinds!)),
  ...(metadata
    ? Object.entries(metadata).map(([key, value]) =>
        sql`${schema.resource.metadata}->>${key} = ${value}`
      )
    : []),
].filter((c): c is NonNullable<typeof c> => c != null);

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/api/src/types/openapi.ts (1)

1563-1572: Consider making order optional or clarifying default sort behavior.

The order field is required with a default of "asc", but sortBy is optional. When a client omits sortBy, the semantics of order become ambiguous—what field does it apply to? Consider either:

  1. Making order optional as well (only meaningful when sortBy is provided)
  2. Documenting the default sort field when sortBy is omitted

This is a minor API design consideration for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/types/openapi.ts` around lines 1563 - 1572, The schema currently
declares order as a required field with default "asc" while sortBy is optional,
making semantics unclear; update the OpenAPI type so that either (a) order
becomes optional (change the "order" property to "order?: \"asc\" | \"desc\""
and remove the assumption that it always applies) OR (b) document/implement a
default sort field by adding a default for sortBy (e.g., set sortBy default to
"createdAt") and update the JSDoc to state that order applies to that default
when sortBy is omitted; refer to the properties named order and sortBy in the
same type and ensure the comment/@default annotations are adjusted accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/types/openapi.ts`:
- Around line 1563-1572: The schema currently declares order as a required field
with default "asc" while sortBy is optional, making semantics unclear; update
the OpenAPI type so that either (a) order becomes optional (change the "order"
property to "order?: \"asc\" | \"desc\"" and remove the assumption that it
always applies) OR (b) document/implement a default sort field by adding a
default for sortBy (e.g., set sortBy default to "createdAt") and update the
JSDoc to state that order applies to that default when sortBy is omitted; refer
to the properties named order and sortBy in the same type and ensure the
comment/@default annotations are adjusted accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 307e98f8-a8bd-4aab-9081-fe921594e604

📥 Commits

Reviewing files that changed from the base of the PR and between 537cec9 and 873ebc0.

📒 Files selected for processing (3)
  • apps/api/openapi/openapi.json
  • apps/api/openapi/schemas/resources.jsonnet
  • apps/api/src/types/openapi.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/openapi/schemas/resources.jsonnet
  • apps/api/openapi/openapi.json

Co-authored-by: Justin Brooks <jsbroks@gmail.com>
Signed-off-by: Mike Leone <2907207+mleonidas@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/api/src/routes/v1/workspaces/resources.ts (1)

349-369: ⚠️ Potential issue | 🟠 Major

Align the local pagination guards with the documented request schema.

apps/api/openapi/schemas/resources.jsonnet:107-144 documents limit as optional with default 500 and range 1..1000, and offset as optional with default 0. This block validates the raw body values instead, so omitted fields still fail here, limit = 0 is accepted, and oversized limits are unchecked.

🛠 Proposed fix
 const {
   providerId,
   version,
   identifier,
   query,
   kinds,
-  limit,
-  offset,
+  limit: rawLimit,
+  offset: rawOffset,
   metadata,
   sortBy,
   order,
 } = req.body;
 
-  if (!Number.isInteger(limit) || limit < 0) {
-    res.status(400).json({ error: "`limit` must be a non-negative integer" });
+  const limit = rawLimit ?? 500;
+  const offset = rawOffset ?? 0;
+
+  if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
+    res
+      .status(400)
+      .json({ error: "`limit` must be an integer between 1 and 1000" });
     return;
   }
 
   if (!Number.isInteger(offset) || offset < 0) {
     res.status(400).json({ error: "`offset` must be a non-negative integer" });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/resources.ts` around lines 349 - 369, The
pagination guards in resources.ts currently validate raw req.body values and
reject omitted fields; change them to apply schema defaults and enforce the
documented ranges: when destructuring (providerId, version, identifier, query,
kinds, limit, offset, metadata, sortBy, order) treat undefined limit as default
500 and undefined offset as default 0, then validate that limit is an integer in
1..1000 (reject 0 and >1000) and offset is an integer >= 0; update the error
messages to reflect these constraints and use the normalized/filled limit and
offset values for subsequent logic.
🧹 Nitpick comments (2)
apps/api/src/routes/v1/workspaces/resources.ts (2)

406-411: Keep the Promise.all branch async/await-only.

The inline .then((r) => r[0]) is the only raw promise chain in this handler. Pulling the first row out after the await keeps the flow consistent and easier to scan.

♻️ Proposed refactor
-  const [countResult, rows] = await Promise.all([
+  const [countRows, rows] = await Promise.all([
     db
       .select({ total: count() })
       .from(schema.resource)
-      .where(and(...conditions))
-      .then((r) => r[0]),
+      .where(and(...conditions)),
     db
       .select()
       .from(schema.resource)
       .where(and(...conditions))
       .orderBy(orderFn(orderCol), orderFn(schema.resource.identifier))
       .limit(limit)
       .offset(offset),
   ]);
 
-  const total = countResult?.total ?? 0;
+  const total = countRows[0]?.total ?? 0;

As per coding guidelines, "Prefer async/await over raw promises".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/resources.ts` around lines 406 - 411, The
inline .then((r) => r[0]) breaks the async/await flow; change the Promise.all
branch to use only await by removing the .then and extracting the first row
after awaiting the db.select call (the symbols involved are the Promise.all
call, db.select({ total: count()
}).from(schema.resource).where(and(...conditions)), and the variables
countResult/rows). Await the select, then pull r[0] into countResult (or
destructure) before continuing so the entire handler uses async/await
consistently.

378-392: Plan supporting indexes for these new predicates.

packages/db/src/schema/resource.ts:48-51 only indexes workspaceId and deletedAt. The new ILIKE(name/identifier) and metadata->> filters will therefore scan the workspace slice, and this endpoint pays that cost twice per request (count + page query). I’d queue a follow-up migration/query-plan check before rolling this out to large workspaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/resources.ts` around lines 378 - 392, The
new filters using ilike on schema.resource.name and schema.resource.identifier
and equality on schema.resource.metadata->>key will cause full scans because
only workspaceId and deletedAt are indexed; add appropriate indexes via a
migration: create trigram (GIN with pg_trgm) indexes on schema.resource.name and
schema.resource.identifier to support ILIKE, and add either a GIN jsonb index on
schema.resource.metadata (jsonb_path_ops) or per-key btree expression indexes
like ((metadata->>'yourKey')) for the specific metadata keys you expect to
query; after adding indexes, run EXPLAIN on the count and page queries in
apps/api/src/routes/v1/workspaces/resources.ts (where conditions.push with
ilike/inArray/sql`${schema.resource.metadata}->>${key} = ${value}` is used) to
verify both queries use the new indexes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/api/src/routes/v1/workspaces/resources.ts`:
- Around line 349-369: The pagination guards in resources.ts currently validate
raw req.body values and reject omitted fields; change them to apply schema
defaults and enforce the documented ranges: when destructuring (providerId,
version, identifier, query, kinds, limit, offset, metadata, sortBy, order) treat
undefined limit as default 500 and undefined offset as default 0, then validate
that limit is an integer in 1..1000 (reject 0 and >1000) and offset is an
integer >= 0; update the error messages to reflect these constraints and use the
normalized/filled limit and offset values for subsequent logic.

---

Nitpick comments:
In `@apps/api/src/routes/v1/workspaces/resources.ts`:
- Around line 406-411: The inline .then((r) => r[0]) breaks the async/await
flow; change the Promise.all branch to use only await by removing the .then and
extracting the first row after awaiting the db.select call (the symbols involved
are the Promise.all call, db.select({ total: count()
}).from(schema.resource).where(and(...conditions)), and the variables
countResult/rows). Await the select, then pull r[0] into countResult (or
destructure) before continuing so the entire handler uses async/await
consistently.
- Around line 378-392: The new filters using ilike on schema.resource.name and
schema.resource.identifier and equality on schema.resource.metadata->>key will
cause full scans because only workspaceId and deletedAt are indexed; add
appropriate indexes via a migration: create trigram (GIN with pg_trgm) indexes
on schema.resource.name and schema.resource.identifier to support ILIKE, and add
either a GIN jsonb index on schema.resource.metadata (jsonb_path_ops) or per-key
btree expression indexes like ((metadata->>'yourKey')) for the specific metadata
keys you expect to query; after adding indexes, run EXPLAIN on the count and
page queries in apps/api/src/routes/v1/workspaces/resources.ts (where
conditions.push with ilike/inArray/sql`${schema.resource.metadata}->>${key} =
${value}` is used) to verify both queries use the new indexes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d04e9194-d648-4ca9-8b58-daf9e8e1455f

📥 Commits

Reviewing files that changed from the base of the PR and between 873ebc0 and a5190b9.

📒 Files selected for processing (1)
  • apps/api/src/routes/v1/workspaces/resources.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
apps/api/src/types/openapi.ts (1)

5206-5207: ⚠️ Potential issue | 🟠 Major

The new search response schema still hides id.

The handler serializes rows through toResourceResponse(...), which includes the resource primary key, but this response still advertises components["schemas"]["Resource"][]. Generated clients for the new endpoint won't see id unless the source OpenAPI schema is updated and this file is regenerated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/types/openapi.ts` around lines 5206 - 5207, The OpenAPI schema
advertised for the search response still references
components["schemas"]["Resource"][] but the handler actually returns objects
produced by toResourceResponse(...) which include the resource primary key (id);
update the OpenAPI spec so the response schema matches the actual shape (e.g.,
add id to components.schemas.Resource or create a new
components.schemas.ResourceWithId and use that), then regenerate the types so
apps/api/src/types/openapi.ts is rebuilt and the search endpoint's response uses
the corrected schema instead of components["schemas"]["Resource"][]. Ensure the
change targets the operation/schema used by the new search endpoint and
references the exact schema name you choose so generated clients will include
id.
apps/api/src/routes/v1/workspaces/resources.ts (1)

349-369: ⚠️ Potential issue | 🟠 Major

Normalize pagination before validating and honor the documented bounds.

limit/offset are read raw from the body, so omitting them still returns 400 even though the new contract defines defaults. This block also accepts limit = 0 and anything above the documented max of 1000, which lets callers bypass the API contract and issue unexpectedly large queries.

🛠 Suggested fix
   const {
     providerIds,
     versions,
     identifiers,
     query,
     kinds,
-    limit,
-    offset,
     metadata,
     sortBy,
     order,
   } = req.body;
+  const limit = req.body.limit ?? 500;
+  const offset = req.body.offset ?? 0;
 
-  if (!Number.isInteger(limit) || limit < 0) {
-    res.status(400).json({ error: "`limit` must be a non-negative integer" });
+  if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
+    res
+      .status(400)
+      .json({ error: "`limit` must be an integer between 1 and 1000" });
     return;
   }
 
   if (!Number.isInteger(offset) || offset < 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/workspaces/resources.ts` around lines 349 - 369,
Normalize pagination values from req.body before validating: if limit is
undefined set it to the documented default (e.g., DEFAULT_LIMIT) and if offset
is undefined set it to 0, then validate using Number.isInteger(limit) and
Number.isInteger(offset) and enforce documented bounds (limit must be >= 1 and
<= MAX_LIMIT (1000) and offset must be >= 0). Update the validation block around
limit and offset (the variables read from req.body) to reject non-integers or
out-of-range values and return clear 400 messages; introduce DEFAULT_LIMIT and
MAX_LIMIT constants if not present to avoid magic numbers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/api/src/routes/v1/workspaces/resources.ts`:
- Around line 349-369: Normalize pagination values from req.body before
validating: if limit is undefined set it to the documented default (e.g.,
DEFAULT_LIMIT) and if offset is undefined set it to 0, then validate using
Number.isInteger(limit) and Number.isInteger(offset) and enforce documented
bounds (limit must be >= 1 and <= MAX_LIMIT (1000) and offset must be >= 0).
Update the validation block around limit and offset (the variables read from
req.body) to reject non-integers or out-of-range values and return clear 400
messages; introduce DEFAULT_LIMIT and MAX_LIMIT constants if not present to
avoid magic numbers.

In `@apps/api/src/types/openapi.ts`:
- Around line 5206-5207: The OpenAPI schema advertised for the search response
still references components["schemas"]["Resource"][] but the handler actually
returns objects produced by toResourceResponse(...) which include the resource
primary key (id); update the OpenAPI spec so the response schema matches the
actual shape (e.g., add id to components.schemas.Resource or create a new
components.schemas.ResourceWithId and use that), then regenerate the types so
apps/api/src/types/openapi.ts is rebuilt and the search endpoint's response uses
the corrected schema instead of components["schemas"]["Resource"][]. Ensure the
change targets the operation/schema used by the new search endpoint and
references the exact schema name you choose so generated clients will include
id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b0b25690-37e7-4891-bf1c-a58b44ceb9a0

📥 Commits

Reviewing files that changed from the base of the PR and between a5190b9 and d9aa348.

📒 Files selected for processing (4)
  • apps/api/openapi/openapi.json
  • apps/api/openapi/schemas/resources.jsonnet
  • apps/api/src/routes/v1/workspaces/resources.ts
  • apps/api/src/types/openapi.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/openapi/openapi.json

@mleonidas mleonidas merged commit 77650b3 into main Apr 8, 2026
11 checks passed
@mleonidas mleonidas deleted the 920-feat-improved-api-endpoint-for-listing-resources branch April 8, 2026 17:14
@mleonidas mleonidas linked an issue Apr 8, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: improved api endpoint for listing resources

2 participants