4

MirageJS provides all model ids as strings. Our backend uses integers, which are convenient for sorting and so on. After reading around MirageJS does not support integer IDs out of the box. From the conversations I've read the best solution would be to convert Ids in a serializer.

Output:

{
 id: "1",
 title: "Some title",
 otherValue: "Some other value"
}

But what I want is:

Expected Output:

{
 id: 1,
 title: "Some title",
 otherValue: "Some other value"
}

I really want to convert ALL ids. This would included nested objects, and serialized Ids.

Sam Selikoff
  • 12,366
  • 13
  • 58
  • 104

3 Answers3

2

My solution is to traverse the data and recursively convert all Ids. It's working pretty well.

I have a number of other requirements, like removing the data key and embedding or serializing Ids.

const ApplicationSerializer = Serializer.extend({
  root: true,

  serialize(resource, request) {
    // required to serializedIds
    // handle removing root key
    const json = Serializer.prototype.serialize.apply(this, arguments)
    const root = resource.models
      ? this.keyForCollection(resource.modelName)
      : this.keyForModel(resource.modelName)

    const keyedItem = json[root]

    // convert single string id to integer
    const idToInt = id => Number(id)

    // convert array of ids to integers
    const idsToInt = ids => ids.map(id => idToInt(id))

    // check if the data being passed is a collection or model
    const isCollection = data => Array.isArray(data)

    // check if data should be traversed
    const shouldTraverse = entry =>
      Array.isArray(entry) || entry instanceof Object

    // check if the entry is an id
    const isIdKey = key => key === 'id'

    // check for serialized Ids
    // don't be stupid and create an array of values with a key like `arachnIds`
    const isIdArray = (key, value) =>
      key.slice(key.length - 3, key.length) === 'Ids' && Array.isArray(value)

    // traverse the passed model and update Ids where required, keeping other entries as is
    const traverseModel = model =>
      Object.entries(model).reduce(
        (a, c) =>
          isIdKey(c[0])
            ? // convert id to int
              { ...a, [c[0]]: idToInt(c[1]) }
            : // convert id array to int
            isIdArray(c[0], c[1])
            ? { ...a, [c[0]]: idsToInt(c[1]) }
            : // traverse nested entries
            shouldTraverse(c[1])
            ? { ...a, [c[0]]: applyFuncToModels(c[1]) }
            : // keep regular entries
              { ...a, [c[0]]: c[1] },
        {}
      )

    // start traversal of data
    const applyFuncToModels = data =>
      isCollection(data)
        ? data.map(model => 
            // confirm we're working with a model, and not a value
            model instance of Object ? traverseModel(model) : model)
        : traverseModel(data)

    return applyFuncToModels(keyedItem)
  }
})
  • 4
    This is brutal :/ We should add a hook to serializers to do this, something like `valueForId`. I'm guessing you saw this but Mirage still coerced to string? https://miragejs.com/docs/advanced/mocking-guids – Sam Selikoff Apr 17 '20 at 14:14
  • I'll take a look. I may have missed that though :( – Francois Carstens Apr 17 '20 at 14:18
  • 1
    @SamSelikoff, has some trouble initially but found some examples in the Discord for how to integrate Identity Managers. Once I get it to work I'll update my answer to include that as a solution. – Francois Carstens Apr 17 '20 at 16:21
  • Unfortunately an Identity Manager is not helping. I'm using a copy of the default Identity Manager and forcing everything to Integer, but it's just creating duplicates with a mix of number and string ids. Looks like I'll have to stick with this reduce for now. At least my collections aren't very big right now. – Francois Carstens Apr 20 '20 at 13:07
2

I think you should be able to use a custom IdentityManager for this. Here's a REPL example. (Note: REPL is a work in progress + currently only works on Chrome).

Here's the code:

import { Server, Model } from "miragejs";

class IntegerIDManager {
  constructor() {
    this.ids = new Set();
    this.nextId = 1;
  }

  // Returns a new unused unique identifier.
  fetch() {
    let id = this.nextId++;
    this.ids.add(id);

    return id;
  }

  // Registers an identifier as used. Must throw if identifier is already used.
  set(id) {
    if (this.ids.has(id)) {
      throw new Error('ID ' + id + 'has already been used.');
    }

    this.ids.add(id);
  }

  // Resets all used identifiers to unused.
  reset() {
    this.ids.clear();
  }
}

export default new Server({
  identityManagers: {
    application: IntegerIDManager,
  },

  models: {
    user: Model,
  },

  seeds(server) {
    server.createList("user", 3);
  },

  routes() {
    this.resource("user");
  },
});

When I make a GET request to /users with this server I get integer IDs back.

Sam Selikoff
  • 12,366
  • 13
  • 58
  • 104
  • This is working for collections, but it doesn't seem to work on my side for models. I was unable to load a model in the REPL, getting a 404 for GET `/users/1`. Shorthand `this.resource("user")` should be creating that route, correct? Sidenote, I was unable to add "miragejs" as a tag, I don't have enough reputation. – Francois Carstens Apr 22 '20 at 13:24
  • 1
    @FrancoisCarstens sorry for delayed response! I updated your question to add "miragejs" tag, thank you. I think the 404 is because the shorthand is looking the model up by string ID. I think you'll have to implement your own handlers in order to findBy(integer). – Sam Selikoff May 15 '20 at 12:58
  • This pointed me in the right direction for my needs - wasn't sure how to add the identity manager. I used their GUID example (https://miragejs.com/docs/advanced/mocking-guids/) and it worked nicely thank you – user115014 Jan 21 '21 at 14:44
1

I had to solve this problem as well (fingers crossed that this gets included into the library) and my use case is simpler than the first answer.

function convertIdsToNumbers(o) {
  Object.keys(o).forEach((k) => {
    const v = o[k]
    if (Array.isArray(v) || v instanceof Object) convertIdsToNumbers(v)
    if (k === 'id' || /.*Id$/.test(k)) {
      o[k] = Number(v)
    }
  })
}

const ApplicationSerializer = RestSerializer.extend({
  root: false,
  embed: true,
  serialize(object, request) {
    let json = Serializer.prototype.serialize.apply(this, arguments)
    convertIdsToNumbers(json)
    return {
      status: request.status,
      payload: json,
    }
  },
})
Stefan Bajić
  • 374
  • 4
  • 14