feathersjs / feathers

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

[schema] add built-in support for nested/dotted queries to querySyntax() utility #2994

Open miguelrk opened 1 year ago

miguelrk commented 1 year ago

TLDR: currently, nested/dotted query support is only possible by explicitly listing the nested paths in the <service>QuerySchema, the querySyntax() utility could handle this automatically when being passed object/array fields.

For example, instead of this

export const resourceQueryProperties = Type.Pick(resourceSchema, ['access'])
export const resourceQuerySchema = Type.Intersect(
  [
    querySyntax(resourceQueryProperties),
    // Add additional query properties here
    Type.Object({
      // allow querying by nested properties
      'access.userId': Type.Optional(Type.String()),
      'access.level': Type.Optional(Type.String()),
    }, { additionalProperties: false }),
  ],
  { $id: 'ResourceQuery', additionalProperties: false },
)

the following should be possible to achieve the same

export const resourceQueryProperties = Type.Pick(resourceSchema, ['access'])
export const resourceQuerySchema = Type.Object(
  querySyntax(resourceQueryProperties),
  { $id: 'ResourceQuery', additionalProperties: false },
)

I understand that doing so gives less control as to which subfields e.g. access.<subfield>to allow to query by. So there's a tradeoff to be made, but maybe there's still a way of having built-in support, and also have the fine-grain, explicit control.

daffl commented 1 year ago

This is actually a good idea now that we started to figure out how to get references working in #2993. We can't query $ref objects directly with the query syntax but the dotted syntax also works with SQL (you just have to declare which tables to join) so it would be pretty intuitive.

daffl commented 1 year ago

The problem is that there isn't a way to get types for it since TypeScript doesn't support statically typed composite key indexes. It might be possible with a static array

export const resourceQuerySchema = Type.Object(
  querySyntax(resourceQueryProperties, [ 'access.userId' ]),
  { $id: 'ResourceQuery', additionalProperties: false },
)

This would also allow the full query syntax which I believe is possible for both, MondoDB and SQL

miguelrk commented 1 year ago

The static array would be an elegant solution, at least its much less cumbersome. It also allows explicit control as to which nested properties to include, while avoiding having to re-declare schemas for nested properties, so best of both worlds 👍🏼

daffl commented 1 year ago

Unfortunately it turns out this still won't get us static type inference. Even if the nested properties and string keys are known, there is no way to access them statically based on a dot separated path. It might work when using nested queries like the following though:

{
  query: {
    access: {
      userId: 'something',
      level: 2
    }
  }
}
eXigentCoder commented 1 year ago

Just to add to the discussion, I'm using mongodb and json-schema not TypeBox and I'm having trouble figuring out how to query nested properties in my document (not a table linked by a $ref).

Using ...querySyntax({}) doesn't work because "Only types string, number, integer, boolean, null are allowed" and the nested property is an object.

Should I be specifying the nested property on the query manually? If so do you maybe have an example of the syntax?

What I tried for my query looks like this:

image
daffl commented 1 year ago

If you upgrade to the latest version you should no longer see that error.

eXigentCoder commented 1 year ago

Thanks a ton, working!

gupta82anish commented 1 year ago

Hi, idk if this is the right place to put this, but there seems to be something missing in the docs.

image

How is the userQuery related to userQuerySchema? I'm viewing the Javascript version of the docs and this isn't making sense to me. I am trying to override/extend the find method to use the select query to only return 'id', 'title' from the the posts schema.

// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import { resolve, getValidator, querySyntax } from '@feathersjs/schema'
import { dataValidator, queryValidator } from '../../validators.js'

// export const postsSchema = {
//   $id: 'Posts',
//   type: 'object',
//   additionalProperties: false,
//   required: ['id', 'text'],
//   properties: {
//     id: { type: 'number' },
//     text: { type: 'string' }
//   }
// }
// Main data model schema
export const postsSchema = {
  $id: 'Posts',
  type: 'object',
  additionalProperties: false,
  required: ['id','title', 'content', 'date_published', 'description', 'author'],
  properties: {
      id: { type: 'number' },
      title: { type: 'string' },
      content: { type: 'string' },
      date_published: { type: 'string', format: 'date-time' },
      description: { type: 'string' },
      author: { type: 'string' },
  }
}

export const postsValidator = getValidator(postsSchema, dataValidator)
export const postsResolver = resolve({})

export const postsExternalResolver = resolve({})

// Schema for creating new data
export const postsDataSchema = {
  $id: 'PostsData',
  type: 'object',
  additionalProperties: false,
  required: ['title', 'content', 'date_published', 'description', 'author'],
  properties: {
    ...postsSchema.properties
  }
}
export const postsDataValidator = getValidator(postsDataSchema, dataValidator)
export const postsDataResolver = resolve({})

// Schema for updating existing data
export const postsPatchSchema = {
  $id: 'PostsPatch',
  type: 'object',
  additionalProperties: false,
  required: [],
  properties: {
    ...postsSchema.properties
  }
}
export const postsPatchValidator = getValidator(postsPatchSchema, dataValidator)
export const postsPatchResolver = resolve({})

// Schema for allowed query properties
export const postsQuerySchema = {
  $id: 'PostsQuery',
  type: 'object',
  additionalProperties: false,
  properties: {
    ...querySyntax(postsSchema.properties)
  }
}

const postsQuery = {
  $select: ['id','title', 'description']
}

export const postsQueryValidator = getValidator(postsQuerySchema, queryValidator)
export const postsQueryResolver = resolve({})

This is my posts.schema.js I can't figure out what i'm missing here. @daffl