feathersjs / feathers

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

Using queryResolver with non-publicly queryable properties causes a TS error #2938

Closed AshotN closed 1 year ago

AshotN commented 1 year ago

Steps to reproduce

Given a simple schema,


// Main data model schema
export const companySchema = Type.Object(
  {
    id: Type.String({ format: 'uuid' }),
    name: Type.String(),
    active: Type.Boolean()
  },
  { $id: 'Company', additionalProperties: false }
)

And we want only the active field to be publicly queryable.


// Schema for allowed query properties
export const companyQueryProperties = Type.Pick(companySchema, ['active'])
export const companyQuerySchema = Type.Intersect(
  [
    querySyntax(companyQueryProperties),
    // Add additional query properties here
    Type.Object({}, { additionalProperties: false })
  ],
  { additionalProperties: false }
)
export type CompanyQuery = Static<typeof companyQuerySchema>
export const companyQueryValidator = getValidator(companyQuerySchema, queryValidator)
export const companyQueryResolver = resolve<CompanyQuery, HookContext>({
  // @ts-ignore
  name: async (value, user, context) => {
    return "a name"
  }
})

Expected behavior

No TS errors

Actual behavior

Tell us what happens instead

Typescript complains that name is not an available property

Object literal may only specify known properties, and 'name' does not exist in type 'ResolverConfig<Partial<{ $limit: number; $skip: number; $sort: { active?: number | undefined; }; $select: "active"[]; $or: { active?: boolean | Partial<{ $gt: boolean; $gte: boolean; $lt: boolean; $lte: boolean; $ne: boolean; $in: boolean[]; $nin: boolean[]; }> | undefined; }[]; $and: { ...; }[]; }> & { ...; } & {},...'.

Likewise trying to access name as a query property in a hook will give a TS error

after: {
      create: [
        async (context) => {
          const nameQuery = context.params.query?.name
        }
      ]
    },
TS2339: Property 'name' does not exist on type 'Partial{ $limit: number; $skip: number; $sort: { active?: number | undefined; }; $select: "active"[]; $or: { active?: boolean | Partial{ $gt: boolean; $gte: boolean; $lt: boolean; $lte: boolean; $ne: boolean; $in: boolean[]; $nin: boolean[]; }> | undefined; }[]; $and: { ...; }[]; }> & { ...; } & {}'.

System configuration

Tell us about the applicable parts of your setup.

AshotN commented 1 year ago

I'm not sure if this issue is actually solvable in any reasonable way. So my solution is to have a seperate queryResolver like so

export const companyFilterQueryResolver = resolve<Company, HookContext>({
  name: async (value, obj, context) => {
    if (context.params.user) {
      return "Something"
    }
    return value
  }
})

and just add it to my hooks under the regular queryResolver

schemaHooks.resolveQuery(companyQueryResolver),
schemaHooks.resolveQuery(companyFilterQueryResolver)

My use case for queryResolvers is to create filters for what the user is allowed to fetch

I have an ownership pattern something like this,

An invoice that is owned by a company A company that is controlled by a user

So if a user wants to access a invoice, the invoice query resolver gets the company ids like this

 company_id: async (value, obj, context: HookContext<InvoiceService>) => {
    const { user } = context.params
    if (!user) return value

    const companies: { id: string }[] = await app
      .service('company')
      .find({ user, paginate: false, query: { $select: ['id'] } })
    const companyIds = companies.map((c) => c.id)

    return { $in: companyIds }
  }

The company query resolver does another check like this

 controlling_user_id: async (value, obj, context) => {
    if (context.params.user) {
      return context.params.user.id
    }
    return value
  }

This allows my invoice query to only return invoices that are owned by companies that are controlled by the requesting user

If this all seems correct @daffl I can close this issue as it was clearly user error