Skip to content

Commit

Permalink
mongo ssie
Browse files Browse the repository at this point in the history
  • Loading branch information
aexol committed Jul 5, 2024
1 parent 9a4ee07 commit 749a8eb
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 88 deletions.
63 changes: 55 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,64 @@ const resolver = () =>
## Experimental data loader

You can use experimental data loader for requests that can cause n+1 problem. It is still in experimental phase.
Consider the following schema
```graphql
type Person{
id: String!
username:String!
friends: [Person!]!
}

type Query{
person(_id:String!): Person!
}
```


## List objects
```ts
const result = await MongoOrb("Source").list({})
And the following query:

```gql
query GetPersonWithFriends{
person(id:"38u198rh89h"){
username
id
friends{
username
id
friends{
username
id
}
}
}
}
```

This will return list of objects but also make resolved promise for each of _id contained inside list. So later during the same GraphQL query if you request some object:
Here is how you can implement to limit db calls and avoid n+1 problem

```ts
const result = await MongoOrb("Source").oneByPk("892194hruh8hasd")
```
const peopleLoader = dataLoader<{[id:string]: PersonModel}>({})

It will load the object from promise instead of calling the database
export const QueryPeople = async (_,args) => {
const person = await MongoOrb("Person").collection.findOne({_id:args._id})
const friends = await MongoOrb("Person").collection.find({
_id:{
$in: person.friends
}
}).toArray()
const friendsOfFriends = await MongoOrb("Person").collection.find({
_id:{
$in: friends.flatMap(f => f.friends)
}
})
const allPeople = Object.fromEntries([person,...friends,friendsOfFriends].map(p => ([p.id,p])))
return peopleLoader.withData(person,allPeople)
}

export const PersonFriends = (src,args) =>{
const source = peopleLoader.fromSrc(src)
return {
...src,
friends: src.friends.map(f => source.__dataLoader[f])
}
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "i-graphql",
"version": "0.1.8",
"version": "0.1.9",
"private": false,
"license": "MIT",
"description": "GraphQL Friendly ORM for MongoDB",
Expand Down
38 changes: 38 additions & 0 deletions src/cacheFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
const promises: Record<string, Promise<any> | undefined> = {};

type ObjectWithDataLoader<T, D> = T & { __dataLoader: D };
type InputWithDataLoader<T, D> = T extends undefined
? undefined
: T extends null
? null
: T extends Array<infer R>
? Array<InputWithDataLoader<R, D>>
: ObjectWithDataLoader<T, D>;

export const dataLoader = <DataLoaderType>() => {
const withData = <T>(v: any, dl: DataLoaderType): InputWithDataLoader<T, DataLoaderType> | undefined => {
if (Array.isArray(v)) {
return v
.filter(<V>(value: V | undefined | null): value is V => !!v)
.map((value) => withData(value, dl)) as InputWithDataLoader<T, DataLoaderType>;
}
if (v === null) return;
if (v === undefined) return;
if (typeof v === 'object') {
return {
...v,
__dataLoader: dl,
};
}
return;
};
const fromSource = <T>(src: T) => {
return src as T extends Array<infer R>
? Array<ObjectWithDataLoader<R, DataLoaderType>>
: ObjectWithDataLoader<T, DataLoaderType>;
};
return {
withData,
fromSource,
};
};

const setToPromise = <PromiseType>(key: string, value: () => Promise<PromiseType>) => {
const promise = value();
promises[key] = promise;
Expand Down
66 changes: 9 additions & 57 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clearPromises, getFromPromise } from '@/cacheFunctions';
import { clearPromises } from '@/cacheFunctions';
import { mc } from '@/db';
import { Db, WithId, OptionalUnlessRequiredId, MongoClient } from 'mongodb';
type AutoCreateFields = {
Expand All @@ -12,36 +12,20 @@ type SharedKeys<AutoTypes, MTypes> = {
export const iGraphQL = async <
IGraphQL extends Record<string, Record<string, any>>,
CreateFields extends AutoCreateFields = {},
>(
//setting this allows to use list responses project the individual object cache.
primaryKeys: {
[P in keyof IGraphQL]: keyof IGraphQL[P];
},
props: {
autoFields: CreateFields;
afterConnection?: (database: Db) => void;
// override the process.env.MONGO_URL variable
mongoClient?: MongoClient;
},
) => {
>(props: {
autoFields: CreateFields;
afterConnection?: (database: Db) => void;
// override the process.env.MONGO_URL variable
mongoClient?: MongoClient;
}) => {
const { autoFields, afterConnection, mongoClient } = props;
const { db } = await mc({ afterConnection, mongoClient });
clearPromises();
return <T extends keyof IGraphQL>(k: T extends string ? T : never) => {
type PK = IGraphQL[T][(typeof primaryKeys)[T]];
return <T extends keyof IGraphQL>(k: T) => {
type O = IGraphQL[T];
const collection = db.collection<O>(k);
type CurrentCollection = typeof collection;
const primaryKey = primaryKeys[k] as string;
const collection = db.collection<O>(k as string);
const create = async (params: OptionalUnlessRequiredId<O>) => {
const result = await collection.insertOne(params);
await getFromPromise(
k,
JSON.stringify({
[primaryKey]: params[primaryKey],
}),
async () => params,
);
return result;
};
const createWithAutoFields = <Z extends SharedKeys<CreateFields, O>>(...keys: Array<Z>) => {
Expand Down Expand Up @@ -124,42 +108,10 @@ export const iGraphQL = async <
});
};

//method to get one object by Id using data loader cache
const oneByPk = async (pkValue: PK): Promise<WithId<O> | null | undefined> => {
// if we have the list primary key we need to check the cache only by using this key
const paramKey = JSON.stringify({
[primaryKey]: pkValue,
});
return getFromPromise(k, paramKey, () => {
return collection.findOne({ [primaryKey as any]: pkValue });
});
};

type CurrentCollectionFindType = CurrentCollection['find'];
//method to get list of objects - working with inner cache. Calls toArray at the end so you don't have to.
const list = async (...params: Parameters<CurrentCollectionFindType>): Promise<WithId<O>[]> => {
const paramKey = JSON.stringify(params[0]);
const result = await getFromPromise(k, paramKey, () => collection.find(...params).toArray());
for (const individual of result) {
if (individual[primaryKeys[k] as string]) {
getFromPromise(
k,
JSON.stringify({
[primaryKey]: individual[primaryKey],
}),
async () => individual,
);
}
}
return result;
};

return {
collection,
create,
createWithAutoFields,
list,
oneByPk,
related,
composeRelated,
};
Expand Down
27 changes: 5 additions & 22 deletions src/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,12 @@ const iGraphQLClient = async () => {
{
_id: () => string;
}
>(
{
Todo: '_id',
},
{
autoFields: {
_id: () => new ObjectId().toHexString(),
},
mongoClient: client,
>({
autoFields: {
_id: () => new ObjectId().toHexString(),
},
);
mongoClient: client,
});
return MongoOrb;
};

Expand All @@ -45,17 +40,5 @@ describe('Testing i-graphql with mongodb in memory module', () => {
title,
});
expect(result.insertedId).toBeTruthy();
const resultFetch = await MongoOrb('Todo').oneByPk(result.insertedId);
expect(resultFetch?._id).toEqual(result.insertedId);
expect(resultFetch?.title).toEqual(title);
});
it('should have the same id in list and oneByPk method while calling the db only once', async () => {
const MongoOrb = await iGraphQLClient();
const resultInsert = await MongoOrb('Todo').createWithAutoFields('_id')({
title: 'aaa',
});
await MongoOrb('Todo').list({});
const resultPk = await MongoOrb('Todo').oneByPk(resultInsert.insertedId);
expect(resultPk?._id).toEqual(resultInsert.insertedId);
});
});

0 comments on commit 749a8eb

Please sign in to comment.