Skip to content

Commit

Permalink
Refactor QL (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
daogrady authored Oct 7, 2024
1 parent 9571d15 commit 4f90ed9
Show file tree
Hide file tree
Showing 7 changed files with 455 additions and 275 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
- `cds.cli` CLI arguments
- `cds.requires` types for MTX services
- `cds.utils.colors` types
- The CQL methods `.where` and `.having` now suggest property names for certain overloads.

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

## Version 0.6.5 - 2024-08-13
### Fixed
Expand Down
13 changes: 8 additions & 5 deletions apis/internal/inference.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ export interface ArrayConstructable<T = any> {

// concrete singular type.
// `SingularType<typeof Books>` == `Book`.
export type SingularType<T extends ArrayConstructable<T>> = InstanceType<T>[number]

export type PluralType<T extends Constructable> = Array<InstanceType<T>>
export type SingularInstanceType<T extends ArrayConstructable> = InstanceType<T>[number]
export type PluralInstanceType<T extends Constructable> = Array<InstanceType<T>>

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

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


/*
* the following three types are used to convert union types to intersection types.
Expand Down Expand Up @@ -63,6 +65,7 @@ export type Unwrap<T> = T extends ArrayConstructable
* Places where these types are used are subject to a rework!
* the idea behind the conversion can be found in this excellent writeup: https://fettblog.eu/typescript-union-to-intersection/
*/
export type Scalarise<A> = A extends Array<infer N> ? N : A
export type UnionToIntersection<U> = Partial<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never>
export type UnionsToIntersections<U> = Array<UnionToIntersection<Scalarise<U>>>
export type Scalarise<A> = A extends Array<infer N> ? N : A
export type Pluralise<S> = S extends Array<any> ? S : Array<S>
226 changes: 226 additions & 0 deletions apis/internal/query.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import type { Definition } from '../csn'
import type { entity } from '../linked/classes'
import type { column_expr, ref } from '../cqn'
import type { ArrayConstructable, Constructable, SingularInstanceType, Unwrap, UnwrappedInstanceType } from './inference'
import { ConstructedQuery } from '../ql'
import { KVPairs, DeepRequired } from './util'

// https://cap.cloud.sap/docs/node.js/cds-ql?q=projection#projection-functions
type Projection<T> = (e: QLExtensions<T extends ArrayConstructable ? SingularInstanceType<T> : T>) => void
type Primitive = string | number | boolean | Date
type NonPrimitive<T> = Exclude<T, string | number | boolean | symbol | bigint | null | undefined>
type EntityDescription = entity | Definition | string // FIXME: Definition not allowed here?, FIXME: { name: string } | ?
type PK = number | string | object
// used as a catch-all type for using tagged template strings: SELECT `foo`. from `bar` etc.
// the resulting signatures are actually not very strongly typed, but they at least accept template strings
// when run in strict mode.
// This signature has to be added to a method as intersection type.
// Defining overloads with it will override preceding signatures and the other way around.
type TaggedTemplateQueryPart<T> = (strings: TemplateStringsArray, ...params: unknown[]) => T

type QueryArtefact = {

/**
* Alias for this attribute.
*/
as (alias: string): void,

/**
* Accesses any nested attribute based on a [path](https://cap.cloud.sap/cap/docs/java/query-api#path-expressions):
* `X.get('a.b.c.d')`. Note that you will not receive
* proper typing after this call.
* To still have access to typed results, use
* `X.a().b().c().d()` instead.
*/
get (path: string): any,

}

// Type for query pieces that can either be chained to build more complex queries or
// awaited to materialise the result:
// `Awaitable<SELECT<Book>, Book> = SELECT<Book> & Promise<Book>`
//
// While the benefit is probably not immediately obvious as we don't exactly
// save a lot of typing over explicitly writing `SELECT<Book> & Promise<Book>`,
// it makes the semantics more explicit. Also sets us up for when TypeScript ever
// improves their generics to support:
//
// `Awaitable<T> = T extends unknown<infer I> ? (T & Promise<I>) : never`
// (at the time of writing, infering the first generic parameter of ANY type
// does not seem to be possible.)
export type Awaitable<T, I> = T & Promise<I>

// note to self: don't try to rewrite these intersection types into overloads.
// It does not work because TaggedTemplateQueryPart will not fit in as regular overload
export interface ByKey {
byKey (primaryKey?: PK): this
}

// unwrap the target of a query and extract its keys.
// Normalise to scalar,
// or fall back to general strings/column expressions
type KeyOfTarget<T, F = string | column_expr> = T extends ConstructedQuery<infer U>
? (U extends ArrayConstructable // Books
? keyof SingularInstanceType<U>
: U extends Constructable // Book
? keyof InstanceType<U>
: F)
: F

type KeyOfSingular<T> = Unwrap<T> extends T
? keyof T
: keyof Unwrap<T>

// as static SELECT borrows the type of Columns directly,
// we need this second type argument to explicitly specific that "this"
// refers to a STATIC<T>, not to a Columns. Or else we could not chain
// other QL functions to .columns
export interface Columns<T, This = undefined> {
columns:
((...col: KeyOfSingular<T>[]) => This extends undefined ? this : This)
& ((col: KeyOfSingular<T>[]) => This extends undefined ? this : This)
& ((...col: (string | column_expr)[]) => This extends undefined ? this : This)
& ((col: (string | column_expr)[]) => This extends undefined ? this : This)
& TaggedTemplateQueryPart<This extends undefined ? this : This>
}

type Op = '=' | '<' | '>' | '<=' | '>=' | '!=' | 'in' | 'like'
type WS = '' | ' '
type Expression<E extends string | number | bigint | boolean> = `${E}${WS}${Op}${WS}`
type ColumnValue = Primitive | Readonly<Primitive[]> | SELECT<any> // not entirely sure why Readonly is required here
// TODO: it would be nicer to check for E[x] for the value instead of Primitive, where x is the key
type Expressions<L,E> = KVPairs<L, Expression<Exclude<keyof E, symbol>>, ColumnValue> extends true
? L
// fallback: allow for any string. Important for when user renamed properties
: KVPairs<L, Expression<string>, ColumnValue> extends true
? L
: never

type HavingWhere<This, E> =
/**
* @param predicate - An object with keys that are valid fields of the target entity and values that are compared to the respective fields.
* @example
* ```js
* SELECT.from(Books).where({ ID: 42 }) // where ID is a valid field of Book
* SELECT.from(Books).having({ ID: 42 }) // where ID is a valid field of Book
* ```
*/
((predicate: Partial<{[column in KeyOfTarget<This extends ConstructedQuery<infer E> ? E : never, never>]: any}>) => This)
/**
* @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.
* @example
* ```js
* SELECT.from(Books).where(['ID =', 42 ]) // where ID is a valid, numerical field of Book
* SELECT.from(Books).having(['ID =', 42 ]) // where ID is a valid, numerical field of Book
*```
*/
& (<const L extends unknown[]>(...expr: Expressions<L, UnwrappedInstanceType<E>>) => This)
& ((...expr: string[]) => This)
& TaggedTemplateQueryPart<This>

export interface Having<T> {
having: HavingWhere<this, T>
}

export interface Where<T> {
where: HavingWhere<this, T>
}

export interface GroupBy {
groupBy: TaggedTemplateQueryPart<this>
& ((columns: Partial<{[column in KeyOfTarget<this extends ConstructedQuery<infer E> ? E : never, never>]: any}>) => this)
& ((...expr: string[]) => this)
& ((ref: ref) => this)
// columns currently not being auto-completed due to complexity
}

export interface OrderBy<T> {
orderBy: TaggedTemplateQueryPart<this>
& ((...col: KeyOfSingular<T>[]) => this)
& ((...expr: string[]) => this)
}

export interface Limit {
limit: TaggedTemplateQueryPart<this>
& ((rows: number, offset?: number) => this)
}

export interface And {
and: TaggedTemplateQueryPart<this>
& ((predicate: object) => this)
& ((...expr: any[]) => this)
}

export interface InUpsert<T> {
data (block: (e: T) => void): this

entries (...entries: object[]): this

values (...val: (null | Primitive)[]): this
values (val: (null | Primitive)[]): this

rows (...row: (null | Primitive)[][]): this
rows (row: (null | Primitive)[][]): this

into: (<T extends ArrayConstructable> (entity: T) => this)
& TaggedTemplateQueryPart<this>
& ((entity: EntityDescription) => this)
}

// don't wrap QLExtensions in more QLExtensions (indirection to work around recursive definition)
export type QLExtensions<T> = T extends QLExtensions_<any> ? T : QLExtensions_<DeepRequired<T>>

/**
* QLExtensions are properties that are attached to entities in CQL contexts.
* They are passed down to all properties recursively.
*/
// have to exclude undefined from the type, or we'd end up with a distribution of Subqueryable
// over T and undefined, which gives us zero code completion within the callable.
type QLExtensions_<T> = { [Key in keyof T]: QLExtensions<T[Key]> } & QueryArtefact & Subqueryable<Exclude<T, undefined>>

/**
* Adds the ability for subqueries to structured properties.
* The final result of each subquery will be the property itself:
* `Book.title` == `Subqueryable<Book>.title()`
*/
type Subqueryable<T> = T extends Primitive ? unknown
// composition of many/ association to many
: T extends readonly unknown[] ? {

/**
* @example
* ```js
* SELECT.from(Books, b => b.author)
* ```
* means: "select all books and project each book's author"
*
* whereas
* ```js
* SELECT.from(Books, b => b.author(a => a.ID))
* ```
* means: "select all books, subselect each book's author's ID
*
* Note that you do not need to return anything from these subqueries.
*/
(fn: ((a: QLExtensions<T[number]>) => any) | '*'): T[number],
}
// composition of one/ association to one
: {

/**
* @example
* ```js
* SELECT.from(Books, b => b.author)
* ```
* means: "select all books and project each book's author"
*
* whereas
* ```js
* SELECT.from(Books, b => b.author(a => a.ID))
* ```
* means: "select all books, subselect each book's author's ID
*
* Note that you do not need to return anything from these subqueries.
*/
(fn: ((a: QLExtensions<T>) => any) | '*'): T,
}
19 changes: 19 additions & 0 deletions apis/internal/util.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,23 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
* Object structure that exposes both array-like and object-like behaviour.
* @see [capire](https://cap.cloud.sap/docs/node.js/cds-reflect#iterable)
*/

export type IterableMap<T> = { [name: string]: T } & Iterable<T>

/**
* T is a tuple of alternating K, V pairs -> true, else false
* Allows for variadic parameter lists with alternating expecing types,
* like we have in cql.SELECT.where
*/
type KVPairs<T,K,V> = T extends []
? true
: T extends [K, V, ...infer R]
? KVPairs<R,K,V>
: false

/**
* Recursively excludes nullability from all properties of T.
*/
export type DeepRequired<T> = {
[K in keyof T]: DeepRequired<T[K]>
} & Exclude<Required<T>, null>
Loading

0 comments on commit 4f90ed9

Please sign in to comment.