Skip to content

Comments

refactor: convert insights services to use kysely#22166

Closed
eunjae-lee wants to merge 4 commits intomainfrom
devin/insights-services-kysely-1735668293
Closed

refactor: convert insights services to use kysely#22166
eunjae-lee wants to merge 4 commits intomainfrom
devin/insights-services-kysely-1735668293

Conversation

@eunjae-lee
Copy link
Contributor

@eunjae-lee eunjae-lee commented Jun 30, 2025

refactor: convert insights services to use kysely

Summary

This PR converts the InsightsBookingService and InsightsRoutingService from Prisma to Kysely ORM. This is a foundational refactor that maintains the same service interfaces while migrating the underlying database query implementation.

The changes include:

  • Service Migration: Both insightsBooking.ts and insightsRouting.ts now use Kysely for database queries instead of Prisma
  • Query Builder Refactor: Complex authorization and filtering logic has been rewritten using Kysely's query builder
  • Test Updates: Integration tests updated to work with the new Kysely-based implementation
  • Interface Preservation: Public service APIs remain unchanged to ensure compatibility with dependent features

This PR is part of a larger effort to split PR #22106 - this contains just the service layer changes, with the UI features to be stacked on top.

Review & Testing Checklist for Human

  • Verify query correctness: Compare Kysely query outputs with original Prisma queries to ensure identical results, especially for complex authorization filters
  • Test authorization logic: Verify user/team/org scoping works correctly for different user roles (owner, member) across all three scopes
  • Run integration tests: Execute TZ=UTC yarn test packages/lib/server/service/__tests__/insights* to ensure all test scenarios pass
  • Validate service interfaces: Confirm that service method signatures and return types remain unchanged for backward compatibility
  • Test edge cases: Verify behavior with invalid inputs, unauthorized users, and boundary conditions in the authorization logic

Recommended Test Plan:

  1. Set up test data with users in different org/team configurations
  2. Call service methods with different scope parameters (user/team/org) and user roles
  3. Verify that data filtering and authorization work as expected
  4. Compare results with the original Prisma implementation if possible

Diagram

%%{ init : { "theme" : "default" }}%%
graph TB
    subgraph "Insights Services"
        IBS["packages/lib/server/service/insightsBooking.ts"]:::major-edit
        IRS["packages/lib/server/service/insightsRouting.ts"]:::major-edit
    end
    
    subgraph "Integration Tests"
        IBT["packages/lib/server/service/__tests__/insightsBooking.integration-test.ts"]:::major-edit
        IRT["packages/lib/server/service/__tests__/insightsRouting.integration-test.ts"]:::major-edit
    end
    
    subgraph "Database Layer"
        Kysely["Kysely ORM"]:::context
        DB["Database"]:::context
    end
    
    subgraph "Dependent Features"
        Funnel["Funnel Chart Feature"]:::context
        Router["tRPC Router"]:::context
    end
    
    IBS --> Kysely
    IRS --> Kysely
    Kysely --> DB
    
    IBT --> IBS
    IRT --> IRS
    
    Router --> IBS
    Router --> IRS
    Funnel --> Router
    
    subgraph Legend
        L1["Major Edit"]:::major-edit
        L2["Minor Edit"]:::minor-edit  
        L3["Context/No Edit"]:::context
    end
    
    classDef major-edit fill:#90EE90
    classDef minor-edit fill:#87CEEB
    classDef context fill:#FFFFFF
Loading

Notes

⚠️ Important: This is a foundational change that other features depend on. Thorough testing of the authorization and query logic is essential before merging.

@vercel
Copy link

vercel bot commented Jun 30, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
cal ⬜️ Ignored (Inspect) Visit Preview Jun 30, 2025 7:27pm
cal-eu ⬜️ Ignored (Inspect) Visit Preview Jun 30, 2025 7:27pm

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@keithwillcode keithwillcode added consumer core area: core, team members only labels Jun 30, 2025
@delve-auditor
Copy link

delve-auditor bot commented Jun 30, 2025

No security or compliance issues detected. Reviewed everything up to 8fe9958.

Security Overview
  • 🔎 Scanned files: 4 changed file(s)
Detected Code Changes
Change Type Relevant files
Refactor ► insightsBooking.integration-test.ts
    Update tests to use Kysely instead of Prisma
► insightsRouting.integration-test.ts
    Update tests to use Kysely instead of Prisma
► insightsBooking.ts
    Migrate from Prisma to Kysely query builder
► insightsRouting.ts
    Migrate from Prisma to Kysely query builder

Reply to this PR with @delve-auditor followed by a description of what change you want and we'll auto-submit a change to this PR to implement it.

@github-actions
Copy link
Contributor

E2E results are ready!

@eunjae-lee eunjae-lee marked this pull request as ready for review June 30, 2025 19:32
@eunjae-lee eunjae-lee requested a review from hbjORbj June 30, 2025 19:32
@graphite-app graphite-app bot requested a review from a team June 30, 2025 19:32
devin-ai-integration bot added a commit that referenced this pull request Jun 30, 2025
- Remove insightsBooking.ts and insightsRouting.ts (now in PR #22166)
- Remove corresponding test files to eliminate duplication
- Funnel chart feature now depends on services from base branch

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
@dosubot dosubot bot added insights area: insights, analytics 💻 refactor labels Jun 30, 2025
@graphite-app
Copy link

graphite-app bot commented Jun 30, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (06/30/25)

1 reviewer was added to this PR based on Keith Williams's automation.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic found 7 issues across 4 files. Review them in cubic.dev

React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai to give specific feedback.

}

async buildAuthorizationConditions(): Promise<Prisma.BookingTimeStatusDenormalizedWhereInput> {
async buildAuthorizationConditions(): Promise<WhereCondition> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rule violated: Enforce Singular Naming for Single-Item Functions

  Function name is plural (`buildAuthorizationConditions`) but it returns a single `WhereCondition`, violating the "Enforce Singular Naming for Single-Item Functions" rule and misleading callers about the expected return.

}

async buildFilterConditions(): Promise<WhereCondition | null> {
if (!this.filters) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rule violated: Enforce Singular Naming for Single-Item Functions

  Function name is plural (`buildFilterConditions`) but it returns a single `WhereCondition` (or null), violating the "Enforce Singular Naming for Single-Item Functions" rule and potentially confusing callers about the return type.

) => ReturnType<ExpressionBuilder<DB, "BookingTimeStatusDenormalized">["and"]>
) {
const compiled = db.selectFrom("BookingTimeStatusDenormalized").selectAll().where(condition).compile();
const where = compiled.sql.replace(/^select \* from "BookingTimeStatusDenormalized" where /, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

The regex only matches a very specific, lowercase select * from pattern; if Kysely changes its SQL casing, inserts extra whitespace, or prefixes the table with a schema, the replacement will fail and the tests will break. Making the pattern case-insensitive and tolerant of whitespace would make the helper more robust.

Suggested change
const where = compiled.sql.replace(/^select \* from "BookingTimeStatusDenormalized" where /, "");
const where = compiled.sql.replace(/^select\s+\*\s+from\s+"BookingTimeStatusDenormalized"\s+where\s+/i, "");

) => ReturnType<ExpressionBuilder<DB, "RoutingFormResponseDenormalized">["and"]>
) {
const compiled = db.selectFrom("RoutingFormResponseDenormalized").selectAll().where(condition).compile();
const where = compiled.sql.replace(/^select \* from "RoutingFormResponseDenormalized" where /, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

The string replacement assumes the generated SQL will always have the exact prefix select * from "RoutingFormResponseDenormalized" where . This is fragile—any aliasing, whitespace change, capitalization difference, or addition (e.g., schema name, DISTINCT) will cause the regex not to match and the test will break even though the query is still correct.

Suggested change
const where = compiled.sql.replace(/^select \* from "RoutingFormResponseDenormalized" where /, "");
const where = (compiled.sql.split(" where ")[1] ?? "").trim();

eb.or([
eb.and([eb("teamId", "=", options.teamId), eb("isTeamBooking", "=", true)]),
eb.and([eb("userId", "in", userIdsFromTeam), eb("isTeamBooking", "=", false)]),
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

If userIdsFromTeam is an empty array, this produces an SQL IN () clause, which is invalid in most databases and will throw at runtime. Guard against the empty array before adding this condition.

Suggested change
eb.and([eb("userId", "in", userIdsFromTeam), eb("isTeamBooking", "=", false)]),
...(userIdsFromTeam.length > 0 ? [eb.and([eb("userId", "in", userIdsFromTeam), eb("isTeamBooking", "=", false)])] : []),


if (this.filters?.startDate) {
const startDate = this.filters.startDate;
conditions.push((eb) => eb("createdAt", ">=", new Date(startDate)));
Copy link
Contributor

Choose a reason for hiding this comment

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

Constructing dates directly from unchecked strings can yield "Invalid Date" values (e.g., when provided ISO with timezone or bad input) leading to SQL errors at runtime; input should be validated or parsed with zod before usage.

query() {
if (!this.baseWhereConditions) {
throw new Error("Service must be initialized before building base query. Call init() first.");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Public methods (e.g., getDropOffData) call query() without ensuring init() has run, so any consumer that forgets to call init() first will hit this runtime error – a breaking change from the previous Prisma service that required no explicit initialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

consumer core area: core, team members only insights area: insights, analytics ready-for-e2e 💻 refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants