Skip to content

Commit 4f90ed9

Browse files
authored
Refactor QL (#201)
1 parent 9571d15 commit 4f90ed9

File tree

7 files changed

+455
-275
lines changed

7 files changed

+455
-275
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1111
- `cds.cli` CLI arguments
1212
- `cds.requires` types for MTX services
1313
- `cds.utils.colors` types
14+
- The CQL methods `.where` and `.having` now suggest property names for certain overloads.
1415

1516
### Changed
1617
- Most `cds.requires` entries are now optionals.
1718
- `cds.connect.to` now also supports using a precompiled model.
19+
- Properties of entities are no longer optional in projections, eliminating the need to perform optional chaining on them when using nested projections
1820

1921
## Version 0.6.5 - 2024-08-13
2022
### Fixed

apis/internal/inference.d.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ export interface ArrayConstructable<T = any> {
1616

1717
// concrete singular type.
1818
// `SingularType<typeof Books>` == `Book`.
19-
export type SingularType<T extends ArrayConstructable<T>> = InstanceType<T>[number]
20-
21-
export type PluralType<T extends Constructable> = Array<InstanceType<T>>
19+
export type SingularInstanceType<T extends ArrayConstructable> = InstanceType<T>[number]
20+
export type PluralInstanceType<T extends Constructable> = Array<InstanceType<T>>
2221

2322
// Convenient way of unwrapping the inner type from array-typed values, as well as the value type itself
2423
// `class MyArray<T> extends Array<T>``
@@ -28,11 +27,14 @@ export type PluralType<T extends Constructable> = Array<InstanceType<T>>
2827
// This type introduces an indirection that streamlines their behaviour for both cases.
2928
// For any scalar type `Unwrap` behaves idempotent.
3029
export type Unwrap<T> = T extends ArrayConstructable
31-
? SingularType<T>
30+
? SingularInstanceType<T>
3231
: T extends Array<infer U>
3332
? U
3433
: T
3534

35+
// ...and sometimes Unwrap gives us a class (typeof Book), but we need an instance (Book)
36+
export type UnwrappedInstanceType<T> = Unwrap<T> extends Constructable ? InstanceType<Unwrap<T>> : Unwrap<T>
37+
3638

3739
/*
3840
* the following three types are used to convert union types to intersection types.
@@ -63,6 +65,7 @@ export type Unwrap<T> = T extends ArrayConstructable
6365
* Places where these types are used are subject to a rework!
6466
* the idea behind the conversion can be found in this excellent writeup: https://fettblog.eu/typescript-union-to-intersection/
6567
*/
66-
export type Scalarise<A> = A extends Array<infer N> ? N : A
6768
export type UnionToIntersection<U> = Partial<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never>
6869
export type UnionsToIntersections<U> = Array<UnionToIntersection<Scalarise<U>>>
70+
export type Scalarise<A> = A extends Array<infer N> ? N : A
71+
export type Pluralise<S> = S extends Array<any> ? S : Array<S>

apis/internal/query.d.ts

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import type { Definition } from '../csn'
2+
import type { entity } from '../linked/classes'
3+
import type { column_expr, ref } from '../cqn'
4+
import type { ArrayConstructable, Constructable, SingularInstanceType, Unwrap, UnwrappedInstanceType } from './inference'
5+
import { ConstructedQuery } from '../ql'
6+
import { KVPairs, DeepRequired } from './util'
7+
8+
// https://cap.cloud.sap/docs/node.js/cds-ql?q=projection#projection-functions
9+
type Projection<T> = (e: QLExtensions<T extends ArrayConstructable ? SingularInstanceType<T> : T>) => void
10+
type Primitive = string | number | boolean | Date
11+
type NonPrimitive<T> = Exclude<T, string | number | boolean | symbol | bigint | null | undefined>
12+
type EntityDescription = entity | Definition | string // FIXME: Definition not allowed here?, FIXME: { name: string } | ?
13+
type PK = number | string | object
14+
// used as a catch-all type for using tagged template strings: SELECT `foo`. from `bar` etc.
15+
// the resulting signatures are actually not very strongly typed, but they at least accept template strings
16+
// when run in strict mode.
17+
// This signature has to be added to a method as intersection type.
18+
// Defining overloads with it will override preceding signatures and the other way around.
19+
type TaggedTemplateQueryPart<T> = (strings: TemplateStringsArray, ...params: unknown[]) => T
20+
21+
type QueryArtefact = {
22+
23+
/**
24+
* Alias for this attribute.
25+
*/
26+
as (alias: string): void,
27+
28+
/**
29+
* Accesses any nested attribute based on a [path](https://cap.cloud.sap/cap/docs/java/query-api#path-expressions):
30+
* `X.get('a.b.c.d')`. Note that you will not receive
31+
* proper typing after this call.
32+
* To still have access to typed results, use
33+
* `X.a().b().c().d()` instead.
34+
*/
35+
get (path: string): any,
36+
37+
}
38+
39+
// Type for query pieces that can either be chained to build more complex queries or
40+
// awaited to materialise the result:
41+
// `Awaitable<SELECT<Book>, Book> = SELECT<Book> & Promise<Book>`
42+
//
43+
// While the benefit is probably not immediately obvious as we don't exactly
44+
// save a lot of typing over explicitly writing `SELECT<Book> & Promise<Book>`,
45+
// it makes the semantics more explicit. Also sets us up for when TypeScript ever
46+
// improves their generics to support:
47+
//
48+
// `Awaitable<T> = T extends unknown<infer I> ? (T & Promise<I>) : never`
49+
// (at the time of writing, infering the first generic parameter of ANY type
50+
// does not seem to be possible.)
51+
export type Awaitable<T, I> = T & Promise<I>
52+
53+
// note to self: don't try to rewrite these intersection types into overloads.
54+
// It does not work because TaggedTemplateQueryPart will not fit in as regular overload
55+
export interface ByKey {
56+
byKey (primaryKey?: PK): this
57+
}
58+
59+
// unwrap the target of a query and extract its keys.
60+
// Normalise to scalar,
61+
// or fall back to general strings/column expressions
62+
type KeyOfTarget<T, F = string | column_expr> = T extends ConstructedQuery<infer U>
63+
? (U extends ArrayConstructable // Books
64+
? keyof SingularInstanceType<U>
65+
: U extends Constructable // Book
66+
? keyof InstanceType<U>
67+
: F)
68+
: F
69+
70+
type KeyOfSingular<T> = Unwrap<T> extends T
71+
? keyof T
72+
: keyof Unwrap<T>
73+
74+
// as static SELECT borrows the type of Columns directly,
75+
// we need this second type argument to explicitly specific that "this"
76+
// refers to a STATIC<T>, not to a Columns. Or else we could not chain
77+
// other QL functions to .columns
78+
export interface Columns<T, This = undefined> {
79+
columns:
80+
((...col: KeyOfSingular<T>[]) => This extends undefined ? this : This)
81+
& ((col: KeyOfSingular<T>[]) => This extends undefined ? this : This)
82+
& ((...col: (string | column_expr)[]) => This extends undefined ? this : This)
83+
& ((col: (string | column_expr)[]) => This extends undefined ? this : This)
84+
& TaggedTemplateQueryPart<This extends undefined ? this : This>
85+
}
86+
87+
type Op = '=' | '<' | '>' | '<=' | '>=' | '!=' | 'in' | 'like'
88+
type WS = '' | ' '
89+
type Expression<E extends string | number | bigint | boolean> = `${E}${WS}${Op}${WS}`
90+
type ColumnValue = Primitive | Readonly<Primitive[]> | SELECT<any> // not entirely sure why Readonly is required here
91+
// TODO: it would be nicer to check for E[x] for the value instead of Primitive, where x is the key
92+
type Expressions<L,E> = KVPairs<L, Expression<Exclude<keyof E, symbol>>, ColumnValue> extends true
93+
? L
94+
// fallback: allow for any string. Important for when user renamed properties
95+
: KVPairs<L, Expression<string>, ColumnValue> extends true
96+
? L
97+
: never
98+
99+
type HavingWhere<This, E> =
100+
/**
101+
* @param predicate - An object with keys that are valid fields of the target entity and values that are compared to the respective fields.
102+
* @example
103+
* ```js
104+
* SELECT.from(Books).where({ ID: 42 }) // where ID is a valid field of Book
105+
* SELECT.from(Books).having({ ID: 42 }) // where ID is a valid field of Book
106+
* ```
107+
*/
108+
((predicate: Partial<{[column in KeyOfTarget<This extends ConstructedQuery<infer E> ? E : never, never>]: any}>) => This)
109+
/**
110+
* @param expr - An array of expressions, where every odd element is a valid field of the target entity and every even element is a value that is compared to the respective field.
111+
* @example
112+
* ```js
113+
* SELECT.from(Books).where(['ID =', 42 ]) // where ID is a valid, numerical field of Book
114+
* SELECT.from(Books).having(['ID =', 42 ]) // where ID is a valid, numerical field of Book
115+
*```
116+
*/
117+
& (<const L extends unknown[]>(...expr: Expressions<L, UnwrappedInstanceType<E>>) => This)
118+
& ((...expr: string[]) => This)
119+
& TaggedTemplateQueryPart<This>
120+
121+
export interface Having<T> {
122+
having: HavingWhere<this, T>
123+
}
124+
125+
export interface Where<T> {
126+
where: HavingWhere<this, T>
127+
}
128+
129+
export interface GroupBy {
130+
groupBy: TaggedTemplateQueryPart<this>
131+
& ((columns: Partial<{[column in KeyOfTarget<this extends ConstructedQuery<infer E> ? E : never, never>]: any}>) => this)
132+
& ((...expr: string[]) => this)
133+
& ((ref: ref) => this)
134+
// columns currently not being auto-completed due to complexity
135+
}
136+
137+
export interface OrderBy<T> {
138+
orderBy: TaggedTemplateQueryPart<this>
139+
& ((...col: KeyOfSingular<T>[]) => this)
140+
& ((...expr: string[]) => this)
141+
}
142+
143+
export interface Limit {
144+
limit: TaggedTemplateQueryPart<this>
145+
& ((rows: number, offset?: number) => this)
146+
}
147+
148+
export interface And {
149+
and: TaggedTemplateQueryPart<this>
150+
& ((predicate: object) => this)
151+
& ((...expr: any[]) => this)
152+
}
153+
154+
export interface InUpsert<T> {
155+
data (block: (e: T) => void): this
156+
157+
entries (...entries: object[]): this
158+
159+
values (...val: (null | Primitive)[]): this
160+
values (val: (null | Primitive)[]): this
161+
162+
rows (...row: (null | Primitive)[][]): this
163+
rows (row: (null | Primitive)[][]): this
164+
165+
into: (<T extends ArrayConstructable> (entity: T) => this)
166+
& TaggedTemplateQueryPart<this>
167+
& ((entity: EntityDescription) => this)
168+
}
169+
170+
// don't wrap QLExtensions in more QLExtensions (indirection to work around recursive definition)
171+
export type QLExtensions<T> = T extends QLExtensions_<any> ? T : QLExtensions_<DeepRequired<T>>
172+
173+
/**
174+
* QLExtensions are properties that are attached to entities in CQL contexts.
175+
* They are passed down to all properties recursively.
176+
*/
177+
// have to exclude undefined from the type, or we'd end up with a distribution of Subqueryable
178+
// over T and undefined, which gives us zero code completion within the callable.
179+
type QLExtensions_<T> = { [Key in keyof T]: QLExtensions<T[Key]> } & QueryArtefact & Subqueryable<Exclude<T, undefined>>
180+
181+
/**
182+
* Adds the ability for subqueries to structured properties.
183+
* The final result of each subquery will be the property itself:
184+
* `Book.title` == `Subqueryable<Book>.title()`
185+
*/
186+
type Subqueryable<T> = T extends Primitive ? unknown
187+
// composition of many/ association to many
188+
: T extends readonly unknown[] ? {
189+
190+
/**
191+
* @example
192+
* ```js
193+
* SELECT.from(Books, b => b.author)
194+
* ```
195+
* means: "select all books and project each book's author"
196+
*
197+
* whereas
198+
* ```js
199+
* SELECT.from(Books, b => b.author(a => a.ID))
200+
* ```
201+
* means: "select all books, subselect each book's author's ID
202+
*
203+
* Note that you do not need to return anything from these subqueries.
204+
*/
205+
(fn: ((a: QLExtensions<T[number]>) => any) | '*'): T[number],
206+
}
207+
// composition of one/ association to one
208+
: {
209+
210+
/**
211+
* @example
212+
* ```js
213+
* SELECT.from(Books, b => b.author)
214+
* ```
215+
* means: "select all books and project each book's author"
216+
*
217+
* whereas
218+
* ```js
219+
* SELECT.from(Books, b => b.author(a => a.ID))
220+
* ```
221+
* means: "select all books, subselect each book's author's ID
222+
*
223+
* Note that you do not need to return anything from these subqueries.
224+
*/
225+
(fn: ((a: QLExtensions<T>) => any) | '*'): T,
226+
}

apis/internal/util.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,23 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
1414
* Object structure that exposes both array-like and object-like behaviour.
1515
* @see [capire](https://cap.cloud.sap/docs/node.js/cds-reflect#iterable)
1616
*/
17+
1718
export type IterableMap<T> = { [name: string]: T } & Iterable<T>
19+
20+
/**
21+
* T is a tuple of alternating K, V pairs -> true, else false
22+
* Allows for variadic parameter lists with alternating expecing types,
23+
* like we have in cql.SELECT.where
24+
*/
25+
type KVPairs<T,K,V> = T extends []
26+
? true
27+
: T extends [K, V, ...infer R]
28+
? KVPairs<R,K,V>
29+
: false
30+
31+
/**
32+
* Recursively excludes nullability from all properties of T.
33+
*/
34+
export type DeepRequired<T> = {
35+
[K in keyof T]: DeepRequired<T[K]>
36+
} & Exclude<Required<T>, null>

0 commit comments

Comments
 (0)