-
-
Notifications
You must be signed in to change notification settings - Fork 1
RFC - a place to discuss and refine the API #20
Comments
I would say that Joi is to The difference to the lower level validation library is what schema definitions add:
A lot of this can already be done in individual hooks (in fact, in a Feathers app itself, There are now npm packages for almost every schema format combination (JSON schema to SQL, GraphQL to SQL, SQL to GraphQL, JSON schema to GraphQL, Joi to JSON schema) but so far there does not seem to be anything that addresses the actual underlying problem of providing a JavaScript way to define a schema that is not tied to an ORM, database or specific validation library. |
Thanks @daffl, that helps a lot and I'm glad we are kind of piggybacking on an established library, especially Joi now that it works in the browser (since September '19). Here are my initial comments (I tried to keep it short as possible :/):
|
I like @DesignByOnyx notes on user: {
type: User,
resolve: async (todo, context) => {
// The dev needs to bypass the DB adapter by plucking off invalid query params and stashing
// theme somewhere for use in after hooks, here it would have been done in a before hook
// and placed on a prop called stashedQuery
const { $populate } = context.params.stashedQuery
const { params, app } = context;
if ($populate && $populate.includes('user')) {
return app.service('users').get(todo.userId, params);
}
}
} Having that baked in would be cool, but it may be quite difficult for it to play nicely with DB adapters unless I am missing something? I am also curious what happens when the user does not return anything from the resolve function, either because they used an if-block like above or if the resolve function just doesn't return anything. I assume that the result would just be resolver () => (todo, context) => {
todo.user = await context.app.users.get(todouser_.id);
return todo;
} So, I made the API such that you |
// Here `validator` is a string validator
@property<Joi.StringValidator> (validator => validator.email().required())
email: string;
@DaddyWarbucks These were my thoughts on resolvers (getters) and value (setters): const User = schema({
name: 'user'
}, {
password: {
type: Type.string().required(),
async value (value, user, context) {
const hashedPassword = await bcrypt(value);
return hashedPassword;
},
async resolve (value, user, context) {
// Hide password for external call
if (context.params.provider) {
return undefined;
}
return value;
}
},
permission: {
type: Type.string(),
async value (value, user, context) {
const { user } = context.params;
// Only an admin can change the user permission
// With authentication there always will be a user for external requests
if (user && user.permission !== 'admin') {
return undefined;
}
return value;
}
}
}); If you return |
Another feature I think we may want to explore is some "partial" validation. Often we need to run some hooks that only require part of the data in order to get the rest of the data, before being validated. For example this following hook is used to validate some data before sending it off to Cloudinary. Cloudinary then sends back its results which are merged with the original data and then validated and persisted. The following code is from very similar setup to feathers-schema where I store the schema on the service, etc. const sendToCloudinary = async context => {
const schema = context.service.schema.clonePartial([
'entity_id',
'entity_type',
'document_type'
]);
// I want to throw the same ValidationError here as what would be thrown from the
// feathers-schema validation hook, but I am not ready to validate the whole thing yet
const cloudinaryStuff = await schema.validateData(context.data);
const cloudinaryRes = await Cloudinary.doTheThing(cloudinaryStuff);
context.data = { ...context.data, cloudinaryRes };
// Now the context.data is ready to be validated against the whole schema
return context;
}; Note the This could be handled manually, but it requires the developer to create an additional Schema and also manually throw the Another suggestion, we may also want to let the developer pass in the schema via props. For example change this line: https://github.com/feathersjs/schema/blob/c9f345b23263c157efc32224f82eb1fc58e714ff/packages/schema-hooks/src/index.ts#L19 const schema = getSchema(context.params.schema || service?.options?.Schema || service.Schema); Perhaps that is a little too close to a validation hook rather than a "This schema describes this service" type of thing, but just a suggestion. |
Hi, I'm not very familiar with the scope and goals of @featherjs/schema, but wanted to share a couple of learnings from using ajv with Feathers. Ajv async validatorsAjv does support async validators, I use that feature to check things based on context or database. E.g. I've build custom validation keywords such as:
This isn't to try and sway you away from yup, just wanted to point out that ajv supports this. The main difference is that it doesn't support inline validation functions (that I know of). For those cases I tend to do extra work in my hooks. Read vs create vs updateFor all my resources, I tend to need 3 things:
For that I actually export each of these definitions separately. I was wondering how you were thinking about handling that here. A typical schema file (e.g. foo.schemas.js) ends up looking like this: const fields = [
'id',
'personId',
'name',
'email',
'phoneNumber',
'formattedPhoneNumber', // derived in a hook
'relationship',
'primary',
'createdAt',
'updatedAt',
]
const patch = {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', nullable: true },
phoneNumber: { type: 'string', nullable: true },
relationship: { type: 'string', nullable: true },
},
additionalProperties: false,
}
const create = {
...patch,
properties: {
...patch.properties,
personId: { type: 'string' },
primary: { type: 'boolean' },
},
required: ['personId', 'name'],
}
module.exports.create = create
module.exports.patch = patch
module.exports.read = {
owner: fields,
admin: fields,
user fields,
self: fields,
} |
@daffl - I totally agree with you on this point:
I agree and would like to avoid that at all costs. I guess my concern was more in describing the relationship of the data rather than how to hydrate the data. These are two separate things, and if the schema focused on describing the relationship, then tools/extensions/plugins can be built to take care of "how" the data is hydrated. This ties into my comment about AJV below. @DaddyWarbucks - partial schema validation is possible with Joi, though the way they recommend doing it is kind of wonky to me. I second your recommendations about providing a @KidkArolis - I am with you on AJV, which (for those who don't know) is a tool that compiles JSON Schema with the ability to provide custom extensions. My biggest problem with Joi/Yup is that they are so rich that it's not shareable with different languages. This is particularly painful if you have a non-JavaScript backend service with which you need to stay in sync. @daffl mentioned earlier that there are tools to convert in every direction, but in practice I have found that these tools are basically useless. JSON Schema is the "least common denominator" and the most interoperable due to its static nature. Individual teams can share and extend these static schemas in their own native languages. If done thoughtfully, these language-specific extensions can be tested in a uniform way with uniform error messaging. The same cannot be said for sharing Joi/Yup schemas. If I could have my druthers, I would make the core of feathers-schema based on AJV, encouraging users to describe their models with static JSON Schema, writing custom "resolver" functions and other extensions separate from the static schemas. However, I'm totally fine with sticking with Joi due to the ecosystem surrounding it. |
Just wondering, is the RFC taking into consideration the plans to make the core of Feathersjs compatible with deno? Will Feathersjs + deno users have to wait for Joi/Ajv to be released on deno too? |
I am working on making the core libraries Deno compatible and seeing how much work that already is (I basically have to create our own Deno and Node compatible replacements for every module that's being used) this is probably not going to happen right away for schema definitions. As mentioned in feathersjs/feathers#1964 initial Deno support will focus on core functionality only and not include any module that has significant third party dependencies. |
I retract my previous statement. Given that Joi's future is somewhat uncertain I opted for making schema core validation library independent and have it stay flexible enough to use it with whichever validation library you choose. The core concept here are schema functions ( import { schema, string, number, boolean } from '@feathersjs/schema';
const User = schema({
name: 'users'
}, {
email: {
type: string(),
resolve: async (email, user, context, metadata) {
if (!context.params.user.isAdmin) {
// Non admins can not see the email
return undefined;
}
return email;
}
},
age: {
type: number(),
value: async (age, user, context, metadata) {
if (age < 0) {
throw new Error('Age can not be negative');
}
return age;
}
},
enabled: {
type: boolean(),
value: async (enabled, user, context, metadata) {
if (context.params.provider) {
throw new Error('Can not be set externally');
}
return enabled;
}
}
}); It is possible to make chainable schema functions (e.g. |
I trust that you don't make any decision lightly, but this starts to tread into dangerous maintenance territory. It also feels kind of can-define'y in a not-so-good way: What's the difference between In an effort to not be redundant, my previous comment highlights my thoughts around AJV and the separation of 1) static schema descriptions (JSON Schema) and 2) how instances get hydrated/validated. I would greatly encourage that feathers-schema keeps from conflating those concepts (which is what can-define suffered from - not trying to pick on that tool, I just know you're intimate with it). |
To answer the (easier) first two questions:
I really don't feel static schema descriptions are the best way to do this. JSON schema isn't bad but it's one of those things that developers (sometimes) use but don't actually like using:
Do you have an example why it is bad to conflate those two things? To me they really belong together and it is basically what e.g. GraphQLJS is doing (one might say successfully). I think can-define's weakness was more that it wasn't really universally usable and not targeting anything useful (you basically had to re-define your server schema in a custom DSL again on the client). |
Maybe const UserSchema = schema({
name: 'user'
}, {
email: {
type: string()
},
password: {
type: string(),
value: async password => {
// Hash the password before saving to the database
return bcrypt(password);
},
resolve: async (password, user, context) {
// Never return the actual value for external requests
if (context.params.provider) {
return undefined;
}
return password;
}
}
}); This also allows assembling queries with more complex conditions in one place: const MessageQuerySchema = schema({
name: 'message-query'
}, {
userId: {
type: string(),
value: async (userId, query, context) => {
const { user } = context.params;
// Non admins can only query their own messages
return user.isAdmin ? userId : user.id;
}
},
private: {
type: boolean(),
value: async (private, query, context) => {
const { user } = context.params;
// Free plan members can never query private messages
return user.plan === 'free' ? false : private;
}
}
}); |
value -> write
resolve -> read
Everything async might be slower for validation purposes.. (especially per
field).
Fastify uses json schemas:
https://www.fastify.io/docs/latest/Validation-and-Serialization/, and its
swappable with other libs. Although I haven't used fastify / explored
enough to comment further. I just had a lot of success with ajv in my own
app. It did sometimes feel like “I didn’t enjoy” using it, things like
conditional logic was tricky, sometimes i moved parts of validation to a
hook. But overall a lot of it made sense, and created consistency.
…On Tue, 7 Jul 2020 at 09:39, David Luecke ***@***.***> wrote:
Maybe value and resolve is not super aptly named (set and get doesn't
really fit though either) but the user schema is a good example for
unifying things that currently have to be done with (imo clunky)
hashPassword and protect hooks:
const UserSchema = schema({
name: 'user'}, {
email: {
type: string()
},
password: {
type: string(),
value: async password => {
// Hash the password before saving to the database
return bcrypt(password);
},
resolve: async (password, user, context) {
// Never return the actual value for external requests
if (context.params.provider) {
return undefined;
}
return password;
}
}});
This also allows assembling queries with more complex conditions in one
place possible:
const MessageQuerySchema = schema({
name: 'message-query'}, {
userId: {
type: string(),
value: async (userId, query, context) => {
const { user } = context.params;
// Non admins can only query their own messages
return user.isAdmin ? userId : user.id;
}
},
private: {
type: boolean(),
value: async (private, query, context) => {
const { user } = context.params;
// Free plan members can never query private messages
return user.plan === 'free' ? false : private;
}
}});
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#20 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACPGWB4VLADNKPOIBYN5MLR2LNMFANCNFSM4MMSPTCA>
.
|
Sorry for being a little slow but I just wanted to let you know that you fine folks are right 😄 The thing I'd like to solve here is context based resolving of properties, not re-inventing all of validation and coercion. I think a pluggable solution (similar to Feathers transport adapters) with JSON schema as the internal format should be doable here. |
So a bit of a distraction from value/resolve, I was just pondering this and thought it might be a useful fit into feathers-schema. Let's say we have a database with three services:
How would one easily/cleanly query all the students that have classes in one room, or what rooms a student has classes in? (assuming we don't care about what classes for this query) You could query the classes, and then pull the students/rooms, but this can lead to a lot of boilerplate code in the client. I've kind of gotten around this by implementing hooks on find that will detect a field and swap it out, e.g. detect a roomId field in a find on the students service, do a find for all classes with that roomId, and then modify the student query to be like What would be cool is the ability to define a "virtual ID" of sorts in the schema, that has a definition of where the relationship is actually stored. Say on the students service there is a roomVIds field (or similar) which isn't actually stored in the database but queriable and populates when resolved. Food for thought. |
Thank you again for the feedback everybody! I moved the discussion with the latest proposal to feathersjs/feathers#2312 and development to https://github.com/feathersjs/feathers/tree/schema since it currently makes more sense to have it in core vs. in a separate repository. I will archive this repository for now but we may bring it back later if it turns out to be something that could be generally useful. |
The "RFC" (request for comments) is intended to provide a place to discuss features and such for this library prior to v1. The goal of this is to collect insight from different developers and discuss use cases we have all encountered in the wild. In particular, I'd like to tap into developer experiences with multiple schema and validation libraries such as yup, joi, AJV, JSON Schema, sequelize, mongoose, et al.
Remember to be kind and focus on providing constructive feedback.
The text was updated successfully, but these errors were encountered: