Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add querybuilder #320

Merged
merged 2 commits into from
Nov 26, 2024
Merged

feat: add querybuilder #320

merged 2 commits into from
Nov 26, 2024

Conversation

MartianGreed
Copy link
Collaborator

@MartianGreed MartianGreed commented Nov 8, 2024

Closes #296

Introduced changes

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added relevant tests
  • Add a dedicated CI job for new examples
  • Performed self-review of the code

Summary by CodeRabbit

  • New Features

    • Introduced a new QueryBuilder module for constructing complex queries with nested namespaces and entities.
    • Added methods for defining entities and constraints, enhancing query flexibility.
    • Exported the queryBuilder module for easier access through the SDK's main entry point.
    • Improved example usage by transitioning to a structured query format with enhanced error handling.
  • Tests

    • Implemented a new test suite for the QueryBuilder, verifying instantiation and query construction functionality.
  • Chores

    • Updated the changelog to reflect a version change from 1.0.0-alpha.30 to 1.0.0.

Copy link

coderabbitai bot commented Nov 8, 2024

Walkthrough

The changes introduce a new QueryBuilder class to facilitate the construction of queries in a more structured manner within the SDK. The QueryBuilder allows users to define namespaces and entities with constraints using a builder pattern. Additionally, a new test suite for the QueryBuilder is created to validate its functionality, and the queryBuilder module is exported from the SDK's main entry point.

Changes

File Change Summary
packages/sdk/src/tests/queryBuilder.test.ts Added a new test suite for the QueryBuilder class with two test cases validating its functionality.
packages/sdk/src/index.ts Added export statement for queryBuilder: export * from "./queryBuilder";
packages/sdk/src/queryBuilder.ts Introduced QueryBuilder, Namespace, QueryEntity, and Constraint classes with various methods for query construction.
packages/sdk/CHANGELOG.md Updated version from 1.0.0-alpha.30 to 1.0.0, with minor formatting changes.
packages/sdk/src/example/index.ts Updated to import QueryBuilder and refactor example usage to utilize the new query construction approach.

Assessment against linked issues

Objective Addressed Explanation
Add builder pattern on top of existing SDK query patterns (#296)

🐰 In the meadow, a builder so bright,
Crafting queries, a pure delight!
With namespaces and entities in tow,
A structured way for queries to flow.
Hopping along, the tests now confirm,
Our new pattern is ready to affirm! 🌼

Warning

Rate limit exceeded

@MartianGreed has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 3 minutes and 13 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6a98b79 and 738beda.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@MartianGreed MartianGreed marked this pull request as ready for review November 20, 2024 09:17
@MartianGreed MartianGreed force-pushed the feat/query-builder branch 2 times, most recently from a28e949 to f9b283a Compare November 20, 2024 09:22
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: 5

🧹 Outside diff range and nitpick comments (6)
packages/sdk/src/__tests__/queryBuilder.test.ts (1)

10-35: Expand test coverage for complex scenarios

While the current example is good, consider adding tests for:

  1. Different condition types (gt, lt, contains, etc.)
  2. Array conditions
  3. Multiple conditions combined with AND/OR
  4. Deeply nested structures

Also, consider adding JSDoc comments to document the expected query structure and usage patterns:

/**
 * Example of building a complex query with multiple namespaces and conditions.
 * The resulting query structure follows the pattern:
 * {
 *   namespace: {
 *     entity: {
 *       $: {
 *         where: { field: { $operator: value } }
 *       }
 *     }
 *   }
 * }
 */
it("should work with example", () => {
packages/sdk/src/queryBuilder.ts (5)

9-17: Error handling for the callback functions in namespace method.

Consider adding error handling around the callback cb in the namespace method to catch potential exceptions that might occur during namespace configuration. This can prevent unexpected crashes and make debugging easier.

Example:

 public namespace(
     name: string,
     cb: (ns: Namespace<T>) => void
 ): Namespace<T> {
     const ns = new Namespace(this, name);
     this.namespaces.set(name, ns);
-    cb(ns);
+    try {
+        cb(ns);
+    } catch (error) {
+        console.error(`Error configuring namespace ${name}:`, error);
+    }
     return ns;
 }

19-47: Refactor the build method for better readability and efficiency.

The build method is quite complex due to deeply nested loops and object constructions. Refactoring it can improve readability and maintainability. Consider extracting parts of the logic into helper methods or simplifying the object merging.

Example:

+private buildConstraints(entityObj: QueryEntity<T>): Record<string, any> {
+    const constraints: Record<string, Record<string, any>> = {};
+    for (const [field, constraint] of entityObj.constraints) {
+        constraints[field] = {
+            [constraint.operator]: constraint.value,
+        };
+    }
+    return constraints;
+}

 public build(): QueryType<T> {
     const qt: Record<
         string,
         Record<
             string,
             { $: { where: Record<string, Record<string, any>> } }
         >
     > = {};
     for (const [ns, namespace] of this.namespaces) {
         qt[ns] = {};
         for (const [entity, entityObj] of namespace.entities) {
-            const constraints: Record<string, Record<string, any>> = {};
-            for (const [field, constraint] of entityObj.constraints) {
-                constraints[field] = {
-                    [`${constraint.operator}`]: constraint.value,
-                };
-            }
+            const constraints = this.buildConstraints(entityObj);
             qt[ns][entity] = {
                 $: {
                     where: {
                         ...(qt[ns]?.[entity]?.$?.where ?? {}),
                         ...constraints,
                     },
                 },
             };
         }
     }
     return qt as QueryType<T>;
 }

136-138: Unnecessary use of .toString() in operator getter.

Since the Operator enum values are strings, calling .toString() is redundant. You can return this._operator directly.

Apply this diff:

 get operator(): string {
-    return this._operator.toString();
+    return this._operator;
 }

145-153: Consider extending the Operator enum for additional query capabilities.

Depending on the requirements, you might want to include additional operators like $in, $nin, $exists, or $regex to support more complex queries.

Example:

 enum Operator {
     is = "$is",
     eq = "$eq",
     neq = "$neq",
     gt = "$gt",
     gte = "$gte",
     lt = "$lt",
     lte = "$lte",
+    in = "$in",
+    nin = "$nin",
+    exists = "$exists",
+    regex = "$regex",
 }

And add corresponding methods in QueryEntity:

 class QueryEntity<T extends SchemaType> {
     // Existing methods...

+    public in(field: string, values: any[]): QueryEntity<T> {
+        this.constraints.set(field, new Constraint(Operator.in, values));
+        return this;
+    }
+
+    public nin(field: string, values: any[]): QueryEntity<T> {
+        this.constraints.set(field, new Constraint(Operator.nin, values));
+        return this;
+    }
+
+    public exists(field: string, value: boolean): QueryEntity<T> {
+        this.constraints.set(field, new Constraint(Operator.exists, value));
+        return this;
+    }
+
+    public regex(field: string, pattern: string): QueryEntity<T> {
+        this.constraints.set(field, new Constraint(Operator.regex, pattern));
+        return this;
+    }
 }

89-122: Potential duplication in constraint methods in QueryEntity.

The methods is, eq, neq, gt, gte, lt, lte are very similar. Consider refactoring them to reduce code duplication.

Example:

+private addConstraint(field: string, operator: Operator, value: any): QueryEntity<T> {
+    this.constraints.set(field, new Constraint(operator, value));
+    return this;
+}

 public is(field: string, value: any): QueryEntity<T> {
-    this.constraints.set(field, new Constraint(Operator.is, value));
-    return this;
+    return this.addConstraint(field, Operator.is, value);
 }

 // Repeat for other methods...

 public eq(field: string, value: any): QueryEntity<T> {
-    this.constraints.set(field, new Constraint(Operator.eq, value));
-    return this;
+    return this.addConstraint(field, Operator.eq, value);
 }

 // And so on for neq, gt, gte, lt, lte
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b47c945 and f9b283a.

📒 Files selected for processing (3)
  • packages/sdk/src/__tests__/queryBuilder.test.ts (1 hunks)
  • packages/sdk/src/index.ts (1 hunks)
  • packages/sdk/src/queryBuilder.ts (1 hunks)
🔇 Additional comments (4)
packages/sdk/src/__tests__/queryBuilder.test.ts (2)

1-2: LGTM! Imports are clean and appropriate.


1-36: Consider implementing a comprehensive test strategy

To ensure robust testing of the QueryBuilder feature:

  1. Create separate test suites for different aspects (basic operations, error handling, complex queries)
  2. Add integration tests with actual SDK usage
  3. Consider property-based testing for query structure validation
  4. Add performance tests for large query structures

This will help ensure the reliability of this core SDK feature.

Would you like assistance in generating a comprehensive test suite structure?

packages/sdk/src/index.ts (1)

12-12: LGTM! Verify documentation updates.

The export statement is correctly placed and aligns with the module's export pattern. Since this exposes new functionality to SDK users, ensure that:

  • The SDK's public API documentation is updated
  • Examples demonstrating the QueryBuilder usage are added
  • The changelog reflects this addition
packages/sdk/src/queryBuilder.ts (1)

2-7: QueryBuilder class is well-structured and initialized properly.

The QueryBuilder class is correctly defined with a proper initialization of the namespaces map. The use of generics with T extends SchemaType ensures type safety throughout the query building process.

Comment on lines +5 to +9
it("should be implemented", () => {
const query = new QueryBuilder().build();
expect(query).toStrictEqual({});
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance basic test coverage

The current test case "should be implemented" could be improved:

  1. Rename to something more descriptive like "should create empty query when no parameters are provided"
  2. Add test cases for edge cases and error scenarios

Consider adding these test cases:

it("should throw error when namespace name is empty", () => {
    expect(() => new QueryBuilder().namespace("", () => {})).toThrow();
});

it("should throw error when entity name is empty", () => {
    expect(() => 
        new QueryBuilder().namespace("test", n => n.entity("", () => {}))
    ).toThrow();
});

Comment on lines 82 to 95
constructor(
private parent: Namespace<T>,
private name: string
) {
this.constraints = new Map<string, Constraint<T>>();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unused parameter name in QueryEntity constructor.

The name parameter in the QueryEntity constructor is not stored or used within the class. Store it as a property if it's intended for future use or remove it if unnecessary.

Apply this diff to store the name property:

 class QueryEntity<T extends SchemaType> {
     constraints: Map<string, Constraint<T>>;
+    private name: string;

     constructor(
         private parent: Namespace<T>,
-        private name: string
+        name: string
     ) {
+        this.name = name;
         this.constraints = new Map<string, Constraint<T>>();
     }
 }
📝 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
constructor(
private parent: Namespace<T>,
private name: string
) {
this.constraints = new Map<string, Constraint<T>>();
}
constructor(
private parent: Namespace<T>,
name: string
) {
this.name = name;
this.constraints = new Map<string, Constraint<T>>();
}

Comment on lines 129 to 143
class Constraint<T extends SchemaType> {
constructor(
private parent: QueryEntity<T>,
private _operator: Operator,
private _value: any
) {}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove unused parameter parent in Constraint constructor.

The parent parameter in the Constraint constructor is not used within the class. Removing it simplifies the code and avoids potential confusion.

Apply this diff:

 class Constraint<T extends SchemaType> {
     constructor(
-        private parent: QueryEntity<T>,
         private _operator: Operator,
         private _value: any
     ) {}
 }

Also, update the instantiation of Constraint objects in QueryEntity methods:

 public is(field: string, value: any): QueryEntity<T> {
-    this.constraints.set(field, new Constraint(this, Operator.is, value));
+    this.constraints.set(field, new Constraint(Operator.is, value));
     return this;
 }

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

Comment on lines 53 to 63
constructor(
private parent: QueryBuilder<T>,
private name: string
) {
this.entities = new Map<string, QueryEntity<T>>();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unused parameter name in Namespace constructor.

The name parameter in the Namespace constructor is not stored or used within the class. If the namespace name is needed later, consider storing it as a class property. Otherwise, remove it to avoid confusion.

Apply this diff to store the name property:

 class Namespace<T extends SchemaType> {
     entities: Map<string, QueryEntity<T>>;
+    private name: string;

     constructor(
         private parent: QueryBuilder<T>,
-        private name: string
+        name: string
     ) {
+        this.name = name;
         this.entities = new Map<string, QueryEntity<T>>();
     }
 }
📝 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
constructor(
private parent: QueryBuilder<T>,
private name: string
) {
this.entities = new Map<string, QueryEntity<T>>();
}
constructor(
private parent: QueryBuilder<T>,
name: string
) {
this.name = name;
this.entities = new Map<string, QueryEntity<T>>();
}

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

🧹 Outside diff range and nitpick comments (3)
packages/sdk/src/__example__/index.ts (1)

Line range hint 1-156: Consider restructuring the examples for better clarity

While the example demonstrates the QueryBuilder functionality, consider the following improvements:

  1. Split the examples into separate files for different use cases (e.g., basic-queries.ts, complex-queries.ts)
  2. Move the schema definitions to a separate schema.ts file
  3. Add examples demonstrating more QueryBuilder features like:
    • Different comparison operators
    • Combining conditions with AND/OR
    • Pagination/limiting results
    • Sorting options

This would make the examples more maintainable and educational.

Would you like me to help create a PR for these improvements?

packages/sdk/src/queryBuilder.ts (2)

28-34: Simplify complex type definition

Consider extracting the nested type definition to improve readability:

type QueryConstraint = Record<string, Record<string, any>>;
type QueryStructure = Record<string, Record<string, { $: { where: QueryConstraint } }>>

152-160: Add documentation for operators

The Operator enum would benefit from JSDoc comments explaining the behavior of each operator:

 enum Operator {
+    /** Exact match comparison */
     is = "$is",
+    /** Equality comparison */
     eq = "$eq",
+    /** Not equal comparison */
     neq = "$neq",
+    /** Greater than comparison */
     gt = "$gt",
+    /** Greater than or equal comparison */
     gte = "$gte",
+    /** Less than comparison */
     lt = "$lt",
+    /** Less than or equal comparison */
     lte = "$lte",
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f9b283a and 2438663.

📒 Files selected for processing (4)
  • packages/sdk/CHANGELOG.md (1 hunks)
  • packages/sdk/src/__example__/index.ts (2 hunks)
  • packages/sdk/src/__tests__/queryBuilder.test.ts (1 hunks)
  • packages/sdk/src/queryBuilder.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/sdk/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/src/tests/queryBuilder.test.ts
🔇 Additional comments (2)
packages/sdk/src/__example__/index.ts (1)

4-4: LGTM: QueryBuilder import added correctly

The QueryBuilder import is appropriately placed alongside the existing init import.

packages/sdk/src/queryBuilder.ts (1)

3-8: Well-designed type-safe nested key access!

The NestedKeyOf type elegantly handles both direct and nested property access, ensuring type safety when accessing nested fields in the schema.

Comment on lines +114 to +132
const query = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n.entity("player", (e) => e.eq("name", "Alice"))
);

db.subscribeEntityQuery(query.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying todos and goals:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Queried todos and goals:",
resp.data.map((a) => a.models.world)
);
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix misleading error and log messages

The error and log messages seem to be copied from a different example (mentioning "todos and goals" while the code is actually querying players).

Apply this diff to fix the messages and improve response handling:

     db.subscribeEntityQuery(query.build(), (resp) => {
         if (resp.error) {
             console.error(
-                "Error querying todos and goals:",
+                "Error querying players:",
                 resp.error.message
             );
             return;
         }
         if (resp.data) {
             console.log(
-                "Queried todos and goals:",
+                "Queried players:",
-                resp.data.map((a) => a.models.world)
+                resp.data.map((a) => a.models.world.player)
             );
         }
     });
📝 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 query = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n.entity("player", (e) => e.eq("name", "Alice"))
);
db.subscribeEntityQuery(query.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying todos and goals:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Queried todos and goals:",
resp.data.map((a) => a.models.world)
);
}
});
const query = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n.entity("player", (e) => e.eq("name", "Alice"))
);
db.subscribeEntityQuery(query.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying players:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Queried players:",
resp.data.map((a) => a.models.world.player)
);
}
});

Comment on lines +134 to +156
try {
const eq = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n
.entity("item", (e) =>
e.eq("type", "sword").lt("durability", 5)
)
.entity("game", (e) => e.eq("status", "completed"))
);
const entities = await db.getEntities(eq.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying todos and goals:",
"Error querying completed important todos:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Queried todos and goals:",
resp.data.map((a) => a.models.world)
"Completed important todos:",
resp.data.map((a) => a.models)
);
}
}
);
// Example usage of getEntities with where clause
try {
const entities = await db.getEntities(
{
world: {
item: {
$: {
where: {
type: { $eq: "sword" },
durability: { $lt: 5 },
},
},
},
game: {
$: {
where: {
status: { $eq: "completed" },
},
},
},
},
},
(resp) => {
if (resp.error) {
console.error(
"Error querying completed important todos:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Completed important todos:",
resp.data.map((a) => a.models)
);
}
}
);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix misleading messages and improve type safety

The error and log messages reference "todos" instead of the actual entities being queried (items and games). Additionally, the response handling could be more specific.

Apply these changes to improve the code:

     try {
         const eq = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
             n
                 .entity("item", (e) =>
                     e.eq("type", "sword").lt("durability", 5)
                 )
                 .entity("game", (e) => e.eq("status", "completed"))
         );
-        const entities = await db.getEntities(eq.build(), (resp) => {
+        const entities: { world: { item: ItemModel[], game: GameModel[] } } = await db.getEntities(eq.build(), (resp) => {
             if (resp.error) {
                 console.error(
-                    "Error querying completed important todos:",
+                    "Error querying items and games:",
                     resp.error.message
                 );
                 return;
             }
             if (resp.data) {
                 console.log(
-                    "Completed important todos:",
+                    "Query results:",
+                    "Low durability items:",
+                    resp.data.map((a) => a.models.world.item),
+                    "Completed games:",
+                    resp.data.map((a) => a.models.world.game)
-                    resp.data.map((a) => a.models)
                 );
             }
         });
📝 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
try {
const eq = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n
.entity("item", (e) =>
e.eq("type", "sword").lt("durability", 5)
)
.entity("game", (e) => e.eq("status", "completed"))
);
const entities = await db.getEntities(eq.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying todos and goals:",
"Error querying completed important todos:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Queried todos and goals:",
resp.data.map((a) => a.models.world)
"Completed important todos:",
resp.data.map((a) => a.models)
);
}
}
);
// Example usage of getEntities with where clause
try {
const entities = await db.getEntities(
{
world: {
item: {
$: {
where: {
type: { $eq: "sword" },
durability: { $lt: 5 },
},
},
},
game: {
$: {
where: {
status: { $eq: "completed" },
},
},
},
},
},
(resp) => {
if (resp.error) {
console.error(
"Error querying completed important todos:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Completed important todos:",
resp.data.map((a) => a.models)
);
}
}
);
});
try {
const eq = new QueryBuilder<MockSchemaType>().namespace("world", (n) =>
n
.entity("item", (e) =>
e.eq("type", "sword").lt("durability", 5)
)
.entity("game", (e) => e.eq("status", "completed"))
);
const entities: { world: { item: ItemModel[], game: GameModel[] } } = await db.getEntities(eq.build(), (resp) => {
if (resp.error) {
console.error(
"Error querying items and games:",
resp.error.message
);
return;
}
if (resp.data) {
console.log(
"Query results:",
"Low durability items:",
resp.data.map((a) => a.models.world.item),
"Completed games:",
resp.data.map((a) => a.models.world.game)
);
}
});

Comment on lines +21 to +22
const ns = new Namespace(this);
this.namespaces.set(name as string, ns);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Pass namespace name to constructor

The namespace name is not passed to the Namespace constructor, which was flagged in a previous review. This is needed for proper namespace hierarchy.

-const ns = new Namespace(this);
+const ns = new Namespace(this, name as string);

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

Comment on lines +137 to +152
class Constraint {
constructor(
private _operator: Operator,
private _value: any
) {}

get operator(): string {
return this._operator.toString();
}

get value(): any {
return this._value;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add type safety for constraint values

The Constraint class accepts any value type. Consider adding generic type parameter for better type safety:

-class Constraint {
+class Constraint<T> {
     constructor(
         private _operator: Operator,
-        private _value: any
+        private _value: T
     ) {}
 
     get operator(): string {
         return this._operator.toString();
     }
 
-    get value(): any {
+    get value(): T {
         return this._value;
     }
 }
📝 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
class Constraint {
constructor(
private _operator: Operator,
private _value: any
) {}
get operator(): string {
return this._operator.toString();
}
get value(): any {
return this._value;
}
}
class Constraint<T> {
constructor(
private _operator: Operator,
private _value: T
) {}
get operator(): string {
return this._operator.toString();
}
get value(): T {
return this._value;
}
}

Comment on lines 97 to 132
public is(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.is, value));
return this;
}

public eq(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.eq, value));
return this;
}

public neq(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.neq, value));
return this;
}

public gt(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.gt, value));
return this;
}

public gte(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.gte, value));
return this;
}

public lt(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.lt, value));
return this;
}

public lte(field: string, value: any): QueryEntity<T> {
this.constraints.set(field, new Constraint(Operator.lte, value));
return this;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce code duplication in operator methods

The operator methods (is, eq, neq, gt, gte, lt, lte) all follow the same pattern. Consider refactoring to reduce duplication:

+private addConstraint(field: string, operator: Operator, value: any): QueryEntity<T> {
+    this.constraints.set(field, new Constraint(operator, value));
+    return this;
+}

 public is(field: string, value: any): QueryEntity<T> {
-    this.constraints.set(field, new Constraint(Operator.is, value));
-    return this;
+    return this.addConstraint(field, Operator.is, value);
 }

 public eq(field: string, value: any): QueryEntity<T> {
-    this.constraints.set(field, new Constraint(Operator.eq, value));
-    return this;
+    return this.addConstraint(field, Operator.eq, value);
 }
 // Apply similar changes to other operator methods

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

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

🧹 Outside diff range and nitpick comments (2)
packages/sdk/src/__example__/index.ts (1)

114-156: Consider expanding examples for common query patterns

While the current examples effectively demonstrate basic and advanced QueryBuilder usage, consider adding examples for other common query patterns:

  • Sorting and pagination
  • Complex logical conditions (AND/OR combinations)
  • Aggregation queries (if supported)
  • Error handling best practices

This would make the example file more valuable as a reference for SDK users.

packages/sdk/src/queryBuilder.ts (1)

27-55: Consider improving type safety in build method

The build method uses generic Record types which could be made more specific.

Consider using a more specific type for the query structure:

interface QueryStructure<T> {
    [namespace: string]: {
        [entity: string]: {
            $: {
                where: Record<string, Record<string, any>>
            }
        }
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 2438663 and 6a98b79.

📒 Files selected for processing (4)
  • packages/sdk/CHANGELOG.md (1 hunks)
  • packages/sdk/src/__example__/index.ts (2 hunks)
  • packages/sdk/src/__tests__/queryBuilder.test.ts (1 hunks)
  • packages/sdk/src/queryBuilder.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/sdk/CHANGELOG.md
  • packages/sdk/src/tests/queryBuilder.test.ts
🔇 Additional comments (8)
packages/sdk/src/__example__/index.ts (5)

4-4: LGTM!

The import statement correctly includes the new QueryBuilder alongside the existing init import, aligning with the PR's objective of introducing the query builder pattern.


114-116: LGTM! Clean QueryBuilder implementation

The QueryBuilder usage demonstrates a clear and type-safe way to construct queries, which is a significant improvement over the previous query pattern. The namespace and entity structure is well-defined and easy to understand.


118-132: Previous review comment about misleading messages is still valid

The error and log messages still reference "todos and goals" instead of "players".


135-141: LGTM! Excellent demonstration of advanced QueryBuilder features

The code effectively demonstrates advanced QueryBuilder capabilities:

  • Multiple entity queries in a single builder
  • Chained conditions (eq + lt)
  • Clear and readable query structure

142-156: Previous review comment about misleading messages and type safety is still valid

The error and log messages still reference "todos" instead of the actual entities being queried (items and games).

packages/sdk/src/queryBuilder.ts (3)

3-8: LGTM! Well-structured type definition for nested object paths.

The NestedKeyOf type effectively handles nested object paths with proper type safety.


154-162: LGTM! Well-defined operator enum

The operators are well-defined and follow common query operator conventions with proper $ prefix.


75-77: ⚠️ Potential issue

Fix namespace hierarchy implementation

The current implementation creates sibling namespaces instead of nested ones.

Apply this diff:

 public namespace(ns: string, cb: (ns: Namespace<T>) => void): Namespace<T> {
-    return this.parent.namespace(ns, cb);
+    const nestedNamespace = new Namespace(this.parent, `${this.name}.${ns}`);
+    this.entities.set(ns, nestedNamespace);
+    cb(nestedNamespace);
+    return nestedNamespace;
 }

Likely invalid or redundant comment.

Comment on lines +97 to +123
public is(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.is);
}

public eq(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.eq);
}

public neq(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.neq);
}

public gt(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.gt);
}

public gte(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.gte);
}

public lt(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.lt);
}

public lte(field: string, value: any): QueryEntity<T> {
return this.addConstraint(field, value, Operator.lte);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation for constraint values

The constraint methods accept any value without validation, which could lead to injection vulnerabilities.

Consider adding:

  1. Type validation for values based on field types
  2. Sanitization for string values
  3. Range validation for numeric values

Example implementation:

private validateConstraintValue(field: string, value: any): void {
    // Add validation based on field type
    if (typeof value === 'string') {
        // Add string sanitization
        if (value.length > MAX_STRING_LENGTH) {
            throw new Error(`Value for field "${field}" exceeds maximum length`);
        }
    }
    // Add more validation as needed
}

Then update the addConstraint method:

 private addConstraint(field: string, value: any, op: Operator): QueryEntity<T> {
+    this.validateConstraintValue(field, value);
     this.constraints.set(field, new Constraint(op, value));
     return this;
 }

Comment on lines +58 to +63
class Namespace<T extends SchemaType> {
entities: Map<string, QueryEntity<T>>;

constructor(private parent: QueryBuilder<T>) {
this.entities = new Map<string, QueryEntity<T>>();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add name property to Namespace class

The name property is missing but required for proper namespace hierarchy.

Apply this diff:

 class Namespace<T extends SchemaType> {
     entities: Map<string, QueryEntity<T>>;
+    private name: string;

-    constructor(private parent: QueryBuilder<T>) {
+    constructor(private parent: QueryBuilder<T>, name: string) {
+        this.name = name;
         this.entities = new Map<string, QueryEntity<T>>();
     }
 }
📝 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
class Namespace<T extends SchemaType> {
entities: Map<string, QueryEntity<T>>;
constructor(private parent: QueryBuilder<T>) {
this.entities = new Map<string, QueryEntity<T>>();
}
class Namespace<T extends SchemaType> {
entities: Map<string, QueryEntity<T>>;
private name: string;
constructor(private parent: QueryBuilder<T>, name: string) {
this.name = name;
this.entities = new Map<string, QueryEntity<T>>();
}

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

🧹 Outside diff range and nitpick comments (2)
packages/sdk/src/__example__/index.ts (1)

Line range hint 1-162: Consider enhancing the example file's educational value

While this example effectively demonstrates the QueryBuilder functionality, consider these improvements to make it more educational:

  1. Add a section header comment explaining the purpose of each example
  2. Include comments showing the equivalent raw query structure that the QueryBuilder replaces
  3. Add examples of other QueryBuilder features mentioned in the PR objectives

Example structure:

// Basic QueryBuilder Example
// Demonstrates simple entity filtering
// Raw query equivalent:
// { world: { player: { name: { $eq: "Alice" } } } }
const simpleQuery = new QueryBuilder()...

// Advanced QueryBuilder Example
// Demonstrates multiple entity queries with conditions
// Raw query equivalent:
// { world: { 
//   item: { type: { $eq: "sword" }, durability: { $lt: 5 } },
//   game: { status: { $eq: "completed" } }
// } }
const advancedQuery = new QueryBuilder()...
packages/sdk/src/queryBuilder.ts (1)

10-56: Add JSDoc documentation for public API.

The QueryBuilder class and its public methods lack documentation:

+/**
+ * A builder for creating type-safe subscription queries.
+ * @template T The schema type that defines the structure of available entities
+ */
export class QueryBuilder<T extends SchemaType> {
    // ...

+   /**
+    * Creates a new namespace for grouping related entities.
+    * @param name The name of the namespace
+    * @param cb A callback function for configuring the namespace
+    * @returns The created namespace
+    */
    public namespace(
        name: keyof T,
        cb: (ns: Namespace<T>) => void
    ): Namespace<T> {
        // ...
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6a98b79 and 738beda.

📒 Files selected for processing (5)
  • packages/sdk/CHANGELOG.md (1 hunks)
  • packages/sdk/src/__example__/index.ts (2 hunks)
  • packages/sdk/src/__tests__/queryBuilder.test.ts (1 hunks)
  • packages/sdk/src/index.ts (1 hunks)
  • packages/sdk/src/queryBuilder.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/sdk/CHANGELOG.md
  • packages/sdk/src/tests/queryBuilder.test.ts
  • packages/sdk/src/index.ts
🔇 Additional comments (5)
packages/sdk/src/__example__/index.ts (3)

4-4: LGTM!

The import statement correctly includes the new QueryBuilder class alongside the existing init import.


114-132: QueryBuilder implementation looks good, but error messages need updating

The QueryBuilder implementation correctly demonstrates the new query pattern for filtering players. However, the error messages still need to be updated.

The previous review comment about fixing the misleading messages is still valid. Please refer to the existing comment for the suggested changes to error messages and response handling.


134-156: Good QueryBuilder demonstration, needs message updates and type safety

The implementation effectively demonstrates advanced QueryBuilder features:

  • Multiple entity queries (items and games)
  • Condition chaining (.eq() and .lt())
  • Namespace and entity nesting

However, there are still some issues to address:

The previous review comment about fixing misleading messages and improving type safety is still valid. Please refer to the existing comment for the suggested improvements.

Additionally, consider adding a code comment explaining this complex query pattern for documentation purposes:

// Example: Query low durability swords and completed games
// This demonstrates how to combine multiple entity queries within a namespace
packages/sdk/src/queryBuilder.ts (2)

1-8: LGTM! Well-designed type definition for nested object keys.

The NestedKeyOf type correctly implements recursive type generation for nested object properties, enabling type-safe dot-notation paths.


84-89: 🛠️ Refactor suggestion

Improve type safety in QueryEntity.

The constraints Map uses a non-generic Constraint type:

class QueryEntity<T extends SchemaType> {
-   constraints: Map<string, Constraint>;
+   constraints: Map<string, Constraint<unknown>>;

Likely invalid or redundant comment.

Comment on lines +125 to +132
private addConstraint(
field: string,
value: any,
op: Operator
): QueryEntity<T> {
this.constraints.set(field, new Constraint(op, value));
return this;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation in addConstraint method.

The method accepts any value without validation:

private addConstraint(
    field: string,
    value: any,
    op: Operator
): QueryEntity<T> {
+   if (value === undefined || value === null) {
+       throw new Error(`Invalid value for field "${field}": value cannot be null or undefined`);
+   }
+   if (typeof field !== 'string' || field.trim() === '') {
+       throw new Error('Field name must be a non-empty string');
+   }
    this.constraints.set(field, new Constraint(op, value));
    return this;
}
📝 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
private addConstraint(
field: string,
value: any,
op: Operator
): QueryEntity<T> {
this.constraints.set(field, new Constraint(op, value));
return this;
}
private addConstraint(
field: string,
value: any,
op: Operator
): QueryEntity<T> {
if (value === undefined || value === null) {
throw new Error(`Invalid value for field "${field}": value cannot be null or undefined`);
}
if (typeof field !== 'string' || field.trim() === '') {
throw new Error('Field name must be a non-empty string');
}
this.constraints.set(field, new Constraint(op, value));
return this;
}

Comment on lines +27 to +55
public build(): SubscriptionQueryType<T> {
const qt: Record<
string,
Record<
string,
{ $: { where: Record<string, Record<string, any>> } }
>
> = {};
for (const [ns, namespace] of this.namespaces) {
qt[ns] = {};
for (const [entity, entityObj] of namespace.entities) {
const constraints: Record<string, Record<string, any>> = {};
for (const [field, constraint] of entityObj.constraints) {
constraints[field] = {
[`${constraint.operator}`]: constraint.value,
};
}
qt[ns][entity] = {
$: {
where: {
...(qt[ns]?.[entity]?.$?.where ?? {}),
...constraints,
},
},
};
}
}
return qt as SubscriptionQueryType<T>;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety in build method.

The build method uses loose typing with Record<string, any>:

public build(): SubscriptionQueryType<T> {
-   const qt: Record<
-       string,
-       Record<
-           string,
-           { $: { where: Record<string, Record<string, any>> } }
-       >
-   > = {};
+   const qt: {
+       [K in keyof T]?: {
+           [E in NestedKeyOf<T[K]>]?: {
+               $: {
+                   where: Record<string, Record<string, unknown>>
+               }
+           }
+       }
+   } = {};
    // ... rest of the method
}

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

Comment on lines +75 to +77
public namespace(ns: string, cb: (ns: Namespace<T>) => void): Namespace<T> {
return this.parent.namespace(ns, cb);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve namespace hierarchy implementation.

The current implementation creates sibling namespaces instead of nested ones:

public namespace(ns: string, cb: (ns: Namespace<T>) => void): Namespace<T> {
-   return this.parent.namespace(ns, cb);
+   const fullName = `${this.name}.${ns}`;
+   return this.parent.namespace(fullName as keyof T, cb);
}

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

Comment on lines +17 to +25
public namespace(
name: keyof T,
cb: (ns: Namespace<T>) => void
): Namespace<T> {
const ns = new Namespace(this);
this.namespaces.set(name as string, ns);
cb(ns);
return ns;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation and proper namespace initialization.

The namespace method needs validation and proper initialization:

public namespace(
    name: keyof T,
    cb: (ns: Namespace<T>) => void
): Namespace<T> {
+   const nameStr = String(name);
+   if (this.namespaces.has(nameStr)) {
+       throw new Error(`Namespace "${nameStr}" already exists`);
+   }
-   const ns = new Namespace(this);
+   const ns = new Namespace(this, nameStr);
    this.namespaces.set(name as string, ns);
    cb(ns);
    return ns;
}
📝 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
public namespace(
name: keyof T,
cb: (ns: Namespace<T>) => void
): Namespace<T> {
const ns = new Namespace(this);
this.namespaces.set(name as string, ns);
cb(ns);
return ns;
}
public namespace(
name: keyof T,
cb: (ns: Namespace<T>) => void
): Namespace<T> {
const nameStr = String(name);
if (this.namespaces.has(nameStr)) {
throw new Error(`Namespace "${nameStr}" already exists`);
}
const ns = new Namespace(this, nameStr);
this.namespaces.set(name as string, ns);
cb(ns);
return ns;
}

@MartianGreed MartianGreed merged commit e7715f6 into main Nov 26, 2024
3 checks passed
@MartianGreed MartianGreed deleted the feat/query-builder branch November 26, 2024 09:59
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.

[Feature]: Add builder pattern on top of existing SDK query patterns
1 participant