Skip to content

Commit

Permalink
allow selecting nested paths, fix type inference when grouping nested…
Browse files Browse the repository at this point in the history
… paths
  • Loading branch information
heivo committed Mar 7, 2023
1 parent 2319cf4 commit bd90d60
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 23 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ interface Machine {
mode: 'idle' | 'running';
tags: string[];
softDeleted?: {
atDate: string;
byUser: string;
at: string;
by: string;
};
}
```
Expand All @@ -51,7 +51,7 @@ const { querySpec } = new CosmosQueryBuilder<Machine>()
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')
Expand All @@ -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
Expand All @@ -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" }
]
}
```
Expand All @@ -104,10 +104,10 @@ const { resources } = await new CosmosQueryBuilder<Machine>().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:
Expand Down
2 changes: 1 addition & 1 deletion src/CosmosQueryBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Machine>()
.selectSum('price', { groupBy: ['mode', 'id'] })
.selectSum('price', { groupBy: ['softDeleted.at', 'mode'] })
.equals('id', '123')
.build()
.query(container)
Expand Down
34 changes: 19 additions & 15 deletions src/CosmosQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -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 = ' ';

Expand Down Expand Up @@ -225,7 +225,7 @@ type SortOrder = 'ASC' | 'DESC';

export class CosmosQueryBuilder<
T extends Record<string, any>,
S extends Pick<T, any> | Record<string, any> = T
S extends UnionToIntersection<PickPath<T, Path<T>>> | Record<string, any> = T
> extends ConjunctionQueryBuilder<T> {
private selection: string[] = [];
private sorting: Array<{ by: string; order: SortOrder }> = [];
Expand All @@ -247,13 +247,15 @@ export class CosmosQueryBuilder<
this.build = this.build.bind(this);
}

select<F extends keyof T, NewS extends Pick<S, F>>(...fields: F[]): CosmosQueryBuilder<T, NewS> {
this.selection = fields.map((f) => `c.${String(f)}`);
select<P extends Path<T>, NewS extends UnionToIntersection<PickPath<T, P>>>(
...paths: P[]
): CosmosQueryBuilder<T, NewS> {
this.selection = paths.map((p) => `c.${String(p)}`);
// @ts-ignore required for well-typed response when using the query() function
return this;
}

selectCount<GroupBy extends Path<T>, NewS extends Pick<T, GroupBy> & { count: number }>({
selectCount<GroupBy extends Path<T>, NewS extends UnionToIntersection<PickPath<T, GroupBy>> & { count: number }>({
groupBy,
}: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder<T, NewS> {
this.selection = ['count(1) as count'];
Expand All @@ -262,20 +264,22 @@ export class CosmosQueryBuilder<
return this;
}

selectMax<P extends Path<T>, GroupBy extends Path<T>, NewS extends Pick<T, GroupBy> & { max: PathValue<T, P> }>(
path: P,
{ groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}
): CosmosQueryBuilder<T, NewS> {
selectMax<
P extends Path<T>,
GroupBy extends Path<T>,
NewS extends UnionToIntersection<PickPath<T, GroupBy>> & { max: PathValue<T, P> }
>(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder<T, NewS> {
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<P extends Path<T>, GroupBy extends Path<T>, NewS extends Pick<T, GroupBy> & { min: PathValue<T, P> }>(
path: P,
{ groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}
): CosmosQueryBuilder<T, NewS> {
selectMin<
P extends Path<T>,
GroupBy extends Path<T>,
NewS extends UnionToIntersection<PickPath<T, GroupBy>> & { min: PathValue<T, P> }
>(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder<T, NewS> {
this.selection = [`MIN(c.${String(path)}) as min`];
this.groupBy(groupBy);
// @ts-ignore required for well-typed response when using the query() function
Expand All @@ -285,7 +289,7 @@ export class CosmosQueryBuilder<
selectSum<
P extends Exclude<Path<T>, NonNullable<PathValue<T, P>> extends number ? never : P>,
GroupBy extends Path<T>,
NewS extends Pick<T, GroupBy> & { sum: PathValue<T, P> }
NewS extends UnionToIntersection<PickPath<T, GroupBy>> & { sum: PathValue<T, P> }
>(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder<T, NewS> {
this.selection = [`SUM(c.${String(path)}) as sum`];
this.groupBy(groupBy);
Expand All @@ -296,7 +300,7 @@ export class CosmosQueryBuilder<
selectAvg<
P extends Exclude<Path<T>, NonNullable<PathValue<T, P>> extends number ? never : P>,
GroupBy extends Path<T>,
NewS extends Pick<T, GroupBy> & { avg: PathValue<T, P> }
NewS extends UnionToIntersection<PickPath<T, GroupBy>> & { avg: PathValue<T, P> }
>(path: P, { groupBy }: { groupBy?: GroupBy | GroupBy[] } = {}): CosmosQueryBuilder<T, NewS> {
this.selection = [`AVG(c.${String(path)}) as min`];
this.groupBy(groupBy);
Expand Down
12 changes: 12 additions & 0 deletions src/typeHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type ArrayElement<T> = T extends Array<infer Element> ? Element : never;

export type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;

type PathImpl<T, Key extends keyof T> = Key extends string
? Required<T>[Key] extends Record<string, any>
? Required<T>[Key] extends any[]
Expand All @@ -21,3 +23,13 @@ export type PathValue<T, P extends Path<T>> = P extends `${infer Key}.${infer Re
: P extends keyof T
? T[P]
: never;

export type PickPath<T, P extends Path<T>> = P extends keyof T
? Pick<T, P>
: P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<Required<T>[Key]>
? PickPath<Required<T>[Key], Rest>
: never
: never
: never;

0 comments on commit bd90d60

Please sign in to comment.