feathers icon indicating copy to clipboard operation
feathers copied to clipboard

[FR] MongoDB strict JSON v1 mode (remove convert all EJSON types to JSON compatible ones)

Open FossPrime opened this issue 2 years ago • 0 comments

Problem

Using setNow, softDelete and many other hooks will generate non JSON types in the database. Which causes problems when making comparisons on the client side or sharing server code with the client. Date !== string date, nor does ObjectID or Geocode.

Continuation of this 2019 issue

Instead of this:
Screenshot 2023-05-30 09 13 25

Most of us would much rather have this: (source)
Screenshot 2023-05-30 09 14 29

Proposed solution

Strict JSON mode on the adapter.

Example to rectify deviants
/*
  Replacement for feathers-mongodb database adapter
  ensures no exotic objects are created in MongoDB.
  Works like middleware using inheritance.
*/
import { MongoDBService as Service } from '@feathersjs/mongodb'
import { ObjectId }  from 'mongodb'
import { select } from '@feathersjs/adapter-commons'
import { Logger } from '../logger.js'

const logger = new Logger('MenDB')
const USE_STRICT_JSON = true

// Casts all values to JSON (Dates, Geoloc, ObjectId, etc)
function toStrictJson(data) {
  if(USE_STRICT_JSON !== true) {
    return data
  }
  if (data) { // Only on create, update, patch
    if (!Array.isArray(data)) {
      return JSON.parse(JSON.stringify(data))
    } else {
      return data.map(e => JSON.parse(JSON.stringify(e)))
    }
  } else {
    return data
  }
}

export default class MenDB extends Service {
  // Override to create String Id's
  // Uses strict json for deep cloning
  _setId (ctx) {
    return (item) => {
      const entry = toStrictJson(item) // Object.assign({}, item)

      // Generate a String ID if ID not provided
      if (typeof entry[ctx.id] === 'undefined') {
        entry[ctx.id] = new ObjectId().toHexString()
      }

      return entry
    }
  }

  // Debugging helper
  async _get(id, params = {}) {
    logger.info('SUPER GET', params.tenant ? params.tenant + '/' + id : id)
    return super._get(id, params)
  }

  // Manually sets string ID before creation
  // Otherwise, MongoDB would set it to a ObjectId
  async _create (data, params = {}) {
    let payload = null
    let promise = null
    const model = await this.getModel(params)
    
    if(Array.isArray(data)) {
      payload = data.map(this._setId(this))
      promise = model.insertMany(payload)
    } else {
      payload = this._setId(this)(data)
      promise = model.insertOne(payload)
    }
    
    // const mongoResult = promise
    await promise
    const selectFn = select(params, this.id)
    // console.log(selectFn, mongoResult, this.id, params)
    const result = selectFn(payload)
    // console.log(result)
    return result
  }

  async _patch(id, _data, params) {
    return super._patch(id, toStrictJson(_data), params)
  }

  async _update(id, data, params) {
    return super._update(id, toStrictJson(data), params)
  }
}

Alternative solution

We could export a strict JSON adapter, completely separately. This would negate any performance penalty for those who do not need isomorphic-friendly types.

Risks and other considerations

  • The ObjectID conversion setting should be respected
  • Dove has no Migration tools for NoSQL (Mongoose is no longer supported)

FossPrime avatar May 30 '23 14:05 FossPrime