Skip to content

Commit

Permalink
feat(core): add MikroORM.initSync() helper (#4166)
Browse files Browse the repository at this point in the history
As opposed to the async `MikroORM.init` method, you can prefer to use
synchronous variant `initSync`. This method has some limitations:

- database connection will be established when you first interact with
the database (or you can use `orm.connect()` explicitly)
- no loading of the `config` file, `options` parameter is mandatory
- no support for folder based discovery
- no check for mismatched package versions

Related: #4164
  • Loading branch information
B4nan committed Nov 5, 2023
1 parent 929d7d7 commit 8b1a1fa
Show file tree
Hide file tree
Showing 34 changed files with 326 additions and 192 deletions.
15 changes: 12 additions & 3 deletions docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,23 @@ const orm = await MikroORM.init<PostgreSqlDriver>({
});
```

If we are experiencing problems with folder based discovery, try using `mikro-orm debug` CLI command to check what paths are actually being used.
If you are experiencing problems with folder based discovery, try using `mikro-orm debug` CLI command to check what paths are actually being used.

> Since v4, we can also use file globs, like `./dist/app/**/entities/*.entity.js`.
> Since v4, you can also use file globs, like `./dist/app/**/entities/*.entity.js`.
We can also set the configuration via [environment variables](configuration.md#using-environment-variables).
You can also set the configuration via [environment variables](configuration.md#using-environment-variables).

> We can pass additional options to the underlying driver (e.g. `mysql2`) via `driverOptions`. The object will be deeply merged, overriding all internally used options.
## Synchronous initialization

As opposed to the async `MikroORM.init` method, you can prefer to use synchronous variant `initSync`. This method has some limitations:

- database connection will be established when you first interact with the database (or you can use `orm.connect()` explicitly)
- no loading of the `config` file, `options` parameter is mandatory
- no support for folder based discovery
- no check for mismatched package versions

## Possible issues with circular dependencies

Our entities will most probably contain circular dependencies (e.g. if we use bi-directional relationship). While this is fine, there might be issues caused by wrong order of entities during discovery, especially when we are using the folder based way.
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/upgrading-v5-to-v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,7 @@ ref.age = raw(`age * 2`);
await em.flush();
console.log(ref.age); // real value is available after flush
```

## Metadata CacheAdapter requires sync API

To allow working with cache inside `MikroORM.initSync`, the metadata cache now enforces sync API. You should usually depend on the file-based cache for the metadata, which now uses sync methods to work with the file system.
1 change: 1 addition & 0 deletions packages/better-sqlite/src/BetterSqliteConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class BetterSqliteConnection extends AbstractSqlConnection {
this.getPatchedDialect();
this.client = this.createKnexClient('better-sqlite3');
await this.client.raw('pragma foreign_keys = on');
this.connected = true;
}

getDefaultClientUrl(): string {
Expand Down
7 changes: 7 additions & 0 deletions packages/better-sqlite/src/BetterSqliteMikroORM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export class BetterSqliteMikroORM extends MikroORM<BetterSqliteDriver> {
return super.init(options);
}

/**
* @inheritDoc
*/
static override initSync<D extends IDatabaseDriver = BetterSqliteDriver>(options: Options<D>): MikroORM<D> {
return super.initSync(options);
}

}

export type BetterSqliteOptions = Options<BetterSqliteDriver>;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/ClearCacheCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class ClearCacheCommand implements CommandModule {
return;
}

const cache = config.getCacheAdapter();
const cache = config.getMetadataCacheAdapter();
await cache.clear();

CLIHelper.dump(colors.green('Metadata cache was successfully cleared'));
Expand Down
51 changes: 46 additions & 5 deletions packages/core/src/MikroORM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
return orm;
}

/**
* Synchronous variant of the `init` method with some limitations:
* - database connection will be established when you first interact with the database (or you can use `orm.connect()` explicitly)
* - no loading of the `config` file, `options` parameter is mandatory
* - no support for folder based discovery
* - no check for mismatched package versions
*/
static initSync<D extends IDatabaseDriver = IDatabaseDriver>(options: Options<D>): MikroORM<D> {
ConfigurationLoader.registerDotenv(options);
const env = ConfigurationLoader.loadEnvironmentVars<D>();
options = Utils.merge(options, env);

if ('DRIVER' in this && !options!.driver) {
(options as Options).driver = (this as unknown as { DRIVER: Constructor<IDatabaseDriver> }).DRIVER;
}

const orm = new MikroORM(options!);

// we need to allow global context here as we are not in a scope of requests yet
const allowGlobalContext = orm.config.get('allowGlobalContext');
orm.config.set('allowGlobalContext', true);
orm.discoverEntitiesSync();
orm.config.set('allowGlobalContext', allowGlobalContext);
orm.driver.getPlatform().lookupExtensions(orm);

for (const extension of orm.config.get('extensions')) {
extension.register(orm);
}

return orm;
}

constructor(options: Options<D> | Configuration<D>) {
if (options instanceof Configuration) {
this.config = options;
Expand Down Expand Up @@ -135,8 +167,8 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
await this.driver.close(force);
}

if (this.config.getCacheAdapter()?.close) {
await this.config.getCacheAdapter().close!();
if (this.config.getMetadataCacheAdapter()?.close) {
await this.config.getMetadataCacheAdapter().close!();
}

if (this.config.getResultCacheAdapter()?.close) {
Expand Down Expand Up @@ -169,6 +201,15 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {

async discoverEntities(): Promise<void> {
this.metadata = await this.discovery.discover(this.config.get('tsNode'));
this.createEntityManager();
}

discoverEntitiesSync(): void {
this.metadata = this.discovery.discoverSync(this.config.get('tsNode'));
this.createEntityManager();
}

private createEntityManager(): void {
this.driver.setMetadata(this.metadata);
this.em = this.driver.createEntityManager<D>();
(this.em as { global: boolean }).global = true;
Expand All @@ -179,12 +220,12 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
/**
* Allows dynamically discovering new entity by reference, handy for testing schema diffing.
*/
async discoverEntity(entities: Constructor | Constructor[]): Promise<void> {
discoverEntity(entities: Constructor | Constructor[]): void {
entities = Utils.asArray(entities);
const tmp = await this.discovery.discoverReferences(entities);
const tmp = this.discovery.discoverReferences(entities);
const options = this.config.get('discovery');
new MetadataValidator().validateDiscovered([...Object.values(this.metadata.getAll()), ...tmp], options.warnWhenNoEntities, options.checkDuplicateTableNames);
const metadata = await this.discovery.processDiscoveredEntities(tmp);
const metadata = this.discovery.processDiscoveredEntities(tmp);
metadata.forEach(meta => this.metadata.set(meta.className, meta));
this.metadata.decorate(this.em);
}
Expand Down
29 changes: 24 additions & 5 deletions packages/core/src/cache/CacheAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,45 @@ export interface CacheAdapter {
/**
* Gets the items under `name` key from the cache.
*/
get(name: string): Promise<any>;
get<T = any>(name: string): T | Promise<T> | undefined;

/**
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
*/
set(name: string, data: any, origin: string, expiration?: number): Promise<void>;
set(name: string, data: any, origin: string, expiration?: number): void | Promise<void>;

/**
* Removes the item from cache.
*/
remove(name: string): Promise<void>;
remove(name: string): void | Promise<void>;

/**
* Clears all items stored in the cache.
*/
clear(): Promise<void>;
clear(): void | Promise<void>;

/**
* Called inside `MikroORM.close()` Allows graceful shutdowns (e.g. for redis).
*/
close?(): Promise<void>;
close?(): void | Promise<void>;

}

export interface SyncCacheAdapter extends CacheAdapter {

/**
* Gets the items under `name` key from the cache.
*/
get<T = any>(name: string): T | undefined;

/**
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
*/
set(name: string, data: any, origin: string, expiration?: number): void;

/**
* Removes the item from cache.
*/
remove(name: string): void;

}
51 changes: 24 additions & 27 deletions packages/core/src/cache/FileCacheAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import globby from 'globby';
import { ensureDir, pathExists, readFile, readJSON, unlink, writeJSON } from 'fs-extra';
import { ensureDirSync, pathExistsSync, readFileSync, readJSONSync, unlinkSync, writeJSONSync } from 'fs-extra';

import type { CacheAdapter } from './CacheAdapter';
import type { SyncCacheAdapter } from './CacheAdapter';
import { Utils } from '../utils/Utils';

export class FileCacheAdapter implements CacheAdapter {
export class FileCacheAdapter implements SyncCacheAdapter {

private readonly VERSION = Utils.getORMVersion();

Expand All @@ -15,15 +15,15 @@ export class FileCacheAdapter implements CacheAdapter {
/**
* @inheritDoc
*/
async get(name: string): Promise<any> {
const path = await this.path(name);
get(name: string): any {
const path = this.path(name);

if (!await pathExists(path)) {
if (!pathExistsSync(path)) {
return null;
}

const payload = await readJSON(path);
const hash = await this.getHash(payload.origin);
const payload = readJSONSync(path);
const hash = this.getHash(payload.origin);

if (!hash || payload.hash !== hash) {
return null;
Expand All @@ -35,46 +35,43 @@ export class FileCacheAdapter implements CacheAdapter {
/**
* @inheritDoc
*/
async set(name: string, data: any, origin: string): Promise<void> {
const [path, hash] = await Promise.all([
this.path(name),
this.getHash(origin),
]);

set(name: string, data: any, origin: string): void {
const path = this.path(name);
const hash = this.getHash(origin);
const opts = this.pretty ? { spaces: 2 } : {};
await writeJSON(path!, { data, origin, hash, version: this.VERSION }, opts);
writeJSONSync(path!, { data, origin, hash, version: this.VERSION }, opts);
}

/**
* @inheritDoc
*/
async remove(name: string): Promise<void> {
const path = await this.path(name);
await unlink(path);
remove(name: string): void {
const path = this.path(name);
unlinkSync(path);
}

/**
* @inheritDoc
*/
async clear(): Promise<void> {
const path = await this.path('*');
const files = await globby(path);
await Promise.all(files.map(file => unlink(file)));
clear(): void {
const path = this.path('*');
const files = globby.sync(path);
files.forEach(file => unlinkSync(file));
}

private async path(name: string): Promise<string> {
await ensureDir(this.options.cacheDir);
private path(name: string): string {
ensureDirSync(this.options.cacheDir);
return `${this.options.cacheDir}/${name}.json`;
}

private async getHash(origin: string): Promise<string | null> {
private getHash(origin: string): string | null {
origin = Utils.absolutePath(origin, this.baseDir);

if (!await pathExists(origin)) {
if (!pathExistsSync(origin)) {
return null;
}

const contents = await readFile(origin);
const contents = readFileSync(origin);

return Utils.hash(contents.toString() + this.VERSION);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/cache/MemoryCacheAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class MemoryCacheAdapter implements CacheAdapter {
/**
* @inheritDoc
*/
async get<T = any>(name: string): Promise<T | undefined> {
get<T = any>(name: string): T | undefined {
const data = this.data.get(name);

if (data) {
Expand All @@ -26,21 +26,21 @@ export class MemoryCacheAdapter implements CacheAdapter {
/**
* @inheritDoc
*/
async set(name: string, data: any, origin: string, expiration?: number): Promise<void> {
set(name: string, data: any, origin: string, expiration?: number): void {
this.data.set(name, { data, expiration: Date.now() + (expiration ?? this.options.expiration) });
}

/**
* @inheritDoc
*/
async remove(name: string): Promise<void> {
remove(name: string): void {
this.data.delete(name);
}

/**
* @inheritDoc
*/
async clear(): Promise<void> {
clear(): void {
this.data.clear();
}

Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/cache/NullCacheAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type { CacheAdapter } from './CacheAdapter';
import type { SyncCacheAdapter } from './CacheAdapter';

export class NullCacheAdapter implements CacheAdapter {
export class NullCacheAdapter implements SyncCacheAdapter {

/**
* @inheritDoc
*/
async get(name: string): Promise<any> {
get(name: string): any {
return null;
}

/**
* @inheritDoc
*/
async set(name: string, data: any, origin: string): Promise<void> {
set(name: string, data: any, origin: string): void {
// ignore
}

/**
* @inheritDoc
*/
async remove(name: string): Promise<void> {
remove(name: string): void {
// ignore
}

/**
* @inheritDoc
*/
async clear(): Promise<void> {
clear(): void {
// ignore
}

Expand Down

0 comments on commit 8b1a1fa

Please sign in to comment.