From 759e97562f618c1ef7e879c404c1b2f59d9624cb Mon Sep 17 00:00:00 2001 From: Gregor Becker Date: Sun, 19 Jul 2026 23:37:05 +0200 Subject: [PATCH] feat(pinia-orm): add whereLike & orWhereLike query methods SQL LIKE style filtering with % and _ wildcards, case insensitive by default like Laravel's whereLike. Available on query and repository. closes #1692 --- docs/content/2.api/3.query/where-like.md | 36 +++++ packages/pinia-orm/src/query/Query.ts | 16 +++ .../pinia-orm/src/repository/Repository.ts | 9 ++ packages/pinia-orm/src/support/Utils.ts | 15 +++ .../repository/retrieves_where_like.spec.ts | 123 ++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 docs/content/2.api/3.query/where-like.md create mode 100644 packages/pinia-orm/tests/feature/repository/retrieves_where_like.spec.ts diff --git a/docs/content/2.api/3.query/where-like.md b/docs/content/2.api/3.query/where-like.md new file mode 100644 index 000000000..b568a00c4 --- /dev/null +++ b/docs/content/2.api/3.query/where-like.md @@ -0,0 +1,36 @@ +--- +title: 'whereLike()' +description: 'Add a where clause matching a SQL LIKE style pattern to the query.' +--- + +# `whereLike()` + +Filters records with a SQL `LIKE` style pattern. `%` matches any number of characters, `_` matches exactly one character. Matching is case insensitive by default — pass `true` as third argument for case sensitive matching. + +## Usage + +````ts +import { useRepo } from 'pinia-orm' +import User from './models/User' + +const userRepo = useRepo(User) + +// All users whose name contains "john" (case insensitive). +userRepo.whereLike('name', '%john%').get() + +// All users whose name starts with "John" (case sensitive). +userRepo.whereLike('name', 'John%', true).get() + +// Single character wildcard: matches "John Doe" and "Jahn Doe". +userRepo.whereLike('name', 'J_hn Doe').get() + +// Combine with other where clauses. +userRepo.where('active', true).orWhereLike('name', 'jane%').get() +```` + +## Typescript Declarations + +````ts +function whereLike(field: string, value: string | number, caseSensitive = false): Query +function orWhereLike(field: string, value: string | number, caseSensitive = false): Query +```` diff --git a/packages/pinia-orm/src/query/Query.ts b/packages/pinia-orm/src/query/Query.ts index 1784529ea..9888529f8 100644 --- a/packages/pinia-orm/src/query/Query.ts +++ b/packages/pinia-orm/src/query/Query.ts @@ -1,6 +1,7 @@ import type { Pinia } from 'pinia' import { acceptHMRUpdate } from 'pinia' import { + compareLike, compareWithOperator, generateKey, groupBy, @@ -289,6 +290,21 @@ export class Query { return this.where(query => query[field as keyof Model] != null) } + /** + * Add a "where like" clause to the query. The value may contain `%` as + * a wildcard for any number of characters and `_` for a single character. + */ + whereLike (field: string, value: string | number, caseSensitive = false): this { + return this.where(query => compareLike(query[field as keyof Model], value, caseSensitive)) + } + + /** + * Add an "or where like" clause to the query. + */ + orWhereLike (field: string, value: string | number, caseSensitive = false): this { + return this.orWhere(query => compareLike(query[field as keyof Model], value, caseSensitive)) + } + /** * Add a "where has" clause to the query. */ diff --git a/packages/pinia-orm/src/repository/Repository.ts b/packages/pinia-orm/src/repository/Repository.ts index 8d4bd7617..2ee9e8191 100644 --- a/packages/pinia-orm/src/repository/Repository.ts +++ b/packages/pinia-orm/src/repository/Repository.ts @@ -50,6 +50,15 @@ export interface Repository { * Add a where clause to get all results where `field` is not null */ whereNotNull (field: string): Query + /** + * Add a where clause where `field` matches a SQL LIKE style pattern. + * `%` matches any number of characters, `_` a single character. + */ + whereLike (field: string, value: string | number, caseSensitive?: boolean): Query + /** + * Add an "or where like" clause to the query. + */ + orWhereLike (field: string, value: string | number, caseSensitive?: boolean): Query /** * Find the model with the given id. */ diff --git a/packages/pinia-orm/src/support/Utils.ts b/packages/pinia-orm/src/support/Utils.ts index dd95a5167..4a19e62a6 100644 --- a/packages/pinia-orm/src/support/Utils.ts +++ b/packages/pinia-orm/src/support/Utils.ts @@ -30,6 +30,21 @@ export function isNullish (value: any): value is undefined | null { return value === undefined || value === null } +/** + * Compare a value against a SQL LIKE style pattern where `%` matches any + * sequence of characters and `_` matches a single character. + */ +export function compareLike (value: any, pattern: string | number, caseSensitive = false): boolean { + if (isNullish(value)) { return false } + + const source = String(pattern) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/%/g, '[\\s\\S]*') + .replace(/_/g, '[\\s\\S]') + + return new RegExp(`^${source}$`, caseSensitive ? '' : 'i').test(String(value)) +} + /** * Check if the given value is a Date object. */ diff --git a/packages/pinia-orm/tests/feature/repository/retrieves_where_like.spec.ts b/packages/pinia-orm/tests/feature/repository/retrieves_where_like.spec.ts new file mode 100644 index 000000000..a8388525c --- /dev/null +++ b/packages/pinia-orm/tests/feature/repository/retrieves_where_like.spec.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest' + +import { Model, useRepo } from '../../../src' +import { Attr, Num, Str } from '../../../src/decorators' +import { assertInstanceOf, assertModels, fillState } from '../../helpers' + +describe('feature/repository/retrieves_where_like', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + @Num(0) age!: number + } + + const state = { + users: { + 1: { id: 1, name: 'John Doe', age: 30 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'johnny walker', age: 20 }, + 4: { id: 4, name: 'Doe John', age: 40 }, + }, + } + + it('can filter with a contains pattern', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('name', '%john%').get() + + const expected = [ + { id: 1, name: 'John Doe', age: 30 }, + { id: 3, name: 'johnny walker', age: 20 }, + { id: 4, name: 'Doe John', age: 40 }, + ] + + expect(users).toHaveLength(3) + assertInstanceOf(users, User) + assertModels(users, expected) + }) + + it('can filter with a starts with pattern', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('name', 'john%').get() + + const expected = [ + { id: 1, name: 'John Doe', age: 30 }, + { id: 3, name: 'johnny walker', age: 20 }, + ] + + expect(users).toHaveLength(2) + assertModels(users, expected) + }) + + it('matches case sensitive when the flag is set', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('name', 'john%', true).get() + + expect(users).toHaveLength(1) + assertModels(users, [{ id: 3, name: 'johnny walker', age: 20 }]) + }) + + it('supports single character wildcards', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('name', 'J_hn Doe').get() + + expect(users).toHaveLength(1) + assertModels(users, [{ id: 1, name: 'John Doe', age: 30 }]) + }) + + it('matches without wildcards like an equality check', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('name', 'jane doe').get() + + expect(users).toHaveLength(1) + assertModels(users, [{ id: 2, name: 'Jane Doe', age: 30 }]) + }) + + it('can filter with `orWhereLike`', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.where('age', 40).orWhereLike('name', 'jane%').get() + + const expected = [ + { id: 2, name: 'Jane Doe', age: 30 }, + { id: 4, name: 'Doe John', age: 40 }, + ] + + expect(users).toHaveLength(2) + assertModels(users, expected) + }) + + it('can filter non string fields', () => { + const userRepo = useRepo(User) + + fillState(state) + + const users = userRepo.whereLike('age', '3%').get() + + const expected = [ + { id: 1, name: 'John Doe', age: 30 }, + { id: 2, name: 'Jane Doe', age: 30 }, + ] + + expect(users).toHaveLength(2) + assertModels(users, expected) + }) +})