Skip to content

Conversation

@adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Aug 26, 2025

Summary by CodeRabbit

  • Refactor
    • Added a generic repository interface and a workspace-scoped resource repository with full create/read/update/delete support. No user-facing UI changes.
  • Chores
    • Added a new internal database helper dependency to support the repository implementation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds @ctrlplane/db dependency to event-queue, introduces a generic Repository<T> interface, and adds a new DbResourceRepository implementing workspace-scoped CRUD and exists methods with optional transaction support.

Changes

Cohort / File(s) Summary
Dependency Update
apps/event-queue/package.json
Added dependency @ctrlplane/db with version workspace:*.
Repository Interface
apps/event-queue/src/repository/repository.ts
Added type Entity = { id: string } and exported generic Repository<T extends Entity> interface with methods: get, getAll, create, update, delete, exists.
DB-backed Resource Repository
apps/event-queue/src/repository/db-resource-repository.ts
Added DbResourceRepository implementing Repository<schema.Resource> with constructor(workspaceId: string, tx?: Tx). Provides workspace-scoped get, getAll, create, update, delete, and exists using @ctrlplane/db query helpers and optional Tx or global db client.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant R as DbResourceRepository
  participant TX as Tx (optional)
  participant DB as db (default)

  rect rgba(220,230,241,0.8)
    note right of R: Workspace-scoped CRUD & exists
    C->>R: create/get/getAll/update/delete/exists
    alt tx provided
      R->>TX: execute query (scoped by workspaceId)
      TX-->>R: row(s) / boolean
    else no tx
      R->>DB: execute query (scoped by workspaceId)
      DB-->>R: row(s) / boolean
    end
    R-->>C: result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

I hop through rows and code so bright,
A workspace burrow nested right,
CRUD carrots buried in my stead,
Tx may join or leave the bed,
I nibble bytes and bound with delight 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b0b0c4d and 20ae829.

📒 Files selected for processing (1)
  • apps/event-queue/src/repository/repository.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch basic-ts-rpo

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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 (6)
apps/event-queue/src/repository/repository.ts (4)

3-10: Unify return types with a MaybePromise alias (or standardize on Promises).

Unioning Promise | T across all methods complicates consumption. Introduce a MaybePromise alias to improve readability (or consider making the interface fully async).

Apply this diff:

-type Entity = { id: string };
+export type Entity = { id: string };
+export type MaybePromise<T> = T | Promise<T>;
 
 export interface Repository<T extends Entity> {
-  get(id: string): Promise<T | null> | T | null;
-  getAll(): Promise<T[]> | T[];
-  create(entity: T): Promise<T> | T;
-  update(entity: T): Promise<T> | T;
-  delete(id: string): Promise<T | null> | T | null;
-  exists(id: string): Promise<boolean> | boolean;
+  get(id: string): MaybePromise<T | null>;
+  getAll(): MaybePromise<T[]>;
+  create(entity: T): MaybePromise<T>;
+  update(entity: T): MaybePromise<T>;
+  delete(id: string): MaybePromise<T | null>;
+  exists(id: string): MaybePromise<boolean>;
 }

1-2: Export Entity for reuse.

External modules may benefit from reusing the Entity constraint; export it explicitly.

Apply the first hunk of the previous diff (exporting Entity).


25-29: Avoid non-null assertion and improve typing in update().

Return the value you just assigned to eliminate the non-null assertion and keep typing precise.

Apply this diff:

-  update(entity: T): T {
-    const existing = this.entities[entity.id] ?? {};
-    this.entities[entity.id] = { ...existing, ...entity };
-    return this.entities[entity.id]!;
-  }
+  update(entity: T): T {
+    const existing = (this.entities[entity.id] ?? {}) as Partial<T>;
+    const updated = { ...existing, ...entity } as T;
+    this.entities[entity.id] = updated;
+    return updated;
+  }

12-21: Optional: prefer Map over Record for the in-memory store.

Map avoids prototype pitfalls, offers clearer intent for key/value stores, and provides better iteration semantics.

If you decide to switch later, I can provide a targeted diff to convert Record<string, T> usage to Map<string, T>.

apps/event-queue/src/repository/db-resource-repository.ts (2)

9-15: Optional: clarify transaction usage in the constructor docstring.

The tx param is great. Consider noting that when omitted it uses the shared db client; callers should pass tx for atomic multi-step operations.

Happy to add a brief JSDoc explaining behavior and example usage with a Tx.


9-64: Schema/indexing note: ensure uniqueness expectations match scoping.

If resource.id is only unique per-workspace (not globally), enforce a composite unique index on (workspaceId, id). If id is globally unique, the additional workspace filter remains a defense-in-depth guard.

Would you like me to draft a migration for the appropriate unique index based on your schema expectations?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between de6c9ca and f1deb53.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • apps/event-queue/package.json (1 hunks)
  • apps/event-queue/src/repository/db-resource-repository.ts (1 hunks)
  • apps/event-queue/src/repository/repository.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (CLAUDE.md)

Formatting: Prettier is used with @ctrlplane/prettier-config

Files:

  • apps/event-queue/package.json
  • apps/event-queue/src/repository/db-resource-repository.ts
  • apps/event-queue/src/repository/repository.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with explicit types (prefer interfaces for public APIs)
Import styles: Use named imports, group imports by source (std lib > external > internal)
Consistent type imports: import type { Type } from "module"
Prefer async/await over raw promises
Handle errors explicitly (use try/catch and typed error responses)

Files:

  • apps/event-queue/src/repository/db-resource-repository.ts
  • apps/event-queue/src/repository/repository.ts

⚙️ CodeRabbit configuration file

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

Files:

  • apps/event-queue/src/repository/db-resource-repository.ts
  • apps/event-queue/src/repository/repository.ts
🧬 Code graph analysis (2)
apps/event-queue/src/repository/db-resource-repository.ts (3)
apps/event-queue/src/repository/repository.ts (1)
  • Repository (3-10)
packages/db/src/common.ts (3)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
  • takeFirst (9-13)
packages/db/src/client.ts (1)
  • db (15-15)
apps/event-queue/src/repository/repository.ts (1)
apps/workspace-engine/pkg/model/types.go (1)
  • Entity (5-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (1)
apps/event-queue/src/repository/db-resource-repository.ts (1)

17-34: LGTM: read operations are correctly scoped to workspace.

get() and getAll() constrain by workspaceId. Good guardrails for multi-tenant safety.

Comment on lines +42 to +49
async update(entity: schema.Resource) {
return this.db
.update(schema.resource)
.set(entity)
.where(eq(schema.resource.id, entity.id))
.returning()
.then(takeFirst);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Scope update() to workspace and prevent workspaceId/id drift.

  • Current WHERE only matches by id, which can update a record in a different workspace.
  • .set(entity) may overwrite workspaceId (or attempt to update id).

Constrain by workspaceId and force workspaceId in the SET payload; avoid updating id.

Apply this diff:

-  async update(entity: schema.Resource) {
-    return this.db
+  async update(entity: schema.Resource) {
+    const { id, workspaceId: _ignored, ...rest } = entity;
+    return this.db
       .update(schema.resource)
-      .set(entity)
-      .where(eq(schema.resource.id, entity.id))
+      .set({ ...rest, workspaceId: this.workspaceId })
+      .where(
+        and(
+          eq(schema.resource.id, id),
+          eq(schema.resource.workspaceId, this.workspaceId),
+        ),
+      )
       .returning()
       .then(takeFirst);
   }
📝 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.

Suggested change
async update(entity: schema.Resource) {
return this.db
.update(schema.resource)
.set(entity)
.where(eq(schema.resource.id, entity.id))
.returning()
.then(takeFirst);
}
async update(entity: schema.Resource) {
const { id, workspaceId: _ignored, ...rest } = entity;
return this.db
.update(schema.resource)
.set({ ...rest, workspaceId: this.workspaceId })
.where(
and(
eq(schema.resource.id, id),
eq(schema.resource.workspaceId, this.workspaceId),
),
)
.returning()
.then(takeFirst);
}
🤖 Prompt for AI Agents
In apps/event-queue/src/repository/db-resource-repository.ts around lines 42 to
49, the update() currently matches only by id and blindly .set(entity), which
can modify/attempt to modify id or update a row in another workspace; change the
WHERE to include workspaceId (e.g., where id = entity.id AND workspaceId =
entity.workspaceId), ensure the SET payload never contains id (omit id from the
object being set) and explicitly force workspaceId in the SET payload to
entity.workspaceId so workspaceId cannot drift.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ID is globally unique in our postgres db

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Thank you for that clarification! If IDs are globally unique in your PostgreSQL database, then you're absolutely right - there's no risk of updating records across different workspaces when matching by ID alone.

My suggestion to add workspace scoping to the WHERE clause is unnecessary overhead in your case. The current approach with eq(schema.resource.id, entity.id) is perfectly safe.

The only remaining consideration would be preventing accidental workspaceId changes in the SET payload, but given your database design, this is more of a defensive programming practice than a critical issue.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#664
File: apps/event-queue/src/repository/db-resource-repository.ts:42-49
Timestamp: 2025-08-26T20:10:10.042Z
Learning: In the ctrlplane codebase, resource IDs are globally unique across the entire PostgreSQL database, so workspace scoping in WHERE clauses is not necessary when querying by ID.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +50 to +56
async delete(id: string) {
return this.db
.delete(schema.resource)
.where(eq(schema.resource.id, id))
.returning()
.then(takeFirstOrNull);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Scope delete() to workspace.

Deleting by id without workspace scoping can remove records across tenants.

Apply this diff:

   async delete(id: string) {
     return this.db
       .delete(schema.resource)
-      .where(eq(schema.resource.id, id))
+      .where(
+        and(
+          eq(schema.resource.id, id),
+          eq(schema.resource.workspaceId, this.workspaceId),
+        ),
+      )
       .returning()
       .then(takeFirstOrNull);
   }
📝 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.

Suggested change
async delete(id: string) {
return this.db
.delete(schema.resource)
.where(eq(schema.resource.id, id))
.returning()
.then(takeFirstOrNull);
}
async delete(id: string) {
return this.db
.delete(schema.resource)
.where(
and(
eq(schema.resource.id, id),
eq(schema.resource.workspaceId, this.workspaceId),
),
)
.returning()
.then(takeFirstOrNull);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ID is globally unique

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Thank you for the clarification! You're absolutely right. Since resource IDs are globally unique across the entire database, workspace scoping in the WHERE clause is indeed unnecessary for the delete() method. My suggestion was based on the incorrect assumption that IDs might only be unique within workspace boundaries.

The current implementation is correct as-is - querying by a globally unique ID will never accidentally affect resources from other workspaces.

I appreciate the correction and will keep this architectural decision in mind for future reviews of this codebase.

🐰
✨ Ah, learning something new!
( ◡ ◡ )
\ ▽ /
-----


🧠 Learnings used
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#664
File: apps/event-queue/src/repository/db-resource-repository.ts:42-49
Timestamp: 2025-08-26T20:10:10.042Z
Learning: In the ctrlplane codebase, resource IDs are globally unique across the entire PostgreSQL database, so workspace scoping in WHERE clauses is not necessary when querying by ID.

Comment on lines +57 to +64
async exists(id: string) {
return this.db
.select()
.from(schema.resource)
.where(eq(schema.resource.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Scope exists() to workspace and avoid selecting all columns.

exists() should be tenant-safe; also prefer selecting a minimal column.

Apply this diff:

   async exists(id: string) {
     return this.db
-      .select()
+      .select({ id: schema.resource.id })
       .from(schema.resource)
-      .where(eq(schema.resource.id, id))
+      .where(
+        and(
+          eq(schema.resource.id, id),
+          eq(schema.resource.workspaceId, this.workspaceId),
+        ),
+      )
       .then(takeFirstOrNull)
       .then((r) => r != null);
   }
📝 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.

Suggested change
async exists(id: string) {
return this.db
.select()
.from(schema.resource)
.where(eq(schema.resource.id, id))
.then(takeFirstOrNull)
.then((r) => r != null);
}
async exists(id: string) {
return this.db
.select({ id: schema.resource.id })
.from(schema.resource)
.where(
and(
eq(schema.resource.id, id),
eq(schema.resource.workspaceId, this.workspaceId),
),
)
.then(takeFirstOrNull)
.then((r) => r != null);
}
🤖 Prompt for AI Agents
In apps/event-queue/src/repository/db-resource-repository.ts around lines 57 to
64, the exists() query currently selects all columns and is not scoped to the
workspace/tenant; update it to only select the minimal column (resource id) and
add a where clause restricting results to the current workspace (e.g., where
eq(schema.resource.workspaceId, this.workspaceId) or the appropriate workspace
id field on the resource), then return whether a row exists — this makes the
check tenant-safe and avoids fetching unnecessary columns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ID is globally unique

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26, thank you for the clarification! You're absolutely right - I have this noted in my knowledge base from our previous interaction that resource IDs are globally unique across the entire PostgreSQL database, so workspace scoping is not necessary when querying by ID.

My suggestion to add workspace scoping to the exists() method was incorrect. The current implementation is already correct in terms of scoping. The only potential optimization would be selecting minimal columns (select({ id: schema.resource.id }) instead of select()), but that's a minor performance consideration and not a correctness issue.

(つ˶ᵔ ᵕ ᵔ˶)つ curious rabbit learning


🧠 Learnings used
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#601
File: packages/job-dispatch/src/job-update.ts:264-270
Timestamp: 2025-06-24T23:56:54.799Z
Learning: In this codebase, the `Tx` type is just an alias for the database client type (`Omit<typeof db, "$client">`) and does not necessarily indicate an active transaction context. Functions like `createReleaseJob` need to be called within a transaction, which is why they are wrapped with `db.transaction()` even when the parameter is typed as `Tx`. Drizzle supports nested transactions via breakpoints, so additional transaction wrappers are safe even if already within a transaction.

Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#664
File: apps/event-queue/src/repository/db-resource-repository.ts:42-49
Timestamp: 2025-08-26T20:10:10.042Z
Learning: In the ctrlplane codebase, resource IDs are globally unique across the entire PostgreSQL database, so workspace scoping in WHERE clauses is not necessary when querying by ID.

@adityachoudhari26 adityachoudhari26 merged commit f446f53 into main Aug 27, 2025
2 of 10 checks passed
@adityachoudhari26 adityachoudhari26 deleted the basic-ts-rpo branch August 27, 2025 18:07
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.

2 participants