Skip to content

Commit

Permalink
feat(metadata): add ReflectMetadataProvider, rename the ts-morph
Browse files Browse the repository at this point in the history
…one (#240)

- `TypescriptMetadataProvider` is now renamed to `TsMorphProvider`.
- `ReflectMetadataProvider` is provided that does not use `ts-morph` but have some limitations on entity definition

Closes #235
  • Loading branch information
B4nan committed Nov 19, 2019
1 parent f006c3f commit d740eb3
Show file tree
Hide file tree
Showing 39 changed files with 546 additions and 232 deletions.
29 changes: 22 additions & 7 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,24 @@ is to just deploy your TS source files next to the compiled output, just like du

## Deploy a bundle of entities and dependencies with [Webpack](https://webpack.js.org/)

Webpack can be used to bundle every entity and dependency: you get a single file that contains every required module/file and has no external dependencies.
Webpack can be used to bundle every entity and dependency: you get a single file that contains
every required module/file and has no external dependencies.

### Prepare your project for Webpack

Webpack requires every required file to be hardcoded in your code. Code like this won't work (it will throw an error because Webpack doesn't know which file to include in the bundle):
Webpack requires every required file to be hardcoded in your code. Code like this won't work
(it will throw an error because Webpack doesn't know which file to include in the bundle):

```typescript
let dependencyNameInVariable = 'dependency';
const dependency = import(dependencyNameInVariable);
```

As Webpack creates a file bundle, it isn't desired that it scans directories for entities or metadata. Therefore you need to use the `entities` option in the initialization funcion and entitiesDirs/entitiesDirsTs will be ignored (see dynamically including entities as an alternative solution). Also you need to fill `type` or `entity` attributes everywhere (see above) and disable caching (it will decrease start-time slightly).
As Webpack creates a file bundle, it isn't desired that it scans directories for entities
or metadata. Therefore you need to use the `entities` option in the initialization function
and `entitiesDirs`/`entitiesDirsTs` will be ignored (see dynamically including entities as
an alternative solution). Also you need to fill `type` or `entity` attributes everywhere
(see above) and disable caching (it will decrease start-time slightly).

#### Manually defining entities

Expand All @@ -97,9 +103,13 @@ await MikroORM.init({

#### Dynamically loading dependencies

This will make use of a Webpack feature called [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports). This way you can import dependencies as long as part of the path is known.
This will make use of a Webpack feature called [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports).
This way you can import dependencies as long as part of the path is known.

In following example [`require.context`](https://webpack.js.org/guides/dependency-management/#requirecontext) is used. This 'function' is only usable during the building process from Webpack so therefore there is an alternative solution provided that will as long as the environment variable WEBPACK is not set (e.g. during development with `ts-node`).
In following example [`require.context`](https://webpack.js.org/guides/dependency-management/#requirecontext)
is used. This 'function' is only usable during the building process from Webpack so therefore
there is an alternative solution provided that will as long as the environment variable
WEBPACK is not set (e.g. during development with `ts-node`).

Here, all files with the extension `.ts` will be imported from the directory `../entities`.

Expand Down Expand Up @@ -132,7 +142,10 @@ async function getEntities(): Promise<any[]> {

### Webpack configuration

Webpack can be run without [configuration file](https://webpack.js.org/configuration/) but for building MikroORM and [Node.js](https://nodejs.org/) bundles it requires additional configuration. Configuration for Webpack is stored in the root of the project as `webpack.config.js`. For all the options please refer to the following [page](https://webpack.js.org/configuration/).
Webpack can be run without [configuration file](https://webpack.js.org/configuration/) but
for building MikroORM and [Node.js](https://nodejs.org/) bundles it requires additional
configuration. Configuration for Webpack is stored in the root of the project as
`webpack.config.js`. For all the options please refer to the following [page](https://webpack.js.org/configuration/).

For bundling MikroORM the following configuration is required:

Expand Down Expand Up @@ -176,4 +189,6 @@ module.exports = {

### Running Webpack

To run Webpack execute `webpack` (or `npx webpack` if not installed globally) in the root of the project. It will probably throw a few warnings but you can ignore the errors regarding MikroORM: the mentioned pieces of code won't be executed if properly bundled with Webpack.
To run Webpack execute `webpack` (or `npx webpack` if not installed globally) in the root
of the project. It will probably throw a few warnings but you can ignore the errors regarding
MikroORM: the mentioned pieces of code won't be executed if properly bundled with Webpack.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ compatible with Vanilla JavaScript.
- [Property Validation](property-validation.md)
- [Lifecycle Hooks](lifecycle-hooks.md)
- [Naming Strategy](naming-strategy.md)
- [Metadata Providers](metadata-providers.md)
- [Metadata Cache](metadata-cache.md)
- [Debugging](debugging.md)
- [Schema Generator](schema-generator.md)
Expand Down
18 changes: 15 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ const orm = await MikroORM.init({
## Entity Discovery in TypeScript

Internally, `MikroORM` uses [performs analysis](metadata-cache.md) of source files of entities
to sniff types of all properties. This process can be slow if your project contains lots of
files. To speed up the discovery process a bit, you can provide more accurate paths where your
Internally, `MikroORM` uses [`ts-morph` to perform analysis](metadata-providers.md) of source files
of entities to sniff types of all properties. This process can be slow if your project contains lots
of files. To speed up the discovery process a bit, you can provide more accurate paths where your
entity source files are:

```typescript
Expand All @@ -83,6 +83,18 @@ const orm = await MikroORM.init({
});
```

You can also use different [metadata provider](metadata-providers.md) or even write custom one:

- `ReflectMetadataProvider` that uses `reflect-metadata` instead of `ts-morph`
- `JavaScriptMetadataProvider` that allows you to manually provide the entity schema (mainly for Vanilla JS)

```typescript
const orm = await MikroORM.init({
metadataProvider: ReflectMetadataProvider,
// ...
});
```

## Setting up the Commandline Tool

MikroORM ships with a number of command line tools that are very helpful during development,
Expand Down
142 changes: 142 additions & 0 deletions docs/metadata-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
---

# Metadata Providers

As part of entity discovery process, MikroORM uses so called `MetadataProvider` to get necessary
type information about your entities' properties. There are 3 built in metadata providers you can
use:

> You can also implement custom metadata provider by extending abstract `MetadataProvider` class.
## TsMorphMetadataProvider

By default, MikroORM uses [`ts-morph`](https://github.com/dsherret/ts-morph) to read
TypeScript source files of all entities to be able to detect all types. Thanks to this,
defining the type is enough for runtime validation.

This process can be a bit slow as well as memory consuming, mainly because `ts-morph` will
scan all your source files based on your `tsconfig.json`. You can speed up this process by
whitelisting only the folders where your entities are via `entitiesDirsTs` option.

> You can specify the path to `tsconfig.json` manually via `discovery: { tsConfigPath: '...' }`.
After the discovery process ends, all [metadata will be cached](metadata-cache.md). By default,
`FileCacheAdapter` will be used to store the cache inside `./temp` folder in JSON files.

> You can generate production cache via CLI command `mikro-orm cache:generate`.
> You can implement custom cache adapter by implementing `CacheAdapter` interface.
## ReflectMetadataProvider

`ReflectMetadataProvider` uses `reflect-metadata` module to read the type from decorator
metadata exported by TypeScript compiler.

You will need to install `reflect-metadata` module and import at the top of your app's
bootstrap script (e.g. `main.ts` or `app.ts`).

```typescript
import 'reflect-metadata';
```

Next step is to enable `emitDecoratorMetadata` flag in your `tsconfig.json`.

> As this approach does not have performance impact, metadata caching is not really necessary.
```typescript
await MikroORM.init({
metadataProvider: ReflectMetadataProvider,
cache: { enabled: false },
// ...
});
```

### Limitations and requirements

#### Explicit types

Type inference is not supported, you need to always explicitly specify the type:

```typescript
@Property()
createdAt: Date = new Date();
```

#### Collection properties and Identified references

You need to provide target entity type in `@OneToMany` and `@ManyToMany` decorators:

```typescript
@OneToMany(() => Book, b => b.author)
books = new Collection<Book>(this);

@ManyToOne(() => Publisher, { wrappedReference: true })
publisher!: IdentifiedReference<Publisher>;
```

#### Optional properties

Reading property nullability is not supported, you need to explicitly set `nullable` attribute:

```typescript
@Property({ nullable: true })
prop?: string;
```

#### Enums

By default, enum is considered as numeric type. For string enums, you need to explicitly
provide one of:

- reference to the enum (which will force you to define the enum before defining the entity)
```typescript
@Enum(() => UserRole)
role: UserRole;
```
- name of the enum (if it is present in the same file)
```typescript
@Enum({ type: 'UserRole' })
role: UserRole;
```
- list of the enum items
```typescript
@Enum({ items: ['a', 'b', 'c'] })
role: UserRole;
```

#### Circular dependencies

Reading type of referenced entity in `@ManyToOne` and `@OneToOne` properties fails if there is
circular dependency. You will need to explicitly define the type in the decorator (preferably
via `entity: () => ...` callback).
```typescript
@ManyToOne({ entity: () => Author })
author: Author;
```

> There can be recursion issues when you define multiple entities (with circular dependencies
> between each other) in single file. In that case, you might want to provide the type via decorator's
> `type` or `entity` attributes and set the TS property type to something else (like `any` or `object`).
#### Additional typings might be required

You might have to install additional typings, one example is use of `ObjectId` in MongoDB,
which requires `@types/mongodb` to be installed.

## JavaScriptMetadataProvider

This provider should be used only if you are not using TypeScript at all and therefore you do
not use decorators to annotate your properties. It will require you to specify the whole schema
manually.

```typescript
await MikroORM.init({
metadataProvider: JavaScriptMetadataProvider,
cache: { enabled: false },
// ...
});
```

You can read more about it in [Usage with JavaScript section](usage-with-js.md).
5 changes: 5 additions & 0 deletions docs/upgrading-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ override the default implementation to accommodate their specific needs.
If you used custom naming strategy, you will either need to implement this method yourself,
or extend `AbstractNamingStrategy`.

## `TypescriptMetadataProvider` has been renamed

The name is now `TsMorphMetadataProvider`, there is also newly added `ReflectMetadataProvider`
that uses `reflect-metadata` instead.

## Updated mongodb driver

MongoDB driver version 3.3.4 or higher is now required.
9 changes: 8 additions & 1 deletion lib/MikroORM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import chalk from 'chalk';

import { EntityManager } from './EntityManager';
import { AbstractSqlDriver, IDatabaseDriver } from './drivers';
import { MetadataDiscovery, MetadataStorage } from './metadata';
import { MetadataDiscovery, MetadataStorage, ReflectMetadataProvider } from './metadata';
import { Configuration, Logger, Options } from './utils';
import { SchemaGenerator } from './schema';
import { EntityGenerator } from './schema/EntityGenerator';
import { Migrator } from './migrations';
import { NullCacheAdapter } from './cache';

export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {

Expand Down Expand Up @@ -41,6 +42,12 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
this.config = new Configuration(options);
}

if (process.env.WEBPACK) {
this.config.set('metadataProvider', ReflectMetadataProvider);
this.config.set('cache', { adapter: NullCacheAdapter });
this.config.set('discovery', { requireEntitiesArray: true });
}

this.driver = this.config.getDriver();
this.logger = this.config.getLogger();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/CLIHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class CLIHelper {
}

if (Utils.isDefined(warnWhenNoEntities)) {
options.set('warnWhenNoEntities', warnWhenNoEntities);
options.get('discovery').warnWhenNoEntities = warnWhenNoEntities;
}

return MikroORM.init(options);
Expand Down
Loading

0 comments on commit d740eb3

Please sign in to comment.