├── app
├── .gitkeep
└── initializers
│ └── graph-data.js
├── addon
├── .gitkeep
├── transport.js
├── utils.js
├── serializer.js
└── adapter.js
├── vendor
└── .gitkeep
├── tests
├── unit
│ ├── .gitkeep
│ ├── build
│ │ ├── test-fragment.graphql
│ │ ├── test-query.graphql
│ │ └── graphql-filter-test.js
│ ├── adapters
│ │ └── application-test.js
│ └── serializers
│ │ └── application-test.js
├── integration
│ └── .gitkeep
├── dummy
│ ├── app
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ ├── .gitkeep
│ │ │ ├── admin-user.js
│ │ │ ├── user
│ │ │ │ ├── role.js
│ │ │ │ └── blog-post.js
│ │ │ └── user.js
│ │ ├── routes
│ │ │ ├── .gitkeep
│ │ │ ├── users
│ │ │ │ └── show.js
│ │ │ └── users.js
│ │ ├── styles
│ │ │ └── app.css
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ ├── .gitkeep
│ │ │ └── users.js
│ │ ├── templates
│ │ │ ├── components
│ │ │ │ └── .gitkeep
│ │ │ ├── application.hbs
│ │ │ ├── users
│ │ │ │ └── show.hbs
│ │ │ └── users.hbs
│ │ ├── resolver.js
│ │ ├── graph
│ │ │ ├── fragments
│ │ │ │ └── user-fragment.graphql
│ │ │ ├── queries
│ │ │ │ ├── users.graphql
│ │ │ │ └── user.graphql
│ │ │ └── mutations
│ │ │ │ └── user-create.graphql
│ │ ├── serializers
│ │ │ └── application.js
│ │ ├── adapters
│ │ │ └── application.js
│ │ ├── router.js
│ │ ├── app.js
│ │ └── index.html
│ ├── public
│ │ ├── robots.txt
│ │ └── crossdomain.xml
│ ├── mirage
│ │ ├── scenarios
│ │ │ └── default.js
│ │ ├── models
│ │ │ └── user.js
│ │ ├── serializers
│ │ │ └── application.js
│ │ ├── factories
│ │ │ └── user.js
│ │ └── config.js
│ └── config
│ │ ├── targets.js
│ │ └── environment.js
├── .eslintrc.js
├── test-helper.js
├── helpers
│ ├── destroy-app.js
│ ├── resolver.js
│ └── start-app.js
├── acceptance
│ └── users-test.js
└── index.html
├── .watchmanconfig
├── lib
├── .eslintrc.js
└── graphql-filter.js
├── config
├── environment.js
└── ember-try.js
├── .npmignore
├── .eslintrc.js
├── .ember-cli
├── .gitignore
├── testem.js
├── .editorconfig
├── ember-cli-build.js
├── LICENSE.md
├── .travis.yml
├── index.js
├── package.json
└── README.md
/app/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/integration/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": ["tmp", "dist"]
3 | }
4 |
--------------------------------------------------------------------------------
/tests/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | embertest: true
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/tests/unit/build/test-fragment.graphql:
--------------------------------------------------------------------------------
1 | fragment testFragment on Object {
2 | name
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 |
Welcome to Ember
2 |
3 | {{outlet}}
--------------------------------------------------------------------------------
/lib/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/tests/dummy/app/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from 'ember-resolver';
2 |
3 | export default Resolver;
4 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/scenarios/default.js:
--------------------------------------------------------------------------------
1 | export default function(server) {
2 | server.createList('user', 10)
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/users/show.hbs:
--------------------------------------------------------------------------------
1 | User details
2 | {{model.email}}
3 | {{model.firstName}}
4 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/models/user.js:
--------------------------------------------------------------------------------
1 | import { Model } from 'ember-cli-mirage'
2 |
3 | export default Model.extend({
4 | })
5 |
--------------------------------------------------------------------------------
/tests/dummy/app/graph/fragments/user-fragment.graphql:
--------------------------------------------------------------------------------
1 | fragment UserFragment on User {
2 | id
3 | firstName
4 | email
5 | }
6 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/admin-user.js:
--------------------------------------------------------------------------------
1 | import User from './user'
2 |
3 | export default User.extend({
4 | isAdmin: true
5 | })
6 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import resolver from './helpers/resolver';
2 | import { setResolver } from 'ember-mocha';
3 |
4 | setResolver(resolver);
5 |
--------------------------------------------------------------------------------
/tests/unit/build/test-query.graphql:
--------------------------------------------------------------------------------
1 | #import './test-fragment'
2 |
3 | query TestQuery {
4 | subject {
5 | ...testFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/serializers/application.js:
--------------------------------------------------------------------------------
1 | import GraphSerializer from 'ember-graph-data/serializer'
2 |
3 | export default GraphSerializer.extend()
4 |
--------------------------------------------------------------------------------
/config/environment.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | module.exports = function(/* environment, appConfig */) {
5 | return { };
6 | };
7 |
--------------------------------------------------------------------------------
/tests/dummy/app/graph/queries/users.graphql:
--------------------------------------------------------------------------------
1 | #import '../fragments/user-fragment'
2 |
3 | query users {
4 | users {
5 | ...UserFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/serializers/application.js:
--------------------------------------------------------------------------------
1 | import { JSONAPISerializer } from 'ember-cli-mirage';
2 |
3 | export default JSONAPISerializer.extend({
4 | });
5 |
--------------------------------------------------------------------------------
/tests/dummy/app/graph/queries/user.graphql:
--------------------------------------------------------------------------------
1 | #import '../fragments/user-fragment'
2 |
3 | mutation user($id: ID!) {
4 | user(id: $id) {
5 | ...UserFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/user/role.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data'
2 |
3 | const {
4 | Model,
5 | attr
6 | } = DS
7 |
8 | export default Model.extend({
9 | name: attr()
10 | })
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/user/blog-post.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data'
2 |
3 | const {
4 | Model,
5 | attr
6 | } = DS
7 |
8 | export default Model.extend({
9 | title: attr(),
10 | body: attr()
11 | })
12 |
--------------------------------------------------------------------------------
/tests/helpers/destroy-app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default function destroyApp(application) {
4 | Ember.run(application, 'destroy');
5 | if (window.server) {
6 | window.server.shutdown();
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/dummy/config/targets.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | browsers: [
4 | 'ie 9',
5 | 'last 1 Chrome versions',
6 | 'last 1 Firefox versions',
7 | 'last 1 Safari versions'
8 | ]
9 | };
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/users/show.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route'
2 | import query from 'dummy/graph/queries/user'
3 |
4 | export default Route.extend({
5 | model({id}) {
6 | return this.store.graphQuery({query, variables: {id}})
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/factories/user.js:
--------------------------------------------------------------------------------
1 | import { Factory, faker } from 'ember-cli-mirage'
2 |
3 | export default Factory.extend({
4 | firstName(i) {
5 | return `Person ${i}`
6 | },
7 | email() {
8 | return faker.internet.email()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/graph/mutations/user-create.graphql:
--------------------------------------------------------------------------------
1 | #import '../fragments/user-fragment'
2 |
3 | mutation userCreate($id: ID, $email: String!, $firstName: String!) {
4 | userCreate(id: $id, email: $email, firstName: $firstName) {
5 | ...UserFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /bower_components
2 | /config/ember-try.js
3 | /dist
4 | /tests
5 | /tmp
6 | **/.gitkeep
7 | .bowerrc
8 | .editorconfig
9 | .ember-cli
10 | .gitignore
11 | .jshintrc
12 | .watchmanconfig
13 | .travis.yml
14 | bower.json
15 | ember-cli-build.js
16 | testem.js
17 |
--------------------------------------------------------------------------------
/tests/dummy/app/adapters/application.js:
--------------------------------------------------------------------------------
1 | import GraphAdapter from 'ember-graph-data/adapter'
2 |
3 | export default GraphAdapter.extend({
4 | host: 'http://localhost:4000',
5 | namespace: 'graph',
6 |
7 | headers: {
8 | 'my-header': 'my-header'
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globals: {
3 | server: true,
4 | },
5 | root: true,
6 | parserOptions: {
7 | ecmaVersion: 6,
8 | sourceType: 'module'
9 | },
10 | extends: 'eslint:recommended',
11 | env: {
12 | browser: true
13 | },
14 | rules: {
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/users.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route'
2 | import query from 'dummy/graph/queries/users'
3 |
4 | export default Route.extend({
5 | model() {
6 | let variables = {page: 1, size: 10}
7 | return this.store.graphQuery({query, variables})
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": true
9 | }
10 |
--------------------------------------------------------------------------------
/tests/helpers/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from '../../resolver';
2 | import config from '../../config/environment';
3 |
4 | const resolver = Resolver.create();
5 |
6 | resolver.namespace = {
7 | modulePrefix: config.modulePrefix,
8 | podModulePrefix: config.podModulePrefix
9 | };
10 |
11 | export default resolver;
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # misc
12 | /.sass-cache
13 | /connect.lock
14 | /coverage/*
15 | /libpeerconnection.log
16 | npm-debug.log*
17 | testem.log
18 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/user.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data'
2 |
3 | const {
4 | Model,
5 | attr,
6 | hasMany,
7 | belongsTo
8 | } = DS
9 |
10 | export default Model.extend({
11 | email: attr(),
12 | firstName: attr(),
13 |
14 | posts: hasMany('user/blog-post'),
15 | role: belongsTo('user/role')
16 | })
17 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/users.hbs:
--------------------------------------------------------------------------------
1 | {{input name='id' value=id}}
2 | {{input name='email' value=email}}
3 | {{input name='firstName' value=firstName}}
4 |
5 |
8 |
9 |
10 | {{#each model.users as |user|}}
11 | -
12 | {{user.firstName}}
13 |
14 | {{/each}}
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import config from './config/environment';
3 |
4 | const Router = Ember.Router.extend({
5 | location: config.locationType,
6 | rootURL: config.rootURL
7 | });
8 |
9 | Router.map(function() {
10 | this.route('users', function() {
11 | this.route('show', { path: ':id' })
12 | })
13 | });
14 |
15 | export default Router;
16 |
--------------------------------------------------------------------------------
/app/initializers/graph-data.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data'
2 |
3 | export function initialize() {
4 | DS.Store.reopen({
5 | graphQuery() {
6 | return this.adapterFor('application').query(...arguments)
7 | },
8 |
9 | graphMutate() {
10 | return this.adapterFor('application').mutate(...arguments)
11 | }
12 | })
13 | }
14 |
15 | export default {
16 | name: 'graph-data',
17 | initialize
18 | };
19 |
--------------------------------------------------------------------------------
/testem.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | test_page: 'tests/index.html?hidepassed',
4 | disable_watching: true,
5 | launch_in_ci: [
6 | 'Chrome'
7 | ],
8 | launch_in_dev: [
9 | 'Chrome'
10 | ],
11 | browser_args: {
12 | Chrome: [
13 | '--disable-gpu',
14 | '--headless',
15 | '--remote-debugging-port=9222',
16 | '--window-size=1440,900'
17 | ]
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Resolver from './resolver';
3 | import loadInitializers from 'ember-load-initializers';
4 | import config from './config/environment';
5 |
6 | const App = Ember.Application.extend({
7 | modulePrefix: config.modulePrefix,
8 | podModulePrefix: config.podModulePrefix,
9 | Resolver
10 | });
11 |
12 | loadInitializers(App, config.modulePrefix);
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.hbs]
17 | insert_final_newline = false
18 |
19 | [*.{diff,md}]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/users.js:
--------------------------------------------------------------------------------
1 | import Controller from '@ember/controller'
2 | import mutation from 'dummy/graph/mutations/user-create'
3 |
4 | export default Controller.extend({
5 | actions: {
6 | userCreate() {
7 | let variables = this.getProperties('id', 'email', 'firstName')
8 | return this.store.graphMutate({mutation, variables}).then(
9 | ({userCreate}) => this.transitionToRoute('users.show', userCreate)
10 | )
11 | }
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/tests/helpers/start-app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Application from '../../app';
3 | import config from '../../config/environment';
4 |
5 | export default function startApp(attrs) {
6 | let attributes = Ember.merge({}, config.APP);
7 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override;
8 |
9 | return Ember.run(() => {
10 | let application = Application.create(attributes);
11 | application.setupForTesting();
12 | application.injectTestHelpers();
13 | return application;
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
5 |
6 | module.exports = function(defaults) {
7 | let app = new EmberAddon(defaults, {
8 | // Add options here
9 | });
10 |
11 | /*
12 | This build file specifies the options for the dummy test app of this
13 | addon, located in `/tests/dummy`
14 | This build file does *not* influence how the addon or the app using it
15 | behave. You most likely want to be modifying `./index.js` or app's build file
16 | */
17 |
18 | return app.toTree();
19 | };
20 |
--------------------------------------------------------------------------------
/tests/dummy/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/config.js:
--------------------------------------------------------------------------------
1 | const mockData = {
2 | users({schema}) {
3 | return {
4 | users: schema.users.all().models
5 | }
6 | },
7 |
8 | userCreate({schema, variables}) {
9 | return {
10 | userCreate: schema.users.create(variables)
11 | }
12 | }
13 | }
14 |
15 | export default function() {
16 | this.urlPrefix = 'http://localhost:4000';
17 |
18 | this.post('graph', (schema, {requestBody}) => {
19 | let query = JSON.parse(requestBody)
20 | let variables = query.variables
21 |
22 | let operationResolver = mockData[query.operationName]
23 | let responseBody = operationResolver ? operationResolver({schema, variables}) : {}
24 |
25 | return {data: responseBody}
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy
7 |
8 |
9 |
10 | {{content-for "head"}}
11 |
12 |
13 |
14 |
15 | {{content-for "head-footer"}}
16 |
17 |
18 | {{content-for "body"}}
19 |
20 |
21 |
22 |
23 | {{content-for "body-footer"}}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/addon/transport.js:
--------------------------------------------------------------------------------
1 | import {extractFiles} from './utils'
2 |
3 | export const transportJson = function(params) {
4 | return {data: params}
5 | }
6 |
7 | export const transportMultipart = function(params) {
8 | if (typeof FormData === 'undefined')
9 | return transportJson(...arguments)
10 | let variables = params.variables
11 | let files = extractFiles(variables)
12 | let formData = new FormData()
13 |
14 | formData.append('query', params.query)
15 | if(params.operationName)
16 | formData.append('operationName', params.operationName)
17 | files.forEach(({ path, file }) => {
18 | let fullPath = `variables.${path}`
19 | formData.append(fullPath, file)
20 | variables[path] = fullPath
21 | })
22 | formData.append('variables', JSON.stringify(variables))
23 | return {body: formData}
24 | }
25 |
26 | export default {
27 | json: transportJson,
28 | multipart: transportMultipart
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Damian Romanów
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/lib/graphql-filter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Filter = require('broccoli-filter')
4 | const gql = require('graphql-tag')
5 |
6 | module.exports = class GraphQLFilter extends Filter {
7 | constructor(inputNode, options) {
8 | super(inputNode, options)
9 | this.extensions = ['graphql']
10 | this.targetExtension = 'js'
11 | }
12 |
13 | processString(source) {
14 | let output = [
15 | `const doc = ${JSON.stringify(gql([source]), null, 2)};`
16 | ]
17 | output.push('let lines = [];')
18 |
19 | source.split('\n').forEach((line, i) => {
20 | let match = /^#import\s+(.*)/.exec(line)
21 | if (match && match[1]) {
22 | output.push(`import dep${i} from ${match[1]};`)
23 | output.push(`lines.push(dep${i}.string);`)
24 | output.push(`doc.definitions = doc.definitions.concat(dep${i}.definitions);`);
25 | } else {
26 | output.push('lines.push(`' + line + '`);')
27 | }
28 | })
29 | output.push('doc.string = lines.join(`\n`);')
30 | output.push('export default doc;')
31 | return output.join('\n')
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/acceptance/users-test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, beforeEach, afterEach } from 'mocha'
2 | import { expect } from 'chai'
3 | import startApp from '../helpers/start-app'
4 | import destroyApp from '../helpers/destroy-app'
5 |
6 | describe('Acceptance | users', function() {
7 | let application
8 |
9 | beforeEach(function() {
10 | application = startApp()
11 | server.createList('user', 10)
12 | })
13 |
14 | afterEach(function() {
15 | destroyApp(application)
16 | })
17 |
18 | it('displays users list', function() {
19 | visit('/users')
20 |
21 | andThen(() => {
22 | expect(currentURL()).to.equal('/users')
23 |
24 | expect(find('ul')).to.ok
25 | expect(find('li').length).to.eq(10)
26 | })
27 | })
28 |
29 | it('adds new user', function() {
30 | visit('/users')
31 | andThen(() => {
32 | fillIn('input[name=id]', 314)
33 | fillIn('input[name=email]', 'madderdin@hez-hezron.com')
34 | fillIn('input[name=firstName]', 'Mordimer')
35 |
36 | click('button.add-user')
37 | andThen(() => {
38 | expect(currentURL()).to.equal('/users/314')
39 | })
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "4"
5 | - "6"
6 |
7 | sudo: false
8 |
9 | cache:
10 | yarn: true
11 |
12 | sudo: required
13 | dist: trusty
14 |
15 | addons:
16 | apt:
17 | sources:
18 | - google-chrome
19 | packages:
20 | - google-chrome-stable
21 |
22 | env:
23 | # we recommend testing LTS's and latest stable release (bonus points to beta/canary)
24 | - EMBER_TRY_SCENARIO=ember-lts-2.8
25 | - EMBER_TRY_SCENARIO=ember-release
26 | - EMBER_TRY_SCENARIO=ember-beta
27 | - EMBER_TRY_SCENARIO=ember-canary
28 | - EMBER_TRY_SCENARIO=ember-default
29 |
30 | matrix:
31 | fast_finish: true
32 | allow_failures:
33 | - env: EMBER_TRY_SCENARIO=ember-canary
34 |
35 | before_install:
36 | - "export DISPLAY=:99.0"
37 | - "sh -e /etc/init.d/xvfb start"
38 | - yarn install
39 | - export PATH=$HOME/.yarn/bin:$PATH
40 |
41 | install:
42 | - yarn install --no-lockfile --non-interactive
43 |
44 | script:
45 | # Usually, it's ok to finish the test scenario without reverting
46 | # to the addon's original dependency state, skipping "cleanup".
47 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup
48 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy Tests
7 |
8 |
9 |
10 | {{content-for "head"}}
11 | {{content-for "test-head"}}
12 |
13 |
14 |
15 |
16 |
17 | {{content-for "head-footer"}}
18 | {{content-for "test-head-footer"}}
19 |
20 |
21 | {{content-for "body"}}
22 | {{content-for "test-body"}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{content-for "body-footer"}}
31 | {{content-for "test-body-footer"}}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict'
3 |
4 | const Webpack = require('broccoli-webpack')
5 | const PackOpts = (name, entry) => {
6 | entry = entry || name
7 | return {
8 | entry: entry,
9 | output: {
10 | filename: `${name}.js`,
11 | library: name,
12 | libraryTarget: 'umd'
13 | }
14 | }
15 | }
16 |
17 | const transformAMD = (name) => ({
18 | using: [{ transformation: 'amd', as: name }]
19 | })
20 |
21 | module.exports = {
22 | name: 'ember-graph-data',
23 |
24 | options: {
25 | nodeAssets: {
26 | 'graphql-tag': {
27 | vendor: {
28 | include: ['index.js'],
29 | processTree(input) {
30 | return new Webpack([input], PackOpts('graphql-tag'))
31 | }
32 | }
33 | }
34 | }
35 | },
36 |
37 | included(app) {
38 | this._super.included.apply(this, arguments)
39 | app.import('vendor/graphql-tag.js', transformAMD('graphql-tag'))
40 | },
41 |
42 | setupPreprocessorRegistry(type, registry) {
43 | if (type === 'parent') {
44 | registry.add('js', {
45 | name: 'ember-graph-data',
46 | ext: 'graphql',
47 | toTree(tree) {
48 | const GraphQLFilter = require('./lib/graphql-filter')
49 | return new GraphQLFilter(tree)
50 | }
51 | })
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | module.exports = function(environment) {
5 | let ENV = {
6 | modulePrefix: 'dummy',
7 | environment,
8 | rootURL: '/',
9 | locationType: 'auto',
10 | EmberENV: {
11 | FEATURES: {
12 | // Here you can enable experimental features on an ember canary build
13 | // e.g. 'with-controller': true
14 | },
15 | EXTEND_PROTOTYPES: {
16 | // Prevent Ember Data from overriding Date.parse.
17 | Date: false
18 | }
19 | },
20 |
21 | APP: {
22 | // Here you can pass flags/options to your application instance
23 | // when it is created
24 | },
25 | 'ember-graph-data': {
26 | },
27 | };
28 |
29 | if (environment === 'development') {
30 | // ENV.APP.LOG_RESOLVER = true;
31 | // ENV.APP.LOG_ACTIVE_GENERATION = true;
32 | // ENV.APP.LOG_TRANSITIONS = true;
33 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
34 | // ENV.APP.LOG_VIEW_LOOKUPS = true;
35 | }
36 |
37 | if (environment === 'test') {
38 | // Testem prefers this...
39 | ENV.locationType = 'none';
40 |
41 | // keep test console output quieter
42 | ENV.APP.LOG_ACTIVE_GENERATION = false;
43 | ENV.APP.LOG_VIEW_LOOKUPS = false;
44 |
45 | ENV.APP.rootElement = '#ember-testing';
46 | }
47 |
48 | if (environment === 'production') {
49 |
50 | }
51 |
52 | return ENV;
53 | };
54 |
--------------------------------------------------------------------------------
/config/ember-try.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | useYarn: true,
4 | scenarios: [
5 | {
6 | name: 'ember-lts-2.8',
7 | bower: {
8 | dependencies: {
9 | 'ember': 'components/ember#lts-2-8'
10 | },
11 | resolutions: {
12 | 'ember': 'lts-2-8'
13 | }
14 | },
15 | npm: {
16 | devDependencies: {
17 | 'ember-source': null
18 | }
19 | }
20 | },
21 | {
22 | name: 'ember-lts-2.12',
23 | npm: {
24 | devDependencies: {
25 | 'ember-source': '~2.12.0'
26 | }
27 | }
28 | },
29 | {
30 | name: 'ember-release',
31 | bower: {
32 | dependencies: {
33 | 'ember': 'components/ember#release'
34 | },
35 | resolutions: {
36 | 'ember': 'release'
37 | }
38 | },
39 | npm: {
40 | devDependencies: {
41 | 'ember-source': null
42 | }
43 | }
44 | },
45 | {
46 | name: 'ember-beta',
47 | bower: {
48 | dependencies: {
49 | 'ember': 'components/ember#beta'
50 | },
51 | resolutions: {
52 | 'ember': 'beta'
53 | }
54 | },
55 | npm: {
56 | devDependencies: {
57 | 'ember-source': null
58 | }
59 | }
60 | },
61 | {
62 | name: 'ember-canary',
63 | bower: {
64 | dependencies: {
65 | 'ember': 'components/ember#canary'
66 | },
67 | resolutions: {
68 | 'ember': 'canary'
69 | }
70 | },
71 | npm: {
72 | devDependencies: {
73 | 'ember-source': null
74 | }
75 | }
76 | },
77 | {
78 | name: 'ember-default',
79 | npm: {
80 | devDependencies: {}
81 | }
82 | }
83 | ]
84 | };
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-graph-data",
3 | "version": "0.2.1",
4 | "description": "GraphQL & ember-data integration for ambitious web apps.",
5 | "keywords": [
6 | "ember-addon",
7 | "ember-data",
8 | "graphql"
9 | ],
10 | "license": "MIT",
11 | "author": "Damian Romanów",
12 | "directories": {
13 | "doc": "doc",
14 | "test": "tests"
15 | },
16 | "repository": "https://github.com/HarenBroog/ember-graph-data",
17 | "scripts": {
18 | "build": "ember build",
19 | "start": "ember server",
20 | "test": "ember try:each"
21 | },
22 | "dependencies": {
23 | "broccoli-webpack": "^1.0.0",
24 | "ember-cli-babel": "^6.3.0",
25 | "ember-cli-node-assets": "^0.2.2",
26 | "graphql-tag": "^1.2.4"
27 | },
28 | "devDependencies": {
29 | "broccoli-asset-rev": "^2.4.5",
30 | "ember-ajax": "^3.0.0",
31 | "ember-cli": "~2.15.1",
32 | "ember-cli-chai": "^0.4.0",
33 | "ember-cli-cors": "^0.0.2",
34 | "ember-cli-dependency-checker": "^2.0.0",
35 | "ember-cli-eslint": "^4.0.0",
36 | "ember-cli-htmlbars": "^2.0.1",
37 | "ember-cli-htmlbars-inline-precompile": "^1.0.0",
38 | "ember-cli-inject-live-reload": "^1.4.1",
39 | "ember-cli-mirage": "^0.4.0",
40 | "ember-cli-mocha": "^0.14.4",
41 | "ember-cli-shims": "^1.1.0",
42 | "ember-cli-sri": "^2.1.0",
43 | "ember-cli-uglify": "^1.2.0",
44 | "ember-data": "^2.17.0",
45 | "ember-disable-prototype-extensions": "^1.1.2",
46 | "ember-export-application-global": "^2.0.0",
47 | "ember-load-initializers": "^1.0.0",
48 | "ember-resolver": "^4.0.0",
49 | "ember-source": "~2.15.0",
50 | "loader.js": "^4.2.3"
51 | },
52 | "engines": {
53 | "node": "^4.5 || 6.* || >= 7.*"
54 | },
55 | "ember-addon": {
56 | "configPath": "tests/dummy/config",
57 | "before": [
58 | "ember-cli-babel"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/unit/build/graphql-filter-test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { describe, it, context, beforeEach } from 'mocha'
3 | import gql from 'graphql-tag'
4 | // import { setupTest } from 'ember-mocha'
5 | import testFragment from './test-fragment'
6 | import testQuery from './test-query'
7 |
8 | const normalizeString = (str) => str.replace(/\s+/g, ' ').trim()
9 | const compiledFragment = gql`
10 | fragment testFragment on Object {
11 | name
12 | }`
13 | const compiledQuery = gql`
14 | query TestQuery {
15 | subject {
16 | ...testFragment
17 | }
18 | }
19 |
20 | fragment testFragment on Object {
21 | name
22 | }`
23 |
24 | describe('Unit | Build | graphql-filter', function() {
25 | beforeEach(function() {
26 | this.query = testFragment
27 | this.originalQuery = compiledFragment
28 | this.originalString = normalizeString(`fragment testFragment on Object {
29 | name
30 | }`)
31 | })
32 |
33 | it('compiles AST definitions', function() {
34 | expect(this.query.definitions).to.deep.equal(this.originalQuery.definitions)
35 | })
36 |
37 | it('compiles string value', function() {
38 | expect(normalizeString(this.query.string)).to.equal(this.originalString)
39 | })
40 |
41 | context('query with #import references', function() {
42 | beforeEach(function() {
43 | this.query = testQuery
44 | this.originalQuery = compiledQuery
45 | this.originalString = normalizeString(`
46 | fragment testFragment on Object {
47 | name
48 | }
49 |
50 | query TestQuery {
51 | subject {
52 | ...testFragment
53 | }
54 | }`
55 | )
56 | })
57 |
58 | it('compiles AST definitions', function() {
59 | expect(normalizeString(this.query.string)).to.equal(this.originalString)
60 | })
61 |
62 | it('compiles string value', function() {
63 | expect(normalizeString(this.query.string)).to.equal(this.originalString)
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/addon/utils.js:
--------------------------------------------------------------------------------
1 | export const mapValues = (object, fun) => {
2 | let result = {}
3 | Object.keys(object).forEach(key => {
4 | result[key] = fun(object[key], key)
5 | })
6 | return result
7 | }
8 |
9 | export const objectFilter = (object, fun) => {
10 | let result = {}
11 | Object.keys(object).forEach(key => {
12 | let val = object[key]
13 | if(fun(val, key))
14 | result[key] = object[key]
15 | })
16 | return result
17 | }
18 |
19 | export const objectReject = (object, fun) => {
20 | return objectFilter(
21 | object,
22 | (val, key) => !fun(val, key)
23 | )
24 | }
25 | export const isObject = val => typeof val === 'object' && val !== null
26 |
27 | export const extractFiles = (tree, treePath = '') => {
28 | const files = []
29 |
30 | const recurse = (node, nodePath) => {
31 | // Iterate enumerable properties of the node
32 | Object.keys(node).forEach(key => {
33 | // Skip non-object
34 | if (!isObject(node[key])) return
35 |
36 | const path = `${nodePath}${key}`
37 |
38 | if (
39 | // Node is a File
40 | (typeof File !== 'undefined' && node[key] instanceof File)
41 | ) {
42 | // Extract the file and it's object tree path
43 | files.push({ path, file: node[key] })
44 |
45 | // Delete the file. Array items must be deleted without reindexing to
46 | // allow repopulation in a reverse operation.
47 | delete node[key]
48 |
49 | // No further checks or recursion
50 | return
51 | }
52 |
53 | if (typeof FileList !== 'undefined' && node[key] instanceof FileList)
54 | // Convert read-only FileList to an array for manipulation
55 | node[key] = Array.from(node[key])
56 |
57 | // Recurse into child node
58 | recurse(node[key], `${path}.`)
59 | })
60 | }
61 |
62 | if (isObject(tree))
63 | // Recurse object tree
64 | recurse(
65 | tree,
66 | // If a tree path was provided, append a dot
67 | treePath === '' ? treePath : `${treePath}.`
68 | )
69 |
70 | return files
71 | }
72 |
--------------------------------------------------------------------------------
/addon/serializer.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember'
2 | const {
3 | run
4 | } = Ember
5 | import DS from 'ember-data'
6 | import {mapValues, isObject} from './utils'
7 | import {isNone} from '@ember/utils'
8 | import {isArray} from '@ember/array'
9 | import ArrayProxy from '@ember/array/proxy'
10 | import {get} from '@ember/object'
11 | import {
12 | camelize,
13 | underscore
14 | } from '@ember/string'
15 |
16 | export default DS.JSONSerializer.extend({
17 | isNewSerializerAPI: true,
18 |
19 | normalizeCase(string) {
20 | return camelize(string)
21 | },
22 |
23 | normalize(payload) {
24 | return run(() => this._normalize(payload))
25 | },
26 |
27 | _normalize(payload) {
28 | if(isArray(payload)) return this._normalizeArray(payload)
29 | let modelClass = this.extractModelClass(payload)
30 | if(modelClass) return this._normalizeModel(payload, modelClass)
31 | if(isObject(payload)) return this._normalizeObject(payload)
32 | return payload
33 | },
34 |
35 | _normalizeArray(array) {
36 | return ArrayProxy.create({
37 | content: array.map(item => this._normalize(item))
38 | })
39 | },
40 |
41 | _normalizeModel(payload, modelClass = null) {
42 | modelClass = modelClass || this.extractModelClass(payload)
43 |
44 | let resourceHash = {
45 | id: this.extractId(modelClass, payload),
46 | type: modelClass.modelName,
47 | attributes: this.extractAttributes(modelClass, payload),
48 | relationships: this.extractRelationships(modelClass, payload)
49 | }
50 | this.applyTransforms(modelClass, resourceHash.attributes)
51 | return this.get('store').push({data: resourceHash})
52 | },
53 |
54 | _normalizeObject(payload) {
55 | return mapValues(payload, (val) => this._normalize(val))
56 | },
57 |
58 | modelNameNamespaceSeparator: '--',
59 | modelNameFromGraphResponse(response) {
60 | return response.__typename
61 | },
62 |
63 | lookupModelClass(name) {
64 | try {
65 | return name ? this.get('store').modelFor(name) : null
66 | } catch(e) {
67 | return null
68 | }
69 | },
70 |
71 | extractModelClass(resourceHash) {
72 | if(!resourceHash)
73 | return null
74 | let type = this.modelNameFromGraphResponse(resourceHash)
75 | if(type)
76 | type = underscore(type.replace(this.get('modelNameNamespaceSeparator'), '/'))
77 | return this.lookupModelClass(type)
78 | },
79 |
80 | extractAttributes(modelClass, resourceHash) {
81 | let attributeKey
82 | let attributes = {}
83 |
84 | modelClass.eachAttribute((key) => {
85 | attributeKey = this.keyForAttribute(key, 'deserialize')
86 | if (resourceHash.hasOwnProperty(attributeKey)) {
87 | let val = resourceHash[attributeKey]
88 |
89 | attributes[key] = this._normalize(val)
90 | }
91 | })
92 |
93 | return attributes
94 | },
95 |
96 | extractRelationships(modelClass, resourceHash) {
97 | let relationships = {}
98 | modelClass.eachRelationship((key, meta) => {
99 | let data = resourceHash[key]
100 | if (isNone(data)) return
101 |
102 | let normalized = this._normalize(data)
103 |
104 | if(meta.kind === "hasMany") {
105 | relationships[key] = {
106 | data: normalized.map(item => ({ id: get(item, 'id'), type: meta.type }))
107 | }
108 | } else {
109 | relationships[key] = {
110 | data: { id: get(normalized, 'id'), type: meta.type }
111 | }
112 | }
113 | })
114 | return relationships
115 | },
116 | });
117 |
--------------------------------------------------------------------------------
/tests/unit/adapters/application-test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { describe, context, it, beforeEach, afterEach } from 'mocha'
3 | import { setupTest } from 'ember-mocha'
4 | import { startMirage } from 'dummy/initializers/ember-cli-mirage'
5 |
6 | import ApplicationSerializer from 'dummy/serializers/application'
7 | import mutation from 'dummy/graph/mutations/user-create'
8 | import gql from 'graphql-tag'
9 |
10 | const multipleDefinitionsQuery = gql`
11 | query users($firstName: String!) {
12 | users(firstName: $firstName) {
13 | id
14 | }
15 | }
16 |
17 | query friends($lastName: String!) {
18 | friends(lastName: $lastName) {
19 | id
20 | }
21 | }
22 | `
23 | describe('Unit | Adapter | application', function() {
24 | setupTest('adapter:application', {
25 | })
26 | let adapter
27 |
28 | beforeEach(function() {
29 | this.server = startMirage()
30 | this.subject({
31 | store: {
32 | serializerFor() {
33 | return ApplicationSerializer.create()
34 | }
35 | }
36 | })
37 | adapter = this.subject()
38 | })
39 |
40 | afterEach(function() {
41 | this.server.shutdown()
42 | })
43 |
44 | describe('#query', function() {
45 | it('exists', function() {
46 | expect(this.subject().query).to.be.ok
47 | })
48 | })
49 |
50 | describe('#mutate', function() {
51 | it('exists', function() {
52 | expect(this.subject().mutate).to.be.ok
53 | })
54 | })
55 |
56 | describe('#request', function() {
57 | it('returns response', function() {
58 | let variables = {email: 'madderdin@hez-hezron.com', firstName: 'M'}
59 | this.subject().mutate({mutation, variables}).then(
60 | ({userCreate}) => {
61 | let result = userCreate
62 | expect(result).to.be.an('object')
63 | expect(result.email).to.be.eq('madderdin@hez-hezron.com')
64 | expect(result.firstName).to.be.eq('M')
65 | }
66 | )
67 | })
68 | })
69 |
70 | describe('graphHelpers', function() {
71 | let variables,
72 | _subject,
73 | testQuery
74 |
75 | beforeEach(function() {
76 | testQuery = mutation
77 | })
78 |
79 | describe('#normalizeVariables', function() {
80 | beforeEach(function() {
81 | variables = {
82 | notAllowedVar: 1,
83 | email: undefined,
84 | firstName: 'Mordimer',
85 | lastName: 'Madderdin'
86 | }
87 |
88 | _subject = () => adapter.graphHelper('normalizeVariables', variables, testQuery)
89 | })
90 |
91 | it('filters not allowed variables', function() {
92 | expect(_subject()).not.to.have.key('notAllowedVar')
93 | })
94 |
95 | it('filters undefined variables', function() {
96 | expect(_subject()).not.to.have.key('email')
97 |
98 | adapter.mutate({mutation, variables})
99 | })
100 |
101 | it('passes allowed variables', function() {
102 | expect(_subject()).to.have.key('firstName')
103 | expect(_subject().firstName).to.eq(variables.firstName)
104 |
105 | adapter.mutate({mutation, variables})
106 | })
107 |
108 | context('query with multiple definitions', function() {
109 | beforeEach(function() {
110 | testQuery = multipleDefinitionsQuery
111 | })
112 |
113 | it('passes allowed variables', function() {
114 | expect(_subject()).to.have.keys('firstName', 'lastName')
115 | })
116 | })
117 | })
118 |
119 | describe('#prepareQuery', function() {
120 | beforeEach(function() {
121 | testQuery = {
122 | string: `query users {
123 | users {
124 | id
125 | }
126 | }`
127 | }
128 | _subject = () => adapter.graphHelper('prepareQuery', testQuery).string
129 | })
130 |
131 | it('appends __typename on all levels', function() {
132 | let occurences = _subject().match(/__typename/g || []).length
133 | expect(occurences).to.eq(2)
134 | })
135 |
136 | context('graphOptions.addTypename disabled', function() {
137 | beforeEach(function() {
138 | adapter.graphOptions.addTypename = false
139 | })
140 |
141 | it('does not append any __typename', function() {
142 | expect(_subject()).not.to.contains('__typename')
143 | })
144 | })
145 | })
146 | })
147 | })
148 |
--------------------------------------------------------------------------------
/addon/adapter.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data'
2 |
3 | import {
4 | get, getProperties, computed
5 | } from '@ember/object'
6 |
7 | import {
8 | isBlank
9 | } from '@ember/utils'
10 |
11 | import {
12 | objectReject
13 | } from './utils'
14 |
15 | import {
16 | assign
17 | } from '@ember/polyfills'
18 |
19 | import {
20 | getOwner
21 | } from '@ember/application'
22 |
23 | import Transport from './transport'
24 |
25 | export default DS.RESTAdapter.extend({
26 | mergedProperties: ['graphOptions'],
27 |
28 | cachedShoe: computed(function() {
29 | return getOwner(this).lookup('service:cached-shoe')
30 | }),
31 |
32 | graphOptions: {
33 | addTypename: true,
34 | },
35 |
36 | request(opts) {
37 | let {query, variables} = opts
38 | let originalVariables = variables
39 | let operationName = this.graphHelper('operationName', query)
40 |
41 | query = this.graphHelper('prepareQuery', query)
42 | variables = this.graphHelper('normalizeVariables', originalVariables, query)
43 |
44 | let data = {
45 | variables,
46 | query: query.string,
47 | }
48 | if(operationName)
49 | data.operationName = operationName
50 |
51 | let mergedOpts = assign(
52 | {},
53 | opts,
54 | {variables, originalVariables}
55 | )
56 |
57 | return this.ajax(
58 | [this.get('host'), this.get('namespace')].join('/'),
59 | 'POST',
60 | this.requestParams(data, mergedOpts)
61 | )
62 | .then(r => this.graphHelper('normalizeResponse', r))
63 | .then(r => this.handleGraphResponse(r, mergedOpts))
64 | .catch(e => this.handleGraphError(e, mergedOpts))
65 | },
66 |
67 | ajaxOptions() {
68 | let opts = this._super(...arguments)
69 |
70 | if(opts.body && opts.body instanceof FormData) {
71 | opts.processData = false
72 | opts.contentType = false
73 | opts.data = opts.body
74 | }
75 |
76 | return opts
77 | },
78 |
79 | requestParams(data, opts) {
80 | let transportType = null
81 | try {
82 | transportType = opts.options.transport
83 | } catch(e) {
84 | transportType = 'json'
85 | }
86 | return Transport[transportType](...arguments)
87 | },
88 |
89 | mutate(opts) {
90 | let query = opts.mutation
91 | let variables = opts.variables
92 |
93 | return this.request(
94 | assign({}, opts, {query, variables})
95 | )
96 | },
97 |
98 | query(opts) {
99 | return this.request(opts)
100 | },
101 |
102 | handleGraphResponse(response) {
103 | return response
104 | },
105 |
106 | handleGraphError(error) {
107 | return error
108 | },
109 |
110 | graphHelper(name, ...args) {
111 | return this.graphHelpers[name].call(this, ...args)
112 | },
113 |
114 | tokenizeAjaxRequest(url, type, options = {}) {
115 | let content = {}
116 | let maybeFormData = (options.body || options.data)
117 | let separator = this.get('separator')
118 | let b2a = this.get('cachedShoe.b2a')
119 |
120 | if(typeof FormData !== 'undefined' && maybeFormData instanceof FormData) {
121 | for (let [k, v] of maybeFormData.entries()) {
122 | content[k] = v
123 | }
124 | content.variables = JSON.parse(content.variables)
125 | } else {
126 | content = maybeFormData
127 | }
128 |
129 | let stringifyObject = (o) => JSON.stringify(o, Object.keys(o).sort())
130 | let tokenizeString = (s) => b2a.btoa(s.replace(/\\r\\n/g, '\\n').replace(/\s\s+/g, ' '))
131 |
132 | return tokenizeString(
133 | [url, type, stringifyObject(content)].join(separator)
134 | )
135 | .replace(/\+/g, '-')
136 | .replace(/\//g, '_')
137 | .replace(/=+$/, '')
138 | },
139 |
140 | graphHelpers: {
141 | operationName(query) {
142 | try {
143 | return query.definitions[0].name.value
144 | } catch(e) {
145 | return null
146 | }
147 | },
148 |
149 | prepareQuery(query) {
150 | let preparedQuery = assign({}, query)
151 | if(this.get('graphOptions.addTypename')) preparedQuery.string = preparedQuery.string.replace(/}/g, ` __typename\n}`)
152 | return preparedQuery
153 | },
154 |
155 | allowedVariables(query) {
156 | return query
157 | .definitions
158 | .map(
159 | def => (def.variableDefinitions || []).map(x => get(x, 'variable.name.value'))
160 | )
161 | .reduce((vars, acc) => acc.concat(vars), [])
162 | },
163 |
164 | normalizeVariables(variables, query) {
165 | if(!variables) return {}
166 | let allowedVariables = this.graphHelper('allowedVariables', query)
167 |
168 | return objectReject(
169 | getProperties(variables, allowedVariables),
170 | isBlank
171 | )
172 | },
173 |
174 | normalizeResponse(response) {
175 | if(response.errors) {
176 | throw new DS.AdapterError(response.errors)
177 | } else {
178 | return this.get('store').serializerFor('application').normalize(response.data)
179 | }
180 | }
181 | }
182 | })
183 |
--------------------------------------------------------------------------------
/tests/unit/serializers/application-test.js:
--------------------------------------------------------------------------------
1 | import {
2 | get
3 | } from '@ember/object'
4 | import {
5 | dasherize
6 | } from '@ember/string'
7 |
8 | import { expect } from 'chai'
9 | import { describe, context, it, beforeEach, afterEach } from 'mocha'
10 | import { setupTest } from 'ember-mocha'
11 |
12 | import startApp from '../../helpers/start-app'
13 | import destroyApp from '../../helpers/destroy-app'
14 |
15 | const isModel = (obj, name) => {
16 | if(name)
17 | return obj._internalModel.modelName == name
18 | return !!obj._internalModel
19 | }
20 |
21 | const behavesLikePOJO = function() {
22 | it('returns POJO', function() {
23 | expect(this._subject()).to.be.an('object')
24 | expect(isModel(this._subject())).not.to.be.ok
25 | })
26 |
27 | it('assigns all attributes', function() {
28 | expect(this._subject()).to.have.keys(Object.keys(this.payload()))
29 | })
30 |
31 | it('lookups models on nested levels', function() {
32 | let result = this._subject()
33 | let probablyUserRole = get(result, 'role')
34 | let probablyUserBlogPosts = get(result, 'posts')
35 |
36 | expect(isModel(probablyUserRole, 'user/role')).to.be.ok
37 | expect(isModel(probablyUserBlogPosts.get('firstObject'), 'user/blog-post')).to.be.ok
38 | })
39 | }
40 |
41 | describe('Unit | Serializer | application', function() {
42 | setupTest('serializer:application', {
43 | })
44 |
45 | beforeEach(function() {
46 | this.application = startApp()
47 | this.store = this.application.__container__.lookup('service:store', true)
48 | this.serializer = this.subject()
49 | this.serializer.set('store', this.store)
50 | })
51 |
52 | afterEach(function() {
53 | destroyApp(this.application)
54 | })
55 |
56 | describe('#normalize', function() {
57 | beforeEach(function() {
58 | this.payload = function() {
59 | return {
60 | id: '1',
61 | email: 'madderdin@hez-hezron.com',
62 | nonModelAttribute: 'madderdin@hez-hezron.com',
63 |
64 | posts: [
65 | {id: '1', title: 'A', body: 'aaaa', __typename: 'user/blog-post' },
66 | {id: '2', title: 'B', body: 'bbbb', __typename: 'user/blog-post' }
67 | ],
68 |
69 | role: {
70 | id: '1',
71 | name: 'inquisitor',
72 | __typename: 'user/role'
73 | },
74 |
75 | __typename: this.typename
76 | }}
77 | this._subject = function() { return this.serializer.normalize(this.payload()) }
78 | })
79 |
80 | context('model for __typename exists', function() {
81 | beforeEach(function() {
82 | this.typename = 'user'
83 | })
84 |
85 | it('returns DS.Model', function() {
86 | expect(this._subject()._internalModel.modelName).to.eq('user')
87 | })
88 |
89 | it('assigns DS.Model attributes', function() {
90 | expect(this._subject().get('id')).to.eq(this.payload().id)
91 | expect(this._subject().get('email')).to.eq(this.payload().email)
92 | })
93 |
94 | it('omits attributes not defined in DS.Model', function() {
95 | expect(this._subject().get('nonModelAttribute')).not.to.ok
96 | })
97 |
98 | it('serializes hasMany relationship', function() {
99 | let relationship = this._subject().get('posts').toArray()
100 | expect(relationship).to.be.an('array')
101 | expect(isModel(relationship[0], 'user/blog-post')).to.be.ok
102 | })
103 |
104 | it('serializes belongsTo relationship', function() {
105 | let relationship = this._subject().get('role.content')
106 | expect(isModel(relationship, 'user/role')).to.be.ok
107 | })
108 | })
109 |
110 | context('model for __typename does not exist', function() {
111 | beforeEach(function() {
112 | this.typename = 'non-existing-model'
113 | })
114 |
115 | behavesLikePOJO()
116 | })
117 |
118 | context('__typename missing', function() {
119 | beforeEach(function() {
120 | this.payload = function() {
121 | return {
122 | id: '1',
123 | email: 'madderdin@hez-hezron.com',
124 | nonModelAttribute: 'madderdin@hez-hezron.com',
125 |
126 | posts: [
127 | {id: '1', title: 'A', body: 'aaaa', __typename: 'user/blog-post' },
128 | {id: '2', title: 'B', body: 'bbbb', __typename: 'user/blog-post' }
129 | ],
130 |
131 | role: {
132 | id: '1',
133 | name: 'inquisitor',
134 | __typename: 'user/role'
135 | }
136 | }
137 | }
138 | })
139 |
140 | behavesLikePOJO()
141 | })
142 | })
143 |
144 | describe('#extractModelClass', function() {
145 | beforeEach(function() {
146 | this._subject = function() { return this.serializer.extractModelClass(this.payload()) }
147 | this.payload = function() {
148 | return {
149 | __typename: this.typename
150 | }
151 | }
152 | this.typename = 'user/blog-post'
153 | }),
154 |
155 | it('calls #modelNameFromGraphResponse', function() {
156 | let called = false
157 | this.serializer.modelNameFromGraphResponse = function() {
158 | called = true
159 | }
160 |
161 | this._subject()
162 | expect(called).to.be.ok
163 | })
164 |
165 | it('returns DS.Model class', function() {
166 | expect(this._subject().modelName).to.eq(this.typename)
167 | })
168 |
169 | context('__typename in PascalCase', function() {
170 | beforeEach(function() {
171 | this.typename = 'AdminUser'
172 | })
173 |
174 | it('returns DS.Model class', function() {
175 | expect(this._subject().modelName).to.eq(dasherize(this.typename))
176 | })
177 | })
178 |
179 | context('__typename in camelCase', function() {
180 | beforeEach(function() {
181 | this.typename = 'adminUser'
182 | })
183 |
184 | it('returns DS.Model class', function() {
185 | expect(this._subject().modelName).to.eq(dasherize(this.typename))
186 | })
187 | })
188 |
189 | context('__typename dasherized with double dash', function() {
190 | beforeEach(function() {
191 | this.typename = 'user--blog-post'
192 | })
193 |
194 | it('returns DS.Model class', function() {
195 | expect(this._subject().modelName).to.eq('user/blog-post')
196 | })
197 | })
198 |
199 | context('__typename with custom namespace separator', function() {
200 | beforeEach(function() {
201 | this.typename = 'User$$$BlogPost'
202 | this.serializer.modelNameNamespaceSeparator = '$$$'
203 | })
204 |
205 | it('returns DS.Model class', function() {
206 | expect(this._subject().modelName).to.eq('user/blog-post')
207 | })
208 | })
209 |
210 | it('returns DS.Model class for some strange cases', function() {
211 | [
212 | `user--blog-post`,
213 | `User--BlogPost`,
214 | `user--Blog-Post`,
215 | `User--blogPost`,
216 | `User--blog_Post`,
217 | `user/blog-post`,
218 | `user/blog_post`,
219 | `User/BlogPost`,
220 | ].forEach((typename) => {
221 | this.typename = typename
222 | expect(this._subject().modelName).to.eq('user/blog-post')
223 | })
224 | })
225 | })
226 | })
227 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/ember-graph-data) [](https://www.npmjs.com/package/ember-graph-data) [](http://emberobserver.com/addons/ember-graph-data) [](https://travis-ci.org/HarenBroog/ember-graph-data) [](https://codeclimate.com/github/HarenBroog/ember-graph-data)
2 |
3 | # ember-graph-data
4 |
5 | GraphQL & EmberData integration for ambitious apps!
6 |
7 |
8 | * [Installation](#installation)
9 | * [Minimal setup](#minimal-setup)
10 | * [Usage](#usage)
11 | * [Transport layer](#transport-layer)
12 | * [Adapter](#adapter)
13 | * [headers support](#headers-support)
14 | * [handle error & handle response](#handle-error--handle-response)
15 | * [additional config](#additional-config)
16 | * [Serializer](#serializer)
17 | * [custom modelName mapping](#custom-modelname-mapping)
18 | * [custom modelName namespace separator](#custom-modelname-namespace-separator)
19 | * [Automatic model lookup](#automatic-model-lookup)
20 | * [Fastboot](#fastboot)
21 | * [enhanced rehydration](#enhanced-rehydration)
22 |
23 |
24 | ## Installation
25 |
26 | Ensure you have `ember-data` installed:
27 |
28 | ```bash
29 | ember install ember-data
30 | ```
31 |
32 | And then:
33 | ```bash
34 | ember install ember-graph-data
35 | ```
36 | ## Minimal setup
37 |
38 | `app/adapters/application.js`
39 | ```js
40 | import GraphAdapter from 'ember-graph-data/adapter'
41 |
42 | export default GraphAdapter.extend({
43 | host: 'http://localhost:4000', // your API host
44 | namespace: 'api/v1/graph', // your API namespace
45 | })
46 | ```
47 | `app/serializers/application.js`
48 | ```js
49 | import GraphSerializer from 'ember-graph-data/serializer'
50 |
51 | export default GraphSerializer.extend()
52 | ```
53 |
54 | ## Usage
55 |
56 | `app/routes/posts.js`
57 | ```js
58 | import Route from '@ember/routing/route'
59 | import query from 'my-app/gql/queries/posts'
60 | import mutation from 'my-app/gql/mutations/post-create'
61 |
62 | export default Route.extend({
63 | model(params) {
64 | let variables = { page: 1 }
65 | return this.store.graphQuery({query, variables})
66 | }
67 |
68 | actions: {
69 | createPost(variables) {
70 | return this.store.graphMutate({mutation, variables})
71 | }
72 | }
73 | })
74 | ```
75 |
76 | ### Transport layer
77 |
78 | Sometimes `json` is not sufficient in expressing our application needs. File upload is a good example. Of course it can be done by sending them in `base64` form, but it is extremely ineffective (particularly with big files). Or we can prepare special non-graphql endpoint on the server side. None of the above seems to be a good solution. That's why `ember-graph-data` supports sending `graphql` queries and mutations in `json` and `multipart` form. In `multipart` mode, adapter will serialize any file encountered in `variables` as another field in multipart request.
79 |
80 | `app/routes/images.js`
81 | ```js
82 | import Route from '@ember/routing/route'
83 | import query from 'my-app/gql/queries/images'
84 | import mutation from 'my-app/gql/mutations/image-create'
85 |
86 | export default Route.extend({
87 | model(params) {
88 | let variables = { page: 1 }
89 | return this.store.graphQuery({query, variables})
90 | }
91 |
92 | actions: {
93 | createImage(file) {
94 | let variables = { file }
95 | let options = { transport: 'multipart' }
96 | return this.store.graphMutate({mutation, variables, options})
97 | }
98 | }
99 | })
100 | ```
101 | On the server side it was tested with:
102 | * [absinthe_plug](https://github.com/absinthe-graphql/absinthe_plug)
103 |
104 | ## Adapter
105 | ### headers support
106 | `app/adapters/application.js`
107 | ```js
108 | import GraphAdapter from 'ember-graph-data/adapter'
109 | import {computed} from '@ember/object'
110 | import {inject as service} from '@ember/service'
111 |
112 | export default GraphAdapter.extend({
113 | session: service(),
114 | headers: computed('session.jwt', function() {
115 | return {
116 | // authorize reuests
117 | 'Authorization': `Bearer ${this.get('session.jwt')}`,
118 | // maybe provide localized output?
119 | 'Content-Language': 'pl'
120 | // etc
121 | }
122 | })
123 | })
124 | ```
125 |
126 | ### handle error & handle response
127 | `app/adapters/application.js`
128 | ```js
129 | import GraphAdapter from 'ember-graph-data/adapter'
130 | import {inject as service} from '@ember/service'
131 |
132 | export default GraphAdapter.extend({
133 | eventBus: service(),
134 |
135 | handleGraphError(error, {query, variables}) {
136 | let errors = error.response.errors || []
137 | if (errors.every((err) => err.code !== 'unauthorized')) return error
138 | // example only. Do whatever you want :)
139 | this.get('eventBus').dispatch({type: 'UNAUTHORIZED'})
140 | }
141 |
142 | // Hook after successful request
143 | handleGraphResponse(response, {query, variables}) {
144 | return response
145 | },
146 | }
147 | ```
148 |
149 | ### additional config
150 | You can configure behaviour of graph adapter. Below options are defaults.
151 | `app/adapters/application.js`
152 | ```js
153 | import GraphAdapter from 'ember-graph-data/adapter'
154 | export default GraphAdapter.extend({
155 | graphOptions: {
156 | /* appends __typename field to every object in query.
157 | This is used for model lookup.
158 | */
159 | addTypename: true,
160 | },
161 | })
162 | ```
163 | ## Serializer
164 | ### custom modelName mapping
165 | In case when `__typename` field from API does not directly reflect your DS model names, you can customize it in `modelNameFromGraphResponse`:
166 | `app/serializers/application.js`
167 | ```js
168 | import GraphSerializer from 'ember-graph-data/serializer'
169 |
170 | export default GraphSerializer.extend({
171 | modelNameFromGraphResponse(response) {
172 | return response.__typename
173 | }
174 | }
175 | ```
176 |
177 | ### custom modelName namespace separator
178 | Proper handling namespaced DS models requires `__typename` to contain namespace separator. For instance, model `user/blog-post` will be looked-up correctly, for following `__typename` values:
179 |
180 | * `user--blog-post`
181 | * `User--BlogPost`
182 | * `user--Blog-Post`
183 | * `User--blogPost`
184 | * `User--blog_Post`
185 | * `user/blog-post`
186 | * `user/blog_post`
187 | * `User/BlogPost`
188 |
189 | It is not adviced to apply such incoherency in a naming convetion, but still it will be handled. `ember-graph-data` accepts `--` and `/` defaultly as a namespace separator. You can adjust that to your needs like this:
190 |
191 | `app/serializers/application.js`
192 | ```js
193 | import GraphSerializer from 'ember-graph-data/serializer'
194 |
195 | export default GraphSerializer.extend({
196 | modelNameNamespaceSeparator: '$$$'
197 | }
198 | ```
199 |
200 | And from now, `user$$$blog-post` and other variations will be recognized correctly.
201 |
202 | ## Automatic model lookup
203 |
204 | `GraphSerializer` automatically lookups and instantiates models for you. This process relies on `__typename` field which is returned from GraphQL server in every object. Lets make some assumptions:
205 |
206 | You have defined following models:
207 |
208 | `app/models/user-role.js`
209 | ```js
210 | import DS from 'ember-data'
211 |
212 | const {
213 | Model,
214 | attr
215 | } = DS
216 |
217 | export default Model.extend({
218 | name: attr(),
219 | code: attr()
220 | })
221 | ```
222 |
223 | `app/models/user.js`
224 | ```js
225 | import DS from 'ember-data'
226 |
227 | const {
228 | Model,
229 | attr,
230 | belongsTo
231 | } = DS
232 |
233 | export default Model.extend({
234 | firstName: attr(),
235 | lastName: attr(),
236 | email: attr(),
237 | role: belongsTo('user-role', { async: false })
238 | });
239 | ```
240 |
241 | You have sent following query to the GraphQL server:
242 |
243 | `app/graph/queries/users.graphql`
244 | ```graphql
245 | query users {
246 | users {
247 | {
248 | id
249 | firstName
250 | lastName
251 |
252 | role {
253 | id
254 | name
255 | code
256 | }
257 | }
258 | }
259 | }
260 | ```
261 |
262 | In result of above actions, you will get an array of User models. You can also inspect those models in a `Data` tab of Ember inspector. Moreover, each User will have association `role` properly set. Simple, yet powerful.
263 |
264 | # Fastboot
265 | Fastboot is supported by default.
266 |
267 | ## enhanced rehydration
268 |
269 | Moreover, this addon supports automatic requests caching in [Shoebox](https://github.com/ember-fastboot/ember-cli-fastboot#the-shoebox). Thanks to this, application does not need to refetch already gathered data on the browser side. Mechanics of this process is provided by [ember-cached-shoe](https://github.com/Appchance/ember-cached-shoe). More details to be found in this addon README.
270 |
--------------------------------------------------------------------------------