{"id":13806059,"url":"https://github.com/ember-m3/ember-m3","last_synced_at":"2025-05-13T21:32:11.766Z","repository":{"id":24318541,"uuid":"85887421","full_name":"ember-m3/ember-m3","owner":"ember-m3","description":null,"archived":false,"fork":false,"pushed_at":"2025-04-14T15:57:49.000Z","size":18238,"stargazers_count":85,"open_issues_count":58,"forks_count":38,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-20T00:48:25.384Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ember-m3.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-03-22T23:51:12.000Z","updated_at":"2025-03-19T18:30:55.000Z","dependencies_parsed_at":"2024-05-06T17:29:30.509Z","dependency_job_id":"82729593-ca64-4552-b2a8-806c3e020c72","html_url":"https://github.com/ember-m3/ember-m3","commit_stats":null,"previous_names":["hjdivad/ember-m3"],"tags_count":97,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ember-m3%2Fember-m3","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ember-m3%2Fember-m3/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ember-m3%2Fember-m3/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ember-m3%2Fember-m3/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ember-m3","download_url":"https://codeload.github.com/ember-m3/ember-m3/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254031230,"owners_count":22002724,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T01:01:07.480Z","updated_at":"2025-05-13T21:32:10.837Z","avatar_url":"https://github.com/ember-m3.png","language":"JavaScript","funding_links":[],"categories":["Packages"],"sub_categories":["Data Management"],"readme":"# ember-m3 ![CI](https://github.com/hjdivad/ember-m3/workflows/CI/badge.svg)\n\nThis addon provides an alternative model implementation to `DS.Model` that is\ncompatible with the rest of the [ember-data](https://github.com/emberjs/data) ecosystem.\n\n## Background\n\nEmber-data users define their schemas via `DS.Model` classes which explicitly\nstate what attributes and relationships they expect. Having many such classes\neach explicitly defining their schemas provides a lot of clarity and a pleasant\nenvironment for implementing standard object oriented principles.\n\nHowever, it can be an issue in environments where the API responses are not\neasily known in advance, or where they are so varied as to require thousands of\n`DS.Model`s which can be a burden both to developer ergonomics as well as\nruntime performance.\n\n---\n\nember-m3 lets you use a single class for many API endpoints, inferring the\nschema from the payload and API-specific conventions.\n\nFor example, if your API returns responses like the following:\n\n```js\n{\n  \"data\": {\n    \"id\": \"isbn:9780439708180\",\n    \"type\": \"com.example.bookstore.Book\",\n    \"attributes\": {\n      \"name\": \"Harry Potter and the Sorcerer's Stone\",\n      \"author\": \"urn:Author:3\",\n      \"chapters\": [{\n        \"name\": \"The Boy Who Lived\",\n        \"mentionedCharacters\": [\"urn:Character:harry\"],\n        \"readerComments\": [{\n          \"id\": \"urn:ReaderComment:1\",\n          \"type\": \"com.example.bookstore.ReaderComment\",\n          \"name\": \"Someone or Other\",\n          \"body\": \"I have it on good authority that this is part of a book of some kind\",\n        }]\n      }],\n    },\n  },\n  \"included\": [{\n    \"id\": \"urn:author:3\",\n    \"type\": \"com.example.bookstore.Author\",\n    \"attributes\": {\n      \"name\": \"JK Rowling\",\n    },\n  }],\n}\n```\n\nYou could support it with the following schema:\n\n```js\n// app/services/m3-schema.js\n//\n// generated via `ember generate service m3-schema`\nimport DefaultSchema from 'ember-m3/services/m3-schema';\n\nconst BookStoreRegExp = /^com\\.example\\.bookstore\\./;\nconst ISBNRegExp = /^isbn:/;\nconst URNRegExp = /^urn:/;\n\nfunction computeValue(key, value, modelName, schemaInterface) {\n   // the value is a reference\n    if (typeof value === 'string' \u0026\u0026 (ISBNRegExp.test(value) || URNRegExp.test(value))) {\n      return schemaInterface.reference({\n        type: null,\n        id: value,\n      });\n    }\n    // The value is a nested model\n    if (typeof value === 'object' \u0026\u0026 value !== null \u0026\u0026 typeof value.$type === 'string') {\n      return {\n        id: value.isbn,\n        type: value.$type,\n        attributes: value,\n      };\n    }\n    // Otherwise return the raw value\n    return value;\n}\n\nexport default class Schema extends DefaultSchema {\n  includesModel(modelName) {\n    return BookStoreRegExp.test(modelName);\n  }\n\n  computeAttribute(key, value, modelName, schemaInterface) {\n    if (Array.isArray(value)) {\n      return schemaInterface.managedArray(value.map((v) =\u003e computeValue(key, v, modelName, schemaInterface)));\n    }\n  } else {\n    return computeValue(key, value, modelName, schemaInterface);\n  }\n}\n```\n\nNotice that in this case, the schema doesn't specify anything model-specific and\nwould work whether the API returns 3 different kinds of models or 3,000.\n\nModel-specific information _is_ still needed to handle cases that cannot be\ngenerally inferred from the payload (such as distinguishing `Date` fields). See\nthe [Schema](#Schema) section for details.\n\n## Trade-Offs\n\nThe benefits of using ember-m3 over `DS.Model` are:\n\n- handle dynamic schemas whose structure is not known in advance\n- handle relationship references at arbitrary points in the payload seamlessly (eg relationship references within POJO attributes)\n- limit the payload size of schema information by inferring as much as\n  possible from the structure of the payload itself\n- more easily query arbitrary URLs, especially when the types of the returned models are\n  not known in advance\n\nThe trade-offs made for this include:\n\n- Having only one model class prevents the use of some OOP patterns: You can't\n  add computed properties to only one model for instance, and will need to\n  rely on a different pattern of helpers and utility functions\n- Inferring the schema from the payload can make the client side code less\n  clear as is often the case in \"static\" vs. \"dynamic\" tradeoffs\n\n## Installation\n\n- `ember install ember-m3`\n\n## Querying\n\nThe existing store API works as expected. `findRecord`, `queryRecord` \u0026c., will\nbuild a URL using the `-ember-m3` adapter and create a record for the returned\nresponse using `MegamorphicModel`. Note that the actual name queried will be\npassed to the adapter so you can build URLs correctly.\n\nFor example\n\n```js\nstore.findRecord('com.example.bookstore.book', 'isbn:9780439708180');\n```\n\nResults in an adapter call\n\n```js\nimport MegamorphicModel from 'ember-m3/model';\n\nfindRecord(store, modelClass, id, snapshot) {\n  modelClass === MegamorphicModel;\n  snapshot.modelName === 'com.example.bookstore.book';\n  id === 'isbn:9780439708180';\n}\n```\n\nember-m3 does not define an `-ember-m3` adapter but you can define one in your\napp. Otherwise the default adapter lookup rules are followed (ie your\n`application` adapter will be used).\n\n### Store.queryURL\n\nember-m3 also adds `store.queryURL`. This is helpful for one-off endpoints or\nendpoints where the type returned is not known and you just want a thin wrapper\naround the API response that knows how to look up relationships.\n\n```js\nstore.queryURL(url, options);\n```\n\n#### Return Value\n\nReturns a promise that will resolve to\n\n1.  A `MegamorphicModel` if the [primary data][json-api:primary-data] of the normalized response is a resource.\n2.  A `RecordArray` of `MegamorphicModel`s if the [primary data][json-api:primary-data] of the normalized response is an array of\n    resources.\n\nThe raw API response is normalized via the `-ember-m3` serializer. M3 does not\ndefine such a serializer but you can add one to your app if your API requires\nnormalization to JSON API.\n\n#### Arguments\n\n- `url` The URL path to query. The `-ember-m3` adapter is consulted for its\n  `host` and `namespace` properties.\n\n  - When `url` is an absolute URL, (eg `http://bookstore.example.com/books`) or a network-path reference (eg `//books`), the adapter's `host` and `namespace` properties are ignored.\n  - When `url` is an absolute path reference (eg `/books`) it is prefixed with the adapter's `host` and/or `namespace` if they are present.\n  - When `url` is a relative path reference it is prefixed with the adapter's\n    `host` and/or `namespace`, whichever is present. It is an error to call\n    `queryURL` when `url` is a relative path reference and the adapter specifies\n    neither `host` nor `namespace`.\n\n- `options` additional options. All are optional, as is the `options` object\n  itself.\n\n  - `options.method` defaults to `GET`. The HTTP method to use.\n\n  - `options.params` defaults to `null`. The parameters to include, either in the URL (for `GET`\n    requests) or request body (for others).\n\n  - `options.queryParams` defaults to `null`. The parameters that will be\n    converted to query string and appended to the URL, especially useful for\n    `POST` method which needs both URL with query string(`options.queryParams`)\n    and payload data(`options.params`).\n\n  - `options.cacheKey` defaults to `null`. A string to uniquely identify this\n    request. `null` or `undefined` indicates the result should not be cached.\n    It is passed to `serializer.normalizeResponse` as the `id` parameter, but\n    the serializer is free to ignore it.\n\n  - `options.reload` defaults to `false`. If `true`, make a request even if an\n    entry was found under `cacheKey`. Do not resolve the returned promise\n    until that request completes.\n\n  - `options.backgroundReload` defaults to `false`. If `true`, make a request\n    even if an entry was found under `cacheKey`. If `true` and a cached entry\n    was found, resolve the returned promise immediately with the cached entry\n    and update the store when the request completes.\n\n  - `options.adapterOptions` defaults to `undefined`. The custom options to pass along to the `queryURL` function on the adapter.\n\n#### Caching\n\nWhen `cacheKey` is provided, the response is cached under `cacheKey`.\n\nIf the response contains a model with an id, that model will be cached under that id as\nwell as under the `cacheKey`. The entry under the model's id and under the `cacheKey` will\npoint to the same model. Changes to the model will be reflected in both the models\nretrieved by `cacheKey` and the models retreived by the model's id.\n\nUsing `cacheKey` with `queryURL` can be useful to show, eg dashboard data or any other data\nthat changes over time.\n\nConsider the following:\n\n```js\nstore.queryURL('/newsfeed/latest', { cacheKey: 'newsfeed.latest', backgroundReload: true });\n```\n\nIn this example, the first time the user visits a route that makes this query,\nthe promise will wait to resolve until the request completes. The second time\nthe request is made the promise will resolve immediately with the cached values\nwhile loading fresh values in the background.\n\nNote that what is actually cached is the result: ie either a `MegamorphicModel`\nor, more likely, a `RecordArray` of `MegamorphicModel`s.\n\n---\n\nIt is possible to do the same thing in stock Ember Data by making a `@ember-data/model`\nclass to wrap your search results and querying via:\n\n```js\n// app/models/news-feed.js\nimport Model, { hasMany } from '@ember-data/model';\nexport class NewsFeed extends Model {\n  @hasMany('feed-item')\n  feedItems;\n}\n\n// somewhere, presumably in a route\nstore.findRecord('news-feed', 'latest', { backgroundReload: true });\n```\n\nAs with ember-m3 generally, similar functionality is provided without the need\nto create models and relationships within your app code.\n\n##### Cache Eviction\n\nBecause models (or `RecordArray`s of models) are cached, the cache can be emptied\nautomatically when the models are unloaded. In the case of `RecordArray`s of\nmodels, the entire cache entry is evicted if _any_ of the member models is\nunloaded.\n\n##### Manual Cache Insertion\n\nIn cases where we need to manually insert into the cache, we can use `cacheURL`.\nAs an example, we may need to compute a secondary cache key once we receive\nresponse from our API.\n\n```js\nstore.queryURL('/foo', { cacheKey }).then((result) =\u003e {\n  const secondaryCacheKey = computeSecondaryCacheKey(result);\n  store.cacheURL(secondaryCacheKey, result);\n});\n```\n\nWhen we unload the model, we will evict _both_ the initial `cacheKey` as well as\n`secondaryCacheKey`.\n\n```js\nstore.queryURL('/foo', { cacheKey: 'foo' }).then((result) =\u003e {\n  store.cacheURL('bar', result);\n\n  // Cache conceptually looks like: { foo: ..., bar: ...' }\n  result.unloadRecord();\n  // Cache is now empty\n});\n```\n\n## Schema\n\nYou have to register a schema to tell ember-m3 what types it should be enabled\nfor, as well as information that cannot be inferred from the response payload.\nYou can think of the schema as a service that represents the same\ninformation, more or less, as all of your `DS.Model` files.\n\n### What is a schema\n\nWhen modeling a payload it is necessary to know what properties of the payload\nare attributes that should be accessible to the application \u0026 templates, what\nproperties are relationships that should look up other models, and what\nproperties should be ignored.\n\n`DS.Model` achieves this by enumerating the attributes and relationships on a\n`DS.Model` subclass inside your `app/models` directory.\n\nBy contrast ember-m3 relies on application-wide conventions to know the\ndifference between attributes and relationships and otherwise reports all\nproperties returned by the API as accessible to the application.\n\nFor APIs with many models, the ember-m3 approach can produce a substantially\nsmaller application. Similarly the approach uses fewer classes which reduces\nthe runtime cost of relationships.\n\n### API\n\n`Schema` is a service registered from `app/services/m3-schema.js`. For convenience,\nyou can extend a default schema from `ember-m3/services/schema`. The `schema` should\nhave following properties.\n\n- `includesModel(modelName)` Whether or not ember-m3 should handle this\n  `modelName`. It's fine to just `return true` here but this hook allows\n  `ember-m3` to work alongside `DS.Model`.\n\n- `computeAttribute(key, value, modelName, schemaInterface)` A function that computes\n  the value and type of an attribute.\n\nAn attribute can be:\n\n1.  A reference to a record that exists in the identity map\n2.  A nested m3 record that exists as a child of the m3 parent record\n3.  A simple value, like a POJO or a string\n4.  A managedArray of references, nested records or simple values\n\nIf the attribute is a reference return:\n`schemaInterface.reference({ id, type })`\nwhere the object properties are\n`id` The id of the referenced model (either m3 or `@ember-data/model`)\n`type` The type of the referenced model (either m3 or `@ember-data/model`)\n`null` is also a valid type in which case `id` will be looked up in a global cache.\n\nNote that attribute references are all treated as synchronous.\nThere is no ember-m3 analogue to `@ember-data/model` async relationships.\n\nIf you are returning a nested m3 model, return:\n`schemaInterface.nested({ id, type, attributes })`\n\nIf you are returning a managed array, return:\n`schemaInterface.managedArray([schemaInterface.nested(obj), someOtherValue])`\n\nIf you are returning the value you can return the raw value without passing it\nthrough the schemaInterface call\n\nFor example, if we have a book object:\n\n    ```json\n    {\n      id: 'book-id:1',\n      type: 'com.example.library.book',\n      mostSimiliarBook: 'book-id:2'\n      bestChapter: {\n        number: 7,\n        title: 'My chapter'\n        characterPOV: 'urn:character:2'\n      }\n    }\n    ```\n    We would want  `model.get('mostSimilarBook')` to return the book object and the\n\n`model.get('bestChapter.characterPOV')` to return the character model with id `2`\nas an object. This requires us to interpret `mostSimiliarBook` as a reference\nand `bestChapter` a nested m3 model and not a simple object.\n\n    We would write the following method.\n\n\n    ```js\n    computeAttribute(key, value, modelName, schemaInterface) {\n      if (key === 'mostSimilarBook') {\n        return schemaInterface.reference({\n          type: 'com.example.library.book',\n          id: value\n        })\n      } else if (key === 'bestChapter') {\n        return schemaInterface.nested({\n          type: 'chapter',\n          attributes: value\n        })\n      }\n    }\n    ```\n\n- `setAttribute(modelName, attrName, value, schemaInterface)` A function that can be used\n  to update the record-data with raw value instead of resolved value.\n  `schemaInterface.setAttr(key,value)` should be invoked inside the function to set\n  the value. If this function is not provided, m3 will set value as is.\n\n  Example:\n\n  ```js\n  setAttribute(modelName, attrName, value, schemaInterface) {\n    // Check if the value is resolved as model\n    // update attribute record-data with id information.\n    if (value \u0026\u0026 value.constructor \u0026\u0026 value.constructor.isModel) {\n      schemaInterface.setAttr(attrName, value.get('id'));\n    }\n  }\n  ```\n\n- `isAttributeResolved(modelName, attrName, value, schemaInterface)` A function\n  that determines whether a value that is being set should be treated as\n  resolved or not. Unresolved values that are set will be resoled when they\n  are next accessed -- resolved values are cached upon being set.\n\n  Example:\n\n  ```js\n  isAttributeResolved(modelName, attrName, value, schemaInterface) {\n    if (Array.isArray(value)) {\n      // treat all arrays as unresolved without examining their contents\n      return false;\n    } else {\n      return super.isAttributeResolved(...arguments);\n    }\n  }\n  ```\n\n- `computeAttributes(keys, modelName)` Compute the actual attribute names, default just return the\n  array passed in.\n  This is useful if you need to \"decode/encode\" your attribute names in a certain form, e.g.,\n  add a prefix when serializing.\n\n- `models` an object containing type-specific information that cannot be\n  inferred from the payload. The `models` property has the form:\n\n  ```js\n  {\n    models: {\n      myModelName: {\n        attributes: [],\n        defaults: {\n          attributeName: 'defaultValue',\n        },\n        aliases: {\n          aliasName: 'attributeName',\n        }\n        transforms: {\n          attributeName: transformFunctionn /* value */\n        }\n      }\n    }\n  }\n  ```\n\n  The keys to `models` are the types of your models, as they exist in your\n  normalized payload.\n\n  - `attributes` A list of whitelisted attributes. It is recommended to omit\n    this unless you explicitly want to prevent unknown properties returned in\n    the API payload from being read. If present, it is an array of strings that\n    list whitelisted attributes. Reads of non-whitelisted properties will\n    return `undefined`.\n\n  - `defaults` An object whose key-value pairs map attribute names to default\n    values. Reads of properties not included in the API will return the default\n    value instead, if it is specified in the schema.\n\n  - `aliases` Alternate names for payload attributes. Aliases are read-only, ie\n    equivalent to `Ember.computed.reads` and not `Ember.computed.alias`\n\n  - `transforms` An object whose key-value pairs map attribute names to\n    functions that transform their values. This is useful to handle attributes\n    that should be treated as `Date`s instead of strings, for instance.\n\n    ```js\n    function dateTransform(value) {\n      if (!value) { return; }\n      return new Date(Date.parse());\n    }\n\n    {\n      models: {\n        'com.example.bookstore.book': {\n          transforms: {\n            publishDate: dateTransform,\n          }\n        }\n      }\n    }\n    ```\n\n- `useUnderlyingErrorsValue(modelName)` Helps the `model.js` determine whether the `errors` attribute\n  should be read from the underlying data payload. The default return is false which creates an object compatible with\n  how Ember Data treats `errors` property. Return true to read from the data payload for the model.\n\n- `useNativeProperties(modelName)` If `true` is returned, removes the need to use `.set` and `.get` on m3 record of a given type.\nInstead of `model.get('someAttribute')` and `model.set('someAttribute)`, you can do `model.someAttribute` and \n`model.someAttribute = value`. When set to `false` deprecates your current `.` access to aid in the migration. For migration\nand deprecation guide see the [deprecations guide](DEPRECATIONS.md).\n\n## Serializer / Adapter\n\nember-m3 will use the `-ember-m3` adapter to make queries via `findRecord`,\n`queryRecord`, `queryURL` \u0026c. Responses will be normalized via the `-ember-m3`\nserializer.\n\nember-m3 provides neither an adapter nor a serializer. If your app does not\ndefine an `-ember-m3` adapter, the normal lookup rules are followed and your\n`application` adapter is used instead\n\nIt is perfectly fine to use your `application` adapter and serializer. However,\nif you have an app that uses both m3 models as well as `DS.Model`s you may\nwant to have different request headers, serialization or normalization for\nyour m3 models. The `-ember-m3` adapter and serializer are the appropriate\nplaces for this.\n\n## Debugging\n\nTo learn how to debug `m3` records, refer to the [debugging documentation](DEBUGGING.md)\n\n## Deprecations\n\nFor help with migrating deprecations refer to the [deprecations guide](DEPRECATIONS.md)\n\n## Customizing Store\n\nIf your app customizes the store service, it will need to import and extend the store service provided by `ember-m3` instead of the store provided by `@ember-data/store`. Example:\n\n```js\nimport M3Store from 'ember-m3/services/store';\n\nexport default class AppStore extends M3Store {}\n```\n\n## Alternative Patterns\n\nIf you are converting an application that uses `DS.Model`s (perhaps because it\nhas a very large number of them and ember-m3 can help with performance) you may\nhave some patterns in your model classes beyond schema specification.\n\nThere are no particular requirements around refactoring these except that when\nyou only have a single class for your models you won't be able to use typical\nobject-oriented patterns.\n\nThe following are simply recommendations for common patterns.\n\n### Constants\n\nUse the schema `defaults` feature to replace constant values in your `DS.Model`\nclasses. For example:\n\n```js\n// app/models/my-model.js\nimport Model from '@ember-data/model';\n\nexport Model.extend({\n  myConstant: 24601,\n});\n\n// convert to\n\n// app/initializers/schema-initializer.js\n{\n  models: {\n    'my-model': {\n      defaults: {\n        myConstant: 24601,\n      }\n    }\n  }\n}\n```\n\n### Ember.computed.reads\n\nUse the schema `aliases` feature to replace use of `Ember.computed.reads`. You\ncan likely do this also to replace the use of `Ember.computed.alias` as quite\noften they can be read only.\n\n```js\n// app/models/my-model.js\nimport Model, { attr } from '@ember-data/model';\n\nexport Model.extend({\n  name: attr(),\n  aliasName: Ember.computed.reads('name'),\n});\n\n// convert to\n\n// app/initializers/schema-initializer.js\n{\n  models: {\n    'my-model': {\n      aliases: {\n        aliasName: 'name',\n      }\n    }\n  }\n}\n```\n\n### Random UI State or other non-attr non-relationship properties\n\nLet's say you are converting the following `Museum` model:\n\n```js\n// models/museum.js\nimport Model, { attr } from '@ember-data/model';\n\nexport Model.extend({\n  name: attr(),\n})\n```\n\nAnd that for `Bad Reasons™` you discover that your team has been stashing a custom\nobject on the museum describing some ad-hoc state (maybe for the ui?):\n\n```js\nEmber.set(museum, 'retrofit', retrofitState);\n```\n\nLet's say this state has a formal class:\n\n```js\nconst RetrofitState = Ember.Object.extend({\n  statusText: Ember.computed('statusCode', function () {\n    let code = this.get('statusCode');\n\n    switch (code) {\n      case 0:\n        return 'Not started';\n      case 1:\n        return 'In Progress';\n      case 2:\n        return 'Incomplete, on hold';\n      case 3:\n        return 'Completed';\n      default:\n        return 'Unknown';\n    }\n  }),\n});\n```\n\nWhile you should not store local-state/ui-state (e.g. any state not part of the schema) on records, you can make this\npattern temporarily work with `M3` by doing a `Bad Thing™` and giving the class constructor a static `isModel` flag:\n\n```js\nRetrofitState.isModel = true; // THIS COMES WITH CONSEQUENCES\n```\n\nThis is not without consequences. Setting this flag makes `M3` treat this object as a `resolvedValue`, meaning that **it\nwill be included as an attribute when snapshot.eachAttribute is called by a serializer**. This is very likely not what\nyou want and very likely will cause \"spooky action at a distance\" bugs for others on your team (like suddenly sending\nserialized information about retrofits to the API).\n\nBefore saving these records, you would need to carefully scrub it by deleting this and any other local properties off of\nit, or you would need to ensure that the serializer did not serialize this attribute. This will be tedious, annoying and\nbrittle, but that is the sacrifice paid for such `Bad Things™`.\n\nUltimately, you should refactor your application away from this `Bad Practice™` to pass these separate objects alongside\neach other, for instance by wrapping them in an hash like the following:\n\n```js\nlet museumRetrofit = {\n  museum,\n  retrofit,\n};\n```\n\n### Other Computed Properties\n\nMore involved computed properites can be converted to either utility functions\n(if used within JavaScript) or helper functions (if used in templates).\n\nFor properties used in both templates and elsewhere (eg components) a convenient\npattern is to define a helper that exports both.\n\n```js\n// app/models/my-model.js\nimport Model, { attr } from '@ember-data/model';\n\nexport Model.extend({\n  name: attr('string'),\n  sillyName: Ember.computed('name', function() {\n    return `silly ${this.get('name')}`;\n  }).readOnly(),\n});\n```\n\n```hbs\n{{! some-template.hbs }}\n{{model.sillyName}}\n{{my-component name=model.sillyName}}\n```\n\n```js\n// app/routes/index.js\nlet sn = model.get('sillyName');\n```\n\nCoverted to\n\n```js\n// app/helpers/silly-name.js\nexport function getSillyName(model) {\n  if (!model) {\n    return;\n  }\n  return `silly ${model.get('name')}`;\n}\n\nfunction sillyNameHelper(positionalArgs) {\n  if (positionalArgs.length \u003c 1) {\n    return;\n  }\n\n  return getSillyName(positionalArgs[0]);\n}\n\nexport default Ember.Helper.helper(sillyNameHelper);\n```\n\n```hbs\n{{! some-template.hbs }}\n{{silly-name model}}\n{{my-component name=(silly-name model)}}\n```\n\n```js\n// app/routes/index.js\nimport { getSillyName } from '../helpers/silly-name';\n\n// ...\nlet sn = getSillyName(model);\n```\n\n### Saving\n\nember-m3 does not impose any particular requirements with saving models. If\nyour endpoints cannot reliably be determined via `snapshot.modelName` it is\nrecommended to add support for `adapterOptions.url` in your adapter. For\nexample:\n\n```js\n// app/adapters/-ember-m3.js\nimport ApplicationAdapter from './application';\nexport default ApplicationAdapter.extend({\n  findRecord(store, type, id, snapshot) {\n    let adapterOptions = snapshot.adapterOptions || {};\n    let url = adapterOptions.url;\n    if (!url) {\n      url = this.buildURL(snapshot.modelName, id, snapshot, 'findRecord');\n    }\n\n    return this.ajax(url, 'GET');\n  },\n\n  // \u0026c.\n});\n\n// somewhere else, perhaps in a route\nthis.store.findRecord('com.example.bookstore.book', 1, { url: '/book/from/surprising/endpoint' });\n```\n\n## Requirements\n\n* Ember \u003e= 3.16.x \u003c 4.x\n* Ember Data \u003e= 3.16.x \u003c 4.x\n* Node \u003e= 14.x (i.e. an [active version](https://nodejs.org/en/about/releases/))\n\n## Utilizing less of EmberData\n\nember-m3 does not require all of EmberData to function properly, and if your app does not need\nall of EmberData either then you can choose to use ember-m3 with only the subset of EmberData\npackages ember-m3 currently requires.\n\nAs of 13 May 2020 this means:\n\n- @ember-data/store \u003e= 3.16\n- @ember-data/model \u003e= 3.16\n- ember-inflector \u003e= 3.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fember-m3%2Fember-m3","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fember-m3%2Fember-m3","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fember-m3%2Fember-m3/lists"}