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

[BUG][Regression] Incorrect types generated with composition of many #225

Open
1 task done
tsteckenborn opened this issue Apr 25, 2024 · 5 comments
Open
1 task done
Labels
bug Something isn't working keepalive Will not be closed by Stale bot

Comments

@tsteckenborn
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Nature of Your Project

TypeScript

Current Behavior

(Could be related to earlier behavior in #183)

Given a definition such as:

service TestService {
    entity testNamespace.Test {
        key id              : String;
            testComposition : Composition of many {
                                  key id : String;
                              };
    }

    entity Test as projection on testNamespace.Test;
}

Leads in the @cds-models/TestService/index.ts to:

// ...
  return class Test extends Base {
        id?: string;
        testComposition?: __.Composition.of.many<__.DeepRequired<TestService.testNamespace.Test>['testComposition']>;
      static readonly actions: Record<never, never>
  };
}
// ...

With <__.DeepRequired<TestService < --- Cannot find namespace 'TestService'.ts(2503)

Expected Behavior

Would've expected the generated types be correct.

Steps To Reproduce

See above

Environment

- **OS**: MacOS
- **Node**: 20.12.2
- **npm**: /
- **cds-typer**: 20.0.1
- **cds**: 7.8.1

Repository Containing a Minimal Reproducible Example

No response

Anything else?

No response

@tsteckenborn tsteckenborn added bug Something isn't working new labels Apr 25, 2024
@tsteckenborn
Copy link
Author

There seems to be a second issue with regards to composition of many.

Having:

entity Test.UserInterfaces {
    key id            : String;
        testProperty1 : Composition of many {
                            test : String;
                        };
        testProperty2 : Composition of many {
                            test : String;
                        };
}

service TestService {
    entity UserInterfaces as projection on Test.UserInterfaces;
}

yields the following:

[...]
export function _UserInterfaceAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class UserInterface extends Base {
        id?: string;
        usageStatus?: __.Composition.of.many<__.DeepRequired<sap.test.TestService.UserInterface>['usageStatus']>;
        intentMapping?: __.Composition.of.many<__.DeepRequired<sap.test.TestService.UserInterface>['intentMapping']>;
      static readonly actions: Record<never, never>
  };
}
export class UserInterface extends _UserInterfaceAspect(__.Entity) {}
Object.defineProperty(UserInterface, 'name', { value: 'sap.test.TestService.UserInterfaces' })
export class UserInterfaces extends Array<UserInterface> {$count?: number}
Object.defineProperty(UserInterfaces, 'name', { value: 'sap.test.TestService.UserInterfaces' })

export namespace UserInterfaces {
  export function _usageStatuAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
    return class usageStatu extends Base {
            up_?: __.Association.to<_sap_test_Test.UserInterface>;
            up__id?: string;
            contextContent?: string | null;
        static readonly actions: Record<never, never>
    };
  }
  export class usageStatu extends _usageStatuAspect(__.Entity) {}
  Object.defineProperty(usageStatu, 'name', { value: 'sap.test.Test.UserInterfaces.usageStatus' })
  export class UserInterfaces extends Array<usageStatu> {$count?: number}
  Object.defineProperty(UserInterfaces, 'name', { value: 'sap.test.Test.UserInterfaces.usageStatus' })
  
  export function _intentMappingAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
    return class intentMapping extends Base {
            up_?: __.Association.to<_sap_test_Test.UserInterface>;
            up__id?: string;
            externalLinkOpenInNewTab?: boolean | null;
        static readonly actions: Record<never, never>
    };
  }
  export class intentMapping extends _intentMappingAspect(__.Entity) {}
  Object.defineProperty(intentMapping, 'name', { value: 'sap.test.Test.UserInterfaces.intentMapping' })
  export class UserInterfaces extends Array<intentMapping> {$count?: number}
  Object.defineProperty(UserInterfaces, 'name', { value: 'sap.test.Test.UserInterfaces.intentMapping' })
  
}

With the pluralized versions appearing to be named as the containing entity:

[...]
  export class usageStatu extends _usageStatuAspect(__.Entity) {}
  Object.defineProperty(usageStatu, 'name', { value: 'sap.test.Test.UserInterfaces.usageStatus' })
  export class UserInterfaces extends Array<usageStatu> {$count?: number}
  Object.defineProperty(UserInterfaces, 'name', { value: 'sap.test.Test.UserInterfaces.usageStatus' })
[...]
  export class intentMapping extends _intentMappingAspect(__.Entity) {}
  Object.defineProperty(intentMapping, 'name', { value: 'sap.test.Test.UserInterfaces.intentMapping' })
  export class UserInterfaces extends Array<intentMapping> {$count?: number}
  Object.defineProperty(UserInterfaces, 'name', { value: 'sap.test.Test.UserInterfaces.intentMapping' })
[...]

Which a) seems to incorrect and b) yields the type error Duplicate identifier 'UserInterfaces'.ts(2300)

@tsteckenborn
Copy link
Author

tsteckenborn commented Apr 30, 2024

The bottom one seems to be due to:

plural = util.getPluralAnnotation(typeInfo.csn) ?? typeInfo.plainName

Here it's running as typeInfo.plainName which is then for these ones:

{
  typeName: undefined,
  singular: 'usageStatu',
  plural: 'UserInterfaces'
}
{
  'typeInfo.csn.name': '[...].UserInterfaces.usageStatus'
}
{ 'typeInfo.plainName': 'UserInterfaces' }
{
  typeName: undefined,
  singular: 'intentMapping',
  plural: 'UserInterfaces'
},
{
  'typeInfo.csn.name': '[...].UserInterfaces.intentMapping'
}
{ 'typeInfo.plainName': 'UserInterfaces' }

So the fallback here (plainName) seems to not work properly.

// Edit:

I assume it boils down to

untangle(fq) {
where it's seen as e.g.:

fq: [...someService].UserInterfaces.intentMapping
ns: [...someService]
property: intentMapping
nameParts: UserInterfaces
scope: 
name: UserInterfaces

Due to util.getPluralAnnotation(typeInfo.csn) being undefined in then falls back to UserInterfaces as that's set as the "plainType", which then results in the name being used when creating the classes with the pluralized names.

@daogrady daogrady removed the new label May 7, 2024
@prophet1906
Copy link

I am facing the same issue. The issue is reproducible for >= 0.20.0.

Copy link

This issue has not been updated in a while. If it is still relevant, please comment on it to keep it open. The issue will be closed soon if it remains inactive.

@github-actions github-actions bot added the stale label Jun 10, 2024
@daogrady daogrady added keepalive Will not be closed by Stale bot and removed stale labels Jun 11, 2024
@martondaniamista
Copy link

I'm facing the same issue. I'd like to add a few information blocks to the thread:

  1. Not only composition of many produces this error, but any composition for me.
  2. Until I don't import the entity (that has a composition in it) into the service.ts file the error will not show. It only appears to happen when the I import the entity into the service.ts.
    I'm suspecting here that Tobias's service.ts file imports one of the entities so it looks something like this:
[...]
import { Test } from "@testNamespace/cds-models/TestService ";
[...]
service.before("CREATE", Test, (req)=>{
     console.log(req.data)
});

As it was written before me it is reproducable in older versions, as well as in the new 0.23.0.

I was just learning CAP with ts, but I'd like to add my use case as well, maybe it will help resolve this issue.
It's just a practice project, but this is how the landscape looks to me:

I have the schema.cds file

context cameragearrental {
    entity Products {
        productType : String enum { lens; flash; body };
        key serialNum   : Integer;
        key brand       : Association to one Brands;
        title           : String;
        lensDetails : Composition of {
            minFocalLength: Integer;
            maxFocalLength: Integer;
            lensType: String enum { prime; zoom };
            aperture: Decimal(2,1);
            stabilization: Boolean;
            apertureBlades: UInt8 ;
            mount: Mount;
            afMotor: String;
        };
        reservations: Association to many Reservations on reservations.product = $self;
    }
    [...]
}

I have more compositions in the entity, and I have other entities that I haven't included just for the sake of simplicity.
I have the service.cds file that looks like this:

[...]
service AdminService {
    entity Products as projection on cameragearrental.Products;
}
[...]

And the service.ts that looks like this:

[...]
import { Product } from "@gearrental/cds-models/AdminService";
[...]
service.before("CREATE", Product, (req)=>{
     console.log(req.data)
});

All this results the following in the cds-models/AdminService/index.ts

// This is an automatically generated file. Please do not change its contents manually!
import * as _cameragearrental from './../cameragearrental';
import * as __ from './../_';
import * as _ from './..';
export default { name: 'AdminService' }
// enum
const Product_productType = {
  lens: "lens",
  flash: "flash",
  body: "body",
} as const;
type Product_productType = "lens" | "flash" | "body"

// enum
const Reservation_state = {
  reserved: "reserved",
  lended: "lended",
  returned: "returned",
  canceled: "canceled",
} as const;
type Reservation_state = "reserved" | "lended" | "returned" | "canceled"

// enum
const lensDetail_lensType = {
  prime: "prime",
  zoom: "zoom",
} as const;
type lensDetail_lensType = "prime" | "zoom"

export function _ProductAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends Base {
        productType?: Product_productType | null;
        serialNum?: number;
        brand?: __.Association.to<_cameragearrental.Brand>;
        brand_ID?: string;
        title?: string | null;
        lensDetails?: __.Composition.of<__.DeepRequired<AdminService.Product>['lensDetails']> | null;
        reservations?: __.Association.to.many<Reservations>;
      static productType = Product_productType
      static readonly actions: Record<never, never>
  };
}
export class Product extends _ProductAspect(__.Entity) {}
Object.defineProperty(Product, 'name', { value: 'AdminService.Products' })
Object.defineProperty(Product, 'is_singular', { value: true })
export class Products extends Array<Product> {$count?: number}
Object.defineProperty(Products, 'name', { value: 'AdminService.Products' })

export function _ReservationAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends _._cuidAspect(Base) {
        ID?: string;
        product?: __.Association.to<Product> | null;
        product_serialNum?: number | null;
        product_brand_ID?: string | null;
        startDate?: __.CdsDate | null;
        endDate?: __.CdsDate | null;
        userName?: string | null;
        state?: Reservation_state | null;
      static state = Reservation_state
      static readonly actions: Record<never, never>
  };
}
export class Reservation extends _ReservationAspect(__.Entity) {}
Object.defineProperty(Reservation, 'name', { value: 'AdminService.Reservations' })
Object.defineProperty(Reservation, 'is_singular', { value: true })
export class Reservations extends Array<Reservation> {$count?: number}
Object.defineProperty(Reservations, 'name', { value: 'AdminService.Reservations' })

export namespace Products {
  export function _lensDetailAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
    return class extends Base {
            up_?: __.Association.to<_cameragearrental.Product>;
            up__serialNum?: number;
            up__brand_ID?: string;
            minFocalLength?: number | null;
            maxFocalLength?: number | null;
            lensType?: lensDetail_lensType | null;
            aperture?: number | null;
            stabilization?: boolean | null;
            apertureBlades?: number | null;
            mount?: _cameragearrental.Mount | null;
            afMotor?: string | null;
        static lensType = lensDetail_lensType
        static readonly actions: Record<never, never>
    };
  }
  export class lensDetail extends _lensDetailAspect(__.Entity) {}
  Object.defineProperty(lensDetail, 'name', { value: 'cameragearrental.Products.lensDetails' })
  Object.defineProperty(lensDetail, 'is_singular', { value: true })
  export class Products extends Array<lensDetail> {$count?: number}
  Object.defineProperty(Products, 'name', { value: 'cameragearrental.Products.lensDetails' })
  
}

cds-models/AdminService/index.ts

// This is an automatically generated file. Please do not change its contents manually!
import * as __ from './../_';
import * as _ from './..';
// enum
export const Mount = {
  CANONEF: "Canon-EF",
  CANONRF: "Canon-RF",
  NIKONZ: "Nikon-Z",
  NIKONF: "Nikon-F",
  SONYA: "Sony-A",
  SONYE: "Sony-E",
} as const;
export type Mount = "Canon-EF" | "Canon-RF" | "Nikon-Z" | "Nikon-F" | "Sony-A" | "Sony-E"

// enum
const Product_productType = {
  lens: "lens",
  flash: "flash",
  body: "body",
} as const;
type Product_productType = "lens" | "flash" | "body"

// enum
const Reservation_state = {
  reserved: "reserved",
  lended: "lended",
  returned: "returned",
  canceled: "canceled",
} as const;
type Reservation_state = "reserved" | "lended" | "returned" | "canceled"

export function _ProductAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends Base {
        productType?: Product_productType | null;
        serialNum?: number;
        brand?: __.Association.to<Brand>;
        brand_ID?: string;
        title?: string | null;
        lensDetails?: __.Composition.of< {
  minFocalLength?: number | null,
  maxFocalLength?: number | null,
  lensType?: lensDetail_lensType | null,
  aperture?: number | null,
  stabilization?: boolean | null,
  apertureBlades?: number | null,
  mount?: Mount | null,
  afMotor?: string | null,
}> | null;
        reservations?: __.Association.to.many<Reservations>;
      static productType = Product_productType
      static readonly actions: Record<never, never>
  };
}
export class Product extends _ProductAspect(__.Entity) {}
Object.defineProperty(Product, 'name', { value: 'cameragearrental.Products' })
Object.defineProperty(Product, 'is_singular', { value: true })
export class Products extends Array<Product> {$count?: number}
Object.defineProperty(Products, 'name', { value: 'cameragearrental.Products' })

export function _BrandAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends _._cuidAspect(Base) {
        name?: string | null;
        products?: __.Association.to.many<Products>;
      static readonly actions: Record<never, never>
  };
}
export class Brand extends _BrandAspect(__.Entity) {}
Object.defineProperty(Brand, 'name', { value: 'cameragearrental.Brands' })
Object.defineProperty(Brand, 'is_singular', { value: true })
export class Brands extends Array<Brand> {$count?: number}
Object.defineProperty(Brands, 'name', { value: 'cameragearrental.Brands' })

export function _ReservationAspect<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends _._cuidAspect(Base) {
        product?: __.Association.to<Product> | null;
        product_serialNum?: number | null;
        product_brand_ID?: string | null;
        startDate?: __.CdsDate | null;
        endDate?: __.CdsDate | null;
        userName?: string | null;
        state?: Reservation_state | null;
      static state = Reservation_state
      static readonly actions: Record<never, never>
  };
}
export class Reservation extends _ReservationAspect(__.Entity) {}
Object.defineProperty(Reservation, 'name', { value: 'cameragearrental.Reservations' })
Object.defineProperty(Reservation, 'is_singular', { value: true })
export class Reservations extends Array<Reservation> {$count?: number}
Object.defineProperty(Reservations, 'name', { value: 'cameragearrental.Reservations' })

When trying to transpile this file, I'm getting a bunch of errors such as:

@cds-models/AdminService/index.ts:37:57 - error TS2503: Cannot find namespace 'AdminService'.
@cds-models/AdminService/index.ts:51:9 - error TS2612: Property 'ID' will overwrite the base property in '_cuidAspect<TBase>.(Anonymous class) & object'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration.
@cds-models/cameragearrental/index.ts:42:14 - error TS2304: Cannot find name 'lensDetail_lensType'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working keepalive Will not be closed by Stale bot
Projects
None yet
Development

No branches or pull requests

4 participants