├── .editorconfig ├── __tests__ ├── client │ ├── models │ │ ├── Animal.js │ │ ├── Cat.js │ │ ├── Dog.js │ │ └── Person.js │ ├── fragments.js │ └── index.js ├── normalization.spec.js ├── mocks.spec.js ├── generation.spec.js └── __snapshots__ │ └── generation.spec.js.snap ├── .flowconfig ├── .gitignore ├── .babelrc ├── src ├── index.js ├── tools │ ├── uuid.js │ ├── public.js │ ├── logger.js │ ├── fetch.js │ └── filters.js ├── data │ ├── normalization.js │ ├── dataGeneration.js │ └── mocks.js ├── types │ └── index.js └── core │ ├── modelBuilder.js │ ├── requestBuilder.js │ ├── queryGeneration.js │ └── modelizr.js ├── docs ├── patterns │ ├── README.md │ └── Fragments.md ├── v0.7.x │ ├── patterns │ │ ├── README.md │ │ └── Fragments.md │ ├── styles │ │ └── website.css │ ├── api │ │ ├── README.md │ │ ├── FetchAPI.md │ │ ├── Normalizr.md │ │ ├── Models.md │ │ ├── UnionCreator.md │ │ ├── QueryTools.md │ │ ├── ModelCreator.md │ │ └── Mocks.md │ ├── modifiers │ │ ├── README.md │ │ ├── ModelModifiers.md │ │ └── QueryModifiers.md │ ├── usage │ │ ├── README.md │ │ ├── Production.md │ │ ├── Preparing.md │ │ ├── Models.md │ │ ├── Mocking.md │ │ └── Querying.md │ ├── README.md │ └── Introduction.md ├── api │ ├── README.md │ ├── FetchAPI.md │ ├── Modelizr.md │ ├── QueryTools.md │ ├── Models.md │ ├── Mocks.md │ └── ModelSchemas.md ├── CHANGELOG.md ├── styles │ └── website.css ├── usage │ ├── README.md │ ├── Production.md │ ├── Mocking.md │ ├── Models.md │ └── Querying.md └── Introduction.md ├── example ├── models │ ├── Dog.js │ ├── Cat.js │ ├── Person.js │ └── index.js ├── index.html ├── store │ └── index.js ├── app │ ├── components │ │ ├── DevTools.js │ │ └── Example.js │ └── app.js ├── webpack.dev.js ├── actions │ └── index.js ├── reducers │ └── index.js └── index.js ├── .npmignore ├── .travis.yml ├── book.json ├── .eslintrc ├── SUMMARY.md ├── package.json └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 3 -------------------------------------------------------------------------------- /__tests__/client/models/Animal.js: -------------------------------------------------------------------------------- 1 | export default { 2 | models: ["Cat", "Dog"], 3 | schemaAttribute: "__type" 4 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | .*/_book/.* 4 | 5 | [include] 6 | 7 | node_modules 8 | 9 | [libs] 10 | 11 | [options] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | coverage 8 | _book 9 | node_modules 10 | lib -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["transform-flow-strip-types", "lodash"], 4 | "ignore": ["lib/*"] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Modelizr from './core/modelizr' 2 | export { GraphQLError } from './tools/public' 3 | 4 | export { Modelizr as default, Modelizr } -------------------------------------------------------------------------------- /docs/patterns/README.md: -------------------------------------------------------------------------------- 1 | # Patterns 2 | 3 | A collection of useful patterns that can be used when creating models or queries. 4 | 5 | * [Fragments](Fragments.md) -------------------------------------------------------------------------------- /docs/v0.7.x/patterns/README.md: -------------------------------------------------------------------------------- 1 | # Patterns 2 | 3 | A collection of useful patterns that can be used when creating models or queries. 4 | 5 | * [Fragments](Fragments.md) -------------------------------------------------------------------------------- /__tests__/client/models/Cat.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "Cats", 3 | fields: { 4 | __type: String, 5 | id: String, 6 | name: String, 7 | Owner: "Person" 8 | } 9 | } -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | * [Model Schemas](ModelSchemas.md) 4 | * [Models](Models.md) 5 | * [Query Tools](QueryTools.md) 6 | * [Mocks](Mocks.md) 7 | * [Fetch API](FetchAPI.md) -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The changelog lives on the releases page of the modelizr repo: [https://github.com/julienvincent/modelizr/releases](https://github.com/julienvincent/modelizr/releases) -------------------------------------------------------------------------------- /docs/styles/website.css: -------------------------------------------------------------------------------- 1 | pre code { 2 | font-family: "Operator Mono", Consolas, Monaco, 'Andale Mono', monospace; } 3 | pre code span { 4 | font-family: "Operator Mono", Consolas, Monaco, 'Andale Mono', monospace; } 5 | -------------------------------------------------------------------------------- /example/models/Dog.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "Dogs", 3 | fields: { 4 | __type: String, 5 | ID: Number, 6 | name: String, 7 | breed: String, 8 | Owner: "Person" 9 | }, 10 | primaryKey: "ID" 11 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | docs 3 | 4 | __tests__ 5 | coverage 6 | 7 | .babelrc 8 | .eslintrc 9 | .idea 10 | .DS_Store 11 | .flowconfig 12 | .travis.yml 13 | 14 | yarn-error.log 15 | npm-debug.log 16 | 17 | _book 18 | example -------------------------------------------------------------------------------- /docs/v0.7.x/styles/website.css: -------------------------------------------------------------------------------- 1 | pre code { 2 | font-family: "Operator Mono", Consolas, Monaco, 'Andale Mono', monospace; } 3 | pre code span { 4 | font-family: "Operator Mono", Consolas, Monaco, 'Andale Mono', monospace; } 5 | -------------------------------------------------------------------------------- /example/models/Cat.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "Cats", 3 | fields: { 4 | __type: String, 5 | id: { 6 | type: String, 7 | alias: "ID" 8 | }, 9 | name: String, 10 | type: String, 11 | Owner: "Person" 12 | }, 13 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | modelizr 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/v0.7.x/api/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | * [Model Creator](ModelCreator.md) 4 | * [Union Creator](UnionCreator.md) 5 | * [Models](Models.md) 6 | * [Query Tools](QueryTools.md) 7 | * [Mocks](Mocks.md) 8 | * [Normalizr](Normalizr.md) 9 | * [Fetch API](FetchAPI.md) -------------------------------------------------------------------------------- /__tests__/client/models/Dog.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "Dogs", 3 | fields: { 4 | __type: String, 5 | ID: Number, 6 | name: String, 7 | breed: { 8 | type: String, 9 | pattern: "Lab|Colly" 10 | }, 11 | Owner: "Person" 12 | }, 13 | primaryKey: "ID" 14 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | 5 | before_install: 6 | - npm i -g yarn 7 | 8 | install: yarn 9 | 10 | script: 11 | - yarn flow 12 | - yarn lint 13 | - yarn test 14 | 15 | after_success: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js -------------------------------------------------------------------------------- /docs/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | What follows is a simple usage example for setting up and using modelizr to **mock**, **generate queries** and **normalize responses**. 4 | 5 | * [Creating Models](Models.md) 6 | * [Querying](Querying.md) 7 | * [Mocking](Mocking.md) 8 | * [Production](Production.md) 9 | -------------------------------------------------------------------------------- /docs/v0.7.x/modifiers/README.md: -------------------------------------------------------------------------------- 1 | # Modifiers 2 | 3 | Modifiers are functions that mutate the model or tool they are applied to and then return the modified object. You can chain modifiers in almost any order. 4 | 5 | There are two categories of modifiers and they cannot be used across categories. 6 | 7 | * [Model Modifiers](ModelModifiers.md) 8 | * [Query Modifiers](QueryModifiers.md) -------------------------------------------------------------------------------- /src/tools/uuid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility that generates a V4 UUID 3 | */ 4 | export const v4 = (): string => { 5 | let uuid = '' 6 | for (let i = 0; i < 32; i++) { 7 | const value = Math.random() * 16 | 0 8 | if (i > 4 && i < 21 && !(i % 4)) uuid += '-' 9 | uuid += ((i === 12) ? 4 : ((i === 16) ? (value & 3 | 8) : value)).toString(16) 10 | } 11 | return uuid 12 | } -------------------------------------------------------------------------------- /__tests__/client/fragments.js: -------------------------------------------------------------------------------- 1 | import {models} from './index' 2 | 3 | const {Person, Animal, Cat, Dog} = models 4 | 5 | export const PersonWithFriend = Person( 6 | Person("Friend") 7 | ) 8 | 9 | export const PersonWithPets = Person( 10 | Animal("Pets", 11 | Cat, Dog 12 | ) 13 | ) 14 | 15 | export const PersonWithPetsWithFriend = PersonWithPets( 16 | Person("Friend") 17 | ) -------------------------------------------------------------------------------- /docs/v0.7.x/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | What follows is a simple usage example for setting up and using modelizr to **mock**, **generate queries** and **normalize responses**. 4 | 5 | * [Creating Models](Models.md) 6 | * [Querying](Querying.md) 7 | * [Mocking](Mocking.md) 8 | * [Query Preparation](Preparing.md) 9 | * [Production](Production.md) 10 | * [Validation](Validation.md) -------------------------------------------------------------------------------- /example/store/index.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore, applyMiddleware } from 'redux' 2 | import DevTools from '../app/components/DevTools' 3 | import reducers from '../reducers/index' 4 | import thunk from 'redux-thunk' 5 | 6 | const store = createStore( 7 | reducers, 8 | compose( 9 | applyMiddleware(thunk), 10 | DevTools.instrument() 11 | ) 12 | ) 13 | 14 | export { store as default, store } -------------------------------------------------------------------------------- /example/models/Person.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "People", 3 | fields: { 4 | id: String, 5 | isFunny: Boolean, 6 | age: { 7 | type: Number, 8 | decimal: true, 9 | min: 1, 10 | max: 100 11 | }, 12 | ok: { 13 | type: [Object], 14 | properties: { 15 | prop1: {type: [String], quantity: 1}, 16 | p2: Number 17 | } 18 | }, 19 | Pets: {type: ["Animal"], quantity: 20}, 20 | Friend: "Person" 21 | } 22 | } -------------------------------------------------------------------------------- /__tests__/client/index.js: -------------------------------------------------------------------------------- 1 | import { Modelizr } from '../../src' 2 | 3 | import Person from './models/Person' 4 | import Animal from './models/Animal' 5 | import Cat from './models/Cat' 6 | import Dog from './models/Dog' 7 | 8 | const {query, mutate, models} = new Modelizr({ 9 | models: { 10 | Person, 11 | Animal, 12 | Cat, 13 | Dog 14 | }, 15 | config: { 16 | endpoint: "http://localhost:8001" 17 | } 18 | }) 19 | 20 | export { query, models, mutate } -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.2", 3 | "title": "Modelizr", 4 | "author": "Julien Vincent", 5 | "plugins": [ 6 | "prism", 7 | "-highlight", 8 | "github", 9 | "anchors", 10 | "ga" 11 | ], 12 | "styles": { 13 | "website": "docs/styles/website.css" 14 | }, 15 | "pluginsConfig": { 16 | "github": { 17 | "url": "https://github.com/julienvincent/modelizr" 18 | }, 19 | "ga": { 20 | "token": "UA-53097839-2" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /example/models/index.js: -------------------------------------------------------------------------------- 1 | import Modelizr, { union } from '../../src' 2 | 3 | import Person from './Person' 4 | import Cat from './Cat' 5 | import Dog from './Dog' 6 | 7 | const client = new Modelizr({ 8 | models: { 9 | Person, 10 | Cat, 11 | Dog, 12 | Animal: { 13 | models: ["Cat", "Dog"], 14 | schemaAttribute: "__type" 15 | }, 16 | }, 17 | config: { 18 | endpoint: "http://localhost:8000/graphql", 19 | mock: true, 20 | debug: true 21 | } 22 | }) 23 | 24 | export default client -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "modules": true 4 | }, 5 | "env": { 6 | "browser": true, 7 | "node": true 8 | }, 9 | "parser": "babel-eslint", 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "no-undef": 0, 13 | "no-var": 1, 14 | "prefer-arrow-callback": 1, 15 | "prefer-const": 1, 16 | "prefer-spread": 1, 17 | "prefer-template": 1, 18 | "no-constant-condition": 0, 19 | "no-console": 1 20 | }, 21 | "plugins": [ 22 | "babel" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/client/models/Person.js: -------------------------------------------------------------------------------- 1 | export default { 2 | normalizeAs: "People", 3 | fields: { 4 | id: String, 5 | name: { 6 | type: String, 7 | faker: "name.firstName" 8 | }, 9 | otherName: { 10 | type: String, 11 | alias: "middleName" 12 | }, 13 | aliases: { 14 | type: [String], 15 | quantity: 4, 16 | pattern: "alias" 17 | }, 18 | age: { 19 | type: Number, 20 | min: 1, 21 | max: 100 22 | }, 23 | licensed: Boolean, 24 | Pets: ["Animal"], 25 | Friend: "Person" 26 | } 27 | } -------------------------------------------------------------------------------- /src/tools/public.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type ErrorsType = { 4 | message: string, 5 | locations: Array<{line: number, column: number}> 6 | } 7 | 8 | /** 9 | * An error type that denotes a response containing 10 | * GraphQL errors 11 | */ 12 | export class GraphQLError { 13 | 14 | graphQLErrors: Array; 15 | message: string; 16 | 17 | constructor(message: ?string, errors: Array) { 18 | this.message = message || "The GraphQL server responded with errors." 19 | this.graphQLErrors = errors 20 | } 21 | } -------------------------------------------------------------------------------- /example/app/components/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import DockMonitor from 'redux-devtools-dock-monitor' 4 | import Inspector from 'redux-devtools-inspector' 5 | 6 | const DevTools = createDevTools( 7 | 13 | 14 | 15 | 16 | ) 17 | 18 | export { DevTools as default, DevTools } -------------------------------------------------------------------------------- /example/webpack.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devtool: 'cheap-module-source-map', 3 | entry: [ 4 | 'babel-polyfill', 5 | `${__dirname}/app/app.js` 6 | ], 7 | output: { 8 | path: '/', 9 | filename: 'app.js', 10 | publicPath: '/' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | loaders: ['babel-loader?presets=react'], 17 | exclude: /node_modules/ 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /docs/v0.7.x/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 | fragments.js 6 | ```javascript 7 | import { user, book } from './models' 8 | 9 | const BookFragment = book( 10 | user().as('author') 11 | ) 12 | 13 | export { BookFragment } 14 | ``` 15 | 16 | actions.js 17 | ```javascript 18 | import { query } from 'modelizr' 19 | import { BookFragment } from './fragments' 20 | 21 | query( 22 | BookFragment 23 | ).then(res => { ... }) 24 | ``` -------------------------------------------------------------------------------- /example/actions/index.js: -------------------------------------------------------------------------------- 1 | import client from '../models' 2 | 3 | const {models, query, mutate, fetch} = client 4 | const {Person, Dog, Cat, Animal} = models 5 | 6 | export const TOGGLE_MOCK = "TOGGLE_MOCK" 7 | export const toggleMock = () => ({ 8 | type: TOGGLE_MOCK 9 | }) 10 | 11 | export const SET_ENTITIES = "SET_ENTITIES" 12 | 13 | export const fetchPeople = mock => dispatch => { 14 | query( 15 | Person("Peoples", {}, 16 | Animal("Pets", 17 | Cat, Dog 18 | ), 19 | undefined, 20 | Person("Friend") 21 | ).without(["ok"]) 22 | ).normalize(res => dispatch({ 23 | type: SET_ENTITIES, 24 | payload: res.entities 25 | })) 26 | } -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Read Me](README.md) 2 | * [Introduction](docs/Introduction.md) 3 | * [Usage](docs/usage/README.md) 4 | * [Creating Models](docs/usage/Models.md) 5 | * [Querying](docs/usage/Querying.md) 6 | * [Mocking](docs/usage/Mocking.md) 7 | * [Production](docs/usage/Production.md) 8 | * [Patterns](docs/patterns/README.md) 9 | * [Fragments](docs/patterns/Fragments.md) 10 | * [API Reference](docs/api/README.md) 11 | * [Model Schemas](docs/api/ModelSchemas.md) 12 | * [Modelizr](docs/api/Modelizr.md) 13 | * [Models](docs/api/Models.md) 14 | * [Query Tools](docs/api/QueryTools.md) 15 | * [Fetch API](docs/api/FetchAPI.md) 16 | * [Changelog](docs/CHANGELOG.md) -------------------------------------------------------------------------------- /example/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { bindActionCreators } from 'redux' 4 | window.React = React 5 | 6 | import store from '../store' 7 | import * as unboundActions from '../actions' 8 | const actions = bindActionCreators(unboundActions, store.dispatch) 9 | 10 | import DevTools from './components/DevTools' 11 | import Example from './components/Example' 12 | import { Provider } from 'react-redux' 13 | 14 | const App = () => ( 15 | 16 |
17 | 18 | 19 | 20 |
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 | [![Coverage Status](https://coveralls.io/repos/github/julienvincent/modelizr/badge.svg?branch=master)](https://coveralls.io/github/julienvincent/modelizr?branch=master) 3 | [![Build Status](https://travis-ci.org/julienvincent/modelizr.svg?branch=master)](https://travis-ci.org/julienvincent/modelizr) 4 | [![npm version](https://badge.fury.io/js/modelizr.svg)](https://badge.fury.io/js/modelizr) 5 | [![Gitter](https://badges.gitter.im/julienvincent/modelizr.svg)](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 | } --------------------------------------------------------------------------------