setting type of index in for of loop in Typescript - node.js

I am trying to set the type of an item when I loop through a response which is an array of objects, however, I am not sure how to declare the type.
I have the following type:
export type Movie = {
title: string;
director: string;
year: string;
};
I get the following response from an api
const movies = [{
"movie": {
"title": 'The Dark Knight',
"director": 'Christofer Nolan',
},
"details": {
"year": 2008,
"rating": 4.5
}
},
{
"movie": {
"title": 'The Joker',
"director": 'Todd Phillips',
},
"details": {
"year": 2019,
"rating": 4.7
}
}
}]
I want to map the response to xml which I have the following function for
function mapToMoviesXML(movies: Movie[]) {
let data = `<?xml version="1.0" encoding="UTF-8"?>`;
data += `<movies>`;
for (let item of movies) {
data += `<movies>
<title>${item.movie.title}</title>
<director>${item.movie.director}</director>
<year>${item.details.year}</year>
<rating>${item.details.rating}</rating>
</movies>`;
}
however, I get the following error for item.movie and item.details within the loop
Property 'movie' does not exist on type 'Movie'
Property 'details' does not exist on type 'Movie'
I thought because I am getting the final value i.e. item.movie.title which is defined in the Movie type I would not need to declare a type for item. Any ideas what I need to change or update my type to?

To satisfy the way movies: Movie[] is being used (and to mirror what the API is actually sending) the type definition would need to be something along the lines of this:
export type Movie = {
movie: {
title: string;
director: string;
}
details: {
year: number;
rating: number;
};
};
or potentially more useful would be to break the nested objects into their own types and reference them from the parent object, i.e.
export interface MovieOverview {
title: string;
director: string;
}
export interface MovieDetails {
year: number;
rating: number;
}
export interface Movie {
movie: MovieOverview;
details: MovieDetails;
}

Related

How to make a request("query") to an API in graphql using Typescript in NodeJS

I need some guidance on how to make a request or query using graphql. I have done similar tasks using JSON, but in this case I am inexperienced using Graphql. I have the body code of the API query that I show below, with the 4 fields that I must pass as dynamic variables in each request. Do you know of any way to do it or some site that you can consult that clearly shows the procedure. Thanks in advance.
There are different parameters that can be set up in the request:
`checkIn: "2022-09-28",
checkOut: "2022-09-29",
occupancies: [{ paxes: [{age: 30}, {age: 30}] }],
hotels: ["1"],`
These are the interfaces I defined:
`
export interface SearchRQ{
checkIn:string;
checkOut:string;
occupancies: Occupancy[];
hotels: string[];
nationality: string
} export interface Occupancy {
paxes: Pax[];
}
export interface Pax {
age: number;
}`
*And this is a part of the body of the query:*
```
query {
hotelX {
search(
criteria: {
checkIn: "2022-12-28",
checkOut: "2022-12-29",
occupancies: [{ paxes: [{age: 30}, {age: 30}] }],
hotels: ["1","2"],
currency: "EUR",
market: "ES",
language: "es",
nationality: "ES"
},
settings: {
client: "client_demo",
context: "HOTELTEST",
auditTransactions: false,
testMode: true,
timeout: 25000
},
filter: {
access: {
includes: ["0"]
}
}) {
context
errors{
code
type
description
}
warnings{
code
type
description
}
options {
id
accessCode
supplierCode
hotelCode
hotelName
boardCode
paymentType
status
occupancies {
id
paxes {
age
}
}
rooms {
occupancyRefId
code
description
refundable
roomPrice {
price {
currency
binding
net
gross
exchange {
currency
rate
}
}
```

How to sync the schema change for old document collection in mongodb with default values

How can I apply the schema changes to sync with default value to all the old data in mongodb
import mongoose from "mongoose";
interface ITodo {
title: string;
description: string;
by: string;
}
interface todoModelInterface extends mongoose.Model<TodoDoc> {
build(attr: ITodo): TodoDoc;
}
interface TodoDoc extends mongoose.Document {
title: string;
description: string;
by: string;
}
const todoSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
by: {
type: String,
required: true,
},
});
todoSchema.statics.build = (attr: ITodo) => {
return new Todo(attr);
};
const Todo = mongoose.model<TodoDoc, todoModelInterface>("Todo", todoSchema);
Todo.build({
title: "some title",
description: "some description",
by: "special",
});
Todo.collection.dropIndexes(function () {
Todo.collection.reIndex(function (finished) {
console.log("finished re indexing");
});
});
Todo.collection
.getIndexes()
.then((indexes: any) => {
console.log("indexes:", indexes);
})
.catch(console.error);
export { Todo };
Db:
[{
"_id": {
"$oid": "62cee1eea60e181e412cb0a2"
},
"title": "one",
"description": "one desc"
},{
"_id": {
"$oid": "62cee2bd44026b1f85464d41"
},
"title": "one",
"description": "one desc",
"by": "alphs"
},{
"_id": {
"$oid": "62cee3c8cf1592205dacda3e"
},
"title": "one",
"description": "one desc",
"by": "alphs"
}]
Here the old data still missing the "by" key, similarly if there is nested schema change it may impact the old users, how can we define the default collection for old data in mongodb at runtime without using update query migration?
Have you tried setting the default value for "by". By giving a default value, if the old data is missing a value then the default will kick in and return the default value provided. Read about Mongoose Default: Here. I don't know if this is a good practice but we also use this method when there is change in schema and don't want to run update query.

How to save dynamic number of variables into database using graphql?

I'm trying to mutate and query dynamic variables. The user has the choice to add as many variables as they want before sending them off to the server. For example, my app is a productivity app that allows a user to add as many metrics as they want to track their goal so if "Gym" is their goal, the metrics would be "running", "bench press", etc. My problem is, I'm unsure how to save them in the database since there is no pre-configured Schema for these user-created variables.
I've managed to send the variables to the back end using the following:
mutation CreateGoal ($title: String!, $description: String, $metric: [Json!]) {
createGoal(
data: {
title: $title
description: $description
metric: { set: $metric }
}
){
id
}
}
Schema:
type Mutation {
createGoal(data: CreateGoalInput!): Goal!
}
input CreateGoalInput {
title: String!
description: String
metric: GoalCreatemetricInput
}
input GoalCreatemetricInput {
set: [Json!]
}
Once the variables arrive in the resolver, it's in the Json format:
{ set: [ 'running', 'bench press' ] }
Normally, I'd simply save the variables through Prisma:
async createGoal(parent, { data }, { request, prisma }, info) {
const { title, description, metric } = data && data
return prisma.mutation.createGoal({
data: {
user: {
connect: {
email: user.email
}
},
title,
description,
}
}, info)
},
However, since the number of variables are unknown, how do I save 'metric' into my database?
If I were to try the following:
async createGoal(parent, { data }, { request, prisma }, info) {
const { title, description, metric } = data && data
return prisma.mutation.createGoal({
data: {
user: {
connect: {
email: user.email
}
},
title,
description,
metric,
}
}, info)
},
I get the error:
Error: Variable "$_v0_data" got invalid value [ "running", "bench
press" ] at "_v0_data.metric"; Field "0" is not defined by type
GoalCreatemetricInput.
If I were to try:
async createGoal(parent, { data }, { request, prisma }, info) {
const { title, description, metric } = data && data
return prisma.mutation.createGoal({
data: {
user: {
connect: {
email: user.email
}
},
title,
description,
metric: metric.set
}
}, info)
},
I get the error:
Error: Variable "$_v0_data" got invalid value ["running", "bench
press"] at "_v0_data.metric"; Field "0" is not defined by type
GoalCreatemetricInput. Variable "$_v0_data" got invalid value
["Asdfasdf", "Asdfasdf"] at "_v0_data.metric"; Field "1" is not
defined by type GoalCreatemetricInput.
I don't think you need to use the Json scalar at all. It looks like you're trying to pass an array of strings so instead of [Json!] you may just need to use [String!].
input CreateGoalInput {
title: String!
description: String
metric: [String!]
}
Then you should be able to get rid of
input GoalCreatemetricInput {
set: [Json!]
}
Here you should be able to pass the array of strings to the backend:
mutation CreateGoal ($title: String!, $description: String, $metric: [String!]) {
createGoal(
data: {
title: $title
description: $description
metric: $metric
}
){
id
}
}
And in your resolover I think all you need to do is:
async createGoal(parent, { data }, { request, prisma }, info) {
const { title, description, metric } = data && data
return prisma.mutation.createGoal({
data: {
user: {
connect: {
email: user.email
}
},
title,
description,
metric: { set: metric },
}
}, info)
},

How to manage GraphQL child objectType that can be nullable in an output type?

I'm setting up a nodeJS GraphQL API and I'm experimenting a blocking point regarding one of my resource output type.
The feature is a form that contain three different level :
Level 1- formTemplate
Level 2- formItems (templateId, type (video, image, question) - 1-N relation with formTemplate)
Level 3- formQuestions (0-1 relation with formItem if and only if formItems.type is 'question')
My GraphQL resource is returning all the templates in the database so it's an array that for each template is returning all his items and each item of type "question" needs to return an array containing the associated question.
My problem is : I really don't know how to return an empty object type for the formItems where type is different from "question" or if there is a better approach for this kind of situation
I've tried to look at GraphQL directives and inline fragments but I think it really needs to be manage by the backend side because it's transparent for the API consumer.
const formTemplate = new GraphQLObjectType({
name: 'FormTemplate',
fields: () => {
return {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
authorId: {
type: new GraphQLNonNull(GraphQLInt)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
items: {
type: new GraphQLList(formItem),
resolve: parent => FormItem.findAllByTemplateId(parent.id)
}
}
}
})
const formItem = new GraphQLObjectType({
name: 'FormItem',
fields: () => {
return {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
templateId: {
type: new GraphQLNonNull(GraphQLInt)
},
type: {
type: new GraphQLNonNull(GraphQLString)
},
question: {
type: formQuestion,
resolve: async parent => FormQuestion.findByItemId(parent.id)
}
}
}
})
const formQuestion= new GraphQLObjectType({
name: 'FormQuestion',
fields: () => {
return {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
itemId: {
type: new GraphQLNonNull(GraphQLInt)
},
type: {
type: new GraphQLNonNull(GraphQLString)
},
label: {
type: new GraphQLNonNull(GraphQLString)
}
}
}
})
My GraphQL request :
query {
getFormTemplates {
name
items {
type
question {
label
type
}
}
}
}
What I'm expected is
{
"data": {
"getFormTemplates": [
{
"name": "Form 1",
"items": [
{
"type": "question",
"question": {
"label": "Question 1",
"type": "shortText"
},
{
"type": "rawContent"
"question": {}
}
]
}
]
}
}
I'd design your "level 2" items so that the "type" property corresponded to actual GraphQL types, implementing a common interface. Also, in general, I'd design the schema so that it had actual links to neighboring items and not their identifiers.
So if every form item possibly has an associated template, you can make that be a GraphQL interface:
interface FormItem {
id: ID!
template: FormTemplate
}
Then you can have three separate types for your three kinds of items
# Skipping VideoItem
type ImageItem implements FormItem {
id: ID!
template: FormTemplate
src: String!
}
type QuestionItem implements FormItem {
id: ID!
template: FormTemplate
questions: [FormQuestion!]!
}
The other types you describe would be:
type FormTemplate {
id: ID!
author: Author!
name: String!
items: [FormItem!]!
}
type FormQuestion {
id: ID!
question: Question
type: String!
label: String!
}
The other tricky thing is, since not all form items are questions, you have to specifically mention that you're interested in questions in your query to get the question-specific fields. Your query might look like
query {
getFormTemplates {
name
items {
__typename # a GraphQL builtin that gives the type of this object
... on Question {
label
type
}
}
}
}
The ... on Question syntax is an inline fragment, and you can similarly use it to pick out the fields specific to other kinds of form items.
Thank you David for your answer !
I've figured it out how to solve my problem using inline fragments and UnionTypes that seems to be the most adapted for this use case. Here is the code :
const formItemObjectType = new GraphQLUnionType({
name: 'FormItemObject',
types: [formItemContent, formItemQuestion],
resolveType(parent) {
switch (parent.type) {
case ('question'): return formItemQuestion
default: return formItemContent
}
}
})
and the GraphQL query using inline fragment:
query {
getFormTemplates {
name
items {
...on FormItemContent {
type,
meta
}
...on FormItemQuestion {
type,
meta,
question {
label
}
}
}
}
}

Use a GQLObject as arg for a mutation?

I have following mutation on serverside (nodeJS) (RequiredDataType is imported):
mutationA: {
type: MutationResponseType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
requiredData: {
type: new GraphQLNonNull(new GraphQLList(RequiredDataType))
}
},
async resolve(parentValue, {
id,
name,
requiredData
}, req) {
// Some Magic Code
}
},
The RequiredDataType is coded as follow (All GraphQL things are imported :)):
const RequiredDataType = new GraphQLObjectType({
name: 'RequiredDataType',
fields: {
name: {
type: GraphQLString
},
value: {
type: GraphQLString
},
required: {
type: GraphQLBoolean
}
}
});
module.exports = RequiredDataType;
When I use this code I get the following error: "module initialization error: Error"
If I change the RequiredDataType in the mutation to GraphQLString it works without any error but I can't use the object which I need :)
At the end I will send and process following data structure:
{
"name": "Hallo"
"id": "a54de3d0-a0a6-11e7-bf70-7b64ae72d2b6",
"requiredData": [
{
"name": "givenName",
"value": null,
"required": true
},
{
"name": "familyName",
"value": null,
"required": false
}
]
}
On the client (reactJS with apollo-client) I use the following gql-tag code:
export default gql`
mutation MutationA($id: String!, $name: String!, $requiredData: [RequiredDataType]!){
mutationA(id: $id, name: $name, requiredData: $requiredData) {
id,
somethingElse
}
}
`;
But in the first place it crashes on the mutation declaration on the server. So is it not possible to use and GQLObject as an argument at an mutation or where is my error in the code?
Thank you for your help!
Best,
Fabian
Unfortunately, a type cannot be used in place of an input, and an input cannot be used in place of a type. This is by design. From the official specification:
Fields can define arguments that the client passes up with the query,
to configure their behavior. These inputs can be Strings or Enums, but
they sometimes need to be more complex than this.
The Object type defined above is inappropriate for re‐use here,
because Objects can contain fields that express circular references or
references to interfaces and unions, neither of which is appropriate
for use as an input argument. For this reason, input objects have a
separate type in the system.
You can check this answer for more details as to the why
You'll need to define RequiredDataType as a GraphQLInputObjectType, not a GraphQLObjectType, to get your mutation working. If you need it as a GraphQLObjectType too, you'll need to declare them as two separate types -- something like RequiredDataType and RequiredDataInput.

Resources