Resolving auto-generated typescript-mongodb types for GraphQL output - mongodb

I'm using the typescript-mongodb plugin to graphql-codegen to generate Typescript types for pulling data from MongoDB and outputting it via GraphQL on Node.
My input GraphQL schema looks like this
type User #entity{
id: ID #id,
firstName: String #column #map(path: "first_name"),
...
The generated output Typescript types look correct
export type User = {
__typename?: 'User',
id?: Maybe<Scalars['ID']>,
firstName?: Maybe<Scalars['String']>,
...
And the corresponding DB object
export type UserDbObject = {
_id?: Maybe<String>,
first_name: Maybe<string>,
...
The problem is when actually sending back the mongo document as a UserDbObject I do not get the fields mapped in the output. I could write a custom resolver that re-maps the fields back to the User type, but that would mean I'm mapping the fields in two different places.
i.e. I do not get mapped fields from a resolver like this
userById: async(_root: any, args: QueryUserByIdArgs, _context: any) : Promise<UserDbObject> => {
const result = await connectDb().then((db) => {
return db.collection<UserDbObject>('users').findOne({'_id': args.id}).then((doc) => {
return doc;
});
})
...
return result as UserDbObject;
}
};
Is there a way to use the typescript-mongodb plugin to only have to map these fields in the schema, then use the auto-generated code to resolve them?

You can use mappers feature of codegen to map between your GraphQL types and your models types.
See:
https://graphql-code-generator.com/docs/plugins/typescript-resolvers#mappers---overwrite-parents-and-resolved-values
https://graphql-code-generator.com/docs/plugins/typescript-resolvers#mappers-object
Since all codegen plugins are independent and not linked together, you should do it manually, something like:
config:
mappers:
User: UserDbObject
This will make typescript-resolvers plugin to use UserDbObject at any time (as parent value, or as return value).
If you wish to automate this, you can either use the codegen programmatically (https://graphql-code-generator.com/docs/getting-started/programmatic-usage), or you can also create a .js file instead of .yaml file that will create the config section according to your needs.

Related

How to explicitly allow additional fields when using stripUnknown in Yup?

I have generic framework code that validates incoming requests using Yup with stripUnknown: true so that excess fields are removed. However, I have one place where I explicitly want to allow any JSON object as input.
How can I explicitly allow one object within a schema to have any fields while otherwise using stripUnknown: true?
Things I've considered but haven't figured out how to implement:
Use yup.object().test(...) or similar to explicitly allow the object
Use yup.addMethod to add a method to yup.object() which would short-circuit the stripping
Use yup.lazy to generate a schema which allows anything (but the type should allow nested JSON, not only top-level fields)
Add a new top-level type yup.anyObject() which would allow any object
Sandbox example
Allowing (and keeping) any value is actually as simple as:
const schema = yup.object().shape({
json: yup.mixed()
});
This allows any value, not just an object. If you want to validate that json is an object containing anything, you can use yup.lazy to map it into a schema having yup.mixed() for every key existing in the object:
const schema = yup.object().shape({
json: yup.lazy((value) =>
yup
.object()
.shape(
Object.keys(value).reduce(
(map, key) => ({ ...map, [key]: yup.mixed() }),
{}
)
)
)
});

Is there a Deconstruct Mongo Response to DTO short cut?

If I have a table in a mongoDB with five properties and I only want to return four of them and none of the mongo added info such as v1 I can map the reposne to a dto like so,
const product = await this.productModel.findById(productId).exec()
return { id: product.id, title: product.title }
Is there a deconstruct shortcut for the return, to extract every field from an interface (Product) from the product response, to save typing each property out ? If for example im retunring 127 properties from a table of entires with 140.
interface Product {
id: string
title: string
...
}
Unfortunately no, typescript interfaces do not really exist when your program compiles
Interface is a structure that defines the contract in your application. It defines the syntax for classes to follow. Classes that are derived from an interface must follow the structure provided by their interface.
The TypeScript compiler does not convert interface to JavaScript. It
uses interface for type checking. This is also known as "duck typing"
or "structural subtyping".
So, you can't really read interface fields and then write some logic (you can maybe achieve this through reflection but it's a bad practice)
An alternative is to explicitly define what fields are to include/or exclude from your object
Suppose that I have an object with this interface:
interface Foo {
field1: string;
field2: string;
field3: string;
.....
field140: string;
}
What you can do here is to define what properties you want to exclude (you take the exclude approach here since you are returning 127 fields of 140)
// This isn't an implementation with mongoose (if you are using it),
// it's just to give you the idea
const FIELDS_TO_EXCLUDE = ["field128", "field129", "field130", ..., "field140"];
productModel.toDTO(){
const documentData = this;
FIELDS_TO_EXCLUDE.forEach(x => delete documentData[x]);
return documentData;
}
In this way, when you will execute the toDTO function your manipulate itself excluding (or including) the fields you want

Common fields on graphql interface type in react apollo with a graphene backend

I have a python graphene-django backend with an interface and two types, let's say
interface InterfaceType {
id: ID!
name: String!
}
type Type1 implements InterfaceType {
aField: String
}
type Type2 implements InterfaceType {
anotherField: String
}
I'm able to query this from my react-apollo frontend using inline fragments:
query {
interfaceQuery {
...on Type1 {
id
name
}
...on Type1 {
id
name
}
}
}
But from what I understand it should also be possible to query the common fields simply as
query {
interfaceQuery
id
name
}
}
When I try this, however, I get the error Cannot query field "id" on type "InterfaceType". Did you mean to use an inline fragment on "Type1" or "Type2"?
I'm using an IntrospectionFragmentMatcher.
Am I misunderstanding and this kind of simple access of common fields is not possible, or is it just not implemented in either graphene or apollo?
If you're seeing that error, it's coming from the server, not Apollo. However, there's nothing specific to either Apollo or Graphene that should prevent you from querying the interface fields without a fragment.
The error is thrown because whatever schema you're using literally doesn't have that field on the provided type. Maybe you updated your schema without restarting the service, or were mistaken about what fields were actually part of the interface -- it's hard to know with only pseudocode provided.

Populate a query in Mongoose with Schema First approach and NestJS

First off I want to say this question is similar to this one which references this one. I have the exact same question as the second link except a notable difference. I'm trying to extend a class generated by NestJS which defines a property.
I'm using NestJs with the Schema first approach found here. I'm also generating a classes file based on my GraphQL Schema.
Here is the Schema:
type Location {
name: String!
owner: User!
}
Which generates the class:
export class Location {
name: string;
owner: User;
}
Now, I want to extend this class so I don't have to repeat the data (there are a lot more fields not shown). I also I want to add fields that live on a document but are not in the schema (_id in this example). Here is my LocationDocument and my schema.
export interface LocationDocument extends Location, Document {
_id: Types.ObjectId
}
export const LocationSchema: Schema = new Schema(
{
name: {
type: String,
required: true,
},
owner: {
type: Types.ObjectId,
ref: 'User',
}
);
Now here is my issue. The generated Location class from the GraphQL schema defines the owner property as a User type. But in reality it's a just a mongodb id until it is populated by Mongoose. So it could be a Types.ObjectId or a User on a UserDocument. So I attempted to define it as:
export interface LocationDocument extends Location, Document {
_id: Types.ObjectId
owner: User | Types.ObjectId;
}
But this throws an error in the compiler that LocationDocument incorrectly extends Location. This makes sense. Is there any way to extend the User Class but say that owner property can be a User Type (once populated by Mongoose) or a mongo object ID (as is stored in the database).
I decided that having a property that can be both types, while easy with Mongoose and JS, isn't the typed way. In my schema I have an owner which is a User type. In my database and the document which extends it, I have an OwnerId. So to people accessing the API, they don't care about the ownerId for the relationship. But in my resolver, I use the Id. One is a Mongo ID type, the other is a User type.

Defining an API with swagger: GET call that uses JSON in parameters

I am trying to create a proper, REST API, and document it with Swagger (2.0).
So, I have an API call that is a query, ie, it makes no changes and doesn't create anything (idempotent and safe). But it requires passing in a complex JSON parameter (list of items, 2 or 3 sets of addresses, etc). So I'm doing a GET with a parameter thats URL encoded JSON. That seems like the proper way to do it.
I see so often API's like this where they do it as a POST for this reason, but that's an incorrect use of the POST verb.
I'm seeing lots of swagger API's that do this...
I can't figure out if there's a way to do a proper rest API with Swagger, using a JSON parameter. You can define the parameter as a string, of course, and pass your encoded JSON into it, but then the swagger tooling doesn't understand that there's a schema/definition for it.
Is swagger not able to properly document this kind of call?
OpenAPI 2.0 (Swagger 2.0)
OpenAPI 2.0 does not support objects in query strings, it only supports primitive values and arrays of primitives. The most you can do is define your parameter as type: string, add an example of a JSON value, and use description to document the JSON object structure.
swagger: '2.0'
...
paths:
/something:
get:
parameters:
- in: query
name: params
required: true
description: A JSON object with the `id` and `name` properties
type: string
example: '{"id":4,"name":"foo"}'
OpenAPI 3.x
JSON in query string can be described using OpenAPI 3.x. In OAS 3, query parameters can be primitives, arrays as well as objects, and you can specify how these parameters should be serialized – flattened into key=value pairs, encoded as a JSON string, and so on.
For query parameters that contain a JSON string, use the content keyword to define a schema for the JSON data:
openapi: 3.0.1
...
paths:
/something:
get:
parameters:
- in: query
name: params
required: true
# Parameter is an object that should be serialized as JSON
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
This corresponds to the following GET request (before URL encoding):
GET /something?params={"id":4,"name":"foo"}
or after URL encoding:
GET /something?params=%7B%22id%3A4%2C%22name%22%3A%22foo%22%7D
Note for Swagger UI users:
Parameters with content are supported in Swagger UI 3.23.8+ and Swagger Editor 3.6.34+.
Workaround for earlier versions of UI/Editor:
Define the parameter as just type: string and add an example of the JSON data. You lose the ability to describe the JSON schema for the query string, but "try it out" will work.
parameters:
- in: query
name: params
required: true
schema:
type: string # <-------
example: '{"id":4,"name":"foo"}' # <-------
For .Net and Swashbuckle (tested on 3.0)
I have a generic class JsonModelBinder that implements IModelBinder interface. The class is used like this:
public IActionResult SomeAction(
[FromRoute] int id,
[FromQuery][ModelBinder(BinderType = typeof(JsonModelBinder<SomeModel>))] SomeModelquery query) => {}
I have created Operation filter that does the following:
Removes parameters created by Swashbuckle from properties of my model
Add query parameter of type string
As a result in Swagger I have a text field where I can insert json and test requests
public class JsonModelBinderOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.Parameters == null || context.ApiDescription.HttpMethod != HttpMethod.Get.ToString())
return;
//Find json parameters
var jsonGetParameters = context.ApiDescription.ActionDescriptor.Parameters.Cast<ControllerParameterDescriptor>()
.Where(p => p.ParameterInfo.CustomAttributes.Any(c => c.AttributeType == typeof(ModelBinderAttribute) && c.NamedArguments.Any(IsJsonModelBinderType))).ToArray();
if (jsonGetParameters.Length > 0)
{
//Select parameters names created by Swagger from json parameters
var removeParamNames = new HashSet<string>(context.ApiDescription.ParameterDescriptions.Where(d => jsonGetParameters.Any(p => p.Name == d.ParameterDescriptor.Name)).Select(p => p.Name));
//Create new Swagger parameters from json parameters
var newParams = jsonGetParameters.Select(p => new NonBodyParameter()
{
In = "query",
Name = p.Name,
Type = "string",
Description = "Json representation of " + p.ParameterType.Name
});
//Remove wrong parameters and add new parameters
operation.Parameters = operation.Parameters.Where(p => p.In != "query" || !removeParamNames.Contains(p.Name)).Concat(newParams).ToList();
}
}
private static bool IsJsonModelBinderType(CustomAttributeNamedArgument arg)
{
var t = arg.TypedValue.Value as Type;
return t != null && t.GetGenericTypeDefinition().IsAssignableFrom(typeof(JsonModelBinder<>));
}
}
Notes:
I use IsAssignableFrom because I have classes derived from JsonModelBinder. You can omit it if you don't inherit
You can also omit GetGenericTypeDefinition if your binder is not generic
This solution doesn't check for parameter name collision, though you should never have it if the API made with common sense