loopbackio / loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.
https://loopback.io
Other
4.93k stars 1.06k forks source link

Support embedded models for nested properties #2130

Closed metamono closed 5 years ago

metamono commented 5 years ago

Description / Steps to reproduce / Feature proposal

This issue was discussed at stackoverflow.

An error occurs on create if nested entities are defined.

Current Behavior

on create of an entity with a nested child (or more), an error occurs:

Unhandled error in POST /myMainEntity: 500 TypeError: Class constructor MySubEntity cannot be invoked without 'new'

if the nested entity was included like this:

@property({
    type: MySubEntity, // maybe in aphostrope
  })
  subEntity?: MySubEntity;

Expected Behavior

The objective of this was to get nested entities, which

Even more helpful would be to use jsonSchema for the datamodel and get the described features like autocomplete, typesafety if this is possible anyhow.

bajtos commented 5 years ago

Cross-posting my recent answer from StackOverflow (https://stackoverflow.com/a/53655494/69868).

This looks like a limitation of the current implementation in loopback-datasource-juggler and/or the way how LoopBack 4 translates LB4 model definitions to Juggler models.

The error is triggered by this code:

// Assume the type constructor handles Constructor() call
// If not, we should call new DataType(value).valueOf();
this.__data[propertyName] = (value instanceof DataType) ? value : DataType(value);

Juggler assumes that types (models) are defined as ES5 functions which can be called either with or without new. However, LB4 models are implemented as ES6 classes, which must be always constructed using new keyword.

I think there are two options, they are not mutually exclusive:

bajtos commented 5 years ago

Possibly related: https://stackoverflow.com/q/52939388/69868

Cross-posting the relevant bits here:

@property.array(PhoneNumber)
phoneNumbers: PhoneNumber[];

In the above example, I get full schema validation, but if I try to save an instance of Contact using a generated Repository that extends DefaultCrudRespository, it just drops whatever was provided in the phoneNumbers field and saves an empty column in the db.

If I change the property annotation to:

@property.array(Object)
phoneNumbers: PhoneNumber[];

It will save the field properly, serialized as json, but it won't attempt to validate the field, and also won't specify the type as an array PhoneNumber in the generated openapi.json spec

tamer-mohamed commented 5 years ago

any update on this? I want to have similar setup but it fails with a different error.

my setup is like the following

// .../models/user.model.ts

@model()
class Address {
  @property()
  country: string;

 @property()
 city: string;
}

@model()
export class User extends Entity{ 
  // ...
  @property()
   address: Address
   // ...
}

I get the following error when I execute any find/findOne/findById methods:

TypeError: Cannot read property 'properties' of undefined
    at MongoDB.fromDatabase (.../loopback-connector-mongodb/lib/mongodb.js:371:25)
    at MongoDB.fromDatabase (.../loopback-connector-mongodb/lib/mongodb.js:396:22)
akash-gupt commented 5 years ago

any update on this? I want to have similar setup but it fails with a different error.

my setup is like the following

// .../models/user.model.ts

@model()
class Address {
  @property()
  country: string;

 @property()
 city: string;
}

@model()
export class User extends Entity{ 
  // ...
  @property()
   address: Address
   // ...
}

I get the following error when I execute any find/findOne/findById methods:

TypeError: Cannot read property 'properties' of undefined
    at MongoDB.fromDatabase (.../loopback-connector-mongodb/lib/mongodb.js:371:25)
    at MongoDB.fromDatabase (.../loopback-connector-mongodb/lib/mongodb.js:396:22)

have you find any update for this issue

akash-gupt commented 5 years ago

@bajtos @aetheric8 @tamer-mohamed @fabien @rmg need help

metamono commented 5 years ago

hello @techaks, we are also waiting on a solution. Temporarily we use the loopback-3 approach

bajtos commented 5 years ago

Now that juggler has been improved to support ES6 classes in nested properties (see https://github.com/strongloop/loopback-datasource-juggler/pull/1670), let's find out what's needed to make that feature work in LB4 too.

The following code snippet from an earlier comment looks reasonable to me - I don't see any obvious error there:

@model()
class Address {
  @property()
  country: string;

 @property()
 city: string;
}

@model()
export class User extends Entity{ 
  // ...
  @property()
   address: Address
   // ...
}

Based on the comments above, it looks like this setup does not work with the MongoDB connector. How about other connectors like memory or mysql? Can the issue be reproduced there too?

In general, it would be very helpful if somebody could create a small application reproducing the problem - see our bug reporting instructions.

bajtos commented 5 years ago

Actually we already have a pull request opened to fix the problem - see https://github.com/strongloop/loopback-next/pull/2505

HMarzban commented 4 years ago

HI @bajtos i ran test of embedded models for nested properties but face to an error!

Post Model:

// ...
@model()
class Review extends Entity {
  @property()
  rate: Number;

  @property()
  gender: String;
}
// ...
@model()
export class Post extends Entity {
  @property({
    type: 'string',
    id: true,
    required: true,
    generated: true,
  })
  id: string;

  @property({
    type: 'string',
  })
  title?: string;

  @property()
  review: Review;

  constructor(data?: Partial<Post>) {
    super(data);
  }
}
// ...

as #2505 pull request this model must be okay and work, but when i hit npm start i getting the below error:

$ npm start

> test@1.0.0 prestart E:\Projects\_lb4\test
> npm run build

> test@1.0.0 build E:\Projects\_lb4\test
> lb-tsc

> test@1.0.0 start E:\Projects\_lb4\test
> node -r source-map-support/register .

Cannot start the application. RangeError: Maximum call stack size exceeded       
    at baseIteratee (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:3458:26)
    at getIteratee (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:5926:33)
    at Function.mapValues (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:13398:18)
    at jsonToSchemaObject (E:\Projects\_lb4\test\node_modules\@loopback\openapi-v3\src\json-to-schema.ts:72:31)
    at result.definitions._.mapValues.def (E:\Projects\_lb4\test\node_modules\@loopback\openapi-v3\src\json-to-schema.ts:67:11)
    at E:\Projects\_lb4\test\node_modules\lodash\lodash.js:13401:38
    at E:\Projects\_lb4\test\node_modules\lodash\lodash.js:4905:15
    at baseForOwn (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:2990:24)
    at Function.mapValues (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:13400:7)
    at jsonToSchemaObject (E:\Projects\_lb4\test\node_modules\@loopback\openapi-v3\src\json-to-schema.ts:66:32)
    at result.definitions._.mapValues.def (E:\Projects\_lb4\test\node_modules\@loopback\openapi-v3\src\json-to-schema.ts:67:11)
    at E:\Projects\_lb4\test\node_modules\lodash\lodash.js:13401:38
    at E:\Projects\_lb4\test\node_modules\lodash\lodash.js:4905:15
    at baseForOwn (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:2990:24)
    at Function.mapValues (E:\Projects\_lb4\test\node_modules\lodash\lodash.js:13400:7)
    at jsonToSchemaObject (E:\Projects\_lb4\test\node_modules\@loopback\openapi-v3\src\json-to-schema.ts:66:32)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! test@1.0.0 start: `node -r source-map-support/register .`
npm ERR! Exit status 1

It's better to mention that I tested with mongodb and memory connectors and both of them had the same error result. also, I tried to find out an error flow to fixed this issue but I didn't find any relevant flow to fix it.

vanildov commented 4 years ago

Hi,

I'm experiencing the same error described above by @HMarzban . Any news on that?

raymondfeng commented 4 years ago

Maybe it's related to https://github.com/strongloop/loopback-next/pull/3897

HMarzban commented 4 years ago

HI @raymondfeng, I fount out that this problem comes from openapi package!

// controller
@post('/posts')
  async create(
    @requestBody({
      content: {
        'application/json':
        {
          schema: getModelSchemaRef(Post, { title: "PostSchema" })
        }
      }
    })
    post: Omit<Post, 'id'>,
  ): Promise<Post> {
    return this.postRepository.create(post);
  }

all these error comes from this line schema: getModelSchemaRef(Post, { title: "PostSchema" }) with some digging found out that the title makes a problem when we use embedded models for nested properties. e.g.

// model
@model()
class Category{
  @property({
    type: 'string',
  })
  name?: string;

  @property({
    type: 'string',
  })
  desc?: string;

}

@model()
export class Post extends Entity {
  @property({ type: 'number', id: true, required: true, generated: true, })
  id: number;

  @property({ type: 'string', })
  title?: string;

  @property({ type: 'string', })
  content?: string;

  @property()
  category: Category;

  constructor(data?: Partial<Post>) {
    super(data);
  }
}
// controller
export class PostController {
  constructor(
    @repository(PostRepository)
    public postRepository: PostRepository,
  ) { }

  @post('/posts')
  async create(
    @requestBody({
      content: {
        'application/json':
        {
          schema: getModelSchemaRef(Post, { title: "Postschema" })
        }
      }
    })
    post: Omit<Post, 'id'>,
  ): Promise<Post> {
    return this.postRepository.create(post);
  }
}

regularly when I don't use the embedded model the getJsonSchemaRef() which located in @loopback\repository-json-schema\src\build-schema.ts retrieve some things like this:

{
  title: 'Postschema',
    description: '(Schema options: { title: \'Postschema\' })',
      properties:
  {
    id: { type: 'number' },
    title: { type: 'string' },
    content: { type: 'string' },
    catagory: { type: 'string' }
  },
  required: ['id']
}

but wen embedded model comes to the road the function retrieve some things like this:

{
  '$ref': '#/definitions/Postschema',
    definitions:
  {
    Catagory:
    {
      title: 'Postschema',
        description: '(Schema options: { title: \'Postschema\' })',
          properties: [Object],
            required: [Array],
              definitions: [Object]
    },
    Postschema:
    {
      title: 'Postschema',
        description: '(Schema options: { title: \'Postschema\' })',
          properties: [Object],
            required: [Array]
    }
  }
}

this new schema makes all problem and create infinity loop, in @loopback\openapi-v3\src\json-to-schema.ts file and jsonToSchemaObject() function.

I hope this digging make some help 😊

bajtos commented 4 years ago

@HMarzban is the issue fixed now that #3897 has been landed? If not then please open a new GitHub issue.