sidewinder icon indicating copy to clipboard operation
sidewinder copied to clipboard

Validation fails when using $push operator in mongo database

Open waikikamoukow opened this issue 2 years ago • 3 comments

I'm currently running into an issue where validation fails when using the $push operator in a mongo database to add an item to an array. It appears the validator is expecting an array, when I'm passing in a single value to be added to the array.

As a minimal example, consider this model:

export type Plant = Static<typeof Plant>
export const Plant = Type.Object({
  plantType: PlantType,
  size: Type.Number()
})

export type GardenBed = Static<typeof GardenBed>
export const GardenBed = Type.Object({
  _id: Type.String(),
  plants: Type.Array(Plant)
})

Then I create a mongo database like this:

export const PlantDatabaseSchema = Type.Database({
  gardens: GardenBed
})

export class PlantDatabase extends MongoDatabase<typeof PlantDatabaseSchema> {
  constructor(db: Db) {
    super(PlantDatabaseSchema, db)
  }

/**
   * Adds a plant to the garden bed.
   * Currently non-functional due to validation error.
   * @param request plot to update
   */
  public async plantInGardenBed(plant: Plant) {
    await this.collection('gardens').updateOne({
      _id: request._id
    }, {
      $push: {
        plants: plant
      }
    })
  }
}

When I attempt to call plantInGardenBed I get a validation error like this:

ValidateError: Data did not to validate
    at Validator.assert ()
    at MongoCollection.validateUpdate ()
    at Object.2 ()
    at matchArguments ()
    at MongoCollection.updateOne ()
    at PlantDatabase.plantInGardenBed ()
    at GardenService.addPlantToGardenBed ()
    at processTicksAndRejections ()
    at async Object.callback ()
    at async ServiceMethods.execute () {
  errors: [
    {
      type: 0,
      schema: [Object],
      path: '/plots',
      value: [Object],
      message: 'Expected array'
    }
  ]

I'm basing my query format on the mongodb docs for $push. It could be that I'm missing something on how to format this, but I can't seem to see an issue with what I'm doing based on the mongodb docs, so suspect it might be an issue with the way it is being validated.

waikikamoukow avatar Jun 15 '23 21:06 waikikamoukow

@waikikamoukow Heya!

Yeah, this looks likely to be a validation issue. Will take a look at this this evening :)

Cheers!

sinclairzx81 avatar Jun 16 '23 02:06 sinclairzx81

@waikikamoukow Heya,

So, have had a look at this. Unfortunately, I don't think there's going to be much I can do in the short term to solve for partial $push updates. The issue is that the Collection<T> type treats the document as a complete type, and because the $push is pushing a sub element / property of that type, the Collection would need to extract a sub schema from the full schematic to validate it correctly. The work to support sub schema select / validation for $push (as well as other ops) is fairly prohibitive at stage, however there is some upstream functionality in TypeBox that might make this more feasible in future.

https://github.com/sinclairzx81/typebox#types-indexed

I think rather than undertaking the work to support sub schema select (which would be a fairly large body of work specific to the @sidewinder/mongo package), It might be better to upgrade Sidewinder to support Indexed Access Types first (to the latest version of TypeBox), then implement sub schema select via Indexed Accessors. Would be looking at a few days minimum to implement this, and given some of the other changes, would probably be best to defer at this stage.

Workaround

As a workaround, you can actually by-pass the type checking by calling .collection to access the underlying driver collection (which doesn't perform any validation at all). Just note that string coercion to ObjectId is not automatically handled on the raw collection, so you will need to use new ObjectId(...). I've also added a quick import for Value which will allow you to check the array element (so manual type checking)

import { Value } from '@sidewinder/value'

export type Plant = Static<typeof Plant>
export const Plant = Type.Object({
  plantType: PlantType,
  size: Type.Number()
})

export type GardenBed = Static<typeof GardenBed>
export const GardenBed = Type.Object({
  _id: Type.String(),
  plants: Type.Array(Plant)
})

export const PlantDatabaseSchema = Type.Database({
  gardens: GardenBed
})

export class PlantDatabase extends MongoDatabase<typeof PlantDatabaseSchema> {
  constructor(db: Db) {
    super(PlantDatabaseSchema, db)
  }

/**
   * Adds a plant to the garden bed.
   * Currently non-functional due to validation error.
   * @param request plot to update
   */
  public async plantInGardenBed(plant: Plant) {
    if(!Value.Check(Plant, plant)) throw Error('Invalid plant')
    await this.collection('gardens').collection.updateOne({ // use `.collection` to access raw collection
      _id: new ObjectId(request._id) // use ObjectId as string to ObjectId coercion is not supported on raw collection
    }, {
      $push: {
        plants: plant
      }
    })
  }
}

Let me know if this helps! Cheers S

sinclairzx81 avatar Jun 16 '23 04:06 sinclairzx81

Thanks @sinclairzx81 for the prompt responses and workaround. The workaround you've suggested solves the issue for me.

waikikamoukow avatar Jun 16 '23 19:06 waikikamoukow