feathersjs / feathers

The API and real-time application framework
https://feathersjs.com
MIT License
15.04k stars 751 forks source link

[FR] Build resolvers into hooks #3222

Closed FossPrime closed 1 year ago

FossPrime commented 1 year ago

Summary

It would standardize things more and clean up boilerplate and ease learning if we had Resolvers in the hooks DSL-like format. This is now possible as of TypeScript 4.4.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#symbol-and-template-string-pattern-index-signatures

Proposed format

import createDataSchema from './schema.json' assert { type: 'json' }

const hooks = {
  before: {
    createDataSchema,
    findQuery: {
      company: (v, user) => user?.role === 'super-admin' ? v : user.company
    }
  },
  after: {
    createData: {
      role: role => { role === 'admin' ? sendAdminWelcome() : sendWelcome() }
    }
  }
}

Current format

// Resolvers file (user.schema.ts)
import { resolve } from '@feathersjs/schema'
import { dataValidator } from '../../validators'
import type { HookContext } from '../../declarations'

export const userValidator = getValidator(userSchema, dataValidator)
export const userDataResolver = resolve<User, HookContext>({
  role: role => { role === 'admin' ? sendAdminWelcome() : sendWelcome() }
})
export const userQueryResolver = resolve<UserQuery, HookContext>({
  company: (v, user) => user?.role === 'super-admin' ? v : user.company
})
// Main service file (User.ts)
import { hooks as schemaHooks } from '@feathersjs/schema'

import {
  userDataResolver,
  userQueryResolver,
  userDataValidator
} from './users.schema'

const hooks = {
  before: {
    create: [ schemaHooks.validateData(userDataValidator) ],
    find: [
      schemaHooks.resolveQuery(userQueryResolver)
    ],
  },
  after: {
    create: [
      schemaHooks.resolveResult(userDataResolver)
    ]
  }
} 

Additional notes

daffl commented 1 year ago

This is somewhat related to my comment in https://github.com/feathersjs/feathers/issues/3209#issuecomment-1605619697 in that making all service method implementations explicit, we can use the now finalised decorators to make it a lot more easily visible in a single file what the flow of your data will be (of course you can use the normal format as well). This is an example that I sent to @marshallswain which I believe shows the same thing you are suggesting in the resolveData hook:

import { feathers, hooks, service } from '@feathersjs/feathers'
import { authenticate } from '@feathersjs/authentication'
import { Type, Static, validateData } from '@feathersjs/typebox'
import { KnexDatabase } from '@birdhaus/knex'

const messageSchema = Type.Object({
  id: Type.Number(),
  text: Type.String()
})

type Message = Static<typeof messageSchema>

@hooks([
  authenticate('jwt')
])
@service()
class MessageService {
  db: KnexDatabase<Message>

  constructor () {
    this.db = new KnexDatabase<Message>({
      Model: app.get('knexClient')
    })
  }

  async find () {
    return this.db.find()
  }

  @hooks([
    validateData(messageSchema),
    resolveData({
      createdAt: async () => Date.now()
    })
  ])
  async create(data: Message) {
    return this.db.create(data)
  }
}

export const app = feathers<{ messages: MessageService }>()
  .use('messages', new MessageService())
FossPrime commented 1 year ago

Sounds like good ideas! Closing in favor of #3209 as it's apparently the first step towards DX improvements in this area.