Skip to content

Latest commit

 

History

History
467 lines (353 loc) · 16.1 KB

migrating.md

File metadata and controls

467 lines (353 loc) · 16.1 KB

Feathers v3 (Buzzard)

Quick upgrade

To quickly upgrade any Feathers plugin or application you can use the upgrade command from the new CLI. First, if you have it installed, uninstall the old feathers-cli:

npm uninstall feathers-cli -g

Then install @feathersjs/cli and upgrade a project:

npm install @feathersjs/cli -g
cd path/to/project
feathers upgrade

In short (for more details see below) this will:

  • Upgrade all core packages to the new scoped package names and their latest versions
  • Remove all feathers-hooks imports and single line app.configure(hooks()); (chained .configure(hooks()) calls will have to be removed manually))
  • Add Express compatibility to any application that uses feathers-rest (other Feathers apps without feathers-rest have to be updated manually)
  • Remove all .filter imports and calls to service.filter which has been replaced by channel functionality

Adding channels

If you are using real-time (with Socket.io or Primus), add the following file as src/channels.js:

module.exports = function(app) {
  if(typeof app.channel !== 'function') {
    // If no real-time functionality has been configured just return
    return;
  }

  app.on('connection', connection => {
    // On a new real-time connection, add it to the anonymous channel
    app.channel('anonymous').join(connection);
  });

  app.on('login', (authResult, { connection }) => {
    // connection can be undefined if there is no
    // real-time connection, e.g. when logging in via REST
    if(connection) {
      // Obtain the logged in user from the connection
      // const user = connection.user;
      
      // The connection is no longer anonymous, remove it
      app.channel('anonymous').leave(connection);

      // Add it to the authenticated user channel
      app.channel('authenticated').join(connection);

      // Channels can be named anything and joined on any condition 
      
      // E.g. to send real-time events only to admins use
      // if(user.isAdmin) { app.channel('admins').join(connection); }

      // If the user has joined e.g. chat rooms
      // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
      
      // Easily organize users by email and userid for things like messaging
      // app.channel(`emails/${user.email}`).join(channel);
      // app.channel(`userIds/$(user.id}`).join(channel);
    }
  });

  app.publish((data, hook) => { // eslint-disable-line no-unused-vars
    // Here you can add event publishers to channels set up in `channels.js`
    // To publish only for a specific event use `app.publish(eventname, () => {})`

    // e.g. to publish all service events to all authenticated users use
    return app.channel('authenticated');
  });

  // Here you can also add service specific event publishers
  // e..g the publish the `users` service `created` event to the `admins` channel
  // app.service('users').publish('created', () => app.channel('admins'));
  
  // With the userid and email organization from above you can easily select involved users
  // app.service('messages').publish(() => {
  //   return [
  //     app.channel(`userIds/${data.createdBy}`),
  //     app.channel(`emails/${data.recipientEmail}`)
  //   ];
  // });
};

And require and configure it in src/app.js (note that it should be configured after all services so that channels.js can register service specific publishers):

const channels = require('./channels');

// After `app.configure(services)`
app.configure(channels);

Very important: The channels.js file shown above will publish all real-time events to all authenticated users. This is already safer than the previous default but you should carefully review the channels documentation and implement appropriate channels so that only the right users are going to receive real-time events.

Once you migrated your application to channels you can remove all <servicename>.filter.js files.

Protecting fields

Feathers v3 has a new mechanism to ensure that sensitive information never gets published to any client. To protect always protect the user password, add the protect hook in src/services/users/users.hooks.js instead of the remove('password') hook:

const { hashPassword } = require('@feathersjs/authentication-local').hooks;
const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks;

module.exports = {
  before: {
    all: [],
    find: [ authenticate('jwt') ],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },

  after: {
    all: [
      // Make sure the password field is never sent to the client
      // Always must be the last hook
      protect('password')
    ],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },

  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
};

@feathersjs npm scope

All Feathers core modules have been moved to the @feathersjs npm scope. This makes it more clear which modules are considered core and which modules are community supported and also allows us to more easily manage publishing permissions. The following modules have been renamed:

Main Feathers

Old name Scoped name
feathers @feathersjs/feathers
feathers-cli @feathersjs/cli
feathers-commons @feathersjs/commons
feathers-rest @feathersjs/express/rest
feathers-socketio @feathersjs/socketio
feathers-primus @feathersjs/primus
feathers-errors @feathersjs/errors
feathers-configuration @feathersjs/configuration
feathers-socket-commons @feathersjs/socket-commons

Authentication

Old name Scoped name
feathers-authentication @feathersjs/authentication
feathers-authentication-jwt @feathersjs/authentication-jwt
feathers-authentication-local @feathersjs/authentication-local
feathers-authentication-oauth1 @feathersjs/authentication-oauth1
feathers-authentication-oauth2 @feathersjs/authentication-oauth2
feathers-authentication-client @feathersjs/authentication-client

Client side Feathers

Old name Scoped name
feathers/client @feathersjs/feathers
feathers-client @feathersjs/client
feathers-rest/client @feathersjs/rest-client
feathers-socketio/client @feathersjs/socketio-client
feathers-primus/client @feathersjs/primus-client
feathers-authentication/client @feathersjs/authentication-client

Documentation changes

With a better focus on Feathers core, the repositories, documentation and guides for non-core module have been moved to more appropriate locations:

Framework independent

@feathersjs/feathers v3 is framework independent and will work on the client and in Node out of the box. This means that it is not extending Express by default anymore.

Instead @feathersjs/express provides the framework bindings and the REST provider (previously feathers-rest) in either require('@feathersjs/express').rest or @feathersjs/express/rest. @feathersjs/express also brings Express built-in middleware like express.static and the recently included express.json and express.urlencoded body parsers. Once a Feathers application is "expressified" it can be used like the previous version:

Before

const feathers = require('feathers');
const bodyParser = require('body-parser');
const rest = require('feathers-rest');
const errorHandler = require('feathers-errors/handler');

const app = feathers();

app.configure(rest());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Register an Express middleware
app.get('/somewhere', function(req, res) {
  res.json({ message: 'Data from somewhere middleware' });
});
// Statically host some files
app.use('/', feathers.static(__dirname));

// Use a Feathers friendly Express error handler
app.use(errorHandler());

Now

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

// Create an Express compatible Feathers application
const app = express(feathers());

// Add body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize REST provider (previous in `feathers-rest`)
app.configure(express.rest());

// Register an Express middleware
app.get('/somewhere', function(req, res) {
  res.json({ message: 'Data from somewhere middleware' });
});
// Statically host some files
app.use('/', express.static(__dirname));

// Use a Feathers friendly Express error handler
app.use(express.errorHandler());

Hooks in core

The feathers-hooks plugin is now a part of core and no longer has to be imported and configured. All services will have hook functionality included right away. Additionally it is now also possible to define different data that should be sent to the client in hook.dispatch which allows to properly secure properties that should not be shown to a client.

Before

const feathers = require('feathers');
const hooks = require('feathers-hooks');

const app = feathers();

app.configure(hooks());
app.use('/todos', {
  get(id) {
    return Promise.resolve({
      message: `You have to do ${id}`
    });
  }
});

app.service('todos').hooks({
  after: {
    get(hook) {
      hook.result.message = `${hook.result.message}!`;
    }
  }
});

Now

const feathers = require('feathers');

const app = feathers();

app.use('/todos', {
  get(id) {
    return Promise.resolve({
      message: `You have to do ${id}`
    });
  }
});

app.service('todos').hooks({
  after: {
    get(hook) {
      hook.result.message = `${hook.result.message}!`;
    }
  }
});

Event channels and publishing

Previously, filters were used to run for every event and every connection to determine if the event should be sent or not.

Event channels are a more secure and performant way to define which connections to send real-time events to. Instead of running for every event and every connection you define once which channels a connection belongs to when it is established or authenticated.

// On login and if it is a real-time connectionn, add the connection to the `authenticated` channel
app.on('login', (authResult, { connection }) => {
  if(connection) {
    const { user } = connection;

    app.channel('authenticated').join(connection);
  }
});

// Publish only `created` events from the `messages` service
app.service('messages').publish('created', (data, context) => app.channel('authenticated'));

// Publish all real-time events from all services to the authenticated channel
app.publish((data, context) => app.channel('authenticated'));

To only publish to rooms a user is in:

// On login and if it is a real-time connection, add the connection to the `authenticated` channel
app.on('login', (authResult, { connection }) => {
  if(connection) {
    const { user } = connection;

    // Join `authenticated` channel
    app.channel('authenticated').join(connection);

    // Join rooms channels for that user
    rooms.forEach(roomId => app.channel(`rooms/${roomId}`).join(connection));
  }
});

Better separation of client and server side modules

Feathers core was working on the client and the server since v2 but it wasn't always entirely clear which related modules should be used how. Now all client side connectors are located in their own repositories while the main Feathers repository can be required the same way on the client and the server.

Before

const io = require('socket.io-client');
const feathers = require('feathers/client');
const hooks = require('feathers-hooks');
const socketio = require('feathers-socketio/client');
const auth = require('feathers-authentication-client');

const socket = io();
const app = feathers()
  .configure(hooks())
  .configure(socketio(socket))
  .configure(auth());

Now

const io = require('socket.io-client');
const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');
const auth = require('@feathersjs/authentication-client');

const socket = io();
const app = feathers()
  .configure(socketio(socket))
  .configure(auth());

Node 6+

The core repositories mentioned above also have been migrated to be directly usable (e.g. when npm installing the repository as a Git/GitHub dependency) without requiring a Babel transpilation step.

Since all repositories make extensive use of ES6 that also means that Node 4 is no longer supported.

Also see /feathers/issues/608.

A new Socket message format

The websocket messaging format has been updated to support proper error messages when trying to call a non-existing service or method (instead of just timing out). Using the new @feathersjs/socketio-client and @feathersjs/primus-client will automatically use that format. You can find the details in the Socket.io client and Primus client documentation.

Note: The old message format is still supported so the clients do not have to be updated at the same time.

Deprecations and other API changes

  • Callbacks are no longer supported in Feathers service methods. All service methods always return a Promise. Custom services must return a Promise or use async/await.
  • service.before and service.after have been replaced with a single app.hooks({ before, after })
  • app.service(path) only returns a service and cannot be used to register a new service anymore (via app.service(path, service)). Use app.use(path, service) instead.
  • Route parameters which were previously added directly to params are now in params.route
  • Express middleware like feathers.static is now located in const express = require('@feathersjs/express') using express.static

Backwards compatibility polyfills

Besides the steps outlined above, existing hooks, database adapters, services and other plugins should be fully compatible with Feathers v3 without any additional modifications.

This section contains some quick backwards compatibility polyfills for the breaking change that can be used to make the migration easier or continue to use plugins that use deprecated syntax.

Basic service filter

This is a basic emulation of the previous event filter functionality. It does not use promises or allow modifying the data (which should now be handled by setting hook.dispatch).

app.mixins.push(service => {
  service.mixin({
    filter(eventName, callback) {
      const args = callback ? [ eventName ] : [];

      // todos.filter('created', function(data, connection, hook) {});
      if(!callback) {
        callback = eventName;
      }

      // A publisher function that sends to the `authenticated`
      // channel that we defined in the quick upgrade section above
      args.push((data, hook) => app.channel('authenticated')
        .filter(connection =>
          callback(data, connection, hook)
        )
      );

      service.publish(... args);
    }
  });
});

Route parameters

app.hooks({
  before(hook) {
    Object.assign(hook.params, hook.params.route);

    return hook;
  }
})

.before and .after hook registration

app.mixins.push(service => {
  service.mixin({
    before(before) {
      return this.hooks({ before });
    },

    after(after) {
      return this.hooks({ after });
    },    
  })
});