From bd90d60505efda32571bd310fe99deb61873e2f1 Mon Sep 17 00:00:00 2001 From: Ivo Heidelbach Date: Tue, 7 Mar 2023 22:31:19 +0100 Subject: [PATCH] allow selecting nested paths, fix type inference when grouping nested paths --- README.md | 14 +++++++------- src/CosmosQueryBuilder.spec.ts | 2 +- src/CosmosQueryBuilder.ts | 34 +++++++++++++++++++--------------- src/typeHelpers.ts | 12 ++++++++++++ 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 38bea17..bb1ef76 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ interface Machine { mode: 'idle' | 'running'; tags: string[]; softDeleted?: { - atDate: string; - byUser: string; + at: string; + by: string; }; } ``` @@ -51,7 +51,7 @@ const { querySpec } = new CosmosQueryBuilder() d.isUndefined('softDeleted'); d.and((c) => { c.isDefined('softDeleted'); - c.lower('softDeleted.atDate', '2023-03-01'); // 👈 nested keys are also supported + c.lower('softDeleted.at', '2023-03-01'); // 👈 nested keys are also supported }); }) .orderBy('serial') @@ -75,7 +75,7 @@ The result is a `SqlQuerySpec` that you can pass to the `Items.query()` function NOT IS_DEFINED(c.softDeleted) OR ( IS_DEFINED(c.softDeleted) - AND c.softDeleted.atDate < @softDeleted_atDate + AND c.softDeleted.at < @softDeleted_at ) ) ORDER BY c.serial ASC @@ -84,7 +84,7 @@ The result is a `SqlQuerySpec` that you can pass to the `Items.query()` function "parameters": [ { "name": "@id", "value": "^0001-abc-.*" }, { "name": "@mode", "value": ["idle", "running"] }, - { "name": "@softDeleted_atDate", "value": "2023-03-01" } + { "name": "@softDeleted_at", "value": "2023-03-01" } ] } ``` @@ -104,10 +104,10 @@ const { resources } = await new CosmosQueryBuilder().select('id', 'mode ### Selecting -By default the whole document is selected with `SELECT * from c`. The `select()` function let's you defined which fields to query. As of now it is only possible to pass root keys here, nested keys are not supported yet. +By default the whole document is selected with `SELECT * from c`. The `select()` function let's you define which fields or paths to query. ```ts -.select('id', 'serial', 'isConnected') +.select('id', 'serial', 'isConnected', 'softDeletd.at') ``` Alternatively you can use any of those aggregation functions: diff --git a/src/CosmosQueryBuilder.spec.ts b/src/CosmosQueryBuilder.spec.ts index 8e49163..0c1a423 100644 --- a/src/CosmosQueryBuilder.spec.ts +++ b/src/CosmosQueryBuilder.spec.ts @@ -59,7 +59,7 @@ GROUP BY c.mode, c.softDeleted.by", const container = new CosmosClient('').database('').container(''); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { resources } = await new CosmosQueryBuilder() - .selectSum('price', { groupBy: ['mode', 'id'] }) + .selectSum('price', { groupBy: ['softDeleted.at', 'mode'] }) .equals('id', '123') .build() .query(container) diff --git a/src/CosmosQueryBuilder.ts b/src/CosmosQueryBuilder.ts index f782e0a..e7b1026 100644 --- a/src/CosmosQueryBuilder.ts +++ b/src/CosmosQueryBuilder.ts @@ -1,6 +1,6 @@ import type { Container, SqlParameter, SqlQuerySpec, JSONValue, FeedOptions } from '@azure/cosmos'; import { unpretty } from './helpers'; -import type { ArrayElement, Path, PathValue } from './typeHelpers'; +import type { ArrayElement, Path, PathValue, PickPath, UnionToIntersection } from './typeHelpers'; const TAB = ' '; @@ -225,7 +225,7 @@ type SortOrder = 'ASC' | 'DESC'; export class CosmosQueryBuilder< T extends Record, - S extends Pick | Record = T + S extends UnionToIntersection>> | Record = T > extends ConjunctionQueryBuilder { private selection: string[] = []; private sorting: Array<{ by: string; order: SortOrder }> = []; @@ -247,13 +247,15 @@ export class CosmosQueryBuilder< this.build = this.build.bind(this); } - select>(...fields: F[]): CosmosQueryBuilder { - this.selection = fields.map((f) => `c.${String(f)}`); + select

, NewS extends UnionToIntersection>>( + ...paths: P[] + ): CosmosQueryBuilder { + this.selection = paths.map((p) => `c.${String(p)}`); // @ts-ignore required for well-typed response when using the query() function return this; } - selectCount, NewS extends Pick & { count: number }>({ + selectCount, NewS extends UnionToIntersection> & { count: number }>({ groupBy, }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder { this.selection = ['count(1) as count']; @@ -262,20 +264,22 @@ export class CosmosQueryBuilder< return this; } - selectMax

, GroupBy extends Path, NewS extends Pick & { max: PathValue }>( - path: P, - { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {} - ): CosmosQueryBuilder { + selectMax< + P extends Path, + GroupBy extends Path, + NewS extends UnionToIntersection> & { max: PathValue } + >(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder { this.selection = [`MAX(c.${String(path)}) as max`]; this.groupBy(groupBy); // @ts-ignore required for well-typed response when using the query() function return this; } - selectMin

, GroupBy extends Path, NewS extends Pick & { min: PathValue }>( - path: P, - { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {} - ): CosmosQueryBuilder { + selectMin< + P extends Path, + GroupBy extends Path, + NewS extends UnionToIntersection> & { min: PathValue } + >(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder { this.selection = [`MIN(c.${String(path)}) as min`]; this.groupBy(groupBy); // @ts-ignore required for well-typed response when using the query() function @@ -285,7 +289,7 @@ export class CosmosQueryBuilder< selectSum< P extends Exclude, NonNullable> extends number ? never : P>, GroupBy extends Path, - NewS extends Pick & { sum: PathValue } + NewS extends UnionToIntersection> & { sum: PathValue } >(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder { this.selection = [`SUM(c.${String(path)}) as sum`]; this.groupBy(groupBy); @@ -296,7 +300,7 @@ export class CosmosQueryBuilder< selectAvg< P extends Exclude, NonNullable> extends number ? never : P>, GroupBy extends Path, - NewS extends Pick & { avg: PathValue } + NewS extends UnionToIntersection> & { avg: PathValue } >(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder { this.selection = [`AVG(c.${String(path)}) as min`]; this.groupBy(groupBy); diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index d767aed..1c62ac9 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -1,5 +1,7 @@ export type ArrayElement = T extends Array ? Element : never; +export type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; + type PathImpl = Key extends string ? Required[Key] extends Record ? Required[Key] extends any[] @@ -21,3 +23,13 @@ export type PathValue> = P extends `${infer Key}.${infer Re : P extends keyof T ? T[P] : never; + +export type PickPath> = P extends keyof T + ? Pick + : P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends Path[Key]> + ? PickPath[Key], Rest> + : never + : never + : never;