graphql-compose-mongoose
graphql-compose-mongoose copied to clipboard
switch database :useDb()
I have been exploring various libraries for mongoose and graphql and this really looks promising.Great work guys .
I have a scenario where i need to switch database on the bases of request parameters is there any way i can replace the model object for every resolver using useDb function
I may be trying to find some thing which is already available in graphql-compose-mongoose
Hm, I'm really don't know how mongoose model will work when GraphQL will serve in same time multiple requests from different dbs. May be unexpected situation when doc readed from one db and written to another (cause other request switch connection to another db).
Anyway you may use generated graphql types from mongoose models and write your own resolvers.
But recommend way is a creation several models on different connections, eg. User, UserArchive and put them to your schema.
ok.I think i was not clear enough ,actually this is for a multi-tenant situation , so there would be one database for all the user under the same tenant.Thus when i get a request from a user i would like to switch the DB so that they are separate logically at the database level.
Tenant1 ==>DB1 | Tenant2==>DB2 | ....... | Tenant N==>DB N |
---|---|---|---|
USER1,USER2,.... | USER3,USER4,.... | ----- | USER N,...... |
(Read/Write ==>DB1) | (Read/Write ==>DB2) | ----- | Read/Write ==>DB N |
Hope i am able to explain this @nodkz
Currently with how I see it (I would dig deeper hopefully later) you bind the model to the connection upon creation time with a call like:
let MyModel = db2conn.model('MyModelName', modelSchema);
where db2conn
is a mongoose.Connection
created with something like:
let db2conn = mongoose.createConnection(db2ConnectionString);
So assuming a bound model cannot switch a db connection you would go about your multi-tenancy needs like this:
- Generate a all models for each connection.
- Generate the GQL schema for all models for each connection
- Use per-request set GQL schema according to tenant req params.
This might look something like this (I'm just writing this code in the gist.. not tested or running it..)
const tenantsConfig = [
{ name: 't1', connStr: 'mongodb://...' },
{ name: 't2', connStr: 'mongodb://...' }
];
let tenantConns = tenantsConfig.map(tenant => ({
...tenant,
conn: mongoose.createConnection(tenant.connStr))
});
let tenantModels = tenantConns.map(tenant => ({
...tenant,
models: createModels(tenant.conn) // see below
}));
let tenantGqlSchemas = tenantModels.map(tenant => ({
...tenant,
gqlSchema: createGQLSchema(tenant.models) // see below
})).reduce((map, tenant) => {
return {
...map,
[tenant.name]: tenant.gqlSchema
}
}, {});
// When setting up the GQL api endpint (Assuming express-graphql)
app.use('/graphql', graphqlHTTP(req => ({
schema: tenantGqlSchemas[getTenantNameFromRequest(req)].gqlSchema,
graphiql: process.env.NODE_ENV !== 'production',
pretty: process.env.NODE_ENV !== 'production',
})));
// Somewhere else
import { ComposeStorage } from 'graphql-compose';
import composeWithMongoose from 'graphql-compose-mongoose';
import { composeWithRelay } from 'graphql-compose-relay';
import UserSchema from './Schemas/User';
import DogSchema from './Schemas/Dog';
const createModels = (conn) => {
let UserModel = conn.model('User', UserSchema);
let DogModel = conn.model('Dog', DogSchema);
return {
user: UserModel,
dog: DogModel
}
}
const createGQLSchema = (models) => {
const GQC = new ComposeStorage(); // Don't want to reuse the GQC storage - this allows creating a new one.
const userTC = composeWithRelay(composeWithMongoose(models.user));
const dogTC = composeWithRelay(composeWithMongoose(models.dog));
return GQC.rootQuery().addFields({
user: userTC.get('$findById'),
dog: dogTC.get('$findById')
//... more
}).buildSchema();
}
@nodkz I was not sure if using ComposeStorage
would provide the isolation between the two schema trees.. would love to get your input on this.
This looks great ,i'll try this and let you know. My implementation is very naive something like this
function getNewResolver(context, resolverName) {
const conn=mongoose.createConnection(context.tenant.connStr)
const newCustomerModel=conn.model('Customer', CustomerSchema);
const newCustomerModelCT = composeWithMongoose(newCustomerModel)
return newCustomerModelCT.getResolver(resolverName).resolve;
}
//in the GQL schema definition resolver
customers: CustomerTC.getResolver('findMany').wrapResolve(next => rp => {
const resolve = getNewResolver(rp.context, "findMany");
const resultPromise = resolve(rp);
return resultPromise; // return payload promise to upper wrapper
})
obviously i'll have to create all the models.and make sure that i dont create them for every new request . but definitely your implementation looks very promising ,thanks a lot for your efforts.
I tried a basic example from the documentation
import { Resolver, TypeComposer, ComposeStorage } from 'graphql-compose';
import { composeWithMongoose, GraphQLMongoID, filterHelperArgs } from 'graphql-compose-mongoose';
import { model, Model, Document, Schema, SchemaTypes, Connection, createConnection } from 'mongoose';
const conn = createConnection('mongodb://mongo:27017');
const dbs = [
conn.useDb('test1'),
conn.useDb('test2'),
conn.useDb('test3'),
];
dbs.map((db, idx) => {
const MessageSchema = new Schema({
owner: SchemaTypes.ObjectId,
message: String,
});
const MessageModel = db.model('MessageModel', MessageSchema);
const MessageTC = composeWithMongoose(MessageModel, {});
const GQC = new ComposeStorage();
GQC.rootQuery().addFields({
messageById: MessageTC.getResolver('findById'),
messageByIds: MessageTC.getResolver('findByIds'),
messageOne: MessageTC.getResolver('findOne'),
messageMany: MessageTC.getResolver('findMany'),
messageTotal: MessageTC.getResolver('count'),
messageConnection: MessageTC.getResolver('connection'),
});
GQC.rootMutation().addFields({
messageCreate: MessageTC.getResolver('createOne'),
messageUpdateById: MessageTC.getResolver('updateById'),
messageUpdateOne: MessageTC.getResolver('updateOne'),
messageUpdateMany: MessageTC.getResolver('updateMany'),
messageRemoveById: MessageTC.getResolver('removeById'),
messageRemoveOne: MessageTC.getResolver('removeOne'),
messageRemoveMany: MessageTC.getResolver('removeMany'),
});
GQC.buildSchema();
new MessageModel({message: 'hello'}).save();
});
I will get an error like
Error: Schema must contain unique named types but contains multiple types named "FilterFindManyMessageModelInput".
web_1 | at invariant (/app/node_modules/graphql/jsutils/invariant.js:19:11)
web_1 | at typeMapReducer (/app/node_modules/graphql/type/schema.js:254:29)
web_1 | at Array.reduce (native)
web_1 | at /app/node_modules/graphql/type/schema.js:287:36
web_1 | at Array.forEach (native)
web_1 | at typeMapReducer (/app/node_modules/graphql/type/schema.js:280:27)
web_1 | at Array.reduce (native)
web_1 | at new GraphQLSchema (/app/node_modules/graphql/type/schema.js:137:34)
web_1 | at ComposeStorage.buildSchema (/app/node_modules/graphql-compose/lib/storage.js:144:16)
web_1 | at dbs.map (/app/src/index.ts:52:7)
web_1 | at Array.map (native)
web_1 | at Object.<anonymous> (/app/src/index.ts:23:5)
web_1 | at Module._compile (module.js:571:32)
web_1 | at Object.Module._extensions..js (module.js:580:10)
web_1 | at Module.load (module.js:488:32)
web_1 | at tryModuleLoad (module.js:447:12)
The error goes away when I remove the line for the connection resolver.
messageConnection: MessageTC.getResolver('connection'),
Any ideas?
@Anthonyzou please open separate issue and provide you package.json
Also will be cool if you can provide public repo with your problem, it helps me to resolve this problem much faster.
Thanks.
@Anthonyzou it something untrivial. Because in my setup (with my libs' versions) it works. Maybe some breaking changes were introduced in last graphql-js
.
Anyway, separate issue, please, for this problem.
If you use multiple DBS and generate one model several times for different schemas you need to clear internal graphql-compose-mongoose
typeStorage, otherwise, it will reuse generated types and resolvers also will pointing to the wrong DB (from the previous generation).
import { composeWithMongoose, mongooseTypeStorage } from 'graphql-compose-mongoose';
...
const schemas = dbs.map((db) => {
...
const MessageModel = db.model('MessageModel', MessageSchema);
mongooseTypeStorage.clear(); // <---- cleare before each generation for separate graphql schema
const MessageTC = composeWithMongoose(MessageModel, {});
...
const GQC = new ComposeStorage();
...
return GQC.buildSchema();
});
Related: https://github.com/nodkz/graphql-compose-mongoose/issues/32
How about using multi-tenant at the document level? https://www.npmjs.com/package/mongo-tenant any support for this approach? Thank you!
@nodkz i am getting the following error while using multiple DB's if i have a pagination resolver but if i remove it the error goes
Schema must contain unique named types but contains multiple types named "FilterFindManyEmployeeInput".
here is the repo https://github.com/anoopsinghbayes/ATMS/blob/d9541e4ac484e8de9c9522849e6e8fc39b925231/src/initModel.js#L36
@anoopsinghbayes thanks for reporting and sorry for the late response.
Use this as a workaround for your [email protected] version:
//how to clear typestorage as in earlier version
schemaComposer.clear();
models.address.schema._gqcTypeComposer = null;
models.businessPartner.schema._gqcTypeComposer = null;
models.customer.schema._gqcTypeComposer = null;
models.employee.schema._gqcTypeComposer = null;
Or upgrade till new [email protected] where this behavior is fixed and should work without code changes.