Code Monkey home page Code Monkey logo

discuss's Introduction

Mirage JS โ€“ Discuss

Ask questions, get help, and share what you've built with Mirage

View the issues to get involved:

discuss's People

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

discuss's Issues

Add support for OpenAPI

It would be awesome if Mirage could load and parse an OpenAPI Specification and use that to configure the routes and data stores.

Proposal: defer requests to improve testing pending states

TL;TR: Providing three additional methods on server would make testing pending states a pleasure.

Testing application state while requests are pending is currently not that smooth with mirage as it could be. This proposal tries to address that issues.

Ember's test helper waitFor and waitUntil provides a nice API for testing pending states but mirage's server.timing configuration does not fit in well. To test pending states a timing must be chosen that does not resolve before all assertions for the pending state are done, but on the other hand does not block the tests longer than necessary. This is even more complex cause ember's runloop and timers interacting in a non predictable way. Depending on the device used for running the tests or if the browser tab has focus, a runloop might take way longer than 1ms. So the timing has to be chosen to be long enough for all use cases and you may still see flickering tests if executing in browser (http://localhost:4200/tests) and not having focus on the tab.

The only other solution I'm aware of is using RSVP.defer and overriding route handlers in each test. But that will produce unreadable and unmaintainable testing code - especially if a route handler is triggered more than once.

I like to propose a new API to mirage to support that use case. It should allow to defer requests and resolve them not before a predictable point of time in your test. To do so server object should provide three additional methods: deferRequests(), resolveRequests() and undeferRequests().

server.deferRequests() starts deferring requests. Every request fired afterwards is not settled before server.resolveRequests() is called. Deferring requests could be stopped by server.undeferRequests(). This will also settle all pending requests.

A test using it could look this:

module('Acceptance | post', function(hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('create a new post', async function(assert) {
    await visit('/posts/new');
    await fillIn('[data-test-input=title', 'A new post');
    await fillIn('[data-test-input=body', 'Some text for our new post');

    // defer responses to all requests
    server.deferRequests();

    // do stuff that triggers an API request
    click('[data-test-button=submit]');

    // test application while API requests are pending
    await waitFor('[data-test-button=submit] [data-test-loading-spinner]', {
      timeoutMessage: 'loading spinner has not appeared'
    });
    assert.ok(
      find('[data-test-button=submit]').disabled,
      'submit button is disabled while save request is pending'
    );

    // resolve all pending API requests
    server.resolveRequests();
    await settled();

    // test application after API requests have been settled
    assert.ok(
      find('[data-test-button=submit]').disabled,
      'submit button is enabled again after save request is settled'
    );
  });
});

Overview

/**
 * Start deferring requests. Pass optional `path` and `options` arguments to only defer some requests.
 *
 * `path` must match a path of a route registered before.
 * `server.namespace` configuration is taken into consideration.
 * If `path` contains a dynamic segment also it's name must match the one used for route registration.
 *
 * `options` only supports filtering by HTTP request methods. To do so, pass an array of HTTP request methods under
 * `methods` key. Allowed values are: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD`. Additionally
 * `DEL` is supported as an alias for `DELETE`. The value is case-insensitive.
 *
 * `path` is considered a resource helper, if it does not start with a protocol and domain (full URL) or with a slash.
 * In that case it registers deferring for same endpoint, method combination as resource helper does for route
 * registration.
 *
 * @method deferRequests
 * @param  {String} [path]
 * @param  {Object} [options]
 * @return void
 * @public
 */
deferRequests(path, options) {}

/**
 * Cancels deferring requests. Pass optional `path` and `options` arguments to only cancel deferring of some requests.
 * Resolves all deferred requests for affected routes.
 *
 * @method resolveRequests
 * @param  {String}   [path]
 * @param  {Object}   [options]
 * @return {Integer}  number of requests resolved
 * @public
 */
undeferRequests(path, options = {}) {}

/**
 * Resolves deferred requests. Pass optional `path` and `options` arguments to only resolve some deferred requests.
 *
 * @method resolveRequests
 * @param  {String}   [path]
 * @param  {Object}   [options]
 * @return {Integer}  number of requests resolved
 * @public
 */
resolveRequests(path, options = {}) {}

Limit to some requests

All methods should accept a path as first argument to only defer, undefer or resolve requests for a specific endpoint. The path must match a registered route. E.g. server.deferRequests('/posts') would only defer requests for /posts endpoint.

Path is handled the same as on route registration. It could contain a protocol and domain. If not current server.namespace configuration is taken into consideration.

This could be further limited by an options hash provided as second argument. The only option supported so far is methods. It takes an array of strings. Allowed values are: 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'. Additionally DEL is supported as an alias for DELETE to be inline with verbs used on route registration. The value is case-insensitive. If methods option is missing, all requests to specified path are deferred independently of method.

Additionally resource helper are supported. If a path does not start with a protocol or a slash, it's considered a resource helper and will register deferring for the same routes as on route registration.

Another call to deferRequests() with the same path will override an earlier one.

Some examples:

server.namespace = '';

/*
 * register deferring
 */
server.deferRequests('/companies');
server.deferRequests('/users', { methods: ['GET', 'POST'] });

// this requests will be deferred
jQuery.ajax({ method: 'GET',     url: '/companies' });
jQuery.ajax({ method: 'POST',    url: '/companies' });
jQuery.ajax({ method: 'PUT',     url: '/companies' });
jQuery.ajax({ method: 'PATCH',   url: '/companies' });
jQuery.ajax({ method: 'DELETE',  url: '/companies' });
jQuery.ajax({ method: 'HEAD',    url: '/companies' });
jQuery.ajax({ method: 'OPTIONS', url: '/companies' });

jQuery.ajax({ method: 'GET',     url: '/users' });
jQuery.ajax({ method: 'POST',    url: '/users' });

// this requests won't be deferred (only some examples)
jQuery.ajax({ method: 'GET',     url: '/companies/1' });
jQuery.ajax({ method: 'PATCH',   url: '/users' });
jQuery.ajax({ method: 'DELETE',  url: '/users' });

// using a resource helper
server.deferRequests('customers');
// is just a short for
server.deferRequests('/customers',     { methods: ['GET', 'POST'] });
server.deferRequests('/customers/:id', { methods: ['GET', 'PUT', 'PATCH', 'DELETE'] });

/*
 * resolving requests
 */
server.resolveRequests('/users', { method: 'GET' });
// POST request to /users endpoint is still deferred

// this request is deferred cause deferring of GET requests
// for /users endpoint was not stopped
fetch('/users', { method: 'GET' });

/*
 * cancel deferring of requests
 */
server.undeferRequets('/users');
// new requests to '/users' pendingpoint won't be deferred
// pending POST request to /users endpoint will be resolved

Timing

If route handler returns a promise, requests must not resolve before that one is settled.

import { later } from '@ember/runloop';

// route handler which will resolve after 50ms
server.get('/users', () => {
  return new Promise((resolve) => {
    later(() => {
      resolve();
    }, 50);
  });
});

server.deferRequests();
jQuery.ajax({
  method: 'GET',
  url: '/users'
});

// wait 25 ms
await new Promise((resolve) => {
  later(() => {
    resolve();
  }, 25);
});
server.resolveRequests();

// pending promise not fulfilled yet
await settled();
// pending promise fulfilled after 50ms

The same applies for timing parameter if used (even so I don't see any use case for that).

Implementation

An example implementation is provided here: miragejs/ember-cli-mirage#1345 Use it to test out the provided API in real-world scenarios. Please report bugs of the concrete implementation in the PR. This issue should be limited to the overall discussion.

Open questions

  • Did I missed the obvious way solution to the issue with existing API? Hopefully not. Otherwise this was a fair amout of wasted time...
  • Upcoming versions of mirage might drop pretender.js and implement the mocked API using Service Worker or express.js. Would this new API add any limitations to that ones?
  • I'm not totally convinced of naming - especially undeferRequests() feels a little bit strange. I also thought of deferResponses() instead of deferRequests() and settleRequests() instead of resolveRequests().

Custom exception post-processing

First up, thanks a lot for this amazing library, this is exactly what I needed!

Now, coming from a Loopback 4 background in the backend (specifically this section in their docs), I'd like to do something like this:

import { Server } from 'miragejs';
import HttpErrors from 'http-errors';

new Server({
  routes() {
    this.post('/foo', () => {
      throw new HttpErrors.UnprocessableEntity('dont-want-this');
    });
  },
};

That is, throwing HTTP error objects with nice, descriptive names instead of returning miragejs.Response objects; right now, the code above of course always results in HTTP 500. To achieve the desired behavior, wrapping the entire route handler in a try-block is necessary:

this.post('/foo', () => {
  try {
    throw new HttpErrors.UnprocessableEntity('dont-want-this');
  } catch (e) {
    if (e instanceof HttpErrors.HttpError) {
      // if HttpError, transform to valid response
      return new Response(
        e.statusCode,
        {},
        { error: { statusCode: e.statusCode, name: e.name, message: e.message } },
      );
    }

    // otherwise, it was some unknown error, so just re-throw it to produce HTTP 500
    throw e;
  }
});

Obviously, not everyone will want to use this style of error in case a HttpError is thrown and possibly adding http-errors as a dependency to Mirage JS should be avoided, so I could imagine something like the following to be added to Mirage JS in order to result in the same behavior as the previous piece of code:

new Server({
  exceptionHandlers: [
    {
      inst: HttpErrors.HttpError,
      handler: e => {   // always called with 'e instanceof HttpErrors.HttpError' being true
        // in here, could either throw 'e' again (leading to HTTP 500) or return Response object
        return new Response(e.statusCode, {}, { error: { statusCode: e.statusCode, name: e.name, message: e.message } },
      )
    },
    // all other types of errors will not be custom handled, i.e. always result in HTTP 500
  ],

  routes() {
    this.post('/foo', () => {
      throw new HttpErrors.UnprocessableEntity('dont-want-this');
    });
  }
};

I guess the definition of the exception handlers might be questionable, but you should get the idea.

hasMany to hasMany (manyToMany) fixture associations not created

This may not be a bug, but the example in the docs

If this happens to be a bi-directional relationship

  models: {
    user: Model.extend({
+     posts: hasMany()
    }),

    post: Model.extend({
      author: belongsTo("user"),
    }),
  },

then Mirage will add an array of foreign keys for the new hasMany association.

I have a many to many relationship

creative: Model.extend({
  campaigns: hasMany(),
}),

campaign: Model.extend({
  creatives: hasMany(),
}),

And some fixtures

  {
    id: '48dc4215-6f6c-470c-8c3e-40d95311ef19',
    name: 'Example',
    creativeIds: [
      '3fd34999-9519-42b0-8f08-1c2dfea8d338',
      '247c689a-8834-4b64-a052-b73228bdb407',
    ],
  },

When querying the campaigns, I can see the two creatives, but when querying the creatives, I can't see the campaigns. Is this expected behaviour or a bug?

Generate Schema and Seeds from existing DB

Thanks a lot for this amazing tool!

Wow, this is going to revolutionize collaboration between backend and frontend developers. The primary reason for me to use this tool is to be able to let my frontend devs worry only about the frontend and not having to deploy the API themselves.

At the moment, what I do is host a dev server for them and have them all hit the same server for their tests. Which is inherently a bad idea.

Now, I have many existing projects and and they are pretty big. To be able to distribute Mirage for my designers, I'd need to spend about 2-4 hours (rough estimations) building the right Mirage endpoints for them.

At the moment, I start by only building the ones they currently need for their current task until I can cover the whole codebase.

It would be grand to be able to generate the Mirage server right from my MySQL/MongoDB database structures.
It would be even better to be able to add an option to also generate seeds from the DB content.

I'm thinking of a CLI tool that basically behaves like mysqldump:
Generate Mirage server from DB
mirage sync --no-data -h<IP-TO-YOUR-DB-SERVER> -u<YOUR-DB-USERNAME> -p --databases <YOUR-DB-NAME> > mirage.js

I expect the above to generate the right Mirage server into a mirage.js (that I passed at the end) file that I can import from my App.js file.

Remove the --no-data flag to make it generate seeds too.

What do you awesome people think?

Problem with inregular singularization of FKS of HasMany

hello, i am brazilian and having problem with translation of fks of hasMany

i have two models ( autorizacao and permissao ) autorizacao hasMany permissao

// mirage\models\autorizacao.js
import { Model, hasMany } from 'ember-cli-mirage';

export default Model.extend({

  permissoes: hasMany('permissao'),

});
// mirage\models\permissao.js
import { Model, belongsTo } from 'ember-cli-mirage';

export default Model.extend({

  autorizacao: belongsTo('autorizacao'),

});

I set configured my Inflector

// app\initializers\pluralization.js
import Inflector from 'ember-inflector'

export function initialize() {
  var inflector = Inflector.inflector;

  inflector.irregular('permissao', 'permissoes');
  inflector.irregular('autorizacao', 'autorizacoes');

}

export default {
  name: 'pluralization',
  before: 'ember-cli-mirage',
  initialize
};

so in portuguese i have a permissoes: hasMany('permissao'), and i expected an fk permisaoIds,
but an permissoIds is set insted.

i tested the singularize of permissoes, and it gives me permissao.

the console of autorizacao
image
image

i don't want to use permisso because this don't make sense in portuguese.

Remove Fixtures as a feature

Fixtures are just sugar over

server.db.loadData({
  countries: [ ... ]
})

and are confusing when used in conjunction with the default scenario, because of the load order & conditional loading logic based on environment. We should remove them and teach people how to do the same thing using loadData.

Merging real data with Mirage data.

Feature request

Is it currently or could it be possible to use fake/seeded data in combination with real data being requested via Mirage's passthrough feature? In my case, I'd like to create records via Mirage which are tied to a real user in our system. I can't, however, figure out how to delay Mirage initialization until after the user has been loaded. My hope would be able to use the afterCreate hook to tie a newly created record back to that existing user. Seems like this feature might be related to: miragejs/ember-cli-mirage#1416.

HasManyThroughAssociation

I wrote a HasManyThroughAssociation. It's not bullet-proof and I'm sure it could be improved, but it seems to be working. Most of it is copied from the HasManyAssociation with a few changes:

  • get foreignKey is built not directly from the database, but from a through collection
  • set foreignKey and set key intentionally blow up since it's a read-only association
  • it doesn't define create[Model] and new[Model] methods
import { camelize } from '@ember/string'
import { singularize } from 'ember-inflector'
import Association from 'ember-cli-mirage/orm/associations/association'
import Collection from 'ember-cli-mirage/orm/collection'
import { toCollectionName } from 'ember-cli-mirage/utils/normalize-name'

export default function(throughAssociationName) {
  return new HasManyThroughAssociation(throughAssociationName)
}

// Adapted from ember-cli-mirage/orm/associations/has-many
class HasManyThroughAssociation extends Association {
  constructor(throughAssociationName) {
    super({ polymorphic: false, throughAssociationName })
  }

  /**
   * @method getForeignKeyArray
   * @return {Array} Array of camelized model name of associated objects
   * and foreign key for the object owning the association
   * @public
   */
  getForeignKeyArray() {
    return [camelize(this.ownerModelName), this.getForeignKey()]
  }

  /**
   * @method getForeignKey
   * @return {String} Foreign key for the object owning the association
   * @public
   */
  getForeignKey() {
    return `${singularize(camelize(this.key))}Ids`
  }

  /**
   * Registers has-many association defined by given key on given model,
   * defines getters / setters for associated records and associated records' ids,
   * adds methods for creating unsaved child records and creating saved ones
   *
   * @method addMethodsToModelClass
   * @param {Function} ModelClass
   * @param {String} key
   * @public
   */
  addMethodsToModelClass(ModelClass, key) {
    let modelPrototype = ModelClass.prototype
    let association = this
    let foreignKey = this.getForeignKey()
    let associationHash = { [key]: this }

    modelPrototype.hasManyAssociations = Object.assign(
      modelPrototype.hasManyAssociations,
      associationHash
    )

    // Add to target's dependent associations array
    this.schema.addDependentAssociation(this, this.modelName)

    // TODO: look how this is used. Are these necessary, seems like they could be gotten from the above?
    // Or we could use a single data structure to store this information?
    modelPrototype.associationKeys.push(key)
    modelPrototype.associationIdKeys.push(foreignKey)

    // This is the most significant change from HasManyAssociation. Instead of looking
    // up IDs from the database, we map them from the through-association.
    Object.defineProperty(modelPrototype, foreignKey, {
      /*
        object.childrenIds
          - returns an array of the associated children's ids
      */
      get() {
        return this[association.opts.throughAssociationName].models
          .map(throughModel => {
            return throughModel[singularize(key)]
          })
          .filter(Boolean)
          .mapBy('id')
      },

      set() {
        throw new Error(
          `${key} is a has-many-through relationship; use ${this.throughAssociationName}Ids`
        )
      },
    })

    Object.defineProperty(modelPrototype, key, {
      /*
        object.children
          - returns an array of associated children
      */
      get() {
        this._tempAssociations = this._tempAssociations || {}
        let collection = null

        if (this._tempAssociations[key]) {
          collection = this._tempAssociations[key]
        } else {
          if (this[foreignKey]) {
            collection = association.schema[toCollectionName(association.modelName)].find(
              this[foreignKey]
            )
          } else {
            collection = new Collection(association.modelName)
          }
        }

        this._tempAssociations[key] = collection

        return collection
      },

      set() {
        throw new Error(
          `${key} is a has-many-through relationship; use ${this.throughAssociationName}`
        )
      },
    })
  }

  /**
   *
   *
   * @public
   */
  disassociateAllDependentsFromTarget(model) {
    let owner = this.ownerModelName
    let fk

    if (this.isPolymorphic) {
      fk = { type: model.modelName, id: model.id }
    } else {
      fk = model.id
    }

    let dependents = this.schema[toCollectionName(owner)].where(potentialOwner => {
      let currentIds = potentialOwner[this.getForeignKey()]

      // Need this check because currentIds could be null
      return (
        currentIds &&
        currentIds.find(id => {
          if (typeof id === 'object') {
            return id.type === fk.type && id.id === fk.id
          } else {
            return id === fk
          }
        })
      )
    })

    dependents.models.forEach(dependent => {
      dependent.disassociate(model, this)
      dependent.save()
    })
  }
}

I haven't written any tests. This issue is meant to be a starting point for someone who wants to drive the feature to completion.

Support multiple server instance.

I want to separate the mock data by module, but miragejs seems only surpport one instance, that means I need to locate all mock routes in one file.

Refactorings: toCollectionName

I'm refactoring toCollectionName to be a method on Schema because it depends on inflector.

We should rename DbCollection to DbTable or something, so collection can be reserved for an ORM/Collection.

Then we should have schema.collectionFor(type) method (something like this).

Provide public API to generate data based on factory

Having the ability to generate dummy data based on factories is helpful for acceptance testing of create and edit functionality. This is currently only possible using not documented private API server.build('type'). It would be awesome if we could expose this API publicly. The return should be the same as sever.create() but without persisting the data. build() is not the best name for such a method in my opinion. I would expect it to be named make(). I don't think there is a need for a makeList() method.

Usage example:

test('create a post', async function(assert) {
  let data = this.server.make('post');
  
  await vist('/create-post');
  await fillIn('[data-test-form-element-for="title"] input', data.title);
  await fillIn('[data-test-form-element-for="text"] textarea', data.text);
  await click('[data-test-button="submit"]');

  assert.deepEqual(data, this.server.db.posts[0], 'post has been persisted with correct data');
});

Auth + upload

Would be nice to have an example on clientside-auth and file uploading

Hooks for server.create

I have a large number of factories and I need to debug the ones that are slow to create. I'm having trouble tracking these down, since my app has factories that run factories that run factories.

Would be nice to hook into server.create calls so I can time them with chrome's performance tools.

Something like...

server.beforeCreate((factoryArgs) => {
  console.log(factoryArgs) // => ['model', 'trait1', 'trait2', { key: value }]
});

server.afterCreate((factoryArgs) => {
  console.log(factoryArgs) // => ['model', 'trait1', 'trait2', { key: value }]
});

Embeded relationship data not loading

Hello,

Have an issue I don't understand. I've an Ember-Data model, AssetLocation that has several belongsTo relationships. I can get one relationship to load data but the others don't. I confirmed that the mirage server is creating the data behind the scenes so I think this is a config or serialization issue.

Here's my Mirage serializer for the model.

import ApplicationSerializer from './application';

export default class AssetLocationSerializer extends ApplicationSerializer {
  root = false;
  embed = true;
  include = ['location', 'asset', 'assignedTo', 'seenBy'];
}

Here the actual Ember-Data serializer for the same model.

import DS from "ember-data";
import ApplicationSerializer from './application';

const { EmbeddedRecordsMixin } = DS;


export default class AssetLocationSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
  attrs = {
    asset: { embedded: 'always' },
    assignedTo: { embedded: 'always' },
    location: { embedded: 'always' },
    seenBy: { embedded: 'always' },
  }
}

Here's a sample request made from my app.
image

The embedded assets get created just fine. It's the others that don't. So assignedTo and seenBy for example never get created in the store. Am I missing something here?

Factory performance issues

Issues with using factories on larger data sets keeps coming up so I wanted to open an issue and consolidate the conversations so we can get down to the root of the issue.

It probably has something to do with some much older code somewhere, possibly in db.

Ryan was asking about a way to track which factories were taking longest over in https://github.com/miragejs/server/issues/91 and I hacked something together quickly that should give folks a way to do some measurement:

// mirage/config.js
import { Server } from "@miragejs/server";

let originalCreate = Server.prototype.create;
Server.prototype.create = function() {
  console.log("beforeCreate");
  let model = originalCreate.apply(this, arguments);
  console.log("afterCreate");

  return model;
};

export default function() {
  this.get("/users", ...)
}

It would be great to get some small demo projects going so we can start to fix this long-standing issue!

I want to help point anyone in the right direction who can help as every time this comes up something else is always more important.

WebSockets Support in Mirage

WebSockets Support in Mirage

Summary

Our team has been working on integrating web sockets mocking with Mirage. We set out to find something that worked for us. We've built this and have been using it to augment our tests and our development environment. It's worked well for us, and has even allowed us to provide proper test coverage for bugs being reported in the field and has allowed us to simulate complex situations in development, helping us catch bugs we would have otherwise had a lot of trouble seeing. We'd like to propose it as something that could be integrated with Mirage itself.

Motivation

WebSockets are a critical part of many application's infrastructure, and without the ability to simulate them in development and control them in test, Mirage cannot fulfill it's mission of being a client-side mock of your server.

Detailed design

I am proposing only one addition to the api:

Server.ws(url)

Returns: a mock-socket Server
Creates a new mock socket at the specified, fully-resolved url. This method should be called in mirage/config.js to setup the socket endpoint. With no further action taken, this will prevent Mirage from yelling at us when attempts are made to connect to this endpoint.

In order to send messages down (or listen for messages from the client), you can then use the returned Server, which is documented over at mock-socket.

Examples:
This will send a singular WebSocket message whenever the client connects to the socket.

// in mirage/config.js
this.ws('ws://localhost:4200/socket').on('connection', (ws) => {
  ws.send({ data });
});

If you want to manually send messages or otherwise control a WebSocket from your tests, you can easily access and control that WebSocket like you would any other endpoint:

// in mirage/config.js
this.ws('ws://localhost:4200/socket');
// in a test file
test('displays text when receiving websocket message', function(assert) {
  this.server.ws('ws://localhost:4200/socket').send({ message: "Hello World!" });
  assert.dom('[data-test-socket-message]').hasText("Hello World!");
});

For Development, you may wish to define custom web socket behavior to better mimic your server. Mirage's scenarios seem to be the best place for this to live.

// in mirage/config.js
this.ws('ws://localhost:4200/socket');
// in mirage/scenarios/default.js
const socket = this.ws('ws://localhost:4200/socket');
setInterval(() => {
  socket.send({ message: "Hello World!"});
}, 5000);

Special Considerations

  • the .ws method also stores a reference to each WebSocket mock server defined, unique by URL. This is to avoid multiple calls to .ws with the same url causing collisions.
  • Mirage's .shutdown must now also clean up any mock socket servers.

How we teach this

  • There is very little to teach, since we are mostly exposing the socket server itself (In this case, mock-socket, but in the future that could be an actual web socket server) for the user to manipulate as necessary.
  • We approached this with a "less is more" mentality, since there are infinite ways any given application could use WebSockets, and there exists no standards like REST that we can cater to like the rest of mirage does.

Drawbacks

  • The mock-socket library itself does not support Socket.io. It claims to, but we were not able to get that working, it seems it diverged from Socket.io long ago, and there is no test coverage for it's socket.io mocking. My team decided that being able to properly test web sockets was more important to us than what Socket.io provided, and the path to least resistance was to tear out Socket.io and move foreward with vanilla web sockets.
  • The dependency on mock-socket, complete with piping through it's api so directly, might prove problematic, if that library falls behind or breaks for any reason it would heavily affect Mirage users.

Alternatives

  • We could provide more shorthands and conventions for common WebSocket usages, but when my team sat down to think about this, we realized that WebSockets were so much more versatile and open ended than RESTful http. Exposing the socket itself to send and receive messages from provided both the simplest experience and most control.

  • We could find another tool other than mock-socket if we feel there is anything lacking from it. However there do not seem to be many out there. We would probably need to build our own implementation should there be any concern surrounding mock-socket.

server.create() fails for models which are named as a plural

Boilerplate example: https://github.com/lougreenwood/ember-cli-mirage-boilerplate

When an ember-data model is named as plural, for example settings, calling server.create('settings') or server.createList('settings') fails with the error:

Promise rejected during "server.create singular model fails": Mirage: You called server.create('settings') but no model or factory was found. Make sure you're passing in the singularized version of the model or factory name.

Instead, if we use server.create('setting'), this works, but this is not the actual model name.

Examples can be seen here:
Model:
https://github.com/lougreenwood/ember-cli-mirage-boilerplate/blob/master/app/models/settings.js

Mirage Factory:
https://github.com/lougreenwood/ember-cli-mirage-boilerplate/blob/master/mirage/factories/settings.js

Example failure test:
https://github.com/lougreenwood/ember-cli-mirage-boilerplate/blob/master/tests/integration/components/settings-test.js

Expected behaviour

Even if a model name is actually plural, server.create() should not require an incorrect, singularized version of the model name to be passed to it.

Add Middleware support to Mirage

Over in miragejs/miragejs#267 @asantos00 kicked off an idea for adding middleware to Mirage.

We've definitely needed something like this for a while.

We should discuss possible APIs here. Ideally just borrow from the latest + greatest implementation in server frameworks that we like the best.

APIs I want to change

Wanted to start collecting some APIs I'm consider changing/breaking, just because they feel very outdated.

  1. Route handlers should have a single object as their sole argument, and schema and request should be destructurable properties from it.

    // before
    this.get('/users', (schema, request) => {
      // logic
    });
    
    // after
    this.get('/users', ({ schema, request }) => {
      // logic
    });

    We should also add db and perhaps other things.

  2. We have two route handler "helpers" that you currently access off of a route handler's this context: serialize and normalizedRequestAttrs. This is super confusing because it prevents you from being able to use fat arrow functions.

    // whoops! won't work.
    this.get('/users', (schema, request) => {
      let json = this.serialize(schema.users.all())
    });
    
    // now it works.
    this.get('/users', function(schema, request) {
      let json = this.serialize(schema.users.all())
    });

    This sucks. But now that we have destructuring we have a much better way to make these helpers available - injection!

    // wew \o/
    this.get('/users', ({ schema, request, serialize }) => {
      let json = serialize(schema.users.all())
    });

    No more this, just grab the helper from the argument if you need it! Now you can use fat arrows all the time.

  3. The Response API is kinda awkward and feels like a big step to take just to customize the response code. We have a weird shorthand version that can do it but it might be better to revisit this. If we make change 2, we could possibly inject a response function that could be used more easily.

Model hooks / default attributes

Is it possible to have timestamps automatically created/touched on models when created/updated?

I saw it can be done with factories when seeding and then explicitly writing handlers for all the routes, but if I want to use the nice shorthand route handlers then it'd be nice to have some hooks i can set up on the model itself. which always run when created/touched perhaps?

Something like the below maybe?

Model.extend({
  beforeCreate: model => {
    model.createdAt = new Date;
	return this;
  }
})

The following hooks may be useful.

beforeCreate
afterCreate
beforeUpdate
afterUpdate
beforeSave (both create/update)
afterSave (both create/update)
beforeDelete
afterDelete

Mechanism for per-test request assertions

First off, thanks for the nice addon @samselikoff.

In acceptance tests, I oftentimes want to assert that my app made a request to the right endpoint with the right parameters. This can be accomplished by overriding request handlers in the test, like so:

test('Item should be hidden when the hide button is selected', function (assert) {
  let item = server.create('item');
  server.put('/items/:id', (schema, request) => {
    assert.equal(request.params.id, item.id);
    // perform the usual logic for this endpoint, then...
    return item;
  });

  // test
  visit(...);
});

This pattern works, but it entails duplicating whatever logic you may have in the request handler you overrode.

To solve this problem, I propose adding a hook that lets you examine the parameters passed to mirage before the request handler is invoked. Something like:

  server.beforePut('/items/:id', request => {
    assert.equal(request.items.id, expectedItemId);
  });

By default, the beforeVERB hooks would be NOOPs.
Any return value would be discarded.
All beforeVERB hooks would be reset to their default NOOP between tests.

I'm glad to do the work to add the feature, but wanted to discuss the idea and the potential API first.

Ability to throw a `Response` object

The thing that I am trying to implement is an authentication check for most of the routes. I imagined I could implement it roughly like this:

function checkAuth() {
  if (!isAuthenticated()) {
    throw new Response(401, {}, { message: 'Not Authenticated' });
  }
}

but it seems that throwing a response does not work as I would have expected and instead it is always resulting in an Internal Server Error.

@samselikoff do you think it would be useful to check if the thrown error is a Response instance and in that case use that instead of creating a new 500 response? if that sounds good to you I'd be happy to open a PR :)

Dynamic attrs in factories (functions) are called twice

This arose while doing this:

Factory.extend({
  id() {
    return autoincrementalIdSharedBySeveralModels();
  }
});

This code generates ids [2, 4, 6, 8, 10] etc...

I dig a bit into why this happens and it's because mirage allows functions to be called out of order. By example, the next code is allowed:

  let BazFactory = Mirage.Factory.extend({
    bar() {
      return this.foo + 2; // This function calls foo as a property despite of being a function
    },

    baz: 6,

    foo(i) {
      return this.baz * i;
    }
  });

To allow this mirage has to evaluate the functions twice, once to generate a Directed Acyclic Graph to know the exection order and the second to actually call them in order.

It's not easy to fix, but I think that if there functions were defined using getters/setters, none of this would be needed. However this outcome seems so obvious and the current solution so unintuitive that It makes me think that there is a reason I don't see why things are the way they are.

Was it because of IE8 support? Other?

Tracking issue for routing DSL / passthrough / namespace improvements

Some of the routing rules are confusing, we should just use a nested routing DSL like most other routing libs of this nature.

this.namespace('api', () => {
  this.resource('users')

  this.namespace('teams', () => {
    this.get('/settings')
  })
})

this.namespace('my.api.com', () => {
  this.passthrough()
})

just quick ideas/the sorts of things we need to support.

Question: Expose mocked API as standalone on a specific port?

Is there any way to expose the same mocked API on a port in NodeJS (like json-server)?
(instead of calling the create server on the front app)

Like running a http.createServer in NodeJS that responds with the mock? (or an express app)

I know it's not the miragejs objective but we have a use case where we want to use it in both contexts (front app and standalone HTTP server).

Hopes there is a way to do that because we like a lot mirage :)

Suggestion: Allow partial response mocking

I've just had a go at chucking this into a mature app, with an existing API.
I only need to make a small change; eg. add a couple of properties, to an existing call.

But I want the rest of the api response to remain the same.

It would be great if you could trap a response from the server & manipulate it before handing back to the calling code.

Starting mirage for Storybook

Feature request

The ability to use mirage's factories as a source of data when working with components in Storybook would be extremely useful. I've tried to get it up and running but I'm starting to feel out of my depth after a couple of hours trying, so far its just been resolving a seemingly endless permutation of Module not found: Error: Can't resolve $foo only to meet yet another Module not found: Error: Can't resolve $nextFoo ... so I'm starting to think I may be missing something fundamental and figured I'd see what the author/maintainers think about how to solve it.


If this is a feature request, add a ๐Ÿ‘ reaction to it. We use these to help us prioritize new feature development!

Response metadata

We need to come up with an API to attach metadata (page numbers, sort directions, collection size etc.) to a response.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.