Setup & configure NestJS 9 (TypeORM 0.3+) to use custom repositories - graphql

I am currently attempting to update my project to the latest version of NestJS and TypeORM and I am having a great deal of difficulty wrapping my head around how to setup the project so that I can use custom repositories in a similar way that it was before.
The biggest problem is that #EntityRepository and .getCustomRepository have been deprecated. I have been pouring over documentation and I can't seem to figure out the new way of doing things.
From the TypeORM docs, it says to do something like this:
export const UserRepository = dataSource.getRepository(User).extend({
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany()
},
})
ok... I get that... use the dataSource to create the repo and extend it with the additional functions. Seem's easy enough, right?
WRONG!
You can't create the repo until the dataSource object is created. In straight up TypeORM you can create your new DataSource but in nest, it's an async bootstrap... which means when you import your repo/service files the data source isn't yet there.
grrr.
So, here is my code (simplified).
Basically, it's a small app with users. The users have a role. So you can get the user by name, or update the user name so long as it has a role. If the role is null, then you can't do it. (the create method is not included, assume it's there)
app.config.ts
// database values and such are loaded from an env file. (not included)
export const applicationModuleConfig: ApplicationModuleConfig = {
typeOrmConfig: {
type: "postgres",
host,
username,
database,
port,
synchronize: false,
cache: true,
entities: [resolve(__dirname, "./**/*.entity.ts"), resolve(__dirname, "./**/*.entity.js")],
migrations: [resolve(__dirname, "./migrations/**/*{.ts,.js}")],
migrationsTableName: "migrations",
migrationsRun: true,
password,
}
}
users.entity.ts (trimmed, assume there are more fields/columns)
#Entity()
#ObjectType("Users")
export class Users {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number;
#Field()
#Column({ type: "varchar", nullable: false, default: "" })
name: string;
#Field()
#Column({ type: "varchar", nullable: false, default: "" })
role: string;
}
users.repository.ts
export const UsersRepository = dataSource.getRepository(Users).extend({
findByName(name: string) {
return this.createQueryBuilder("users")
.where("users.name = :name", { name })
.getOne()
},
async verifyRole(id: number) {
const user = await this.createQueryBuilder("users")
.where("users.id = :id", { id })
.getOne()
return user?.role !== null;
},
})
users.service.ts
#Injectable()
export class UsersService {
constructor(private readOnly usersRepo: UsersRepository) {}
findByName(name: string) {
return this.usersRepo.findByName(name);
}
verifyRole(id: number) {
return this.usersRepo.verifyRole(id);
}
}
users.resolver.ts
#Resolver(() => User)
#UseGuards(GraphqlAuthGuard, PermissionsGuard, ScopeGuard)
export class UserResolver {
constructor(private readonly usersService: UsersService) {}
#Query(() => User, { name: "user" })
async getUser(
#Args({
name: "name",
type: () => String,
})
name: string,
#CurrentUser() currentUser: AuthorizedUser,
): Promise<Users> {
return this.usersService.findByName(name);
}
// assume EditUsersInput is a object matching the entity fields.
#Mutation(() => Users)
#VerifyUser<{ input: EditUsersInput }>(({ input }) => {
// >>> this doesn't work <<<*
return UsersRepository.verifyRole(input.id);
})
async editUser(
#Args(
{
name: "input",
type: () => EditUsersInput,
},
new ValidationPipe(),
)
input: EditUsersInput,
): Promise<Users> {
await UsersRepository.editUser(input); // assume the editUser method exists in the service and repo.
return UsersRepository.findByUserId(input.id); // assume the findByUserId method exists in the service and repo.
}
app.module.ts (I will admit I am not sure what I am supposed to be doing in this file)
#Global()
#Module({
imports: [TypeOrmModule.forFeature([...entitiesWithoutAModuleHere])],
providers: [UsersService, UsersRepository],
exports: [UsersService, UsersRepository],
})
export class ApplicationModule {
static forRoot(config: ApplicationModuleConfig): DynamicModule {
const {
graphql,
typeOrmConfig,
numConcurrentReportJobs,
redis: { tls: redisTls, ...redisRest },
} = config;
////
// a bunch of redis code goes in here...
////
return {
module: ApplicationModule,
imports: [
// omitted unrelated imports, such as bullQueue and stuff like that.
TypeOrmModule.forRoot({
...typeOrmConfig,
}),
GraphQLModule.forRoot(graphql),
ConfigurationModule.forRoot(config), // I am not sure if this file is important or not, but I am omitting it for now.
],
};
}
constructor(public dataSource: DataSource) {
// AH HA! The dataSource has been initialized by nest!
// but, the repository errors have already occurred and it never got this far.
// I don't know what to do here.
}
}
main.ts
import blah from "blah";
import { applicationModuleConfig } from "./app.config";
import everythingElse from "otherStuff";
import { ApplicationModule } from "./app.module"; // <-- KaBOOM!
async function bootstrap() {
initializeTransactionalContext();
patchTypeORMRepositoryWithBaseRepository();
const ApplicationModule = require("./app.module");
const app = await NestFactory.create(ApplicationModule.forRoot(applicationModuleConfig), { bodyParser: false });
app.use(
session({
resave: false, //As per https://github.com/expressjs/session#resave
saveUninitialized: false,
store: new (require("connect-pg-simple")(session))({
createTableIfMissing: true,
}),
secret: process.env.APP_SESSION_SECRET, cookie: { secure: secureCookies },
}),
);
await app.listen(3000);
}
bootstrap().then(() => null);
I know I am doing it wrong, but I can't figure out how to do it right. Any thoughts, comments, suggestions, examples, tutorials, or explanations of the right way?
Thanks!

Related

Prisma / Graphql resolver

Good morning all,
I m currently back in the famous world of web development and I have in mind to develop a tool by using Nest/Prisma/Graphl.
However, I'm struggling a little bit on key element like the following one.
Basically, I can see that, by using the "include" function in Prisma (module.service.ts), I'm getting subModules list: this is the expected behavior.
However, on Graphl side, to cover field resolver (module.resolver.ts), I can see that the same request is executing again to cover SubModules field.....
What am I missing?????
See below the code:
module.module.ts
import { Field, ID, ObjectType } from '#nestjs/graphql'
import { SubModule } from './submodule.model'
#ObjectType()
export class Module {
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Field((type) => ID)
id: number
name: string
description: string
icon: string
active: boolean
position: number
subModules?: SubModule[]
}
submodule.model.ts
import { Field, ObjectType, ID } from '#nestjs/graphql';
import { Module } from './module.model';
#ObjectType()
export class SubModule {
#Field((type) => ID)
id: number;
name: string;
description: string;
icon: string;
active: boolean;
position: number;
module: Module;
}
module.resolver.ts
import {
Resolver,
Query,
ResolveField,
Parent,
Args,
InputType,
} from '#nestjs/graphql'
import { Module } from 'src/models/module.model'
import { ModuleService } from './module.service'
import { SubModuleService } from './sub-module.service'
#InputType()
class FilterModules {
name?: string
description?: string
icon?: string
active?: boolean
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Resolver((of) => Module)
export class ModuleResolver {
constructor(
private moduleService: ModuleService,
private subModuleService: SubModuleService,
) {}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => Module)
async module(#Args('ModuleId') id: number) {
return this.moduleService.module(id)
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => [Module], { nullable: true })
async modules(
#Args({ name: 'skip', defaultValue: 0, nullable: true }) skip: number,
#Args({ name: 'filterModules', defaultValue: '', nullable: true })
filterModules: FilterModules,
) {
return this.moduleService.modules({
skip,
where: {
name: {
contains: filterModules.name,
},
},
})
}
#ResolveField()
async subModules(#Parent() module: Module) {
const { id } = module
return this.subModuleService.subModules({ where: { moduleId: id } })
}
}
module.service.ts
import { Injectable } from '#nestjs/common'
import { PrismaService } from 'src/prisma.service'
import { Prisma, Module } from '#prisma/client'
#Injectable()
export class ModuleService {
constructor(private prisma: PrismaService) {
prisma.$on<any>('query', (event: Prisma.QueryEvent) => {
console.log('Query: ' + event.query)
console.log('Params' + event.params)
console.log('Duration: ' + event.duration + 'ms')
})
}
async module(id: number): Promise<Module | null> {
return this.prisma.module.findUnique({
where: {
id: id || undefined,
},
})
}
async modules(params: {
skip?: number
take?: number
cursor?: Prisma.ModuleWhereUniqueInput
where?: Prisma.ModuleWhereInput
orderBy?: Prisma.ModuleOrderByWithRelationInput
}): Promise<Module[]> {
const { skip, take, cursor, where, orderBy } = params
return this.prisma.module.findMany({
skip,
take,
cursor,
where,
orderBy,
})
}
async updateModule(params: {
where: Prisma.ModuleWhereUniqueInput
data: Prisma.ModuleUpdateInput
}): Promise<Module> {
const { where, data } = params
return this.prisma.module.update({
data,
where,
})
}
}
Thanks in advance for your help

Schema must contain uniquely named types named "Project"

I am creating a Apollo Graphql backend using type-orm. I create an entity called Project:
import { Field, ObjectType } from "type-graphql";
import { BaseEntity, Column, Entity, ObjectID, ObjectIdColumn } from "typeorm";
#ObjectType()
#Entity()
export class Project extends BaseEntity {
#Field(() => String)
#ObjectIdColumn()
id: ObjectID;
#Field()
#Column({ unique: true })
name!: string;
#Field()
#Column()
startDate!: Date;
#Field()
#Column({nullable: true})
endDate!: Date
#Field()
#Column({unique:true})
githubUrl: string;
}
and the resolver project:
import { Arg, Mutation, Query, Resolver } from 'type-graphql'
import {Project} from '../entities/project'
import {ProjectInput, ProjectResponse} from '../types/ProjectTypes'
#Resolver()
export class ProjectResolver {
#Query(() => [Project])
async getProjects(): Promise<Project[] | null> {
let projects = await Project.getRepository().find();
return projects;
}
#Mutation(() => ProjectResponse)
async createProject(
#Arg("input") input: ProjectInput
): Promise<ProjectResponse>{
let project : Project;
if(input.name == ""){
throw Error("Invalid input")
}
try{
project = await Project.create({
name: input.name,
startDate: input.startDate,
}).save();
}catch (error) {
if (error.code === 11000) {
return {
errors: [
{
field: "project",
message: "The project name is already in use",
},
],
};
} else return error;
}
return {project: project};
}
#Mutation(() => ProjectResponse)
async setProjectEndDate(
#Arg("projectId") projectId: string,
#Arg("endDate") endDate: Date
): Promise<ProjectResponse>{
let project = await Project.getRepository().findOne(projectId)
if(project){
if(project?.startDate > endDate){
return {
errors:[{
field:"EndDate",
message:"The end date must be a date after the start date of a project."
}]
}
}
project.endDate = endDate;
project.save();
}
return {
errors:[{
field:"Project",
message:"Project could not be found."
}]
}
}
}
this is the code of the 2 auxiliary classes for the input and response of the resolver:
#InputType()
export class ProjectInput{
#Field()
name: string
#Field()
startDate: Date
#Field(()=> Date,{nullable:true})
endDate?: Date | null
#Field(()=> String, {nullable:true})
githubUrl?: string
}
#ObjectType()
export class ProjectResponse{
#Field(() => [FieldError], { nullable: true })
errors?: FieldError[]
#Field(() => Project, { nullable: true })
project?: Project | null
}
this is the code I use to create the ApolloServer object:
const apolloServer = new ApolloServer({
introspection: true,
playground: true,
schema: await buildSchema({
resolvers: [ProjectResolver],
validate: false, // Disable default GraphQL errors
}),
context: ({ req, res }) => ({ req, res}), // Enables use of context (with request) in resolvers
})
And the error I get is the following:
Error: Schema must contain uniquely named types but contains multiple types named "Project".
at new GraphQLSchema (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\graphql\type\schema.js:194:15)
at Function.generateFromMetadataSync (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\schema\schema-generator.js:31:32)
at Function.generateFromMetadata (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\schema\schema-generator.js:16:29)
at Object.buildSchema (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\utils\buildSchema.js:10:61)
at C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\dist\index.js:42:38
at Generator.next ()
at fulfilled (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\dist\index.js:5:58)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
I don't know what the problem is so I would appreciate your help
I have managed to solve the problem by changing the import of the class 'Project' made in the 'ProjectResolver' class.
Instead of:
import {Project} from '../entities/project'
Now looks like this:
import {Project} from '../../src/entities/project'

Prisma2: How to solve n +1 Problem with Paljs

thx for any help.
Im using at the frontend the apollo-client and at the backend graphql-nexus,prisma2 and graphql-yoga server.
I want to solve the n + 1 problem with #paljs/plugins.
At the frontend I have a query posts like:
query posts{
posts {
id
favoritedBy(where: { id: { equals: $currentUserId } }) {
id
}
author {
id
avatar {
id
}
}
link {
id
}
games {
id
}
tags {
id
}
likes(where: { user: { id: { equals: $currentUserId } } }) {
id
}
}
}
Posts resolver:
import { PrismaSelect } from '#paljs/plugins'
export const posts = queryField('posts', {
type: 'Post',
list: true,
args: {
...
},
resolve: async (_parent, args, { prisma, request }, info) => {
const select = new PrismaSelect(info).value
let opArgs: FindManyPostArgs = {
take: 10,
orderBy: {
[args.orderBy]: 'desc',
},
...select
}
const post = await prisma.post.findMany(opArgs)
//The result I want to return with the "sub-models" like likes, author tags...
console.log(JSON.stringify(post, undefined, 2))
return post
},
})
I logging the queries
const prisma = new PrismaClient({
log: ['query'],
})
My Problem: With PrismaSelect, I have 5 queries more than without and If I check the request-time at the frontend I need 300-400ms longer with PrismaSelect. So what I'm doing wrong?
I saw in the #paljs/plugins doc the select in the context. Maybe that is my mistake. How can I use the select in the context?
Here ist my Context:
import { PrismaClient, PrismaClientOptions } from '#prisma/client'
import { PubSub } from 'graphql-yoga'
import { PrismaDelete, onDeleteArgs } from '#paljs/plugins'
class Prisma extends PrismaClient {
constructor(options?: PrismaClientOptions) {
super(options)
}
async onDelete(args: onDeleteArgs) {
const prismaDelete = new PrismaDelete(this)
await prismaDelete.onDelete(args)
}
}
export const prisma = new PrismaClient({
log: ['query'],
})
export const pubsub = new PubSub()
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
}
export function createContext(request: any): Context {
return { prisma, request, pubsub }
}
You need to know that to use my PrismaSelect plugin you need to remove the nexus-prisma-plugin package and use my Pal.js CLI to create your CRUD and ObjectType for nexus and using #paljs/nexus plugin to add in mackSchema function
import { makeSchema } from '#nexus/schema';
import * as types from './graphql';
import { paljs } from '#paljs/nexus'; // import our plugin
export const schema = makeSchema({
types,
plugins: [paljs()],// here our plugin don't use nexus-prisma-plugin
outputs: {
schema: __dirname + '/generated/schema.graphql',
typegen: __dirname + '/generated/nexus.ts',
},
typegenAutoConfig: {
sources: [
{
source: require.resolve('./context'),
alias: 'Context',
},
],
contextType: 'Context.Context',
},
});
Now add this type to your Context
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
select: any // here our select type
}
export function createContext(request: any): Context {
// our paljs plugin will add select object before resolver
return { prisma, request, pubsub, select: {} }
}
after you add our plugin your query will log like this
extendType({
type: 'Query',
definition(t) {
t.field('findOneUser', {
type: 'User',
nullable: true,
args: {
where: arg({
type: 'UserWhereUniqueInput',
nullable: false,
}),
},
resolve(_, { where }, { prisma, select }) {
// our plugin add select object into context for you
return prisma.user.findOne({
where,
...select,
});
},
});
},
});
Can you please try to use my pal c command to start an example from my list and try your schema and make tests with it
It is working, thx Ahmed your plugin is AWESOME!!!!!
I changed my Post-Object from
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.authorId()
t.model.tags()
t.model.games()
t.model.link()
t.model.report()
t.model.notifications()
t.model.author()
t.model.favoritedBy({
filtering: {
id: true,
},
})
t.model.likes({
filtering: {
user: true,
},
})
}
})
to
const Post = objectType({
name: 'Post',
definition(t) {
t.string('id')
t.field('tags', {
nullable: false,
list: [true],
type: 'Tag',
resolve(parent: any) {
return parent['tags']
},
})
t.field('games', {
list: [true],
type: 'Game',
resolve(parent: any) {
return parent['games']
},
})
t.field('link', {
type: 'Link',
nullable: true,
resolve(parent: any) {
return parent['link']
},
})
t.field('notifications', {
list: [true],
type: 'Notification',
resolve(parent: any) {
return parent['notifications']
},
})
t.field('author', {
nullable: false,
type: 'User',
resolve(parent: any) {
return parent['author']
},
})
t.field('favoritedBy', {
nullable: false,
list: [true],
type: 'User',
args: {
where: 'UserWhereInput',
},
resolve(parent: any) {
return parent['favoritedBy']
},
})
t.field('likes', {
list: [true],
type: 'Like',
args: {
where: 'LikeWhereInput',
},
resolve(parent: any) {
return parent['likes']
},
})
},
})
And I also used the nexus-prisma-plugin and paljs-plugin at the same time

TypeGraphql - #inputtype on typeorm

Hello I need to check if there is an email in the database already:
with this:
return User.findOne({ where: { email } }).then((user) => {
if (user) return false;
return true;
});
I have the following inputtypes:
#InputType()
export class RegisterInput {
#Field()
#IsEmail({}, { message: 'Invalid email' })
email: string;
#Field()
#Length(1, 255)
name: string;
#Field()
password: string;
}
I would like to know if there is any way for me to validate the email in the inputtype? or just in my resolve:
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
Actually you can register your own decorator for class-validator
For example it can look something like this:
isEmailAlreadyExists.ts
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { UserRepo } from '../../repositories/UserRepo';
import { InjectRepository } from 'typeorm-typedi-extensions';
#ValidatorConstraint({ async: true })
export class isEmailAlreadyExist
implements ValidatorConstraintInterface {
#InjectRepository()
private readonly userRepo: UserRepo;
async validate(email: string) {
const user = await this.userRepo.findOne({ where: { email } });
if (user) return false;
return true;
}
}
export function IsEmailAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: isEmailAlreadyExist,
});
};
}
If you're injecting dependencies than you should in inject it in class-validator too. Simply add to your main file this:
import { Container } from 'typedi';
import * as classValidator from 'class-validator';
classValidator.useContainer(Container);
...
const schema = await buildSchema({
resolvers: [...],
container: Container,
});
Then you can use decorator in your InputType
import { InputType, Field } from 'type-graphql';
import { IsEmailAlreadyExist } from '../../../utils/validators/isEmailAlreadyExist';
#InputType()
export class YourInput {
#Field()
#IsEmailAlreadyExist()
email: string;
}
I actually just figured this out myself for my own project.
You can simply add a validation on the email from RegisterInput argument and throw an error if the email already exists.
import { Repository } from 'typeorm'
import { InjectRepository } from 'typeorm-typedi-extensions'
...
// Use dependency injection in the resolver's constructor
constructor(
#InjectRepository(User) private readonly userRepository: Repository<User>
) {}
...
// Your mutation
#Mutation(() => User)
async register(
#Arg('data')
{ email, name, password }: RegisterInput,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 12);
const userWithEmail = this.userRepository.find({ email: email })
// If a user with the email was found
if (userWithEmail) {
throw new Error('A user with that email already exists!')
}
const user = await User.create({
email,
name,
password: hashedPassword,
}).save();
return user;
}
To use the InjectRepository make sure you add a "container" to your buildSchema function:
import { Container } from 'typedi'
...
const schema = await buildSchema({
resolvers: [...],
container: Container
})
Let me know if this works out for you? Thanks!

graphql-subscriptions withFilter returns undefined; subscriptions without variables work ok

I'm trying to get my head around graphql-subscriptions and withFilter. Subscriptions without variables work as intended, but if I try to use withFilter, I only get 'Subscription field must return Async Iterable. Received: undefined' error when I try to run the subscription.
Am I doing something wrong with setting up withFilter, are the some incompatibilities with packages I'm using or am I completely missing something obvious here? All queries and mutations work properly, so the basic set up should be fine.
My set up is similar to this (all code snippets are in https://gist.github.com/aqmattil/41e10e7c9f30b8ea964cecdc61c58f20
Package.json
// package.json
"dependencies": {
"apollo-server-express": "^2.0.0-beta.2",
"body-parser": "^1.18.3",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-subscriptions": "^0.5.8",
"subscriptions-transport-ws": "^0.9.11"
}
Mutations
// mutations.js
const mutation = new GraphQLObjectType({
name: 'mutation',
fields: {
addSite: {
type: SiteType,
description: "Create a new Site",
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
location: { type: GraphQLString },
company: { type: GraphQLString }
},
async resolve(parentValue, { name, location, company }) {
const site = await new Site({ name, location, company }).save()
const siteid = site._id;
console.log("addSite resolve", siteid, name, location, company );
pubsub.publish('siteAdded', { 'siteAdded': site } );
return site;
}
}
}
});
module.exports = mutation;
Subscriptions
// subscriptions.js
const graphql = require('graphql');
const {
GraphQLObjectType,
GraphQLString
} = graphql;
const { withFilter } = require('graphql-subscriptions');
const SiteType = require('./site_type');
const pubsub = require('./pubsub_helper');
const Subscriptions = new GraphQLObjectType({
name: 'subscription',
fields: () => ({
/*
// this code works, commented out to test withfilter
siteAdded: {
type: SiteType,
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
return pubsub.asyncIterator('siteAdded');
}
},
*/
// test withFilter
siteAdded: {
type: SiteType,
args: {
name: { type: GraphQLString }
},
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
// this returns undefined
withFilter(
() => {
console.log("in subscribe withfilter");
return pubsub.asyncIterator('siteAdded');
}
),
(payload, variables) => {
console.log("payload, variables", payload, variables);
return true;
}
}
}
})
});
module.exports = Subscriptions;
I'm using graphiql to run the queries,
// this is used to add a site
mutation {
addSite(name:"test name", location: "somewhere") {
id
}
}
// simple subscription - this works as inteded, and new sites are shown
subscription {
siteAdded {
name
location
company {
id
}
}
}
// using query variables --> returns "Subscription
// field must return Async Iterable. Received: undefined"
subscription {
siteAdded(name: "test name") {
name
location
company {
id
}
}
}

Resources