Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor QL #201

Merged
merged 27 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f9dab81
Refactor QL
daogrady Aug 14, 2024
66d6f16
Reuse interfaces
daogrady Aug 14, 2024
1fff36d
Factor out byKey
daogrady Aug 14, 2024
93f5e08
Fix compilation tests
daogrady Aug 14, 2024
b570d0a
Add ByKey to update
daogrady Aug 19, 2024
39fd89c
Fix UPDATE.entity overload
daogrady Aug 19, 2024
4fb0cbb
Enable columns in static select
daogrady Aug 19, 2024
5ad291b
Enhance where and having clauses
daogrady Aug 19, 2024
0b878ee
Add changelog entry
daogrady Aug 19, 2024
d84430d
Lint
daogrady Aug 19, 2024
e6bdac3
Fix .where (intermediate commit)
daogrady Aug 22, 2024
2d1d669
Re-enable missing signature
daogrady Aug 26, 2024
e2e4f1b
Pass on template type
daogrady Aug 26, 2024
e1029af
Fix compile errors
daogrady Aug 26, 2024
61e794a
Cleanup
daogrady Aug 26, 2024
3562f77
Factor out common functionality from insert update
daogrady Aug 26, 2024
4c0732b
Merge branch 'main' into chore/refactor-ql
daogrady Sep 2, 2024
1bf5773
Allow subselects and arrays in column expressions
daogrady Sep 2, 2024
25a05a2
Remove some shadowing template arguments
daogrady Sep 2, 2024
91fd74f
Suggest columns names in orderBy
daogrady Sep 10, 2024
016c2e1
Merge branch 'main' into chore/refactor-ql
daogrady Sep 10, 2024
f14313e
Deep Required Projections (#240)
daogrady Sep 24, 2024
0f7c6ca
Add feedback from code review
daogrady Oct 7, 2024
b4111bc
Add comments to unfixed issues
daogrady Oct 7, 2024
67273e3
Merge branch 'main' into chore/refactor-ql
daogrady Oct 7, 2024
a175503
Fix typo
daogrady Oct 7, 2024
a222f9b
Lint
daogrady Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 0.6.6 - TBD
### Added
- The CQL methods `.where` and `.having` now suggest property names for certain overloads.

### Changed
- 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
- The `@types/sap__cds` link created by the `postinstall` script now also works in monorepo setups where the target `@cap-js/cds-types` might already be preinstalled (often hoisted some levels up).
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,
}
21 changes: 21 additions & 0 deletions apis/internal/util.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { QLExtensions_ } from "./query"

/** @internal */
export type _TODO = any

Expand All @@ -14,4 +16,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