Skip to content

Refactor/cleanup - #41

Merged
SteakFisher merged 5 commits into
mainfrom
refactor/cleanup
May 17, 2026
Merged

Refactor/cleanup#41
SteakFisher merged 5 commits into
mainfrom
refactor/cleanup

Conversation

@SteakFisher

@SteakFisher SteakFisher commented May 17, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Refactor

    • Reduced and consolidated exported error types and factory methods across services
    • Centralized checkout pricing into a new pricing service and simplified pricing handlers
    • Refactored event/query response construction and AI token usage ingestion paths
    • Simplified auth surface (removed a role-guard export) and made internal lookup constants private
    • Restructured webhook handling for checkout events
  • Chores

    • Removed unused dev/runtime dependencies
    • Updated ignore/config settings and logging guidance in documentation

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2fb1c7d7-c3b3-4bb0-8c46-37052941e2af

📥 Commits

Reviewing files that changed from the base of the PR and between 968759e and d42d72b.

📒 Files selected for processing (2)
  • src/routes/http/createdCheckout.ts
  • src/zod/internals.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/zod/internals.ts
  • src/routes/http/createdCheckout.ts

📝 Walkthrough

Walkthrough

Consolidates error enums and removes many static error factories; introduces calculatePaymentPrice and routes checkout pricing to it; removes common PAYMENT pricing module and related adapter exports; refactors ClickHouse/Postgres pricing utilities, AI token usage aggregation, query→proto helpers, webhook handling, and Zod filter-group schemas.

Changes

Error Type Consolidation and Pricing Service Refactoring

Layer / File(s) Summary
Build & Dependency Updates
.fallowrc.json, package.json
Add ignore pattern for generated gRPC JS files; remove ts-protoc-gen, @opentelemetry/instrumentation-pg, and pino-pretty from dependencies.
Error Type System Consolidation
src/errors/*
Prune many error enum variants and remove corresponding static factory methods across APIKeyError, AuthError, EventError, PaymentError, and StorageError.
Authentication Guard Removal
src/interceptors/auth.ts
Remove exported requireRole gRPC handler guard.
Payment Pricing Service Implementation
src/services/pricingService.ts, src/routes/gRPC/payment/createCheckoutLink.ts
Add calculatePaymentPrice to sum BASIC_USAGE and AI_TOKEN_USAGE prices; update checkout route to call it instead of adapter price.
Adapter PAYMENT Handler Removal
src/storage/adapter/*
Remove handlePriceRequestPayment re-exports and PAYMENT dispatch from ClickHouse/Postgres adapters and handler barrels; PAYMENT now falls through to unknown-event error.
ClickHouse Price Utility & Handlers
src/storage/adapter/clickhouse/utils.ts, handlers/*
Add runClickHousePriceQuery and last-billed lookup; delegate BASIC_USAGE/AI_TOKEN_USAGE handlers to it and remove inline DB/query code.
AI Token Usage Insert Refactors
src/storage/adapter/*/handlers/addAiTokenUsage.ts
Extract validation, aggregation, and insert-row builders; use helpers in ClickHouse and Postgres handlers.
Storage Common Removal & Privatization
src/storage/adapter/common/priceRequestPayment.ts, src/storage/adapter/common/queryEventsBase.ts, src/storage/db/postgres/helpers/users.ts
Delete common payment pricing module; convert event-type/table constants to internal-only const; remove checkUserExists alias.
Routes & Webhook Refactors
src/routes/gRPC/query/queryEvents.ts, src/routes/http/createdCheckout.ts
Extract row->proto helpers for queryEvents; centralize webhook unwrapping, header building, response helpers, and payment-store delegation in createdCheckout.
Zod Filter-Group Helper Extraction
src/zod/internals.ts, src/zod/data.ts, src/zod/query.ts
Add createFilterGroupSchema and FilterGroupOutput; replace inline recursive filterGroupSchema usage in data and query schemas with the helper.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ScrawnDotDev/Scrawn#34: Modifies authentication error types and auth interceptor logic in related ways.
  • ScrawnDotDev/Scrawn#35: Changes the PAYMENT event-type dispatch and pricing handler routing across adapters.
  • ScrawnDotDev/Scrawn#36: Adds or adjusts Zod query/data filter-group logic that this PR centralizes via createFilterGroupSchema.

Poem

🐰 From enums trimmed to pricing spun anew,
Adapters quieted as services do the due,
ClickHouse queries tidy, Zod schemas made neat,
Guards and re-exports folded, the changes all complete,
A rabbit cheers with a little hop and a chew.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'Refactor/cleanup' is vague and generic, providing no meaningful information about the specific changes in this extensive pull request. Use a more descriptive title that highlights the main change, such as 'Refactor pricing service and simplify error types' or 'Consolidate payment pricing logic and prune error variants'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 refactor/cleanup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/pricingService.ts`:
- Around line 1-2: Add operation-level logging around all storage adapter
interactions and error paths in the PricingService: import logger from
"errors/logger" and call logger.logOperationInfo(...) before each
StorageAdapterFactory usage (include operation name like
"PricingService.storeEvent", and context object with userId, mode, and
eventType) and call logger.logOperationError(...) in each catch or when
returning StorageError (include same context, the caught error, and operation
name). Ensure every adapter call site created via StorageAdapterFactory is
bracketed by a logOperationInfo before/after and any error branch or thrown
StorageError uses logOperationError with the operation name and the context keys
userId, mode, and eventType.
- Around line 8-9: Normalize beforeTimestamp to UTC at the service boundary by
creating a single normalized variable (e.g., beforeTimestampUtc =
beforeTimestamp.toUTC()) inside the entry function that receives
beforeTimestamp, and use that normalized value for all downstream adapter calls
instead of the original beforeTimestamp; update the places in this file that
call adapters (references to beforeTimestamp in adapter method invocations) to
pass beforeTimestampUtc so both adapter calls use the same UTC-normalized
timestamp.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: db338341-8a8e-4b3a-b44d-70acaf6c061f

📥 Commits

Reviewing files that changed from the base of the PR and between 29d4a18 and ee08a9b.

📒 Files selected for processing (19)
  • .fallowrc.json
  • package.json
  • src/errors/apikey.ts
  • src/errors/auth.ts
  • src/errors/event.ts
  • src/errors/payment.ts
  • src/errors/storage.ts
  • src/interceptors/auth.ts
  • src/routes/gRPC/payment/createCheckoutLink.ts
  • src/services/pricingService.ts
  • src/storage/adapter/clickhouse/ClickHouseAdapter.ts
  • src/storage/adapter/clickhouse/handlers/index.ts
  • src/storage/adapter/clickhouse/handlers/priceRequestPayment.ts
  • src/storage/adapter/common/priceRequestPayment.ts
  • src/storage/adapter/common/queryEventsBase.ts
  • src/storage/adapter/postgres/handlers/index.ts
  • src/storage/adapter/postgres/handlers/priceRequestPayment.ts
  • src/storage/adapter/postgres/postgres.ts
  • src/storage/db/postgres/helpers/users.ts
💤 Files with no reviewable changes (15)
  • src/storage/adapter/common/priceRequestPayment.ts
  • src/storage/adapter/clickhouse/handlers/priceRequestPayment.ts
  • src/storage/adapter/postgres/handlers/priceRequestPayment.ts
  • src/storage/db/postgres/helpers/users.ts
  • package.json
  • src/interceptors/auth.ts
  • src/storage/adapter/postgres/postgres.ts
  • src/storage/adapter/clickhouse/handlers/index.ts
  • src/storage/adapter/clickhouse/ClickHouseAdapter.ts
  • src/errors/auth.ts
  • src/storage/adapter/postgres/handlers/index.ts
  • src/errors/apikey.ts
  • src/errors/event.ts
  • src/errors/payment.ts
  • src/errors/storage.ts

Comment on lines +1 to +2
import { StorageAdapterFactory } from "../factory/EventStorageAdapterFactory";
import { StorageError } from "../errors/storage";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add operation-level logging in this new pricing flow.

This service is a key billing path and currently has no logOperationInfo / logOperationError calls. Please add operation/context logging (at least userId, mode, and event type) around adapter calls and error branches.

As per coding guidelines, "Use logger from errors/logger with logOperationInfo and logOperationError; include operation name and context".

Also applies to: 6-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/pricingService.ts` around lines 1 - 2, Add operation-level
logging around all storage adapter interactions and error paths in the
PricingService: import logger from "errors/logger" and call
logger.logOperationInfo(...) before each StorageAdapterFactory usage (include
operation name like "PricingService.storeEvent", and context object with userId,
mode, and eventType) and call logger.logOperationError(...) in each catch or
when returning StorageError (include same context, the caught error, and
operation name). Ensure every adapter call site created via
StorageAdapterFactory is bracketed by a logOperationInfo before/after and any
error branch or thrown StorageError uses logOperationError with the operation
name and the context keys userId, mode, and eventType.

Comment thread src/services/pricingService.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/storage/adapter/clickhouse/utils.ts (2)

83-84: 💤 Low value

Add explicit radix to parseInt.

While modern JavaScript defaults to base 10 for numeric strings, explicitly passing the radix improves clarity and avoids edge cases with unexpected input formats.

♻️ Proposed fix
-    const parsed = parseInt(data[0].total);
+    const parsed = parseInt(data[0].total, 10);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/adapter/clickhouse/utils.ts` around lines 83 - 84, The parseInt
call that converts data[0].total to a number should include an explicit radix to
avoid ambiguous parsing; update the parseInt usage that assigns parsed (from
data[0].total) to call parseInt(data[0].total, 10) so the code returns the
intended base-10 integer before the isNaN check.

23-25: 💤 Low value

Consider logging errors instead of silently swallowing them.

The empty catch block discards all error information, which could mask database connectivity issues or other problems during debugging. Consider at minimum logging the error, even if you still return null for graceful degradation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/adapter/clickhouse/utils.ts` around lines 23 - 25, The catch
block in src/storage/adapter/clickhouse/utils.ts that currently just returns
null should capture and log the error before returning to avoid swallowing
failures; change the empty catch to catch (err) (or e) and call the project's
logging facility (e.g., logger.error or processLogger.error) with a clear
message like "ClickHouse utility error" plus the caught error details, then
return null to preserve graceful degradation; if a project logger isn't
available in this module, use console.error with the same contextual message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routes/http/createdCheckout.ts`:
- Around line 61-81: The function resolvePaymentSession is defined but unused;
replace the duplicate manual session lookup and validation in the handler (where
getSessionByCheckoutId is called and fields checked) by calling
resolvePaymentSession(checkoutSessionId) and using its returned ResolvedSession
(or handle null) to simplify the flow, or remove resolvePaymentSession entirely
if you prefer not to refactor; ensure any references to session.userId,
session.billed_upto, session.apiKeyId, and session.mode are updated to use the
ResolvedSession properties userId, billedUpto, apiKeyId, and mode when adopting
the refactor.
- Around line 163-171: Wrap the two DB updates and the payment event creation in
a single Drizzle transaction so they succeed or roll back atomically: use
getPostgresDB()'s transaction API to execute the updates to usersTable and
sessionsTable together and call storePaymentEvent (and builder.setUser /
builder.setPaymentContext) inside that transaction, ensuring either both updates
and the payment record persist or none do to prevent duplicate events.

In `@src/storage/adapter/clickhouse/handlers/addAiTokenUsage.ts`:
- Around line 22-29: The validateNonNegative function currently allows NaN and
Infinity; update its checks to reject non-finite numbers by verifying typeof
value === "number" && !Number.isFinite(value) (or using Number.isFinite(value)
in the positive branch) and throw StorageError.insertFailed with the same
descriptive message (e.g., `Negative ${label} not allowed...` or for non-finite:
`${label} ${value} is not a finite number`) so NaN/Infinity are treated as
invalid; apply the same finite-number validation fix to the other similar helper
used at the 92-103 region (the alternate validation function referenced there)
so neither path accepts non-finite numeric values.

In `@src/storage/adapter/clickhouse/utils.ts`:
- Around line 86-93: Replace the duck-typing check that inspects e.name and
casts to any with a direct instanceof check against StorageError: remove the
object/type/name checks and use "if (e instanceof StorageError) throw e;".
Ensure StorageError is imported/visible in the module so the instanceof check is
type-safe and delete the unnecessary any cast and property access.

In `@src/storage/adapter/postgres/handlers/addAiTokenUsage.ts`:
- Around line 88-92: The helper buildAiTokenInsertValues currently relies on
inference for its return type; annotate its signature with an explicit return
type (e.g., a specific array type like AiTokenInsertRow[] or an existing type
such as AiTokenUsageInsert[]) and, if needed, declare a small interface/type
alias (e.g., AiTokenInsertRow) describing the shape returned (fields derived
from AggregatedEvent + apiKeyId and mode). Update the function signature for
buildAiTokenInsertValues(aggregatedEvents: AggregatedEvent[], apiKeyId: string,
mode: "production" | "test"): AiTokenInsertRow[] and ensure any callers/imports
use the same type; create the new type near the top of the file or in the module
types file if one exists.

In `@src/zod/internals.ts`:
- Around line 16-21: The current logical schema (the "logical" zod entry using
logicalMap) silently defaults unknown numeric values to "AND"; change it to
reject unknown values instead: validate that the incoming number exists in
logicalMap (e.g., via .refine or a transform that throws when logicalMap[v] is
undefined) so validation fails for invalid numerics rather than mapping them to
"AND"; keep the symbol names logical and logicalMap so you locate and update
that schema entry.

---

Nitpick comments:
In `@src/storage/adapter/clickhouse/utils.ts`:
- Around line 83-84: The parseInt call that converts data[0].total to a number
should include an explicit radix to avoid ambiguous parsing; update the parseInt
usage that assigns parsed (from data[0].total) to call parseInt(data[0].total,
10) so the code returns the intended base-10 integer before the isNaN check.
- Around line 23-25: The catch block in src/storage/adapter/clickhouse/utils.ts
that currently just returns null should capture and log the error before
returning to avoid swallowing failures; change the empty catch to catch (err)
(or e) and call the project's logging facility (e.g., logger.error or
processLogger.error) with a clear message like "ClickHouse utility error" plus
the caught error details, then return null to preserve graceful degradation; if
a project logger isn't available in this module, use console.error with the same
contextual message.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2764feea-b7bb-48e3-948f-17fc022c3f9b

📥 Commits

Reviewing files that changed from the base of the PR and between ee08a9b and 968759e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • AGENTS.md
  • src/routes/gRPC/query/queryEvents.ts
  • src/routes/http/createdCheckout.ts
  • src/services/pricingService.ts
  • src/storage/adapter/clickhouse/handlers/addAiTokenUsage.ts
  • src/storage/adapter/clickhouse/handlers/priceRequestAiTokenUsage.ts
  • src/storage/adapter/clickhouse/handlers/priceRequestSdkCall.ts
  • src/storage/adapter/clickhouse/utils.ts
  • src/storage/adapter/postgres/handlers/addAiTokenUsage.ts
  • src/zod/data.ts
  • src/zod/internals.ts
  • src/zod/query.ts
✅ Files skipped from review due to trivial changes (1)
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/pricingService.ts

Comment thread src/routes/http/createdCheckout.ts Outdated
Comment on lines +163 to +171
const db = getPostgresDB();

await db.update(usersTable).set({ last_billed_timestamp: billedUpto }).where(eq(usersTable.id, userId));
await db.update(sessionsTable).set({ processed: true }).where(eq(sessionsTable.sessionId, checkout_session_id));

builder.setUser(userId);
builder.setPaymentContext({ creditAmount });

try {
const paymentEvent = new Payment(userId, { creditAmount });
const adapter =
await StorageAdapterFactory.getEventStorageAdapter("PAYMENT");

await adapter.add(paymentEvent.serialize(), session.apiKeyId, session.mode);

builder.setSuccess(200);
return {
statusCode: 200,
body: { message: "Webhook processed successfully" },
};
} catch (dbError) {
Sentry.captureException(dbError, {
extra: {
context: "payment event storage",
checkoutSessionId: checkout_session_id,
paymentId: payment_id,
},
});
const errorMessage =
dbError instanceof Error ? dbError.message : String(dbError);
builder.setError(500, {
type: "DatabaseError",
message: `Failed to store payment event: ${errorMessage}`,
cause: dbError instanceof Error ? dbError.message : undefined,
stack: isDev && dbError instanceof Error ? dbError.stack : undefined,
});
return { statusCode: 500, body: { error: "Database error" } };
}
return await storePaymentEvent(userId, creditAmount, apiKeyId, mode, checkout_session_id, payment_id, builder);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Wrap database updates in a transaction to prevent duplicate payment events.

Lines 165-166 perform two separate updates without atomicity. If the usersTable update succeeds but the sessionsTable update fails, a webhook retry would:

  1. Pass the session.processed check (line 146) since it remains false
  2. Execute storePaymentEvent again, creating a duplicate payment record

This is a data integrity risk for financial operations.

🐛 Proposed fix using a transaction
     const db = getPostgresDB();
 
-    await db.update(usersTable).set({ last_billed_timestamp: billedUpto }).where(eq(usersTable.id, userId));
-    await db.update(sessionsTable).set({ processed: true }).where(eq(sessionsTable.sessionId, checkout_session_id));
+    await db.transaction(async (tx) => {
+      await tx.update(usersTable).set({ last_billed_timestamp: billedUpto }).where(eq(usersTable.id, userId));
+      await tx.update(sessionsTable).set({ processed: true }).where(eq(sessionsTable.sessionId, checkout_session_id));
+    });
 
     builder.setUser(userId);
     builder.setPaymentContext({ creditAmount });

As per coding guidelines: "Use Drizzle ORM with transactions".

📝 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
const db = getPostgresDB();
await db.update(usersTable).set({ last_billed_timestamp: billedUpto }).where(eq(usersTable.id, userId));
await db.update(sessionsTable).set({ processed: true }).where(eq(sessionsTable.sessionId, checkout_session_id));
builder.setUser(userId);
builder.setPaymentContext({ creditAmount });
try {
const paymentEvent = new Payment(userId, { creditAmount });
const adapter =
await StorageAdapterFactory.getEventStorageAdapter("PAYMENT");
await adapter.add(paymentEvent.serialize(), session.apiKeyId, session.mode);
builder.setSuccess(200);
return {
statusCode: 200,
body: { message: "Webhook processed successfully" },
};
} catch (dbError) {
Sentry.captureException(dbError, {
extra: {
context: "payment event storage",
checkoutSessionId: checkout_session_id,
paymentId: payment_id,
},
});
const errorMessage =
dbError instanceof Error ? dbError.message : String(dbError);
builder.setError(500, {
type: "DatabaseError",
message: `Failed to store payment event: ${errorMessage}`,
cause: dbError instanceof Error ? dbError.message : undefined,
stack: isDev && dbError instanceof Error ? dbError.stack : undefined,
});
return { statusCode: 500, body: { error: "Database error" } };
}
return await storePaymentEvent(userId, creditAmount, apiKeyId, mode, checkout_session_id, payment_id, builder);
const db = getPostgresDB();
await db.transaction(async (tx) => {
await tx.update(usersTable).set({ last_billed_timestamp: billedUpto }).where(eq(usersTable.id, userId));
await tx.update(sessionsTable).set({ processed: true }).where(eq(sessionsTable.sessionId, checkout_session_id));
});
builder.setUser(userId);
builder.setPaymentContext({ creditAmount });
return await storePaymentEvent(userId, creditAmount, apiKeyId, mode, checkout_session_id, payment_id, builder);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/http/createdCheckout.ts` around lines 163 - 171, Wrap the two DB
updates and the payment event creation in a single Drizzle transaction so they
succeed or roll back atomically: use getPostgresDB()'s transaction API to
execute the updates to usersTable and sessionsTable together and call
storePaymentEvent (and builder.setUser / builder.setPaymentContext) inside that
transaction, ensuring either both updates and the payment record persist or none
do to prevent duplicate events.

Comment on lines +22 to +29
function validateNonNegative(value: unknown, label: string, userId: UserId): void {
if (typeof value === "number" && value < 0) {
throw StorageError.insertFailed(
`Negative ${label} not allowed for AI token usage for user ${userId}`,
new Error(`${label} ${value} is negative`)
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

validateNonNegative allows non-finite numbers, which can serialize to null metrics.

NaN/Infinity currently pass validation and are converted to null by JSON.stringify, which can silently corrupt billing data.

💡 Proposed fix
 function validateNonNegative(value: unknown, label: string, userId: UserId): void {
-  if (typeof value === "number" && value < 0) {
+  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
     throw StorageError.insertFailed(
-      `Negative ${label} not allowed for AI token usage for user ${userId}`,
-      new Error(`${label} ${value} is negative`)
+      `Invalid ${label} for AI token usage for user ${userId}`,
+      new Error(
+        `${label} must be a finite non-negative number, received ${String(value)}`
+      )
     );
   }
 }

Also applies to: 92-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/adapter/clickhouse/handlers/addAiTokenUsage.ts` around lines 22 -
29, The validateNonNegative function currently allows NaN and Infinity; update
its checks to reject non-finite numbers by verifying typeof value === "number"
&& !Number.isFinite(value) (or using Number.isFinite(value) in the positive
branch) and throw StorageError.insertFailed with the same descriptive message
(e.g., `Negative ${label} not allowed...` or for non-finite: `${label} ${value}
is not a finite number`) so NaN/Infinity are treated as invalid; apply the same
finite-number validation fix to the other similar helper used at the 92-103
region (the alternate validation function referenced there) so neither path
accepts non-finite numeric values.

Comment on lines +86 to +93
if (
e &&
typeof e === "object" &&
"type" in e &&
(e as any).name === "StorageError"
) {
throw e;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use instanceof instead of duck-typing with any.

The current approach casts to any (discouraged per coding guidelines) and relies on the name property which could be minified or unreliable. Use instanceof StorageError for type-safe error identification.

♻️ Proposed fix
-    if (
-      e &&
-      typeof e === "object" &&
-      "type" in e &&
-      (e as any).name === "StorageError"
-    ) {
+    if (e instanceof StorageError) {
       throw e;
     }
📝 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
if (
e &&
typeof e === "object" &&
"type" in e &&
(e as any).name === "StorageError"
) {
throw e;
}
if (e instanceof StorageError) {
throw e;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/adapter/clickhouse/utils.ts` around lines 86 - 93, Replace the
duck-typing check that inspects e.name and casts to any with a direct instanceof
check against StorageError: remove the object/type/name checks and use "if (e
instanceof StorageError) throw e;". Ensure StorageError is imported/visible in
the module so the instanceof check is type-safe and delete the unnecessary any
cast and property access.

Comment on lines +88 to +92
function buildAiTokenInsertValues(
aggregatedEvents: AggregatedEvent[],
apiKeyId: string,
mode: "production" | "test"
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add an explicit return type to buildAiTokenInsertValues.

This helper currently relies on inference for its return type.

💡 Proposed fix
 function buildAiTokenInsertValues(
   aggregatedEvents: AggregatedEvent[],
   apiKeyId: string,
   mode: "production" | "test"
-) {
+): Array<Record<string, unknown>> {

As per coding guidelines: "Always use explicit types for function parameters and return values; avoid any".

📝 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
function buildAiTokenInsertValues(
aggregatedEvents: AggregatedEvent[],
apiKeyId: string,
mode: "production" | "test"
) {
function buildAiTokenInsertValues(
aggregatedEvents: AggregatedEvent[],
apiKeyId: string,
mode: "production" | "test"
): Array<Record<string, unknown>> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/adapter/postgres/handlers/addAiTokenUsage.ts` around lines 88 -
92, The helper buildAiTokenInsertValues currently relies on inference for its
return type; annotate its signature with an explicit return type (e.g., a
specific array type like AiTokenInsertRow[] or an existing type such as
AiTokenUsageInsert[]) and, if needed, declare a small interface/type alias
(e.g., AiTokenInsertRow) describing the shape returned (fields derived from
AggregatedEvent + apiKeyId and mode). Update the function signature for
buildAiTokenInsertValues(aggregatedEvents: AggregatedEvent[], apiKeyId: string,
mode: "production" | "test"): AiTokenInsertRow[] and ensure any callers/imports
use the same type; create the new type near the top of the file or in the module
types file if one exists.

Comment thread src/zod/internals.ts
Comment on lines +16 to +21
logical: z
.number()
.int()
.min(0)
.max(2)
.transform((v) => (logicalMap[v] ?? "AND") as "AND" | "OR"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unknown logical operator values instead of defaulting to AND.

Invalid logical values currently pass validation and silently become AND, which can alter query behavior instead of failing fast.

💡 Proposed fix
-        logical: z
-          .number()
-          .int()
-          .min(0)
-          .max(2)
-          .transform((v) => (logicalMap[v] ?? "AND") as "AND" | "OR"),
+        logical: z
+          .number()
+          .int()
+          .refine((v) => logicalMap[v] !== undefined, "Invalid logical operator")
+          .transform((v) => logicalMap[v]),
📝 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
logical: z
.number()
.int()
.min(0)
.max(2)
.transform((v) => (logicalMap[v] ?? "AND") as "AND" | "OR"),
logical: z
.number()
.int()
.refine((v) => logicalMap[v] !== undefined, "Invalid logical operator")
.transform((v) => logicalMap[v]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zod/internals.ts` around lines 16 - 21, The current logical schema (the
"logical" zod entry using logicalMap) silently defaults unknown numeric values
to "AND"; change it to reject unknown values instead: validate that the incoming
number exists in logicalMap (e.g., via .refine or a transform that throws when
logicalMap[v] is undefined) so validation fails for invalid numerics rather than
mapping them to "AND"; keep the symbol names logical and logicalMap so you
locate and update that schema entry.

@SteakFisher
SteakFisher merged commit 3e8c5bf into main May 17, 2026
3 checks passed
@SteakFisher
SteakFisher deleted the refactor/cleanup branch May 17, 2026 22:24
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