This issue consolidates the work from feathersjs/schema and the discussion from https://github.com/feathersjs/schema/issues/20 into an up to date proposal in the main repository since I think for now it makes the most sense to include the @feathersjs/schema
here (with the option to turn it into a standalone module later once it has been used for a bit).
Problem
A very common problem we are seeing in web application development at the moment, especially in the NodeJS world, is that data model definitions are duplicated in many different places, be it ORM models, JSON schema validators, Swagger/OpenAPI specifications, TypeScript type definitions or GraphQL schemas. It is difficult to keep them synchronized and any new protocol or database is probably going to need the same work done again.
You can find converters from almost any specific format to any other format on npm but there are many challenges from relying on a compilation step (generating duplicate code that doesn't really have to exist in the first place) to only covering a small subset of functionality. It also appeared a majority of those modules is no longer actively maintained. ORMs are trying to address the challenge by locking you into their way of doing things with database or ORM specific idiosynchrasies dictating how flexible you can be in defining your model.
Feathers schema
Feathers schema provides a common schema definition format for JavaScript and TypeScript that allows to target different formats like GraphQL, JSON schema (Swagger/OpenApi), SQL tables, validations and more with a single definition. Similar to how Feathers services allow different transport mechanisms to access their API without having to change your application code, schemas are Feathers answer to address the ever expanding list of data schema definition and validation formats.
It does not only address the definition of data types but also how to resolve them within the context of your application. This is a different approach to most ORMs where you define your data model based on the database (or other ORM specific convention). Some examples where schemas are useful:
- Ability to add read- and write permissions on the property level
- Complex validations and type conversions
- Shared model definition types between the client and server (when using TypeScript)
- Associations and loader optimizations
- Query string conversion, validation (no more type mismatches) and protections
- Automatic API docs
How it works
@feathersjs/schema
uses JSON schema as the main definition format with the addition of property resolvers. Resolvers are functions that are called with the application context and return the actual value of a property. This can be anything from the user associated to a message to the hashed password that should be stored in the database.
Schema definitions
import { schema } from '@feathersjs/schema';
const UserSchema = schema({
type: 'object',
required: [ 'email', 'password' ],
additionalProperties: false,
properties: {
id: { type: 'number' },
email: { type: 'email' },
password: {
type: 'string',
minLength: 8
}
}
});
const MessageSchema = schema({
type: 'object',
required: [ 'text' ],
additionalProperties: false,
properties: {
id: { type: 'number' },
userId: { type: 'number' },
text: {
type: 'string',
maxLength: 400
}
}
});
// This defines a `data` schema (for `create`, `update` and `patch`)
// based on UserSchema that hashes the password before putting it into the db
const UserDataSchema = schema(UserSchema, {
properties: {
password: {
resolve (password) {
return hashPassword(password);
}
}
}
});
// A new schema based on MessageSchema that includes the associated user
const MessageResultSchema = schema(MessageSchema, {
properties: {
user: {
resolve (value, message, context) {
const { app, params } = context;
return app.service('users').get(message.userId, params);
}
}
}
});
TypeScript
TypeScript types can be inferred from schema definitions. This is where to me TypeScript finally is starting to make real sense πΈ You get type definitions, dynamic validations and resolvers all in one place:
import { schema, Infer } from '@feathersjs/schema';
const UserSchema = schema({
type: 'object',
required: [ 'email', 'password' ],
additionalProperties: false,
properties: {
id: { type: 'number' },
email: { type: 'string' },
password: {
type: 'string',
minLength: 8
}
}
} as const);
type User = Infer<typeof UserSchema>;
const user = UserSchema.resolve({
email: '[email protected]',
password: 'supersecret'
});
Both the User
type and user
variable will have a type of the following interface:
type User = {
id?: number;
email: string;
password: string;
}
Using schemas with Feathers
In a Feathers application schemas can be passed when registering a service via the options introduced in v5. Different schemas can be passed for validating data
and params.query
as well as resolving the result
object returned to the client.
import { feathers } from '@feathersjs/feathers';
const app = feathers();
// One schema for everything
app.use('/messages', new MessageService(), {
schema: MessageSchema
});
// Different schema for `result`, `data` and `query`
app.use('/messages', new MessageService(), {
schema: {
result: MessageResultSchema,
data: MessageDataSchema,
query: MessageQuerySchema
}
});
// With override for a specific method
app.use('/messages', new MessageService(), {
schema: {
result: MessageResultSchema,
data: MessageDataSchema,
query: MessageQuerySchema,
methods: {
patch: {
data: MessagePatchSchema
}
}
}
});
Feedback wanted
Development is currently happening in the schema branch and will eventually be moved to dove. I will start adding documentation in the Dove API docs section once a pre-release PR is ready. Many thanks already to the feedback to @DaddyWarbucks, @DesignByOnyx and @mrfrase3, please continue to challenge my assumptions if you see anything else that this proposal is missing π
Feature Schema