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

How can extensions contribute Entities with persistence behavior and REST API #5476

Closed
5 tasks
bajtos opened this issue May 18, 2020 · 5 comments
Closed
5 tasks

Comments

@bajtos
Copy link
Member

bajtos commented May 18, 2020

This story is extracted from #4099, where we researched existing LoopBack 3 components and various techniques they use.

Now we need to write documentation for extension authors to show how to solve the following use cases in LoopBack 4:

  • Contribute custom entities (Application, Installation) to be persisted via CRUD, exposed via REST and possibly further customized by the app. Customization options include which datasource to use, the base path where REST API is exposed (e.g. /api/apps and /api/devices), additional fields (e.g. Application.tenantId) and changes in persistence behavior (e.g. via Operation Hooks)

  • Add a custom Operation Hook to given models, with a config option to enable/disable this feature. The list of models can be provided explicitly in the component configuration or obtained dynamically via introspection (e.g. all models having a "belongsTo" relation with the Group model). This may require How can extensions introspect application artifacts #5426 How can extensions introspect application artifacts.

  • Add new relations, e.g. between an app-provided entity User and a component-provided entity File. In this variant, the relation is added on small fixed number of models.

  • A model mixing adding new relations (hasMany ModelEvents), installing Operation Hooks (to generate model events/audit log entries), adding new Repository APIs (for working with related model events).

    (The mixin-based design may be difficult to accomplish in LB4, we may want to use introspection and a model setting instead. The trickier part is how to apply changes to models added after the component was mounted.)

  • For all models with a flag enabled in model settings, setup a custom afterRemote hook to modify the HTTP response (e.g. add additional headers).

In most cases, the new content will be useful to authors building new LB4 components too, therefore we should structure the content in two parts:

@bajtos
Copy link
Member Author

bajtos commented May 18, 2020

Let me share few ideas I considered while working on the spike.

We can also advise the following solution(s):

  1. Use lb4 import-lb3-models to import a LB3 model into a LB4 entity (see the
    instructions in the previous section).

  2. Use lb4 repository and lb4 controller to create a Repository and a
    Controller class inside the LB4 extension. It may be necessary to add a dummy
    datasource to make lb4 repository work.

  3. Remove @inject decorators from the scaffolded code, it will be up to the
    application to set up dependency injection using the artifacts from the
    application.

I see two options how to wire dependencies from the application.

Option A: Add wrapper files to the app.

Benefits: the repository and the controller will be seen by other lb4
commands.

Repository file:

// src/repositories/installation.repository.ts
import {InstallationRepository as BaseRepository} from 'loopback-component-push';

export class InstallationRepository extends BaseRepository {
  constructor(
    @inject('datasources.db') // <-- use the datasource name from your app
    dataSource: juggler.DataSource,
  ) {
    super(dataSource);
  }
}

Controller file:

// src/controllers/installation.controller.ts
import {InstallationController as BaseController} from 'loopback-component-push';
import {InstallationRepository} from '../repositories';

export class InstallationController extends BaseController {
  constructor(
    @repository(InstallationRepository)
    protected installationRepository: InstallationRepository,
    // if the controller requires request/response or other dependencies,
    // then we need to repeat them here :(
    @inject(RestBindings.Http.CONTEXT) private requestContext: RequestContext,
  ) {
    super(installationRepository, requestContext);
  }
}

Option B: wire dependencies inside the extension based on the config

Benefits: the user does not need to create any additional files.

  • In the repository implementation, don't decorate the first constructor
    argument.

  • In the controller implementation, use the same decorators as if you were
    writing a regular application controller.

Component code to put it all together:

import {inject} from '@loopback/core';
import {InstallationRepository} from './repositories';
import {InstallationController} from './controllers';

export class PushNotificationComponent implements Component {
  controllers = [InstallationController];
  repositories = [InstallationRepository];

  constructor(
    @inject(CoreBindings.APPLICATION_INSTANCE)
    private application: RestApplication,
    @config()
    config: PushNotificationConfig = {},
  ) {
    inject(config.dataSourceKey)(InstallationRepository, undefined, 0);
  }
}

I am not sure how this approach will work when the entities contributed by an
extension need to have a relation to Entities provided by the application. Is
the user going to provide Repository classes for related models via the config
too?

@bajtos
Copy link
Member Author

bajtos commented May 18, 2020

Add a custom Operation Hook to given models, with a config option to enable/disable this feature. The list of models can be provided explicitly in the component configuration or obtained dynamically via introspection (e.g. all models having a "belongsTo" relation with the Group model)

This is tricky because LB4 does not have first-class implementation of operation
hooks yet. The current workaround is to leverage juggler's Operation Hooks as
explained in
Migrating CRUD operation hooks.

@pbalan
Copy link

pbalan commented Jun 2, 2020

@bajtos Here's my original question

As per DRY, I'd like to contain my business logic in extension (loopback component). However, when defining models I'd like to specify belongsTo relationship to a property.

In LB3, I'd do the same by defining the following module and override via configuration:

module.exports = (blogModels, options) => {
  const debug = require('debug')('component:blog:postlike:model');
  const {userModel} = options;
  const postLikeModel = blogModels.PostLike;
  const postModel = blogModels.Post;

  // update relationships
  postModel.belongsTo(userModel,
    {as: 'userCreated', foreignKey: 'createdBy'});
  postModel.belongsTo(userModel,
    {as: 'userDeleted', foreignKey: 'deletedBy'});
  postModel.belongsTo(postModel,
    {as: 'post', foreignKey: 'postId'});

  let postLike = {};
  return postLike;
};

Also, I'd like to know how can I let developer specify the model in case he wants to specify a custom one.

In LB3, this would be done like this. https://github.com/pbalan/component-blog/blob/master/lib/models/index.js#L51-L63

const debug = require('debug')('component:blog');
const accessLogger = require('../middleware/access-logger');
const userContext = require('../middleware/user-context');
const logger = require('../middleware/logging');
const reqLogger = require('../middleware/request-logging');

module.exports = function componentBlog(app, options) {
  debug('initializing component');
  const {loopback} = app;
  options = options || {};

  let dataSource = options.dataSource;
  /* istanbul ignore if */
  if (typeof dataSource === 'string') {
    dataSource = app.dataSource[dataSource];
  }
  const blogModels = require('./component-blog-models')(dataSource);
  const userModel = loopback.findModel(options.userModel) ||
      loopback.getModelByType(loopback.User);
  debug('User model: %s', userModel.modelName);

  const venueModel = loopback.findModel(options.venueModel);
  // debug('Venue model: %s', venueModel.modelName);

  // Initialize middleware
  app.middleware('initial:before', logger());
  app.middleware('initial:before', reqLogger());
  app.middleware('auth:after', userContext());
  app.middleware('routes:before', accessLogger());

  let users = {};
  let venue = {};

  let internalConfig = {
    userModel: userModel,
    venueModel: venueModel,
  };

  // specific to app
  const post = require('./Post')(blogModels, internalConfig);
  const postLike = require('./PostLike')(blogModels, internalConfig);
  const postMention = require('./PostMention')(blogModels, internalConfig);
  const postShare = require('./PostShare')(blogModels, internalConfig);
  const postComment = require('./PostComment')(blogModels, internalConfig);
  const commentComment =
    require('./CommentComment')(blogModels, internalConfig);
  const postMedia = require('./PostMedia')(blogModels, internalConfig);
  const blogReported = require('./BlogReported')(blogModels, internalConfig);

  let customModels = options.models || {};
  let models = {
    user: customModels.users || users,
    venue: customModels.venue || venue,
    blogReported: customModels.blogReported || blogReported,
    post: customModels.post || post,
    postLike: customModels.postLike || postLike,
    postMention: customModels.postMention || postMention,
    postShare: customModels.postShare || postShare,
    postComment: customModels.postComment || postComment,
    commentComment: customModels.commentComment || commentComment,
    postMedia: customModels.postMedia || postMedia,
  };

  return models;
};

Then, in a boot script, we could provide the necessary overrides.

https://github.com/pbalan/component-blog/blob/master/test/fixtures/simple-app/server/boot/003-component-blog.js#L7

module.exports = function blog(app) {
  var blog = require('../../../../../lib');

  var options = {
    // custom user model
    userModel: 'user', // specify your custom user model
    uploadMediaUrl: '/api/containers/blog-media/upload',
    baseUrl: 'http://0.0.0.0:3000',

    // Data source for metadata persistence
    dataSource: app.dataSources.db,
  };
  app.set('component-blog', options);
  blog(app, options);
};

@stale
Copy link

stale bot commented Dec 25, 2020

This issue has been marked stale because it has not seen activity within six months. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository. This issue will be closed within 30 days of being stale.

@stale stale bot added the stale label Dec 25, 2020
@stale
Copy link

stale bot commented Jul 14, 2021

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.

@stale stale bot closed this as completed Jul 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants