Skip to content

Commit

Permalink
fix: improve handling of missing design-time type metadata
Browse files Browse the repository at this point in the history
Improve the code to handle the case where design-time type metadata
is not available, e.g. when the project is written in pure JavaScript or
not enabling TypeScript compiler option `emitDecoratorMetadata`.

Signed-off-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
bajtos committed Sep 15, 2020
1 parent 4816cae commit 95b6a2b
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 17 deletions.
68 changes: 68 additions & 0 deletions docs/site/reference/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ sets the error `code` property to a machine-readable string.
| VALIDATION_FAILED | The data provided by the client is not a valid entity. |
| INVALID_PARAMETER_VALUE | The value provided for a parameter of a REST endpoint is not valid. For example, a string value was provided for a numeric parameter. |
| MISSING_REQUIRED_PARAMETER | No value was provided for a required parameter. |
| CANNOT_INFER_PROPERTY_TYPE | See below: [CANNOT_INFER_PROPERTY_TYPE](#cannot_infer_property_type) |

Besides LoopBack-specific error codes, your application can encounter low-level
error codes from Node.js and the underlying operating system. For example, when
Expand All @@ -26,3 +27,70 @@ See the following resources for a list of low-level error codes:

- [Common System Errors](https://nodejs.org/api/errors.html#errors_common_system_errors)
- [Node.js Error Codes](https://nodejs.org/api/errors.html#errors_node_js_error_codes)

## CANNOT_INFER_PROPERTY_TYPE

LoopBack is using TypeScript design-time type metadata to automatically infer
simple property types like `string`, `number` or a model class.

In the following example, the type of the property `name` is inferred from
TypeScript metadata as `string`.

```ts
@model()
class Product {
@property()
name: string;
}
```

Design-time property type is not available in the following cases:

- The property has a type not supported by TypeScript metadata engine:
- `undefined`
- `null`
- complex types like arrays (`string[]`), generic types (`Partial<MyModel>`),
union types (`string | number`), and more.
- The TypeScript project has not enabled the compiler option
`emitDecoratorMetadata`.
- The code is written in vanilla JavaScript.

<a id="cannot_infer_property_type-solutions"></a>

### Solutions

You have the following options how to fix the error
`CANNOT_INFER_PROPERTY_TYPE`:

1. If you are using TypeScript, make sure `emitDecoratorMetadata` is enabled in
your compiler options.

```json
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"emitDecoratorMetadata": true
}
}
```

A typical LoopBack project is inheriting compiler options from
`@loopback/build`, where `emitDecoratorMetadata` is already enabled.

```json
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "@loopback/build/config/tsconfig.common.json"
}
```

2. If your property uses a complex type, then specify the type in the `type`
field of `@property()` definition.

```ts
@model()
class UserProfile {
@property({type: 'string'})
hourFormat: '12h' | '24h';
}
```
2 changes: 1 addition & 1 deletion packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ export function inspectTargetType(injection: Readonly<Injection>) {
injection.target,
injection.member!,
);
return designType.parameterTypes[
return designType?.parameterTypes?.[
injection.methodDescriptorOrParameterIndex as number
];
}
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ContextTags,
ContextView,
createBindingFromClass,
DecoratorFactory,
inject,
InjectionMetadata,
isDynamicValueProviderClass,
Expand Down Expand Up @@ -81,14 +82,27 @@ export function service(
serviceType = MetadataInspector.getDesignTypeForMethod(
injection.target,
injection.member!,
).parameterTypes[injection.methodDescriptorOrParameterIndex];
)?.parameterTypes[injection.methodDescriptorOrParameterIndex];
} else {
serviceType = MetadataInspector.getDesignTypeForProperty(
injection.target,
injection.member!,
);
}
}
if (serviceType === undefined) {
const targetName = DecoratorFactory.getTargetName(
injection.target,
injection.member,
injection.methodDescriptorOrParameterIndex,
);
const msg =
`No design-time type metadata found while inspecting ${targetName}. ` +
'You can either use `@service(ServiceClass)` or ensure `emitDecoratorMetadata` is enabled in your TypeScript configuration. ' +
'Run `tsc --showConfig` to print the final TypeScript configuration of your project.';
throw new Error(msg);
}

if (serviceType === Object || serviceType === Array) {
throw new Error(
'Service class cannot be inferred from design type. Use @service(ServiceClass).',
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-v3/src/controller-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
constructor.prototype,
op,
);
const paramTypes = opMetadata.parameterTypes;
const paramTypes = opMetadata?.parameterTypes ?? [];

const isComplexType = (ctor: Function) =>
!includes([String, Number, Boolean, Array, Object], ctor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {MetadataInspector} from '@loopback/core';
import {MetadataInspector, Reflector} from '@loopback/core';
import {
belongsTo,
Entity,
hasMany,
model,
ModelDefinitionSyntax,
MODEL_KEY,
property,
PropertyType,
} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import {
Expand All @@ -23,20 +26,44 @@ import {expectValidJsonSchema} from '../helpers/expect-valid-json-schema';
describe('build-schema', () => {
describe('modelToJsonSchema', () => {
context('properties conversion', () => {
it('reports error for null or undefined property', () => {
@model()
class TestModel {
@property()
nul: null;
@property()
undef: undefined;
}
it('reports error for property without type (`null`)', () => {
// We cannot use `@model()` and `@property()` decorators because
// they no longer allow missing property type. Fortunately,
// it's possible to reproduce the problematic edge case by
// creating the model definition object directly.
class TestModel {}
const definition: ModelDefinitionSyntax = {
name: 'TestModel',
properties: {
nul: {type: (null as unknown) as PropertyType},
},
};
Reflector.defineMetadata(MODEL_KEY.key, definition, TestModel);

expect(() => modelToJsonSchema(TestModel)).to.throw(
/Property TestModel.nul does not have "type" in its definition/,
);
});

it('reports error for property without type (`undefined`)', () => {
// We cannot use `@model()` and `@property()` decorators because
// they no longer allow missing property type. Fortunately,
// it's possible to reproduce the problematic edge case by
// creating the model definition object directly.
class TestModel {}
const definition: ModelDefinitionSyntax = {
name: 'TestModel',
properties: {
undef: {type: (undefined as unknown) as PropertyType},
},
};
Reflector.defineMetadata(MODEL_KEY.key, definition, TestModel);

expect(() => modelToJsonSchema(TestModel)).to.throw(
/Property TestModel.undef does not have "type" in its definition/,
);
});

it('allows property of null type', () => {
@model()
class TestModel {
Expand Down
9 changes: 9 additions & 0 deletions packages/repository/src/decorators/model.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ export function buildModelDefinition(
const propertyDef = propertyMap[p];
const designType = MetadataInspector.getDesignTypeForProperty(prototype, p);
if (!propertyDef.type) {
if (!designType) {
const err: Error & {code?: string} = new Error(
`The definition of model property ${modelDef.name}.${p} is missing ` +
'`type` field and TypeScript did not provide any design-time type. ' +
'Learn more at https://loopback.io/doc/en/lb4/Error-codes.html#cannot_infer_property_type',
);
err.code = 'CANNOT_INFER_PROPERTY_TYPE';
throw err;
}
propertyDef.type = designType;
}
modelDef.addProperty(p, propertyDef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {MetadataInspector} from '@loopback/core';
import {DecoratorFactory, MetadataInspector} from '@loopback/core';
import {property} from '../../decorators/model.decorator';
import {Entity, EntityResolver, PropertyDefinition} from '../../model';
import {relation} from '../relation.decorator';
Expand All @@ -23,16 +23,32 @@ export function belongsTo<T extends Entity>(
propertyDefinition?: Partial<PropertyDefinition>,
) {
return function (decoratedTarget: Entity, decoratedKey: string) {
const propType =
MetadataInspector.getDesignTypeForProperty(
decoratedTarget,
decoratedKey,
) ?? propertyDefinition?.type;

if (!propType) {
const fullPropName = DecoratorFactory.getTargetName(
decoratedTarget,
decoratedKey,
);
throw new Error(
`Cannot infer type of model property ${fullPropName} because ` +
'TypeScript compiler option `emitDecoratorMetadata` is not set. ' +
'Please enable `emitDecoratorMetadata` or use the third argument of ' +
'`@belongsTo` decorator to specify the property type explicitly.',
);
}

const propMeta: PropertyDefinition = Object.assign(
{},
// properties provided by the caller
propertyDefinition,
// properties enforced by the decorator
{
type: MetadataInspector.getDesignTypeForProperty(
decoratedTarget,
decoratedKey,
),
type: propType,
// TODO(bajtos) Make the foreign key required once our REST API layer
// allows controller methods to exclude required properties
// required: true,
Expand Down

0 comments on commit 95b6a2b

Please sign in to comment.