Skip to content
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

RFC: use LB3 models in an LB4 app [SPIKE - DO NOT MERGE] #2274

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions lb3api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,36 @@ How to test code migrated from strong-remoting and loopback (v3)? Do we want to
copy existing tests over? Migrate them to async/await style? Don't bother with
testing at all, use few acceptance-level tests only?

How to split 1k+ lines of new (migrated) code into smaller chunks that will be
How to split 2k+ lines of new (migrated) code into smaller chunks that will be
easier to review?

TODO:

1. expose remote methods via REST
2. boot
Should we register LB3 Models for dependency injection into LB4 code? Register
them as repositories, models, services, or something else?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions; LB3 Datasources were easy to register for DI into LB4 app, but LB3 models are tricky. IMO LB3 models are synonymous with LB4 repositories or services.


## Should have/next iterations:

- PersistedModel - all CRUD APIs
- register Models and DataSources for dependency injection?
- allow LB3 models to be attached to LB4 dataSources?
- pick up models/methods added after app start
- hasUpdateOnlyProps (different request body schema for "create" method)
- set options from HTTP context (`http: 'optionsFromRequest'`)
- `{rest: {after: convertNullToNotFoundError}}`
- mixins
- case-insensitive URL paths (?) (/Todo is same as /todo)
- model-sources and mixin-sources in model-config.json

On aside

## Model
- https://github.com/Microsoft/TypeScript/issues/6480
- extract the Booter contract into a standalone package so that v3compat does
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what 'Booter contract' is in this case? EDIT: I kind of have an idea what it is (i.e. a booter class).

not have to inherit entire boot

### Model

- beforeRemote/afterRemote/afterRemoteError
- disableRemoteMethodByName(name)

## HttpContext
### HttpContext

- method
- req
- res
Expand All @@ -40,13 +44,15 @@ TODO:
- methodString
- result

## SharedMethod
### SharedMethod

- isMethodEnabled(sharedMethod)
- resolve(resolver)
- findMethodByName
- disableMethodByName

### Application

- app.connector(name, connector)
- app.connectors.{name}

Expand Down Expand Up @@ -74,10 +80,12 @@ TODO:

### Remoting features

- respond with a Buffer, respond with a ReadableStream
- respond with a Buffer, respond with a ReadableStream

## Won't have
## WILL NOT HAVE

- allow LB3 models to be attached to LB4 dataSources. This won't work
because LB3 requires all datasources to share the same ModelBuilder
- CLS-based context
- global registry: loopback.createModel, loopback.findModel, etc.
- Model.checkAccess(token, modelId, sharedMethod, ctx, cb)
Expand All @@ -102,6 +110,7 @@ TODO:
- fillCustomChangeProperties

built-in models

- Access-token
- Acl
- Application
Expand All @@ -114,13 +123,16 @@ built-in models
- User

SharedClass

- find: was already deprecated
- disableMethod: was already deprecated

SharedMethod

- prototype.invoke(scope, args, remotingOptions, ctx, cb)

HttpContext

- ~~typeRegistry~~
- ~~supportedTypes~~
- invoke
Expand All @@ -134,6 +146,11 @@ HttpContext
- (etc.)

Remoting

- XML
- JSON API
- piping retval of remote function into response

Booting

- datasources
6 changes: 6 additions & 0 deletions packages/v3compat/fixtures/model-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Todo": {
"dataSource": "db",
"public": true
}
}
11 changes: 11 additions & 0 deletions packages/v3compat/fixtures/models/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/v3compat
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

module.exports = function(Todo) {
// A dummy custom method to verify that model JS file was correctly processed
Todo.findByTitle = function(title) {
return this.find({where: {title}});
};
};
11 changes: 11 additions & 0 deletions packages/v3compat/fixtures/models/todo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "Todo",
"base": "PersistedModel",
"strict": false,
"properties": {
"title": {
"type": "string",
"required": true
}
}
}
2 changes: 1 addition & 1 deletion packages/v3compat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"url": "https://github.com/strongloop/loopback-next.git"
},
"dependencies": {
"@loopback/context": "^1.4.0",
"@loopback/boot": "^1.0.10",
"@loopback/core": "^1.1.3",
"@loopback/rest": "^1.5.3",
"debug": "^4.1.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/v3compat/src/boot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# v3compat/boot

Source code migrated from
[loopback-boot](https://github.com/strongloop/loopback-boot).
6 changes: 6 additions & 0 deletions packages/v3compat/src/boot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/v3compat
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './lb3-model-booter';
121 changes: 121 additions & 0 deletions packages/v3compat/src/boot/lb3-model-booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/v3compat
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {BootBindings, Booter, discoverFiles} from '@loopback/boot';
import {Application, CoreBindings, inject} from '@loopback/core';
import * as debugFactory from 'debug';
import * as fs from 'fs';
import * as path from 'path';
import {promisify} from 'util';
import {Lb3Application} from '../core';

const fileExists = promisify(fs.exists);

const debug = debugFactory('loopback:v3compat:model-booter');

const DefaultOptions = {
root: './legacy',
};

export class Lb3ModelBooter implements Booter {
options: Lb3ModelBooterOptions;

root: string;
modelDefinitionsGlob: string;
modelConfigFile: string;

discoveredDefinitionFiles: string[];

constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
public app: Application & {v3compat: Lb3Application},
@inject(BootBindings.PROJECT_ROOT)
public projectRoot: string,
@inject(`${BootBindings.BOOT_OPTIONS}#v3compat`)
options: Partial<Lb3ModelBooterOptions> = {},
) {
this.options = Object.assign({}, DefaultOptions, options);
}

async configure?(): Promise<void> {
this.root = path.join(this.projectRoot, this.options.root);
this.modelDefinitionsGlob = '/models/*.json';
this.modelConfigFile = 'model-config.json';
}

async discover?(): Promise<void> {
debug(
'Discovering LB3 model definitions in %j using glob %j',
this.root,
this.modelDefinitionsGlob,
);

const allFiles = await discoverFiles(this.modelDefinitionsGlob, this.root);
this.discoveredDefinitionFiles = allFiles.filter(
f => f[0] !== '_' && path.extname(f) === '.json',
);
debug(' -> %j', allFiles);
}

async load?(): Promise<void> {
for (const f of this.discoveredDefinitionFiles) await this.loadModel(f);

await this.configureModels();
}

private async loadModel(jsonFile: string) {
const basename = path.basename(jsonFile, path.extname(jsonFile));
const sourceDir = path.dirname(jsonFile);
// TODO: support additional extensions like `.ts` and `.coffee`
const sourceFile = path.join(sourceDir, `${basename}.js`);
const sourceExists = await fileExists(sourceFile);

debug(
'Loading model from %j (%j)',
path.relative(this.projectRoot, jsonFile),
sourceExists
? path.relative(this.projectRoot, sourceFile)
: '<no source code>',
);

const definition = require(jsonFile);
const script = sourceExists ? require(sourceFile) : undefined;

debug(' creating a new model %j', definition.name);
const modelCtor = this.app.v3compat.registry.createModel(definition);

if (typeof script === 'function') {
debug(
' customizing model %j using %j',
definition.name,
path.relative(this.projectRoot, sourceFile),
);
script(modelCtor);
} else if (sourceExists) {
debug(
' skipping model file %s - `module.exports` is not a function',
sourceFile,
);
}
}

private async configureModels() {
const configFile = path.join(this.root, this.modelConfigFile);
debug('Loading model-config from %j', configFile);

const config = require(configFile);
for (const modelName in config) {
if (modelName === '_meta') continue;
const modelConfig = config[modelName];
debug(' configuring %j with %j', modelName, modelConfig);
const modelCtor = this.app.v3compat.registry.getModel(modelName);
this.app.v3compat.model(modelCtor, modelConfig);
}
}
}

export interface Lb3ModelBooterOptions {
root: string;
}
11 changes: 11 additions & 0 deletions packages/v3compat/src/compat.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/v3compat
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Component} from '@loopback/core';
import {Lb3ModelBooter} from './boot';

export class CompatComponent implements Component {
booters = [Lb3ModelBooter];
}
4 changes: 3 additions & 1 deletion packages/v3compat/src/compat.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import {Constructor} from '@loopback/context';
import {Application} from '@loopback/core';
import {Lb3Application} from './core/lb3-application';
import {CompatComponent} from './compat.component';
import {Lb3Application} from './core';

/**
* A mixin class for Application that adds `v3compat` property providing
Expand Down Expand Up @@ -39,6 +40,7 @@ export function CompatMixin<T extends Constructor<any>>(superClass: T) {
constructor(...args: any[]) {
super(...args);
this.v3compat = new Lb3Application((this as unknown) as Application);
this.component(CompatComponent);
}
};
}
6 changes: 3 additions & 3 deletions packages/v3compat/src/core/lb3-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class Lb3Application {
}

dataSource(name: string, config: DataSourceConfig): DataSource {
debug('registering datasource %s with config %j', name, config);
debug('Registering datasource %j with config %j', name, config);
// TODO: use the implementation from LB3's lib/application.js
const ds = this.registry.createDataSource(name, config);
this.dataSources[name] = ds;
Expand All @@ -46,7 +46,7 @@ export class Lb3Application {
}

model(modelCtor: ModelClass, config: ModelConfig) {
debug('registering model %s with config %s', modelCtor.modelName, config);
debug('Registering model %j with config %j', modelCtor.modelName, config);
// TODO: use the implementation from LB3's lib/application.js
if (typeof config.dataSource === 'string') {
const dataSource = this.dataSources[config.dataSource];
Expand All @@ -56,7 +56,7 @@ export class Lb3Application {
this.models[modelCtor.modelName] = modelCtor;
modelCtor.app = this;

// TODO: register Model schema
// TODO: register Model schema for OpenAPI spec
this.restAdapter.registerSharedClass(modelCtor.sharedClass);
}

Expand Down
8 changes: 8 additions & 0 deletions packages/v3compat/src/core/lb3-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import {
} from './lb3-types';
import {PersistedModelClass} from './lb3-persisted-model';

// A workaround for https://github.com/Microsoft/TypeScript/issues/6480
// I was not able to find a way how to mix arbitrary static properties
// while preserving constructor signature. We need to contribute this
// feature to TypeScript ourselves.
export interface AnyStaticMethods {
[methodName: string]: Function;
}

export type ModelClass = typeof Model;

export declare class Model extends ModelBase {
Expand Down
3 changes: 2 additions & 1 deletion packages/v3compat/src/core/lb3-persisted-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Constructor} from '@loopback/core';
import {RemoteMethodOptions} from '../remoting';
import {Model} from './lb3-model';
import {AnyStaticMethods, Model} from './lb3-model';
import {Lb3Registry} from './lb3-registry';
import {
Callback,
Expand Down
Loading