Skip to content

Migrating 4.x to 5.x

This guide will likely work for apps on 3.28 that have resolved EmberData deprecations from the 3.x series.

This guide is primarily intended for apps that got "stuck" on either 4.6 (due to ModelFragments) or 4.12 (typically due to the ArrayLike deprecation)

Note - it is not actually a requirement of 5.x to replace Models with Schemas (nor to replace adapters/serializers with requests). These things are deprecated in 5.x, but they still work.

The reason to take the approach outlined in this guide is because we have used capabilities provided by the @warp-drive/legacy package and by LegacyMode schemas together with Extensions to mimic much of the removed API surface to allow apps to bridge the gap to 5.x more easily.

Pre-Migration (update to Native Types)

If you use Typescript, before migrating, you should update your types to use the native types provided by both ember-source and WarpDrive.

You can do this even if you are on an older version (pre-5.x) that didn't ship it's own types by using the "types" packages we specially publish for this purpose.

Step 1 - delete all the ember/ember-data DT type packages

package.json
{
  "dependencies": { 
    "@types/ember": "4.0.11",
    "@types/ember-data": "4.4.16",
    "@types/ember-data__adapter": "4.0.6",
    "@types/ember-data__model": "4.0.5",
    "@types/ember-data__serializer": "4.0.6",
    "@types/ember-data__store": "4.0.7",
    "@types/ember__application": "4.0.11",
    "@types/ember__array": "4.0.10",
    "@types/ember__component": "4.0.22",
    "@types/ember__controller": "4.0.12",
    "@types/ember__debug": "4.0.8",
    "@types/ember__destroyable": "4.0.5",
    "@types/ember__engine": "4.0.11",
    "@types/ember__error": "4.0.6",
    "@types/ember__helper": "4.0.7",
    "@types/ember__modifier": "4.0.9",
    "@types/ember__object": "4.0.12",
    "@types/ember__owner": "4.0.9",
    "@types/ember__routing": "4.0.22",
    "@types/ember__runloop": "4.0.10",
    "@types/ember__service": "4.0.9",
    "@types/ember__string": "3.16.3",
    "@types/ember__template": "4.0.7",
    "@types/ember__test": "4.0.6",
    "@types/ember__utils": "4.0.7",
  }
}

Step 2 - install the official packages using the latest versions.

Each package that we publish has a corresponding types-only package that you can use to gain access to official types while still using an older version of the library that doesn't have its own types yet.

PackageTypes Package
ember-dataember-data-types
@ember-data/*@ember-data-types/*
@warp-drive/*@warp-drive-types/*

💡 Why are there non-types packages below?

Starting in 5.7, due to package unification these types also require the installation of the new "package unification" packages since the actual source code (and types) originates from there.

sh
pnpm add -E ember-data-types@canary \
  @ember-data-types/adapter@canary \
  @ember-data-types/graph@canary \
  @ember-data-types/json-api@canary \
  @ember-data-types/legacy-compat@canary \
  @ember-data-types/model@canary \
  @ember-data-types/request@canary \
  @ember-data-types/request-utils@canary \
  @ember-data-types/serializer@canary \
  @ember-data-types/store@canary \
  @warp-drive-types/core-types@canary \
  @warp-drive/core@canary \
  @warp-drive/json-api@canary \
  @warp-drive/legacy@canary \
  @warp-drive/utilities@canary
sh
npm add -E ember-data-types@canary \
  @ember-data-types/adapter@canary \
  @ember-data-types/graph@canary \
  @ember-data-types/json-api@canary \
  @ember-data-types/legacy-compat@canary \
  @ember-data-types/model@canary \
  @ember-data-types/request@canary \
  @ember-data-types/request-utils@canary \
  @ember-data-types/serializer@canary \
  @ember-data-types/store@canary \
  @warp-drive-types/core-types@canary
  @warp-drive/core@canary \
  @warp-drive/json-api@canary \
  @warp-drive/legacy@canary \
  @warp-drive/utilities@canary
sh
yarn add -E ember-data-types@canary \
  @ember-data-types/adapter@canary \
  @ember-data-types/graph@canary \
  @ember-data-types/json-api@canary \
  @ember-data-types/legacy-compat@canary \
  @ember-data-types/model@canary \
  @ember-data-types/request@canary \
  @ember-data-types/request-utils@canary \
  @ember-data-types/serializer@canary \
  @ember-data-types/store@canary \
  @warp-drive-types/core-types@canary
  @warp-drive/core@canary \
  @warp-drive/json-api@canary \
  @warp-drive/legacy@canary \
  @warp-drive/utilities@canary
sh
bun add --exact ember-data-types@canary \
  @ember-data-types/adapter@canary \
  @ember-data-types/graph@canary \
  @ember-data-types/json-api@canary \
  @ember-data-types/legacy-compat@canary \
  @ember-data-types/model@canary \
  @ember-data-types/request@canary \
  @ember-data-types/request-utils@canary \
  @ember-data-types/serializer@canary \
  @ember-data-types/store@canary \
  @warp-drive-types/core-types@canary
  @warp-drive/core@canary \
  @warp-drive/json-api@canary \
  @warp-drive/legacy@canary \
  @warp-drive/utilities@canary

This will install the following at the latest canary

package.json
{
  "dependencies": { 
    "ember-data-types": "alpha",
    "@ember-data-types/adapter": "alpha",
    "@ember-data-types/graph": "alpha",
    "@ember-data-types/json-api": "alpha",
    "@ember-data-types/legacy-compat": "alpha",
    "@ember-data-types/model": "alpha",
    "@ember-data-types/request-utils": "alpha",
    "@ember-data-types/request": "alpha",
    "@ember-data-types/serializer": "alpha",
    "@ember-data-types/store": "alpha",
    "@warp-drive-types/core-types": "alpha",
    "@warp-drive/core": "alpha",
    "@warp-drive/json-api": "alpha"
    "@warp-drive/legacy": "alpha",
    "@warp-drive/utilities": "alpha",
  }
}

Step 3 - configure tsconfig.json

diff
 {
   "compilerOptions": {
     "types": [
        "ember-source/types",
        "ember-data-types/unstable-preview-types",
        "@ember-data-types/store/unstable-preview-types",
        "@ember-data-types/adapter/unstable-preview-types",
        "@ember-data-types/graph/unstable-preview-types",
        "@ember-data-types/json-api/unstable-preview-types",
        "@ember-data-types/legacy-compat/unstable-preview-types",
        "@ember-data-types/request/unstable-preview-types",
        "@ember-data-types/request-utils/unstable-preview-types",
        "@ember-data-types/model/unstable-preview-types",
        "@ember-data-types/serializer/unstable-preview-types",
        "@warp-drive-types/core-types/unstable-preview-types"
      ]
    }
 }

Step 4 - brand your models

ts
import Model from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';

export default class User extends Model {
  declare [Type]: 'user';
}

Step 5 - replace registry usage with branded model usages

ts
// find
store.findRecord('user', '1'); 
store.findRecord<User>('user', '1');  

store.findAll('user'); 
store.findAll<User>('user');  

store.query('user', {}); 
store.query<User>('user');  

store.queryRecord('user', {}); 
store.queryRecord<User>('user');  

// peek
store.peekRecord('user', '1'); 
store.peekRecord<User>('user', '1');  

// push
const user = store.push({ 
const user = store.push<User>({ 
  data: {
    type: 'user',
    id: '1',
    attributes: { name: 'Chris' }
  }
}) as User;  
});  

Additional Resources

Step 6 - fix other type issues that arise

ArrayLike API usage is likely to give you the most issues here, if anything does.

Migration

Step 1 - Install the Mirror Packages

sh
pnpm add -E @warp-drive-mirror/core@canary @warp-drive-mirror/json-api@canary @warp-drive-mirror/ember@canary @warp-drive-mirror/legacy@canary @warp-drive-mirror/utilities@canary
sh
npm add -E @warp-drive-mirror/core@canary @warp-drive-mirror/json-api@canary @warp-drive-mirror/ember@canary @warp-drive-mirror/legacy@canary @warp-drive-mirror/utilities@canary
sh
yarn add -E @warp-drive-mirror/core@canary @warp-drive-mirror/json-api@canary @warp-drive-mirror/ember@canary @warp-drive-mirror/legacy@canary @warp-drive-mirror/utilities@canary
sh
bun add --exact @warp-drive-mirror/core@canary @warp-drive-mirror/json-api@canary @warp-drive-mirror/ember@canary @warp-drive-mirror/legacy@canary @warp-drive-mirror/utilities@canary

This will install the following at the latest canary

package.json
{
  "dependencies": {
    "@warp-drive-mirror/core": "alpha",
    "@warp-drive-mirror/ember": "alpha",
    "@warp-drive-mirror/json-api": "alpha"
    "@warp-drive-mirror/legacy": "alpha",
    "@warp-drive-mirror/utilities": "alpha",
  }
}

Step 2 - Configure The Build

ember-cli-build.js
ts
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { compatBuild } = require('@embroider/compat');

module.exports = async function (defaults) {
  const { setConfig } = await import('@warp-drive-mirror/core/build-config'); 
  const { buildOnce } = await import('@embroider/vite');
  const app = new EmberApp(defaults, {});

  setConfig(app, __dirname, { 
    // this should be the most recent <major>.<minor> version for
    // which all deprecations have been fully resolved
    // and should be updated when that changes
    compatWith: '4.12',
    deprecations: {
      // ... list individual deprecations that have been resolved here
    }
  });

  return compatBuild(app, buildOnce);
};

Step 3 - Configure Reactivity

Next we configure WarpDrive to use Ember's signals implementation. Add this near the top of your app/app.ts file. If you already have import '@warp-drive/ember/install'; leave that too, you'll need both. Their order does not matter.

app/app.ts
ts
import '@warp-drive-mirror/ember/install';

Step 4 - Configure the Store

We use useLegacyStore to create a store service preconfigured with maximal support for legacy APIs.

app/services/v2-store.ts
ts
import { useLegacyStore } from '@warp-drive/legacy';
import { JSONAPICache } from '@warp-drive/json-api';

const Store = useLegacyStore({
  legacyRequests: true,
  cache: JSONAPICache,
  schemas: [
     // -- your schemas here for
     // anything migrated off of Model
  ],
  handlers: [
    // -- your additional handlers here
    // Fetch, LegacyNetworkHandler, and CacheHandler
    // are automatically provided when needed
  ]
});
type Store = InstanceType<typeof Store>;

export default Store;

Additional Reading (for when you have questions later)

Step 5 - Convert + Profit

Key concepts:


Migrating away from Model involves decomposing the various responsibilities it may have taken on in your codebase into the correct corresponding primitive.

Below is a complete example of migrating a Model with a Mixin. After showing the full breakdown, we'll walk through decomposing the Model and Mixin files in discrete steps in order to teach you about each part of the change.

We don't expect you to do this migration manually, but instead to use the provided codemod.

app/models/user.ts
ts
import Model, { attr, belongsTo, hasMany, type AsyncHasMany } from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';
import { cached } from '@glimmer/tracking';
import { computed } from '@ember/object';
import Timestamped from '../mixins/timestamped';

export default class User extends Model.extend(Timestamped) {
  declare [Type]: 'user';

  @attr declare firstName: string;
  @attr declare lastName: string;

  @belongsTo('user', { async: false, inverse: null })
  declare bestFriend: User | null;

  @hasMany('user', { async: true, inverse: null })
  declare friends: AsyncHasMany<User>;

  @cached
  get fullName(): string {
    return this.firstName + ' ' + this.lastName;
  }

  @computed('firstName')
  get greeting(): string {
    return 'Hello ' + this.firstName + '!';
  }

  sayHi(): void {
    alert(this.greeting);
  }
}
app/mixins/timestamped.ts
ts
import Mixin from '@ember/object/mixin';
import { attr } from '@ember-data/model';

export default Mixin.create({
  createdAt: attr(),
  deletedAt: attr(),
  updatedAt: attr(),

  async softDelete(): Promise<void> {
    const result = await fetch(`/api/${this.constructor.modelName}/${this.id}`, { method: 'DELETE' });
    const newTimestamps = await result.json();
    this.store.push({
      data: {
        type: this.constructor.modelName,
        id: this.id,
        attributes: newTimestamps
      }
    });
  }
});
  1. The file-path based convention for defining the ResourceType is replaced with specifying a ResourceType on a ResourceSchema. File paths are now purely organizational and discretionary.
app/data/user/schema.ts
ts
// The ResourceSchema in this example is intentionally
// incomplete, we will fill it out below
//
const UserSchema = {
  type: 'user',
}
  1. We differentiate between schemas for embedded objects (which have no identity of their own) and schemas for resources (which do have their own identity) by specifying a primaryKey. On Model this was id (this is also added by withDefaults which we'll see next).
app/data/user/schema.ts
ts
// The ResourceSchema in this example is intentionally
// incomplete, we will fill it out below
//
const UserSchema = {
  type: 'user',
  identity: { kind: '@id', name: 'id' } 
}
  1. We put the ResourceSchema in "LegacyMode" so that our ReactiveResources will behave in the same mutable manner as Model did.
app/data/user/schema.ts
ts
// The ResourceSchema in this example is intentionally
// incomplete, we will fill it out below
//
const UserSchema = {
  type: 'user',
  identity: { kind: '@id', name: 'id' },
  legacy: true,
}
  1. We decorate our ResourceSchema with the default behaviors that Models had (fields like isNew hasDirtyAttributes or currentState as well as methods like rollbackAttributes and save).

This also automatically sets us up in LegacyMode and adds the id field for identity, in effect this replaces class User extends Model.

app/data/user/schema.ts
ts
import { withDefaults } from '@warp-drive-mirror/legacy/model/migration-support';
// The ResourceSchema in this example is intentionally
// incomplete, we will fill it out below
//
const UserSchema = withDefaults({ 
  type: 'user',
  identity: { kind: '@id', name: 'id' }, 
  legacy: true,
}) 
  1. Schema properties from the Model (hasMany belongsTo and attr) become fields on the matching ResourceSchema
app/data/user/schema.ts
ts
import { withDefaults } from '@warp-drive-mirror/legacy/model/migration-support';
// The ResourceSchema in this example is intentionally
// incomplete, we will fill it out below
//
const UserSchema = withDefaults({
  type: 'user',
  fields: [ 
    { kind: 'attribute', name: 'firstName' },
    { kind: 'attribute', name: 'lastName' },
    { 
      kind: 'belongsTo',
      name: 'bestFriend',
      type: 'user',
      options: { async: false, inverse: null }
    },
    {
      kind: 'hasMany',
      name: 'friends',
      type: 'user',
      options: { async: true, inverse: null }
    },
  ],
})
  1. Mixins get converted to traits.

Because Model functioned as a type, a ResourceSchema, a reactive object, and

We migrate models with ResourceSchemas and extensions.

app/models/user.ts
ts
import Model, { attr, belongsTo, hasMany, type AsyncHasMany } from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';
import { cached } from '@glimmer/tracking';
import { computed } from '@ember/object';

export default class User extends Model {
  declare [Type]: 'user';

  @attr firstName;
  @attr lastName;

  @belongsTo('user', { async: false, inverse: null })
  declare bestFriend: User | null;

  @hasMany('user', { async: true, inverse: null })
  declare friends: AsyncHasMany<User>;

  @cached
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }

  @computed('firstName')
  get greeting() {
    return 'Hello ' + this.firstName + '!';
  }

  sayHi() {
    alert(this.greeting);
  }
}

A Model with Mixins

We can migrate mixins with traits and extensions.

ts
import Model, { attr } from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';
import Timestamped from '../mixins/timestamped';

export default class User extends Model.extend(Timestamped) {
  declare [Type]: 'user';

  @attr firstName;
  @attr lastName;
}
ts
import Mixin from '@ember/object/mixin';
import { attr } from '@ember-data/model';

export default Mixin.create({
  createdAt: attr(),
  deletedAt: attr(),
  updatedAt: attr(),

  async softDelete() {
    const result = await fetch(`/api/${this.constructor.modelName}/${this.id}`, { method: 'DELETE' });
    const newTimestamps = await result.json();
    this.store.push({
      data: {
        type: this.constructor.modelName,
        id: this.id,
        attributes: newTimestamps
      }
    });
  }
});

A Model with Fragments

ts
TBD

Post Migration

  • drop the old packages
  • drop config for the old packages
  • delete the store service
  • rename v2-store => store
  • rename packages and imports from @warp-drive-mirror to @warp-drive

Released under the MIT License.