21 |
22 | )
23 |
24 | window.onload = () => {
25 | render(, document.getElementById('root'))
26 | }
--------------------------------------------------------------------------------
/__tests__/normalization.spec.js:
--------------------------------------------------------------------------------
1 | import { query } from './client'
2 | import { PersonWithPetsWithFriend } from './client/fragments'
3 |
4 | const testNormalization = entities => {
5 | expect(entities.People).toBeDefined()
6 | expect(entities.Dogs).toBeDefined()
7 | expect(entities.Cats).toBeDefined()
8 | }
9 |
10 | it("Normalizes query responses correctly", () => {
11 | return query(PersonWithPetsWithFriend)
12 | .mock()
13 | .then((res, normalize) => {
14 | const {entities} = normalize(res.data)
15 |
16 | testNormalization(entities)
17 | })
18 | })
19 |
20 | it("Normalizes with the .normalize modifier", () => {
21 | return query(PersonWithPetsWithFriend)
22 | .mock()
23 | .normalize(res => {
24 | testNormalization(res.entities)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/docs/v0.7.x/api/FetchAPI.md:
--------------------------------------------------------------------------------
1 | # Fetch API
2 |
3 | The internal Fetch API used by modelizr. The API is a function that accepts all modifications made to the query tool calling it, and returns a promise containing the server result.
4 |
5 | Basic structure of the API
6 | ```javascript
7 | const api = ({body, path, contentType, headers, method}) => {
8 | return fetch(path, {
9 | headers: {
10 | 'Accept': 'application/json',
11 | 'Content-Type': contentType || 'application/json',
12 | ...headers || {}
13 | },
14 | method: method || 'POST',
15 | body
16 | }).then(res => res.json())
17 | }
18 | ```
19 |
20 | If you are wanting to provide your own custom fetch api, then it must return a promise in order to work with modelizr.
--------------------------------------------------------------------------------
/example/app/components/Example.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Component, PropTypes } from 'react'
3 | import { connect } from 'react-redux'
4 |
5 | const enhance = connect(({People, Animals, Cats, Dogs, Settings}) => ({
6 | People,
7 | Cats,
8 | Dogs,
9 | Animals,
10 | Settings
11 | }))
12 |
13 | export default
14 | enhance(class Example extends Component {
15 |
16 | state = {}
17 |
18 | componentDidMount() {
19 | this.props.actions.toggleMock()
20 | }
21 |
22 | action = name => e => {
23 | e.preventDefault()
24 | const {Settings} = this.props
25 |
26 | this.props.actions[name](Settings.mock)
27 | }
28 |
29 | render() {
30 | const {Settings} = this.props
31 |
32 | return (
33 |
34 |
35 |
36 | )
37 | }
38 | })
--------------------------------------------------------------------------------
/example/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { SET_ENTITIES, TOGGLE_MOCK } from '../actions/index'
2 | import { combineReducers } from 'redux'
3 |
4 | const entityReducer = entityType => (state = {}, action) => {
5 | switch (action.type) {
6 | case SET_ENTITIES: {
7 | return {...state, ...action.payload[entityType] || {}}
8 | }
9 |
10 | default:
11 | return state
12 | }
13 | }
14 |
15 | const settingsReducer = (state = {
16 | mock: false
17 | }, action) => {
18 | switch (action.type) {
19 | case TOGGLE_MOCK: {
20 | return {...state, mock: !state.mock}
21 | }
22 |
23 | default: {
24 | return state
25 | }
26 | }
27 | }
28 |
29 | export default combineReducers({
30 | People: entityReducer('People'),
31 | Cats: entityReducer('Cats'),
32 | Dogs: entityReducer('Dogs'),
33 | Settings: settingsReducer
34 | })
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express'),
2 | server = express(),
3 | bodyParser = require('body-parser'),
4 | _ = require('lodash'),
5 | path = require('path'),
6 | webpack = require('webpack'),
7 | config = require('./webpack.dev.js'),
8 | port = 8000
9 |
10 | server.use(require('webpack-dev-middleware')(webpack(config), {
11 | noInfo: true,
12 | publicPath: config.output.publicPath
13 | }))
14 |
15 | server.use(bodyParser.json())
16 | server.use('/graphql', function (req, res) {
17 | res.json(require('./data'))
18 | })
19 |
20 | server.use('/custom-request', function (req, res) {
21 | res.json({
22 | name: 'John'
23 | })
24 | })
25 |
26 | server.use('*', function (req, res) {
27 | res.sendFile(path.join(__dirname, 'index.html'))
28 | })
29 |
30 | server.listen(port, function () {
31 | console.log(`Server listening at http://localhost:${port}/`)
32 | })
33 |
--------------------------------------------------------------------------------
/src/tools/logger.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import _ from 'lodash'
3 |
4 | export type Logger = {
5 | add: (key: string, value: any) => any,
6 | print: () => any
7 | }
8 |
9 | /**
10 | * A logger utility to help debug and follow individual requests
11 | */
12 | export const createLogger = (name: string): Logger => {
13 | const group = []
14 | const time = Date.now()
15 |
16 | return {
17 | add: (key, value) => group.push({key, value}),
18 | print: () => {
19 |
20 | /* eslint-disable */
21 | if (typeof console.groupCollapsed === 'function') console.groupCollapsed(`${name} [${(Date.now() - time) / 1000}s]`)
22 |
23 | _.forEach(group, ({key, value}) => {
24 | if (typeof console.groupCollapsed === 'function') console.groupCollapsed(key)
25 | console.log(value)
26 | if (typeof console.groupEnd === 'function') console.groupEnd()
27 | })
28 |
29 | if (typeof console.groupEnd === 'function') console.groupEnd()
30 | /* eslint-enable */
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/docs/usage/Production.md:
--------------------------------------------------------------------------------
1 | # Production
2 |
3 | Modelizr relies on `fakerjs` in order to produce high quality mocks, however this is not so great for production builds where mocks aren't needed and small
4 | file sizes are required. Modelizr therefore only requires `fakerjs` while `NODE_ENV` is not set to `production` and as long as you have dead code removal
5 | in your build step, your production bundle will be small.
6 |
7 | In some situations you may want to keep the stripped dependencies in your production bundle for testing or other reasons. To do this you can manually pass
8 | `faker` into modelizr via configuration object.
9 | ```javascript
10 | import { Modelizr } from 'modelizr'
11 | import faker from 'fakerjs'
12 |
13 | const client = new Modelizr({
14 | models: { ... },
15 | config: {
16 | faker
17 | }
18 | })
19 | ```
20 |
21 | Now fakerjs will remain in production. You may also use this configuration option to pass in a custom fakerjs instance if you want to extend faker.
--------------------------------------------------------------------------------
/docs/v0.7.x/usage/Production.md:
--------------------------------------------------------------------------------
1 | # Production
2 |
3 | Modelizr relies on some large dependencies like `faker` in order to produce high quality mocks, however this is not so great for production builds where mocks aren't needed and small
4 | file sizes are required. Modelizr therefore only requires these large dependencies while `NODE_ENV` is not set to `production` and as long as you have dead code removal in your build
5 | step, your production bundle will be small.
6 |
7 | In some situations you may want to keep the stripped dependencies in your production bundle for testing or other reasons. To do this you can manually pass `faker` or `chance` to modelizr
8 | via the mock configuration object.
9 | ```javascript
10 | import { prepare } from 'modelizr'
11 | import faker from 'faker'
12 | import chance from 'chance'
13 |
14 | const { query, mutation } = prepare().mockConfig({
15 | exnensions: {
16 | faker: () => faker,
17 | chance: () => chance
18 | }
19 | })
20 | ```
21 | Now both `faker` and `chance` will remain in production.
--------------------------------------------------------------------------------
/docs/v0.7.x/README.md:
--------------------------------------------------------------------------------
1 | * [Introduction](/docs/Introduction.md)
2 | * [Usage](/docs/v0.7.x/usage/README.md)
3 | * [Creating Models](/docs/v0.7.x/usage/Models.md)
4 | * [Querying](/docs/v0.7.x/usage/Querying.md)
5 | * [Mocking](/docs/v0.7.x/usage/Mocking.md)
6 | * [Query Preparation](/docs/v0.7.x/usage/Preparing.md)
7 | * [Production](/docs/v0.7.x/usage/Production.md)
8 | * [Patterns](/docs/v0.7.x/patterns/README.md)
9 | * [Fragments](/docs/v0.7.x/patterns/Fragments.md)
10 | * [Modifiers](/docs/v0.7.x/modifiers/README.md)
11 | * [Model Modifiers](/docs/v0.7.x/modifiers/ModelModifiers.md)
12 | * [Query Modifiers](/docs/v0.7.x/modifiers/QueryModifiers.md)
13 | * [API Reference](/docs/v0.7.x/api/README.md)
14 | * [Model Creator](/docs/v0.7.x/api/ModelCreator.md)
15 | * [Union Creator](/docs/v0.7.x/api/UnionCreator.md)
16 | * [Models](/docs/v0.7.x/api/Models.md)
17 | * [Query Tools](/docs/v0.7.x/api/QueryTools.md)
18 | * [Mocks](/docs/api/v0.7.x/Mocks.md)
19 | * [Normalizr](/docs/v0.7.x/api/Normalizr.md)
20 | * [Fetch API](/docs/v0.7.x/api/FetchAPI.md)
21 | * [Changelog](changelog.md)
--------------------------------------------------------------------------------
/docs/patterns/Fragments.md:
--------------------------------------------------------------------------------
1 | # Fragments
2 |
3 | A useful pattern when making multiple queries that use the same sequence of models is to pre-define a query fragment. Something like this:
4 |
5 | ```javascript
6 | const {models, query} = new Modelizr({ ... })
7 |
8 | const {Person, Animal, Cat, Dog} = models
9 |
10 | const PersonFragment = Person(
11 | Animal("Pets",
12 | Dog, Cat
13 | )
14 | )
15 |
16 | query(
17 | PersonFragment
18 | ).then(res => { ... })
19 | ```
20 |
21 | A fragment can even be executed again without losing it's previous children
22 |
23 | ```javascript
24 | ...
25 |
26 | query(
27 | PersonFragment("People",
28 | Person("Friend")
29 | )
30 | )
31 |
32 | /* Resulting query will look as follows */
33 | {
34 | People {
35 | id,
36 | name,
37 | ...,
38 | Friend {
39 | id,
40 | name,
41 | ...
42 | },
43 | Pets {
44 | ... on Dog {
45 | __type,
46 | id,
47 | breed,
48 | ...
49 | },
50 |
51 | ... on Cat {
52 | __type,
53 | id,
54 | name,
55 | ...
56 | }
57 | }
58 | }
59 | }
60 | ```
--------------------------------------------------------------------------------
/docs/v0.7.x/usage/Preparing.md:
--------------------------------------------------------------------------------
1 | # Query Preparation
2 |
3 | Looking at the way we are using query tools up to this point, we can see how things can quick become verbose and out of hand when setting up our query tools.
4 | For instance, if we want to make queries, mutations and requests to the same endpoint, using the same headers and the same api - then we will need to apply our
5 | `.path()`, `.headers()` and `.api()` modifiers to all three tools.
6 |
7 | To solve this, modelizr exports a `prepare()` helper which allows you to chain modifiers on it. When you are done modifying, you can call `.get()` and have access to
8 | all three query tools.
9 |
10 | ```javascript
11 | import { prepare } from 'modelizr'
12 |
13 | const {
14 | query,
15 | mutation,
16 | request
17 | } = prepare().path("http:// ... ").headers({ ... }).api(() => {}).get()
18 |
19 | query( ... ).then( ... )
20 | ```
21 |
22 | Additionally, you can pass in a collection of custom modifiers that you would like available on all query tools.
23 |
24 | ```javascript
25 | const { request } = prepare({
26 | customPathModifiers: apply => path => apply(`http://localhost/${path}`)
27 | })
28 |
29 | request( ... ).customPathModifier('get-users')
30 | ```
31 |
32 | You can read up more on custom mutators in the [API Reference](../api/QueryTools.md#custom-modifiers)
33 |
--------------------------------------------------------------------------------
/__tests__/mocks.spec.js:
--------------------------------------------------------------------------------
1 | import { models, query } from './client'
2 | import { PersonWithFriend, PersonWithPets } from './client/fragments'
3 |
4 | const {Person, Dog} = models
5 |
6 | const personChecks = (person) => {
7 | expect(person).toBeDefined()
8 |
9 | expect(person.id).toMatch(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i)
10 | expect(typeof person.otherName).toBe("string")
11 |
12 | expect(person.aliases).toHaveLength(4)
13 | expect(person.aliases).toContain("alias")
14 |
15 | expect(person.age).toBeGreaterThan(0)
16 | expect(person.age).toBeLessThan(101)
17 | }
18 |
19 | it("Generates data with the correct structure and data", () => {
20 | return query(Person)
21 | .mock()
22 | .then(res => {
23 | return personChecks(res.data.Person)
24 | })
25 | })
26 |
27 | it("Generates an integer primary key for Number type models", () => {
28 | return query(Dog)
29 | .mock()
30 | .then(res => {
31 | expect(typeof res.data.Dog.ID).toBe("number")
32 | })
33 | })
34 |
35 | it("Generates child models correctly", () => {
36 | return query(PersonWithFriend)
37 | .mock()
38 | .then(res => {
39 | const {data: {Person: person}} = res
40 |
41 | personChecks(person)
42 | personChecks(person.Friend)
43 | })
44 | })
45 |
46 | it("Generates the schemaAttribute for unions", () => {
47 | return query(PersonWithPets)
48 | .mock()
49 | .then(res => {
50 | expect(res.data.Person.Pets[0].__type).toMatch(/Dog|Cat/)
51 | })
52 | })
--------------------------------------------------------------------------------
/src/tools/fetch.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { GraphQLError } from './public'
3 | import { ConfigType } from '../types'
4 | import fetch from 'isomorphic-fetch'
5 |
6 | /**
7 | * The fetch implementation that is used to make requests.
8 | *
9 | * It returns a promise containing a RequestResponse type object.
10 | * If the response from the api contains an 'errors' property,
11 | * a GraphQLError will be thrown. This behaviour can be disabled
12 | */
13 | export const FETCH_API = (config: ConfigType) => {
14 | const method: string = (config.method || "POST").toUpperCase()
15 | let server_response
16 |
17 | return fetch(config.endpoint, {
18 | headers: {
19 | 'Accept': 'application/json',
20 | 'Content-Type': 'application/json',
21 | ...config.headers
22 | },
23 | method,
24 | ...(method != "GET" && method != "HEAD" ? {
25 | body: JSON.stringify(config.body)
26 | } : {})
27 | })
28 | .then(res => {
29 | /* convert the body to json but also store the raw server
30 | * response so as to not lose any information.
31 | * */
32 | server_response = res
33 | return res.json()
34 | })
35 | .then(res => {
36 | /* If the GraphQL server responded with errors then throw
37 | * an error of type GraphQLError.
38 | * */
39 | if (res.errors && config.throwOnErrors) {
40 | if (config.throwOnErrors) throw new GraphQLError("The GraphQL server responded with errors.", res.errors)
41 | }
42 |
43 | return {
44 | server_response,
45 | data: res.data ? res.data : res
46 | }
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/docs/api/FetchAPI.md:
--------------------------------------------------------------------------------
1 | # Fetch API
2 |
3 | The internal Fetch API used by Modelizr. The API is a function that accepts all modifications made to the query tool calling it, and returns a promise
4 | containing the server result.
5 |
6 | Basic structure of the API
7 | ```javascript
8 | type ConfigType = {
9 | endpoint: string,
10 | api: Function,
11 | headers: ?{[key: string]: string},
12 | method: ?string,
13 | mock: ?boolean | Object,
14 | debug: ?boolean,
15 | body: ?Object,
16 | throwOnErrors: boolean
17 | }
18 |
19 | export const FETCH_API = (config: ConfigType) => {
20 | const method: string = (config.method || "POST").toUpperCase()
21 | let server_response
22 |
23 | return fetch(config.endpoint, {
24 | headers: {
25 | 'Accept': 'application/json',
26 | 'Content-Type': 'application/json',
27 | ...config.headers
28 | },
29 | method,
30 | ...(method != "GET" && method != "HEAD" ? {
31 | body: JSON.stringify(config.body)
32 | } : {})
33 | })
34 | .then(res => {
35 | server_response = res
36 | return res.json()
37 | })
38 | .then(res => {
39 | if (res.errors && config.throwOnErrors) {
40 | if (config.throwOnErrors) throw new GraphQLError("The GraphQL server responded with errors.", res.errors)
41 | }
42 |
43 | return {
44 | server_response,
45 | ...res
46 | }
47 | })
48 | }
49 | ```
50 |
51 | If you are wanting to provide your own custom fetch api, then it must return a promise in order to work with modelizr.
--------------------------------------------------------------------------------
/src/data/normalization.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ModelFunction, ClientStateType, ModelDataType } from '../types'
3 | import { normalize, arrayOf } from 'normalizr'
4 | import _ from 'lodash'
5 |
6 | type NormalizationParameters = {
7 | data: Object,
8 | modelFunctions: Array,
9 | clientState: ClientStateType
10 | }
11 |
12 | type NormalizedData = {
13 | entities: Object,
14 | result: Object
15 | }
16 |
17 | /**
18 | * A function that normalizes data based on specified model relationships using
19 | * normalizr.
20 | *
21 | * This will map over the provided models and pass in a normalizr schema containing
22 | * each models schema. If the entities from the response that match the model are
23 | * an array, the schema will be wrapped in arrayOf().
24 | */
25 | export default ({data, modelFunctions, clientState}: NormalizationParameters): NormalizedData | Object => {
26 | const {models} = clientState
27 |
28 | /* Create a map of ModelFunctions who's keys match
29 | * those of the Data result
30 | * */
31 | const keyedFunctions: {[key:string]: ModelFunction} = _.mapKeys(modelFunctions,
32 | (modelFunction: ModelFunction) =>
33 | modelFunction.fieldName)
34 |
35 | if (!data) return {}
36 | return normalize(data, _.mapValues(keyedFunctions, (modelFunction: ModelFunction) => {
37 | const entities = data[modelFunction.fieldName]
38 | const modelData: ModelDataType = models[modelFunction.modelName]
39 |
40 | if (Array.isArray(entities)) return arrayOf(modelData.normalizrSchema)
41 | return modelData.normalizrSchema
42 | }))
43 | }
--------------------------------------------------------------------------------
/docs/v0.7.x/api/Normalizr.md:
--------------------------------------------------------------------------------
1 | # Normalizer
2 |
3 | This is a wrapper around [normalizr](https://github.com/paularmstrong/normalizr). Please read the normalizr documentation if you are not already familiar with it.
4 |
5 | #### `normalize(response, ...models)`
6 |
7 | Normalize a response based on given models. Returns the normalized flat-map
8 |
9 | Given the following response:
10 | ```javascript
11 | const response = {
12 | users: [
13 | {
14 | id: 1,
15 | ...,
16 | books: [
17 | {
18 | id: 1,
19 | ...
20 | },
21 | ...
22 | ]
23 | },
24 | ...
25 | ]
26 | }
27 | ```
28 |
29 | ```javascript
30 | // ...
31 |
32 | import { normalize } from 'modelizr'
33 |
34 | normalize(
35 | response,
36 | user(
37 | book()
38 | )
39 | )
40 | ```
41 | Normalizr will produce the following flat-map:
42 | ```javascript
43 | {
44 | entities: {
45 | users: {
46 | 1: {
47 | id: 1,
48 | ...,
49 | books: [
50 | 1,
51 | ...
52 | ]
53 | }
54 | },
55 | books: {
56 | i: {
57 | id: 1,
58 | ...
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | ##### `arrayOf(model)`
66 |
67 | Extension of normalizrs `arrayOf` utility. Used to describe a model as an element in an array.
68 |
69 | ##### `valuesOf(model)`
70 |
71 | Extension of normalizrs `valuesOf` utility. Used to describe a model as a property in an object.
--------------------------------------------------------------------------------
/src/data/dataGeneration.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ModelDatatypeField } from '../types'
3 | import { v4 } from '../tools/uuid'
4 | import _ from 'lodash'
5 |
6 | let createFaker = () => {
7 | // eslint-disable-next-line no-console
8 | console.warn("Faker has been stripped from the production build")
9 | return false
10 | }
11 | if (process.env.NODE_ENV !== 'production') createFaker = () => require('faker')
12 |
13 | /* Generate some fake information based on the type of a field.
14 | * If the field type is an object, then we handle first the
15 | * __faker case, and second the __pattern case.
16 | *
17 | * if the __faker property is set, generate fake information
18 | * using fakerjs.
19 | *
20 | * If the __pattern property is set, split the property by the
21 | * delimiter "|" and select one of the resulting strings
22 | * */
23 | export const generator = (fakerInstance: Object): Function => (field: ModelDatatypeField): any => {
24 | const {type, faker, pattern, min, max, decimal} = field
25 |
26 | if (faker) {
27 | fakerInstance = fakerInstance || createFaker()
28 |
29 | if (!fakerInstance) return generator(fakerInstance)({...field, faker: null})
30 | return _.result(fakerInstance, faker)
31 | }
32 |
33 | if (pattern) {
34 | const options = pattern.split("|")
35 | const result = _.sample(options)
36 |
37 | if (type === Number) return parseInt(result)
38 | return result
39 | }
40 |
41 | switch (type) {
42 | case String: {
43 | return v4().substring(0, 7)
44 | }
45 |
46 | case Number: {
47 | return _.random(min || -10000, max || 10000, decimal || false)
48 | }
49 |
50 | case Boolean: {
51 | return !!_.random(1)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/docs/Introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Modelizr is a tool to simplify data handling in an application. It was specifically written for use with GraphQL - but if you have a REST api instead or some other achitecture, it will
4 | still work with modelizr.
5 |
6 | You can use modelizr for writing **queries**, **mocking**, making **requests** and **normalizing** data.
7 |
8 | #### `What it solves`
9 |
10 | 1. When working with a GraphQL server, you will almost always have deeply nested queries and responses that match - but no one wants to work with data that is structured like that.
11 | So we use normalizr, however we then need to write normalizr schemas and describe the structure of the response to normalizr in order for it to know how to flat-map the response.
12 | With modelizr, you get normalizr for free. You write a query and normalizr can infer how to flat-map your response.
13 |
14 | 2. Trying to integrate mocks into your application can be tedious, especially getting it to return data that matches your GraphQL responses. There is the option of having your
15 | API return mocks according to defined GraphQL types - but what if you haven't constructed the API yet and you still want to mock? Again you get this for free with modelizr.
16 | You write your queries once, and you get mocked data that matches the same structure as the query. Exactly how you would get your real response from GraphQL.
17 |
18 | 3. Writing queries can be ugly and hard to read, especially as it is hard to format in a text editor or IDE. Modelizr queries are more readable and easier to write.
19 |
20 | ___
21 |
22 | have a look at the [usage example](usage/README.md) to get an idea of how you can integrate it into your application.
--------------------------------------------------------------------------------
/docs/v0.7.x/Introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Modelizr is a tool to simplify data handling in an application. It was specifically written for use with GraphQL - but if you have a REST api instead or some other achitecture, it will
4 | still work with modelizr.
5 |
6 | You can use modelizr for writing **queries**, **mocking**, making **requests** and **normalizing** data.
7 |
8 | #### `What it solves`
9 |
10 | 1. When working with a GraphQL server, you will almost always have deeply nested queries and responses that match - but no one wants to work with data that is structured like that.
11 | So we use normalizr, however we then need to write normalizr schemas and describe the structure of the response to normalizr in order for it to know how to flat-map the response.
12 | With modelizr, you get normalizr for free. You write a query and normalizr can infer how to flat-map your response.
13 |
14 | 2. Trying to integrate mocks into your application can be tedious, especially getting it to return data that matches your GraphQL responses. There is the option of having your
15 | API return mocks according to defined GraphQL types - but what if you haven't constructed the API yet and you still want to mock? Again you get this for free with modelizr.
16 | You write your queries once, and you get mocked data that matches the same structure as the query. Exactly how you would get your real response from GraphQL.
17 |
18 | 3. Writing queries can be ugly and hard to read, especially as it is hard to format in a text editor or IDE. Modelizr queries are more readable and easier to write.
19 |
20 | ___
21 |
22 | have a look at the [usage example](usage/README.md) to get an idea of how you can integrate it into your application.
--------------------------------------------------------------------------------
/__tests__/generation.spec.js:
--------------------------------------------------------------------------------
1 | import { models, query, mutate } from './client'
2 | import { PersonWithFriend, PersonWithPets, PersonWithPetsWithFriend } from './client/fragments'
3 |
4 | const {Person} = models
5 |
6 | test("Query generation for Model with children", () => {
7 | query(PersonWithFriend).generate(query => {
8 | expect(query).toMatchSnapshot()
9 | })
10 | })
11 |
12 | test("Query generation for Model with a Union", () => {
13 | query(PersonWithPets).generate(query => {
14 | expect(query).toMatchSnapshot()
15 | })
16 | })
17 |
18 | test("Query generation for a modified fragment", () => {
19 | query(PersonWithPetsWithFriend).generate(query => {
20 | expect(query).toMatchSnapshot()
21 | })
22 | })
23 |
24 | test("Query generation for query and Model with parameters", () => {
25 | const model = PersonWithFriend({id: 1, arrayParameter: [1, "2"]})
26 | const queryParams = {parameter: 1, nestedParameter: {stringParameter: ""}}
27 |
28 | query(queryParams, model).generate(query => {
29 | expect(query).toMatchSnapshot()
30 | })
31 |
32 | test("Mutation query generation", () => {
33 | mutate(queryParams, model).generate(query => {
34 | expect(query).toMatchSnapshop()
35 | })
36 | })
37 | })
38 |
39 | test("Query generation for model using .only", () => {
40 | query(Person.only(["id"])).generate(query => {
41 | expect(query).toMatchSnapshot()
42 | })
43 | })
44 |
45 | test("Query generation for model using .without", () => {
46 | query(Person.without(["name"])).generate(query => {
47 | expect(query).toMatchSnapshot()
48 | })
49 | })
50 |
51 | test("Query generation for model using .empty", () => {
52 | query(Person.empty()).generate(query => {
53 | expect(query).toMatchSnapshot()
54 | })
55 | })
--------------------------------------------------------------------------------
/docs/v0.7.x/api/Models.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | The created model. This can be given to query tools to generate queries, mock data and normalize responses. Can also be used to validate entities.
4 |
5 | > Entity validation does not yet exist, but it is coming.
6 |
7 | Please note that this documentation also applies to **unions**.
8 |
9 | ### `model([key, params ,] ...models)`
10 |
11 | Define a nested query that can be generated or mocked. If nothing is given to the model, the resulting query will just contain the model key.
12 |
13 | `params [object]` - (optional) Parameters that get added to the generated request.
14 |
15 | `key [string]` - (optional) Pass an alternative key to use for the model. Can use this syntax as a replacement to `aliases` or the `.as()` modifier.
16 | Reference: [#2](https://github.com/julienvincent/modelizr/issues/2)
17 |
18 | **Note** When mocking, parameters that are specified as `primaryKeys` are used when mocking to create entities with expected ids. A single `param`
19 | will generate a single mocked entity, and an array of `[param]` will generate an array of entities
20 |
21 | ```javascript
22 | query(
23 | user(
24 | book()
25 | ).params({id: 2}),
26 |
27 | book({ids: [1, 2, 3]},
28 | user().as("author")
29 | )
30 | ).then( ... )
31 | ```
32 |
33 | ### `alias(model|union, key)`
34 |
35 | Create an alias of a **model** or a **union** to improve query readability. Functionally the same as `model().as(key)`
36 |
37 | ```javascript
38 | import { alias, query } from 'modelizr'
39 | import { book, user } from './models'
40 |
41 | const author = alias(user, "author")
42 |
43 | query(
44 | book(
45 | author()
46 | )
47 | ).then( ... )
48 | ```
--------------------------------------------------------------------------------
/docs/api/Modelizr.md:
--------------------------------------------------------------------------------
1 | # `Modelizr(options: object) => Client`
2 |
3 | The constructor for this library. Accepts model schemas and some configuration options. Returns the query tools and model functions
4 |
5 | ### options
6 |
7 | #### models `object`
8 |
9 | A collection of schemas that will be interpreted by Modelizr
10 |
11 | ```javascript
12 | import { Modelizr } from 'modelizr'
13 |
14 | const Person = {
15 | normalizeAs: "People",
16 | fields: { ... }
17 | }
18 |
19 | const client = new Modelizr({
20 | models: {
21 | Person: Person
22 | },
23 | config: {
24 | ...
25 | }
26 | })
27 | ```
28 |
29 | #### config `object`
30 |
31 | Global configuration properties
32 |
33 | ###### endpoint `string` `required`
34 |
35 | The endpoint to send requests to
36 |
37 | ###### mock `boolean`
38 |
39 | Whether or not to mock all requests. Defaults to `false`
40 |
41 | ###### debug `boolean`
42 |
43 | Whether or not to enable debugging. Defaults to `false`
44 |
45 | ###### throwsOnErrors `boolean`
46 |
47 | Whether or not to throw an error when a GraphQL response contains errors. Defaults to `true`
48 |
49 | Throws a `GraphQLError`:
50 |
51 | ```javascript
52 | import { GraphQLError } from 'modelizr'
53 |
54 | ...
55 |
56 | query(
57 | Person
58 | )
59 | .then(res => { ... })
60 | .catch(e => {
61 | if (e instanceof GraphQLError) {
62 | // contains graphQL errors
63 | }
64 | })
65 | ```
66 |
67 | ###### api `Function`
68 |
69 | Replace the fetch api used internally with a custom one. Have a look at the [Modelizr Fetch Api]() to see what Modelizr expects.
70 |
71 | ## Client
72 |
73 | The client that is constructed contains query tools and model functions generated from the provided model schemas
74 |
75 | ```javascript
76 | import { Modelizr } from 'modelizr'
77 |
78 | const {models, query, mutate, fetch} = new Modelizr({})
79 | ```
--------------------------------------------------------------------------------
/docs/v0.7.x/api/UnionCreator.md:
--------------------------------------------------------------------------------
1 | # Union Creator
2 |
3 | Instead of exporting a tool like `unionOf` - modelizr exports a **union creator**. It is used similarly to a model creator, and the resulting union can be used in the same
4 | manner as a **model**.
5 |
6 | #### `union(key, models, options)`
7 |
8 | Create a new union that can be used in queries, mocked and used to validate entities.
9 |
10 | + `key [string]` - The unions key. This will feature in generated queries, and will be used by normalizr to flat-map entities.
11 | + `models [object]` - A collection of models that belong to the union.
12 | + `options [object]` - Additional `normalizr` Schema options. The property `schemaAttribute` is required.
13 |
14 | The schemaAttribute is the field on the response that defines of what type the entity is.
15 |
16 | ```javascript
17 | import { union } from 'modelizr'
18 | import { user, group } from './models'
19 |
20 | const member = union('members', {
21 | user,
22 | group
23 | }, {schemaAttribute: "type"})
24 | ```
25 |
26 | You may also pass a function to infer the schema type. The function accepts the entity.
27 |
28 | When using a function instead of a string, you will need to specify the name of the schemaAttribute that will get mocked. You can do this in each models schema, or using the
29 | `mockAttribute` property.
30 |
31 | ```javascript
32 | const member = union('members', {
33 | user,
34 | group
35 | }, {schemaAttribute: entity => entity.type})
36 | ```
37 |
38 | This can now be used in a query
39 | ```javascript
40 | // ...
41 |
42 | import { query } from 'modelizr'
43 |
44 | query(
45 | member(
46 | user(),
47 | group()
48 | )
49 | ).then((res, normalize) => { ... })
50 | ```
51 |
52 | When mocking a union, a random model from one of the models defined in the query will be selected and mocked for each instance of the union. If no models are provided in the query,
53 | then a random model will be selected from its pre-defined collection of models and mocked for each instance of the union.
--------------------------------------------------------------------------------
/src/types/index.js:
--------------------------------------------------------------------------------
1 | export type ModelFunction = {
2 | (name: Object | string, params: Object | ModelFunction, models: Array): ModelFunction;
3 | modelName: string,
4 | fieldName: string,
5 | params: Object,
6 | children: Array,
7 | filters: ?{
8 | only: ?Array,
9 | without: ?Array,
10 | empty: ?boolean
11 | },
12 | only: (fields: Array) => ModelFunction,
13 | without: (fields: Array) => ModelFunction,
14 | empty: () => ModelFunction
15 | }
16 |
17 | export type ModelDatatypeField = {
18 | type: String | Number | Boolean | string,
19 | alias: string
20 | }
21 | export type ModelDataType = {
22 | normalizeAs: ?string,
23 | fields: {[field: string]: ModelDatatypeField},
24 | primaryKey: ?string,
25 | normalizrSchema: Object,
26 | normalizrOptions: ?Object
27 | }
28 |
29 | export type UnionDataType = {
30 | normalizeAs: ?string,
31 | models: Array,
32 | schemaAttribute: string | Function,
33 | _unionDataType: boolean
34 | }
35 |
36 | export type ConfigType = {
37 | endpoint: string,
38 | api: Function,
39 | headers: ?{[key: string]: string},
40 | method: ?string,
41 | mock: ?boolean | Object,
42 | debug: ?boolean,
43 | body: ?Object,
44 | throwOnErrors: boolean
45 | }
46 |
47 | export type ModelDataCollection = {[key: string]: ModelDataType | UnionDataType}
48 | export type ClientStateType = {
49 | config: ConfigType,
50 | models: ModelDataCollection
51 | }
52 |
53 | export type RequestResponse = {
54 | server_response: ?Object,
55 | data: Object,
56 | errors: Object,
57 | entities: ?Object,
58 | result: ?Object
59 | }
60 |
61 | export type RequestFunction = (value: any) => RequestObject
62 |
63 | export type RequestObject = {
64 | api: RequestFunction,
65 | endpoint: RequestFunction,
66 | headers: RequestFunction,
67 | method: RequestFunction,
68 | mock: RequestFunction,
69 | debug: RequestFunction,
70 | body: RequestFunction,
71 | throwOnErrors: RequestFunction,
72 | generate: RequestFunction,
73 | then: (cb: Function) => Promise,
74 | normalize: (cb: Function) => Promise,
75 | }
--------------------------------------------------------------------------------
/docs/v0.7.x/api/QueryTools.md:
--------------------------------------------------------------------------------
1 | # Query Tools
2 |
3 | ### `query(...models)`
4 |
5 | Generate a GraphQL query
6 |
7 | ```javascript
8 | query(
9 | user(),
10 | book()
11 | )
12 | ```
13 |
14 | ### `mutation(...models)`
15 |
16 | Generate a GraphQL mutation
17 |
18 | ```javascript
19 | mutation(
20 | user(),
21 | book()
22 | )
23 | ```
24 |
25 | ### `request(...models)`
26 |
27 | Create a non-graphql request where the response is expected to match the given models. Uses the `.body()` modifier to specify the request body
28 |
29 | ```javascript
30 | request(
31 | user()
32 | )
33 | .body({ ... })
34 | .normalize(res => {})
35 | ```
36 |
37 | ### `prepare(modifiers)`
38 |
39 | A utility method that allows you to apply modifiers to all three of the above query tools. `prepare` accepts a collection of custom modifiers that will get added to each
40 | query tools' list of modifiers.
41 |
42 | ###### `custom modifiers`
43 |
44 | Each modifier should be a nested function. The root function gets given two parameters:
45 |
46 | + `apply(key, value)` - This method sets the field `key` on the query tool to the value of `value` and then returns the modified query tool.
47 | + `valueOf(key)` - Returns the value of the field `key` on the query tool.
48 |
49 | The child function becomes the modifier. It accepts whatever is passed to it when called on a query tool. This function must return the result of `apply()`
50 |
51 | Here is a visual example of the above described pattern.
52 | ```javascript
53 | {
54 | customModifier: (apply, valueOf) => val => apply('key', val)
55 | }
56 | ```
57 |
58 | ###### `prepare.get()`
59 |
60 | Returns an object containing the three query tools
61 |
62 | ```javascript
63 | import { prepare } from 'modelizr'
64 |
65 | const { query, mutation, request } = prepare({
66 | customModifier: (apply, valueOf) => someValue => {
67 | return apply('someKey', {...valueOf('someKey'), ...someValue})
68 | }
69 | })
70 | .headers({ ... })
71 | .path("http:// ... ")
72 | .mockConfig({
73 | quantity: 5,
74 | delay: 300
75 | })
76 | .get()
77 |
78 | query(
79 | user()
80 | )
81 | .customModifier({ ... })
82 | .then( ... )
83 | ```
--------------------------------------------------------------------------------
/src/core/modelBuilder.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { normalizeFunctionParameters } from '../tools/filters'
3 | import { ModelFunction } from '../types'
4 | import _ from 'lodash'
5 |
6 | /**
7 | * Construct a functional representation of a model. This method contains
8 | * no field information but rather config that is used when generating
9 | * a query.
10 | *
11 | * The resulting ModelFunction is a model-builder function, meaning the result
12 | * of calling it is a new ModelFunction that contains the changes to the
13 | * original.
14 | */
15 | const createModel = (modelName: string) => {
16 |
17 | /* The ModelFunction that is returned. This stores information for
18 | * query generation such as the FieldName, ModelName of the data it
19 | * represents, query parameters and all children models it should
20 | * generate.
21 | * */
22 | const model: ModelFunction = (fieldName, modelParams, ...children) => {
23 | const {name, params, models} = normalizeFunctionParameters(fieldName, modelParams, children)
24 |
25 | const newModel = createModel(modelName)
26 | newModel.fieldName = name || model.fieldName
27 | newModel.params = {...model.params, ...params || {}}
28 | newModel.children = [...model.children, ...models || []]
29 |
30 | newModel.children = _.uniqBy(newModel.children, (child: ModelFunction) => child.fieldName)
31 |
32 | return newModel
33 | }
34 |
35 | /* We want to store the model name and field name
36 | * separately as the field name can change on a per
37 | * query basis, while the model name must always
38 | * remain consistent.
39 | * */
40 | model.modelName = modelName
41 | model.fieldName = modelName
42 |
43 | model.params = {}
44 | model.children = []
45 | model.filters = {}
46 |
47 | /* Modifiers that allows for white-listing and
48 | * black-listing model fields on a per-query basis.
49 | * */
50 | model.only = (fields: Array) => {
51 | const newModel = model()
52 | newModel.filters.only = [...model.filters.only || [], ...fields]
53 | return newModel
54 | }
55 |
56 | model.without = (fields: Array) => {
57 | const newModel = model()
58 | newModel.filters.without = [...model.filters.without || [], ...fields]
59 | return newModel
60 | }
61 |
62 | model.empty = () => {
63 | const newModel = model()
64 | newModel.filters.empty = true
65 | return newModel
66 | }
67 |
68 | return model
69 | }
70 |
71 | export default createModel
--------------------------------------------------------------------------------
/docs/usage/Mocking.md:
--------------------------------------------------------------------------------
1 | # Mocking
2 |
3 | The goal with mocking is to produce data that is structured in the exact same way as an actual GraphQL response.
4 |
5 | Lets mock the query for a Person we made in the previous step:
6 |
7 | ```javascript
8 | query(
9 | Person({id: 1},
10 | Animal("Pets"
11 | Dog, Cat
12 | )
13 | )
14 | ).mock() // => Appending the 'mock' results in a mocked response
15 | .then((res, normalize) => {
16 | ...
17 | })
18 | ```
19 |
20 | Our mocked response will now look something like this:
21 |
22 | ```javascript
23 | const res = {
24 | data: {
25 | Person: {
26 | id: "05438a43-00e5-4e2d-9720-ecb1bc9093b9",
27 | firstName: "0bf14db1",
28 | age: 50123,
29 | Friend: {
30 | id: "8a008933-b5c3-4809-aef7-5a3c34c1c3b6",
31 | firstName: "c6187c73",
32 | age: 45674
33 | },
34 | Pets: [
35 | {
36 | __type: "Cat",
37 | id: "efaaa503-d453-4191-a177-307774d2ab10",
38 | name: "f0b2b542"
39 | },
40 | {
41 | __type: "Dog",
42 | id: "6a146a9c-779b-486a-bc13-25b72c6d555e,
43 | breed: "6a146a9c",
44 | name: "b245d540"
45 | },
46 | ...
47 | ]
48 | }
49 | }
50 | }
51 | ```
52 |
53 | All the generated data is in the correct format in this example, but it isn't very human readable - for example, the persons name is `0bf14db1`. We can
54 | improve the quality of the mocked data by adding more information to our models.
55 |
56 | ```javascript
57 | const Person = {
58 | normalizeAs: "People",
59 | fields: {
60 | id: {
61 | type: String,
62 | alias: "ID" // The generated query will contain the alias ID
63 | },
64 | firstName: {
65 | type: String,
66 | faker: "name.firstName"
67 | },
68 | age: {
69 | type: Number,
70 | min: 1,
71 | max: 100
72 | },
73 | Friend: "Person", // Reference the 'Person' model as a relationship
74 | Pets: ["Animal"] // Wrapping a reference in an array indicates a collection
75 | },
76 | primaryKey: "id" // Which field to use as the primary key. Defaults to 'id'
77 | }
78 | ```
79 |
80 | Now when queries are mocked, the data generated will be much more accurate. here is an example of a mocked Person after updating the model:
81 |
82 | ```javascript
83 | const res = {
84 | data: {
85 | Person: {
86 | id: "05438a43-00e5-4e2d-9720-ecb1bc9093b9",
87 | firstName: "James",
88 | age: 32,
89 | ...
90 | }
91 | }
92 | }
93 | ```
--------------------------------------------------------------------------------
/src/tools/filters.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ModelFunction, ModelDataCollection, ModelDataType } from '../types'
3 | import _ from 'lodash'
4 |
5 | /* Strips a collection of fields of all model relationships
6 | * */
7 | export const stripRelationships = (fields: Object) => (
8 | _.pickBy(fields, field => {
9 | const check = type => typeof type !== "string"
10 |
11 | if (Array.isArray(field.type)) {
12 | return check(field.type[0])
13 | }
14 | return check(field.type)
15 | })
16 | )
17 |
18 | type NormalizedParameters = {
19 | name?: string,
20 | params: Object,
21 | models: Array
22 | }
23 |
24 | /* Given three parameters, figure out the type of each param and return
25 | * a corrected set of parameters.
26 | * */
27 | export const normalizeFunctionParameters = (name: string | Object | ModelFunction, params: Object | ModelFunction, models: Array): NormalizedParameters => {
28 | const trueModels = models
29 | let trueName,
30 | trueParams = {}
31 |
32 | if (typeof name === 'string') {
33 | trueName = name
34 |
35 | if (typeof params === 'function') trueModels.unshift(params)
36 | if (typeof params === 'object') trueParams = params
37 | } else {
38 | if (params) trueModels.unshift(params)
39 |
40 | if (typeof name === 'function') trueModels.unshift(name)
41 | if (typeof name === 'object') trueParams = name
42 | }
43 |
44 | return {name: trueName, params: trueParams, models: _.filter(trueModels, model => model)}
45 | }
46 |
47 | export const normalizeModelData = (modelData: ModelDataCollection): ModelDataCollection => {
48 | const normalizedData = _.mapValues(modelData, (model: ModelDataType) => {
49 | const normalize = fields => (
50 | _.mapValues(fields, field => {
51 | if (typeof field === "object" && !Array.isArray(field)) {
52 | let type = field.type
53 | if (Array.isArray(type)) type = type[0]
54 |
55 | if (type === Object) {
56 | return {
57 | ...field,
58 | properties: normalize(field.properties)
59 | }
60 | }
61 | return field
62 | }
63 |
64 | return {
65 | type: field
66 | }
67 | })
68 | )
69 |
70 | if (model.fields) {
71 | return {
72 | ...model,
73 | fields: normalize(model.fields)
74 | }
75 | }
76 |
77 | /* Describe the model schema as a union */
78 | if (model.models && model.schemaAttribute) {
79 | return {
80 | ...model,
81 | _unionDataType: true
82 | }
83 | }
84 |
85 | return model
86 | })
87 |
88 | return _.mapKeys(normalizedData, (model: ModelDataType, modelName: string) => {
89 | if (model.name) return model.name
90 | return modelName
91 | })
92 | }
--------------------------------------------------------------------------------
/__tests__/__snapshots__/generation.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test Query generation for Model with a Union 1`] = `
2 | "query modelizr_query {
3 | Person {
4 | id,
5 | name,
6 | otherName: middleName,
7 | aliases,
8 | age,
9 | licensed,
10 | Pets {
11 | ... on Cat {
12 | __type,
13 | id,
14 | name
15 | },
16 | ... on Dog {
17 | __type,
18 | ID,
19 | name,
20 | breed
21 | }
22 | }
23 | }
24 | }"
25 | `;
26 |
27 | exports[`test Query generation for Model with children 1`] = `
28 | "query modelizr_query {
29 | Person {
30 | id,
31 | name,
32 | otherName: middleName,
33 | aliases,
34 | age,
35 | licensed,
36 | Friend {
37 | id,
38 | name,
39 | otherName: middleName,
40 | aliases,
41 | age,
42 | licensed
43 | }
44 | }
45 | }"
46 | `;
47 |
48 | exports[`test Query generation for a modified fragment 1`] = `
49 | "query modelizr_query {
50 | Person {
51 | id,
52 | name,
53 | otherName: middleName,
54 | aliases,
55 | age,
56 | licensed,
57 | Pets {
58 | ... on Cat {
59 | __type,
60 | id,
61 | name
62 | },
63 | ... on Dog {
64 | __type,
65 | ID,
66 | name,
67 | breed
68 | }
69 | },
70 | Friend {
71 | id,
72 | name,
73 | otherName: middleName,
74 | aliases,
75 | age,
76 | licensed
77 | }
78 | }
79 | }"
80 | `;
81 |
82 | exports[`test Query generation for model using .empty 1`] = `
83 | "query modelizr_query {
84 | Person
85 |
86 | }"
87 | `;
88 |
89 | exports[`test Query generation for model using .only 1`] = `
90 | "query modelizr_query {
91 | Person {
92 | id
93 | }
94 | }"
95 | `;
96 |
97 | exports[`test Query generation for model using .without 1`] = `
98 | "query modelizr_query {
99 | Person {
100 | id,
101 | otherName: middleName,
102 | aliases,
103 | age,
104 | licensed
105 | }
106 | }"
107 | `;
108 |
109 | exports[`test Query generation for query and Model with parameters 1`] = `
110 | "query modelizr_query(parameter: 1,nestedParameter: {stringParameter:\"\"}) {
111 | Person(id: 1,arrayParameter: [1,\"2\"]) {
112 | id,
113 | name,
114 | otherName: middleName,
115 | aliases,
116 | age,
117 | licensed,
118 | Friend {
119 | id,
120 | name,
121 | otherName: middleName,
122 | aliases,
123 | age,
124 | licensed
125 | }
126 | }
127 | }"
128 | `;
129 |
--------------------------------------------------------------------------------
/docs/v0.7.x/modifiers/ModelModifiers.md:
--------------------------------------------------------------------------------
1 | # Model Modifiers
2 |
3 | > These modifiers only apply to **models** and **unions**.
4 |
5 | #### `as(key)`
6 |
7 | Accepts a new key which will be used when generating queries and mocking
8 |
9 | ```javascript
10 | query(
11 | book(
12 | user().as("author")
13 | )
14 | )
15 | ```
16 |
17 | #### `params(params)`
18 |
19 | Define parameters for the generated query. Parameters that are of type `array` will get directly mapped in, and parameters of type `object` will get stringified.
20 |
21 | ```javascript
22 | query(
23 | user().params({
24 | ids: [1, 2, 3],
25 | options: {isAdmin: true}
26 | })
27 | )
28 | ```
29 |
30 | #### `properties(props [, overwrite])`
31 |
32 | Add additional properties to the model, if overwrite is `true` - the models' props will be overwritten.
33 |
34 | It is advised to rather add all possible properties to the model before hand and then removed the optional properties from the `required` schema field, or use `.only()` and `.except()`.
35 |
36 | ```javascript
37 | query(
38 | user().properties({
39 | middleName: {type: "string"}
40 | })
41 | )
42 | ```
43 |
44 | #### `only(props)`
45 |
46 | Use only the properties specified in `[props]` when generating a query.
47 |
48 | ```javascript
49 | query(
50 | user().only(["id", "firstName"])
51 | )
52 | ```
53 |
54 | #### `except(props)`
55 |
56 | Use all properties except those specified in `[props]` when generating a query.
57 |
58 | ```javascript
59 | query(
60 | user().except(["lastName"])
61 | )
62 | ```
63 |
64 | #### `onlyIf(statement)`
65 |
66 | Only include this model if `statement` is `true`
67 |
68 | ```javascript
69 | query(
70 | user(
71 | book().onlyIf(shouldQueryBooks)
72 | )
73 | )
74 | ```
75 |
76 | #### `normalizeAs(key)`
77 |
78 | Generate a query using the models key, but normalize it under a different entity key. You shouldn't really be doing this.
79 |
80 | ```javascript
81 | query(
82 | book(
83 | user().normalizeAs("authors")
84 | )
85 | )
86 | ```
87 |
88 | #### `arrayOf()`
89 |
90 | Forcefully specify the model definition as an array. Should only be applied to top level models.
91 |
92 | ```javascript
93 | query(
94 | user(
95 | book()
96 | ).arrayOf()
97 | )
98 | ```
99 |
100 | #### `valuesOf()`
101 |
102 | Forcefully specify the model definition as values. Should only be applied to top level models.
103 |
104 | ```javascript
105 | query(
106 | user(
107 | book()
108 | ).valuesOf()
109 | )
110 | ```
111 |
--------------------------------------------------------------------------------
/docs/api/QueryTools.md:
--------------------------------------------------------------------------------
1 | # Query Tools
2 |
3 | Query tools accept model functions and use them to generate GraphQL queries.
4 |
5 | ###### `query(...models)` | `mutate(...models)`
6 |
7 | Generate a GraphQL query
8 |
9 | ```javascript
10 | query(
11 | Person("People",
12 | Animal("Pets", Cat)
13 | ),
14 | Dog("Dogs")
15 | ).then(res => {})
16 | ```
17 |
18 | ### Modifiers
19 |
20 | Modifiers are functions that can be called on query tools. Modifiers return the query tool which allows them to be chained.
21 |
22 | ###### `endpoint(url: string)`
23 |
24 | Change the endpoint the request will be sent to
25 |
26 | ###### `headers(headers: object)`
27 |
28 | Add request headers to the query
29 |
30 | ###### `api(fetchApi: Function => Promise)`
31 |
32 | Change the api used to make fetch requests
33 |
34 | ###### `mock([shouldMock: boolean])`
35 |
36 | Set whether the request should be mocked or not. `shouldMock` defaults to `true` when called
37 |
38 | ###### `debug([shouldDebug: boolean])`
39 |
40 | Set whether the request should `console.log` debug information while making the request. `shouldDebug` default to `true` when called
41 |
42 | ###### `throwOnErrors([shouldThrow: boolean])`
43 |
44 | Set whether the request should reject the promise when the GraphQL response contains errors. `shouldThrow` defaults to `true` when called
45 |
46 | ###### `generate(cb: Function)`
47 |
48 | When set, the generated query will be given to the callback
49 |
50 | ###### `then(cb: Function)`
51 |
52 | > Terminator
53 |
54 | When called, the request will be made. This returns a promise and the callback will be given the request `response` and an augmented `normalize` function
55 |
56 | example:
57 | ```javascript
58 | query(
59 | Person
60 | ).then((res, normalize) => {
61 | return normalize(res.data)
62 | }).then(normalizedEntities => {
63 | ...
64 | })
65 | ```
66 |
67 | ###### `normalize(cn: Function)`
68 |
69 | > Terminator
70 |
71 | Identical to `.then` except that it automatically normalizes the response before resolving
72 |
73 | ### Fetch
74 |
75 | There is another convenience query tool called `fetch`. This can be used to make a normal `REST` request where the expected response is formatted according to the
76 | model schemas.
77 |
78 | The only difference is that a graphQL query is not generated. The `body` of the request must be provided manually. In addition to the modifiers mentioned above,
79 | this tool has a few additional modifiers.
80 |
81 | ###### `method(method: string)`
82 |
83 | The method to use when making the query. Can be `GET` `POST` `HEAD` `UPDATE`
84 |
85 | ###### `body(body: object)`
86 |
87 | The body of the request. Automatically serialized
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "modelizr",
3 | "version": "1.0.4",
4 | "description": "Generate GraphQL queries from Data Models that can be mocked and normalized.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "start": "node example/index.js",
8 | "test": "jest",
9 | "lint": "eslint src",
10 | "flow": "flow",
11 | "build": "rm -rf lib && babel src -d lib",
12 | "prepublish": "yarn run build",
13 | "docs:clean": "rm -rf _book",
14 | "docs:prepare": "gitbook install",
15 | "docs:make": "yarn run docs:prepare && gitbook build",
16 | "docs:watch": "yarn run docs:prepare && gitbook serve",
17 | "docs:publish": "yarn run docs:clean && yarn run docs:make && cd _book && git init && git add . && git commit -m 'docs' && git checkout -b gh-pages && git push git@github.com:julienvincent/modelizr gh-pages --force && cd ../ && yarn run docs:clean"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/julienvincent/modelizr.git"
22 | },
23 | "keywords": [
24 | "modelizr",
25 | "graphql",
26 | "model",
27 | "normalizr",
28 | "mock",
29 | "queries"
30 | ],
31 | "author": "Julien Vincent",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/julienvincent/modelizr/issues"
35 | },
36 | "homepage": "https://github.com/julienvincent/modelizr#readme",
37 | "dependencies": {
38 | "faker": "^3.1.0",
39 | "isomorphic-fetch": "^2.2.1",
40 | "lodash": "^4.16.6",
41 | "normalizr": "^2.2.1"
42 | },
43 | "devDependencies": {
44 | "babel-cli": "^6.9.0",
45 | "babel-core": "^6.9.0",
46 | "babel-eslint": "^6.0.4",
47 | "babel-jest": "^18.0.0",
48 | "babel-loader": "^6.2.4",
49 | "babel-plugin-lodash": "^3.2.0",
50 | "babel-plugin-transform-flow-strip-types": "^6.18.0",
51 | "babel-preset-es2015": "^6.9.0",
52 | "babel-preset-react": "^6.16.0",
53 | "babel-preset-stage-0": "^6.5.0",
54 | "body-parser": "^1.15.0",
55 | "coveralls": "^2.11.15",
56 | "eslint": "^2.10.2",
57 | "eslint-plugin-babel": "^3.2.0",
58 | "express": "^4.13.4",
59 | "flow-bin": "^0.34.0",
60 | "jest": "^18.0.0",
61 | "react": "^15.1.0",
62 | "react-dom": "^15.1.0",
63 | "react-redux": "^4.4.5",
64 | "redux": "^3.5.2",
65 | "redux-devtools": "^3.3.1",
66 | "redux-devtools-dock-monitor": "^1.1.1",
67 | "redux-devtools-inspector": "0.9.0",
68 | "redux-thunk": "^2.1.0",
69 | "webpack": "1.14.0",
70 | "webpack-dev-middleware": "^1.6.1"
71 | },
72 | "jest": {
73 | "testPathIgnorePatterns": [
74 | "client"
75 | ],
76 | "collectCoverage": true
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/docs/usage/Models.md:
--------------------------------------------------------------------------------
1 | # Creating Models
2 |
3 | Modelizr is a tool that revolves around the models you create. Query generation, data mocking and normalization are all based on the structure of the models
4 | that you build.
5 |
6 | In this usage example we will be creating 4 models:
7 |
8 | + Person
9 | + Dog
10 | + Cat
11 | + Animal - `Union of Dog and Cat`
12 |
13 | Models are defined by creating schemas. These schemas are then converted into Modelizr models and can be then used in queries. When creating these schemas we need to describe:
14 |
15 | + The model's **normalization key**. This is a string that all variations of the model will be normalized as.
16 | + The model's **fields** and **field types**. These are used when generating the GraphQL request and mocking a response.
17 | + Other model relationships that are associated with the model.
18 |
19 | ```javascript
20 | const Person = {
21 | normalizeAs: "People",
22 | fields: {
23 | id: {
24 | type: String,
25 | alias: "ID" // The generated query will contain the alias ID
26 | },
27 | firstName: String,
28 | age: Number,
29 | Friend: "Person", // Reference the 'Person' model as a relationship
30 | Pets: ["Animal"] // Wrapping a reference in an array indicates a collection
31 | },
32 | primaryKey: "id" // Which field to use as the primary key. Defaults to 'id'
33 | }
34 | ```
35 |
36 | ```javascript
37 | const Dog = {
38 | normalizeAs: "Dogs",
39 | fields: {
40 | __type: String,
41 | id: String,
42 | breed: String,
43 | name: String,
44 | Owner: "Person"
45 | }
46 | }
47 | ```
48 |
49 | ```javascript
50 | const Cat = {
51 | normalizeAs: "Cats",
52 | fields: {
53 | __type: String,
54 | id: String,
55 | name: String,
56 | Owner: "Person"
57 | }
58 | }
59 | ```
60 |
61 | Creating a union is done using a utility tool. To create a union you need to define an array of **models** of which it is a union of, and a schemaAttribute which is a key on each model
62 | that describes the type of model it is.
63 |
64 | ```javascript
65 | import { union } from 'modelizr'
66 |
67 | const Animal = union({
68 | models: ["Cat", "Dog"],
69 | schemaAttribute: "__type"
70 | })
71 | ```
72 |
73 | Now that we have defined out model data, we can pass it into modelizr and get back model functions which are used when making queries:
74 |
75 | ```javascript
76 | import { Modelizr } from 'modelizr'
77 |
78 | const {models, query, mutate, fetch} = new Modelizr({
79 | models: {
80 | Person,
81 | Dog,
82 | Cat,
83 | Animal
84 | },
85 | config: {
86 | endpoint: "http:// ..."
87 | }
88 | })
89 |
90 | const {Person, Dog, Cat, Animal} = models
91 | ```
92 |
--------------------------------------------------------------------------------
/docs/api/Models.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | Modelizr model functions are generated from schemas. These model functions are what is used when constructing GraphQL queries and mutations. They have various
4 | properties to make it more convenient to work with them.
5 |
6 | Model functions are a type of `Builder Function` that means they always return another model function when called or chained. Although they are functions, they do
7 | not need to be executed in order to be used in queries, but more on that later.
8 |
9 | #### Execution `([fieldName: string, params: object, ...children: Model])`
10 |
11 | All models can be executed with three optional parameters. When executed, the model function returns a new model function.
12 |
13 | ###### fieldName `string`
14 |
15 | If specified, this changes the fieldName in the generated query. As an example, A model with `name: Person` would generate the following query:
16 |
17 | ```javascript
18 | {
19 | Person {
20 | id,
21 | name,
22 | ...
23 | }
24 | }
25 | ```
26 |
27 | Specifying the fieldName as friend - `Person("Friend")` - would generate the following:
28 |
29 | ```javascript
30 | {
31 | Friend {
32 | id,
33 | name,
34 | ...
35 | }
36 | }
37 | ```
38 |
39 | ###### params `object`
40 |
41 | If specified, this adds parameters to the generated query. As an example, `Person({id: 1})` would generate the following query:
42 |
43 | ```javascript
44 | {
45 | Person(id: 1) {
46 | id,
47 | name,
48 | ...
49 | }
50 | }
51 | ```
52 |
53 | ###### children `...Model`
54 |
55 | All additional arguments may be Model children and will result in the children model's fields being added as sub-fields. As an example, `Person(Dog)` will
56 | generate the following query:
57 |
58 | ```javascript
59 | {
60 | Person {
61 | id,
62 | name,
63 | ...,
64 | Dog {
65 | id,
66 | ...
67 | }
68 | }
69 | }
70 | ```
71 |
72 | #### Modifiers
73 |
74 | In addition to execution parameters, there are a few modifiers which can be applied to models. Modifiers also return a new Model Function when called.
75 |
76 | ###### `.only(fields: Array)`
77 |
78 | Limits the generated fields to only contain fields
79 |
80 | ```javascript
81 | query(
82 | Person.only(["name"])
83 | )
84 |
85 | ...
86 |
87 | {
88 | Person {
89 | name
90 | }
91 | }
92 | ```
93 |
94 | ###### `.without(fields: Array)`
95 |
96 | Limits the generated fields to not contain certain fields
97 |
98 | ```javascript
99 | query(
100 | Person.without(["name"])
101 | )
102 |
103 | ...
104 |
105 | {
106 | Person {
107 | id,
108 | ...
109 | }
110 | }
111 | ```
112 |
113 | ###### `.empty()`
114 |
115 | Remove all fields from the generated query. Commonly used when making empty mutations
116 |
117 | ```javascript
118 | mutate(
119 | Person({id: 1, name: "James"}).empty()
120 | )
121 |
122 | ...
123 |
124 | mutation modelizr_mutation {
125 | Person(id: 1, name: "James")
126 | }
127 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # modelizr
2 | [](https://coveralls.io/github/julienvincent/modelizr?branch=master)
3 | [](https://travis-ci.org/julienvincent/modelizr)
4 | [](https://badge.fury.io/js/modelizr)
5 | [](https://gitter.im/julienvincent/modelizr?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
6 |
7 | A Combination of normalizr, fakerjs and GraphQL that allows you to define multipurpose models that can generate GraphQL queries, mock deeply nested data and normalize
8 |
9 | ## Installation
10 |
11 | `$ yarn add modelizr`
12 |
13 | ## What can I use this for?
14 |
15 | + Easily generating GraphQL queries from models.
16 | + Flat-mapping responses using [normalizr](https://github.com/gaearon/normalizr).
17 | + Mocking deeply nested data that match the structure of a GraphQL query.
18 |
19 | ___
20 |
21 | Read my [medium post](https://medium.com/@julienvincent/modelizr-99e59c1c4431#.applec5ut) on why I wrote modelizr.
22 |
23 | ## What does it look like?
24 |
25 | ```javascript
26 | import { Modelizr } from 'modelizr'
27 |
28 | const ModelData = {
29 | Person: {
30 | normalizeAs: "People",
31 | fields: {
32 | id: Number,
33 | firstName: String,
34 | Books: ["Book"]
35 | }
36 | },
37 |
38 | Book: {
39 | normalizeAs: "Books",
40 | fields: {
41 | id: Number,
42 | title: String,
43 | Author: "Person"
44 | }
45 | }
46 | }
47 |
48 | const {query, models: {Person, Book}} = new Modelizr({
49 | models: ModelData,
50 | config: {
51 | endpoint: "http:// ..."
52 | }
53 | })
54 |
55 | query(
56 | Person({id: 1}
57 | Book("Books")
58 | ),
59 |
60 | Book("Books", {ids: [4, 5]})
61 | ).then((res, normalize) => {
62 | normalize(res.body) // -> normalized response.
63 | })
64 | ```
65 | This will generate the following query and make a request using it.
66 | ```
67 | {
68 | Person(id: 1) {
69 | id,
70 | firstName,
71 | Books {
72 | id,
73 | title
74 | }
75 | },
76 |
77 | Books(ids: [4, 5]) {
78 | id,
79 | title,
80 | Author {
81 | id,
82 | firstName
83 | }
84 | }
85 | }
86 | ```
87 |
88 | ## Documentation
89 |
90 | **NOTE:** Documentation for pre-`v1.0.0` can be found [Here](https://github.com/julienvincent/modelizr/tree/master/docs/v0.7.x)
91 |
92 | All documentation is located at [julienvincent.github.io/modelizr](http://julienvincent.github.io/modelizr)
93 |
94 | * [Usage](http://julienvincent.github.io/modelizr/docs/usage)
95 | * [Patterns](http://julienvincent.github.io/modelizr/docs/patterns)
96 | * [API Reference](http://julienvincent.github.io/modelizr/docs/api)
97 | * [Changelog](https://github.com/julienvincent/modelizr/releases)
98 |
99 | ## Example
100 |
101 | + `$ yarn`
102 | + `$ yarn start`
103 |
104 | navigate to `http://localhost:8000` in your browser
105 |
--------------------------------------------------------------------------------
/docs/v0.7.x/usage/Models.md:
--------------------------------------------------------------------------------
1 | # Creating Models
2 |
3 | Modelizr is a tool that revolves around the models you create. Generated queries, mocked data and normalizing are all based on of the structure of the models
4 | that you define. The models are a mix between json-schema-faker schemas and normalizr schemas.
5 |
6 | When defining normalizr schemas, normalizr provides you with three tools - `arrayOf()`, `valuesOf()` and `unionOf` - to help describe your data structure. Modelizr
7 | exports similar tools **but they are not the same**. Do not attempt to use normalizr's tools when working with modelizr models. Additionally, modelizrs' version of `unionOf()`
8 | (called `union`) should be used in a similar manner to `models` - but more on that later.
9 |
10 | Going forward with this usage example, we will be creating three models - **user**, **group** and **book** - and a union model - **owner**. Each model will get a key to describe
11 | its entity collection, and a schema to describe the structure of the model.
12 |
13 | ```javascript
14 | import { model, union } from 'modelizr'
15 |
16 | const user = model('users', {
17 | id: {type: "primary"},
18 | firstName: {type: "string", faker: "name.firstName"},
19 | lastName: {type: "string", faker: "name.lastName"}
20 | })
21 |
22 | const book = model('books', {
23 | id: {type: "primary"},
24 | title: {type: "string"},
25 | publisher: {type: "string"}
26 | })
27 |
28 | const group = model('groups', {
29 | id: {type: "primary"}
30 | })
31 | ```
32 | These are basic model representations. Look at the [API reference](../api/ModelCreator.md) to see what else can be done with schemas.
33 |
34 | We can now further describe the relationship between these two models. A **user** might own a collection of books, which would result in an array of the **book** model.
35 |
36 | ```javascript
37 | import { arrayOf } from 'modelizr'
38 |
39 | user.define({
40 | books: arrayOf(book)
41 | })
42 | ```
43 | Or as a shorthand for `arrayOf` we can wrap the model in `[]`.
44 | ```javascript
45 | user.define({
46 | books: [book]
47 | })
48 | ```
49 |
50 | A **book** might have an author, which would result in a single **user** model. We can define this like so:
51 |
52 | ```javascript
53 | book.define({
54 | author: user
55 | })
56 | ```
57 |
58 | A **group** will just have a collection of users. In other words, an `arrayOf(user)`
59 |
60 | ```javascript
61 | group.define({
62 | users: [user]
63 | })
64 | ```
65 |
66 | And finally our **owner** union will be a collection of users and groups. The union needs to have a `schemaAttribute` specified to allow normalizr to determine which entity belongs
67 | to which model.
68 |
69 | ```javascript
70 | const owner = union('owners', {
71 | user,
72 | group
73 | }, {schemaAttribute: 'type'})
74 | ```
75 | If we had a set of data with the following structure:
76 | ```javascript
77 | {
78 | owners: [
79 | {
80 | id: 1,
81 | ...,
82 | type: "user"
83 | },
84 |
85 | {
86 | id: 5,
87 | users: [ ... ],
88 | type: "group"
89 | }
90 | ]
91 | }
92 | ```
93 | The `schemaAttribute` we defined on the **collection** union would allow normalizr to figure out the model by looking at each entities `type` field.
--------------------------------------------------------------------------------
/docs/v0.7.x/usage/Mocking.md:
--------------------------------------------------------------------------------
1 | # Mocking
2 |
3 | Once you have created your models and setup your queries, you essentially get mocking for free. All you need to do is hack on a `.mock()` modifier, and the query will return a
4 | response that matches the structure of your query.
5 |
6 | When mocking, you will never get conflicting id's on entities, no matter how deeply nested they are. Even if you explicitly reference the same id on a model in multiple places, you
7 | will still only get a single mocked entity in return.
8 |
9 | For instance, given the following mocked query:
10 | ```javascript
11 | query(
12 | user(
13 | book()
14 | )
15 | )
16 | .mock()
17 | .then( ... )
18 | ```
19 | We will get the following response:
20 | ```javascript
21 | {
22 | users: [
23 | {
24 | id: 1,
25 | firstName: " ... ",
26 | lastName: " ... ",
27 | books: [
28 | {
29 | id: 1,
30 | title: " ... ",
31 | publisher: " ... "
32 | },
33 | ...
34 | ]
35 | },
36 |
37 | {
38 | id: 2,
39 | ...,
40 | books: [
41 | {
42 | id: 21,
43 | ...
44 | }
45 | ]
46 | },
47 | ...
48 | ]
49 | }
50 | ```
51 |
52 | If you would rather use randomly generated id's, you can use modelizr's `UUID_V4` generator
53 | ```javascript
54 | import { query, ID } from 'modelizr'
55 |
56 | query(
57 | user()
58 | )
59 | .mock(true, {
60 | idType: ID.RANDOM // defaults to ID.INCREMENT
61 | })
62 | ```
63 | ->
64 | ```javascript
65 | {
66 | users: {
67 | c32f6a49-18ee-4786-a188-0101bf03acd1: {
68 | id: "c32f6a49-18ee-4786-a188-0101bf03acd1",
69 | firstName: " ... ",
70 | lastName: " ... "
71 | },
72 | ...
73 | }
74 | }
75 | ```
76 |
77 | As there is no way for modelizr to determine if the top level model should be mocked as values, elements in an array or simple a lone entity (**Note** modelizr can determine
78 | this information __after__ the request has completed by examining the response.), you will need to use the modifiers `.valuesOf(schemaAttribute)` and `.arrayOf(schemaAttribute)`
79 | to infer the way in which it should be mocked.
80 |
81 | > **Do not use these modifiers on child models.**
82 |
83 | Here is how it is used:
84 | ```javascript
85 | query(
86 | user(
87 | book()
88 | ).valuesOf("type")
89 | )
90 | ```
91 | ->
92 | ```javascript
93 | {
94 | users: {
95 | 1: {
96 | id: 1,
97 | firstName: " ... ",
98 | lastName: " ... ",
99 | type: "users",
100 | books: [ ... ]
101 | },
102 | ...
103 | }
104 | }
105 | ```
106 |
107 | If you would like to more precisely configure mock generation, there is a `.mockConfig()` modifier that can be applied to any query tool.
108 |
109 | ```javascript
110 | query( ... )
111 | .mockConfig({
112 | extensions: {
113 | faker: faker => {}
114 | },
115 | quantity: 25,
116 | delay: 200,
117 | error: false
118 | })
119 | ```
120 |
121 | Check out the entire configuration object [here](../api/Mocks.md#mock-configuration)
122 |
123 | Please refer to [json-schema-faker](https://github.com/json-schema-faker/json-schema-faker#custom-formats) to learn about these different options
--------------------------------------------------------------------------------
/docs/v0.7.x/api/ModelCreator.md:
--------------------------------------------------------------------------------
1 | # Model Creator
2 |
3 | ## `model(key, schema, [, options])`
4 |
5 | Create a new model that can be used in queries, mocked and used to validate entities.
6 |
7 | + `key [string]` - The model key. This will feature in generated queries, and will be used by normalizr to flat-map entities.
8 | + `schema [object]` - Define the structure of the model.
9 | + `options [object]` - Additional `normalizr` Schema options.
10 |
11 | The schema of the model should follow the [json-schema](http://json-schema.org/) spec, with the inclusion of json-schema-faker and some modelizr specific fields.
12 |
13 | ```javascript
14 | import { model } from 'modelizr'
15 |
16 | const user = model('users', {
17 | properties: {
18 | id: {type: 'integer', alias: 'ID'},
19 | firstName: {type: 'string', faker: 'name.firstName'},
20 | lastName: {type: 'string', faker: 'name.lastName'},
21 | type: {type: 'schemaAttribute'}
22 | },
23 | required: ['firstname'], // If not included, defaults to all fields that are a part of properties
24 | primaryKey: 'id'
25 | })
26 | ```
27 | + The `primaryKey` field is used when mocking, and by normalizr when flat-mapping.
28 | + The `alias` property (show at the `id` field) will produce a GraphQL alias when generating the query.
29 | + The `schemaAttribute` type definition is used in conjunction with a union's `schemaAttribute`. Only necessary when the unions `schemaAttribute` is a function.
30 |
31 | If you only care about defining the properties of a model, then that's all you need to define.
32 | ```javascript
33 | const user = model('users', {
34 | id: {type: 'primary|integer'},
35 | firstName: {type: 'string', faker: 'name.firstName'},
36 | lastName: {type: 'string', faker: 'name.lastName'},
37 | type: {type: 'schemaAttribute'}
38 | })
39 | ```
40 | You specify the primary key through the `type` field - and separate the actual type with a `|`. The `required` field is automatically fulled with all properties you have defined.
41 |
42 | You may also specify models as property types. You do not need to specify the faker property if you have already specified the type as a model. Modelizr will mock the field based on the
43 | given model.
44 | ```javascript
45 | const user = model('users', {
46 | ...
47 | })
48 |
49 | const book = model('books', {
50 | ...,
51 | author: {type: book}
52 | })
53 | ```
54 |
55 | #### `.define(relationships)`
56 |
57 | Describe the models relationship with other models. Modelizr additionally exports `arrayOf` and `valuesOf` utilities.
58 |
59 | > **They are not the same as normalizrs utilities**.
60 |
61 | ```javascript
62 | // ...
63 |
64 | import { arrayOf, valuesOf } from 'modelizr'
65 |
66 | user.define({
67 | books: arrayOf(book)
68 | })
69 |
70 | book.define({
71 | author: user,
72 | editors: valuesOf(user)
73 | })
74 | ```
75 | Or you can use the `[]` shorthand for `arrayOf` definitions
76 |
77 | ```javascript
78 | user.define({
79 | books: [book]
80 | })
81 | ```
82 |
83 | #### `.primaryKey(key)`
84 |
85 | Explicitly set the primary key of the model
86 |
87 | #### `.getKey()`
88 |
89 | Get the models entity key.
90 |
91 | #### `.setSchema(schema [, options])`
92 |
93 | Explicitly set the models schema. This is useful if you have circular model dependencies in your schema. For example:
94 |
95 | ```javascript
96 | const user = model('users')
97 | const book = model('books', {
98 | id: {type: 'integer'},
99 | ...
100 | })
101 |
102 | const collection = union('collections', {
103 | books: book,
104 | users: user
105 | }, {schemaAttribute: 'type'})
106 |
107 | user.setSchema({
108 | id: {type: 'integer', alias: 'ID'},
109 | ...,
110 | collections: {type: collection}
111 | })
112 | ```
--------------------------------------------------------------------------------
/docs/usage/Querying.md:
--------------------------------------------------------------------------------
1 | # Making Queries
2 |
3 | Models can be used inside of **query tools** to generate a GraphQL query and send it off to the server.
4 |
5 | Lets try use our models we have defined to query a Person, the persons Friend and his Pets.
6 |
7 | ```javascript
8 | ...
9 |
10 | const {Person, Dog, Cat, Animal} = models
11 |
12 | query(
13 | Person({id: 1},
14 | Person("Friend"),
15 |
16 | Animal("Pets"
17 | Dog, Cat
18 | )
19 | )
20 | ).then((res, normalize) => {
21 | ...
22 | })
23 | ```
24 |
25 | Internally this will generate a GraphQL query and post it to the configured endpoint. The query generated will look as follows:
26 |
27 | ```javascript
28 | query modelizr_query {
29 | Person(id: 1) {
30 | id,
31 | firstName,
32 | age,
33 | Friend {
34 | id,
35 | firstName,
36 | age
37 | },
38 | Pets: {
39 | ... on Cat {
40 | __type,
41 | id,
42 | name
43 | },
44 |
45 | ... on Dog {
46 | __type,
47 | id,
48 | breed,
49 | name
50 | }
51 | }
52 | }
53 | }
54 | ```
55 |
56 | The actual GraphQL response received might look something like this:
57 |
58 | ```javascript
59 | const res = {
60 | data: {
61 | Person: {
62 | id: 1,
63 | firstName: "John",
64 | age: 20,
65 | Friend: {
66 | id: 2,
67 | firstName: "Jimmy",
68 | age: 21
69 | },
70 | Pets: [
71 | {
72 | __type: "Cat",
73 | id: 1,
74 | name: "James"
75 | },
76 | {
77 | __type: "Dog",
78 | id: 3,
79 | breed: "Labrador",
80 | name: "Bran"
81 | },
82 | ...
83 | ]
84 | }
85 | }
86 | }
87 | ```
88 |
89 | This is expected, but not something we want to work with. Within our application's state we want to store data in a flat map. Modelizr can do this by
90 | looking at the model relationships already defined previously. Internally it uses [normalizr](https://github.com/paularmstrong/normalizr) to achieve this.
91 | Here is how we can normalize our response:
92 |
93 | ```javascript
94 | ...
95 |
96 | query(
97 | Person({id: 1},
98 | Animal("Pets"
99 | Dog, Cat
100 | )
101 | )
102 | ).then((res, normalize) => {
103 | const {entities, result} = normalize(res.data)
104 |
105 | /* And now we can use it in our application */
106 | store.dispatch({
107 | type: "SET_ENTITIES",
108 | payload: entities
109 | })
110 | })
111 | ```
112 |
113 | The resulting flattened entities will now look like this:
114 |
115 | ```javascript
116 | const entities = {
117 | People: {
118 | 1: {
119 | id: 1,
120 | firstName: "John",
121 | age: 20,
122 | Friend: 2,
123 | Pets: [
124 | {
125 | id: 1,
126 | schema: "Cat"
127 | },
128 | {
129 | id: 3,
130 | schema: "Dog",
131 | },
132 | ...
133 | ]
134 | },
135 | 2: {
136 | id: 2,
137 | firstName: "Jimmy",
138 | age: 21
139 | }
140 | },
141 |
142 | Cats: {
143 | 1: {
144 | id: 1,
145 | name: "James"
146 | }
147 | },
148 |
149 | Dogs: {
150 | 3: {
151 | id: 3,
152 | name: "Brad",
153 | breed: "Labrador"
154 | }
155 | }
156 | }
157 | ```
158 |
159 | Making a mutation is just as simple. Lets try changing a Persons name and age!
160 |
161 | ```javascript
162 | mutate(
163 | Person({id: 1, firstName: "James", age: 21})
164 | )
165 | ```
166 |
167 | This will generate a GraphQL mutation and post it at the configured endpoint:
168 |
169 | ```javascript
170 | mutation modelizr_query {
171 | Person(id: 1, firstName: "James", age: 21) {
172 | id,
173 | firstName,
174 | age
175 | }
176 | }
177 | ```
178 |
179 | Now on to mocking!
--------------------------------------------------------------------------------
/docs/api/Mocks.md:
--------------------------------------------------------------------------------
1 | # Mocks
2 |
3 | #### `mock(...models)`
4 |
5 | Generate fake data that follows the given model definitions.
6 |
7 | ```javascript
8 | import { mock } from 'modelizr'
9 |
10 | mock(
11 | user()
12 | ).then((res, normalize) => {
13 | ...
14 | })
15 | ```
16 |
17 | ###### ID Mocking
18 |
19 | There are two types of id mocking provided by modelizr and they can be toggled in the `configuration object`. The default type, `INCREMENT`, will generate incrementing numerical
20 | ids for each entity. The second, `RANDOM`, will generate a `UUID` id using the `V4` protocol. If you would like to provide your own id generator, you can do so in the
21 | `configuration object`. It will only be used when `idType` is set to `RANDOM`.
22 |
23 | Example of changing the idType and adding a custom generator.
24 | ```javascript
25 | import { query, ID } from 'modelizr'
26 |
27 | query(
28 | user()
29 | )
30 | .mock(true, {
31 | idType: ID.RANDOM,
32 | idGenerator: () => Math.random()
33 | })
34 | .then((res, normalize) => { ... })
35 | ```
36 |
37 | The mocking 'algorithm' for `INCREMENT` is as follows:
38 |
39 | + If a parameter that matches the models `primaryKey` is provided, then mocking will follow the following pattern:
40 | + If the `primaryKey` parameter is an array, then an array of entities will be generated with matching values.
41 | + If the `primaryKey` parameter is an integer, then a single entity with that value will be generated
42 |
43 | + If no `primaryKey` parameter is provided, then mocks will be generated as follows:
44 | + If the entity is a top level query, then 20 entities will be generated with ids `1 => 20`. (the quantity can be changed through `.mockConfig({ ... })`)
45 | + If the entity is nested and is defined in its parent model then it will be mocked according to its normalizr definition. eg:
46 | + `arrayOf()` and `valueOf()` will generate 20 entities with ids `n + 1 => n + 20`
47 | + A plain `model` will generate a single entity with id `n + 1`
48 |
49 | ###### Mocking Mutations
50 |
51 | When mocking a `mutation`, model parameters that match their properties will be given to the generated entities, and model parameters that match nested models will be treated as the
52 | nested models' id's. For example:
53 | ```javascript
54 | import { mutation } from 'modelizr'
55 | import { user, book } from './models.js'
56 |
57 | mutation(
58 | user({id: "PRESET_ID", firstName: "PRESET_NAME", books: ["PRESET_BOOK_1", "PRESET_BOOK_2"]},
59 | book()
60 | )
61 | )
62 | .mock()
63 | .query()
64 | .normalize(res => { ... })
65 |
66 | // The result
67 | {
68 | users: {
69 | PRESET_ID: {
70 | id: "PRESET_ID",
71 | firstName: "PRESET_NAME",
72 | books: [
73 | "PRESET_BOOK_1",
74 | "PRESET_BOOK_2"
75 | ]
76 | ... // other properties get generated normally
77 | }
78 | },
79 |
80 | books: {
81 | PRESET_BOOK_1: {
82 | id: "PRESET_BOOK_1",
83 | ...
84 | },
85 | PRESET_BOOK_2: {
86 | id: "PRESET_BOOK_2",
87 | ...
88 | }
89 | }
90 | }
91 | ```
92 |
93 | ###### Mock Configuration
94 |
95 | ```javascript
96 | {
97 | extensions: {
98 | faker: faker => {},
99 | chance: chance => {}
100 | },
101 | formats: {
102 | semver: (gen, schema) => {}
103 | },
104 | jsfOptions: {
105 | failOnInvalidTypes: true,
106 | ...
107 | },
108 | quantity: 25, // Amount of entities to generate unless otherwise specified in a query
109 | delay: 200, // Add a delay before mocking. Default is 0ms
110 | error: true, // Always throw an error when mocking. Default is false
111 | idType: ID.RANDOM, // The type of id to generate. Defaults to ID.INCREMENT
112 | idGenerator: () => Math.random() // A custom id generator to use when idType is set to random. Defaults to a UUID_V4 generator
113 | }
114 | ```
--------------------------------------------------------------------------------
/docs/v0.7.x/api/Mocks.md:
--------------------------------------------------------------------------------
1 | # Mocks
2 |
3 | #### `mock(...models)`
4 |
5 | Generate fake data that follows the given model definitions.
6 |
7 | ```javascript
8 | import { mock } from 'modelizr'
9 |
10 | mock(
11 | user()
12 | ).then((res, normalize) => {
13 | ...
14 | })
15 | ```
16 |
17 | ###### ID Mocking
18 |
19 | There are two types of id mocking provided by modelizr and they can be toggled in the `configuration object`. The default type, `INCREMENT`, will generate incrementing numerical
20 | ids for each entity. The second, `RANDOM`, will generate a `UUID` id using the `V4` protocol. If you would like to provide your own id generator, you can do so in the
21 | `configuration object`. It will only be used when `idType` is set to `RANDOM`.
22 |
23 | Example of changing the idType and adding a custom generator.
24 | ```javascript
25 | import { query, ID } from 'modelizr'
26 |
27 | query(
28 | user()
29 | )
30 | .mock(true, {
31 | idType: ID.RANDOM,
32 | idGenerator: () => Math.random()
33 | })
34 | .then((res, normalize) => { ... })
35 | ```
36 |
37 | The mocking 'algorithm' for `INCREMENT` is as follows:
38 |
39 | + If a parameter that matches the models `primaryKey` is provided, then mocking will follow the following pattern:
40 | + If the `primaryKey` parameter is an array, then an array of entities will be generated with matching values.
41 | + If the `primaryKey` parameter is an integer, then a single entity with that value will be generated
42 |
43 | + If no `primaryKey` parameter is provided, then mocks will be generated as follows:
44 | + If the entity is a top level query, then 20 entities will be generated with ids `1 => 20`. (the quantity can be changed through `.mockConfig({ ... })`)
45 | + If the entity is nested and is defined in its parent model then it will be mocked according to its normalizr definition. eg:
46 | + `arrayOf()` and `valueOf()` will generate 20 entities with ids `n + 1 => n + 20`
47 | + A plain `model` will generate a single entity with id `n + 1`
48 |
49 | ###### Mocking Mutations
50 |
51 | When mocking a `mutation`, model parameters that match their properties will be given to the generated entities, and model parameters that match nested models will be treated as the
52 | nested models' id's. For example:
53 | ```javascript
54 | import { mutation } from 'modelizr'
55 | import { user, book } from './models.js'
56 |
57 | mutation(
58 | user({id: "PRESET_ID", firstName: "PRESET_NAME", books: ["PRESET_BOOK_1", "PRESET_BOOK_2"]},
59 | book()
60 | )
61 | )
62 | .mock()
63 | .query()
64 | .normalize(res => { ... })
65 |
66 | // The result
67 | {
68 | users: {
69 | PRESET_ID: {
70 | id: "PRESET_ID",
71 | firstName: "PRESET_NAME",
72 | books: [
73 | "PRESET_BOOK_1",
74 | "PRESET_BOOK_2"
75 | ]
76 | ... // other properties get generated normally
77 | }
78 | },
79 |
80 | books: {
81 | PRESET_BOOK_1: {
82 | id: "PRESET_BOOK_1",
83 | ...
84 | },
85 | PRESET_BOOK_2: {
86 | id: "PRESET_BOOK_2",
87 | ...
88 | }
89 | }
90 | }
91 | ```
92 |
93 | ###### Mock Configuration
94 |
95 | ```javascript
96 | {
97 | extensions: {
98 | faker: faker => {},
99 | chance: chance => {}
100 | },
101 | formats: {
102 | semver: (gen, schema) => {}
103 | },
104 | jsfOptions: {
105 | failOnInvalidTypes: true,
106 | ...
107 | },
108 | quantity: 25, // Amount of entities to generate unless otherwise specified in a query
109 | delay: 200, // Add a delay before mocking. Default is 0ms
110 | error: true, // Always throw an error when mocking. Default is false
111 | idType: ID.RANDOM, // The type of id to generate. Defaults to ID.INCREMENT
112 | idGenerator: () => Math.random() // A custom id generator to use when idType is set to random. Defaults to a UUID_V4 generator
113 | }
114 | ```
--------------------------------------------------------------------------------
/src/core/requestBuilder.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import type { ClientStateType, ConfigType, RequestResponse, RequestObject } from '../types'
3 | import { normalizeFunctionParameters } from '../tools/filters'
4 | import { createLogger } from '../tools/logger'
5 | import generate from '../core/queryGeneration'
6 | import Normalizr from '../data/normalization'
7 | import type { Logger } from '../tools/logger'
8 | import Mock from '../data/mocks'
9 | import _ from 'lodash'
10 |
11 | export default (clientState: ClientStateType, queryType: string) => (queryName: string, queryParams: Object, ...children: Array): RequestObject => {
12 | const {name, params, models} = normalizeFunctionParameters(queryName, queryParams, children)
13 |
14 | /* If debugging is enabled, create a logger instance, else
15 | * create a no-op debugger
16 | * */
17 | const logger: Logger = clientState.config.debug ?
18 | createLogger(`[${queryType}: ${_.map(models, model => model.FieldName)}`) : {
19 | add: () => {
20 | },
21 | print: () => {
22 | }
23 | }
24 |
25 | /* generate the query and add it to the request body. If the
26 | * requestType is 'fetch', do not mutate the body
27 | * */
28 | const query: string = generate({clientState, queryModels: models, queryType, queryName: name, queryParams: params})
29 | const config: ConfigType = {
30 | ...clientState.config,
31 | body: queryType == 'fetch' ? {} : {query}
32 | }
33 |
34 | /* Add the generated query to the debugging instance */
35 | if (queryType != 'fetch') logger.add("Query", query)
36 |
37 | /* A utility method that, when given a response - calls our internal
38 | * Normalize method with both the given response and the model tree.
39 | * */
40 | const normalize = (data: Object) => Normalizr({
41 | data,
42 | modelFunctions: models,
43 | clientState
44 | })
45 |
46 | /* A utility method that calls the configured api and
47 | * adds the response to the debugger instance.
48 | * */
49 | const MAKE_REQUEST = (): Promise =>
50 | (config.mock ? Mock({...clientState, config}, models) : config
51 | .api(config))
52 | .then((res) => {
53 | logger.add("Server Response", res.server_response || {})
54 | logger.add("GraphQL response", {
55 | data: res.data,
56 | errors: res.errors
57 | })
58 |
59 | return res
60 | })
61 | .catch(e => {
62 | logger.add("error", e)
63 | logger.print()
64 | throw e
65 | })
66 |
67 | /* Our request object that the user has access to. This object
68 | * contains all modifier methods as well the request catalysts,
69 | * .normalize() and .then()
70 | * */
71 | const REQUEST = {
72 | api: api => {
73 | if (api) config.api = api
74 | return REQUEST
75 | },
76 | endpoint: (value) => {
77 | if (value) config.endpoint = value
78 | return REQUEST
79 | },
80 | headers: (headers) => {
81 | config.headers = {
82 | ...config.headers,
83 | ...headers
84 | }
85 | return REQUEST
86 | },
87 | method: (value) => {
88 | config.method = value || "POST"
89 | return REQUEST
90 | },
91 | mock: (value) => {
92 | config.mock = value === undefined ? true : value
93 | return REQUEST
94 | },
95 | debug: value => {
96 | config.debug = value === undefined ? true : value
97 | return REQUEST
98 | },
99 | body: body => {
100 | if (body) config.body = body
101 | return REQUEST
102 | },
103 | throwOnErrors: value => {
104 | config.throwOnErrors = value === undefined ? true : value
105 | return REQUEST
106 | },
107 |
108 | /* give the generated query to the provided callback and return the REQUEST object */
109 | generate: (cb: (q: string) => any): RequestObject => {
110 | if (typeof cb !== 'function') throw new Error("A function needs to be provided when calling .generate")
111 | cb(query)
112 | return REQUEST
113 | },
114 |
115 | then(cb): Promise {
116 | return MAKE_REQUEST().then(res => {
117 | logger.print()
118 | return cb(res, normalize)
119 | })
120 | },
121 | normalize(cb): Promise {
122 | return MAKE_REQUEST().then(res => {
123 | const normalizedResponse = normalize(res.data)
124 | logger.add("Normalized Response", normalizedResponse)
125 | logger.print()
126 | return cb({
127 | ...res,
128 | ...normalizedResponse
129 | })
130 | })
131 | }
132 | }
133 |
134 | return REQUEST
135 | }
--------------------------------------------------------------------------------
/src/core/queryGeneration.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ClientStateType, ModelFunction, ModelDataType, UnionDataType } from '../types'
3 | import { stripRelationships } from '../tools/filters'
4 | import _ from 'lodash'
5 |
6 | type GeneratorParameters = {
7 | clientState: ClientStateType,
8 | queryModels: Array,
9 | queryType: string,
10 | queryName: ?string,
11 | queryParams: ?Object
12 | }
13 |
14 | type FieldMap = {
15 | name: string,
16 | params: ?Object,
17 | fields: Array
18 | }
19 |
20 | /**
21 | * Utility method that generates a certain amount of spaces
22 | * in a string.
23 | */
24 | const SPACES = 2
25 | const createIndent = (spaces: number): string =>
26 | _.join(_.times((spaces * SPACES) + 1, () => ""), " ")
27 |
28 | /* Construct a valid GraphQL parameter string from an object */
29 | const buildParameters = (params: ?Object): string => {
30 | params = _.pickBy(params || {}, value => value !== null && value !== undefined && !Object.is(value, NaN))
31 | if (_.isEmpty(params)) return ""
32 | return `(${_.map(params, (param, key) =>
33 | `${key}: ${JSON.stringify(param).replace(/"([^(")"]+)":/g, "$1:")}`)})`
34 | }
35 |
36 | /**
37 | * Generate a GraphQL query from a collection of modelizr models.
38 | */
39 | export default ({clientState, queryModels, queryType, queryName, queryParams}: GeneratorParameters): string => {
40 | const {models} = clientState
41 |
42 | /* This compiles a FieldMap from a a collection of models. It is much
43 | * easier to generate a query from a normalized description of fields.
44 | * */
45 | const createMap = (queryModels: Array, prefix: boolean = false): Array => (
46 | _.map(queryModels, (modelFunction: ModelFunction): FieldMap => {
47 | const modelData: ModelDataType | UnionDataType = models[modelFunction.modelName]
48 |
49 | /* Utility that strips modifier rejected fields. */
50 | const filter = (fields, filterFields?: boolean) => filterFields === false ? fields : (
51 | _.pickBy(fields, (field, fieldName: string) => {
52 | const {only, without, empty} = modelFunction.filters
53 | if (only) return _.find(only, field => field == fieldName)
54 | if (without) return !_.find(without, field => field == fieldName)
55 | if (empty) return false
56 | return true
57 | })
58 | )
59 |
60 | /* Filter out fields that have been rejected via modifiers,
61 | * strip any model relationships from the fields and recursively
62 | * generate a FieldMap.
63 | * */
64 | const pruneFields = (fields: Object, filterFields?: boolean): Array => (
65 | _.map(filter(stripRelationships(fields), filterFields), (field, fieldName: string) => {
66 |
67 | /* We check for an alias property on the field type.
68 | * If one is found, use it instead of the fieldName
69 | * */
70 | if (field.alias) fieldName = `${fieldName}: ${field.alias}`
71 |
72 | let type = field.type
73 | if (Array.isArray(type)) type = type[0]
74 | if (type === Object) {
75 | return {
76 | name: fieldName,
77 | fields: pruneFields(field.properties || {}, false)
78 | }
79 | }
80 |
81 | return fieldName
82 | }
83 | )
84 | )
85 |
86 | return {
87 | name: `${prefix ? "... on " : ""}${modelFunction.fieldName}`,
88 | params: modelFunction.params,
89 | fields: [...pruneFields(modelData.fields), ...createMap(modelFunction.children, modelData._unionDataType)]
90 | }
91 | })
92 | )
93 |
94 | const fieldMaps: Array = createMap(queryModels)
95 |
96 | /* Generate an indented and multi-lined GraphQL query string
97 | * from our FieldMap. The type and name of the generated
98 | * query will be determined based on the queryType and queryName
99 | * parameters.
100 | * */
101 | const generateFields = (FieldMap: FieldMap, indent: number = 2): string => {
102 | const {name, fields, params} = FieldMap
103 | const length = !!fields.length
104 |
105 | return `\n${createIndent(indent - 1)}${name}${buildParameters(params)} ${length ? "{" : ""}${_.map(fields, field =>
106 | typeof field === 'string' ? `\n${createIndent(indent)}${field}` :
107 | `${generateFields(field, indent + 1)}`
108 | )}\n${createIndent(indent - 1)}${length ? "}" : ""}`
109 | }
110 |
111 | return `${queryType} ${queryName || `modelizr_${queryType}`}${buildParameters(queryParams)} {${_.map(fieldMaps, fieldMap => generateFields(fieldMap))}\n}`
112 | }
--------------------------------------------------------------------------------
/docs/api/ModelSchemas.md:
--------------------------------------------------------------------------------
1 | # Model Schemas
2 |
3 | A schema is a piece of data that describes the fields and relationships of models.
4 |
5 | ### Model Schema Properties
6 |
7 | ###### name `string`
8 |
9 | The name of the model. This key is used for defining relationships with other models. Defaults to the model `key` when given to modelizr
10 |
11 | ###### normalizeAs `string`
12 |
13 | The key under which all variations of this model will be normalized. Defaults to the model's `name`
14 |
15 | ###### fields `object`
16 |
17 | A collection of `name` => `field` that describes the fields and relationships of the model
18 |
19 | ###### primaryKey `string`
20 |
21 | The field which represents the primary key of the model. Defaults to `id`
22 |
23 | #### Field
24 |
25 | A `field` can be either a javascript `Type`, a `string` or an `object`. A `string` type field represents a relationship with another model. If the type of the field
26 | is wrapped in an array, it will be assumed to be a collection and will be normalized as such (for relationships) and mocked as an array.
27 |
28 | An `object` type field can have the following properties:
29 |
30 | ###### type `Type` **`required`**
31 |
32 | This is the type of the field, and can be one of the 4 javascript Types: `String`, `Number`, `Boolean`, `Object`
33 |
34 | ###### properties `object`
35 |
36 | A collection of `name` => `field`. This property is used in conjunction with `type: Object`
37 |
38 | ###### alias `string`
39 |
40 | Used to declare a GraphQL aliased field. This changes the generated GraphQL query
41 |
42 | ###### faker `string`
43 |
44 | A string referencing a faker category to use when mocking the field. Refer to [Faker Categories](https://github.com/marak/Faker.js/#api-methods) for a
45 | list of available values.
46 |
47 | ###### pattern `string`
48 |
49 | A pipe separated collection of values to use when mocking the field. Example: `Yes|No|Maybe`
50 |
51 | ###### min `number`
52 |
53 | Specify the minimum number to generate when mocking. Used in conjunction with `type: Number`
54 |
55 | ###### max `number`
56 |
57 | Specify the maximum number to generate when mocking. Used in conjunction with `type: Number`
58 |
59 | ###### decimal `boolean`
60 |
61 | Specify whether the generated number should be a decimal or not when mocking. Used in conjunction with `type: Number`. Defaults to `false`
62 |
63 | ###### quantity `number`
64 |
65 | An amount of entities to generate when mocking the field. Used in conjunction with a collection type, eg: `type: [String]`
66 |
67 | ### Union Schema Properties
68 |
69 | A union is a collection of models and is defined slightly differently. A schema is considered a union of it has the following properties:
70 |
71 | ###### models `array` `required`
72 |
73 | A collection of model names that the union is a group of.
74 |
75 | ###### schemaAttribute `string | function` `required`
76 |
77 | A field from each of its model children that can be used to identify the model. The name of the model is used in schema relationships. If a `function` is
78 | given, the function will be used to infer the schema.
79 |
80 | ### Example
81 |
82 | ```javascript
83 | const Cat = {
84 | name: "Cat",
85 | normalizeAs: "Cats",
86 | fields: {
87 | ID: String,
88 | name: {
89 | type: String,
90 | faker: "name.firstName"
91 | },
92 | age: {
93 | type: Number,
94 | min: 1,
95 | max: 10
96 | },
97 | Owner: "Person"
98 | },
99 | primaryKey: "ID"
100 | }
101 |
102 | const Dog = {
103 | name: "Dog",
104 | normalizeAs: "Dogs",
105 | fields: {
106 | id: String,
107 | name: {
108 | type: String,
109 | faker: "name.firstName"
110 | },
111 | breed: {
112 | type: String,
113 | pattern: "Lab|GS|Colly"
114 | },
115 | Owner: "Person"
116 | }
117 | }
118 |
119 | const Animal = {
120 | name: "Animal",
121 | models: ["Cat", "Dog"],
122 | schemaAttribute: "__type"
123 | }
124 |
125 | const Person = {
126 | name: "Person",
127 | normalizeAs: "People",
128 | fields: {
129 | id: String,
130 | name: {
131 | type: String,
132 | faker: "name.firstName"
133 | },
134 | currentLocation: {
135 | type: Object,
136 | properties: {
137 | latitude: {
138 | type: Number,
139 | decimal: true
140 | },
141 | longitude: {
142 | type: Number,
143 | decimal: true
144 | }
145 | }
146 | },
147 | Pets: ["Animal"]
148 | }
149 | }
150 |
151 | import { Modelizr } from 'modelizr'
152 |
153 | const {models} = new Modelizr({
154 | models: {
155 | Person, Dog, Cat, Animal
156 | }
157 | })
158 |
159 | // {Person, Dog, Cat, Animal} = models
160 | ```
--------------------------------------------------------------------------------
/docs/v0.7.x/usage/Querying.md:
--------------------------------------------------------------------------------
1 | # Querying
2 |
3 | Models can be used inside of **query tools** to generate a GraphQL query and send it off to the server.
4 |
5 | #### `query`
6 |
7 | If we want to generate a query that will fetch a users with ids `[1, 2, 3]` and their respective books, then we can use the exported `query()` tool. Both this tool and the models used
8 | can have [modifiers](../modifiers/README.md) applied to them.
9 |
10 | ```javascript
11 | import { query } from 'modelizr'
12 |
13 | query.path('http://path.to.api/graphql')
14 |
15 | query(
16 | user(
17 | book()
18 | ).params({ids: [1, 2, 3]})
19 | ).then((res, normalize) => {
20 | // res -> the response from the server
21 | // normalize(res.body) // normalized response
22 | })
23 | ```
24 |
25 | Internally this will generate the following query and post it to `http://path.to.api/graphql` - as defined with the `.path()` modifier.
26 |
27 | ```
28 | {
29 | users (ids: [1, 2, 3]) {
30 | id,
31 | firstname,
32 | lastname
33 | books {
34 | id,
35 | title,
36 | publisher
37 | }
38 | }
39 | }
40 | ```
41 |
42 | We can make a similar query for books and their respective authors, although we will need to use an `as(key)` modifier to alter the models key.
43 |
44 | ```javascript
45 | query(
46 | book(
47 | user().as("author")
48 | ).params({ids: [1, 2, 3]})
49 | )
50 | .path('http://path.to.api/graphql')
51 | .then((res, normalize) => {
52 | // res -> the response from the server
53 | // normalize(res.body) // normalized response
54 | })
55 | ```
56 | An alternative to using `.as()` is to create an alias of the user model
57 | ```javascript
58 | import { alias } from 'modelizr'
59 |
60 | const author = alias(user, "author")
61 |
62 | query(
63 | book(
64 | author()
65 | )
66 | ) ...
67 | ```
68 | The resulting query will look like this:
69 | ```
70 | {
71 | books (ids: [1, 2, 3]) {
72 | id,
73 | title,
74 | publisher
75 | author {
76 | id,
77 | firstName,
78 | lastName
79 | }
80 | }
81 | }
82 | ```
83 |
84 | You can also use the `.normalize()` modifier instead of `.then()` to directly normalize the servers response.
85 |
86 | ```javascript
87 | query( ... ).normalize(res => {
88 | // res -> normalized response
89 | })
90 | ```
91 |
92 | When using unions in a query, modelizr will prefix child keys with `... on`. For instance the following query:
93 |
94 | ```javascript
95 | query(
96 | owner(
97 | user(),
98 | group()
99 | )
100 | )
101 | ```
102 |
103 | Will generate
104 | ```
105 | {
106 | owners {
107 | id,
108 | ... on users {
109 | id,
110 | firstName,
111 | lastName
112 | },
113 | ... on groups {
114 | id,
115 | users {
116 | id,
117 | firstName,
118 | lastName
119 | }
120 | }
121 | }
122 | }
123 | ```
124 |
125 | #### `mutation`
126 |
127 | To make GraphQL mutations, we can use the `mutation()` tool. This works similarly to the `query()` tool although with a slightly different query generator. Lets mutate a **user**.
128 |
129 | The addition of the `.query()` modifier is to declare that we want the mutated user to be returned by the GraphQL server. This may become the default in future, but for
130 | now you will need to explicitly specify this.
131 |
132 | ```javascript
133 | import { mutation } from 'graphql'
134 |
135 | mutation
136 | .as("createUser")
137 | .params({admin: true})
138 | .path('http://path.to.api/graphql')
139 | .query()
140 |
141 | mutation(
142 | user({firstName: "John", lastName: "Doe"})
143 | ).normalize(res => {})
144 | ```
145 | **Note** - We passed params directly into the model without the use of a modifier. This makes more sense when making single-model mutations.
146 |
147 | This will create a query that looks as follows.
148 | ```
149 | mutation createUser(admin: true) {
150 | users(firstName: "John", lastName: "Doe") {
151 | id,
152 | firstName,
153 | lastName
154 | }
155 | }
156 | ```
157 |
158 | #### `request`
159 |
160 | Finally we have the `request()` tool. This is for making a request to a non GraphQL server, where the returned data can still be expected to match our defined models.
161 | You will need to explicitly give this request a body.
162 |
163 | ```javascript
164 | import { request } from 'modelizr'
165 |
166 | request
167 | .body({
168 | firstName: "John",
169 | lastName: "Doe"
170 | })
171 | .path("http://...")
172 | .method("POST")
173 | .contentType( ... )
174 | .headers( ... )
175 |
176 | request(
177 | user(
178 | book()
179 | )
180 | ).then((res, normalize) => {})
181 | ```
--------------------------------------------------------------------------------
/src/data/mocks.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ClientStateType, ModelDataType, UnionDataType, ModelFunction } from '../types'
3 | import { stripRelationships } from '../tools/filters'
4 | import { generator } from './dataGeneration'
5 | import { v4 } from '../tools/uuid'
6 | import _ from 'lodash'
7 |
8 | /**
9 | * Given a Data Description and a collection of query models, recursively
10 | * generate data that matches the type-specified of the model.
11 | *
12 | * This function will return a promise as it is mocking a fetch request.
13 | */
14 | export default (clientState: ClientStateType, queryModels: Array) => {
15 | const mockConfig = clientState.config.mock
16 |
17 | const generate = generator(clientState.config.faker)
18 |
19 | const mockModels = (models: Array) => {
20 | const mockModel = (model: ModelFunction | Object) => {
21 | let currentModel = model
22 | let modelData: ModelDataType | UnionDataType = clientState.models[model.modelName]
23 | let schemaAttribute: ?string = null
24 |
25 | /* If the model being mocked is a Union, then it has no actual 'fields'
26 | * that we can mock. We need to keep track of its schemaAttribute and
27 | * then randomly select one of its Child Models. We override the
28 | * currently set ModelFunction with this chosen Model.
29 | * */
30 | if (modelData._unionDataType) {
31 | if (!model.children.length) throw new Error(`No children were given to union ${model.fieldName}`)
32 | schemaAttribute = modelData.schemaAttribute
33 | currentModel = _.sample(model.children)
34 | modelData = clientState.models[currentModel.modelName]
35 |
36 | /* If the schemaAttribute of the union is a function, then look for
37 | * a schemaType on the chosen model.
38 | * */
39 | if (typeof schemaAttribute === 'function') schemaAttribute = modelData.schemaType
40 | }
41 | const fieldsToMock = stripRelationships(modelData.fields)
42 |
43 | /* Map over the filtered set of fields and generate information
44 | * based on their data type. If the field is a nested set of fields,
45 | * pass that field back into our mock function.
46 | * */
47 | const mockFields = fields => (
48 | _.mapValues(fields, field => {
49 | const generateOrMock = field => {
50 | if (field.type === Object) {
51 | return mockFields(stripRelationships(field.properties || {}))
52 | }
53 | return generate(field)
54 | }
55 |
56 | if (Array.isArray(field.type)) {
57 | return _.map(_.times(field.quantity || 10), () => generateOrMock({...field, type: field.type[0]}))
58 | }
59 |
60 | return generateOrMock(field)
61 | })
62 | )
63 |
64 | let mockedFields = mockFields(fieldsToMock)
65 |
66 | /* If this model is querying child models, then they also need
67 | * to be mocked. We first check their relationship description
68 | * to figure out if they should be mocked as a collection or as
69 | * a single entity.
70 | * */
71 | const keyedFunctions = _.mapKeys(currentModel.children, (model: ModelFunction) => model.fieldName)
72 | const mockedChildren = _.mapValues(keyedFunctions, (model: ModelFunction, fieldName: string) => {
73 | if (modelData.fields[fieldName]) {
74 | const {type, quantity} = modelData.fields[fieldName]
75 | if (Array.isArray(type)) {
76 | return _.map(_.times(quantity || 10), () => mockModel(model))
77 | }
78 | }
79 |
80 | return mockModel(model)
81 | })
82 |
83 | mockedFields = {
84 | ...mockedFields,
85 | ...mockedChildren
86 | }
87 |
88 | /* Replace the generated primaryKey data with a V4 UUID string and, if
89 | * the model is a union type, set its schemaAttribute accordingly.
90 | * */
91 | if (modelData && mockedFields[modelData.primaryKey]) {
92 | let pk = v4()
93 | if (modelData.fields[modelData.primaryKey].type === Number) {
94 | pk = _.random(10000, 99999)
95 | }
96 | mockedFields[modelData.primaryKey] = pk
97 | }
98 | if (schemaAttribute) mockedFields[schemaAttribute] = currentModel.modelName
99 |
100 | return mockedFields
101 | }
102 |
103 | /* We look at mock config to determine if we should generate an array
104 | * of data or single entities
105 | * */
106 | const keyedFunctions = _.mapKeys(models, (model: ModelFunction) => model.fieldName)
107 | return _.mapValues(keyedFunctions, (model, field) => {
108 | if (typeof mockConfig === 'object') {
109 | if (mockConfig[field] && mockConfig[field] === Array)
110 | return _.map(_.times(10), () => mockModel(model))
111 | }
112 | return mockModel(model)
113 | })
114 | }
115 |
116 | return new Promise(resolve => resolve({
117 | server_response: {},
118 | data: mockModels(queryModels),
119 | errors: null
120 | }))
121 | }
--------------------------------------------------------------------------------
/docs/v0.7.x/modifiers/QueryModifiers.md:
--------------------------------------------------------------------------------
1 | # Query Modifiers
2 |
3 | > These modifiers only apply to query tools - **query**, **mutation** and **request**.
4 |
5 | These modifiers can be applied to either a static or an active tool.
6 | ```javascript
7 | import { query } from 'modelizr'
8 |
9 | query
10 | .path("http:// ... ")
11 | .mock()
12 |
13 | // same as
14 | query( models )
15 | .path("http:// ... ")
16 | .mock()
17 | ```
18 |
19 | #### `path(endpoint)`
20 |
21 | Define the endpoint that the Fetch API should point at.
22 |
23 | ```javascript
24 | query.path("http:// ... ")
25 | ```
26 |
27 | #### `as(name)`
28 |
29 | Can only be applied to **mutation**. Specify the name of a mutation query.
30 |
31 | ```javascript
32 | mutation.as("createUser")
33 | ```
34 |
35 | #### `params(name)`
36 |
37 | Can only be applied to **mutation**. Add parameters to a mutation query.
38 |
39 | ```javascript
40 | mutation.params({forceDelete: true})
41 | ```
42 |
43 | #### `api(function)`
44 |
45 | Replace the default Fetch API with your own custom api. Please read up on the [format of the Fetch API](../api/FetchAPI.md).
46 |
47 | ```javascript
48 | const customAPI = mutations => { ... }
49 | query.api(customAPI)
50 | ```
51 |
52 | #### `spaces(amount)`
53 |
54 | Specify by how many spaces to indent the generated query
55 |
56 | ```javascript
57 | query.spaces(2)
58 | ```
59 |
60 | #### `generate()`
61 |
62 | Causes the query tool to return the generated query as a string.
63 |
64 | > No modifiers can be chained after using this modifier, and cannot be used on static tools.
65 |
66 | Does not apply to **request**.
67 |
68 | ```javascript
69 | console.log(query( ... ).generate())
70 |
71 | /*
72 | {
73 | users {
74 | id,
75 | ...
76 | }
77 | }
78 | */
79 | ```
80 |
81 | #### `mockConfig(config)`
82 |
83 | Configuration for the mocking api.
84 |
85 | ```javascript
86 | query.mockConfig({
87 | extensions: {
88 | faker: faker => {}
89 | },
90 | quantity: 25
91 | })
92 | ```
93 |
94 | Check out the full configuration object [here](../api/Mocks.md#mock-configuration)
95 |
96 | #### `mock(shouldMock, config)`
97 |
98 | The first argument determines weather or not to mock the query (`true` if undefined). The second is a configuration object for the mocking api as defined
99 | [here](../api/Mocks.md#mock-configuration).
100 |
101 | ```javascript
102 | query.mock(true, {
103 | quantity: 3,
104 | error: true
105 | })
106 | ```
107 |
108 | #### `then((res, normalize) => {})`
109 |
110 | Generate the query and send it to the specified GraphQL server. You will get a promise returned and can continue chaining `.then` and `.catch`.
111 |
112 | > No modifiers can be chained after using this modifier, and cannot be used on static tools.
113 |
114 | You will be given the response as the first argument, and a normalize tool as the second.
115 |
116 | ```javascript
117 | query(
118 | user()
119 | ).then((res, normalize) => {
120 | // res -> the returned response
121 | // normalize(res.body) -> the normalize response according to the given model structure.
122 | })
123 | ```
124 |
125 | #### `normalize((res, normalize) => {})`
126 |
127 | Similar to `.then()`, except this will also attempt to normalize the response and give you the normalized response.
128 |
129 | > No modifiers can be chained after using this modifier, and cannot be used on static tools.
130 |
131 | ```javascript
132 | query(
133 | user()
134 | ).normalize(res => {
135 | // res -> the normalized response
136 | })
137 | ```
138 |
139 | #### `middleware(middleware)`
140 |
141 | Add middleware to the request api. Accepts an array of functions. Each function is given the response, a resolve function and a reject function. Each middleware **must** call `next`.
142 |
143 | ```javascript
144 | query(
145 | user()
146 | )
147 | .middleware([(res, next, reject) => next(res)])
148 | .normalize(res => {
149 | // res -> the normalized response
150 | })
151 | ```
152 |
153 | #### `custom((apply [, valueOf]) => apply(key, value))`
154 |
155 | Make a custom modification. Similar to `prepares` custom modifiers. Used for once-off, anonymous modifications.
156 |
157 | Should be given a function that can accept two parameters. `apply(key, value)` and `valueOf(key)`. The function should **return** the result of `apply()`. Read up on
158 | [custom modifiers](../api/QueryTools.md#custom-modifiers)
159 |
160 | ```javascript
161 | query.custom((apply, valueOf) => apply('path', `${valueOf('path')}/get-users`))
162 | ```
163 |
164 | #### `headers(headers)`
165 |
166 | Give the request or query some headers.
167 |
168 | ```javascript
169 | query.headers({
170 | token: " ... "
171 | })
172 | ```
173 |
174 | #### `contentType(type)`
175 |
176 | Specify the content-type of the request
177 |
178 | ```javascript
179 | request.contentType('application/json')
180 | ```
181 |
182 | #### `method(type)`
183 |
184 | Specify the method with which to make a request. `GET`, `POST`, `PUT`, `DELETE`. This modifier only applies to **request**
185 |
186 | ```javascript
187 | request.method('GET')
188 | ```
189 |
190 | #### `body(requestBody)`
191 |
192 | Specify the body of the request. This modifier only applies to **request**
193 |
194 | `object` values will be stringified.
195 |
196 | ```javascript
197 | request.body({ ... })
198 | ```
--------------------------------------------------------------------------------
/src/core/modelizr.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import { ModelFunction, ClientStateType, UnionDataType, ModelDataType, ModelDatatypeField } from '../types'
3 | import { Schema, arrayOf, unionOf } from 'normalizr'
4 | import _ from 'lodash'
5 |
6 | import { normalizeModelData } from '../tools/filters'
7 | import requestBuilder from './requestBuilder'
8 | import { FETCH_API } from '../tools/fetch'
9 | import createModel from './modelBuilder'
10 |
11 | export default class Modelizr {
12 |
13 | clientState: ClientStateType;
14 | models: {[key:string]: ModelFunction} = {};
15 |
16 | constructor(InitialClientState: ClientStateType) {
17 | if (!InitialClientState) throw new Error("Modelizr expects a Client State as its first parameter")
18 |
19 | this.clientState = InitialClientState
20 | this.clientState.models = normalizeModelData(this.clientState.models)
21 |
22 | /* order is important, all model schemas must exist before
23 | * union schemas are created
24 | * */
25 | this.generateModelFunctions()
26 | this.buildUnionSchemas()
27 | this.defineModelRelationships()
28 |
29 | const defaultConfig = {
30 | mock: false,
31 | debug: false,
32 | api: FETCH_API,
33 | throwOnErrors: true
34 | }
35 |
36 | this.clientState.config = {
37 | ...defaultConfig,
38 | ...InitialClientState.config || {}
39 | }
40 | if (!this.clientState.config.endpoint)
41 | throw new Error("Please provide a base endpoint to make queries to")
42 | }
43 |
44 | /* Create ModelFunctions from the given model Data.
45 | *
46 | * If the DataType is a model (and not a union) then build
47 | * its normalizr schema. We do not create schemas' for unions until
48 | * after _all_ model schemas are present.
49 | * */
50 | generateModelFunctions() {
51 | const {models} = this.clientState
52 |
53 | _.forEach(models, (data, name) => {
54 | const ModelData: ModelDataType | UnionDataType = {...data}
55 |
56 | if (!ModelData.normalizeAs) ModelData.normalizeAs = name
57 | if (!ModelData.primaryKey) ModelData.primaryKey = "id"
58 |
59 | if (!ModelData._unionDataType) ModelData.normalizrSchema = new Schema(ModelData.normalizeAs, {
60 | idAttribute: ModelData.primaryKey,
61 | ...ModelData.normalizrOptions || {}
62 | })
63 |
64 | this.clientState.models[name] = ModelData
65 | this.models[name] = createModel(name)
66 | })
67 | }
68 |
69 | /* Build all normalizr schemas for union DataTypes. A check
70 | * to make sure the union is nor referencing imaginary models
71 | * is performed.
72 | * */
73 | buildUnionSchemas() {
74 | const {models} = this.clientState
75 |
76 | _.forEach(models, (modelData: ModelDataType | UnionDataType, modelName: string) => {
77 | if (modelData._unionDataType) {
78 |
79 | /* filter out all non-existing models and warn about them */
80 | const existingModels = _.filter(modelData.models, model => {
81 | if (models[model]) return true
82 |
83 | // eslint-disable-next-line no-console
84 | console.warn(`Model "${model}" on union ${modelName} points to an unknown model`)
85 | })
86 |
87 | /* create a normalizr union */
88 | modelData.normalizrSchema = unionOf(_.mapValues(_.mapKeys(existingModels, model => model), model =>
89 | models[model].normalizrSchema
90 | ), {schemaAttribute: modelData.schemaAttribute}
91 | )
92 | }
93 | })
94 | }
95 |
96 | /* Recursively populate relationship information of each models
97 | * normalizr schema
98 | * */
99 | defineModelRelationships() {
100 | const {models} = this.clientState
101 |
102 | type UnwrappedField = {
103 | type: string,
104 | isArray: boolean
105 | }
106 |
107 | /* Utility that flattens a field wrapped in an array */
108 | const unWrapArray = (field: ModelDatatypeField): UnwrappedField =>
109 | Array.isArray(field.type) ? {isArray: true, type: field.type[0]} :
110 | {isArray: false, type: field.type}
111 |
112 | _.forEach(models, (modelData: ModelDataType | UnionDataType, modelName: string) => {
113 | if (!modelData._unionDataType) {
114 |
115 | /* Filter out any model references that do not exist in our data set */
116 | const modelFields = _.pickBy(modelData.fields, (field: ModelDatatypeField, fieldName: string) => {
117 | const {isArray, type} = unWrapArray(field)
118 | if (typeof type === 'string') {
119 | if (models[type]) return true
120 |
121 | // eslint-disable-next-line no-console
122 | console.warn(
123 | `Field { ${fieldName}: ${isArray ? "[" : ""}"${type}"${isArray ? "]" : ""} } on '${modelName}' points to an unknown model`
124 | )
125 | }
126 | })
127 |
128 | /* Recursively define all model relationships on
129 | * Model normalizr schemas
130 | * */
131 | modelData.normalizrSchema.define(
132 | _.mapValues(modelFields, field => {
133 | const {isArray, type} = unWrapArray(field)
134 | const childModel: ModelDataType | UnionDataType = models[type]
135 |
136 | if (isArray) return arrayOf(childModel.normalizrSchema)
137 | return childModel.normalizrSchema
138 | }
139 | )
140 | )
141 | }
142 | })
143 | }
144 |
145 | query = (...args: Array) => requestBuilder(this.clientState, "query")(...args)
146 | mutate = (...args: Array) => requestBuilder(this.clientState, "mutation")(...args)
147 | fetch = (...args: Array) => requestBuilder(this.clientState, "fetch")(...args)
148 | }
--------------------------------------------------------------------------------