Handling Mongoose Populated Fields in GraphQL - node.js

How do I represent a field that could be either a simple ObjectId string or a populated Object Entity?
I have a Mongoose Schema that represents a 'Device type' as follows
// assetSchema.js
import * as mongoose from 'mongoose'
const Schema = mongoose.Schema;
var Asset = new Schema({ name : String,
linked_device: { type: Schema.Types.ObjectId,
ref: 'Asset'})
export AssetSchema = mongoose.model('Asset', Asset);
I am trying to model this as a GraphQLObjectType but I am stumped on how to allow the linked_ue field take on two types of values, one being an ObjectId and the other being a full Asset Object (when it is populated)
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString } from 'graphql'
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: GraphQLString,
linked_device: ____________ // stumped by this
});
I have looked into Union Types but the issue is that a Union Type expects fields to be stipulated as part of its definition, whereas in the case of the above, there are no fields beneath the linked_device field when linked_device corresponds to a simple ObjectId.
Any ideas?

As a matter of fact, you can use union or interface type for linked_device field.
Using union type, you can implement GQAssetType as follows:
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql'
var LinkedDeviceType = new GraphQLUnionType({
name: 'Linked Device',
types: [ ObjectIdType, GQAssetType ],
resolveType(value) {
if (value instanceof ObjectId) {
return ObjectIdType;
}
if (value instanceof Asset) {
return GQAssetType;
}
}
});
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: { type: GraphQLString },
linked_device: { type: LinkedDeviceType },
})
});
Check out this excellent article on GraphQL union and interface.

I was trying to solve the general problem of pulling relational data when I came across this article. To be clear, the original question appears to be how to dynamically resolve data when the field may contain either the ObjectId or the Object, however I don't believe it's good design in the first place to have a field store either object or objectId. Accordingly, I was interested in solving the simplified scenario where I keep the fields separated -- one for the Id, and the other for the object. I also, thought employing Unions was overly complex unless you actually have another scenario like those described in the docs referenced above. I figured the solution below may interest others also...
Note: I'm using graphql-tools so my types are written schema language syntax. So, if you have a User Type that has fields like this:
type User {
_id: ID
firstName: String
lastName: String
companyId: ID
company: Company
}
Then in my user resolver functions code, I add this:
User: { // <-- this refers to the User Type in Graphql
company(u) { // <-- this refers to the company field
return User.findOne({ _id: u.companyId }); // <-- mongoose User type
},
}
The above works alongside the User resolver functions already in place, and allow you write GQL queries like this:
query getUserById($_id:ID!)
{ getUserById(_id:$_id) {
_id
firstName
lastName
company {
name
}
companyId
}}
Regards,
S. Arora

Related

How to represent a map or object with key-value pairs in GraphQL?

How can I query for the following object?
{
result: {
'1': {
^^^ these are dynamic keys, never constant
id: 'id1',
},
'20': {
id: 'id2',
},
'300': {
id: 'id3',
},
}
}
I know that I can define the result object fairly simply, if it wasn't a key-value pair object.
const ResultQueryType = new GraphQLObjectType({
name: 'ResultQueryType',
fields: () => ({
id: { type: GraphQLString }
})
})
But this is clearly not what I need. I haven't encountered such a scenario with GraphQL yet, what can I do here?
You can try the dynamic key as suggested here. https://graphql.org/graphql-js/type/#graphqlobjecttype
const ResultQueryType = new GraphQLObjectType({
name: "ResultQueryType",
fields: () => ({
[fieldName: string]: { type: GraphQLString },
}),
});
You can only query fields that have been explicitly defined in the schema.
Spec: The target field of a field selection must be defined on the scoped type of the selection set.
Docs Every GraphQL service defines a set of types which completely describe the set of possible data you can query on that service. Then, when queries come in, they are validated and executed against that schema.
In other words, you can't have a Results map type (and therefore can't query it) if its fields are not known to the schema definition. There are a couple of workarounds:
Use JSON. Many implementations let you define custom scalars as JSON or
have a JSON type that is a String alias. You keep the map structure but lose type awareness. It's left to the client to parse the result.
Refactor your map to an array. If you can merge the top-level key into each record, you can return an [Item] array.
You have to abandon the map, but you keep full GraphQL type-awareness.

Mongoose Schema type as a custom interface from TypeScript?

I´d like to store a custom object that has the StationRating interface attributes, can somebody help me ?
It's possible, but it requires some boilerplate. Issues are:
Interfaces and types in TS do not exist in emitted code
While you could go in the other direction - create a schema object and make an interface/type out of it - schema object values must be constructors, eg Number, which is not the same thing as something with a number type.
But you can create a type that maps the constructor types to their primitive types (eg Number to number), then use that to turn the schema object into the type you want:
type ConstructorMapping<T> =
T extends NumberConstructor ? number :
T extends StringConstructor ? string : never; // etc: continue as needed
const schemaObj = {
score: Number,
user_id: String,
station_id: String,
description: String,
};
type SchemaObj = typeof schemaObj;
type StationRating = {
[prop in keyof SchemaObj]: ConstructorMapping<SchemaObj[prop]>
};
Then use schemaObj when calling new Schema, and you'll also have the following usable type:
type StationRating = {
score: number;
user_id: string;
station_id: string;
description: string;
}
Is it worth it? I'm not sure. For smaller objects, maybe not. For larger objects, maybe so. You might prefer just to write out the type and the schema object.
Is there a specific reason you want to store it in an object? You could use a virtual method to create the "Rating". I'm not exactly sure without seeing more, but if all you want to do is reference the score or create some calculations off it, then it seems like an easy enough solution.
https://mongoosejs.com/docs/tutorials/virtuals.html
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
});
// Create a virtual property `fullName` with a getter and setter.
userSchema.virtual('fullName').
get(function() { return `${this.firstName} ${this.lastName}`; }).
set(function(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
});
const User = mongoose.model('User', userSchema);
const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';
doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'

How to find out whether a nested document is an actual subschema or just an object in Mongoose

I have the following situation:
I am creating a general purpose update function for my project, which takes payload and goes through it, checking whether property in payload exists on model schema and if it does it assigns the new property value to document (updating it). It does this also in subdocuments (recursively).
I have a custom defined type Language for multi-language string fields, which is an object that contains properties in form of language ('en', 'de', etc). Now since its a custom type, Mongoose doesn't know if its contents were modified, so I have to use markModified on it. And here comes the problem: Actual subschemas behave here differently than objects. If I call markModified on subschema, it expects path within that subschema, not entire document. On the other hand, if I call markModified on an object, it expect entire path from parent. I don't know whether it is a bug or not, but if I want to support both, I need to differentiate between the two in my function. Is there a way to know whether it's a subschema made by user or just an object (that was converted to subschema by mongoose)?
Example setup model:
const TestSchema = new Schema(
{
object: {
name: {
type: Language
}
},
nestedSchema: {
type: NestedTestSchema
}
}
)
const NestedTestSchema = new Schema(
{
name: {
type: Language
}
}
)
Example code:
const testDocument = new TestModel({
object: {
name: {
en: 'NameEN',
de: 'NameDE'
}
}
nestedSchema: {
name: {
en: 'NameEN',
de: 'NameDE'
}
}
})
// We make a payload to change these values
const payload = {
object: { // Update object
name: {
en: 'Name updated',
fr: 'Something',
}
},
nestedSchema: { // Update subschema
name: {
en: 'Name updated',
fr: 'Something',
}
}
}
And now when I receive this and update the document with these values, for object I have to
const { object, nestedSchema } = document // This, of course, is useless here, I would get nestedSchema and object as argument in recursive function, its only for demonstration
nestedSchema.markModified('name.en') // Etc
and for object I have to
object.markModified('object.name.en') // Etc
Together with co-workers we found out that object is not an actual subschema, it's called a nestedPath. Only nestedPaths have property $__isNested, subschemas don't. As a result, because of this different handling of two cases, in nestedPaths we need to specify full path when using markModified while in subschemas only path within that subschema

NodeJs GraphQL enum type value as dynamic

https://launchpad.graphql.com/9qvqz3v5r
Here is my example graphQL Schema. i am trying to use enum type. How do i get enum values from backend and give it into schema?
// Construct a schema, using GraphQL schema language
const typeDefs = `
type User {
userId: Int
firstName: String
lastName: String
pincode:String
state:String
country:String
}
type Query {
hello: String
user: User
}
type CreateUserLoad {
user: User
}
enum Role {
writer
reader
author
admin
superAdmin
}
type Mutation{
createUser(firstName: String, lastName: String, role: Role): User
}
`;
I want to populate enum Role value from dynamic variable as
const roleData = ['writer','reader','author','admin','superAdmin'];
Can anyone help me?
You can simply use string interpolation:
// Construct a schema, using GraphQL schema language
const typeDefs = `
type User {
userId: Int
firstName: String
lastName: String
pincode:String
state:String
country:String
}
type Query {
hello: String
user: User
}
type CreateUserLoad {
user: User
}
enum Role { ${roles.join(' ')} }
type Mutation{
createUser(firstName: String, lastName: String, role: Role): User
}
`;
In fact, on every incoming grahpql query you have to pass the parsed schema to the graphql server, so you can even change it for every request. In that case, it would be better to change the object representation that the schema parsing returned.
For creating enum types directly, say you have an array of values userRoles and want a RolesEnum type, then you can create it like so:
const roleValues = {}
for (const value of userRoles) {
roleValues[value] = {value}
}
const RolesEnum = new GraphQLEnumType({
name: 'UserRoles',
values: roleValues,
})
you can then assign that directly as a type in your schema.
If your enum values are loaded from a database or any other back-end source, or if the enum list is dynamic, then you can't have a static enum definition in your schema.
The biggest problem with setting enums dynamically is that your schema is meant to be a contract between back-end and front-end and is not suppose to be altered.
If you are going to use your role as an arbitrary string then define it as such!
type Mutation {
createUser(firstName: String, lastName: String, role: String): User
}
The only difference here would be that your resolver will have to check if the role exists, which is what graphql previously did for you when you were using an enum.
Alternatively
If the role is used on a lot of queries / mutations, you can define a scalar Role and in the resolved for the scalar you can check if the role exists and simply throw an error if it doesn't.
In this case your mutation would look the same as if you had a dynamic enum Role, but you will not need to alter the schema at all.
type Mutation {
createUser(firstName: String, lastName: String, role: Role): User
}
To add dynamic enums along with documentation string using string interpolation.
Example: I have a list of countries and their ISO2 Codes
const countryData = [
{name:'India', code: 'IN'},
{name:'Afghanistan', code: 'AF'},
{name:'Algeria', code: 'DZ'},
{name: 'Ireland', code: 'IE'
];
const countryCodes = countryData.flatMap(country => [
`"${country.name}"`,
country.code
]);
Without using Array.join()
enum CountryCode { ${countryCodes} }

Query Parse.com migrated database pointer relationship with Mongoose

Context
So we have migrated from Parse.com to an hosted MongoDB database. Now I have to write a script that queries our database directly (not using Parse).
I'm using nodejs / mongoose and am able to retrieve these documents.
Problem
Here is my schema so far:
var StorySchema = new mongoose.Schema({
_id: String,
genre: String
});
var ActivitySchema = new mongoose.Schema({
_id: String,
action: String,
_p_story: String /* Also tried: { type: mongoose.Schema.Types.ObjectId, ref: 'Story' } and { type: String, ref: 'Story' }*/,
});
I would like to write a query that fetches theses documents with the related Story (stored as a pointer).
Activity
.find({
action: 'read',
})
.exec(function(error, activities) {
activities.forEach(function(activity) {
// I would like to use activity._p_story or whatever the mean to access the story here
});
});
Question
Is there a way to have the fetched activities populated with their story, given that the _p_story field contains Story$ before the object id?
Thanks!
One option I have been looking at is the ability to create a custom data type for each pointer. The unfortunate side is Parse treats these as 'belongsTo' relationships and but does not store the 'hasMany' relationship that Mongoose wants for populate(). But once this is in place you can easily do loops to get the relational data. Not ideal but works and is what populate is really doing under the hood anyways.
PointerTypeClass.js -> This would work for populating the opposite direction.
var Pointer = function(mongoose) {
function PointerId(key, options) {
mongoose.SchemaType.call(this, key, options, 'PointerId');
}
PointerId.prototype = Object.create(mongoose.SchemaType.prototype);
PointerId.prototype.cast = function(val) {
return 'Pointer$' + val;
}
return PointerId;
}
module.exports = Pointer;
Also be sure mongoose knows about the new type by doing mongoose.Schema.Types.PointerId = require('./types/PointerTypeClass')(mongoose);
Lastly. If you are willing to write some cloudcode you could create the array of ids for your populate to know about the objects. Basically in your Object.beforeSave you would update the array of the id for the relationship. Hope this helps.

Resources