diff --git a/README.md b/README.md index 1283acc..c2abd96 100644 --- a/README.md +++ b/README.md @@ -200,12 +200,12 @@ users.findMany((q) => q.where({ name: (name) => name.startsWith('J') }), { }) ``` -You can sort by multiple criteria by providing them in the `orderBy` object: +You can sort by multiple keys by listing them in the `orderBy` object: ```ts users.updateMany((q) => q.where({ name: (name) => name.startsWith('J') }), { data(user) { - user.name = user.name.toUpperCase(), + user.name = user.name.toUpperCase() }, orderBy: { name: 'asc', @@ -214,6 +214,14 @@ users.updateMany((q) => q.where({ name: (name) => name.startsWith('J') }), { }) ``` +You can sort by an ordered list of criteria by passing an array to `orderBy`. Each entry is applied in sequence: the first entry determines the primary sort, and each subsequent entry breaks ties among records that compare equal under the preceding criteria. + +```ts +users.findMany(undefined, { + orderBy: [{ age: 'asc' }, { name: 'desc' }] +}) +``` + ## Relations You can define relations by calling the `.defineRelations()` method on the collection. diff --git a/src/sort.ts b/src/sort.ts index d3ae1fc..c12eef7 100644 --- a/src/sort.ts +++ b/src/sort.ts @@ -1,6 +1,6 @@ import type { StandardSchemaV1 } from '@standard-schema/spec' import { get } from 'es-toolkit/compat' -import { toDeepEntries } from '#/src/utils.js' +import { toDeepEntries, type PropertyPath } from '#/src/utils.js' export type SortDirection = 'asc' | 'desc' @@ -8,15 +8,19 @@ export interface SortOptions { orderBy?: OrderBy } -type OrderBy< +type OrderBy = + | OrderByCriteria + | Array> + +type OrderByCriteria< Schema extends StandardSchemaV1, T = StandardSchemaV1.InferOutput, > = NonNullable extends Array - ? OrderBy + ? OrderByCriteria : NonNullable extends Record ? { - [K in keyof T]?: OrderBy + [K in keyof T]?: OrderByCriteria } : SortDirection @@ -28,7 +32,13 @@ export function sortResults( return } - const criteria = toDeepEntries(sortOptions.orderBy as any) + const criteria: Array<[PropertyPath, SortDirection]> = Array.isArray( + sortOptions.orderBy, + ) + ? sortOptions.orderBy.flatMap((entry) => { + return toDeepEntries(entry as any) + }) + : toDeepEntries(sortOptions.orderBy as any) data.sort((left, right) => { for (const [path, sortDirection] of criteria) { diff --git a/tests/sort.test.ts b/tests/sort.test.ts index ede2ed6..a90fb62 100644 --- a/tests/sort.test.ts +++ b/tests/sort.test.ts @@ -6,7 +6,7 @@ const schema = z.object({ name: z.string(), }) -it('sorts the find results by a single key (asc)', async () => { +it('sorts the results by a single key (asc)', async () => { const users = new Collection({ schema }) await users.create({ id: 1, name: 'John' }) @@ -33,7 +33,7 @@ it('sorts the find results by a single key (asc)', async () => { ]) }) -it('sorts the find results by a single key (desc)', async () => { +it('sorts the results by a single key (desc)', async () => { const users = new Collection({ schema }) await users.create({ id: 1, name: 'John' }) @@ -60,7 +60,7 @@ it('sorts the find results by a single key (desc)', async () => { ]) }) -it('sorts the find results by multiple keys (mixed)', async () => { +it('sorts the results by multiple keys (mixed)', async () => { const users = new Collection({ schema }) await users.create({ id: 1, name: 'John' }) await users.create({ id: 2, name: 'Alice' }) @@ -89,7 +89,7 @@ it('sorts the find results by multiple keys (mixed)', async () => { ]) }) -it('sorts the find results by a nested key', async () => { +it('sorts the results by a nested key', async () => { const users = new Collection({ schema: schema.extend({ address: z.object({ @@ -121,3 +121,89 @@ it('sorts the find results by a nested key', async () => { { id: 1, name: 'John', address: { street: 'C' } }, ]) }) + +it('sorts the results by a list of sort criteria', async () => { + const schema = z.object({ + id: z.number(), + name: z.string(), + age: z.number(), + }) + + const users = new Collection({ schema }) + + await users.create({ id: 1, name: 'John', age: 32 }) + await users.create({ id: 2, name: 'Alice', age: 24 }) + await users.create({ id: 3, name: 'Bob', age: 41 }) + await users.create({ id: 4, name: 'Alice', age: 41 }) + + expect( + users.findMany(undefined, { + orderBy: [{ age: 'asc' }, { name: 'desc' }], + }), + ).toEqual([ + { id: 2, name: 'Alice', age: 24 }, + { id: 1, name: 'John', age: 32 }, + { id: 3, name: 'Bob', age: 41 }, + { id: 4, name: 'Alice', age: 41 }, + ]) +}) + +it('sorts by a relational property', async () => { + const userSchema = z.object({ + id: z.number(), + name: z.string(), + get posts() { + return z.array(postSchema) + }, + }) + const postSchema = z.object({ + id: z.number(), + title: z.string(), + get author() { + return userSchema.optional() + }, + }) + + const users = new Collection({ schema: userSchema }) + const posts = new Collection({ schema: postSchema }) + + users.defineRelations(({ many }) => ({ + posts: many(posts), + })) + posts.defineRelations(({ one }) => ({ + author: one(users, { unique: true }), + })) + + const john = await users.create({ + id: 1, + name: 'John', + posts: await posts.createMany(2, (index) => ({ + id: index + 1, + title: `Post ${index + 1}`, + })), + }) + + const alice = await users.create({ + id: 2, + name: 'Alice', + posts: await posts.createMany(2, (index) => ({ + id: index + 3, + title: `Post ${index + 3}`, + })), + }) + + expect( + posts.findMany(undefined, { + orderBy: { + author: { + name: 'asc', + }, + }, + }), + ).toEqual([ + { id: 3, title: 'Post 3', author: alice }, + { id: 4, title: 'Post 4', author: alice }, + { id: 1, title: 'Post 1', author: john }, + { id: 2, title: 'Post 2', author: john }, + ]) +}) diff --git a/tests/types/delete-many.test-d.ts b/tests/types/delete-many.test-d.ts index d04ae74..a09a740 100644 --- a/tests/types/delete-many.test-d.ts +++ b/tests/types/delete-many.test-d.ts @@ -66,6 +66,11 @@ it('supports sorting the results', () => { name?: SortDirection nested?: { key?: SortDirection } } + | Array<{ + id?: SortDirection + name?: SortDirection + nested?: { key?: SortDirection } + }> | undefined >() }) diff --git a/tests/types/find-many.test-d.ts b/tests/types/find-many.test-d.ts index 00d0f61..d5ad7bd 100644 --- a/tests/types/find-many.test-d.ts +++ b/tests/types/find-many.test-d.ts @@ -117,6 +117,11 @@ it('supports sorting the results', () => { name?: SortDirection | undefined nested?: { key?: SortDirection | undefined } } + | Array<{ + id?: SortDirection | undefined + name?: SortDirection | undefined + nested?: { key?: SortDirection | undefined } + }> | undefined >() }) diff --git a/tests/types/update-many.test-d.ts b/tests/types/update-many.test-d.ts index d05099c..09a864d 100644 --- a/tests/types/update-many.test-d.ts +++ b/tests/types/update-many.test-d.ts @@ -109,6 +109,11 @@ it('supports sorting the results', () => { name?: SortDirection nested?: { key?: SortDirection } } + | Array<{ + id?: SortDirection + name?: SortDirection + nested?: { key?: SortDirection } + }> | undefined >() })