Skip to content

Commit 749a8eb

Browse files
committed
mongo ssie
1 parent 9a4ee07 commit 749a8eb

File tree

5 files changed

+108
-88
lines changed

5 files changed

+108
-88
lines changed

Readme.md

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,64 @@ const resolver = () =>
7070
## Experimental data loader
7171

7272
You can use experimental data loader for requests that can cause n+1 problem. It is still in experimental phase.
73+
Consider the following schema
74+
```graphql
75+
type Person{
76+
id: String!
77+
username:String!
78+
friends: [Person!]!
79+
}
80+
81+
type Query{
82+
person(_id:String!): Person!
83+
}
84+
```
7385

74-
75-
## List objects
76-
```ts
77-
const result = await MongoOrb("Source").list({})
86+
And the following query:
87+
88+
```gql
89+
query GetPersonWithFriends{
90+
person(id:"38u198rh89h"){
91+
username
92+
id
93+
friends{
94+
username
95+
id
96+
friends{
97+
username
98+
id
99+
}
100+
}
101+
}
102+
}
78103
```
79104

80-
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:
105+
Here is how you can implement to limit db calls and avoid n+1 problem
81106

82107
```ts
83-
const result = await MongoOrb("Source").oneByPk("892194hruh8hasd")
84-
```
108+
const peopleLoader = dataLoader<{[id:string]: PersonModel}>({})
85109

86-
It will load the object from promise instead of calling the database
110+
export const QueryPeople = async (_,args) => {
111+
const person = await MongoOrb("Person").collection.findOne({_id:args._id})
112+
const friends = await MongoOrb("Person").collection.find({
113+
_id:{
114+
$in: person.friends
115+
}
116+
}).toArray()
117+
const friendsOfFriends = await MongoOrb("Person").collection.find({
118+
_id:{
119+
$in: friends.flatMap(f => f.friends)
120+
}
121+
})
122+
const allPeople = Object.fromEntries([person,...friends,friendsOfFriends].map(p => ([p.id,p])))
123+
return peopleLoader.withData(person,allPeople)
124+
}
125+
126+
export const PersonFriends = (src,args) =>{
127+
const source = peopleLoader.fromSrc(src)
128+
return {
129+
...src,
130+
friends: src.friends.map(f => source.__dataLoader[f])
131+
}
132+
}
133+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "i-graphql",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"private": false,
55
"license": "MIT",
66
"description": "GraphQL Friendly ORM for MongoDB",

src/cacheFunctions.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,42 @@
11
const promises: Record<string, Promise<any> | undefined> = {};
2+
3+
type ObjectWithDataLoader<T, D> = T & { __dataLoader: D };
4+
type InputWithDataLoader<T, D> = T extends undefined
5+
? undefined
6+
: T extends null
7+
? null
8+
: T extends Array<infer R>
9+
? Array<InputWithDataLoader<R, D>>
10+
: ObjectWithDataLoader<T, D>;
11+
12+
export const dataLoader = <DataLoaderType>() => {
13+
const withData = <T>(v: any, dl: DataLoaderType): InputWithDataLoader<T, DataLoaderType> | undefined => {
14+
if (Array.isArray(v)) {
15+
return v
16+
.filter(<V>(value: V | undefined | null): value is V => !!v)
17+
.map((value) => withData(value, dl)) as InputWithDataLoader<T, DataLoaderType>;
18+
}
19+
if (v === null) return;
20+
if (v === undefined) return;
21+
if (typeof v === 'object') {
22+
return {
23+
...v,
24+
__dataLoader: dl,
25+
};
26+
}
27+
return;
28+
};
29+
const fromSource = <T>(src: T) => {
30+
return src as T extends Array<infer R>
31+
? Array<ObjectWithDataLoader<R, DataLoaderType>>
32+
: ObjectWithDataLoader<T, DataLoaderType>;
33+
};
34+
return {
35+
withData,
36+
fromSource,
37+
};
38+
};
39+
240
const setToPromise = <PromiseType>(key: string, value: () => Promise<PromiseType>) => {
341
const promise = value();
442
promises[key] = promise;

src/index.ts

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { clearPromises, getFromPromise } from '@/cacheFunctions';
1+
import { clearPromises } from '@/cacheFunctions';
22
import { mc } from '@/db';
33
import { Db, WithId, OptionalUnlessRequiredId, MongoClient } from 'mongodb';
44
type AutoCreateFields = {
@@ -12,36 +12,20 @@ type SharedKeys<AutoTypes, MTypes> = {
1212
export const iGraphQL = async <
1313
IGraphQL extends Record<string, Record<string, any>>,
1414
CreateFields extends AutoCreateFields = {},
15-
>(
16-
//setting this allows to use list responses project the individual object cache.
17-
primaryKeys: {
18-
[P in keyof IGraphQL]: keyof IGraphQL[P];
19-
},
20-
props: {
21-
autoFields: CreateFields;
22-
afterConnection?: (database: Db) => void;
23-
// override the process.env.MONGO_URL variable
24-
mongoClient?: MongoClient;
25-
},
26-
) => {
15+
>(props: {
16+
autoFields: CreateFields;
17+
afterConnection?: (database: Db) => void;
18+
// override the process.env.MONGO_URL variable
19+
mongoClient?: MongoClient;
20+
}) => {
2721
const { autoFields, afterConnection, mongoClient } = props;
2822
const { db } = await mc({ afterConnection, mongoClient });
2923
clearPromises();
30-
return <T extends keyof IGraphQL>(k: T extends string ? T : never) => {
31-
type PK = IGraphQL[T][(typeof primaryKeys)[T]];
24+
return <T extends keyof IGraphQL>(k: T) => {
3225
type O = IGraphQL[T];
33-
const collection = db.collection<O>(k);
34-
type CurrentCollection = typeof collection;
35-
const primaryKey = primaryKeys[k] as string;
26+
const collection = db.collection<O>(k as string);
3627
const create = async (params: OptionalUnlessRequiredId<O>) => {
3728
const result = await collection.insertOne(params);
38-
await getFromPromise(
39-
k,
40-
JSON.stringify({
41-
[primaryKey]: params[primaryKey],
42-
}),
43-
async () => params,
44-
);
4529
return result;
4630
};
4731
const createWithAutoFields = <Z extends SharedKeys<CreateFields, O>>(...keys: Array<Z>) => {
@@ -124,42 +108,10 @@ export const iGraphQL = async <
124108
});
125109
};
126110

127-
//method to get one object by Id using data loader cache
128-
const oneByPk = async (pkValue: PK): Promise<WithId<O> | null | undefined> => {
129-
// if we have the list primary key we need to check the cache only by using this key
130-
const paramKey = JSON.stringify({
131-
[primaryKey]: pkValue,
132-
});
133-
return getFromPromise(k, paramKey, () => {
134-
return collection.findOne({ [primaryKey as any]: pkValue });
135-
});
136-
};
137-
138-
type CurrentCollectionFindType = CurrentCollection['find'];
139-
//method to get list of objects - working with inner cache. Calls toArray at the end so you don't have to.
140-
const list = async (...params: Parameters<CurrentCollectionFindType>): Promise<WithId<O>[]> => {
141-
const paramKey = JSON.stringify(params[0]);
142-
const result = await getFromPromise(k, paramKey, () => collection.find(...params).toArray());
143-
for (const individual of result) {
144-
if (individual[primaryKeys[k] as string]) {
145-
getFromPromise(
146-
k,
147-
JSON.stringify({
148-
[primaryKey]: individual[primaryKey],
149-
}),
150-
async () => individual,
151-
);
152-
}
153-
}
154-
return result;
155-
};
156-
157111
return {
158112
collection,
159113
create,
160114
createWithAutoFields,
161-
list,
162-
oneByPk,
163115
related,
164116
composeRelated,
165117
};

src/integration.test.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,12 @@ const iGraphQLClient = async () => {
1616
{
1717
_id: () => string;
1818
}
19-
>(
20-
{
21-
Todo: '_id',
22-
},
23-
{
24-
autoFields: {
25-
_id: () => new ObjectId().toHexString(),
26-
},
27-
mongoClient: client,
19+
>({
20+
autoFields: {
21+
_id: () => new ObjectId().toHexString(),
2822
},
29-
);
23+
mongoClient: client,
24+
});
3025
return MongoOrb;
3126
};
3227

@@ -45,17 +40,5 @@ describe('Testing i-graphql with mongodb in memory module', () => {
4540
title,
4641
});
4742
expect(result.insertedId).toBeTruthy();
48-
const resultFetch = await MongoOrb('Todo').oneByPk(result.insertedId);
49-
expect(resultFetch?._id).toEqual(result.insertedId);
50-
expect(resultFetch?.title).toEqual(title);
51-
});
52-
it('should have the same id in list and oneByPk method while calling the db only once', async () => {
53-
const MongoOrb = await iGraphQLClient();
54-
const resultInsert = await MongoOrb('Todo').createWithAutoFields('_id')({
55-
title: 'aaa',
56-
});
57-
await MongoOrb('Todo').list({});
58-
const resultPk = await MongoOrb('Todo').oneByPk(resultInsert.insertedId);
59-
expect(resultPk?._id).toEqual(resultInsert.insertedId);
6043
});
6144
});

0 commit comments

Comments
 (0)