Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/src/routes/v1_keys_getVerifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,17 @@ export const registerV1KeysGetVerifications = (app: App) =>
const verificationsFromAllKeys = await Promise.all(
ids.map(({ keyId, keySpaceId }) => {
return cache.verificationsByKeyId.swr(`${keyId}:${start}-${end}`, async () => {
return await analytics.getVerificationsDaily({
const res = await analytics.getVerificationsDaily({
workspaceId: authorizedWorkspaceId,
keySpaceId: keySpaceId,
keyId: keyId,
start: start ? start : now - 24 * 60 * 60 * 1000,
end: end ? end : now,
});
if (res.err) {
throw new Error(res.err.message);
}
Comment on lines +231 to +233
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

Use UnkeyApiError for consistent error handling.

The current implementation throws a generic Error, which is inconsistent with the rest of the file that uses UnkeyApiError. This could lead to inconsistent error responses in the API.

Apply this diff to maintain consistency:

-            throw new Error(res.err.message);
+            throw new UnkeyApiError({
+              code: "INTERNAL_SERVER_ERROR",
+              message: `Failed to fetch verifications: ${res.err.message}`,
+            });

Committable suggestion skipped: line range outside the PR's diff.

return res.val;
});
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default async function APIKeyDetailPage(props: {
}),
clickhouse.verifications
.latest({ workspaceId: key.workspaceId, keySpaceId: key.keyAuthId, keyId: key.id })
.then((res) => res.at(0)?.time ?? 0),
.then((res) => res.val?.at(0)?.time ?? 0),
]);

const successOverTime: { x: string; y: number }[] = [];
Expand All @@ -108,7 +108,7 @@ export default async function APIKeyDetailPage(props: {
const expiredOverTime: { x: string; y: number }[] = [];
const forbiddenOverTime: { x: string; y: number }[] = [];

for (const d of verifications.sort((a, b) => a.time - b.time)) {
for (const d of verifications.val!.sort((a, b) => a.time - b.time)) {
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.

🛠️ Refactor suggestion

Consider safer null handling instead of non-null assertion

The non-null assertion (!) could lead to runtime errors if verifications.val is undefined. Consider using optional chaining with a default empty array.

-  for (const d of verifications.val!.sort((a, b) => a.time - b.time)) {
+  for (const d of (verifications.val ?? []).sort((a, b) => a.time - b.time)) {
📝 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
for (const d of verifications.val!.sort((a, b) => a.time - b.time)) {
for (const d of (verifications.val ?? []).sort((a, b) => a.time - b.time)) {

const x = new Date(d.time).toISOString();
switch (d.outcome) {
case "":
Expand Down Expand Up @@ -174,7 +174,7 @@ export default async function APIKeyDetailPage(props: {
expired: 0,
forbidden: 0,
};
verifications.forEach((v) => {
verifications.val!.forEach((v) => {
switch (v.outcome) {
case "VALID":
stats.valid += v.count;
Expand Down Expand Up @@ -317,7 +317,7 @@ export default async function APIKeyDetailPage(props: {
</EmptyPlaceholder>
)}

{latestVerifications.length > 0 ? (
{latestVerifications.val && latestVerifications.val.length > 0 ? (
<>
<Separator className="my-8" />
<h2 className="text-2xl font-semibold leading-none tracking-tight mt-8">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const VerificationTable = ({ verifications }: Props) => {
</TableRow>
</TableHeader>
<TableBody className={"font-mono"}>
{verifications.map((verification, i) => {
{verifications.val?.map((verification, i) => {
/**
* Instead of rounding every row individually, we want to round consecutive colored rows together.
* For example:
Expand All @@ -54,10 +54,10 @@ export const VerificationTable = ({ verifications }: Props) => {
*/
const isStartOfColoredBlock =
verification.outcome !== "VALID" &&
(i === 0 || verifications[i - 1].outcome === "VALID");
(i === 0 || verifications.val[i - 1].outcome === "VALID");
const isEndOfColoredBlock =
verification.outcome !== "VALID" &&
(i === verifications.length - 1 || verifications[i + 1].outcome === "VALID");
(i === verifications.val.length - 1 || verifications.val[i + 1].outcome === "VALID");

return (
<TableRow
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/app/(app)/apis/[apiId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,22 @@ export default async function ApiPage(props: {
.where(and(eq(schema.keys.keyAuthId, api.keyAuthId!), isNull(schema.keys.deletedAt)))
.execute()
.then((res) => res.at(0)?.count ?? 0),
getVerificationsPerInterval(query),
getActiveKeysPerInterval(query),
getVerificationsPerInterval(query).then((res) => res.val!),
getActiveKeysPerInterval(query).then((res) => res.val!),
Comment on lines +57 to +58
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.

🛠️ Refactor suggestion

Consider handling potential null values instead of using non-null assertions

The non-null assertions (!) on .val properties assume these values will never be null. This could lead to runtime errors if the queries fail or return no data.

Consider this safer approach:

-      getVerificationsPerInterval(query).then((res) => res.val!),
-      getActiveKeysPerInterval(query).then((res) => res.val!),
+      getVerificationsPerInterval(query).then((res) => res.val ?? []),
+      getActiveKeysPerInterval(query).then((res) => res.val ?? []),
📝 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
getVerificationsPerInterval(query).then((res) => res.val!),
getActiveKeysPerInterval(query).then((res) => res.val!),
getVerificationsPerInterval(query).then((res) => res.val ?? []),
getActiveKeysPerInterval(query).then((res) => res.val ?? []),

clickhouse.activeKeys
.perMonth({
workspaceId: api.workspaceId,
keySpaceId: api.keyAuthId!,
start: billingCycleStart,
end: billingCycleEnd,
})
.then((res) => res.at(0)),
.then((res) => res.val!.at(0)),
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.

🛠️ Refactor suggestion

Handle potential null values in the chain of operations

The chain of operations res.val!.at(0) has two potential points of failure: the val property could be null, and at(0) could return undefined if the array is empty.

Consider this safer approach:

-        .then((res) => res.val!.at(0)),
+        .then((res) => res.val?.at(0) ?? 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
.then((res) => res.val!.at(0)),
.then((res) => res.val?.at(0) ?? null),

getVerificationsPerInterval({
workspaceId: api.workspaceId,
keySpaceId: api.keyAuthId!,
start: billingCycleStart,
end: billingCycleEnd,
}),
}).then((res) => res.val!),
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.

💡 Codebase verification

🛠️ Refactor suggestion

Based on the gathered context, I can now generate the final response:

Consider safer error handling for verification data

The non-null assertion on verification data is used in billing calculations where verificationsInBillingCycle.reduce((sum, day) => sum + day.count, 0) computes the total count. A null value could cause runtime errors.

-      }).then((res) => res.val!),
+      }).then((res) => res.val ?? []),
  • The safer approach ensures billing calculations continue with zero counts instead of crashing if data is missing
  • This pattern aligns with other safe handling seen in the codebase (e.g., res?.at(0)?.count ?? 0)
🔗 Analysis chain

Ensure safe handling of verification data for billing calculations

The non-null assertion on verification data could affect billing calculations if the query fails or returns no data.

Consider this safer approach:

-      }).then((res) => res.val!),
+      }).then((res) => res.val ?? []),

Let's verify the usage of this data in calculations:


Let me gather more context about the verification data structure and its usage.


Let me fix the regex and try again.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for places where verification data is used in calculations
rg -A 5 "verificationsInBillingCycle" apps/dashboard/

Length of output: 1147


Script:

#!/bin/bash
# Check the type definition and structure of verification data
ast-grep --pattern 'type $_ = { count: number $$$}'

# Look for error handling patterns around verification data
rg -B 3 -A 3 "\.val\!" apps/dashboard/

Length of output: 173


Script:

#!/bin/bash
# Check the type definition and structure of verification data
ast-grep --pattern 'type $_ = { count: number }'

# Look for error handling patterns around verification data
rg -B 3 -A 3 '\.val!' apps/dashboard/

# Get more context around the verification data usage
rg -B 5 -A 5 'reduce\(.+sum \+ day\.count' apps/dashboard/

Length of output: 8560

]);

const successOverTime: { x: string; y: number }[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ const LastUsed: React.FC<{ workspaceId: string; keySpaceId: string; keyId: strin
keySpaceId: props.keySpaceId,
keyId: props.keyId,
})
.then((res) => res.at(0)?.time ?? null);
.then((res) => res.val?.at(0)?.time ?? null);

return (
<TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,7 @@ const AuditLogTable: React.FC<{

success: selected.success ?? undefined,
};
const logs = await clickhouse.ratelimits.logs(query).catch((err) => {
console.error(err);
throw err;
});
const logs = await clickhouse.ratelimits.logs(query).then((res) => res.val!);

if (logs.length === 0) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const LastUsed: React.FC<{
identifier: [identifier],
});

const unixMilli = lastUsed.at(0)?.time;
const unixMilli = lastUsed.val?.at(0)?.time;
if (unixMilli) {
return <span className="text-sm text-content-subtle">{ms(Date.now() - unixMilli)} ago</span>;
}
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/app/(app)/ratelimits/[namespaceId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@ export default async function RatelimitNamespacePage(props: {
.from(schema.ratelimitOverrides)
.where(eq(schema.ratelimitOverrides.namespaceId, namespace.id))
.execute()
.then((res) => res.at(0)?.count ?? 0),
getRatelimitsPerInterval(query),
.then((res) => res?.at(0)?.count ?? 0),
getRatelimitsPerInterval(query).then((res) => res.val!),
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

Avoid non-null assertions for Clickhouse results

Using ! to assert non-null values goes against the PR's goal of proper error handling. We should handle the error case gracefully instead.

-    getRatelimitsPerInterval(query).then((res) => res.val!),
+    getRatelimitsPerInterval(query).then((res) => res.val ?? { time: Date.now(), passed: 0, total: 0 }),

This ensures we have a safe fallback when the Clickhouse query fails or returns no data.

Also applies to: 78-78

getRatelimitsPerInterval({
workspaceId: namespace.workspaceId,
namespaceId: namespace.id,
start: billingCycleStart,
end: billingCycleEnd,
}),
}).then((res) => res.val!),
clickhouse.ratelimits
.latest({ workspaceId: namespace.workspaceId, namespaceId: namespace.id })
.then((res) => res.at(0)?.time),
.then((res) => res.val?.at(0)?.time),
]);

const passedOverTime: { x: string; y: number }[] = [];
Expand Down
16 changes: 9 additions & 7 deletions apps/dashboard/app/(app)/ratelimits/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ export const RatelimitCard: React.FC<Props> = async ({ workspace, namespace }) =
const intervalMs = 1000 * 60 * 60;

const [history, lastUsed] = await Promise.all([
clickhouse.ratelimits.perMinute({
workspaceId: workspace.id,
namespaceId: namespace.id,
start: end - intervalMs,
end,
}),
clickhouse.ratelimits
.perMinute({
workspaceId: workspace.id,
namespaceId: namespace.id,
start: end - intervalMs,
end,
})
.then((res) => res.val!),
Comment on lines +20 to +27
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

Add proper error handling for the perMinute response

The non-null assertion operator (!) on res.val could lead to runtime errors if the response contains an error or if val is undefined. Since this PR aims to improve error handling, consider handling potential error cases explicitly.

Consider this safer approach:

    clickhouse.ratelimits
      .perMinute({
        workspaceId: workspace.id,
        namespaceId: namespace.id,
        start: end - intervalMs,
        end,
      })
-      .then((res) => res.val!),
+      .then((res) => {
+        if (!res.val) {
+          // You might want to return a default value or throw a custom error
+          return [];
+        }
+        return res.val;
+      }),
📝 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
clickhouse.ratelimits
.perMinute({
workspaceId: workspace.id,
namespaceId: namespace.id,
start: end - intervalMs,
end,
})
.then((res) => res.val!),
clickhouse.ratelimits
.perMinute({
workspaceId: workspace.id,
namespaceId: namespace.id,
start: end - intervalMs,
end,
})
.then((res) => {
if (!res.val) {
// You might want to return a default value or throw a custom error
return [];
}
return res.val;
}),

clickhouse.ratelimits
.latest({ workspaceId: workspace.id, namespaceId: namespace.id })
.then((res) => res.at(0)?.time),
.then((res) => res.val?.at(0)?.time),
Comment on lines +20 to +30
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.

💡 Codebase verification

Inconsistent error handling patterns detected

The codebase shows mixed usage of error handling patterns with Clickhouse responses:

  • Some code paths use non-null assertions (res.val!):

    • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/page.tsx
    • apps/dashboard/app/(app)/success/page.tsx
  • While others properly handle potential null values (res.val?):

    • apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx
    • apps/dashboard/app/(app)/ratelimits/card.tsx
    • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx

The underlying Clickhouse client returns Result<T> types that should be properly handled without force unwrapping. The non-null assertions should be replaced with proper null checking to maintain consistent error handling across the codebase.

🔗 Analysis chain

Verify consistent error handling patterns

Let's ensure this error handling pattern is consistently implemented across other Clickhouse query handlers in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent error handling patterns in Clickhouse queries

# Look for direct non-null assertions on Clickhouse response values
rg -A 2 "clickhouse\." | rg "\.val!"

# Look for proper error handling patterns
rg -A 2 "clickhouse\." | rg "\.val\?"

Length of output: 784


Script:

#!/bin/bash
# Let's check the implementation of these Clickhouse query handlers to understand the return types

# Search for clickhouse ratelimits type definitions and implementations
ast-grep --pattern 'interface $_ {
  ratelimits: {
    $$$
  }
}'

# Also check the business methods
ast-grep --pattern 'interface $_ {
  business: {
    $$$
  }
}'

Length of output: 133


Script:

#!/bin/bash
# Let's try a different approach to find the Clickhouse client implementation

# Look for files that might contain the Clickhouse client definition
fd clickhouse

# Search for return type definitions in these files
rg -A 3 "type.*Result.*=" 

# Look for error handling patterns in the implementation
rg -A 3 "export (type|interface).*Click"

Length of output: 2353


Script:

#!/bin/bash
# Let's examine the error handling implementation in the dashboard's clickhouse client

# Check the clickhouse client implementation
cat apps/dashboard/lib/clickhouse.ts

# Also check the internal clickhouse package
cat internal/clickhouse/src/index.ts

# Look for Result type usage in these files
rg -A 3 "Result<" apps/dashboard/lib/clickhouse.ts internal/clickhouse/src/index.ts

Length of output: 3018


Script:

#!/bin/bash
# Let's check the implementation of ratelimits functions to understand the return types

# Check the ratelimits implementation file
cat internal/clickhouse/src/ratelimits.ts

# Also look for Result type usage in any implementation files
rg -A 3 "Result<" internal/clickhouse/src/

Length of output: 8841

]);

const totalRequests = history.reduce((sum, d) => sum + d.total, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ export default async function HistoryPage(props: {
if (!key?.keyAuth?.api) {
return notFound();
}
const history = await clickhouse.verifications.logs({
workspaceId: UNKEY_WORKSPACE_ID,
keySpaceId: key.keyAuthId,
keyId: key.id,
});
const history = await clickhouse.verifications
.logs({
workspaceId: UNKEY_WORKSPACE_ID,
keySpaceId: key.keyAuthId,
keyId: key.id,
})
.then((res) => res.val!);
Comment on lines +42 to +48
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

Remove unsafe non-null assertion and add proper error handling

The current implementation assumes the clickhouse query will always succeed and forcefully unwraps the result with !, which could lead to runtime errors if res.val is null or undefined. This defeats the purpose of returning errors as values.

Consider handling potential errors and empty results explicitly:

-  const history = await clickhouse.verifications
-    .logs({
-      workspaceId: UNKEY_WORKSPACE_ID,
-      keySpaceId: key.keyAuthId,
-      keyId: key.id,
-    })
-    .then((res) => res.val!);
+  const result = await clickhouse.verifications
+    .logs({
+      workspaceId: UNKEY_WORKSPACE_ID,
+      keySpaceId: key.keyAuthId,
+      keyId: key.id,
+    });
+  
+  if (!result.val) {
+    // Handle the error case appropriately
+    // This could be showing an error state or returning notFound()
+    return <div>Failed to load verification history</div>;
+  }
+  
+  const history = result.val;

Committable suggestion skipped: line range outside the PR's diff.


return <AccessTable verifications={history} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const LastUsed: React.FC<{ workspaceId: string; keySpaceId: string; keyId: strin
}) => {
const lastUsed = await clickhouse.verifications
.latest({ workspaceId, keySpaceId, keyId })
.then((res) => res.at(0)?.time ?? 0);
.then((res) => res.val?.at(0)?.time ?? 0);

return (
<Metric label="Last Used" value={lastUsed ? `${ms(Date.now() - lastUsed)} ago` : "Never"} />
Expand Down
12 changes: 7 additions & 5 deletions apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,13 @@ export default async function RootKeyPage(props: {
if (!keyForHistory?.keyAuth?.api) {
return notFound();
}
const history = await clickhouse.verifications.latest({
workspaceId: UNKEY_WORKSPACE_ID,
keySpaceId: key.keyAuthId,
keyId: key.id,
});
const history = await clickhouse.verifications
.latest({
workspaceId: UNKEY_WORKSPACE_ID,
keySpaceId: key.keyAuthId,
keyId: key.id,
})
.then((res) => res.val!);

const apis = workspace.apis.map((api) => {
const apiPermissionsStructure = apiPermissions(api.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import type { Permission } from "@unkey/db";
import { type PropsWithChildren, useMemo, useState } from "react";
import { type PropsWithChildren, useState } from "react";
import { PermissionToggle } from "./permission_toggle";
import { apiPermissions } from "./permissions";

Expand Down Expand Up @@ -38,10 +38,7 @@ export function DialogAddPermissionsForAPI(
});

const [selectedApiId, setSelectedApiId] = useState<string>("");
const selectedApi = useMemo(
() => props.apis.find((api) => api.id === selectedApiId),
[selectedApiId],
);
const selectedApi = props.apis.find((api) => api.id === selectedApiId);

const isSelectionDisabled =
selectedApi && !apisWithoutPermission.some((api) => api.id === selectedApi.id);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/(app)/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default async function SuccessPage() {
});
}

const activeWorkspaces = await clickhouse.business.activeWorkspaces();
const activeWorkspaces = await clickhouse.business.activeWorkspaces().then((res) => res.val!);
const chartData = activeWorkspaces.map(({ time, workspaces }) => ({
x: new Date(time).toLocaleDateString(),
y: workspaces,
Expand Down
1 change: 1 addition & 0 deletions internal/clickhouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@clickhouse/client-web": "^1.6.0",
"@unkey/error": "workspace:^",
"zod": "^3.23.8"
}
}
8 changes: 4 additions & 4 deletions internal/clickhouse/src/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export function getBillableRatelimits(ch: Querier) {
});

const res = await query(args);
if (!res) {
if (res.err || res.val.length === 0) {
return 0;
}
return res.at(0)?.count ?? 0;
return res.val.at(0)?.count ?? 0;
};
}
// get the billable verifications for a workspace in a specific month.
Expand Down Expand Up @@ -65,9 +65,9 @@ export function getBillableVerifications(ch: Querier) {
});

const res = await query(args);
if (!res) {
if (res.err || res.val.length === 0) {
return 0;
}
return res.at(0)?.count ?? 0;
return res.val.at(0)?.count ?? 0;
};
}
48 changes: 36 additions & 12 deletions internal/clickhouse/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type ClickHouseClient, createClient } from "@clickhouse/client-web";
import { Err, Ok, type Result } from "@unkey/error";
import { z } from "zod";
import { InsertError, QueryError } from "./error";
import type { Inserter, Querier } from "./interface";

export type Config = {
url: string;
};
Expand All @@ -12,6 +13,7 @@ export class Client implements Querier, Inserter {
constructor(config: Config) {
this.client = createClient({
url: config.url,

clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1,
Expand All @@ -32,11 +34,11 @@ export class Client implements Querier, Inserter {
// The schema of the output of each row
// Example: z.object({ id: z.string() })
schema: TOut;
}): (params: z.input<TIn>) => Promise<z.output<TOut>[]> {
return async (params: z.input<TIn>): Promise<z.output<TOut>[]> => {
}): (params: z.input<TIn>) => Promise<Result<z.output<TOut>[], QueryError>> {
return async (params: z.input<TIn>): Promise<Result<z.output<TOut>[], QueryError>> => {
const validParams = req.params?.safeParse(params);
if (validParams?.error) {
throw new Error(`Bad params: ${validParams.error.message}`);
return Err(new QueryError(`Bad params: ${validParams.error.message}`, { query: "" }));
}
const res = await this.client
.query({
Expand All @@ -48,7 +50,11 @@ export class Client implements Querier, Inserter {
throw new Error(`${err.message} ${req.query}, params: ${JSON.stringify(params)}`);
});
const rows = await res.json();
return z.array(req.schema).parse(rows);
const parsed = z.array(req.schema).safeParse(rows);
if (parsed.error) {
return Err(new QueryError(`Malformed data: ${parsed.error.message}`, { query: req.query }));
}
return Ok(parsed.data);
};
}

Expand All @@ -57,22 +63,40 @@ export class Client implements Querier, Inserter {
schema: TSchema;
}): (
events: z.input<TSchema> | z.input<TSchema>[],
) => Promise<{ executed: boolean; query_id: string }> {
) => Promise<Result<{ executed: boolean; query_id: string }, InsertError>> {
return async (events: z.input<TSchema> | z.input<TSchema>[]) => {
let validatedEvents: z.output<TSchema> | z.output<TSchema>[] | undefined = undefined;
const v = Array.isArray(events)
? req.schema.array().safeParse(events)
: req.schema.safeParse(events);
if (!v.success) {
throw new Error(v.error.message);
return Err(new InsertError(v.error.message));
}
validatedEvents = v.data;

return await this.client.insert({
table: req.table,
format: "JSONEachRow",
values: Array.isArray(validatedEvents) ? validatedEvents : [validatedEvents],
});
return this.retry(() =>
this.client
.insert({
table: req.table,
format: "JSONEachRow",
values: Array.isArray(validatedEvents) ? validatedEvents : [validatedEvents],
})
.then((res) => Ok(res))
.catch((err) => Err(new InsertError(err.message))),
);
Comment on lines +77 to +86
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

Adjust error handling in insert method to work with retry function

The retry function is designed to handle functions that throw exceptions on failure. However, in the insert method, the function passed to retry returns a Result type with Err on failure instead of throwing an exception. This means that retries may not occur as expected since no exception is thrown to be caught.

Consider modifying the function to throw exceptions on failure so that the retry method can catch them. Apply the following changes:

 return this.retry(() =>
   this.client
     .insert({
       table: req.table,
       format: "JSONEachRow",
       values: Array.isArray(validatedEvents) ? validatedEvents : [validatedEvents],
     })
-    .then((res) => Ok(res))
-    .catch((err) => Err(new InsertError(err.message))),
+    .then((res) => res)
+    .catch((err) => {
+      throw new InsertError(err.message);
+    }),
 );

Then, handle the result after the retry call:

+      try {
+        const res = await this.retry(...);
+        return Ok(res);
+      } catch (err) {
+        return Err(err);
+      }

Committable suggestion skipped: line range outside the PR's diff.

};
}

private async retry<T>(fn: (attempt: number) => Promise<T>): Promise<T> {
let err: Error | undefined = undefined;
for (let i = 1; i <= 3; i++) {
try {
return fn(i);
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

Add await when calling fn(i) to ensure proper error handling

In the retry method, you need to await the call to fn(i) within the try block to ensure that any errors are correctly caught by the catch block. Without await, the try...catch construct won't catch asynchronous errors thrown by the promise returned from fn(i).

Apply this code change:

-          return fn(i);
+          return await fn(i);
📝 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
return fn(i);
return await fn(i);

} catch (e) {
console.warn(e);
err = e as Error;
}
}
throw err;
}
}
Loading