├── app ├── .gitkeep └── initializers │ └── ember-data-meta-links-improvements.js ├── addon ├── .gitkeep ├── patches │ ├── model.js │ ├── rest-adapter.js │ ├── belongs-to-reference.js │ ├── adapter-populated-record-array.js │ ├── json-serializer.js │ ├── record-array.js │ ├── record-reference.js │ ├── rest-serializer.js │ ├── json-api-serializer.js │ ├── has-many-reference.js │ └── store.js ├── initializers │ └── ember-data-meta-links-improvements.js ├── index.js └── references │ ├── link.js │ └── record-array.js ├── vendor └── .gitkeep ├── tests ├── unit │ └── .gitkeep ├── integration │ ├── .gitkeep │ ├── belongs-to-reference-test.js │ ├── has-many-reference-test.js │ ├── findAll-test.js │ ├── query-test.js │ ├── has-many-links-test.js │ └── record-meta-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── author.js │ │ │ ├── chapter.js │ │ │ ├── rest-book.js │ │ │ └── book.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── templates │ │ │ └── components │ │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── serializers │ │ │ └── rest-book.js │ │ ├── router.js │ │ ├── app.js │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ └── crossdomain.xml │ └── config │ │ └── environment.js ├── .eslintrc.js ├── test-helper.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ ├── start-app.js │ └── module-for-acceptance.js └── index.html ├── .watchmanconfig ├── .bowerrc ├── index.js ├── config ├── environment.js └── ember-try.js ├── .eslintrc.js ├── bower.json ├── .npmignore ├── testem.js ├── .ember-cli ├── .gitignore ├── .editorconfig ├── ember-cli-build.js ├── .travis.yml ├── LICENSE.md ├── 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 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../node_modules/ember-cli-eslint/coding-standard/ember-testing.js' 3 | }; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-data-meta-links-improvements' 6 | }; 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/author.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { Model } = DS; 4 | 5 | export default Model.extend({ 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/chapter.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { Model } = DS; 4 | 5 | export default Model.extend({ 6 | }); 7 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/rest-book.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { RESTSerializer } = DS; 4 | 5 | export default RESTSerializer.extend(); 6 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/models/rest-book.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { Model, attr } = DS; 4 | 5 | export default Model.extend({ 6 | name: attr() 7 | }); 8 | -------------------------------------------------------------------------------- /app/initializers/ember-data-meta-links-improvements.js: -------------------------------------------------------------------------------- 1 | export { default, initialize } from 'ember-data-meta-links-improvements/initializers/ember-data-meta-links-improvements'; 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | extends: [ 5 | require.resolve('ember-cli-eslint/coding-standard/ember-application.js') 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/book.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { Model, belongsTo, hasMany } = DS; 4 | 5 | export default Model.extend({ 6 | author: belongsTo(), 7 | chapters: hasMany() 8 | }); 9 | -------------------------------------------------------------------------------- /addon/patches/model.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | DS.Model.reopen({ 4 | ref() { 5 | let modelName = this.constructor.modelName; 6 | return this.store.getReference(modelName, this.id); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /addon/patches/rest-adapter.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { RESTAdapter } = DS; 4 | 5 | RESTAdapter.reopen({ 6 | 7 | findLink(link) { 8 | return this.ajax(link, "GET"); 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /addon/patches/belongs-to-reference.js: -------------------------------------------------------------------------------- 1 | import BelongsToReference from 'ember-data/-private/system/references/belongs-to'; 2 | 3 | BelongsToReference.prototype.parentRef = function() { 4 | return this.internalModel.recordReference; 5 | } 6 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-meta-links-improvements", 3 | "dependencies": { 4 | "ember": "~2.7.0", 5 | "ember-cli-shims": "0.1.1", 6 | "ember-qunit-notifications": "0.1.0", 7 | "pretender": "^0.12.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /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 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /.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": false 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 http://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 | -------------------------------------------------------------------------------- /addon/initializers/ember-data-meta-links-improvements.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements" 2 | 3 | export function initialize(/* application */) { 4 | // patches are applied by importing ember-data-meta-links-improvements 5 | } 6 | 7 | export default { 8 | name: 'ember-data-meta-links-improvements', 9 | initialize 10 | }; 11 | -------------------------------------------------------------------------------- /addon/patches/adapter-populated-record-array.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { AdapterPopulatedRecordArray } = DS; 4 | 5 | AdapterPopulatedRecordArray.reopen({ 6 | 7 | loadRecords(records, payload) { 8 | this._super(...arguments); 9 | 10 | let ref = this.ref(); 11 | ref.__update_meta(payload.meta); 12 | ref.__update_links(payload.links); 13 | } 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /addon/patches/json-serializer.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | 3 | const { JSONSerializer } = DS; 4 | 5 | JSONSerializer.reopen({ 6 | 7 | normalizeFindAllResponse(store, modelClass) { 8 | let normalized = this._super(...arguments); 9 | normalized.__is_findAll = true; 10 | normalized.__primaryModelName = modelClass.modelName; 11 | return normalized; 12 | } 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /addon/patches/record-array.js: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | import RecordArrayReference from "../references/record-array"; 3 | 4 | const { RecordArray } = DS; 5 | 6 | RecordArray.reopen({ 7 | 8 | init() { 9 | this._super(...arguments); 10 | 11 | let { store } = this; 12 | this._ref = new RecordArrayReference({ recordArray: this, store }); 13 | }, 14 | 15 | ref() { 16 | return this._ref; 17 | } 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | import './patches/store'; 2 | import './patches/rest-adapter'; 3 | import './patches/json-serializer'; 4 | import './patches/json-api-serializer'; 5 | import './patches/rest-serializer'; 6 | import './patches/record-reference'; 7 | import './patches/has-many-reference'; 8 | import './patches/belongs-to-reference'; 9 | import './patches/model'; 10 | import './patches/adapter-populated-record-array'; 11 | import './patches/record-array'; 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /addon/patches/record-reference.js: -------------------------------------------------------------------------------- 1 | import RecordReference from 'ember-data/-private/system/references/record'; 2 | 3 | RecordReference.prototype.__update_meta = function(meta) { 4 | this._meta = meta; 5 | }; 6 | 7 | RecordReference.prototype.__update_responseMeta = function(meta) { 8 | this._responseMeta = meta; 9 | }; 10 | 11 | RecordReference.prototype.meta = function(which) { 12 | if (which === "response") { 13 | return this._responseMeta; 14 | } 15 | 16 | return this._meta; 17 | }; 18 | -------------------------------------------------------------------------------- /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 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /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 application; 7 | 8 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /addon/references/link.js: -------------------------------------------------------------------------------- 1 | export default class LinkReference { 2 | 3 | constructor({ store, parentRef, name, href, meta }) { 4 | this._store = store; 5 | this._parentRef = parentRef; 6 | this._name = name; 7 | this._href = href; 8 | this._meta = meta; 9 | } 10 | 11 | name() { 12 | return this._name; 13 | } 14 | 15 | href() { 16 | return this._href; 17 | } 18 | 19 | meta() { 20 | return this._meta; 21 | } 22 | 23 | parentRef() { 24 | return this._parentRef; 25 | } 26 | 27 | load() { 28 | return this._store.loadLink(this); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberAddon(defaults, { 7 | babel: { 8 | includePolyfill: true 9 | } 10 | }); 11 | 12 | /* 13 | This build file specifies the options for the dummy test app of this 14 | addon, located in `/tests/dummy` 15 | This build file does *not* influence how the addon or the app using it 16 | behave. You most likely want to be modifying `./index.js` or app's build file 17 | */ 18 | 19 | return app.toTree(); 20 | }; 21 | -------------------------------------------------------------------------------- /addon/patches/rest-serializer.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { RESTSerializer } = DS; 4 | 5 | RESTSerializer.reopen({ 6 | 7 | normalize(model, payload) { 8 | let normalized = this._super(...arguments); 9 | normalized.data.meta = payload.meta; 10 | return normalized; 11 | }, 12 | 13 | pushPayload(store, payload) { 14 | const orig = store.push; 15 | let normalized; 16 | 17 | store.push = function(data) { 18 | normalized = data; 19 | } 20 | 21 | this._super(...arguments); 22 | 23 | store.push = orig; 24 | 25 | normalized.meta = payload.meta; 26 | 27 | store.push(normalized); 28 | } 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /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/patches/json-api-serializer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import DS from 'ember-data'; 3 | 4 | const { typeOf } = Ember; 5 | const { JSONAPISerializer } = DS; 6 | 7 | JSONAPISerializer.reopen({ 8 | 9 | normalizeSingleResponse(store, modelClass, payload) { 10 | let recordLevelMeta = payload.data && payload.data.meta; 11 | let normalized = this._super(...arguments); 12 | 13 | if (typeOf(normalized.data) === "object") { 14 | normalized.data._top_level_meta = normalized.meta; 15 | normalized.data.meta = recordLevelMeta; 16 | } 17 | 18 | // TODO included 19 | 20 | return normalized; 21 | }, 22 | 23 | normalize(modelClass, resourceHash) { 24 | let normalized = this._super(...arguments); 25 | normalized.data.meta = resourceHash.meta; 26 | return normalized; 27 | } 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | - EMBER_TRY_SCENARIO=ember-1.13 15 | - EMBER_TRY_SCENARIO=ember-release 16 | - EMBER_TRY_SCENARIO=ember-beta 17 | - EMBER_TRY_SCENARIO=ember-canary 18 | 19 | matrix: 20 | fast_finish: true 21 | allow_failures: 22 | - env: EMBER_TRY_SCENARIO=ember-canary 23 | 24 | before_install: 25 | - npm config set spin false 26 | - npm install -g bower 27 | - bower --version 28 | - npm install phantomjs-prebuilt 29 | - phantomjs --version 30 | 31 | install: 32 | - npm install 33 | - bower install 34 | 35 | script: 36 | # Usually, it's ok to finish the test scenario without reverting 37 | # to the addon's original dependency state, skipping "cleanup". 38 | - ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup 39 | -------------------------------------------------------------------------------- /addon/patches/has-many-reference.js: -------------------------------------------------------------------------------- 1 | import HasManyReference from 'ember-data/-private/system/references/has-many'; 2 | import LinkReference from 'ember-data-meta-links-improvements/references/link'; 3 | 4 | HasManyReference.prototype.links = function(name) { 5 | if (name) { 6 | return this._links[name]; 7 | } 8 | 9 | return Object.keys(this._links).map((name) => this[name], this); 10 | }; 11 | 12 | HasManyReference.prototype.__update_links = function(links) { 13 | this._links = {}; 14 | 15 | Object.keys(links || {}).forEach((name) => { 16 | let { href, meta } = links[name]; 17 | 18 | href = href || links[name]; 19 | 20 | let { store } = this; 21 | 22 | let link = new LinkReference({ parentRef: this, store, name, href, meta }); 23 | 24 | this._links[name] = link; 25 | }); 26 | }; 27 | 28 | HasManyReference.prototype.modelName = function() { 29 | return this.type; 30 | } 31 | 32 | HasManyReference.prototype.parentRef = function() { 33 | return this.internalModel.recordReference; 34 | } 35 | -------------------------------------------------------------------------------- /tests/integration/belongs-to-reference-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "BelongsToReference", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "push"); 23 | }, 24 | 25 | afterEach() { 26 | this.server.shutdown(); 27 | } 28 | }); 29 | 30 | test("parentRef()", async function(assert) { 31 | let book = this.push({ 32 | data: { 33 | type: "book", 34 | id: 1 35 | } 36 | }); 37 | 38 | let bookRef = book.ref(); 39 | let authorRef = book.belongsTo("author"); 40 | 41 | assert.deepEqual(bookRef, authorRef.parentRef()); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/integration/has-many-reference-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "HasManyReference", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "push"); 23 | }, 24 | 25 | afterEach() { 26 | this.server.shutdown(); 27 | } 28 | }); 29 | 30 | test("parentRef()", async function(assert) { 31 | let book = this.push({ 32 | data: { 33 | type: "book", 34 | id: 1 35 | } 36 | }); 37 | 38 | let bookRef = book.ref(); 39 | let chaptersRef = book.hasMany("chapters"); 40 | 41 | assert.deepEqual(bookRef, chaptersRef.parentRef()); 42 | }); 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /addon/references/record-array.js: -------------------------------------------------------------------------------- 1 | import LinkReference from 'ember-data-meta-links-improvements/references/link'; 2 | 3 | export default class RecordArrayReference { 4 | 5 | constructor({ store, recordArray }) { 6 | this.store = store; 7 | this._recordArray = recordArray; 8 | } 9 | 10 | value() { 11 | return this._recordArray; 12 | } 13 | 14 | meta() { 15 | return this._meta; 16 | } 17 | 18 | links(name) { 19 | if (name) { 20 | return this._links[name]; 21 | } 22 | 23 | return Object.keys(this._links).map((name) => this[name], this); 24 | } 25 | 26 | modelName() { 27 | return this._recordArray.type.modelName; 28 | } 29 | 30 | __update_meta(meta) { 31 | this._meta = meta; 32 | } 33 | 34 | __update_links(links) { 35 | this._links = {}; 36 | 37 | Object.keys(links || {}).forEach((name) => { 38 | let { href, meta } = links[name]; 39 | 40 | href = href || links[name]; 41 | 42 | let { store } = this; 43 | let link = new LinkReference({ parentRef: this, store, name, href, meta }); 44 | 45 | this._links[name] = link; 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'default', 6 | bower: { 7 | dependencies: { } 8 | } 9 | }, 10 | { 11 | name: 'ember-1.13', 12 | bower: { 13 | dependencies: { 14 | 'ember': '~1.13.0' 15 | }, 16 | resolutions: { 17 | 'ember': '~1.13.0' 18 | } 19 | } 20 | }, 21 | { 22 | name: 'ember-release', 23 | bower: { 24 | dependencies: { 25 | 'ember': 'components/ember#release' 26 | }, 27 | resolutions: { 28 | 'ember': 'release' 29 | } 30 | } 31 | }, 32 | { 33 | name: 'ember-beta', 34 | bower: { 35 | dependencies: { 36 | 'ember': 'components/ember#beta' 37 | }, 38 | resolutions: { 39 | 'ember': 'beta' 40 | } 41 | } 42 | }, 43 | { 44 | name: 'ember-canary', 45 | bower: { 46 | dependencies: { 47 | 'ember': 'components/ember#canary' 48 | }, 49 | resolutions: { 50 | 'ember': 'canary' 51 | } 52 | } 53 | } 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-data-meta-links-improvements 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 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | } 20 | }; 21 | 22 | if (environment === 'development') { 23 | // ENV.APP.LOG_RESOLVER = true; 24 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 25 | // ENV.APP.LOG_TRANSITIONS = true; 26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 27 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 28 | } 29 | 30 | if (environment === 'test') { 31 | // Testem prefers this... 32 | ENV.locationType = 'none'; 33 | 34 | // keep test console output quieter 35 | ENV.APP.LOG_ACTIVE_GENERATION = false; 36 | ENV.APP.LOG_VIEW_LOOKUPS = false; 37 | 38 | ENV.APP.rootElement = '#ember-testing'; 39 | } 40 | 41 | if (environment === 'production') { 42 | 43 | } 44 | 45 | return ENV; 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-meta-links-improvements", 3 | "version": "0.0.2", 4 | "description": "POC for https://github.com/emberjs/rfcs/pull/160", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build", 11 | "start": "ember server", 12 | "test": "ember try:each" 13 | }, 14 | "repository": "https://github.com/pangratz/ember-data-meta-links-improvements", 15 | "engines": { 16 | "node": ">= 0.10.0" 17 | }, 18 | "author": "Clemens Müller ", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "broccoli-asset-rev": "^2.4.2", 22 | "ember-ajax": "^2.0.1", 23 | "ember-cli": "2.7.0", 24 | "ember-cli-app-version": "^1.0.0", 25 | "ember-cli-dependency-checker": "^1.2.0", 26 | "ember-cli-eslint": "1.7.0", 27 | "ember-cli-htmlbars": "^1.0.3", 28 | "ember-cli-htmlbars-inline-precompile": "^0.3.1", 29 | "ember-cli-inject-live-reload": "^1.4.0", 30 | "ember-cli-pretender": "0.6.0", 31 | "ember-cli-qunit": "^2.0.0", 32 | "ember-cli-release": "^0.2.9", 33 | "ember-cli-sri": "^2.1.0", 34 | "ember-cli-test-loader": "^1.1.0", 35 | "ember-cli-uglify": "^1.2.0", 36 | "ember-data": "^2.7.0", 37 | "ember-disable-prototype-extensions": "^1.1.0", 38 | "ember-export-application-global": "^1.0.5", 39 | "ember-load-initializers": "^0.5.1", 40 | "ember-resolver": "^2.0.3", 41 | "ember-welcome-page": "^1.0.1", 42 | "loader.js": "^4.0.1" 43 | }, 44 | "keywords": [ 45 | "ember-addon" 46 | ], 47 | "dependencies": { 48 | "ember-cli-babel": "^5.1.6" 49 | }, 50 | "ember-addon": { 51 | "configPath": "tests/dummy/config", 52 | "versionCompatibility": { 53 | "ember-data": ">=2.5.0" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /addon/patches/store.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | const { Store, Model } = DS; 4 | 5 | Store.reopen({ 6 | 7 | push(data) { 8 | let pushed = this._super(...arguments); 9 | 10 | if (Model.detectInstance(pushed)) { 11 | let recordRef = pushed.ref(); 12 | recordRef.__update_meta(data.data.meta); 13 | recordRef.__update_responseMeta(data.meta); 14 | 15 | let relationships = data.data.relationships || {}; 16 | Object.keys(relationships).forEach((name) => { 17 | let links = relationships[name].links; 18 | pushed.hasMany(name).__update_links(links); 19 | }); 20 | } 21 | 22 | if (Array.isArray(pushed)) { 23 | data.data.forEach(({ type, id, meta }) => { 24 | let recordReference = this.getReference(type, id); 25 | recordReference.__update_meta(meta); 26 | recordReference.__update_responseMeta(data.meta); 27 | }); 28 | } 29 | 30 | if (Array.isArray(data.included)) { 31 | data.included.forEach(({ type, id, meta }) => { 32 | this.getReference(type, id).__update_meta(meta); 33 | }); 34 | } 35 | 36 | if (data.__is_findAll) { 37 | let recordArray = this.peekAll(data.__primaryModelName); 38 | recordArray.ref().__update_meta(data.meta); 39 | recordArray.ref().__update_links(data.links); 40 | } 41 | 42 | return pushed; 43 | }, 44 | 45 | didSaveRecord(internalModel, { data }) { 46 | internalModel.recordReference.__update_meta(data.meta); 47 | internalModel.recordReference.__update_responseMeta(data._top_level_meta); 48 | return this._super(...arguments); 49 | }, 50 | 51 | loadLink(link) { 52 | let modelName = link.parentRef().modelName(); 53 | let href = link.href(); 54 | let model = this.modelFor(modelName); 55 | let adapter = this.adapterFor(modelName); 56 | let serializer = this.serializerFor(modelName); 57 | 58 | return adapter.findLink(href).then((payload) => { 59 | // TODO handle case where primary data of link is single resource 60 | let normalized = serializer.normalizeArrayResponse(this, model, payload); 61 | let pushed = this.push(normalized); 62 | let recordArray = this.recordArrayManager.createAdapterPopulatedRecordArray(modelName, {}); 63 | recordArray.loadRecords(pushed, normalized); 64 | return recordArray; 65 | }); 66 | } 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /tests/integration/findAll-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "store.findAll", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "findAll"); 23 | }, 24 | 25 | afterEach() { 26 | this.server.shutdown(); 27 | } 28 | }); 29 | 30 | test("ref().value()", async function(assert) { 31 | this.server.get('/books', function() { 32 | return [200, {}, JSON.stringify({ 33 | data: [{ 34 | id: 1, 35 | type: 'book' 36 | }] 37 | })]; 38 | }); 39 | 40 | let books = await this.findAll('book'); 41 | 42 | assert.deepEqual(books, books.ref().value()); 43 | }); 44 | 45 | test("links()", async function(assert) { 46 | this.server.get('/books', function() { 47 | return [200, {}, JSON.stringify({ 48 | data: [{ 49 | id: 1, 50 | type: 'book' 51 | }], 52 | links: { 53 | next: { 54 | href: "next-link", 55 | meta: { 56 | next: true 57 | } 58 | } 59 | } 60 | })]; 61 | }); 62 | 63 | let books = await this.findAll('book'); 64 | 65 | let nextLink = books.ref().links("next"); 66 | assert.ok(nextLink); 67 | assert.equal(nextLink.name(), "next"); 68 | assert.equal(nextLink.href(), "next-link"); 69 | assert.deepEqual(nextLink.meta(), { next: true }); 70 | }); 71 | 72 | test("meta()", async function(assert) { 73 | this.server.get('/books', function() { 74 | return [200, {}, JSON.stringify({ 75 | data: [{ 76 | id: 1, 77 | type: 'book' 78 | }], 79 | meta: { 80 | topLevel: true 81 | } 82 | })]; 83 | }); 84 | 85 | let books = await this.findAll('book'); 86 | 87 | let booksRef = books.ref(); 88 | assert.deepEqual(booksRef.meta(), { topLevel: true }); 89 | }); 90 | 91 | test("meta() is updated", async function(assert) { 92 | let counter; 93 | 94 | this.server.get('/books', function() { 95 | return [200, {}, JSON.stringify({ 96 | data: [{ 97 | id: 1, 98 | type: 'book' 99 | }], 100 | meta: { 101 | topLevel: counter 102 | } 103 | })]; 104 | }); 105 | 106 | counter = "first"; 107 | let books = await this.findAll('book'); 108 | 109 | let booksRef = books.ref(); 110 | assert.deepEqual(booksRef.meta(), { topLevel: "first" }); 111 | 112 | counter = "second"; 113 | await this.findAll('book', { reload: true }); 114 | 115 | assert.deepEqual(booksRef.meta(), { topLevel: "second" }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/integration/query-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "store.query", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "query"); 23 | }, 24 | 25 | afterEach() { 26 | this.server.shutdown(); 27 | } 28 | }); 29 | 30 | test("ref().value()", async function(assert) { 31 | this.server.get('/books', function() { 32 | return [200, {}, JSON.stringify({ 33 | data: [{ 34 | id: 1, 35 | type: 'book' 36 | }] 37 | })]; 38 | }); 39 | 40 | let books = await this.query('book', {}); 41 | 42 | assert.deepEqual(books, books.ref().value()); 43 | }); 44 | 45 | test("links()", async function(assert) { 46 | this.server.get('/books', function() { 47 | return [200, {}, JSON.stringify({ 48 | data: [{ 49 | id: 1, 50 | type: 'book' 51 | }], 52 | links: { 53 | next: { 54 | href: "next-link", 55 | meta: { 56 | next: true 57 | } 58 | } 59 | } 60 | })]; 61 | }); 62 | 63 | let books = await this.query('book', {}); 64 | 65 | let nextLink = books.ref().links("next"); 66 | assert.ok(nextLink); 67 | assert.equal(nextLink.name(), "next"); 68 | assert.equal(nextLink.href(), "next-link"); 69 | assert.deepEqual(nextLink.meta(), { next: true }); 70 | }); 71 | 72 | test("links().load()", async function(assert) { 73 | this.server.get('/books', function() { 74 | return [200, {}, JSON.stringify({ 75 | data: [{ 76 | id: 1, 77 | type: 'book' 78 | }], 79 | links: { 80 | next: "/next-link", 81 | prev: "/prev-link" 82 | } 83 | })]; 84 | }); 85 | 86 | let books = await this.query('book', {}); 87 | 88 | this.server.get('/next-link', function() { 89 | return [200, {}, JSON.stringify({ 90 | data: [{ 91 | type: "book", 92 | id: 2 93 | }], 94 | meta: { 95 | isNext: true 96 | } 97 | })]; 98 | }); 99 | 100 | let nextLink = books.ref().links("next"); 101 | let next = await nextLink.load(); 102 | 103 | assert.equal(next.get('length'), 1); 104 | assert.deepEqual(next.ref().meta(), { isNext: true }); 105 | 106 | let book2Ref = this.store.getReference("book", 2); 107 | assert.deepEqual(book2Ref.meta("response"), { isNext: true }); 108 | }); 109 | 110 | test("meta()", async function(assert) { 111 | this.server.get('/books', function() { 112 | return [200, {}, JSON.stringify({ 113 | data: [{ 114 | id: 1, 115 | type: 'book' 116 | }], 117 | meta: { 118 | topLevel: true 119 | } 120 | })]; 121 | }); 122 | 123 | let books = await this.query('book', {}); 124 | 125 | let booksRef = books.ref(); 126 | assert.deepEqual(booksRef.meta(), { topLevel: true }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/integration/has-many-links-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "hasMany links", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "push"); 23 | runify(this, "pushPayload"); 24 | runify(this, "findRecord"); 25 | runify(this, "createRecord"); 26 | runify(this, "queryRecord"); 27 | }, 28 | 29 | afterEach() { 30 | this.server.shutdown(); 31 | } 32 | }); 33 | 34 | test("no links", function(assert) { 35 | let book = this.push({ 36 | data: { 37 | id: 1, 38 | type: 'book', 39 | relationships: { 40 | chapters: { 41 | data: [] 42 | } 43 | } 44 | } 45 | }); 46 | 47 | let chaptersRef = book.hasMany("chapters"); 48 | 49 | let links = chaptersRef.links(); 50 | assert.equal(links.length, 0); 51 | }); 52 | 53 | test("links()", function(assert) { 54 | let book = this.push({ 55 | data: { 56 | id: 1, 57 | type: 'book', 58 | relationships: { 59 | chapters: { 60 | links: { 61 | self: { 62 | href: "self-link", 63 | meta: { 64 | chapters: true 65 | } 66 | }, 67 | related: "related-link" 68 | } 69 | } 70 | } 71 | } 72 | }); 73 | 74 | let chaptersRef = book.hasMany("chapters"); 75 | 76 | let links = chaptersRef.links(); 77 | assert.equal(links.length, 2); 78 | }); 79 | 80 | test("links().load()", async function(assert) { 81 | let book = this.push({ 82 | data: { 83 | id: 1, 84 | type: 'book', 85 | relationships: { 86 | chapters: { 87 | links: { 88 | self: "/self-link", 89 | related: "/related-link" 90 | } 91 | } 92 | } 93 | } 94 | }); 95 | 96 | let chaptersRef = book.hasMany("chapters"); 97 | 98 | this.server.get('/self-link', function() { 99 | return [200, {}, JSON.stringify({ 100 | data: [], 101 | meta: { 102 | isSelf: true 103 | } 104 | })]; 105 | }); 106 | 107 | let next = await chaptersRef.links("self").load(); 108 | 109 | assert.deepEqual(next.ref().meta(), { isSelf: true }); 110 | }); 111 | 112 | test("links(name) with link being an object", function(assert) { 113 | let book = this.push({ 114 | data: { 115 | id: 1, 116 | type: 'book', 117 | relationships: { 118 | chapters: { 119 | links: { 120 | self: { 121 | href: "self-link", 122 | meta: { 123 | chapters: true 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }); 131 | 132 | let chaptersRef = book.hasMany("chapters"); 133 | 134 | let selfLink = chaptersRef.links("self"); 135 | assert.ok(selfLink); 136 | assert.equal(selfLink.name(), "self"); 137 | assert.equal(selfLink.href(), "self-link"); 138 | assert.deepEqual(selfLink.meta(), { 139 | chapters: true 140 | }); 141 | }); 142 | 143 | test("links(name) with link being a string", function(assert) { 144 | let book = this.push({ 145 | data: { 146 | id: 1, 147 | type: 'book', 148 | relationships: { 149 | chapters: { 150 | links: { 151 | related: "related-link" 152 | } 153 | } 154 | } 155 | } 156 | }); 157 | 158 | let chaptersRef = book.hasMany("chapters"); 159 | 160 | let relatedLink = chaptersRef.links("related"); 161 | assert.ok(relatedLink); 162 | assert.equal(relatedLink.name(), "related"); 163 | assert.equal(relatedLink.href(), "related-link"); 164 | assert.notOk(relatedLink.meta()); 165 | }); 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-data-meta-links-improvements 2 | 3 | [![Build Status](https://travis-ci.org/pangratz/ember-data-meta-links-improvements.svg?branch=master)](https://travis-ci.org/pangratz/ember-data-meta-links-improvements) 4 | [![Ember Observer Score](https://emberobserver.com/badges/ember-data-meta-links-improvements.svg)](https://emberobserver.com/addons/ember-data-meta-links-improvements) 5 | 6 | This addon is a Proof of Concept for 7 | [RFC#160](https://github.com/emberjs/rfcs/pull/160) which aims to improve the 8 | meta and links situation within Ember Data. 9 | The goal is to see how the proposed API solves use cases when used in real 10 | applications. The outcome of this addon should be a profound test suite which 11 | covers all the use cases of the RFC. 12 | 13 | :warning: The current implementation heavily relies on patches of Ember Data 14 | internals, so it is definitely not encouraged to use in production, as 15 | stability and decent performance can not be guaranteed. Also, the proposed API 16 | of the RFC and this addon might diverge, though the goal is to keep them in 17 | alignment within a narrow time frame :warning: 18 | 19 | Currently the following improvements are implemented: 20 | 21 | - [x] [single record meta data](#single-record-meta-data) ([tests](tests/integration/record-meta-test.js)) 22 | - [x] get record level meta data via `record.ref().meta()` 23 | - [x] get response level meta data for single resource via `record.ref().meta("response")` 24 | - [ ] single record links 25 | - [ ] links for relationship references 26 | - [ ] belongs to 27 | - [x] [has many](#has-many-links) ([tests](tests/integration/has-many-links-test.js)) 28 | - [ ] meta and links for finders 29 | - [ ] queryRecord (meta works, links still missing) 30 | - [x] [query](#storequery) ([tests](tests/integration/query-test.js)) 31 | - [x] [findAll](#storefindall) ([tests](tests/integration/findAll-test.js)) 32 | - [ ] get reference for has-many via `hasManyRelationship.ref()` 33 | - [x] get parent reference for relationship reference 34 | - [x] belongs to ([tests](tests/integration/belongs-to-reference-test.js)) 35 | - [x] has many ([tests](tests/integration/has-many-reference-test.js)) 36 | - [ ] add hook when new data is received 37 | - [ ] `Model#didReceiveData` 38 | - [ ] further miscellaneous changes 39 | - [x] get parent reference of link via `linkRef.parentRef()` 40 | 41 | ## Installation 42 | 43 | `ember install ember-data-meta-links-improvements` 44 | 45 | ## Code samples 46 | 47 | ### Single record meta data 48 | 49 | ```js 50 | // GET /books/1 51 | // { 52 | // data: { 53 | // type: "book", 54 | // id: 1, 55 | // meta: { 56 | // recordLevel: true 57 | // } 58 | // }, 59 | // meta: { 60 | // topLevel: true 61 | // } 62 | // } 63 | this.store.findRecord('book', 1).then(function(book) { 64 | // get reference for record 65 | let bookRef = book.ref(); 66 | 67 | // get record level meta data 68 | let meta = bookRef.meta(); 69 | meta === { recordLevel: true }; 70 | 71 | // get response level meta data 72 | let topLevelMeta = bookRef.meta("response"); 73 | topLevelMeta === { topLevel: true }; 74 | }); 75 | ``` 76 | 77 | ### Has-many links 78 | 79 | ```js 80 | // GET /books/1 81 | // { 82 | // data: { 83 | // type: "book", 84 | // id: 1, 85 | // relationships: { 86 | // chapters: { 87 | // links: { 88 | // related: "related-link", 89 | // self: { 90 | // href: "self-link", 91 | // meta: { 92 | // selfLink: true 93 | // } 94 | // } 95 | // } 96 | // } 97 | // } 98 | // } 99 | // } 100 | this.store.findRecord('book', 1).then(function(book) { 101 | let chaptersRef = book.hasMany("chapters"); 102 | 103 | let related = chaptersRef.links("related"); 104 | related.href() === "related-link"; 105 | 106 | let next = chaptersRef.links("self"); 107 | next.meta() === { selfLink: true }; 108 | 109 | // GET /self-link 110 | // { 111 | // data: [], 112 | // meta: { 113 | // isSelf: true 114 | // } 115 | // } 116 | next.load().then(function(nextArray) { 117 | nextArray.ref().meta() === { isSelf: true } 118 | }); 119 | }); 120 | ``` 121 | 122 | ### `store.query` 123 | 124 | ```js 125 | // GET /books?page=2 126 | // { 127 | // data: [{ 128 | // type: "book", 129 | // id: 1 130 | // }], 131 | // links: { 132 | // next: { 133 | // href: "/books?page=3", 134 | // meta: { 135 | // isLast: true 136 | // } 137 | // }, 138 | // prev: { 139 | // href: "/books?page=1" 140 | // } 141 | // }, 142 | // meta: { 143 | // total: 123 144 | // } 145 | // } 146 | let books = await this.store.query('book', { page: 2 }).then(function(books) { 147 | let booksRef = books.ref(); 148 | 149 | let prev = booksRef.links("prev"); 150 | prev.href() === "/books?page=1"; 151 | 152 | let next = booksRef.links("next"); 153 | next.meta() === { isLast: true }; 154 | 155 | let meta = booksRef.meta(); 156 | meta === { total: 123 }; 157 | }); 158 | 159 | // GET /books?page=3 160 | // { 161 | // data: [{ 162 | // type: "book", 163 | // id: 1 164 | // }], 165 | // links: { 166 | // prev: { 167 | // href: "/books?page=2" 168 | // } 169 | // }, 170 | // meta: { 171 | // isLastPage: true 172 | // } 173 | // } 174 | let next = await books.ref().links("next").load(); 175 | 176 | next.ref().meta() === { isLastPage: true } 177 | ``` 178 | 179 | ### `store.findAll` 180 | 181 | ```js 182 | // GET /books 183 | // { 184 | // data: [{ 185 | // type: "book", 186 | // id: 1 187 | // }], 188 | // links: { 189 | // self: "self-link" 190 | // }, 191 | // meta: { 192 | // total: 123 193 | // } 194 | // } 195 | let books = await this.store.findAll('book'); 196 | 197 | let booksRef = books.ref(); 198 | 199 | let self = booksRef.links("self"); 200 | self.href() === "self-link"; 201 | 202 | let meta = booksRef.meta(); 203 | meta === { total: 123 }; 204 | 205 | // GET /books 206 | // { 207 | // data: [{ 208 | // type: "book", 209 | // id: 1 210 | // }], 211 | // meta: { 212 | // total: 456 213 | // } 214 | // } 215 | await this.store.findAll('book', { reload: true }); 216 | 217 | booksRef.meta() === { total: 456 }; 218 | ``` 219 | 220 | # Development 221 | 222 | ## Installation 223 | 224 | * `git clone` this repository 225 | * `npm install` 226 | * `bower install` 227 | 228 | ## Running 229 | 230 | * `ember serve` 231 | * Visit your app at http://localhost:4200. 232 | 233 | ## Running Tests 234 | 235 | * `npm test` (Runs `ember try:testall` to test your addon against multiple Ember versions) 236 | * `ember test` 237 | * `ember test --server` 238 | 239 | ## Building 240 | 241 | * `ember build` 242 | 243 | For more information on using ember-cli, visit [http://ember-cli.com/](http://ember-cli.com/). 244 | -------------------------------------------------------------------------------- /tests/integration/record-meta-test.js: -------------------------------------------------------------------------------- 1 | import "ember-data-meta-links-improvements"; 2 | import Ember from "ember"; 3 | import Pretender from "pretender"; 4 | import { moduleFor, test } from "ember-qunit"; 5 | 6 | const { run } = Ember; 7 | 8 | function runify(context, method) { 9 | context[method] = function() { 10 | return run(context.store, method, ...arguments); 11 | }; 12 | } 13 | 14 | moduleFor("service:store", "single record meta", { 15 | integration: true, 16 | 17 | beforeEach() { 18 | this.server = new Pretender(); 19 | 20 | this.store = this.subject(); 21 | 22 | runify(this, "push"); 23 | runify(this, "pushPayload"); 24 | runify(this, "findRecord"); 25 | runify(this, "createRecord"); 26 | runify(this, "queryRecord"); 27 | }, 28 | 29 | afterEach() { 30 | this.server.shutdown(); 31 | } 32 | }); 33 | 34 | test("via store.push(single)", function(assert) { 35 | let book = this.push({ 36 | data: { 37 | id: 1, 38 | type: 'book', 39 | meta: { 40 | recordLevel: true 41 | } 42 | }, 43 | meta: { 44 | topLevel: true 45 | } 46 | }); 47 | 48 | let meta = book.ref().meta(); 49 | assert.deepEqual(meta, { 50 | recordLevel: true 51 | }); 52 | 53 | let topLevelMeta = book.ref().meta("response"); 54 | assert.deepEqual(topLevelMeta, { 55 | topLevel: true 56 | }); 57 | }); 58 | 59 | test("via store.pushPayload(single)", function(assert) { 60 | this.pushPayload({ 61 | data: { 62 | id: 1, 63 | type: 'book', 64 | meta: { 65 | recordLevel: true 66 | } 67 | }, 68 | meta: { 69 | topLevel: true 70 | } 71 | }); 72 | 73 | let bookRef = this.store.getReference("book", 1); 74 | 75 | let meta = bookRef.meta(); 76 | assert.deepEqual(meta, { 77 | recordLevel: true 78 | }); 79 | 80 | let topLevelMeta = bookRef.meta("response"); 81 | assert.deepEqual(topLevelMeta, { 82 | topLevel: true 83 | }); 84 | }); 85 | 86 | test("via store.push(array)", function(assert) { 87 | let array = this.push({ 88 | data: [{ 89 | id: 1, 90 | type: 'book', 91 | meta: { 92 | recordLevel: "first" 93 | } 94 | } , { 95 | id: 2, 96 | type: 'book', 97 | meta: { 98 | recordLevel: "second" 99 | } 100 | }], 101 | meta: { 102 | topLevel: true 103 | } 104 | }); 105 | 106 | assert.deepEqual(array[0].ref().meta(), { 107 | recordLevel: "first" 108 | }); 109 | 110 | assert.deepEqual(array[1].ref().meta(), { 111 | recordLevel: "second" 112 | }); 113 | }); 114 | 115 | test("via included", function(assert) { 116 | this.push({ 117 | data: null, 118 | included: [{ 119 | id: 1, 120 | type: 'book', 121 | meta: { 122 | recordLevel: "first" 123 | } 124 | } , { 125 | id: 2, 126 | type: 'book', 127 | meta: { 128 | recordLevel: "second" 129 | } 130 | }], 131 | meta: { 132 | topLevel: true 133 | } 134 | }); 135 | 136 | let firstBookRef = this.store.getReference("book", 1); 137 | assert.deepEqual(firstBookRef.meta(), { 138 | recordLevel: "first" 139 | }); 140 | 141 | let secondBookRef = this.store.getReference("book", 2); 142 | assert.deepEqual(secondBookRef.meta(), { 143 | recordLevel: "second" 144 | }); 145 | 146 | // TODO API for getting top level meta: store.pushRef()? 147 | }); 148 | 149 | test("via store.findRecord()", async function(assert) { 150 | this.server.get('/books/1', function() { 151 | return [200, {}, JSON.stringify({ 152 | data: { 153 | type: 'book', 154 | id: 1, 155 | meta: { 156 | recordLevel: true 157 | } 158 | }, 159 | meta: { 160 | topLevel: true 161 | } 162 | })]; 163 | }); 164 | 165 | let book = await this.findRecord('book', 1); 166 | 167 | let meta = book.ref().meta(); 168 | assert.deepEqual(meta, { 169 | recordLevel: true 170 | }); 171 | 172 | let topLevelMeta = book.ref().meta("response"); 173 | assert.deepEqual(topLevelMeta, { 174 | topLevel: true 175 | }); 176 | }); 177 | 178 | test("via store.queryRecord()", async function(assert) { 179 | this.server.get('/books', function() { 180 | return [200, {}, JSON.stringify({ 181 | data: { 182 | type: 'book', 183 | id: 1, 184 | meta: { 185 | recordLevel: true 186 | } 187 | }, 188 | meta: { 189 | topLevel: true 190 | } 191 | })]; 192 | }); 193 | 194 | let book = await this.queryRecord('book', {}); 195 | 196 | let meta = book.ref().meta(); 197 | assert.deepEqual(meta, { 198 | recordLevel: true 199 | }); 200 | 201 | let topLevelMeta = book.ref().meta("response"); 202 | assert.deepEqual(topLevelMeta, { 203 | topLevel: true 204 | }); 205 | }); 206 | 207 | test("via store.createRecord()", async function(assert) { 208 | this.server.post('/books', function() { 209 | return [200, {}, JSON.stringify({ 210 | data: { 211 | type: 'book', 212 | id: 1, 213 | meta: { 214 | recordLevel: true 215 | } 216 | }, 217 | meta: { 218 | topLevel: true 219 | } 220 | })]; 221 | }); 222 | 223 | let book = this.createRecord('book'); 224 | await run(book, "save"); 225 | 226 | let meta = book.ref().meta(); 227 | assert.deepEqual(meta, { 228 | recordLevel: true 229 | }); 230 | 231 | let topLevelMeta = book.ref().meta("response"); 232 | assert.deepEqual(topLevelMeta, { 233 | topLevel: true 234 | }); 235 | }); 236 | 237 | test("via store.updateRecord()", async function(assert) { 238 | this.server.patch('/books/1', function() { 239 | return [200, {}, JSON.stringify({ 240 | data: { 241 | type: 'book', 242 | id: 1, 243 | meta: { 244 | recordLevel: true 245 | } 246 | }, 247 | meta: { 248 | topLevel: true 249 | } 250 | })]; 251 | }); 252 | 253 | let book = this.push({ 254 | data: { 255 | type: 'book', 256 | id: 1 257 | } 258 | }); 259 | await run(book, "save"); 260 | 261 | let meta = book.ref().meta(); 262 | assert.deepEqual(meta, { 263 | recordLevel: true 264 | }); 265 | 266 | let topLevelMeta = book.ref().meta("response"); 267 | assert.deepEqual(topLevelMeta, { 268 | topLevel: true 269 | }); 270 | }); 271 | 272 | test("works for rest serializer", async function(assert) { 273 | this.server.get('/rest-books/1', function() { 274 | return [200, {}, JSON.stringify({ 275 | "rest-book": { 276 | id: 1, 277 | meta: { 278 | recordLevel: true 279 | } 280 | }, 281 | meta: { 282 | topLevel: true 283 | } 284 | })]; 285 | }); 286 | 287 | let book = await this.findRecord("rest-book", 1); 288 | 289 | let meta = book.ref().meta(); 290 | assert.deepEqual(meta, { 291 | recordLevel: true 292 | }); 293 | 294 | let topLevelMeta = book.ref().meta("response"); 295 | assert.deepEqual(topLevelMeta, { 296 | topLevel: true 297 | }); 298 | }); 299 | 300 | test("pushPayload(single) works with rest serializer", async function(assert) { 301 | this.pushPayload("rest-book", { 302 | "rest-book": { 303 | id: 1, 304 | meta: { 305 | recordLevel: true 306 | } 307 | }, 308 | meta: { 309 | topLevel: true 310 | } 311 | }); 312 | 313 | let bookRef = this.store.getReference("rest-book", 1); 314 | 315 | let meta = bookRef.meta(); 316 | assert.deepEqual(meta, { 317 | recordLevel: true 318 | }); 319 | 320 | let topLevelMeta = bookRef.meta("response"); 321 | assert.deepEqual(topLevelMeta, { 322 | topLevel: true 323 | }); 324 | }); 325 | 326 | test("pushPayload(array) works with rest serializer", async function(assert) { 327 | this.pushPayload("rest-book", { 328 | "rest-books": [{ 329 | id: 1, 330 | meta: { 331 | recordLevel: true 332 | } 333 | }], 334 | meta: { 335 | topLevel: true 336 | } 337 | }); 338 | 339 | let bookRef = this.store.getReference("rest-book", 1); 340 | 341 | let meta = bookRef.meta(); 342 | assert.deepEqual(meta, { 343 | recordLevel: true 344 | }); 345 | 346 | let topLevelMeta = bookRef.meta("response"); 347 | assert.deepEqual(topLevelMeta, { 348 | topLevel: true 349 | }); 350 | }); 351 | --------------------------------------------------------------------------------