Skip to content

refactor: retrieve jobs using new method if engine flag is enabled#500

Merged
adityachoudhari26 merged 9 commits intomainfrom
refactor-job-route
Apr 21, 2025
Merged

refactor: retrieve jobs using new method if engine flag is enabled#500
adityachoudhari26 merged 9 commits intomainfrom
refactor-job-route

Conversation

@adityachoudhari26
Copy link
Copy Markdown
Member

@adityachoudhari26 adityachoudhari26 commented Apr 20, 2025

Summary by CodeRabbit

  • New Features
    • Enhanced job retrieval supporting both legacy and new policy engine modes with detailed job data including variables, approvals, and enriched resource metadata.
  • Improvements
    • Streamlined job fetching for improved performance and maintainability.
    • Updated error messages for missing jobs to be clearer.
    • Improved handling and display of job-related data across different policy engine environments.
  • Refactor
    • Internal logic updated to conditionally support legacy and new policy engine data models without altering public APIs.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 20, 2025

"""

Walkthrough

This update introduces a modular approach to retrieving job details by splitting the logic into two distinct modules: one for legacy jobs and another for jobs using the new policy engine. The API route handler now delegates job fetching to these modules by concurrently calling both and returning the first non-null result. Database schema relations are updated to support the new job structure, and authorization utilities are adjusted to handle both legacy and new job models. Variable naming and sorting logic are refined in deployment version job processing to ensure robustness when handling job lists and missing data.

Changes

File(s) Change Summary
apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts
apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts
Added two new modules for fetching detailed job information: getLegacyJob queries legacy job-related tables with joins, approval details, and variable decryption; getNewEngineJob queries new engine job-related tables with nested relations, decryption, and error handling.
apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts Refactored the GET handler to concurrently call getLegacyJob and getNewEngineJob, returning the first found job or a 404 response; removed complex inline SQL and related helper functions; updated authorization to await job ID before permission check.
packages/db/src/schema/job.ts Added a one-to-one relation releaseJob linking job.id to releaseJob.jobId to support new job queries and relations.
packages/api/src/router/deployment-version-jobs.ts Renamed variables for clarity (jobs to releaseJobs) and improved sorting logic by safely accessing job statuses with optional chaining and default values to handle empty job lists gracefully.
packages/auth/src/utils/rbac.ts Introduced two separate helper functions, legacyJobScopes and newJobScopes, for retrieving job scopes via distinct SQL join paths; updated getJobScopes to concurrently run both and prefer new job scopes if available, falling back to legacy scopes otherwise.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API_Route
    participant LegacyJobModule
    participant NewEngineJobModule
    participant DB

    Client->>API_Route: GET /api/v1/jobs/:jobId
    API_Route->>LegacyJobModule: getLegacyJob(db, jobId)
    API_Route->>NewEngineJobModule: getNewEngineJob(db, jobId)
    LegacyJobModule->>DB: Query legacy job tables with joins
    NewEngineJobModule->>DB: Query new engine job tables with nested relations
    DB-->>LegacyJobModule: Legacy job data or null
    DB-->>NewEngineJobModule: New engine job data or null
    alt New engine job found
        NewEngineJobModule-->>API_Route: Job object
    else Legacy job found
        LegacyJobModule-->>API_Route: Job object
    else No job found
        API_Route-->>Client: 404 Job not found
    end
    API_Route-->>Client: JSON response with job data or error
Loading

Possibly related PRs

Suggested reviewers

  • jsbroks

Poem

In the warren of code, two tunnels appear,
For legacy jobs and new engines, both clear.
With joins and decryptions, the data flows right,
Approvals and metadata now come into sight.
Relations are mapped, and the queries refined—
This rabbit hops onward, with robust jobs in mind!
🐇✨
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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.

Documentation and Community

  • 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
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: 5

🧹 Nitpick comments (1)
apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts (1)

52-60: Micro‑optimisation: avoid recreating the AES helper in a tight loop

variablesAES256() is instantiated for every variable value. Initialising it once improves readability and avoids unnecessary work:

-const jobVariables = Object.fromEntries(
-  values.map(({ variableValueSnapshot }) => {
+const crypto = variablesAES256();
+const jobVariables = Object.fromEntries(
+  values.map(({ variableValueSnapshot }) => {
     const { key, value, sensitive } = variableValueSnapshot;
     const strval = String(value);
-    const resolvedValue = sensitive
-      ? variablesAES256().decrypt(strval)
-      : strval;
+    const resolvedValue = sensitive ? crypto.decrypt(strval) : strval;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e66372 and d95e178.

📒 Files selected for processing (4)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts (1 hunks)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts (1 hunks)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (2 hunks)
  • packages/db/src/schema/job.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{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...

**/*.{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.

  • packages/db/src/schema/job.ts
  • apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts
  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts
  • apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts
🧬 Code Graph Analysis (3)
packages/db/src/schema/job.ts (1)
packages/db/src/schema/release.ts (1)
  • releaseJob (126-134)
apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts (5)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (1)
  • Tx (22-22)
packages/db/src/schema/job.ts (1)
  • job (75-107)
packages/db/src/schema/release.ts (6)
  • releaseJob (126-134)
  • release (113-124)
  • versionRelease (46-60)
  • variableSetRelease (62-70)
  • variableValueSnapshot (93-111)
  • releaseTarget (20-42)
packages/db/src/schema/resource.ts (1)
  • resource (59-87)
apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts (3)
packages/db/src/client.ts (1)
  • db (15-15)
packages/db/src/common.ts (2)
  • Tx (22-22)
  • takeFirstOrNull (15-20)
packages/db/src/schema/deployment-version.ts (1)
  • deploymentVersion (109-136)
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Typecheck
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (2)
packages/db/src/schema/job.ts (1)

51-51: Potential circular‑import trap between job.ts and release.ts

job.ts now imports releaseJob while release.ts already imports job. In ESM/TS this creates a bi‑directional module cycle.
Because the Drizzle table constants are evaluated eagerly at module top‑level, whichever side is evaluated first will find the other side’s constant still undefined, which can surface as TypeError: Cannot read properties of undefined (reading "jobId") at runtime or during migrations.

Please verify the load order in your build/testing environment and, if necessary, break the cycle (e.g. move cross‑referenced tables into a single module or use late binding with lazy/function accessors).

apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts (1)

81-82: ⚠️ Potential issue

Resource filter unintentionally discards jobs without a resource

isNull(schema.resource.deletedAt) converts the LEFT JOIN with resource into an INNER‐like filter, because the predicate evaluates to NULL (i.e. false) when the entire row is absent.
Jobs that legitimately have no resource will therefore fall through and be treated as “not found”.

-.where(and(eq(schema.job.id, jobId), isNull(schema.resource.deletedAt)));
+.where(
+  and(
+    eq(schema.job.id, jobId),
+    or(isNull(schema.resource.id), isNull(schema.resource.deletedAt)),
+  ),
+);

Likely an incorrect or invalid review comment.

Comment on lines +117 to +120
releaseJob: one(releaseJob, {
fields: [job.id],
references: [releaseJob.jobId],
}),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Cardinality mismatch: one() may be wrong when job_id is not unique

release_job.job_id is not declared UNIQUE in release.ts, meaning a job could, in theory, appear in multiple rows. Declaring the relation as one(releaseJob, …) assumes a 1‑to‑1 mapping and Drizzle will only return the first matching record, silently discarding the rest.

If the business rule truly enforces uniqueness, add a uniqueIndex().on(releaseJob.jobId) in release.ts; otherwise, change this to many(releaseJob, …) so that no data is lost.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

in the new system, a release job should only ever point to one job and vice versa

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Thanks for the clarification! That makes perfect sense - a one-to-one relationship between job and release job is correctly represented with the one(releaseJob, ...) relation. The implementation is aligned with your intended design.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#500
File: packages/db/src/schema/job.ts:117-120
Timestamp: 2025-04-21T18:34:54.740Z
Learning: In the system's job schema, the relationship between job and releaseJob is a true one-to-one relationship - a release job should only ever point to one job and vice versa. The implementation uses `one(releaseJob, ...)` in the jobRelations to reflect this business rule.

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 +45 to +61
if (jobResult == null) return null;

const { releaseJob, ...job } = jobResult;
const { release } = releaseJob;
const { versionRelease, variableSetRelease } = release;
const { version, releaseTarget } = versionRelease;

const { values } = variableSetRelease;
const jobVariables = Object.fromEntries(
values.map(({ variableValueSnapshot }) => {
const { key, value, sensitive } = variableValueSnapshot;
const strval = String(value);
const resolvedValue = sensitive
? variablesAES256().decrypt(strval)
: strval;
return [key, resolvedValue];
}),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 20, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null‑safety: crashes when a job lacks a releaseJob

releaseJob is destructured without a guard:

const { releaseJob, ...job } = jobResult;
const { release } = releaseJob;   // ← will throw if releaseJob === null

Any job inserted before the new engine rollout – or future edge‑cases – will trigger a runtime TypeError.

-const { releaseJob, ...job } = jobResult;
-if (releaseJob == null) return null;   // or build a sane fallback
-const { release } = releaseJob;
+const { releaseJob, ...job } = jobResult;
+if (releaseJob == null) return null; // early‑return so callers treat it as “not found”
+const { release } = releaseJob;

Apply the same defensive checks for variableSetRelease, versionRelease, resource, and resource.metadata to avoid similar crashes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

tbh this should fail if there is no release job for a job, that mens something went wrong. I wrapped this in a try catch to gracefully return null, but I would want that to error because it means something is wrong

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 I understand your approach now. You've implemented a pragmatic error handling pattern:

  1. Let the code fail internally if invariants are violated (like a job without a releaseJob)
  2. Catch those errors at the function boundary
  3. Log them for visibility and debugging
  4. Return null to the caller

This is a reasonable pattern that balances code readability (avoiding excessive null checks) while maintaining API stability. The try-catch you've added will indeed catch and log any TypeError that occurs if releaseJob is null, making it easier to detect and investigate unexpected data states.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#500
File: apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts:0-0
Timestamp: 2025-04-21T18:47:47.613Z
Learning: In the job API endpoints, it's acceptable to let internal code fail with TypeErrors when encountering unexpected null values (like missing releaseJob) as long as the errors are caught at the function boundary, logged for debugging, and a consistent response is returned to the caller.

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 +25 to +37
// eslint-disable-next-line no-restricted-properties
const isUsingNewEngine = process.env.ENABLE_NEW_POLICY_ENGINE === "true";
const job = isUsingNewEngine
? await getNewEngineJob(db, params.jobId)
: await getLegacyJob(db, params.jobId);

if (row == null)
if (job == null)
return NextResponse.json(
{ error: "Job execution not found." },
{ status: 404 },
{ error: "Job not found" },
{ status: NOT_FOUND },
);

const deploymentVersion =
row.deployment_version != null
? { ...row.deployment_version, metadata: {} }
: null;

const je = {
job: row.job,
runbook: row.runbook,
environment: row.environment,
resource: row.resource,
deployment: row.deployment,
deploymentVersion,
};

const policyId = je.environment?.policyId;

const approval =
je.deploymentVersion?.id && policyId
? await getApprovalDetails(je.deploymentVersion.id, policyId)
: undefined;

const jobVariableRows = await db
.select()
.from(schema.jobVariable)
.where(eq(schema.jobVariable.jobId, params.jobId));

const variables = Object.fromEntries(
jobVariableRows.map((v) => {
const strval = String(v.value);
const value = v.sensitive ? variablesAES256().decrypt(strval) : strval;
return [v.key, value];
}),
);

const jobWithVariables = {
...je,
variables,
release:
je.deploymentVersion != null
? { ...je.deploymentVersion, version: je.deploymentVersion.tag }
: { version: undefined },
};
if (je.resource == null) return NextResponse.json(jobWithVariables);

const metadata = await db
.select()
.from(schema.resourceMetadata)
.where(eq(schema.resourceMetadata.resourceId, je.resource.id))
.then((rows) => Object.fromEntries(rows.map((m) => [m.key, m.value])));

return NextResponse.json({
...jobWithVariables,
resource: { ...jobWithVariables.resource, metadata },
approval,
});
return NextResponse.json(job);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Route must align with helper’s return contract

getLegacyJob may now return null (per earlier comment) rather than a pre‑built Response.
Add a type‑guard so the handler doesn’t inadvertently call NextResponse.json() on a Response object, and propagate the 404 correctly:

-const job = isUsingNewEngine
-  ? await getNewEngineJob(db, params.jobId)
-  : await getLegacyJob(db, params.jobId);
-
-if (job == null)
+const jobData = isUsingNewEngine
+  ? await getNewEngineJob(db, params.jobId)
+  : await getLegacyJob(db, params.jobId);
+
+if (jobData == null)
   return NextResponse.json(
     { error: "Job not found" },
     { status: NOT_FOUND },
   );
-
-return NextResponse.json(job);
+return NextResponse.json(jobData);

This keeps the HTTP semantics consistent across the feature‑flag branches.

📝 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
// eslint-disable-next-line no-restricted-properties
const isUsingNewEngine = process.env.ENABLE_NEW_POLICY_ENGINE === "true";
const job = isUsingNewEngine
? await getNewEngineJob(db, params.jobId)
: await getLegacyJob(db, params.jobId);
if (row == null)
if (job == null)
return NextResponse.json(
{ error: "Job execution not found." },
{ status: 404 },
{ error: "Job not found" },
{ status: NOT_FOUND },
);
const deploymentVersion =
row.deployment_version != null
? { ...row.deployment_version, metadata: {} }
: null;
const je = {
job: row.job,
runbook: row.runbook,
environment: row.environment,
resource: row.resource,
deployment: row.deployment,
deploymentVersion,
};
const policyId = je.environment?.policyId;
const approval =
je.deploymentVersion?.id && policyId
? await getApprovalDetails(je.deploymentVersion.id, policyId)
: undefined;
const jobVariableRows = await db
.select()
.from(schema.jobVariable)
.where(eq(schema.jobVariable.jobId, params.jobId));
const variables = Object.fromEntries(
jobVariableRows.map((v) => {
const strval = String(v.value);
const value = v.sensitive ? variablesAES256().decrypt(strval) : strval;
return [v.key, value];
}),
);
const jobWithVariables = {
...je,
variables,
release:
je.deploymentVersion != null
? { ...je.deploymentVersion, version: je.deploymentVersion.tag }
: { version: undefined },
};
if (je.resource == null) return NextResponse.json(jobWithVariables);
const metadata = await db
.select()
.from(schema.resourceMetadata)
.where(eq(schema.resourceMetadata.resourceId, je.resource.id))
.then((rows) => Object.fromEntries(rows.map((m) => [m.key, m.value])));
return NextResponse.json({
...jobWithVariables,
resource: { ...jobWithVariables.resource, metadata },
approval,
});
return NextResponse.json(job);
// eslint-disable-next-line no-restricted-properties
const isUsingNewEngine = process.env.ENABLE_NEW_POLICY_ENGINE === "true";
const jobData = isUsingNewEngine
? await getNewEngineJob(db, params.jobId)
: await getLegacyJob(db, params.jobId);
if (jobData == null)
return NextResponse.json(
{ error: "Job not found" },
{ status: NOT_FOUND },
);
return NextResponse.json(jobData);

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: 0

🧹 Nitpick comments (1)
e2e/tests/api/jobs.spec.ts (1)

66-124: Consider refactoring to reduce duplication between tests.

There's significant code duplication between the two test cases. The only differences are the test name, job name, and engine type ("new" vs "legacy"), while the structure, assertions, and error handling are identical.

Consider refactoring to a parameterized test approach:

-test("should create and retrieve a job with new engine", async ({ api }) => {
+const testCases = [
+  { engineType: "new", jobName: "test-job" },
+  { engineType: "legacy", jobName: "test-job-legacy" }
+];
+
+for (const { engineType, jobName } of testCases) {
+  test(`should create and retrieve a job with ${engineType} engine`, async ({ api }) => {
     // Create a job
     const createResponse = await api.POST(
       "/v1/jobs" as PathsWithMethod<paths, "post">,
       {
         body: {
           deploymentId: "test-deployment",
-          name: "test-job",
+          name: jobName,
           status: "ready",
           versionSelector: {
             version: "1.0.0",
           },
           jobAgentConfig: {
-            engine: "new",
+            engine: engineType,
           },
         },
       },
     );
   
     // ... rest of the test ...
-    expect(jobData.jobAgentConfig.engine).toBe("new");
+    expect(jobData.jobAgentConfig.engine).toBe(engineType);
     
     // ... rest of the test ...
   
-    expect(retrievedJob.jobAgentConfig.engine).toBe("new");
+    expect(retrievedJob.jobAgentConfig.engine).toBe(engineType);
     
     // ... rest of the test ...
-  });
+  });
+}
-
-test("should create and retrieve a job with legacy engine", async ({ api }) => {
-  // ... duplicate code ...
-});

This approach would reduce code duplication while still testing both engine types thoroughly.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d95e178 and 6f71bd4.

📒 Files selected for processing (1)
  • e2e/tests/api/jobs.spec.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{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...

**/*.{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.

  • e2e/tests/api/jobs.spec.ts
🧬 Code Graph Analysis (1)
e2e/tests/api/jobs.spec.ts (1)
e2e/tests/fixtures.ts (1)
  • test (11-26)
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (4)
e2e/tests/api/jobs.spec.ts (4)

1-5: Imports and setup look good.

The imports are concise and appropriate, using typed API paths from OpenAPI definitions for request type safety, which is a good practice.


6-64: Well-structured test for the new engine job.

This test effectively validates both job creation and retrieval with the new engine. I particularly like:

  • Strong typing with PathsWithMethod for API routes
  • Thorough error handling for unexpected responses
  • Comprehensive assertions checking all relevant job properties
  • Clear separation between creation and retrieval steps

The implementation correctly tests the new job engine functionality according to the PR objectives.


26-28: Good error handling for API responses.

The code properly checks for unexpected response structures before proceeding with assertions, which helps prevent cryptic test failures and provides clearer error messages when API responses don't match expected formats.

Also applies to: 50-56, 86-88, 110-116


8-23: Well-typed API requests using OpenAPI schema.

Using PathsWithMethod<paths, "post"> and PathsWithMethod<paths, "get"> ensures that API requests conform to the defined schema, providing compile-time type safety and reducing the risk of invalid requests.

Also applies to: 38-47, 68-83, 98-107

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

🧹 Nitpick comments (2)
packages/auth/src/utils/rbac.ts (2)

342-358: Soft‑deletion filter is applied only to resource – consider extending it or documenting why other entities are exempt

legacyJobScopes guards against orphaned/soft‑deleted resources with isNull(resource.deletedAt) – good!
However, similar soft‑delete columns often exist on environment, deploymentVersion, etc. If they do exist in your schema, missing filters could surface unexpected rows and leak data. Either:

  1. Extend the and(...) predicate to cover every entity that supports soft deletes, or
  2. Add a comment explaining that only resource.deletedAt is relevant here.

Keeping the rule consistent across queries makes later maintenance safer.


401-407: Environment‑flag check can be more robust & self‑documenting

process.env.ENABLE_NEW_POLICY_ENGINE === "true" works, but it:

  • Fails silently for "TRUE", "1", etc.
  • Reads the env var every call (minor perf hit, but mostly readability).

Consider extracting a single constant at module scope:

const IS_NEW_ENGINE_ENABLED = /^true|1$/i.test(
  process.env.ENABLE_NEW_POLICY_ENGINE ?? "",
);

and reuse it inside getJobScopes.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 568ce45 and 59877f4.

📒 Files selected for processing (2)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (1 hunks)
  • packages/auth/src/utils/rbac.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{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...

**/*.{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.

  • packages/auth/src/utils/rbac.ts
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Typecheck
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)

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

🧹 Nitpick comments (1)
apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (1)

27-30: Consider sequential retrieval for improved efficiency.

While concurrent retrieval using Promise.all is performant, it always executes both functions even if the first one succeeds. Consider a sequential approach that only falls back to legacy if new engine returns null.

-      const [newEngine, legacy] = await Promise.all([
-        getNewEngineJob(db, jobId),
-        getLegacyJob(db, jobId),
-      ]);
-
-      const job = newEngine ?? legacy;
+      // Try new engine first, fall back to legacy if needed
+      const newEngine = await getNewEngineJob(db, jobId);
+      const job = newEngine ?? await getLegacyJob(db, jobId);
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1af2859 and da1be9f.

📒 Files selected for processing (2)
  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (1 hunks)
  • packages/auth/src/utils/rbac.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/auth/src/utils/rbac.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{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...

**/*.{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.

  • apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts
🧬 Code Graph Analysis (1)
apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (5)
apps/webservice/src/app/api/v1/middleware.ts (1)
  • request (24-53)
apps/webservice/src/app/api/v1/auth.ts (2)
  • authn (15-21)
  • authz (23-58)
apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts (1)
  • getNewEngineJob (13-90)
packages/db/src/client.ts (1)
  • db (15-15)
apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts (1)
  • getLegacyJob (48-141)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (4)
apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts (4)

3-3: Good use of HTTP status constants.

Using the NOT_FOUND constant from http-status improves code readability and maintainability compared to using numeric status codes directly.


12-13: Good modularization of job retrieval logic.

The separation of job retrieval into specialized helper functions (legacy and new engine) improves code organization and maintainability.


18-20: Properly awaiting parameters for authorization.

Good improvement to ensure the jobId is properly extracted from params before performing permission checks.


32-37: Proper null handling and standardized error response.

The code correctly handles the case where both job retrieval methods return null and provides a clear error message with the appropriate status code.

Comment on lines +24 to +39
async ({ db }, { params }) => {
const { jobId } = await params;

const [newEngine, legacy] = await Promise.all([
getNewEngineJob(db, jobId),
getLegacyJob(db, jobId),
]);

const job = newEngine ?? legacy;
if (job == null)
return NextResponse.json(
{ error: "Job not found" },
{ status: NOT_FOUND },
);

return NextResponse.json(job);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 21, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify error handling in getLegacyJob function.

While the getNewEngineJob function has error handling that returns null on exceptions, getLegacyJob doesn't appear to have similar protection. This could lead to unhandled exceptions being propagated to this handler.


🏁 Script executed:

#!/bin/bash
# Check if getLegacyJob has error handling
ast-grep --pattern $'export const getLegacyJob = async (db: $_) {
  $$$
  try {
    $$$
  } catch {
    $$$
  }
  $$$
}'

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Locate getLegacyJob implementation with surrounding context
rg -n "getLegacyJob" -C5

Length of output: 2849


🏁 Script executed:

#!/bin/bash
# Extract the full getLegacyJob function to check for error handling
sed -n '/export const getLegacyJob/,/^}$/p' 'apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts'

Length of output: 2647


🏁 Script executed:

#!/bin/bash
# Extract the full getNewEngineJob function to inspect its error handling
sed -n '/export const getNewEngineJob/,/^};/p' 'apps/webservice/src/app/api/v1/jobs/[jobId]/new-engine-job.ts'

Length of output: 2373


Add error handling to getLegacyJob to mirror getNewEngineJob

The getLegacyJob function currently has no try/catch guard, so any database or decryption error will bubble up to the route handler instead of returning null. Wrap its entire body in a try/catch and return null on failure:

• File:
apps/webservice/src/app/api/v1/jobs/[jobId]/legacy-job.ts

• Around line 48 (the start of export const getLegacyJob), update to:

 export const getLegacyJob = async (db: Tx, jobId: string) => {
+  try {
     const rows = await db
       .select()
       .from(schema.job)
       …  // existing joins and logic
     return { … };  // your current return value
+  } catch (error) {
+    log.error("Error getting legacy job", { error });
+    return null;
+  }
 };

This ensures that any unexpected errors result in null—letting your route handler correctly fall back and return a 404 instead of crashing.

📝 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 ({ db }, { params }) => {
const { jobId } = await params;
const [newEngine, legacy] = await Promise.all([
getNewEngineJob(db, jobId),
getLegacyJob(db, jobId),
]);
const job = newEngine ?? legacy;
if (job == null)
return NextResponse.json(
{ error: "Job not found" },
{ status: NOT_FOUND },
);
return NextResponse.json(job);
export const getLegacyJob = async (db: Tx, jobId: string) => {
try {
const rows = await db
.select()
.from(schema.job)
// existing joins and logic
return {}; // your current return value
} catch (error) {
log.error("Error getting legacy job", { error });
return null;
}
};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the authz handler catches errors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 I see, thanks for clarifying. Since the authz handler already has error handling in place at the middleware level, and your coding guidelines mention flexibility in error handling approaches beyond try/catch blocks, my suggestion isn't necessary.

I appreciate you sharing this information about your error handling architecture. Thanks for the feedback!


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#500
File: apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts:24-39
Timestamp: 2025-04-21T20:24:11.545Z
Learning: In the ctrlplane project, the authz handler middleware provides error handling that catches errors from functions like `getLegacyJob`, making additional function-level try/catch blocks unnecessary in some cases.

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.

@adityachoudhari26 adityachoudhari26 merged commit 255b2d0 into main Apr 21, 2025
9 of 10 checks passed
@adityachoudhari26 adityachoudhari26 deleted the refactor-job-route branch April 21, 2025 20:25
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.

1 participant