Skip to content

Latest commit

 

History

History
148 lines (103 loc) · 7.46 KB

incremental-adoption-guide.md

File metadata and controls

148 lines (103 loc) · 7.46 KB

Incremental adoption guide for existing projects

This guide is for existing projects that want to adopt the new APIs of the EmberData incrementally.

Step 1: Upgrade to EmberData 4.12.x

This version of EmberData is the first version that supports the new APIs. It is also a LTS version, so you can stay on it for a while. You can refer the EmberData Compatibility table to see which version of EmberData is compatible with your Ember version.

Step 2: Add Store service to your application

You will need to create your own store service. Before, a store service was automatically injected by EmberData. Here is how you do it:

// eslint-disable-next-line ember/use-ember-data-rfc-395-imports
import Store from 'ember-data/store';
import { service } from '@ember/service';

export default class MyStore extends Store {
  @service requestManager;
}

Notice we still want to import the Store class from ember-data/store package. You might have a lint rule that says don't do it. You can disable it for this import. The reason we want to import it from ember-data/store is because we want to use EmberData models, serializers, adapters, etc. while alongside we want to start utilizing new APIs.

Note: You can also use @ember-data/store package, but you will need to configure a lot more to make things work to use old APIs. We recommend using ember-data/store package to avoid confusion.

Note: Because we are extending ember-data/store, it is still v1 addon, so things might not work for you if you are using typescript. We recommend to have store.js file for now.

Step 3: Add RequestManager service to your application

Now let's create our very own RequestManager service. It is a service that is responsible for sending requests to the server. It is a composable class, which means you can add your own request handlers to it.

First you need to install @ember-data/request and @ember-data/legacy-compat packages. First contains the RequestManager service and a few request handlers, second has LegacyNetworkHandler that gonna handle all old-style this.store.* calls.

Here is how your own RequestManager service may look like:

import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
import type { Handler, NextFn, RequestContext } from '@ember-data/request';
import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

/* eslint-disable no-console */
const TestHandler: Handler = {
  async request<T>(context: RequestContext, next: NextFn<T>) {
    console.log('TestHandler.request', context.request);
    const result = await next(Object.assign({}, context.request));
    console.log('TestHandler.response after fetch', result.response);
    return result;
  },
};

export default class Requests extends RequestManager {
  constructor(args?: Record<string | symbol, unknown>) {
    super(args);
    this.use([LegacyNetworkHandler, TestHandler, Fetch]);
  }
}

Let's go over the code above:

  1. LegacyNetworkHandler is the handler that is responsible for sending requests to the server using the old APIs. It will interrupt handlers chain if it detects request using old APIs. It will process it as it used to be doing with Adapters/Fetch/Serializers workflow.

  2. Next is TestHandler. It is a handler that is responsible for logging requests. It is a quick example of how you can add your own handlers to the request manager. We will take a look at more useful examples later.

  3. Lastly Fetch. It is a handler that sends requests to the server using the fetch API. It expects responses to be JSON and when in use it should be the last handler you put in the chain. After finishing each request it will convert the response into json and pass it back to the handlers chain in reverse order as the request context's response. So TestHandler will receive response property first, and so on if we would have any.

NOTE: Your RequestManager service should be exactly app/services/request-manager.[js|ts] file. It is a convention that Ember uses to find the service.

You can read more about request manager in the request manager guide.

Step 4: Install @ember-data/json-api, @ember-data/request-utils packages

If you were using JSON:API adapter/serializer for your backend communication, you can use @ember-data/json-api package. It is a package that contains predefined builders for JSON:API requests. You can read more about it in the @ember-data/json-api.

If you have different backend format - EmberData provides you with builders for REST(@ember-data/rest) and ActiveRecord(@ember-data/active-record).

@ember-data/request-utils package contains a lot of useful utilities for building requests. You can read more about it in its Readme. It has request builders for all type of requests.

Step 5: Off you go! Start using new APIs

Now you can start refactoring old code to use new APIs. You can start with the findAll method. It is the easiest one to refactor. Here is how you do it:

+ import { query } from '@ember-data/json-api/request';

  loadProjects: Task<void, []> = task(async () => {
-    const projects = await this.store.findAll('project');
-    this.projects = [...projects];
+    const { content } = await this.store.request(query('project', {}, { host: config.api.host }));
+    this.projects = content.data;
  });

You most likely would need to add Auth Handler to your request manager to add accessToken to your requests. Let's say you have your accessToken in the session service. Here is how you can add it to the request manager:

import { service } from '@ember/service';

export default class AuthHandler {
  @service session;

  request({ request }, next) {
    const headers = new Headers(request.headers);
    headers.append(
      'Authorization',
      `Bearer ${this.session.accessToken}`,
    );

    return next(Object.assign({}, request, { headers }));
  }
}

You can read more about auth topic here.

Another good thing to do is to configure default host and namespace for your requests. There is an utility for that out of the box of @ember-data/request-utils called setBuildURLConfig. You can do it anywhere in your app theoretically, but we recommend doing it in the app/app.js file. Here is how you can do it:

import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'base-ember-typescript-app/config/environment';
+import { setBuildURLConfig } from '@ember-data/request-utils';
+
+setBuildURLConfig({
+  host: 'https://api.example.com',
+  namespace: 'v1',
+});

export default class App extends Application {
  modulePrefix = config.modulePrefix;
  podModulePrefix = config.podModulePrefix;
  Resolver = Resolver;
}

loadInitializers(App, config.modulePrefix);