Skip to content

feat(schema): Make schemas validation library independent and add TypeBox support#2772

Merged
daffl merged 12 commits intodovefrom
schema-validator-independent
Oct 7, 2022
Merged

feat(schema): Make schemas validation library independent and add TypeBox support#2772
daffl merged 12 commits intodovefrom
schema-validator-independent

Conversation

@daffl
Copy link
Copy Markdown
Member

@daffl daffl commented Oct 3, 2022

This pull request allows for the schema validation hooks to take a generic validator function. This was the only change necessary to allow making resolvers schema validation library independent. This means resolvers and validation hooks can also be used with most other validation libraries.

It also adds support for TypeBox (see the initial suggestion in #2762) as an option and the new default since it is much more intuitive to use for declaring JSON schemas.

Default application configurations schemas for both, JSON schema and Typebox have been added as well to further reduce the initial application boilerplate.

This also updates the CLI to create schemas without the now unnecessary schema wrapper and with separate hooks for validation and resolving. It also significantly reduces the generated code for new services and only generates a name.ts (with hooks and registration), a name.class.ts (with database class, related types and options) and name.schema.ts file (with schemas and resolvers) as discussed in #2760 and with @marshallswain.

A <name>.ts file with a schema now looks like this:

import { authenticate } from '@feathersjs/authentication'

import { hooks as schemaHooks } from '@feathersjs/schema'

import {
  messageDataValidator,
  messageQueryValidator,
  messageResolver,
  messageDataResolver,
  messageQueryResolver,
  messageExternalResolver
} from './messages.schema'

import type { Application } from '../../declarations'
import { MessageService, getOptions } from './messages.class'

export * from './messages.class'
export * from './messages.schema'

// A configure function that registers the service and its hooks via `app.configure`
export const message = (app: Application) => {
  // Register our service on the Feathers application
  app.use('messages', new MessageService(getOptions(app)), {
    // A list of all methods this service exposes externally
    methods: ['find', 'get', 'create', 'update', 'patch', 'remove'],
    // You can add additional custom events to be sent to clients here
    events: []
  })
  // Initialize hooks
  app.service('messages').hooks({
    around: {
      all: [authenticate('jwt')]
    },
    before: {
      all: [
        schemaHooks.validateQuery(messageQueryValidator),
        schemaHooks.validateData(messageDataValidator),
        schemaHooks.resolveQuery(messageQueryResolver),
        schemaHooks.resolveData(messageDataResolver)
      ]
    },
    after: {
      all: [schemaHooks.resolveResult(messageResolver), schemaHooks.resolveExternal(messageExternalResolver)]
    },
    error: {
      all: []
    }
  })
}

// Add this service to the service type index
declare module '../../declarations' {
  interface ServiceTypes {
    messages: MessageService
  }
}

The <name>.class.ts file (for e.g. MongoDB) like this:

import { MongoDBService } from '@feathersjs/mongodb'
import type { MongoDBAdapterParams } from '@feathersjs/mongodb'

import type { Application } from '../../declarations'
import type { Message, MessageData, MessageQuery } from './messages.schema'

export interface MessageParams extends MongoDBAdapterParams<MessageQuery> {}

// By default calls the standard MongoDB adapter service methods but can be customized with your own functionality.
export class MessageService extends MongoDBService<Message, MessageData, MessageParams> {}

export const getOptions = (app: Application) => {
  return {
    paginate: app.get('paginate'),
    Model: app.get('mongodbClient').then((db) => db.collection('message'))
  }
}

And the <name>.schema.ts file using TypeBox looks like this:

import { Type, jsonSchema, typebox, resolve } from '@feathersjs/schema'
import type { Static } from '@feathersjs/schema'

import type { HookContext } from '../../declarations'
import { dataValidator, queryValidator } from '../../schemas/validators'

// Schema for the basic data model (e.g. creating new entries)
export const messageDataSchema = Type.Object(
  {
    text: Type.String()
  },
  { $id: 'MessageData', additionalProperties: false }
)

export type MessageData = Static<typeof messageDataSchema>

export const messageDataValidator = jsonSchema.getDataValidator(messageDataSchema, dataValidator)

export const messageDataResolver = resolve<MessageData, HookContext>({
  properties: {}
})

export const messageSchema = Type.Intersect(
  [
    messageDataSchema,
    Type.Object({
      _id: Type.String()
    })
  ],
  { $id: 'Message', additionalProperties: false }
)

export type Message = Static<typeof messageSchema>

export const messageResolver = resolve<Message, HookContext>({
  properties: {}
})

export const messageExternalResolver = resolve<Message, HookContext>({
  properties: {}
})

// Schema for allowed query properties
export const messageQuerySchema = Type.Intersect([
  typebox.querySyntax(messageSchema),
  // Add additional query properties here
  Type.Object({})
])

export type MessageQuery = Static<typeof messageQuerySchema>

export const messageQueryValidator = jsonSchema.getValidator(messageQuerySchema, queryValidator)

export const messageQueryResolver = resolve<MessageQuery, HookContext>({
  properties: {}
})

A <name>.ts file without a schema (use at your own risk) looks like this:

import { authenticate } from '@feathersjs/authentication'

import type { Application } from '../../../../declarations'
import { TestingService, getOptions } from './test.class'

export * from './test.class'

// A configure function that registers the service and its hooks via `app.configure`
export const testing = (app: Application) => {
  // Register our service on the Feathers application
  app.use('path/to/test', new TestingService(getOptions(app)), {
    // A list of all methods this service exposes externally
    methods: ['find', 'get', 'create', 'update', 'patch', 'remove'],
    // You can add additional custom events to be sent to clients here
    events: []
  })
  // Initialize hooks
  app.service('path/to/test').hooks({
    around: {
      all: [authenticate('jwt')]
    },
    before: {
      all: []
    },
    after: {
      all: []
    },
    error: {
      all: []
    }
  })
}

// Add this service to the service type index
declare module '../../../../declarations' {
  interface ServiceTypes {
    'path/to/test': TestingService
  }
}

And the <name>.class.ts file like this:

import { MongoDBService } from '@feathersjs/mongodb'
import type { MongoDBAdapterParams } from '@feathersjs/mongodb'

import type { Application } from '../../../../declarations'

export type Testing = any
export type TestingData = any
export type TestingQuery = any

export interface TestingParams extends MongoDBAdapterParams<TestingQuery> {}

// By default calls the standard MongoDB adapter service methods but can be customized with your own functionality.
export class TestingService extends MongoDBService<Testing, TestingData, TestingParams> {}

export const getOptions = (app: Application) => {
  return {
    paginate: app.get('paginate'),
    Model: app.get('mongodbClient').then((db) => db.collection('testing'))
  }
}

@daffl daffl changed the title feat(schema): Make schemas validation library independent and update CLI generator feat(schema): Make schemas validation library independent and add TypeBox option Oct 4, 2022
Copy link
Copy Markdown
Member

@marshallswain marshallswain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking so good!

What else is left? It looks like we still need to generate TypeBox schemas?

@jchamb
Copy link
Copy Markdown

jchamb commented Oct 5, 2022

Ahh!! @daffl @marshallswain thanks 💯 for this!

@daffl daffl changed the title feat(schema): Make schemas validation library independent and add TypeBox option feat(schema): Make schemas validation library independent and add TypeBox support Oct 5, 2022
@daffl daffl merged commit 44172d9 into dove Oct 7, 2022
@daffl daffl deleted the schema-validator-independent branch October 7, 2022 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[@feathersjs/schema] Feathers schema associations using $ref may not be working

3 participants