From 4f90ed9d643e604ff299ffa92f5af4e300fb4463 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady <103028279+daogrady@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:24:47 +0200 Subject: [PATCH] Refactor QL (#201) --- CHANGELOG.md | 2 + apis/internal/inference.d.ts | 13 +- apis/internal/query.d.ts | 226 ++++++++++++++ apis/internal/util.d.ts | 19 ++ apis/ql.d.ts | 389 ++++++++----------------- apis/services.d.ts | 7 +- test/typescript/apis/project/cds-ql.ts | 74 ++++- 7 files changed, 455 insertions(+), 275 deletions(-) create mode 100644 apis/internal/query.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index feb8a61f..4183ec78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apis/internal/inference.d.ts b/apis/internal/inference.d.ts index 2dc499c5..f5cdcb41 100644 --- a/apis/internal/inference.d.ts +++ b/apis/internal/inference.d.ts @@ -16,9 +16,8 @@ export interface ArrayConstructable { // concrete singular type. // `SingularType` == `Book`. -export type SingularType> = InstanceType[number] - -export type PluralType = Array> +export type SingularInstanceType = InstanceType[number] +export type PluralInstanceType = Array> // Convenient way of unwrapping the inner type from array-typed values, as well as the value type itself // `class MyArray extends Array`` @@ -28,11 +27,14 @@ export type PluralType = Array> // This type introduces an indirection that streamlines their behaviour for both cases. // For any scalar type `Unwrap` behaves idempotent. export type Unwrap = T extends ArrayConstructable - ? SingularType + ? SingularInstanceType : T extends Array ? U : T +// ...and sometimes Unwrap gives us a class (typeof Book), but we need an instance (Book) +export type UnwrappedInstanceType = Unwrap extends Constructable ? InstanceType> : Unwrap + /* * the following three types are used to convert union types to intersection types. @@ -63,6 +65,7 @@ export type Unwrap = 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 extends Array ? N : A export type UnionToIntersection = Partial<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never> export type UnionsToIntersections = Array>> +export type Scalarise = A extends Array ? N : A +export type Pluralise = S extends Array ? S : Array diff --git a/apis/internal/query.d.ts b/apis/internal/query.d.ts new file mode 100644 index 00000000..035558a9 --- /dev/null +++ b/apis/internal/query.d.ts @@ -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 = (e: QLExtensions : T>) => void +type Primitive = string | number | boolean | Date +type NonPrimitive = Exclude +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 = (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, Book> = SELECT & Promise` +// +// While the benefit is probably not immediately obvious as we don't exactly +// save a lot of typing over explicitly writing `SELECT & Promise`, +// it makes the semantics more explicit. Also sets us up for when TypeScript ever +// improves their generics to support: +// +// `Awaitable = T extends unknown ? (T & Promise) : never` +// (at the time of writing, infering the first generic parameter of ANY type +// does not seem to be possible.) +export type Awaitable = T & Promise + +// 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 extends ConstructedQuery + ? (U extends ArrayConstructable // Books + ? keyof SingularInstanceType + : U extends Constructable // Book + ? keyof InstanceType + : F) + : F + + type KeyOfSingular = Unwrap extends T + ? keyof T + : keyof Unwrap + +// as static SELECT borrows the type of Columns directly, +// we need this second type argument to explicitly specific that "this" +// refers to a STATIC, not to a Columns. Or else we could not chain +// other QL functions to .columns +export interface Columns { + columns: + ((...col: KeyOfSingular[]) => This extends undefined ? this : This) + & ((col: KeyOfSingular[]) => This extends undefined ? this : This) + & ((...col: (string | column_expr)[]) => This extends undefined ? this : This) + & ((col: (string | column_expr)[]) => This extends undefined ? this : This) + & TaggedTemplateQueryPart +} + +type Op = '=' | '<' | '>' | '<=' | '>=' | '!=' | 'in' | 'like' +type WS = '' | ' ' +type Expression = `${E}${WS}${Op}${WS}` +type ColumnValue = Primitive | Readonly | SELECT // 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 = KVPairs>, ColumnValue> extends true + ? L + // fallback: allow for any string. Important for when user renamed properties + : KVPairs, ColumnValue> extends true + ? L + : never + +type HavingWhere = + /** + * @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 ? 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 + *``` + */ + & ((...expr: Expressions>) => This) + & ((...expr: string[]) => This) + & TaggedTemplateQueryPart + +export interface Having { + having: HavingWhere +} + +export interface Where { + where: HavingWhere +} + +export interface GroupBy { + groupBy: TaggedTemplateQueryPart + & ((columns: Partial<{[column in KeyOfTarget ? E : never, never>]: any}>) => this) + & ((...expr: string[]) => this) + & ((ref: ref) => this) + // columns currently not being auto-completed due to complexity +} + +export interface OrderBy { + orderBy: TaggedTemplateQueryPart + & ((...col: KeyOfSingular[]) => this) + & ((...expr: string[]) => this) +} + +export interface Limit { + limit: TaggedTemplateQueryPart + & ((rows: number, offset?: number) => this) +} + +export interface And { + and: TaggedTemplateQueryPart + & ((predicate: object) => this) + & ((...expr: any[]) => this) +} + +export interface InUpsert { + 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: ( (entity: T) => this) + & TaggedTemplateQueryPart + & ((entity: EntityDescription) => this) +} + +// don't wrap QLExtensions in more QLExtensions (indirection to work around recursive definition) +export type QLExtensions = T extends QLExtensions_ ? T : QLExtensions_> + +/** + * 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_ = { [Key in keyof T]: QLExtensions } & QueryArtefact & Subqueryable> + +/** + * Adds the ability for subqueries to structured properties. + * The final result of each subquery will be the property itself: + * `Book.title` == `Subqueryable.title()` + */ +type Subqueryable = 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) => 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) => any) | '*'): T, + } \ No newline at end of file diff --git a/apis/internal/util.d.ts b/apis/internal/util.d.ts index 0e6d07b6..d9e51ee5 100644 --- a/apis/internal/util.d.ts +++ b/apis/internal/util.d.ts @@ -14,4 +14,23 @@ export type XOR = (T | U) extends object ? (Without & 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 = { [name: string]: T } & Iterable + +/** + * 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 extends [] + ? true + : T extends [K, V, ...infer R] + ? KVPairs + : false + +/** + * Recursively excludes nullability from all properties of T. + */ +export type DeepRequired = { + [K in keyof T]: DeepRequired +} & Exclude, null> diff --git a/apis/ql.d.ts b/apis/ql.d.ts index da3e439c..ac11c4e8 100644 --- a/apis/ql.d.ts +++ b/apis/ql.d.ts @@ -1,199 +1,93 @@ -import { Definition, EntityElements } from './csn' +import { EntityElements } from './csn' +//export type { Query } from './cqn' import * as CQN from './cqn' -import { Constructable, ArrayConstructable, SingularType, PluralType } from './internal/inference' -import * as linked from './linked' -import { ref, column_expr } from './cqn' +import { + Constructable, + ArrayConstructable, + SingularInstanceType, + PluralInstanceType +} from './internal/inference' +import { ref } from './cqn' +import { + And, + Awaitable, + Columns, + EntityDescription, + Having, + GroupBy, + Limit, + OrderBy, + PK, + Projection, + QLExtensions, + TaggedTemplateQueryPart, + Where, + ByKey, + InUpsert, +} from './internal/query' +import { _TODO } from './internal/util' export type Query = CQN.Query -export class ConstructedQuery { +export { QLExtensions } from './internal/query' +// this just serves as a reminder that we can not get rid of some of the anys at this point +// as the would refer to the generic type of the surrounding class +type StaticAny = any + +export class ConstructedQuery { + // branded type to break covariance for the subclasses + // that don't make explicit use of the generic. So `UPDATE !<: UPDATE` + declare private _: T then (_resolved: (x: any) => any, _rejected: (e: Error) => any): any } -export type PK = number | string | object - - -type Primitive = string | number | boolean | Date - -// don't wrap QLExtensions in more QLExtensions (indirection to work around recursive definition) -type QLExtensions = T extends QLExtensions_ ? T : QLExtensions_ - -/** - * Target for any QL operation - */ -type Target = linked.classes.entity | Definition | string - -/** - * QLExtensions are properties that are attached to entities in CQL contexts. - * They are passed down to all properties recursively. -*/ -type QLExtensions_ = { - [Key in keyof T]: QLExtensions -} & { - - /** - * 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, - - // 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. -} & Subqueryable> - -/** - * Adds the ability for subqueries to structured properties. - * The final result of each subquery will be the property itself: - * `Book.title` == `Subqueryable.title()` - */ -type Subqueryable = 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) => 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) => any) | '*'): T, - } - - -// Alias for projections -// https://cap.cloud.sap/docs/node.js/cds-ql?q=projection#projection-functions -// export type Projection = (e:T)=>void -export type Projection = (e: QLExtensions : T>) => void -// Type for query pieces that can either be chained to build more complex queries or -// awaited to materialise the result: -// `Awaitable, Book> = SELECT & Promise` -// -// While the benefit is probably not immediately obvious as we don't exactly -// save a lot of typing over explicitly writing `SELECT & Promise`, -// it makes the semantics more explicit. Also sets us up for when TypeScript ever -// improves their generics to support: -// -// `Awaitable = T extends unknown ? (T & Promise) : never` -// (at the time of writing, infering the first generic parameter of ANY type -// does not seem to be possible.) -export type Awaitable = T & Promise - // all the functionality of an instance of SELECT, but directly callable: // new SELECT(...).(...) == SELECT(...) -export type StaticSELECT = typeof SELECT - & ((...columns: (T extends ArrayConstructable ? keyof SingularType : keyof T)[]) => SELECT) - & ((...columns: string[]) => SELECT) - & ((columns: string[]) => SELECT) - & (TaggedTemplateQueryPart>) +export type StaticSELECT = { columns: SELECT['columns'] } + & typeof SELECT + & SELECT['columns'] & SELECT_from // as it is not directly quantified, ... & SELECT_one // ...we should expect both a scalar and a list -declare class QL { +export declare class QL { SELECT: StaticSELECT - INSERT: typeof INSERT - & ((...entries: object[]) => INSERT) & ((entries: object[]) => INSERT) + INSERT: typeof INSERT + & ((...entries: object[]) => INSERT) & ((entries: object[]) => INSERT) UPSERT: typeof UPSERT - & ((...entries: object[]) => UPSERT) & ((entries: object[]) => UPSERT) + & ((...entries: object[]) => UPSERT) & ((entries: object[]) => UPSERT) - UPDATE: typeof UPDATE - & typeof UPDATE.entity + UPDATE: typeof UPDATE + & typeof UPDATE.entity<_TODO> - DELETE: typeof DELETE - & ((...entries: object[]) => DELETE) & ((entries: object[]) => DELETE) + DELETE: typeof DELETE + & ((...entries: object[]) => DELETE) & ((entries: object[]) => DELETE) - CREATE: typeof CREATE + CREATE: typeof CREATE - DROP: typeof DROP + DROP: typeof DROP } -// 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 = (strings: TemplateStringsArray, ...params: unknown[]) => T - -export class SELECT extends ConstructedQuery { +export interface SELECT extends Where, And, Having, GroupBy, OrderBy, Limit { + // overload specific to SELECT + columns: Columns>['columns'] & ((projection: Projection) => this) +} +export class SELECT extends ConstructedQuery { static one: SELECT_one & { from: SELECT_one } - static distinct: typeof SELECT + static distinct: typeof SELECT static from: SELECT_from - from: SELECT_from & TaggedTemplateQueryPart - & ((entity: Target, primaryKey?: PK, projection?: Projection) => this) - - byKey (primaryKey?: PK): this - columns: TaggedTemplateQueryPart - & ((projection: Projection) => this) - & ((...col: (T extends ArrayConstructable ? keyof SingularType : keyof T)[]) => this) - & ((...col: (string | column_expr)[]) => this) - & ((col: (string | column_expr)[]) => this) - - where: TaggedTemplateQueryPart - & ((predicate: object) => this) - & ((...expr: any[]) => this) - - and: TaggedTemplateQueryPart - & ((predicate: object) => this) - & ((...expr: any[]) => this) - - having: TaggedTemplateQueryPart - & ((...expr: string[]) => this) - & ((predicate: object) => this) - - groupBy: TaggedTemplateQueryPart - & ((...expr: string[]) => this) - - orderBy: TaggedTemplateQueryPart - & ((...expr: string[]) => this) - - limit: TaggedTemplateQueryPart - & ((rows: number, offset?: number) => this) + from: SELECT_from + & TaggedTemplateQueryPart + & ((entity: EntityDescription, primaryKey?: PK, projection?: Projection) => this) forShareLock (): this @@ -224,146 +118,126 @@ export class SELECT extends ConstructedQuery { type SELECT_one = - TaggedTemplateQueryPart, InstanceType>> + TaggedTemplateQueryPart, InstanceType<_TODO>>> & // calling with class - (> - (entityType: T, projection?: Projection>>) - => Awaitable>, SingularType>) + ( + (entityType: T, projection?: Projection>>) + => Awaitable>, SingularInstanceType>) & - (> - (entityType: T, primaryKey: PK, projection?: Projection>>) - => Awaitable>, SingularType>) + ( + (entityType: T, primaryKey: PK, projection?: Projection>>) + => Awaitable>, SingularInstanceType>) - & ((entity: Target, primaryKey?: PK, projection?: Projection) => SELECT) + & ((entity: EntityDescription, primaryKey?: PK, projection?: Projection) => SELECT<_TODO>) & ( (entity: T[], projection?: Projection) => Awaitable, T>) & ( (entity: T[], primaryKey: PK, projection?: Projection) => Awaitable, T>) & ( (entity: { new(): T }, projection?: Projection) => Awaitable, T>) & ( (entity: { new(): T }, primaryKey: PK, projection?: Projection) => Awaitable, T>) - & ((subject: ref) => SELECT) + & ((subject: ref) => SELECT<_TODO>) type SELECT_from = // tagged template - TaggedTemplateQueryPart, InstanceType>> + TaggedTemplateQueryPart, InstanceType<_TODO>>> & // calling with class - (> - (entityType: T, projection?: Projection>>) - => Awaitable, InstanceType>) + ( + (entityType: E, projection?: Projection>>) + => Awaitable, InstanceType>) & - (> - (entityType: T, primaryKey: PK, projection?: Projection>) - => Awaitable>, InstanceType>>) // when specifying a key, we expect a single element as result + ( + (entityType: E, primaryKey: PK, projection?: Projection>) + => Awaitable>, SingularInstanceType>) // when specifying a key, we expect a single element as result // calling with definition - & ((entity: Target, primaryKey?: PK, projection?: Projection) => SELECT) + & ((entity: EntityDescription, primaryKey?: PK, projection?: Projection) => SELECT) // calling with concrete list & ( (entity: T[], projection?: Projection) => SELECT & Promise) & ( (entity: T[], primaryKey: PK, projection?: Projection) => Awaitable, T>) - & ((subject: ref) => SELECT) + & ((subject: ref) => SELECT<_TODO>) // put these overloads at the very end, as they would also match the above // We expect these to be the overloads for scalars since we covered arrays above -> wrap them back in Array - & (>( + & (( + entityType: T, + columns: string[] // could be keyof in the future + ) => Awaitable>, PluralInstanceType>) + & (( + entityType: T, + primaryKey: PK, + columns: string[] // could be keyof in the future + ) => Awaitable>, PluralInstanceType>) + & (( entityType: T, projection?: Projection> - ) => Awaitable>, PluralType>) - & (>( + ) => Awaitable>, PluralInstanceType>) + & (( entityType: T, primaryKey: PK, projection?: Projection> - ) => Awaitable>, PluralType>) + ) => Awaitable>, PluralInstanceType>) + // currently no auto completion of columns, due to complexity +export interface INSERT extends Columns, InUpsert {} +export class INSERT extends ConstructedQuery { -export class INSERT extends ConstructedQuery { - - static into: (> (entity: T, entries?: object | object[]) => INSERT>) + static into: ( (entity: T, entries?: Entries) => INSERT>) & (TaggedTemplateQueryPart>) - & ((entity: Target, entries?: object | object[]) => INSERT) - & ( (entity: Constructable, entries?: object | object[]) => INSERT) - & ( (entity: T, entries?: T | object | object[]) => INSERT) - - into: ( (entity: T) => this) - & TaggedTemplateQueryPart - & ((entity: Target) => this) - - data (block: (e: T) => void): this - - entries (...entries: object[]): this - - columns (...col: (T extends ArrayConstructable ? keyof SingularType : keyof T)[]): this - - columns (...col: string[]): this - - values (...val: any[]): this - - rows (...row: any[]): this + & ((entity: EntityDescription, entries?: Entries) => INSERT) + & ( (entity: Constructable, entries?: Entries) => INSERT) + & ( (entity: T, entries?: T | Entries) => INSERT) + /** + * @deprected + */ as (select: SELECT): this + from (select: SELECT): this INSERT: CQN.INSERT['INSERT'] } +type Entries = {[key:string]: T} | {[key:string]: T} +export interface UPSERT extends Columns, InUpsert {} +export class UPSERT extends ConstructedQuery { -export class UPSERT extends ConstructedQuery { - - static into: (> (entity: T, entries?: object | object[]) => UPSERT>) - & (TaggedTemplateQueryPart>) - & ((entity: Target, entries?: object | object[]) => UPSERT) - & ( (entity: Constructable, entries?: object | object[]) => UPSERT) - & ( (entity: T, entries?: T | object | object[]) => UPSERT) - - into: ( (entity: T) => this) - & TaggedTemplateQueryPart - & ((entity: Target) => this) - - data (block: (e: T) => void): this + static into: ( (entity: T, entries?: Entries) => UPSERT>) + & (TaggedTemplateQueryPart>) + & ((entity: EntityDescription, entries?: Entries) => UPSERT) + & ( (entity: Constructable, entries?: Entries) => UPSERT) + // currently no easy way to restrict T to non-primitives + & ( (entity: T, entries?: T | Entries) => UPSERT) - entries (...entries: object[]): this - columns (...col: (T extends ArrayConstructable ? keyof SingularType : keyof T)[]): this - - columns (...col: string[]): this - - values (...val: any[]): this - - rows (...row: any[]): this UPSERT: CQN.UPSERT['UPSERT'] } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export class DELETE extends ConstructedQuery { +export interface DELETE extends Where, And, ByKey {} +export class DELETE extends ConstructedQuery { static from: - TaggedTemplateQueryPart, InstanceType>> - & ((entity: Target | ArrayConstructable, primaryKey?: PK) => DELETE) - & ((subject: ref) => DELETE) - - byKey (primaryKey?: PK): this - - where (predicate: object): this - - where (...expr: any[]): this + TaggedTemplateQueryPart, InstanceType>> + & ((entity: EntityDescription | ArrayConstructable, primaryKey?: PK) => DELETE) + & ((subject: ref) => DELETE<_TODO>) - and (predicate: object): this - - and (...expr: any[]): this DELETE: CQN.DELETE['DELETE'] } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export class UPDATE extends ConstructedQuery { +export interface UPDATE extends Where, And, ByKey {} + + +export class UPDATE extends ConstructedQuery { // cds-typer plural - static entity> (entity: T, primaryKey?: PK): UPDATE> + // FIXME: this returned UPDATE> before. should UPDATE.entity(...) return Book or Books? + static entity (entity: T, primaryKey?: PK): UPDATE> - static entity (entity: Target, primaryKey?: PK): UPDATE + static entity (entity: EntityDescription, primaryKey?: PK): UPDATE - static entity (entity: Constructable, primaryKey?: PK): UPDATE + static entity (entity: T, primaryKey?: PK): UPDATE + // currently no easy way to restrict T from being a primitive type static entity (entity: T, primaryKey?: PK): UPDATE - byKey (primaryKey?: PK): this // with (block: (e:T)=>void) : this // set (block: (e:T)=>void) : this set: TaggedTemplateQueryPart @@ -372,30 +246,21 @@ export class UPDATE extends ConstructedQuery { with: TaggedTemplateQueryPart & ((data: object) => this) - where (predicate: object): this - - where (...expr: any[]): this - - and (predicate: object): this - - and (...expr: any[]): this UPDATE: CQN.UPDATE['UPDATE'] } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export class CREATE extends ConstructedQuery { +export class CREATE extends ConstructedQuery { - static entity (entity: Target): CREATE + static entity (entity: EntityDescription): CREATE CREATE: CQN.CREATE['CREATE'] } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export class DROP extends ConstructedQuery { +export class DROP extends ConstructedQuery { - static entity (entity: Target): DROP + static entity (entity: EntityDescription): DROP DROP: CQN.DROP['DROP'] diff --git a/apis/services.d.ts b/apis/services.d.ts index 3246d4d2..58ca74d3 100644 --- a/apis/services.d.ts +++ b/apis/services.d.ts @@ -1,5 +1,5 @@ import { SELECT, INSERT, UPDATE, DELETE, Query, ConstructedQuery, UPSERT } from './ql' -import { Awaitable } from './ql' +import { Awaitable } from './internal/query' import { ArrayConstructable, Constructable, Unwrap } from './internal/inference' //import { ModelPart, CSN, LinkedDefinition, LinkedEntity } from './linked' import * as linked from './linked' @@ -7,6 +7,7 @@ import * as csn from './csn' import { EventContext } from './events' import { Request } from './events' import { ReadableStream } from 'node:stream/web' +import { _TODO } from './internal/util' type Key = number | string | any @@ -58,7 +59,7 @@ export class QueryAPI { * @see [docs](https://cap.cloud.sap/docs/node.js/core-services#crud-style-api) */ run: { - (query: ConstructedQuery | ConstructedQuery[]): Promise, + (query: ConstructedQuery<_TODO> | ConstructedQuery<_TODO>[]): Promise, (query: Query): Promise, (query: string, args?: any[] | object): Promise, } @@ -177,7 +178,7 @@ export class Service extends QueryAPI { (event: types.event, path: string, data?: object, headers?: object): Promise, (event: types.event, data?: object, headers?: object): Promise, (details: { event: types.event, data?: object, headers?: object }): Promise, - (details: { query: ConstructedQuery, data?: object, headers?: object }): Promise, + (details: { query: ConstructedQuery, data?: object, headers?: object }): Promise, (details: { method: types.eventName, path: string, data?: object, headers?: object }): Promise, (details: { event: types.eventName, entity: linked.Definition | string, data?: object, params?: object, headers?: object }): Promise, } diff --git a/test/typescript/apis/project/cds-ql.ts b/test/typescript/apis/project/cds-ql.ts index e0d05552..698b3b1e 100644 --- a/test/typescript/apis/project/cds-ql.ts +++ b/test/typescript/apis/project/cds-ql.ts @@ -1,3 +1,6 @@ +import { ArrayConstructable } from '../../../../apis/internal/inference' +import { QLExtensions_ } from '../../../../apis/internal/query' +import { DeepRequired } from '../../../../apis/internal/util' import { QLExtensions } from '../../../../apis/ql' import { Foo, Foos, attach } from './dummy' @@ -11,19 +14,64 @@ sel = SELECT.from(Foo.drafts) sel = SELECT.from(Foos.drafts) sel = SELECT.from(Foos.drafts, 42) +let selSingular: SELECT = undefined as unknown as SELECT +selSingular.columns('ref') // auto suggested + +let selAny: SELECT = undefined as unknown as SELECT +selAny.columns('asd') // unclear target, any string is allowed + const selStatic: SELECT | Promise = SELECT.from(Foos) -SELECT.from(Foos).columns("x") // x was suggested by code completion -sel.from(Foos) -sel.columns("x") // x was suggested by code completion + // x was suggested by code completion +SELECT.from(Foos).columns('x') +sel.from(Foos).columns('x') +sel.from(Foo).columns('x') +sel.columns("x") + +// y is not a valid columns for Foo(s), +// but is allowed anyway since we permit arbitrary strings as well +SELECT.from(Foos).columns('y') +SELECT.from(Foos).where('x=', 42) +SELECT.from(Foos).where('x >', 42, 'y =', '42') +const predefinedArray = [42] +SELECT.from(Foos).where('x in', [42]) +SELECT.from(Foos).where('x in', predefinedArray) +SELECT.from(Foos).where('x in', SELECT.from(Foos)) +// @ts-expect-error - can't just use anything as even parameter +SELECT.from(Foos).where('x in', Foos) +SELECT.from(Foos).where('fn(x) = ', 42) +sel.from(Foos).columns('y') +sel.from(Foo).columns('y') +sel.columns("y") +SELECT.from(Foos, f => f.ref(r => r.x)) // ref should be callable without optional chaining (DeepRequired) + +SELECT.from(Foos).orderBy('x') // x auto completed +SELECT.from(Foos).orderBy('y') // non-columns also still possible + +SELECT.from(Foos, f => { + f.x, + // @ts-expect-error - foobar is not a valid column + f.foobar +}) + sel.SELECT.columns?.filter(e => !e) // check if this is array +sel.from(Foos).where({ ref:42 }) // ref was suggested by code completion +sel.from(Foos).where({ zef:42 }) // non-keys are allowed too + // ensure ql returns a proper CQN -const s = SELECT.from(Foos).columns('ID').where('ID =', 42) +const s = SELECT.from(Foos).columns('x').where('x=', 42) +SELECT.from(Foo).columns('x').where('x=', 42) +SELECT.from(Foo).columns('x').where('y=', 42) // also allowed as per [string, Primitive] signature +// @ts-expect-error invalid key type +SELECT.from(Foo).columns('x').where([new Foo()], 42) +// @ts-expect-error missing operator +SELECT.from(Foo).columns('x').where('y', 42) s.SELECT.from.ref s.SELECT.columns?.[0].ref s.SELECT.where?.[0].ref s.SELECT.where?.[2].val +SELECT.from(Foo).columns('x').where('x =', 42) SELECT(Foos) === SELECT.from(Foos) @@ -34,7 +82,6 @@ ins.into(Foos) ins.into(Foos) ins.columns("x") // x was suggested by code completion ins.INSERT.into === "foo" - INSERT.into("Bla").as(SELECT.from("Foo")) let upd: UPDATE @@ -100,6 +147,8 @@ SELECT.from(Foos, (f:any) => { const number: QLExtensions = f.x }) +SELECT.columns`a`.from`Foo`; + SELECT.from(Foos).columns(f => { const iterator: QLExtensions = f const number: QLExtensions = f.x @@ -131,3 +180,18 @@ DELETE.from `${x}` .where `ID=${x}` SELECT.from(Foos).forUpdate() SELECT.from(Foos).forUpdate({wait: 5}) SELECT.from(Foos).forShareLock() + +INSERT.into('Foos').values(1,2,3) +INSERT.into('Foos').values([1,2,3]) +// @ts-expect-error +INSERT.into('Foos').values([[1,2,3]]) +// @ts-expect-error +INSERT.into('Foos').values([],[]) + +// @ts-expect-error +INSERT.into('Foos').rows(1,2,3) +INSERT.into('Foos').rows([1,2,3]) +INSERT.into('Foos').rows([[1,2,3]]) +INSERT.into('Foos').rows([[1,2,3],[1,2]]) +// @ts-expect-error +INSERT.into('Foos').values([[1,2,3]])