Setup Resource CRUD
This guide will walk you through on how to add CRUD operations against any new/existing resource in the app without much hassle. There are few simple steps you need to follow and in the end you'll have all the required GraphQL queries and mutations against that resource.
Define Schema
For each resource we have schema definitions in multiple formats to be used by respective lib,
GraphQL
GraphQL type needs to be defined in the GraphQL schema, to be used by GraphQL server and later on these will be used to generate typescript definitions using GraphQL codegen. You need to define both return types and input types
type User{}
type Users{
totalCount: Int!,
data: [User!]
}
input CreateInput{
}
input UpdateUserInput{
}
Typescript
Typescript definitions are auto generated by GraphQL schema, to be used by Typescript compiler. All you need to is to run
npn run generateGraphql
Note: We are assuming that you have already updated
codegen.yml
configs with your module
Mongoose Model
Mongoose model schema to be used by mongoose for mongo DB operations. Create a new Mongoose Model
file under store/models
, following the naming convention <resource-name>-model.ts
e.g. user-model.ts
import { Connection, Document, Model, Schema } from 'mongoose';
import { addIdVirtualFields, commonModeSchemaOptions } from '@common-stack/store-mongo';
import { IUser } from 'generated-modals';
export type IUserMongoModel = Document & IUser;
export const userScehma = new Schema<IUserMongoModel>(
{
//.. schema definition for model
},
commonModeSchemaOptions,
);
addIdVirtualFields(userScehma);
export type UserModelType = Model<IUserMongoModel>;
export const UserModelFunc: (db: Connection) => UserModelType = (db) =>
db.model<IUserMongoModel>('users', userScehma);
Define Repository
We are using repositories to communicate with db and perform all db operations. You repository should encapsulate all db related logic and how you are communicating with the db giving an abstraction to upper level services, so they don't have to worry about the which data source is being used.
Create a new Repository file under store/repositoires
, following the naming convention <resource-name>-repository.ts
e.g user-repository.ts
and
repository type definitions under interfaces/<resource-name>-service-interface.ts
e.g user-service-interface.ts
export type IUserRepository = IBaseRepository<IUser>
import { BaseRepository, IMongoOptions } from '@common-stack/store-mongo';
import { inject, injectable, optional } from 'inversify';
import { Connection } from 'mongoose';
import { logger as Logger } from '@cdm-logger/server/lib/logger';
import { UserModelFunc } from '../models';
@injectable()
export class UserRepository extends BaseRepository<IUser> implements IUserRepository {
constructor(
@inject('MongoDBConnection')
db: Connection,
@inject('Logger')
logger: typeof Logger,
@inject('IMongoOptions')
@optional()
options?: IMongoOptions,
) {
super(UserModelFunc, db, logger, options);
}
}
When you extend from the BaseRepository
it adds the following methods to your repository
export interface GetAllArgs<T> {
criteria?: FilterQuery<T>;
sort?: ISort;
skip?: number;
limit?: number;
selectedFields?: string;
}
export interface IBaseRepository<T, D = Document<T>> {
count(conditions?: FilterQuery<D>): Promise<number>;
getAll(options: GetAllArgs<D>): Promise<T[]>;
get(conditions?: FilterQuery<D>, selectedFields?: string): Promise<T>;
create<I>(data: I): Promise<T>;
upsert<I>(conditions: FilterQuery<D>, update: I, options: any): Promise<T>;
update<I>(criteria: FilterQuery<D>, update: UpdateQuery<D>, options?: any): Promise<T>;
bulkUpdate<I>(criteria: FilterQuery<D>, update: UpdateQuery<D>, options?: any): Promise<T[]>;
delete(criteria: FilterQuery<D>): Promise<boolean>;
bulkGet(ids: string[], selectedFields?: string): Promise<T[]>;
bulkCreate<I>(data: I[]): Promise<T[]>;
bulkDelete(criteria: FilterQuery<D>): Promise<number>;
}
You can then use these methods to perform CRUD on your resource.
Register with DI
Once you have your repository set up and in place, you need to register it with the Dependency Injection so it can injected where required
// containers/container.ts
export const localContainerModule: (settings) => interfaces.ContainerModule = () =>
new ContainerModule((bind: interfaces.Bind) => {
//...existing bindings
bind(TYPES.UserRepository).to(UserRepository).inSingletonScope().whenTargetIsDefault();
});
export const externalContainerModule: (settings) => interfaces.ContainerModule = () =>
new ContainerModule((bind: interfaces.Bind) => {
//...existing bindings
bind(TYPES.UserRepository).to(UserRepository).inSingletonScope().whenTargetIsDefault();
});
Note: This assumes that
TYPES
constant has already been updated with the new value.
Define Service
Services incorporate your business rules and domain logic, service utilize repository
to communicate with the data
store, a service can consume other service
if needed to fetch or make changes to other resources.
Create a new service file under services
, following the naming convention <resource-name>-service.ts
e.g user-service.ts
and type
definition for it under interfaces
// interfaces/user-service.interface.ts
export type IUseraService = BaseService<IUser, IUserInput, IUserUpdateInput>
// services/user-service.ts
import { inject, injectable } from 'inversify';
import { CommonType } from '@common-stack/core';
import { ServiceBroker } from 'moleculer';
import { logger as Logger } from '@cdm-logger/server/lib/logger';
@injectable()
export class UserService extends BaseService<IUser, IUserInput, IUserUpdateInput> implements IUserService {
constructor(
@inject(TYPES.UserRepository)
private readonly repository: IUserRepository,
) {
super(repository)
}
}
By extending the BaseService
the following methods gets added to the new Service
export interface IBaseService<T, C = Omit<T, 'id'>, U = C> {
count(conditions?: FilterQuery<Document<T>>): Promise<number>;
get(id: string): Promise<T>;
get(conditions?: string | FilterQuery<Document<T>>): Promise<T>;
getAll(options?: GetAllArgs<Document<T>>): Promise<T[]>;
getByIds(ids: string[]): Promise<T[]>;
create(data: C): Promise<T>;
insert(data: (C | U) & {
id?: string;
}, overwrite?: boolean): Promise<T>;
bulkCreate(data: C[]): Promise<T[]>;
update(id: string, data: U, overwrite?: boolean): Promise<T>;
delete(id: string): Promise<boolean>;
delete(conditions: string | FilterQuery<Document<T>>): Promise<boolean>;
getAllWithCount(options: GetAllArgs<Document<T>>): Promise<{
data: T[];
totalCount: number;
}>;
}
Register with the DI
Once you have your service file in place, next step is to register it too, with the DI so It can be injected on demand wherever required
// containers/container.ts
export const localContainerModule: (settings) => interfaces.ContainerModule = () =>
new ContainerModule((bind: interfaces.Bind) => {
//...existing bindings
bind(TYPES.UserService).to(UserService).inSingletonScope().whenTargetIsDefault();
});
export const externalContainerModule: (settings) => interfaces.ContainerModule = () =>
new ContainerModule((bind: interfaces.Bind) => {
//...existing bindings
bind(TYPES.UserService).to(UserService).inSingletonScope().whenTargetIsDefault();
});
Add Queries and Mutations
Update GraphQL schema to have the new types, queries and mutations, by convention your newly added queries and mutation names should look like this.
extend type Query{
# ... existing queries
user(id:ID!): UserModel!
users(id:ID!): UserPageModel!
}
extend type Mutation{
createUser(user:CreateUserInput!): UserModel!
updateUser(id:ID!, user: UpdateUserInput!): UserModel!
deleteUser(id:ID!): Boolean!
}
After adding to the GraphQL schema generate the new types and next step is to add the resolver for those operations.
export const userResolver = (): IResolvers<IContext> => ({
User:{
// # Nested resolvers for relations
async posts(user,_, args, { postService }){
const {data, totalCount} = await userService.getAllWithCount({
...args,
criteria:{
...args.criteria,
user: user.id,
}
});
return {
totalCount,
data,
};
}
},
Query: {
async users(_, args, { userService }) {
const {data, totalCount} = await userService.getAllWithCount(args);
return {
totalCount,
data,
};
},
user(_, { id }, { userService }) {
return userService.get(id);
},
},
Mutation: {
createUser(_, { user }, { userService }) {
return userService.create(user);
},
updateUser(_, { id, user }, { userService }) {
return userService.update(id, user, false);
},
deleteUser(_, { id }, { userService }) {
return userService.delete(id);
},
},
});
Note: First, you need to register your service in the GraphQL context. Add it under
createServiceFunc
of Feature API. If it does not already exist create one.const createServiceFunc = (container: interfaces.Container): IService => ({
userService: container.get<IUserService>(TYPES.UserService),
});
in main resolvers file, register your newly added resolver.
export const resolvers = [...exisitngResolvers, userResolver];
The last thing you need to do is to register the resolvers
to Feature API (If it's not already registered).
Once everything is done you will have the above defined GraphQL queries and mutation performing CRUD on the resource.