Skip to content

Commit

Permalink
Merge pull request #33 from koyopro/feature/search
Browse files Browse the repository at this point in the history
Flexible Search
  • Loading branch information
koyopro authored Aug 9, 2024
2 parents f0f276d + 35c6f3a commit 642aef2
Show file tree
Hide file tree
Showing 15 changed files with 651 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/accel-record-core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export class RecordNotFound extends Error {}
export class AttributeNotFound extends Error {}
2 changes: 2 additions & 0 deletions packages/accel-record-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ModelMeta } from "./meta.js";
import { Dirty } from "./model/dirty.js";
import { Import } from "./model/import.js";
import { Naming } from "./model/naming.js";
import { Searchable } from "./model/search.js";
import { Serialization } from "./model/serialization.js";
import { Validations } from "./model/validations.js";
import { Persistence } from "./persistence.js";
Expand Down Expand Up @@ -71,6 +72,7 @@ export class Model extends Mix(
Naming,
Persistence,
Query,
Searchable,
Serialization,
Transaction,
Validations
Expand Down
88 changes: 88 additions & 0 deletions packages/accel-record-core/src/model/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Model } from "../index.js";
import { Search } from "../search/index.js";

/**
* Represents a class for handling search parameters.
*
* This class is intended to be inherited by the Model class.
*/
export class Searchable {
/**
* Creates a new search instance.
* @param params - The search parameters.
* @returns A new search instance.
* @example
* ```ts
* const search = User.search({ name_eq: "foo" });
* const users = search.result();
* ```
* @details
* The search parameters are used to filter the results of the search. \
* The search parameters are specified as key-value pairs. \
* The key is the name of the attribute to search for.
* The key can include associations. \
* The value is the value to search for.
*
* The search parameters can include the following:
* - `*_eq` - The attribute is equal to the value.
* - `*_cont` - The attribute contains the value.
* - `*_start` - The attribute starts with the value.
* - `*_end` - The attribute ends with the value.
* - `*_matches` - The attribute matches the value (Use the `LIKE` clause).
* - `*_lt` - The attribute is less than the value.
* - `*_lte` - The attribute is less than or equal to the value.
* - `*_gt` - The attribute is greater than the value.
* - `*_gte` - The attribute is greater than or equal to the value.
* - `*_in` - The attribute is in the list of values.
* - `*_true` - The attribute is true.
* - `*_false` - The attribute is false.
* - `*_null` - The attribute is null.
* - `*_blank` - The attribute is blank.
* - `*_present` - The attribute is present.
*
* The search parameters can also include the following modifiers:
* - `not` (`does_not`) - The value must not match.
* - `all` - All of the values must match.
* - `any` - Any of the values must match.
*
* The search parameters can also include the following logical operators:
* - `and` - The values must match all of the attributes.
* - `or` - The values must match any of the attributes.
* @example
* ```ts
* const search = User.search({
* profile_bio_cont: "foo", // with association
* age_not_null: 1, // with `not` modifier
* email_or_name_cont_any: ["bar", "baz"], // with `or` operator and `any` modifier
* });
* const users = search.result();
* ```
* @details
* The keys of the search parameters can include the names of the searchable scopes defined in the `searchableScopes` array. \
* The values of the search parameters specify the values to search for.
* @example
* ```ts
* class UserModel extends ApplicationRecord {
* static bio_cont(value: string) {
* return this.joins("profile").where({
* profile: { bio: { 'contains': value } },
* });
* }
* static searchableScopes = ["bio_cont"];
* }
* ```
* // The `bio_cont` scope can be used in the search parameters.
* ```ts
* const search = User.search({ bio_cont: "foo" });
* const users = search.result();
* ```
*/
static search<T extends typeof Model>(this: T, params: Record<string, any>) {
return new Search(this, params);
}

/**
* An array of searchable scopes used in the search() method.
*/
static searchableScopes: string[] = [];
}
11 changes: 11 additions & 0 deletions packages/accel-record-core/src/relation/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ export class RelationBase {
}
});
}
for (const where of this.options.orWhereNots) {
q = q.orWhere(function (this: any) {
for (const w of where) {
if (Array.isArray(w)) {
this.whereNot(...w);
} else {
this.whereNot(w);
}
}
});
}
if (this.options.limit) q = q.limit(this.options.limit);
if (this.options.offset) q = q.offset(this.options.offset);
for (const [column, direction] of this.options.orders ?? []) {
Expand Down
4 changes: 3 additions & 1 deletion packages/accel-record-core/src/relation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { Association } from "./association.js";
import { RelationBase } from "./base.js";
import { Batches } from "./batches.js";
import { Calculations } from "./calculations.js";
import { getDefaultOptions, Options } from "./options.js";
import { Merge } from "./merge.js";
import { getDefaultOptions, Options } from "./options.js";
import { Query } from "./query.js";
import { Searchable } from "./search.js";
import { Where } from "./where.js";

export { Options } from "./options.js";
Expand All @@ -24,6 +25,7 @@ export class Relation<T, M extends ModelMeta> extends Mix(
Merge,
Query,
RelationBase,
Searchable,
Where
) {
protected counter = 0;
Expand Down
2 changes: 2 additions & 0 deletions packages/accel-record-core/src/relation/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Options = {
whereNots: any[];
whereRaws: [string, any[]][];
orWheres: any[];
orWhereNots: any[];
orders: [string, "asc" | "desc"][];
offset: number | undefined;
limit: number | undefined;
Expand All @@ -23,6 +24,7 @@ export const getDefaultOptions = (): Options => ({
whereNots: [],
whereRaws: [],
orWheres: [],
orWhereNots: [],
orders: [],
offset: undefined,
limit: undefined,
Expand Down
87 changes: 87 additions & 0 deletions packages/accel-record-core/src/relation/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ModelMeta } from "../meta.js";
import { Search } from "../search/index.js";
import { Relation } from "./index.js";

/**
* Provides the search methods for relations.
*
* This class is intended to be inherited by the Relation class.
*/
export class Searchable {
/**
* Creates a new search instance.
* @param params - The search parameters.
* @returns A new search instance.
* @example
* ```ts
* const search = User.search({ name_eq: "foo" });
* const users = search.result();
* ```
* @details
* The search parameters are used to filter the results of the search. \
* The search parameters are specified as key-value pairs. \
* The key is the name of the attribute to search for.
* The key can include associations. \
* The value is the value to search for.
*
* The search parameters can include the following:
* - `*_eq` - The attribute is equal to the value.
* - `*_cont` - The attribute contains the value.
* - `*_start` - The attribute starts with the value.
* - `*_end` - The attribute ends with the value.
* - `*_matches` - The attribute matches the value (Use the `LIKE` clause).
* - `*_lt` - The attribute is less than the value.
* - `*_lte` - The attribute is less than or equal to the value.
* - `*_gt` - The attribute is greater than the value.
* - `*_gte` - The attribute is greater than or equal to the value.
* - `*_in` - The attribute is in the list of values.
* - `*_true` - The attribute is true.
* - `*_false` - The attribute is false.
* - `*_null` - The attribute is null.
* - `*_blank` - The attribute is blank.
* - `*_present` - The attribute is present.
*
* The search parameters can also include the following modifiers:
* - `not` (`does_not`) - The value must not match.
* - `all` - All of the values must match.
* - `any` - Any of the values must match.
*
* The search parameters can also include the following logical operators:
* - `and` - The values must match all of the attributes.
* - `or` - The values must match any of the attributes.
* @example
* ```ts
* const search = User.search({
* profile_bio_cont: "foo", // with association
* age_not_null: 1, // with `not` modifier
* email_or_name_cont_any: ["bar", "baz"], // with `or` operator and `any` modifier
* });
* const users = search.result();
* ```
* @details
* The keys of the search parameters can include the names of the searchable scopes defined in the `searchableScopes` array. \
* The values of the search parameters specify the values to search for.
* @example
* ```ts
* class UserModel extends ApplicationRecord {
* static bio_cont(value: string) {
* return this.joins("profile").where({
* profile: { bio: { 'contains': value } },
* });
* }
* static searchableScopes = ["bio_cont"];
* }
* ```
* // The `bio_cont` scope can be used in the search parameters.
* ```ts
* const search = User.search({ bio_cont: "foo" });
* const users = search.result();
* ```
*/
search<T, M extends ModelMeta>(
this: Relation<T, M>,
params: Record<string, any>
) {
return new Search(this.model, params, this);
}
}
4 changes: 3 additions & 1 deletion packages/accel-record-core/src/relation/where.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AttributeNotFound } from "../errors.js";
import { Model, Models, Relation } from "../index.js";
import { ModelMeta } from "../meta.js";
import { Relations } from "./index.js";
Expand Down Expand Up @@ -50,7 +51,7 @@ export class Where {
newOptions["wheres"].push(where);
}
} else {
throw new Error(`Attribute not found: ${key}`);
throw new AttributeNotFound(`Attribute not found: ${key}`);
}
} else if (Array.isArray(input[key])) {
newOptions["wheres"].push([col, "in", input[key]]);
Expand Down Expand Up @@ -203,6 +204,7 @@ export class Where {
: new Relation(this.model, {}).where(relationOrInput);
const newOptions = JSON.parse(JSON.stringify(this.options));
newOptions["orWheres"].push(relation.options.wheres);
newOptions["orWhereNots"].push(relation.options.whereNots);
return new Relation(this.model, newOptions);
}
}
Expand Down
34 changes: 34 additions & 0 deletions packages/accel-record-core/src/search/condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const getCondition = (predicate: string, value: any) => {
switch (predicate) {
case "eq":
return value;
case "cont":
return { contains: value };
case "start":
return { startsWith: value };
case "end":
return { endsWith: value };
case "matches":
case "match":
return { like: value };
case "lt":
return { "<": value };
case "lte":
return { "<=": value };
case "gt":
return { ">": value };
case "gte":
return { ">=": value };
case "in":
return { in: value };
case "true":
return true;
case "false":
return false;
case "null":
return null;
default:
// unknown predicate
return value;
}
};
Loading

0 comments on commit 642aef2

Please sign in to comment.