Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Merged
merged 12 commits into from
Oct 7, 2022

Conversation

daffl
Copy link
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
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?

packages/cli/src/app/templates/schemas.tpl.ts Outdated Show resolved Hide resolved
packages/cli/src/authentication/index.ts Show resolved Hide resolved
packages/cli/src/service/index.ts Show resolved Hide resolved
packages/cli/src/service/index.ts Show resolved Hide resolved
packages/cli/src/service/index.ts Show resolved Hide resolved
packages/cli/src/service/service.tpl.ts Outdated Show resolved Hide resolved
packages/cli/src/service/service.tpl.ts Outdated Show resolved Hide resolved
packages/cli/src/service/service.tpl.ts Outdated Show resolved Hide resolved
packages/schema/src/hooks/resolve.ts Show resolved Hide resolved
@jchamb
Copy link

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
# for free to join this conversation on GitHub. Already have an account? # 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