Skip to content

Conversation

@max-programming
Copy link
Contributor

@max-programming max-programming commented Oct 25, 2025

This PR implements the feature of adding tags to keys, and retrieving keys based on one or more tags

  • Tags are stored as array of strings in metadata
  • Matching is case-insensitive (convert to lowercase for comparison)
  • Multiple tags use OR logic (any match returns the key)
  • ownerId parameter is optional for scoping searches
  • Redis implementation uses Sets for efficient tag lookups
  • Drizzle uses JSONB array contains with case-insensitive operators

Summary by cubic

Adds tag support to API keys and a new findByTag method to fetch keys by tag(s). Matching is case-insensitive with OR logic, and searches can be scoped by owner.

  • New Features

    • Added metadata.tags: string[] (normalized to lowercase on create).
    • Added ApiKeyManager.findByTag(tag | string[], ownerId?).
    • Implemented findByTag in Memory, Redis (Sets per tag; cleanup on delete), and Drizzle (JSONB, case-insensitive).
    • Added tests for tag queries; updated README; removed “tags” from feature suggestions.
  • Migration

    • If using a custom Storage, implement findByTag(tag, ownerId?) to match the new interface.

Summary by CodeRabbit

  • New Features

    • Case-insensitive tags for API keys, persisted across create/rotate and cleaned on delete.
    • Tag-based lookup to find keys by one or multiple tags, with optional owner filtering via manager/storage APIs.
  • Documentation

    • README updated with tag lookup usage.
    • Feature suggestions doc reorganized and "Key Tags/Labels" example removed.
  • Tests

    • Added tag-query tests for memory, Redis, and SQL-backed stores.
  • Chores

    • Linter rule added to warn on excessive cognitive complexity.
    • CI workflow permissions adjusted.

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds optional tags to API key metadata, implements tag-based lookup methods (findByTag/findByTags) across Memory/Drizzle/Redis stores, normalizes/propagates tags in manager create/rotate flows, updates tests and README, adds a biome lint rule, and renumbers FEATURE_SUGGESTIONS sections.

Changes

Cohort / File(s) Summary
Type Definitions
src/types/api-key-types.ts, src/types/storage-types.ts
Added tags?: string[] to ApiKeyMetadata and declared findByTags(tags: string[], ownerId?: string): Promise<ApiKeyRecord[]> and findByTag(tag: string, ownerId?: string): Promise<ApiKeyRecord[]> on the Storage interface.
Manager Layer
src/manager.ts
Normalizes (lowercases) and persists metadata.tags on create and rotate; exposes findByTags and findByTag that delegate to storage.
Memory Storage & Tests
src/storage/memory.ts, src/storage/memory.test.ts
Implemented findByTags/findByTag with case-insensitive matching and optional owner filtering; added tests for single/multiple tags and owner scenarios.
Drizzle Storage & Tests
src/storage/drizzle.ts, src/storage/drizzle.test.ts
Added findByTags/findByTag using JSONB array-overlap queries; tightened single-row null checks; updated tests to guard record access and cover tag/owner cases.
Redis Storage & Tests
src/storage/redis.ts, src/storage/redis.test.ts
Added private tagKey helper; maintain tag membership sets on save/delete/deleteByOwner; implemented findByTags (SUNION + optional owner filter) and findByTag; added tests.
Docs & Config
README.md, FEATURE_SUGGESTIONS.md, biome.jsonc
README updated to show storage implementing findByTag/findByTags. FEATURE_SUGGESTIONS.md: added "Key tags/labels" under Currently Implemented and removed the standalone section; renumbered headings. biome.jsonc adds noExcessiveCognitiveComplexity lint rule (warn, maxAllowedComplexity: 50).
CI Workflows
.github/workflows/benchmark.yml
Workflow permissions extended to include contents: read and issues: write.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant ApiKeyManager
    participant Storage
    note over Storage: Memory / Drizzle / Redis implementations

    Client->>ApiKeyManager: findByTag(tag, ownerId?)
    ApiKeyManager->>ApiKeyManager: normalize tag(s) to lowercase
    ApiKeyManager->>Storage: findByTag/findByTags(tags[], ownerId)

    alt Memory backend
        Storage->>Storage: in-memory filter by metadata.tags overlap
    else Drizzle backend
        Storage->>Storage: SQL JSONB array-overlap query
    else Redis backend
        Storage->>Storage: SUNION(tagKeys) (+ optional owner set) → fetch records
    end

    Storage-->>ApiKeyManager: ApiKeyRecord[]
    ApiKeyManager-->>Client: ApiKeyRecord[]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas to focus on:
    • Consistent lowercase normalization and propagation in create/rotate flows (src/manager.ts).
    • Correctness and cleanup of Redis tag sets on save/delete/deleteByOwner (src/storage/redis.ts).
    • Drizzle JSONB overlap query correctness, SQL injection safety, and null handling (src/storage/drizzle.ts).
    • Tests’ guarded access changes and OR semantics for multi-tag matching (src/storage/*.{test.ts}).

Poem

🐰 I hopped through code both new and neat,
Lowercased tags on every key I meet,
In Memory, SQL, and Redis beds,
Labels now guide the tiny threads,
A carrot for tags—searches are sweet 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: implement tags for keys and fix some typescript and biome errors" directly addresses the primary changes in the changeset. The main feature is the implementation of tags across all storage backends (Memory, Redis, Drizzle) with new findByTag and findByTags methods, tag normalization, and comprehensive tests, which aligns with the "implement tags for keys" portion of the title. The secondary part of the title references "fix some typescript and biome errors," which is evidenced by the addition of cognitive complexity linting rules in biome.jsonc and various code quality improvements throughout the PR. The title is concise, specific, and avoids vague terminology; a teammate scanning the commit history would clearly understand that this PR adds tag support for API keys and addresses related configuration issues.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c64d79 and bc52bd5.

📒 Files selected for processing (1)
  • .github/workflows/benchmark.yml (1 hunks)
🔇 Additional comments (1)
.github/workflows/benchmark.yml (1)

12-12: Clarify permission changes and their connection to tag feature.

The added permissions contents: read and issues: write appear to be good practice additions:

  • contents: read is correctly required for the checkout action.
  • issues: write aligns with the github.rest.issues.createComment() call in the script.

However, the AI summary states these permissions are needed to "align with broader PR changes introducing tag-based query capabilities," yet the benchmark workflow itself has no logic changes or references to tags. Clarify whether:

  1. These permissions are a general cleanup/best practice unrelated to the tag feature, or
  2. There is a connection to the tag feature that should be documented.

Also applies to: 14-14


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

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

Copy link

@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.

5 issues found across 12 files

Prompt for AI agents (all 5 issues)

Understand the root cause of the following 5 issues and fix them.


<file name="src/storage/memory.test.ts">

<violation number="1" location="src/storage/memory.test.ts:162">
This test never creates a non-matching owner, so it would still pass even if findByTag ignored the ownerId filter. Please add a second record with the same tag but a different owner and assert it is excluded.</violation>
</file>

<file name="src/storage/redis.test.ts">

<violation number="1" location="src/storage/redis.test.ts:199">
With only one key created for owner_123, this test never verifies that the owner filter is actually applied. Because RedisStore.findByTag currently unions owner and tag sets, an owner_123 key without the tag would still be returned, yet this assertion would still pass. Please add another key that should be filtered out to confirm the owner scoping works.</violation>
</file>

<file name="src/storage/redis.ts">

<violation number="1" location="src/storage/redis.ts:96">
Using `sunion` with the owner set causes `findByTag` to return keys that either match the tag or merely belong to the owner, including keys owned by other users. This breaks owner scoping and can leak cross-tenant data.</violation>
</file>

<file name="src/storage/drizzle.test.ts">

<violation number="1" location="src/storage/drizzle.test.ts:203">
This new test never exercises the “OR” semantics it claims to cover. Because the only fixture record contains all of the requested tags, a regression that requires *all* tags to match would still pass here. Please add distinct records or expectations that demonstrate any-tag matches to actually validate the intended behavior.</violation>
</file>

<file name="src/storage/memory.ts">

<violation number="1" location="src/storage/memory.ts:40">
The owner filter only runs when ownerId is truthy. Passing an empty string (still a valid ownerId) would bypass scoping and return other owners&#39; keys.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

tags: ["test", "key", "more", "tags"],
});

const found = await store.findByTag("test", "user_123");
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

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

This test never creates a non-matching owner, so it would still pass even if findByTag ignored the ownerId filter. Please add a second record with the same tag but a different owner and assert it is excluded.

Prompt for AI agents
Address the following comment on src/storage/memory.test.ts at line 162:

<comment>This test never creates a non-matching owner, so it would still pass even if findByTag ignored the ownerId filter. Please add a second record with the same tag but a different owner and assert it is excluded.</comment>

<file context>
@@ -130,6 +130,41 @@ describe(&quot;MemoryStore&quot;, () =&gt; {
+				tags: [&quot;test&quot;, &quot;key&quot;, &quot;more&quot;, &quot;tags&quot;],
+			});
+
+			const found = await store.findByTag(&quot;test&quot;, &quot;user_123&quot;);
+			expect(found).toHaveLength(1);
+			expect(found[0]?.id).toBe(record.id);
</file context>

✅ Addressed in 689cac9

tags: ["test", "key", "more", "tags"],
});

const found = await store.findByTag("test", "user_123");
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

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

With only one key created for owner_123, this test never verifies that the owner filter is actually applied. Because RedisStore.findByTag currently unions owner and tag sets, an owner_123 key without the tag would still be returned, yet this assertion would still pass. Please add another key that should be filtered out to confirm the owner scoping works.

Prompt for AI agents
Address the following comment on src/storage/redis.test.ts at line 199:

<comment>With only one key created for owner_123, this test never verifies that the owner filter is actually applied. Because RedisStore.findByTag currently unions owner and tag sets, an owner_123 key without the tag would still be returned, yet this assertion would still pass. Please add another key that should be filtered out to confirm the owner scoping works.</comment>

<file context>
@@ -167,6 +167,41 @@ describe(&quot;RedisStore&quot;, () =&gt; {
+				tags: [&quot;test&quot;, &quot;key&quot;, &quot;more&quot;, &quot;tags&quot;],
+			});
+
+			const found = await store.findByTag(&quot;test&quot;, &quot;user_123&quot;);
+			expect(found).toHaveLength(1);
+			expect(found[0]?.id).toBe(record.id);
</file context>

✅ Addressed in c74e819

: [this.tagKey(tag.toLowerCase())];
const ownerKey = ownerId ? this.ownerKey(ownerId) : null;

const tagIds = await this.redis.sunion(
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

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

Using sunion with the owner set causes findByTag to return keys that either match the tag or merely belong to the owner, including keys owned by other users. This breaks owner scoping and can leak cross-tenant data.

Prompt for AI agents
Address the following comment on src/storage/redis.ts at line 96:

<comment>Using `sunion` with the owner set causes `findByTag` to return keys that either match the tag or merely belong to the owner, including keys owned by other users. This breaks owner scoping and can leak cross-tenant data.</comment>

<file context>
@@ -73,6 +84,37 @@ export class RedisStore implements Storage {
+			: [this.tagKey(tag.toLowerCase())];
+		const ownerKey = ownerId ? this.ownerKey(ownerId) : null;
+
+		const tagIds = await this.redis.sunion(
+			ownerKey ? [...tagKeys, ownerKey] : tagKeys
+		);
</file context>

✅ Addressed in 689cac9

expect(found[0]?.id).toBe(record.id);
});

it("should find all records by multiple tags", async () => {
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

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

This new test never exercises the “OR” semantics it claims to cover. Because the only fixture record contains all of the requested tags, a regression that requires all tags to match would still pass here. Please add distinct records or expectations that demonstrate any-tag matches to actually validate the intended behavior.

Prompt for AI agents
Address the following comment on src/storage/drizzle.test.ts at line 203:

<comment>This new test never exercises the “OR” semantics it claims to cover. Because the only fixture record contains all of the requested tags, a regression that requires *all* tags to match would still pass here. Please add distinct records or expectations that demonstrate any-tag matches to actually validate the intended behavior.</comment>

<file context>
@@ -188,6 +188,41 @@ describe(&quot;DrizzleStore&quot;, () =&gt; {
+			expect(found[0]?.id).toBe(record.id);
+		});
+
+		it(&quot;should find all records by multiple tags&quot;, async () =&gt; {
+			const { record } = await keys.create({
+				ownerId: &quot;user_123&quot;,
</file context>

✅ Addressed in 689cac9

ownerId?: string
): Promise<ApiKeyRecord[]> {
return Array.from(await this.keys.values()).filter((record) => {
if (ownerId && record.metadata.ownerId !== ownerId) {
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

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

The owner filter only runs when ownerId is truthy. Passing an empty string (still a valid ownerId) would bypass scoping and return other owners' keys.

Prompt for AI agents
Address the following comment on src/storage/memory.ts at line 40:

<comment>The owner filter only runs when ownerId is truthy. Passing an empty string (still a valid ownerId) would bypass scoping and return other owners&#39; keys.</comment>

<file context>
@@ -31,6 +32,23 @@ export class MemoryStore implements Storage {
+		ownerId?: string
+	): Promise&lt;ApiKeyRecord[]&gt; {
+		return Array.from(await this.keys.values()).filter((record) =&gt; {
+			if (ownerId &amp;&amp; record.metadata.ownerId !== ownerId) {
+				return false;
+			}
</file context>

✅ Addressed in 689cac9

Copy link

@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: 2

🧹 Nitpick comments (5)
README.md (1)

270-270: Consider adding usage examples for the new findByTag method.

The custom storage interface now correctly includes findByTag, but the README lacks usage examples demonstrating how users can query keys by tags. Consider adding a section showing tag-based queries similar to the existing API examples.

Example addition:

// Find keys by tag
const prodKeys = await keys.findByTag('production')
const testKeys = await keys.findByTag(['test', 'staging']) // OR logic

// Find keys by tag for specific owner
const userProdKeys = await keys.findByTag('production', 'user_123')
src/types/storage-types.ts (1)

27-32: Refactor JSDoc to follow TypeScript conventions.

The JSDoc embeds type information in the description text, which is non-standard for TypeScript. Since TypeScript already provides type information, the JSDoc should omit explicit type annotations.

Apply this diff:

 	/**
 	 * Find all API keys by tag(s)
-	 * @param tag - The tag(s) to search for {string | string[]}
-	 * @param ownerId - The owner ID to filter by (optional) {string}
+	 * @param tag - The tag(s) to search for
+	 * @param ownerId - Optional owner ID to filter results
 	 */
 	findByTag(tag: string | string[], ownerId?: string): Promise<ApiKeyRecord[]>;
src/storage/drizzle.test.ts (1)

646-651: Refactor appears unrelated to tag functionality.

This change from direct array access to .at(i) with an undefined guard is safer but appears unrelated to the tag feature implementation. While it's a good defensive coding practice, it might belong in a separate commit for clarity.

src/storage/drizzle.ts (1)

92-97: Optional: Unnecessary length check.

The condition if (tagArray.length > 0) will always be true since tagArray is constructed from either a single tag or an array of tags. Consider removing this check for cleaner code.

src/manager.ts (1)

273-273: Consider adding validation for tag values.

Tags are normalized to lowercase (line 273) and assigned to the record (line 290), but there's no validation to prevent empty or whitespace-only strings. Users could create keys with tags like ["", " ", " "], which may not be useful and could cause confusion.

Apply this diff to add validation:

-const tags = metadata.tags?.map((t) => t.toLowerCase());
+const tags = metadata.tags
+  ?.map((t) => t.trim().toLowerCase())
+  .filter((t) => t.length > 0);

This ensures tags are trimmed and non-empty before storage.

Also applies to: 290-290

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b830e1e and cad6983.

📒 Files selected for processing (12)
  • FEATURE_SUGGESTIONS.md (7 hunks)
  • README.md (1 hunks)
  • biome.jsonc (1 hunks)
  • src/manager.ts (4 hunks)
  • src/storage/drizzle.test.ts (2 hunks)
  • src/storage/drizzle.ts (4 hunks)
  • src/storage/memory.test.ts (1 hunks)
  • src/storage/memory.ts (1 hunks)
  • src/storage/redis.test.ts (1 hunks)
  • src/storage/redis.ts (5 hunks)
  • src/types/api-key-types.ts (1 hunks)
  • src/types/storage-types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/storage/memory.test.ts (1)
examples/drizzle-schema.ts (1)
  • keys (15-19)
src/types/storage-types.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/manager.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/redis.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/memory.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/drizzle.test.ts (1)
examples/drizzle-schema.ts (1)
  • keys (15-19)
src/storage/drizzle.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
🔇 Additional comments (16)
biome.jsonc (1)

9-20: LGTM!

The cognitive complexity rule configuration is a good addition for maintaining code quality. A threshold of 50 with warning level is reasonable and aligns with the PR's broader code quality improvements.

src/storage/drizzle.test.ts (1)

191-224: LGTM!

The test coverage for findByTag is comprehensive, covering single tag, multiple tags (OR logic), and owner-filtered scenarios. The test structure is consistent with similar tests in other storage backends.

FEATURE_SUGGESTIONS.md (2)

7-7: LGTM!

Correctly reflects that key tags/labels functionality has been implemented and moved from the suggestions list.


48-107: LGTM!

The renumbering of remaining feature suggestions is correct after removing the now-implemented tags feature.

src/storage/memory.ts (1)

34-49: LGTM!

The implementation correctly handles all specified requirements:

  • Case-insensitive tag matching via normalization
  • OR logic for multiple tags using some()
  • Optional owner filtering
  • Proper handling of undefined tags via optional chaining
src/storage/memory.test.ts (1)

133-166: LGTM!

Comprehensive test coverage for findByTag that mirrors the test structure in other storage backends. The three scenarios validate single tag, multiple tag (OR logic), and owner-filtered queries effectively.

src/storage/redis.test.ts (1)

170-203: LGTM!

Excellent test coverage that maintains consistency with the other storage backend tests. The three test cases validate all the key scenarios for tag-based queries, and the assertions correctly verify record identity.

src/types/api-key-types.ts (1)

41-42: LGTM! Clean schema addition.

The optional tags field follows the established pattern for array fields in the schema (similar to scopes on line 17).

src/storage/redis.ts (4)

18-20: LGTM! Helper follows naming convention.

The tagKey helper is consistent with existing hashKey and ownerKey methods.


41-45: LGTM! Tag associations stored correctly.

Records are added to tag-specific sets with proper lowercase normalization for case-insensitive matching.


146-150: LGTM! Tag cleanup on delete.

Properly removes the record from all tag sets to maintain consistency.


167-171: LGTM! Tag cleanup on owner deletion.

Correctly removes tag associations when deleting all keys for an owner.

src/storage/drizzle.ts (2)

1-1: LGTM! Necessary imports for tag query functionality.

The added imports (and, arrayContains, sql, SQL) are all used in the new findByTag implementation.


60-60: LGTM! Improved null safety.

The enhanced checks (rows.length > 0 && rows[0]) provide better defensive coding compared to just checking rows[0].

Also applies to: 70-70

src/manager.ts (2)

306-311: LGTM! Clean delegation to storage layer.

The findByTag method properly delegates to the storage implementation, maintaining the abstraction.


406-406: LGTM! Tags propagated through rotation.

The rotation logic correctly carries forward tags from the old record or accepts new tags from the metadata parameter.

@max-programming
Copy link
Contributor Author

@cubic-dev-ai rerun the review, some code is updated

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Oct 25, 2025

@cubic-dev-ai rerun the review, some code is updated

@max-programming I've started the AI code review. It'll take a few minutes to complete.

Copy link

@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.

6 issues found across 12 files

Prompt for AI agents (all 6 issues)

Understand the root cause of the following 6 issues and fix them.


<file name="src/storage/redis.ts">

<violation number="1" location="src/storage/redis.ts:91">
Using SUNION with the owner set breaks owner scoping—queries with ownerId will include other owners&#39; keys that share the tag and also return this owner&#39;s keys that lack it. Please intersect the owner set with the tag matches instead of unioning them.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:270">
The Storage interface now requires both findByTag and findByTags, so this example is incomplete—users copying it will hit a type error. Please add the findByTags stub alongside findByTag to keep the snippet accurate.</violation>
</file>

<file name="src/storage/drizzle.ts">

<violation number="1" location="src/storage/drizzle.ts:90">
Multi-tag searches always miss results because the SQL builds `ARRAY[&#39;tag1,tag2&#39;]`, so `?|` checks only the concatenated string instead of each tag.</violation>
</file>

<file name="src/storage/memory.test.ts">

<violation number="1" location="src/storage/memory.test.ts:151">
This test never validates the intended OR semantics for findByTags—because it only creates one record containing every queried tag, the assertion would still pass even if the implementation required all tags. Please add a contrasting record missing one of the tags (or similar) so the test fails when OR matching is broken.</violation>

<violation number="2" location="src/storage/memory.test.ts:162">
This owner-scoped tag test never verifies that ownerId filtering works—it only creates a record for the matching owner, so the assertion would still pass even if findByTag ignored the owner argument. Please add a record for a different owner with the same tag(s) and assert it is excluded.</violation>
</file>

<file name="src/manager.ts">

<violation number="1" location="src/manager.ts:407">
Updating metadata writes tags without lowercasing, so changing tags to mixed/upper case breaks case-insensitive lookups. Normalize the tags array before persisting.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

@izadoesdev
Copy link
Owner

Have you checked the comments? cubic left a few good notes in there

@max-programming
Copy link
Contributor Author

@izadoesdev making these changes
noticed many failing tests too

… logic, tag normalization, empty owner handlin
@izadoesdev
Copy link
Owner

I've commited some fixes

Copy link

@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

♻️ Duplicate comments (1)
src/storage/drizzle.ts (1)

17-28: Pass the array directly to leverage Drizzle's parameter binding.

Manual SQL escaping with sql.raw works but is less idiomatic than letting Drizzle handle parameter binding. PostgreSQL's ?| operator accepts a text[] parameter, and Drizzle's sql template will bind JS arrays as proper PostgreSQL parameters.

As per coding guidelines

Replace the manual quoting:

 private jsonbArrayOverlaps(tags: string[]): SQL {
   const lowercasedTags = tags.map((t) => t.toLowerCase());
-  const quotedTags = lowercasedTags
-    .map((tag) => `'${tag.replace(/'/g, "''")}'`)
-    .join(",");
-  return sql`${this.table.metadata}->'tags' ?| ARRAY[${sql.raw(quotedTags)}]`;
+  return sql`${this.table.metadata}->'tags' ?| ${lowercasedTags}`;
 }
🧹 Nitpick comments (1)
src/storage/memory.test.ts (1)

179-188: Add a record with a different owner to verify filtering.

This test still doesn't prove that owner filtering works—it only creates a record for the matching owner (user_123), so the assertion would pass even if findByTag ignored the ownerId parameter.

Apply this diff:

 it("should find all records by owner and multiple tags", async () => {
   const { record } = await keys.create({
     ownerId: "user_123",
     tags: ["test", "key", "more", "tags"],
   });
+
+  // Create a key with same tags but different owner
+  await keys.create({
+    ownerId: "user_456",
+    tags: ["test", "key"],
+  });

   const found = await store.findByTag(["test", "key"], "user_123");
   expect(found).toHaveLength(1);
   expect(found[0]?.id).toBe(record.id);
 });

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9125d9 and 689cac9.

📒 Files selected for processing (8)
  • src/manager.ts (4 hunks)
  • src/storage/drizzle.test.ts (3 hunks)
  • src/storage/drizzle.ts (5 hunks)
  • src/storage/memory.test.ts (1 hunks)
  • src/storage/memory.ts (1 hunks)
  • src/storage/redis.test.ts (1 hunks)
  • src/storage/redis.ts (5 hunks)
  • src/types/storage-types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/types/storage-types.ts
  • src/manager.ts
  • src/storage/redis.test.ts
  • src/storage/drizzle.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/storage/redis.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/drizzle.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/memory.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
🔇 Additional comments (4)
src/storage/memory.test.ts (1)

145-177: LGTM! Test coverage improved.

These tests now properly verify:

  • OR semantics for multiple tags (record r2 has only "test", not "key", so it proves OR matching works)
  • Owner filtering (creating a key for user_456 with the same tag proves the filter excludes other owners)
src/storage/memory.ts (1)

34-45: LGTM! Owner filtering fixed.

The ownerId !== undefined check correctly handles all falsy values including empty strings, addressing the previous review concern. The tag matching logic properly normalizes to lowercase and implements OR semantics across multiple tags.

src/storage/redis.ts (2)

87-125: LGTM! Intersection logic correctly implemented.

The tag-to-owner filtering is now correct:

  1. Lines 98-101: Retrieves IDs matching any of the requested tags (OR semantics)
  2. Lines 103-106: Filters those IDs to only include ones owned by the specified user (AND semantics)

This properly addresses the previous critical issue where SUNION was incorrectly combining tag and owner sets.


155-159: LGTM! Tag cleanup is thorough.

Tag associations are properly removed during both delete and deleteByOwner operations, ensuring no orphaned tag entries remain in Redis.

Also applies to: 176-180

@max-programming
Copy link
Contributor Author

@cubic-dev-ai re run the review

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Oct 25, 2025

@cubic-dev-ai re run the review

@max-programming I've started the AI code review. It'll take a few minutes to complete.

@max-programming
Copy link
Contributor Author

some of my commits got overriden but no problem

Copy link

@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.

3 issues found across 12 files

Prompt for AI agents (all 3 issues)

Understand the root cause of the following 3 issues and fix them.


<file name="README.md">

<violation number="1" location="README.md:271">
`Storage` only exposes findByTag(tag: string | string[], ownerId?), so documenting a separate findByTags method makes the sample invalid and will fail type-checking. Please remove or correct this method.</violation>
</file>

<file name="src/storage/drizzle.ts">

<violation number="1" location="src/storage/drizzle.ts:112">
findByTag may return all API keys when called with empty filters (missing WHERE), enabling data exposure</violation>
</file>

<file name="src/storage/redis.ts">

<violation number="1" location="src/storage/redis.ts:87">
findByTag relies on Redis tag sets that we only populate during save(); updateMetadata never updates those sets, so changing metadata.tags leaves this index stale—new tags are missed and old tags still match. Please re-sync the tag sets whenever metadata.tags changes.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/storage/redis.ts (1)

127-142: Critical: Tag sets not synchronized when metadata.tags is updated.

When metadata.tags is modified via updateMetadata, the Redis tag sets (maintained by tagKey()) are not updated. This causes:

  1. Stale lookups: findByTag returns records that no longer have those tags
  2. Missing results: Records with new tags won't be found

The save (lines 41-45) and delete (lines 155-159) methods correctly maintain tag sets, but updateMetadata does not.

Apply this diff to synchronize tag sets:

 async updateMetadata(
   id: string,
   metadata: Partial<ApiKeyMetadata>
 ): Promise<void> {
   const record = await this.findById(id);
   if (!record) {
     throw new Error(`API key with id ${id} not found`);
   }

+  const oldTags = record.metadata.tags || [];
+  const newTags = metadata.tags;
+
   record.metadata = { ...record.metadata, ...metadata };
-  await this.redis.set(this.key(id), JSON.stringify(record));
+  
+  const pipeline = this.redis.pipeline();
+  pipeline.set(this.key(id), JSON.stringify(record));

   if (metadata.revokedAt) {
-    await this.redis.del(this.hashKey(record.keyHash));
+    pipeline.del(this.hashKey(record.keyHash));
   }
+
+  // Update tag sets if tags changed
+  if (newTags !== undefined) {
+    // Remove from old tag sets
+    for (const tag of oldTags) {
+      pipeline.srem(this.tagKey(tag.toLowerCase()), id);
+    }
+    // Add to new tag sets
+    for (const tag of newTags) {
+      pipeline.sadd(this.tagKey(tag.toLowerCase()), id);
+    }
+  }
+
+  await pipeline.exec();
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a053b8a and daec392.

📒 Files selected for processing (8)
  • src/manager.ts (4 hunks)
  • src/storage/drizzle.test.ts (3 hunks)
  • src/storage/drizzle.ts (5 hunks)
  • src/storage/memory.test.ts (1 hunks)
  • src/storage/memory.ts (1 hunks)
  • src/storage/redis.test.ts (1 hunks)
  • src/storage/redis.ts (5 hunks)
  • src/types/storage-types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/storage/memory.ts
  • src/storage/redis.test.ts
  • src/storage/memory.test.ts
🧰 Additional context used
🧬 Code graph analysis (5)
src/storage/drizzle.test.ts (1)
examples/drizzle-schema.ts (1)
  • keys (15-19)
src/storage/drizzle.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/manager.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/storage/redis.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
src/types/storage-types.ts (1)
src/types/api-key-types.ts (1)
  • ApiKeyRecord (50-59)
🔇 Additional comments (10)
src/types/storage-types.ts (1)

27-39: LGTM! Clean API design for tag-based queries.

The new methods are well-documented and follow the established patterns in the Storage interface. The optional ownerId parameter provides flexible filtering.

src/storage/drizzle.test.ts (2)

191-252: LGTM! Comprehensive tag query test coverage.

The test suite properly validates:

  • Single tag searches
  • Multiple tag OR semantics (lines 203-218 correctly test that keys matching any tag are returned)
  • Owner-scoped filtering for both single and multiple tags

673-680: LGTM! Defensive array access prevents edge-case failures.

The .at(i) with null checks guards against race conditions in concurrent test scenarios. Good defensive programming.

Also applies to: 1123-1131

src/storage/drizzle.ts (2)

94-119: LGTM! Tag query logic correctly handles filtering and empty conditions.

The implementation properly:

  • Returns empty array when no conditions are provided (lines 105-107), preventing data exposure
  • Combines tag and owner filters with AND logic via and(...conditions)
  • Delegates single-tag queries to the multi-tag method for code reuse

72-72: LGTM! Defensive null checks prevent undefined access.

Also applies to: 82-82

src/manager.ts (3)

273-290: LGTM! Tags properly normalized to lowercase on creation.

Tag normalization happens at the manager layer before persistence, ensuring case-insensitive tag matching works correctly across all storage backends.


306-312: LGTM! Clean delegation to storage layer.

The new query methods follow the established pattern of delegating to storage, maintaining separation of concerns.


407-409: LGTM! Tag normalization consistent in rotate operation.

New tags are lowercased, or existing (already normalized) tags are preserved, maintaining consistency.

src/storage/redis.ts (2)

87-125: LGTM! Tag query correctly implements OR logic with optional owner filtering.

The implementation properly:

  • Uses SUNION for OR semantics across multiple tags (lines 94-97)
  • Applies owner filtering via client-side intersection when needed (lines 99-102)
  • Efficiently batches record fetches with pipeline (lines 108-120)

41-45: LGTM! Tag sets correctly maintained on save and delete.

The tag index maintenance is properly implemented in save, delete, and deleteByOwner operations.

Also applies to: 155-159, 176-180

@max-programming
Copy link
Contributor Author

@cubic-dev-ai re-run the review now

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Oct 25, 2025

@cubic-dev-ai re-run the review now

@max-programming I've started the AI code review. It'll take a few minutes to complete.

Copy link

@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.

1 issue found across 12 files

Prompt for AI agents (all 1 issues)

Understand the root cause of the following 1 issues and fix them.


<file name="src/storage/drizzle.ts">

<violation number="1" location="src/storage/drizzle.ts:89">
Dynamic IN clause incorrectly embeds a JavaScript array, which will bind as a single parameter and can produce invalid SQL or no matches. Use ANY(array) or expand placeholders with sql.join to properly bind each tag.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

@max-programming
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@izadoesdev izadoesdev merged commit 9b25308 into izadoesdev:main Oct 25, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants