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

DataSource Declarative CLI + Boot #1385

Merged
merged 5 commits into from
Jun 14, 2018
Merged

DataSource Declarative CLI + Boot #1385

merged 5 commits into from
Jun 14, 2018

Conversation

virkt25
Copy link
Contributor

@virkt25 virkt25 commented May 31, 2018

NOTE: This is based on top of the indexer PR -- which has feedback that I will be working to address so it can land before this PR.

fixes #1225

PR is broken by commits for easier review

  • feat(cli): add lb4 datasource command …
  • fix(repository): accept class instead of instance for app.datasource …
  • feat(boot): add datasource and datasource config booters …
  • refactor(example-todo): use datasource cli generated files and dataso… …
  • docs(docs): update docs with datasource cli and booter content …

PENDING:

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

Copy link
Contributor

@shimks shimks left a comment

Choose a reason for hiding this comment

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

Great work! I'll leave the high-level reviews to the senior developers.
It might be better to leave out the make-index stuff outside of this PR

@@ -0,0 +1,750 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to use the json file in loopback-workspace? Having a copy like this may be hard to maintain

Copy link
Member

Choose a reason for hiding this comment

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

+1 for avoiding the need to maintain two identical lists.

I don't like the idea of depending on loopback-workspace, as that would add too many dependencies including LoopBack 3.x

What we can do instead:

Thoughts?

cc @raymondfeng

describe('lb4 datasource', () => {
beforeEach('reset sandbox', () => sandbox.reset());

it('does not run without package.json', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We might need to look into separating out the common tests across artifact generators like this one. Outside of scope in context of this PR though

@dhmlau dhmlau mentioned this pull request Jun 1, 2018
27 tasks
@virkt25 virkt25 force-pushed the datasource branch 2 times, most recently from a566b7c to 02bec9a Compare June 3, 2018 22:08
@virkt25 virkt25 added this to the June Milestone milestone Jun 3, 2018
@virkt25 virkt25 changed the title [WIP] DataSource Declarative CLI + Boot DataSource Declarative CLI + Boot Jun 3, 2018
bajtos
bajtos previously requested changes Jun 4, 2018
import {juggler, DataSource} from '@loopback/repository';

export class <%= className %>DataSource extends juggler.DataSource {
constructor(@inject('datasources.config.<%= name %>') dsConfig: DataSource) {
Copy link
Member

Choose a reason for hiding this comment

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

dsConfig: DataSource looks suspicious to me.

export interface DataSource {
  name: string; // Name of the data source
  connector?: Connector; // The underlying connector instance

  settings: AnyObject; // Settings
  // tslint:disable-next-line:no-any
  [property: string]: any; // Other properties that vary by connectors
}
  1. Configuration wise, we allow developers to specify the connector as a string, e.g. 'mysql' instead of require('loopback-connector-mysql').

  2. DataSource configuration does not contain any settings object, these settings (e.g. url, database, etc.) are top-level properties of the config object.

I think you should use juggler.Options type instead, see juggler.DataSource constructors and [definition of Options](https://github.com/strongloop/loopback-datasource-juggler/blob/77f11cda3b6e9e39c1cc039808180129a23d67ff/types/common.d.ts#L13-L16.

Copy link
Member

Choose a reason for hiding this comment

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

To make these datasource classes easier to use in tests, I think dsConfig should have a default value.

import * as CONFIG from './<%= name %>.json';

export {CONFIG};

export class <%= className %>DataSource extends juggler.DataSource {
  constructor(@inject('datasources.config.<%= name %>') dsConfig: juggler.Options = CONFIG) {
    super(dsConfig);
  }
}

Thoughts?

Copy link
Contributor Author

@virkt25 virkt25 Jun 5, 2018

Choose a reason for hiding this comment

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

Hmm ... I had used DI for the config based on your comment in the boot spike. That said looking at the case of overriding the dsConfig by binding a partial value (such as setting file to undefined in examples/todo) I think it might be better to merge the default config from the file with the config that is bound to Context ... only question is that is that what a user would expect?

import * as CONFIG from './<%= name %>.json';

export class <%= className %>DataSource extends juggler.DataSource {
  constructor(@inject('datasources.config.<%= name %>') dsConfig: juggler.Options = {}) {
    super(Object.assign(CONFIG, dsConfig));
  }
}

Thoughts? ... I'd like agreement before I go about making changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

^ With that my only concern is what a developer expects ... do they expect config that lives in context to be used or is a merge what they expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With regards to the first comment, I wasn't too sure of the purpose of the settings object since I did realize that the settings are actually a top level object. I can change it to juggler.Options --

What is the purpose of the DataSource interface as it's defined here? -- Is it just a remnant of something before the refactor / move to juggler.DataSource?

Copy link
Member

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 DataSource interface as it's defined here? -- Is it just a remnant of something before the refactor / move to juggler.DataSource?

No idea. @raymondfeng could you please clarify?

I had used DI for the config based on your comment in the boot spike.

Sure, I am fine with using DI, I think the Todo example app is already showing why we need that.

I think it might be better to merge the default config from the file with the config that is bound to Context .

That's a bad idea based on my past experience with loopback-boot. The trouble with merging is that users wishing to obtain a specific datasource configuration must unset all unwanted properties provided in the "default" config, which is pretty much impossible.

For example, consider the case where you want to configure a MySQL datasource to use a specific user+password+database combination while ensuring that no other datasource options are set. Now imagine somebody modifies the JSON configuration and adds let's say charset property. Now we need to update the code wanting only user+password+database code to exclude charset from the original config.

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to use import for the json directly within the generated TS file (supported by 2.9.x - https://blogs.msdn.microsoft.com/typescript/2018/05/31/announcing-typescript-2-9/#json-imports)

my-db.datasource.ts

import config from './my-db.json'; // Unfortunately it cannot be `my-db.datasource.json`.

export class MyDBDataSource extends juggler.DataSource {
  constructor() {
    super(config);
  }
}

@@ -0,0 +1,750 @@
{
Copy link
Member

Choose a reason for hiding this comment

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

+1 for avoiding the need to maintain two identical lists.

I don't like the idea of depending on loopback-workspace, as that would add too many dependencies including LoopBack 3.x

What we can do instead:

Thoughts?

cc @raymondfeng

*/
app.bind('datasources.config.db').to({
name: 'db',
connector: 'memory',
Copy link
Member

Choose a reason for hiding this comment

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

-1 for duplicating most of the configuration of db datasource. Could you please try to find a way how to rewrite this code so that it changes only the relevant flags (e.g. set file to undefined - I don't remember exact memory config options)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just binding {file: undefined}won't work as a DataSource needs the name and connector. See my comment above with regards to merging the bound value with the default config.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess an alternative would be to do the merge here instead of the datasource -- I think I like this option better.

const dsKey = 'datasources.config.db';
const config = await app.get(dsKey);
app.bind(dsKet).to(Object.assign(config, {file: undefined}));

Copy link
Member

Choose a reason for hiding this comment

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

I guess an alternative would be to do the merge here instead of the datasource -- I think I like this option better.

Yes please, that's what I had in mind 👍

Copy link
Member

Choose a reason for hiding this comment

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

I created a follow-up issue to improve the overall design to allow tests to modify the default datasource configuration, see #1396

For the scope of this pull request, I am proposing to go with a solution that's fastest to land. We can use the code snippet shown in https://github.com/strongloop/loopback-next/pull/1385/files#r192946885, or even leave {file: undefined} change out of scope and simply use the same datasource config as the application uses at runtime.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does datasources.config.db follow any convention?

Copy link
Contributor Author

@virkt25 virkt25 Jun 13, 2018

Choose a reason for hiding this comment

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

convention for naming or it's config? It's an optional binding a user can set to override the DataSource config (default coming from the generated JSON file). This is more for testing ... with #1396 looking to improve this further.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does @loopback/boot bind datasource config to datasources.config.<dsName>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. It did in the earlier iteration but since we import the config this is the default key that gets set that a user can leverage in testing to override the default config if they wanted to -- it's injected with {optional: true}

* persisting data to file and each injection normally instatiates a new
* instance, we must change the BindingScope to a singleton so only one
* instance is created and used for all injections (preserving access to
* the same memory space).
Copy link
Member

Choose a reason for hiding this comment

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

Every datasource must be always bound in a singleton scope! A datasource holds a list of Model classes, a connection pool, etc. We cannot create a new datasource for each incoming HTTP request, juggler was not designed that way.

// However, we need to investigate how to access these datasources from
// integration tests where we don't have access to the full app object.
// For example, @loopback/boot can provide a helper function for
// performing a partial boot that creates datasources only.
Copy link
Member

Choose a reason for hiding this comment

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

Could you please clarify how integration tests can access datasource configuration now? I guess the answer could be found somewhere in the code, but it will be easier for reviewers if you could summarize it in a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One can access the config / DataSource by getting it from the context. I'm not sure of the question here ...

Copy link
Member

Choose a reason for hiding this comment

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

In integration tests, there is no Application/Context object. We want to test the integration between a repository and the backing database, not the integration with the app/context.

Also test-data-builders need to access a repository instance directly, see e.g. http://loopback.io/doc/en/lb4/Implementing-features.html#update-test-helpers-and-the-controller-use-real-model-and-repository

async function givenEmptyDatabase() {
  await new ProductRepository().deleteAll();
}

async function givenProduct(data: Partial<Product>) {
  return await new ProductRepository().create(
    Object.assign(
      {
        name: 'a-product-name',
        slug: 'a-product-slug',
        price: 1,
        description: 'a-product-description',
        available: true,
      },
      data,
    ),
  );
}

Copy link
Member

Choose a reason for hiding this comment

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

For the scope of this pull request, it's enough to allow tests to instantiate DbDataSource without the need to create a custom datasource config:

const db = new DbDataSource();

This can be achieved e.g. by adding a default value for dsConfig argument, as discussed and shown in #1385 (comment).

I created a follow-up issue to improve the overall design to allow tests to modify the default datasource configuration, see #1396

name: 'db',
connector: 'memory',
});
app.find('datasources.db')[0].inScope(BindingScope.SINGLETON);
Copy link
Member

Choose a reason for hiding this comment

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

See my earlier comment - BindingScope.SINGLETON must be applied to all datasources by @loopback/boot.

@@ -97,9 +97,11 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
* }
* ```
*/
dataSource(dataSource: juggler.DataSource, name?: string) {
dataSource(dataSource: Class<juggler.DataSource>, name?: string) {
Copy link
Member

Choose a reason for hiding this comment

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

I feel LB4 apps should not be required to always define a new DataSource class. Can we support dual usage allowing both a DataSource instance and a DataSource class?

dataSource(dataSource: juggler.DataSource | Class<juggler.DataSource>, name? string): void {
  if (typeof juggler.DataSource === 'function') {
    // handle the Class case
  } else {
    // handle the instance case
  }  
}

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a use case you have in mind where someone will prefer to create an instance instead of a DataSource class?

Copy link
Member

Choose a reason for hiding this comment

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

import {Application} from '@loopback/core';
import {juggler} from '@loopback/repository';

const app = new Application();
app.dataSource(new juggler.DataSource('db', {connector: 'memory'}));
// etc.

While this is not a typical usage in LB4 apps scaffolded by our CLI tooling, it makes it much much simpler to write short scripts reproducing certain behavior (e.g. a bug).

@@ -97,9 +97,11 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
* }
* ```
*/
dataSource(dataSource: juggler.DataSource, name?: string) {
dataSource(dataSource: Class<juggler.DataSource>, name?: string) {
const dataSourceKey = `datasources.${name || dataSource.name}`;
Copy link
Member

Choose a reason for hiding this comment

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

Using dataSource.name as the default name value changes the behavior quite a lot.

Before:

app.dataSource(new DataSource({name: 'db', connector: 'memory'}));
// -> name is "db"

After your change:

app.bind('datasources.config.db').to(({name: 'db', connector: 'memory'});
app.dataSource(DbDataSource);
// -> name is DbDataSource

IMO, the name argument must be required when registering a datasource via a class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The boot module tries to mitigate this by passing in a name to app.dataSource where it removes the suffix DataSource -- if present.

Copy link
Member

Choose a reason for hiding this comment

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

IMO, this API needs to support more than just our boot module. We want to encourage people to build their own boot-strapper if they don't like our convention or have a specific use case were our conventions don't work well.

One use case is to write a short script that reproduces certain behavior and calls all LB APIs directly, without going through the boot module. Our API should guide the user to do the right thing, i.e. ask them to provide a datasource name if they are passing a class constructor.

this.bind(dataSourceKey).to(dataSource);
this.bind(dataSourceKey)
.toClass(dataSource)
.tag('datasource');
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned in another comment, we must change the scope of all datasource binding to a SINGLETON.

);
} else {
this.classes.forEach(cls => {
const name = kebabCase(cls.name.replace('DataSource', ''));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bajtos This is where the name for binding the DataSource class gets changed from DbDataSource to db.

Now that I look at it, maybe this would be better in the .dataSource function so anyone using it directly also benefits from this and not just boot users. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

IMO, this adds too much coupling between our core/repository APIs, the way how boot operates and the naming conventions we use in files generated by our tooling.

Ideally, the core & repository APIs should be agnostic of any class-naming conventions.

Also, what if we have DbDataSource class with JSON config file that says name: "mysql-db" (imagine we started with the same name but then a change was made in one of these two places only)? Which name will take precedence? How are our users going to learn about these precedence rules? Isn't this making the learning curve more steeper than needed?

@virkt25 virkt25 changed the base branch from indexer to master June 5, 2018 05:19
* Ask for DataSource Name -- Must be unique
*/
promptArtifactName() {
return super.promptArtifactName();
Copy link
Contributor

Choose a reason for hiding this comment

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

We should prompt for the name of a datasource, not the class name.

@virkt25
Copy link
Contributor Author

virkt25 commented Jun 6, 2018

Talked with @raymondfeng and agreed with on the following:

  • Import JSON from local file while have DI as an optional way of overriding the default config.
  • CLI will prompt for DataSource name - Which will be set as a static property on the DataSource Class -- and used by app.dataSource for binding so we aren't "magically" guessing the name. The static property can be removed once we have @injectable providing binding metadata (non-DP3)
  • app.dataSource will support binding a juggler.DataSource class / instance / config (create instance in memory)

Outstanding Questions / Todos / Concerns

  • Maintaining 2 lists for the CLI DataSources? -- PROPOSAL: Update this in @loopback/cli by getting the file from loopback-workspace master -- I am ok with this.
  • Create a follow up task on better re-factoring of CLI tests common to all generators -- out of scope of this PR

@bajtos
Copy link
Member

bajtos commented Jun 6, 2018

Import JSON from local file while have DI as an optional way of overriding the default config.

+1

CLI will prompt for DataSource name - Which will be set as a static property on the DataSource Class -- and used by app.dataSource for binding so we aren't "magically" guessing the name. The static property can be removed once we have @Injectable providing binding metadata (non-DP3)

+1

Please note that name is a reserved property of functions/class constructors and you need to use a different static property. In LB 3.x, we use modelName for model names. Perhaps you can use dataSourceName here?

app.dataSource will support binding a juggler.DataSource class / instance / config (create instance in memory)

+1 to support more flavors.

I have a concern though - can we reliably distinguish between a DataSource instance and a DataSource config object? Both values are an object. (DataSource class is a function, that's easy to tell apart.)


I have posted two comments regarding application integration/acceptance tests, I hope they will help us to keep the scope of this PR small(er) and get it landed sooner:

See also the follow-up issue I created: #1396 How to modify datasource configuration in integration/acceptance tests

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

The new code looks mostly good, I have few minor comments to address.

- Install `@loopback/repository` and the connector package (if it's not a custom
connector).
- Create a file with the connector configuration as follows:
`/datasources/${connector.name}.datasource.json`
Copy link
Member

Choose a reason for hiding this comment

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

I think the file name should be based on datasource name provided by the user (e.g. db), not the connector name (e.g. memory), because a single connector can be used for multiple datasources.

The same comment applies to the next line below.

provided by the CLI to generate a DataSource. The CLI will prompt for all necessary connector information and create the following files:

- `${connector.name}.datasource.json` containing the connector configuration
- `${connector.name}.datasource.ts` containing a class extending `juggler.DataSource`. This class can be used to override the default DataSource
Copy link
Member

Choose a reason for hiding this comment

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

Ditto, I think we should use dataSourceName instead of connector.name.

? window.localStorage key to use for persistence (browser only):
? Full path to file for persistence (server only): ./data/db.json

create src/datasources/ds.datasource.json
Copy link
Member

Choose a reason for hiding this comment

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

The prompt shows Datasource name: db, but the file name is ds.datasource.json. I think it should be db.datasource.json?

import {juggler} from '@loopback/repository';
import {inject} from '@loopback/core';
import {juggler, DataSource} from '@loopback/repository';
const config = require('./db.datasource.json');
Copy link
Member

Choose a reason for hiding this comment

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

Can we use import * as config from './db.datasource.json' to get more type information?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I keep getting an error when I do this as follows: Type 'typeof FILENAME' is not assignable to type 'DataSource' --- and the new import from JSON in TS is in 2.9 but we had to revert back to 2.8. This will have to be addressed in a follow up PR once we can use TS 2.9

// performing a partial boot that creates datasources only.
export const db = new juggler.DataSource(config);
constructor(
@inject('datasources.config.db', {optional: true})
Copy link
Member

Choose a reason for hiding this comment

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

An idea to consider - out of scope of this pull request.

@inject.optional('datasources.config.db')

Copy link
Contributor Author

@virkt25 virkt25 Jun 13, 2018

Choose a reason for hiding this comment

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

@inject.optional doesn't exist yet in our code but I do like the idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

We allow @inject('datasources.config.db', {optional: true});

dataSource(dataSource: juggler.DataSource, name?: string) {
const dataSourceKey = `datasources.${name || dataSource.name}`;
this.bind(dataSourceKey).to(dataSource);
dataSource(dataSource: Class<juggler.DataSource>, name?: string) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we agreed that app.dataSource should allow both Class<juggler.DataSource> and juggler.DataSource?

@bajtos bajtos dismissed their stale review June 12, 2018 12:25

most comments have been addressed

async function download() {
var file = fs.createWriteStream(dest);
var request = https
.get(URL, function(response) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, why don't we keep a local copy with @loopback/cli?

Copy link
Contributor

Choose a reason for hiding this comment

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

I realize that it's used as part of the build.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. Build pulls in the latest from loopback-workspace as we didn't want to maintain two lists of connectors & didn't want to make loopback-workspace a dependency.

});
}

async function transformConnectorJSON() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we come up a json format to have more metadata about the prompts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you mean -- using the same format as loopback-workspace but transformed to be simpler for consumption by CLI prompts -- in LB3 the CLI does the transformation when the command is called, I am preferring to do it whenever we get the file.

Copy link
Contributor

@raymondfeng raymondfeng left a comment

Choose a reason for hiding this comment

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

I left a few comments which can be addressed later.

debug(`npmModule - ${pkgs[0]}`);
}

pkgs.push('@loopback/repository');
Copy link
Contributor

Choose a reason for hiding this comment

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

would it make sense to install @loopback/core here as well since we import inject from it in the template datasource ejs file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This command is meant to be run inside a loopback 4 project which if generated using CLI should already have @loopback/core installed but we don't install @loopback/repository.

* Get a validate function for object/array type
* @param {String} type 'object' OR 'array'
*/
exports.validateStringObject = function(type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

function(type: string) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CLI is a JS project not a TS project so no types here. -- In the future we should definitely consider switching to TS.

return true;
}

const err = `The value must be a stringified ${type}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

is this error message applicable to all types of errors we get below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. This function comes from LB3 DataSource CLI command and it's the same error.

Here's the original reference code: https://github.com/strongloop/generator-loopback/blob/master/lib/helpers.js#L210-L228

Copy link
Contributor

@shimks shimks left a comment

Choose a reason for hiding this comment

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

I know this PR's been ongoing forever and you want to merge it asap, so sorry to 🌧 on your 🎪 xd.

Aside from my comments below, we also need to update boot/docs.json for the new files.

Also a question, what are the differences between datasources being bound to dataSource.config.NAME key vs dataSource.NAME key?


### Synopsis

Adds new [DataSource] class and config files to a LoopBack application.
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing link here

- Create a file with the connector configuration as follows:
`/datasources/${dataSource.dataSourceName}.datasource.json`
- Create a DataSource class which recieves the connector config using
[Dependency Injection](Dependency-injection.html) as follows:
Copy link
Contributor

Choose a reason for hiding this comment

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

Dependency-injection.md
I just realized this is probably really inconsistent among our docs and probably needs to be fixed at one point (outside of scope).

### Creating a DataSource

It is recommended to use the `lb4 datasource`
[command](DataSource-generator.html) provided by the CLI to generate a
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: wrap the link around lb4 source as well

`juggler.DataSource`. This class can be used to override the default
DataSource behaviour programaticaly. Note: The connector configuration stored
in the `.json` file is injected into this class using
[Dependency Injection](Dependency-inecjtion.html).
Copy link
Contributor

Choose a reason for hiding this comment

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

wrong spelling for the link and should me md

response.pipe(file);
file.on('finish', function() {
file.close();
transformConnectorJSON();
Copy link
Contributor

Choose a reason for hiding this comment

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

await here

* Ask for DataSource Name -- Must be unique
*/
promptArtifactName() {
debug('Prompting for artifact name');
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to not just super.promptArtifactName() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. super.promptArtifactName() prompts for utils.toClassName(this.artifactInfo.type) + 'class name:' but we need to just prompt for DataSource name and not class name for this generator.

@@ -10,3 +10,4 @@ packages/*/dist*
examples/*/dist*
**/package
.sandbox
packages/cli/generators/datasource/connectors.json
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should commit the latest connectors.json. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's pulled in as part of the build process and will be shipped with the appropriate version of the @loopback/cli because we run build before a release.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the file is added to .gitignore, npm publish will skip it by default. Please check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But we have the dist folders listed in .gitignore and I've verified that they still get published to npm.

Reason being npm searches for .gitignore in each project root and sub-directories ... but for us the .gitignore is at the top most level of the monorepo and not in any package hence no files are ignored.

I also ran npm pack within the cli package and can verify that connectors.json was packed.

const testlab = require('@loopback/testlab');

const expect = testlab.expect;
const TestSandbox = testlab.TestSandbox;
Copy link
Contributor

Choose a reason for hiding this comment

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

As a side, what's preventing us from using the import _ from _ pattern here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a JS project, not a TS project which is where the import _ from _. Support for this will come to JS eventually -- current status is Unstable as documented here: https://nodejs.org/api/esm.html

* Get a validate function for object/array type
* @param {String} type 'object' OR 'array'
*/
exports.validateStringObject = function(type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This utility function needs a test of its own

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

"debug": "^3.1.0",
"glob": "^7.1.2"
"glob": "^7.1.2",
"lodash": "^4.17.10"
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any changes with references to lodash. Probably left in as a mistake?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I was using it for determining the binding name but we since switched to the static dataSourceName property for that ... and as such no longer need lodash.

app.bind('datasources.config.db').to({
name: 'db',
connector: 'memory',
});
Copy link
Contributor

Choose a reason for hiding this comment

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

.inScope(BindingScope.SINGLETON) here? (IIUC from the comment above)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only a DataSource instance needs to be bound as a singleton. The config -- which is just the declarative JSON config can be bound as normal to the context.

@@ -155,7 +174,7 @@ export interface AppWithRepository extends Application {
repository(repo: Class<any>): void;
// tslint:disable-next-line:no-any
getRepository<R extends Repository<any>>(repo: Class<R>): Promise<R>;
dataSource(dataSource: juggler.DataSource, name?: string): void;
dataSource(dataSource: Class<juggler.DataSource>, name?: string): void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Class<juggler.DataSource> | juggler.DataSource

@@ -236,7 +255,7 @@ export class RepositoryMixinDoc {
* }
* ```
*/
dataSource(dataSource: juggler.DataSource, name?: string) {}
dataSource(dataSource: Class<juggler.DataSource>, name?: string) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Class<juggler.DataSource> | juggler.DataSource

@virkt25 virkt25 merged commit 9d48e68 into master Jun 14, 2018
@virkt25 virkt25 deleted the datasource branch June 14, 2018 00:01
var request = https
.get(URL, function(response) {
response.pipe(file);
file.on('finish', async function() {
Copy link
Member

Choose a reason for hiding this comment

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

@virkt25 this is not ideal.

  • If the async function fails, it returns a promise that will trigger unhandled rejection warning. I think you need to wrap the async function with a regular one that installs .catch handler.
  • I am not sure what happens if the file cannot be written - will response.pipe(file) forward the error from file to response stream? I don't think so. Have you considered using await pEvent(file, 'finish') instead?

Alternatively, I think it may be easier to give up on async/await in this script and switch to callbacks . You are already using callbacks in several places, I think the code will become more consistent if callbacks are used everywhere.

Also, IIUC this code, the async function download will immediately return a fulfilled promise - it's not waiting for the request to finish. If it's intentional, then please remove the async keyword.

* - Transforms description to message so it can be used by CLI directly
*/
async function transformConnectorJSON() {
let data = await readFileAsync(DEST, 'utf-8');
Copy link
Member

Choose a reason for hiding this comment

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

You are saving the downloaded JSON content to a file first, then read the file and overwrite its content in the second step. Have you considered passing the JSON content between download and transformConnectorJSON via in-memory value?

My recommendation is to use request-promise-native, it will take care of stream processing, error handling and flow control for you, and also add support for nice-to-have features like process.env.PROXY.

A mock-up implementation:

const data = await request(URL, {json: true});
const out = {};
data.forEach(...);
await writeFileAsync(DEST, JSON.stringify(out, null, 2));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had thought of using the request module but didn't want to introduce a dependency unnecessarily. Will add as a dev dependency and address this in a follow up PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[CLI] Generate DataSource
5 participants