Skip to main content

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.ymlconfigs 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.tsand 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.