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.
const useModelController = defineController(Model);For example:
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.
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
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.
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.
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.
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
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
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:
- Setup object
- Composable instance
- Global configuration
- 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:
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.
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:
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:
const userController = useUserController();
userController.activate();