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 lineapp.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 withoutfeathers-rest
have to be updated manually) - Remove all
.filter
imports and calls toservice.filter
which has been replaced by channel functionality
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.
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: []
}
};
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:
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 |
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 |
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 |
With a better focus on Feathers core, the repositories, documentation and guides for non-core module have been moved to more appropriate locations:
- Non-core modules have been moved to the feathersjs-ecosystem and feathers-plus organizations
- Database adapter specific documentation can now be found in the respective repositories readme. Links to the repositories can be found in the database adapters chapter
- The
feathers-hooks-common
documentation can be found at feathers-plus.github.io/v1/feathers-hooks-common/ - Offline and authentication-management documentation can also be found at feathers-plus.github.io
- The Ecosystem page now points to awesome-feathersjs
@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());
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}!`;
}
}
});
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));
}
});
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());
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.
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.
- 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
andservice.after
have been replaced with a singleapp.hooks({ before, after })
app.service(path)
only returns a service and cannot be used to register a new service anymore (viaapp.service(path, service)
). Useapp.use(path, service)
instead.- Route parameters which were previously added directly to
params
are now inparams.route
- Express middleware like
feathers.static
is now located inconst express = require('@feathersjs/express')
usingexpress.static
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.
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);
}
});
});
app.hooks({
before(hook) {
Object.assign(hook.params, hook.params.route);
return hook;
}
})
app.mixins.push(service => {
service.mixin({
before(before) {
return this.hooks({ before });
},
after(after) {
return this.hooks({ after });
},
})
});