-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(json-schema): add modules to convert TS models to JSON Schema #860
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
Conversation
|
@shimks Have you considered using our existing code in loopback-swagger (see lib/specgen/model-helper.js instead of writing the conversion from scratch? The are several edge cases that require special treatment, this has been all solved in loopback-swagger already. On the other hand, your current approach may be better in the longer term, especially if/when we start making changes in our new model/property definition format that will diverge from LDL used by the current juggler. |
| $ref: '#definitions/Bar', | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am concerned that when this test fails, the error message will be difficult to understand, because the expected and actual objects are so large!
Please split this test into several smaller tests, where each test is checking one of the types.
it('properly converts "string" properties', () => {
@model()
class TestModel {
@property() str: string;
}
const jsonDef = modelToJSONDef(TestModel);
expect(jsonDef.properties).to.deepEqual({
str: {
type: 'string',
},
});
});It is also important to make these tests resilient to future changes. For example, when modelToJSONDef starts emitting more top-level fields beyond properties, then we don't want to have to update all tests in this file with the same default values for these new properties. My example code above is already addressing that problem by verifying only the contents of jsonDef.properties. There should be a dedicated test to verify the overall shape of jsonDef.
See also "Isolation of failures" and "Resilience" in http://loopback.io/doc/en/lb4/Defining-your-testing-strategy.html:
- Isolation of failures: The test suite should make it easy to isolate the source of test failures. To fix a failing test, developers need to find the specific place that does not work as expected. When the project contains thousands of lines and the test failure can be caused by any part of the system, then finding the bug is very difficult, time consuming and demotivating.
- Resilience: The test implementation should be robust and resilient to changes in the tested code. As the project grows and matures, you may need to change existing behavior. With a brittle test suite, each change may break dozens of tests, for example when you have many end-to-end/UI tests that rely on specific UI layout. This makes change prohibitively expensive, up to a point where you may start questioning the value of such test suite.
References:
| type: 'string', | ||
| }, | ||
| }); | ||
| expect(toJSONProperty(propMeta['arrBar'])).to.deepEqual({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One (logical) assert per test please. There should be two tests here - one for string arrays, another for Bar arrays.
| version: '1.0.0', | ||
| }, | ||
| paths: {}, | ||
| definitions: {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The property definitions is optional according to Swagger Spec. Please leave it out from the empty API spec. (It will save you updates of so many tests below.)
| }, | ||
| }, | ||
| }, | ||
| definitions: {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert these changes - see https://github.com/strongloop/loopback-next/pull/860/files#r161257117
Alternatively, rework the test suite in this file to be more robust:
- modify the tests where you had to add an empty
definitionsproperty in such way that they don't depend on exact shape of the spec object (e.g. assert only the value ofpathsproperty) - ensure there is a single test verifying the top-level properties of the object returned by
getControllerSpec
| }, | ||
| }, | ||
| }, | ||
| MySoul: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this part is redundant - we have already verified elsewhere that "simple" models are correctly converted to JSON schema. Please simplify this test so that it asserts only the important/relevant facts.
For example:
expect(Object.keys(spec.definitions)).to.deepEqual(['MyBody', 'MySoul']);
expect(spec.definitions.MyBody).to.deepEqual({
// ...
});| const spec = getControllerSpec(MyController); | ||
| expect(spec).to.containEql(expectedDefs); | ||
| }); | ||
| it('does not infer definition if no class metadata is present', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an empty line before it to serve as a visual delimiter.
packages/rest/src/rest-server.ts
Outdated
| // accidentally modifying our internal routing data | ||
| spec.paths = cloneDeep(this.httpHandler.describeApiPaths()); | ||
|
|
||
| spec.definitions = this.httpHandler.getApiDefinitions(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we apply cloneDeep here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that would make sense when I wrote the code there, but it seems like Nevermind, I'll apply cloneDeep is currently typed for just PathsObject. Maybe I'll add in a similar function for DefinitionsObject.cloneDeep there
|
@bajtos I should've known that the work was done for me haha. Personally, I'd be ok with using loopback-swagger, but you bring up a good point about how this approach might be better long term. I'll leave the discussion to anyone that wants to make a strong argument for either of the cases. |
packages/json-schema/README.md
Outdated
| @@ -0,0 +1,89 @@ | |||
| # @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the module name
packages/json-schema/package.json
Outdated
| { | ||
| "name": "@loopback/json-schema", | ||
| "version": "4.0.0-alpha.1", | ||
| "description": "TBD", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD?
| } | ||
|
|
||
| // tslint:disable-next-line:no-any | ||
| export function toJSONProperty(propMeta: any): JSONDefinition { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The arg type should be PropertyDefinition or something similar.
kjdelisle
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like a lot of what I see, but there are some strange bits:
- The README is for json-schema, not OpenAPI and should be rewritten accordingly
- Commented code needs to go
- Missing TSDocs on the new functions, classes and types (these are a must)
- Keywords in the README should not include references to OpenAPI/Swagger
Ultimately, this package gives us JSON schema that we can leverage across a variety of other protocols and approaches; it shouldn't be tied to OpenAPI or Swagger in any direct sense. Making use of those functions to simplify our OAI/Swagger story is one thing, but no one should need to deal with them to leverage your new module for other purposes.
packages/json-schema/README.md
Outdated
| @@ -0,0 +1,89 @@ | |||
| # @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The json-schema package should be about generating JSON schema from models, and not have anything to do with the OpenAPI spec builder. Did you copy-paste this README as a placeholder?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, the README is not ready to be reviewed at all as in I haven't touched it after I've copy-pasted.
| "version": "4.0.0-alpha.1", | ||
| "description": "TBD", | ||
| "engines": { | ||
| "node": ">=6" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Node 8 or greater, please. "node": ">=8"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, if this new module supports only Node.js 8+, then we cannot start depending on it anywhere in the existing code base until we drop support for Node.js 6.x via #611.
packages/json-schema/package.json
Outdated
| "license": "MIT", | ||
| "keywords": [ | ||
| "Swagger", | ||
| "OpenAPI Spec", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither Swagger nor OpenAPI Spec should be keywords in this package.
| // no metadata for: union, optional, nested array, any, enum, string literal, | ||
| // anonymous types, inherited properties | ||
| export function modelToJSONDef(ctor: Function): JSONDefinition { | ||
| // if (!MetadataInspector.getClassMetadata(MODEL_KEY, ctor)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove commented code
| // } | ||
| // tslint:disable-next-line:no-any | ||
| const meta = ModelMetadataHelper.getModelMetadata(ctor) as any; | ||
| // if (Object.keys(meta.properties).length === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove commented code
kjdelisle
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm much happier with this version. Should still wait for @bajtos and @raymondfeng
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @kjdelisle
Do we need a new package for this functionality? I think we already agree to put all the spec generator functions in package openapi-v2 right?
Sorry never mind I thought this PR converts it to openapi spec.
virkt25
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some nitpicks
packages/json-schema/LICENSE
Outdated
| @@ -0,0 +1,25 @@ | |||
| Copyright (c) IBM Corp. 2018. All Rights Reserved. | |||
| Node module: @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect module name.
packages/json-schema/index.d.ts
Outdated
| @@ -0,0 +1,6 @@ | |||
| // Copyright IBM Corp. 2018. All Rights Reserved. | |||
| // Node module: @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update module name
packages/json-schema/index.js
Outdated
| @@ -0,0 +1,9 @@ | |||
| // Copyright IBM Corp. 2018. All Rights Reserved. | |||
| // Node module: @loopback/testlab | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update module name
packages/json-schema/index.ts
Outdated
| @@ -0,0 +1,7 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
| // Node module: @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update module name
| @@ -0,0 +1,74 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
| // Node module: @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update module name
packages/json-schema/src/index.ts
Outdated
| @@ -0,0 +1,6 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
| // Node module: @loopback/openapi-spec | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update module name
| @@ -0,0 +1,215 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
| // Node module: @loopback/openapi-spec-builder | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update module name
| import {expect} from '@loopback/testlab'; | ||
|
|
||
| describe('build-schema', () => { | ||
| context('exception', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context is an alias for describe. AFAIK we just use nested describe statements. @bajtos can comment on the pattern we should be using.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the usage of contex is correct here, see http://loopback.io/doc/en/contrib/style-guide.html#test-block-naming
In this particular case, I feel this particular nesting block is redundant and the test it('errors out when "@property.array" is not used on an array' should be moved into describe('modelToJsonDef') block below, because modelToJsonDef is the function executed by this test.
| class BadArray { | ||
| @property() badArr: string[]; | ||
| } | ||
| expect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of the anonymous function? Is there a reason this can't be expect(modelToJsonDef(BadArray)).to.throw(/type is defined as an array/);?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently I need to enclose whatever that I expect to throw an error in an anonymous function. Does ShouldJS have an alternative where I we wouldn't need to use the anonymous function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used expect(function()).to.be.rejectedWith() ... where rejectedWith takes the same arguments as throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't work :(, the error just gets thrown at my face instead of being caught
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok. I’m fine with what you had. I was just curious. I think rejectedwith is for promises 😅
| @property() nul: null; | ||
| @property() undef: undefined; | ||
| } | ||
| expect(modelToJsonDef(TestModel).properties).to.not.containEql({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be better to store the results of the function call and then do the checks.
const jsonDef = modelToJsonDef(TestModel);
expect(jsonDef.properties).to.no.containEql({nul: {type: 'null'}});
expect(modelToJsonDef(TestModel).properties).to.not.containEql({undef: {type: 'undefined'}});
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new version looks much better! I have few more comments to consider and discuss.
| * Currently, objects of type JSONDefinition can also be cast as a SchemaObject, | ||
| * a property of OpenAPI-v2's specification | ||
| */ | ||
| export type JsonDefinition = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind maintaining our own copy of TypeScript interface/type definition for JSON Schema, but it still makes me wonder - isn't there any npm package we could leverage for that?
|
|
||
| ## Overview | ||
|
|
||
| This package provides modules to easily convert LoopBack4 models that have been decorated with `@model` and `@property` to a matching JSON Schema Definition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since @model and @property decorators are coming from @loopback/repository package and are technically optional, would it make sense to rename @loopback/json-schema to something like @loopback/repository-json-schema or @loopback/model-json-schema?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense considering the package is somewhat of an extension to the functionalities that @model and @property are going to provide. I changed the name to @loopback/repository-json-schema.
| import {expect} from '@loopback/testlab'; | ||
|
|
||
| describe('build-schema', () => { | ||
| context('exception', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the usage of contex is correct here, see http://loopback.io/doc/en/contrib/style-guide.html#test-block-naming
In this particular case, I feel this particular nesting block is redundant and the test it('errors out when "@property.array" is not used on an array' should be moved into describe('modelToJsonDef') block below, because modelToJsonDef is the function executed by this test.
| } | ||
| const jsonDef = modelToJsonDef(TestModel); | ||
| expect(jsonDef.properties).to.not.containEql({ | ||
| nul: {type: 'null'}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this check weird and possible brittle. In theory, there is an infinite number of ways how to convert a null property into JSON schema, almost all of them invalid, and here we are ruling out only one of the invalid options.
For example, the following output would pass this test:
{
properties: {
nul: { anyOf: [{type: 'null'}] }
}
}I think you wanted to assert that jsonDef.properties does not contain any entry for the nul property, right?
expect(jsonDef.properties).to.not.have.key('nul');The same comment applies to next check for undef below.
| }).to.throw(/type is defined as an array/); | ||
| }); | ||
| }); | ||
| describe('modelToJSON', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add an empty lines between describe blocks and also between it blocks. They server as a visual delimiter and make it easier to spot where on test (case/suite) ends and the next one starts.
| propMeta = meta.properties; | ||
| }); | ||
| it('converts primitively typed property correctly', () => { | ||
| expect(toJsonProperty(propMeta['str'])).to.deepEqual({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
propMeta.str
The same comment applies to most (if not all) similar tests below.
|
|
||
| for (const p of paramTypes) { | ||
| if ( | ||
| !includes([String, Number, Boolean, Array, Object], p) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please extract the multi-line condition into a named variable, see http://loopback.io/doc/en/contrib/style-guide.html#indentation-of-multi-line-expressions-in-if
| basePath: '/api', | ||
| paths: {}, | ||
| 'x-foo': 'bar', | ||
| definitions: {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I am too stubborn about this, but cannot we remove definitions property from the api spec reported by the server when there were not definitions specified?
See my two comments above for an inspiration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, you're not being stubborn. I just forgot to take those out :)
packages/rest/src/http-handler.ts
Outdated
|
|
||
| export class HttpHandler { | ||
| protected _routes: RoutingTable = new RoutingTable(); | ||
| protected _apiDefinitions: DefinitionsObject = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am proposing to change the default value to undefined here.
packages/rest/src/http-handler.ts
Outdated
| } | ||
|
|
||
| registerApiDefinitions(defs: DefinitionsObject) { | ||
| Object.assign(this._apiDefinitions, defs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this._apiDefinitions = Object.assign({}, this._apiDefinitions, defs);
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's a good idea to leave support for Node.js 6.x in the new package as part of this pull request. It affects more than just engines field in package.json, see my comments below.
Also if this new module supports only Node.js 8+, then I think we cannot start depending on it anywhere in the existing code base until we drop support for Node.js 6.x there too (via #611), otherwise CI builds will start failing. (They are not failing yet because we are still transpiling for Node.js 6.x now.)
| "build": "npm run build:dist && npm run build:dist6", | ||
| "build:current": "lb-tsc", | ||
| "build:dist": "lb-tsc es2017", | ||
| "build:dist6": "lb-tsc es2015", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are supporting Node.js >=8, then there is no need for build:dist6.
packages/json-schema/package.json
Outdated
| "clean": "lb-clean loopback-json-schema*.tgz dist dist6 package api-docs", | ||
| "prepare": "npm run build && npm run build:apidocs", | ||
| "pretest": "npm run build:current", | ||
| "test": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js' 'DIST/test/integration/**/*.js' 'DIST/test/acceptance/**/*.js'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are supporting Node.js >=8, then there is no need for lb-dist, because there is only a single dist folder.
packages/json-schema/index.js
Outdated
| const nodeMajorVersion = +process.versions.node.split('.')[0]; | ||
| module.exports = nodeMajorVersion >= 7 ? | ||
| require('./dist/src') : | ||
| require('./dist6/src'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are supporting Node.js >=8, then there should be no dist6 and we should always forward to ./dist/src.
b-admike
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM once comments have been addressed.
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there!
| "codeSectionDepth": 4, | ||
| "assets": { | ||
| "/": "/docs", | ||
| "/docs": "/docs" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't have any API docs asset yet, and /docs does not even exist, I am proposing to remove the "assets" property completely from this file.
| @@ -0,0 +1,8 @@ | |||
| { | |||
| "content": ["index.ts", "src/index.ts", "src/build-schema.ts"], | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually put each content file on its own line, to make future patches smaller.
{
"content": [
"index.ts",
"etc."
],
"etc."
}| "clean": "lb-clean loopback-repository-json-schema*.tgz dist package api-docs", | ||
| "prepare": "npm run build && npm run build:apidocs", | ||
| "pretest": "npm run build:current", | ||
| "test": "mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js' 'DIST/test/integration/**/*.js' 'DIST/test/acceptance/**/*.js'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❗️ DIST directory does not exist, this command will become a no-op on case-sensitive file systems (typically on Linux). (Both MacOS and Windows are case-insensitive AFAIK).
Please use dist instead of DIST.
| @@ -0,0 +1,7 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, the copyright years should be just 2018 in new files.
| @@ -0,0 +1,71 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, the copyright year is just 2018.
| @@ -0,0 +1,6 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, the copyright year is just 2018.
| @@ -0,0 +1,232 @@ | |||
| // Copyright IBM Corp. 2013,2018. All Rights Reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, the copyright year is just 2018.
|
@shimks I don't think it was a good idea to drop support for Node.js 6.x, as I explained in one of my comments. As you can see for yourself, CI builds of your pull request are failing on Node.js 6.x platform now. IMO, removing support for Node.js 6.x from existing packages is out of scope of this pull request, such changes should be made it a new dedicated pull request. |
@shimks please check the coverage report, find the new code that's not covered by our tests and verify that we are not missing important test cases. |
I don't understand why is it needed to generate json-schemas at the time when the decorators are executed. Could you please explain the reasoning behind this decision? IMO, it's ok for consumers of json-schema to call a method from your new json-schema package whenever they need to obtain json-schema description of a model class. If we want to cache those descriptions, then those getter methods are free to add a caching layer as their implementation detail. What am I missing? |
|
I thought it'd be cleaner to generate the json-schemas before any of a LoopBack app run so that we don't have to keep a track of when the schemas are initially generated. But, json-schemas aren't that costly to generate every time they're needed, so maybe it's fine to leave it at previous implementation + caching. @kjdelisle Any thoughts? |
|
I think the reason to generate the schema at decoration time is to ensure that a variety of metadata consumers can rely on the existence of those definitions during application configuration and startup. It's beginning to seem like the concept of a model should be its own package, perhaps under |
|
^^^ This was one thing that I thought of while thinking about ways to resolve the dependency cycle. Just looking at things from a top-level, I think it makes sense to decouple these |
Playing devil's advocate here: what if ConsumerA (a 3rd-party extension) wants to get JSON Schema version draft-07 , but ConsumerB (another 3rd-party extension) wants to get JSON Schema version 1.0 (a hypothetical future), and these two versions have important incompatible differences? Wouldn't it be better if each metadata consumer was explicit about what kind of schema they are expecting? Under the hood, the functions building metadata can cache (memoize) the values if needed, but that should be an implementation detail. I am also worried that we are starting the path towards adding too much responsibility to our Model classes. Consider Single Responsibility Principle:
In your proposal, the model has to change when either of the LDL definition or the JSON Schema version changes.
This is a good point! A similar idea has been already proposed long time ago when we were discussing juggler cleanup/refactoring, looks like it's a very valid one! I have one concern though:
If we want to truly decouple models from repositories, then I think we may need to decouple model definition syntax used by @model
Product {}
// sets Product.definition.version to '1.0'
const repo = new DefaultCrudRepository<Product>(Product, db)
// asserts that Product.definition.version is compatible with juggler.ldlVersion
// alternatively upgrade the spec from 1.0 to the version required by juggler
// before passing it to PeristedModelEven with this design, and after moving model decorators to its own package, I still think it's better to keep JSON-schema generators decoupled from our models. Maybe I am missing an important point here? I admit I am biased by the successful experience we had with loopback-swagger, which provides methods converting LDL to Swagger in a decoupled way. In order to keep this pull request moving forward, I am proposing to leave the integration with Models out of scope of this pull request. Let's start with implementing a Thoughts? |
|
My main concern was the memoization of JSON schemas but it's true that the caching can be done as a part of schema generation. I'll reverse the last commit to keep the PR moving forward like you've said and add in a bit for memoization during JSON schema generations. Do you think we should create an issue for the discussion on decoupling of model decorators ( |
b251b9c to
39d276a
Compare
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am short of time but don't want to block your progress. I quickly scanned the changes and didn't found any major/blocking issues.
Feel free to dismiss my review after you address my comments below and then get somebody else from the team to review and approve this patch.
| } | ||
| case 'allOf': { | ||
| const collector: SchemaObject[] = []; | ||
| for (const item of def.allOf) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
emptySchemaObj.allOf = def.allOf.map(item => jsonToSchemaObject(item));I think emptySchemaObj is not a very good name because emptySchemaObj is no longer empty after this line. Perhaps target or result would be better one?
| case 'properties': { | ||
| const properties: {[key: string]: JsonDefinition} = def.properties; | ||
| const collector: MapObject<SchemaObject> = {}; | ||
| for (const item in properties) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, please use Array.prototype.map or lodash's variant for mapping object properties.
| } | ||
| } | ||
|
|
||
| return <SchemaObject>emptySchemaObj; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this explicit cast necessary? Can we remove it?
return emptySchemaObj;| property: Object, | ||
| expected: Object, | ||
| ) { | ||
| it(name, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please move it(name part out of this helper function? It makes it easier to find and debug individual test cases, e.g. via it.skip.
it('additionalProperties', testPropertyConversion(additionalDef, expectedAdditional));Also the test name "additionalProperties" does not read well ("it additionalProperties" is not an English sentence). Please fix.
| @@ -0,0 +1,3 @@ | |||
| *.tgz | |||
| dist* | |||
| package | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we are using a shared top-level .gitignore file, please remove this package-specific one.
| @property() name: string; | ||
| } | ||
|
|
||
| const jsonDef = getJsonDef(MyModel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are producing JSON Schema, I am proposing to rename getJsonDef to getJsonSchema, and jsonDef to jsonSchema or simply schema.
| "clean": "lb-clean loopback-json-schema*.tgz dist dist6 package api-docs", | ||
| "prepare": "npm run build && npm run build:apidocs", | ||
| "pretest": "npm run build:current", | ||
| "test": "lb-dist mocha --opts ../../test/mocha.opts 'dist/test/unit/**/*.js' 'dist/test/integration/**/*.js' 'dist/test/acceptance/**/*.js'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mocha.opts was moved to a different location (see #876), please rebase your PR and update this line accordingly.
| "prepare": "npm run build && npm run build:apidocs", | ||
| "pretest": "npm run build:current", | ||
| "test": "lb-dist mocha --opts ../../test/mocha.opts 'dist/test/unit/**/*.js' 'dist/test/integration/**/*.js' 'dist/test/acceptance/**/*.js'", | ||
| "verify": "npm pack && tar xf loopback-jsons-schema*.tgz && tree package && npm run clean" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: -jsons-
| properties?: {[property: string]: JsonDefinition}; | ||
| } | ||
|
|
||
| export function getJsonDef(ctor: Function): JsonDefinition { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am proposing getJsonSchema.
| * reflection API | ||
| * @param ctor Constructor of class to convert from | ||
| */ | ||
| export function modelToJsonDef(ctor: Function): JsonDefinition { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here, modelToJsonSchema?
9bc8666 to
23731d8
Compare
kjdelisle
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow. 💯
Top-shelf PR, love it.
One teensy, tiny nit and LGTM
| it('errors out when explicit type decoration is not primitive', () => { | ||
| @model() | ||
| class TestModel { | ||
| @property({type: 'not primitive'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remove the space in this type string? Technically, it wouldn't be a valid class/variable reference as a result, so it doesn't quite prove what's being asserted in the it statement.
type: 'NotPrimitive'
I reviewed, as per previous comment.
19b356d to
55c5ed8
Compare
|
@slnode test please |
@modeland@propertyin@loopback/repositoryto create valid json-schema from the metadata and then store in them global metadata space@paramto access the metadataChecklist
npm testpasses on your machinepackages/cliwere updatedpackages/example-*were updated