Skip to content

defineController()

The defineController() method is used to create a composable that serves as a controller for given model. The controller has, and provides, direct access to model's API and repository functions.

Create controller

Creating a model controller is similar to creating a Pinia store.

typescript
const useModelController = defineController(Model);

For example:

typescript
import { Model, defineController } from '@kovalson/prevue';

class User extends Model {
  // ...
}

const useUserController = defineController(User);

export {
  User,
  useUserController,
};

This is the most common usage of the Prevue package. If your project consists primarily of BREAD endpoints, defining only a controller for each model should suffice in most cases.

Setup

You may pass a setup object as a second parameter to configure the composable.

typescript
import { Model, SimpleResponse, defineController } from '@kovalson/prevue';

class User extends Model {
  public active: boolean = false;
  // ...
}

const useUserController = defineController(User, {
  api: {
    apiUrl: 'https://my.domain/api',
    uri: 'users',
    endpoints: {
      async active() {
        const response = await this.request('/active');
        return new SimpleResponse(response);
      },
    },
  },
  repository: {
    local: false,
    methods: {
      active() {
        return this
          .all()
          .filter((user: User) => user.active);
      },
    },
  },
  actions: {
    async customAction(): Promise<void> {
      const response = await this.api.active();
      const firstUser = response.getData()[0];
      this.repository.update(firstUser);
    },
  },
  reactions: {
    active(response: SimpleResponse<User[]>): User[] {
      const users = response.getData();
      users.forEach((user: User) => {
        this.repository.update(user);
      });
      return users;
    },
  },
});

export {
  User,
  useUserController,
};

Setup Object

typescript
interface ISetupObject {
  /**
   * The API setup object.
   * It's the same as for the `defineApi()` function.
   */
  api?: IApiSetup;

  /**
   * The repository setup object.
   * It's the saem as for the `defineRepository()` function.
   */
  repository?: IRepositorySetup;

  /**
   * Custom controller actions.
   */
  actions?: Record<string, TCustomAction>;
  
  /**
   * Custom endpoints reactions.
   * 
   * These are functions that get the custom API endpoint response and perform custom actions.
   * They serve as a short version for actions (see examples below).
   */
  reactions?: Record<string, TReaction>;
}

Actions

Actions are dev-defined functions for the controller. They are automatically bound a special context that provides the API, repository, controller actions and reactions methods.

Actions can do whatever you need them to do, but bear in mind that the controller should, by definition, control the flow of the resource in the application.

Reactions

Reactions are functions that get the custom API endpoint response and further process it. Reactions are automatically bound a special context that provides the API and repository as well as all other controller actions and reactions.

Reactions are meant to be used with custom API endpoints. Whenever you define a custom endpoint in your API composable, a controller can be defined a reaction to that endpoint so that the resource flow is properly maintained.

Examples

Setup

No Setup

Controller can be defined with no setup at all.

typescript
import { Model, defineController } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserController = defineController(User);

export {
  User,
  useUserController,
}

Controller automatically creates instances of API and Repository composables under the hood, but you don't need to pass them as parameters when there's no customization to them.

With API or Repository

You may pass API or Repository composable as a second parameter.

It might be useful to separate the API or Repository composable from the controller to get more direct access and avoid convoluted code.

typescript
import { Model, defineApi, defineController, defineRepository } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User);

const useUserRepository = defineRepository(User);

const useUserController = defineController(User, useUserApi);

// or const useUserController = defineController(user, useUserRepository);

export {
  User,
  useUserApi,
  useUserController,
  useUserRepository,
}

This way the controller still has access to the API (or Repository) composable, and the exported composable is exactly the same instance as inside the controller.

The other, non-passed composable is automatically created under the hood and can still be used.

With API and Repository

You may pass both API and Repository composables as parameters.

typescript
import { Model, defineApi, defineController, defineRepository } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User);

const useUserRepository = defineRepository(User);

const useUserController = defineController(User, useUserApi, useUserRepository);

export {
  User,
  useUserApi,
  useUserController,
  useUserRepository,
}

Reactions

typescript
import { Model, SimpleResponse, defineApi, defineController, defineRepository } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string): Promise<User> {
      const response = await this.request(`/${id}/activate`, {
        method: 'POST',
      });
      return new SimpleResponse(response);
    },
  },
});

const useUserController = defineController(User, useUserApi, {
  reactions: {
    async activate(response: SimpleResponse<User>): Promise<User> {
      this.repository.update(response.getData());
    },
  },
});

export {
  User,
  useUserApi,
  useUserController,
  useUserRepository,
}

Actions

typescript
import { Model, SimpleResponse, defineApi, defineController, defineRepository } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string): Promise<User> {
      const response = await this.request(`/${id}/activate`, {
        method: 'POST',
      });
      return new SimpleResponse(response);
    },
  },
});

const useUserController = defineController(User, useUserApi, {
  actions: {
    async activate(id: string): Promise<User> {
      const response = await this.api.activate(id);
      const user = response.getData();
      this.repository.update(user);
      return user;
    },
  },
});

export {
  User,
  useUserApi,
  useUserController,
  useUserRepository,
}

Actions VS Reactions

Actions are the base, generic approach. Reactions are an addon that may (or may not) simplify some cases of custom endpoints usage. The main difference between actions and reactions is that reactions only get the response from a single endpoint call. Actions, on the other hand, can perform multiple requests and react to them differently.

As you can see in the examples above, actions' signatures are basically the same as of their corresponding custom API endpoints'. With the cost of a bit of code duplication you get to freely decide how to manage the request. Meanwhile, reactions have always the same generic signature, but can only react to a single response and only when the request is fully done.

Precedence

The following is a prioritized list presenting which configuration takes precedence over the other when customizing API, Repository or Controller:

  1. Setup object
  2. Composable instance
  3. Global configuration
  4. Default values

Also, when wrapping composables one into another, the higher-level composable's setup takes precedence over the lower-level composable.

For example, if you define your model the following way:

typescript
import { Model, defineApi, defineController } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User, {
  apiUrl: 'http://localhost:8080',
});

const useUserRepository = defineRepository(User);

const useUserController = defineController(User, useUserApi, {
  api: {
    apiUrl: 'http://localhost:5173',
  },
});

export {
  User,
  useUserApi,
  useUserController,
  useUserRepository,
}

then when using the controller, the apiUrl will be set to http://localhost:5173.

WARNING

Also beware that in the example above when using the API directly, its apiUrl will be set to http://localhost:8080, so it's always better to keep single configuration or create a global one to avoid mistakes.

Default Actions

Controller Actions

Controller has several actions out-of-the-box. As a rule of thumb, each default API endpoint has its corresponding controller action built in.

typescript
interface IDefaultActions {
  fetchAll(options?: TApiOptions): Promise<M[]>
  fetchOne(id: TIdentifier, options?: TApiOptions): Promise<M>;
  fetchMany(ids: TIdentifier[], options?: TApiOptions): Promise<M[]>;
  createOne<Request extends Partial<M>>(data: Request, options?: TApiOptions): Promise<M>;
  updateOne(id: TIdentifier, data: Partial<M>, options?: TApiOptions): Promise<M>;
  updateMany(ids: TIdentifier[], data: Partial<M>, options?: TApiOptions): Promise<M[]>;
  deleteOne(id: TIdentifier, options?: TApiOptions): Promise<null>;
  deleteMany(ids: TIdentifier[], options?: TApiOptions): Promise<null>;
}

TIP

Note that the functions signatures are practically the same as those of the API composable, but the responses are not wrapped with ApiResponse.

Custom API Endpoints

Controller automatically implements and provides all custom API endpoints. If you define your controller as follows:

typescript
import { Model, SimpleResponse, defineApi, defineController, defineRepository } from '@kovalson/prevue';

class User extends Model {
  //
}

const useUserApi = defineApi(User, {
  endpoints: {
    async activate(id: string): Promise<User> {
      const response = await this.request(`/${id}/activate`, {
        method: 'POST',
      });
      return new SimpleResponse(response);
    },
  },
});

const useUserController = defineController(User, useUserApi);

then whenever you instantiate the controller, the custom endpoint will be available directly from the controller:

typescript
const userController = useUserController();

userController.activate();

Released under the MIT License.