Creating nested models on the fly using types.reference in mobx-state-tree - mobx-state-tree

I have a several models that reference each other. Here is an example.
export const AlbumModel = types
.model("Album")
.props({
id: types.identifier,
name: types.string,
artists: types.array(ArtistModel),
uri: types.string,
releaseDate: types.maybe(types.string),
images: types.array(types.maybe(ImageModel))
})
export const ArtistModel = types
.model("Artist")
.props({
id: types.identifier,
name: types.string,
uri: types.string
})
export const ImageModel = types
.model("Image")
.props({
url: types.identifier,
width: types.maybe(types.number),
height: types.maybe(types.number)
})
Then, when I go to create an album, I do something like this:
Album.create({
id: '12345',
name: 'My Cool Album',
artists: [{
id: '67890',
name: 'Mr. Bean',
uri: 'https://my-cool-site.com/artist/mr-bean'
}],
uri: 'https://my-cool-site.com/album/my-cool-album',
releaseDate: '12-12-2012',
images: [{
url: 'https://my-cool-site.com/pic/1'
}]
})
This works fine, but when I fetch data from a service, I get a bunch of JSON I want to stick into the mst. When I do that as is, I am creating duplicate artists and images.
Question
Is there a (simple-ish) way I can create the artists and images on the fly, and pass them as reference to the Album model. Or do I need to write a separate function that will create the images and artists, stick them into the tree, and then reference them as a part of the album?
What I tried:
export const AlbumModel = types
.model("Album")
.props({
id: types.identifier,
name: types.string,
artists: types.array(types.reference(ArtistModel)),
uri: types.string,
releaseDate: types.maybe(types.string),
images: types.array(types.maybe(types.reference(ImageModel)))
})
which throws the following error:
at path "/images/0" snapshot `{"height":300,"url":"https://my-cool-site.com/pic/1","width":300}` is not assignable to type: `(reference(Image) | undefined)` (No type is applicable for the union), expected an instance of `(reference(Image) | undefined)` or a snapshot like `(reference(Image) | undefined?)` instead.

Not sure if this will work but it's worth trying
If you put this in AlbumModel artists
types.reference(ArtistModel, {
// given an identifier, find the user
get(artist /* string */, parent: any /*AlbumModel*/) {
return parent.artists.find(a => a.id === artist.id) || ArtistModel.create(data)
},
// given a user, produce the identifier that should be stored
set(artist /* ArtistModel */) {
return artist.id
}
})
The idea is initially it tries to find if the artist already exists, if not it will create a new ArtistModel.
Please let me know if this works :)

Related

Prisma2 Error: Invalid `prisma.post.create()` invocation: Unknown arg `tags` in data.tags for type PostUncheckedCreateInput

I want to create a post with a list of tags attached to it. The models are connected many-to-many (one post can have several tags, and one tag can have several posts in it).
Here are my prisma models:
model Post {
id String #id #default(cuid())
slug String #unique
title String
body String
tags Tag[]
}
model Tag {
id String #id #default(cuid())
posts Post[]
name String
slug String #unique
}
And here's a mutation where I'm trying to create a post, and attach tags to it:
t.field('createPost', {
type: 'Post',
args: {
title: nonNull(stringArg()),
body: stringArg(),
tags: list(arg({ type: 'TagInput' }))
},
resolve: async (_, args, context: Context) => {
// Create tags if they don't exist
const tags = await Promise.all(
args.tags.map((tag) =>
context.prisma.tag.upsert({
create: omit(tag, "id"),
update: tag,
where: { id: tag.id || "" },
})
)
)
return context.prisma.post.create({
data: {
title: args.title,
body: args.body,
slug: `${slugify(args.title)}-${cuid()}`,
// Trying to connect a post to an already existing tag
// Without the "tags: {...} everything works
tags: {
set: [{id:"ckql6n0i40000of9yzi6d8bv5"}]
},
authorId: getUserId(context),
published: true, // make it false once Edit post works.
},
})
},
})
This doesn't seem to be working.
I'm getting an error:
Invalid `prisma.post.create()` invocation:
{
data: {
title: 'Post with tags',
body: 'Post with tags body',
slug: 'Post-with-tags-ckql7jy850003uz9y8xri51zf',
tags: {
connect: [
{
id: 'ckql6n0i40000of9yzi6d8bv5'
}
]
},
}
}
Unknown arg `tags` in data.tags for type PostUncheckedCreateInput. Available args:
type PostUncheckedCreateInput {
id?: String
title: String
body: String
slug: String
}
It seems like the tags field on the post is missing? But I did run prisma generate and prisma migrate. Also I can successfully query tags on a post if I add them manually using Prisma Studio. What could be causing this issue?
You need to use connect for the author as well. So the following will work fine:
return context.prisma.post.create({
data: {
title: args.title,
body: args.body,
slug: `${slugify(args.title)}-${cuid()}`,
// Trying to connect a post to an already existing tag
// Without the "tags: {...} everything works
tags: {
set: [{id:"ckql6n0i40000of9yzi6d8bv5"}]
},
author: { connect: { id: getUserId(context) } },
published: true, // make it false once Edit post works.
},
})
In my case, the issue arose when I created a new field on the prisma model called uid and tried to run the command prisma migrate dev
It brought the error
Error:
⚠️ We found changes that cannot be executed:
• Step 0 Added the required column `uid` to the `Transactions` table without a default value. There are 1 rows in this table, it is not possible to execute this step.
You can use prisma migrate dev --create-only to create the migration file, and manually modify it to address the underlying issue(s).
Then run prisma migrate dev to apply it and verify it works.
I solved it by adding the #default("") to it.
model Transactions {
id Int #id #default(autoincrement())
uid String #default("")
account String
description String
category String
reference String
currency String #default("GBP")
amount String
status String
transactionDate String
createdAt String
updatedAt String
}

How to defined non-null elements inside an array in GraphQL Nexus?

I'm using GraphQL Nexus to implement my GraphQL schema.
The target GraphQL type I want to create is this:
input UserCreateInput {
email: String!
name: String
posts: [PostCreateInput!]!
}
However, I'm not sure how I can create the PostCreateInput array such that the elements of the posts are are required as well.
Right now this is what I have:
input UserCreateInput {
email: String!
name: String
posts: [PostCreateInput]!
}
Which is backed by this Nexus type definition:
const UserCreateInput = inputObjectType({
name: 'UserCreateInput',
definition(t) {
t.nonNull.string('email')
t.string('name')
t.nonNull.list.field('posts', {
type: 'PostCreateInput',
})
},
})
Is there a way how I can tell Nexus that each array element should be non-null?
In this case, adding a nonNull after the list should suffice. So something like the following:
const UserCreateInput = inputObjectType({
name: 'UserCreateInput',
definition(t) {
t.nonNull.string('email')
t.string('name')
t.nonNull.list.nonNull.field('posts', {
type: 'PostCreateInput',
})
},
})

Gatsby custom source one to many

I'm following the source plugin tutorial, and all goes well until I try to link my nodes.
My data structure is the following:
type Genre implements Node {
id: ID!
itemsCount: Int
imageUrl: String!
name: String!
items: [Movie]
}
type Movie implements Node {
id: ID!
advisory: [String]!
cast: [String]!
title: String!
}
in my data fetching I can then simply do
const genres = fetch(api/genres); // genres have genres.items which is an array of movies
genres.forEach(genre => createNode({ ...genre, id: createNodeId(genre.id) }));
genres.items.forEach((movie => createNode({ ...movie, id: createNodeId(movie.id) }));
All of the data is added correctly and I can query allGenres and allMovies ... but when I try to query genre items it returns null;
items contains array of ids only, not array of Movies
[Naively - inspired by Apollo local] I would expect to
collect movies into an id indexed object.
for each genre
convert items into array of Movies (create movie nodes [if not exists])
create genre node
... but probably it's enough [I'm naive?] to create Movie nodes first and annotate item: [Movie] #link(from: "Movie___NODE").
You have to dig into API or internals of some source plugins or just search on gatsby issues for examples.
At the end it probably should look like:
...genres.map(genre => {
const content = {
itemsCount: genre.itemsCount,
imageUrl: genre.imageUrl,
name: genre.name,
["items___NODE"]: genre.items.map(id =>
createNodeId(`Movie{${id}}`),
),
};
const id = createNodeId(`Genre{${genre.id}}`);
const nodeContent = JSON.stringify(content);
createNode({
...content,
id,
parent: null,
children: [],
internal: {
type: `Genre`,
content: nodeContent,
contentDigest: crypto
.createHash("md5")
.update(nodeContent)
.digest("hex"),
},
});
});
It's inspired by this use case (self type referencing).
So it turns out what I can do is:
type Genre implements Node {
id: ID!
itemsCount: Int
imageUrl: String!
name: String!
items: [Movie]! #link
}
type Movie implements Node {
id: ID!
advisory: [String]!
cast: [String]!
title: String!
genre: Genre #link(by: "title")
}
and then something like:
genres.forEach(genre => {
// save movie ids from map
const movies = genres.items.map((movie => {
// create movie node - assign genre title as a field on movie
createNode({ ...movie, genre: genre.title })
return movie.id;
});
// create genre node - assign items as an array of ids from movies
createNode({ ...genre, items: movies });
});
There was no need to use gatsby id as my id's were unique already. Here my linking is done from Genre to Movie using #link ( it defaults to using id as the key ) and then from Movie to Genre using #link(by: "title") where I have specified to use the title attribute as the key

Hapijs Joi reference schema keys to reuse in other models or in routes

I have an example model constructed like so:
const MyModelResponse = Joi.object().keys({
id: Joi.number().integer().required()
.description('ID of the example model'),
description: Joi.string()
.description('Description of the example model'),
})
.description('example instance of MyModel with the unique ID present')
.label('MyModelResponse');
In my route, I want to make sure that my input parameter is validated against the id property of MyModeResponse like so:
validate: {
params: {
id: MyModelResponse.id,
},
options: { presence: 'required' },
failAction: failAction('request'),
}
This results in the schema validation error upon server start:
AssertionError [ERR_ASSERTION]: Invalid schema content: (id)
Is there a way to reference a key of a schema? Currently, I have to resort to either of the following:
Not referencing my MyModelResponse schema at all:
validate: {
params: {
id: Joi.number().integer().description('id of MyModel instance to get'),
},
options: { presence: 'required' },
failAction: failAction('request'),
}
Not using the Joi.object.keys() constructor by defining my model like this:
const MyModelResponse = {
id: Joi.number().integer().required()
.description('ID of the example model'),
description: Joi.string()
.description('Description of the example model'),
}
The bottom approach allows me to reference the id property in my route but doesn't allow me to add descriptions and labels to my schema. I have tried using MyModel.describe().children.id in my route validation and I have made some attempts to deserialize the id object into a schema object to no avail.
Any suggestions would be appreciated.
Remove the keys() and use as follows
const MyModelResponse = Joi.object({
id: Joi.number().integer().required()
.description('ID of the example model'),
description: Joi.string()
.description('Description of the example model'),
})
.description('example instance of MyModel with the unique ID present')
.label('MyModelResponse');

Can you make a graphql type both an input and output type?

I have some object types that I'd like to use as both input and output - for instance a currency type or a reservation type.
How do I define my schema to have a type that supports both input and output - I don't want to duplicate code if I don't have to. I'd also prefer not to create duplicate input types of things like currency and status enums.
export const ReservationInputType = new InputObjectType({
name: 'Reservation',
fields: {
hotelId: { type: IntType },
rooms: { type: new List(RoomType) },
totalCost: { type: new NonNull(CurrencyType) },
status: { type: new NonNull(ReservationStatusType) },
},
});
export const ReservationType = new ObjectType({
name: 'Reservation',
fields: {
hotelId: { type: IntType },
rooms: { type: new List(RoomType) },
totalCost: { type: new NonNull(CurrencyType) },
status: { type: new NonNull(ReservationStatusType) },
},
});
In the GraphQL spec, objects and input objects are distinct things. Quoting the spec for input objects:
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... 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.
An Input Object defines a set of input fields; the input fields are either scalars, enums, or other input objects. This allows arguments to accept arbitrarily complex structs.
While an implementation might provide convenience code to create an object and a corresponding input object from a single definition, under the covers, the spec indicates that they'll have to be separate things (with separate names, such as Reservation and ReservationInput).
While working on a project I had a similar problem with code duplication between input and type objects. I did not find the extend keyword very helpful as it only extended the fields of that specific type. So the fields in type objects cannot not be inherited in input objects.
In the end I found this pattern using literal expressions helpful:
const UserType = `
name: String!,
surname: String!
`;
const schema = graphql.buildSchema(`
type User {
${UserType}
}
input InputUser {
${UserType}
}
`)
You can do something like this:
export const createTypes = ({name, fields}) => {
return {
inputType: new InputObjectType({name: `${name}InputType`, fields}),
objectType: new ObjectType({name: `${name}ObjectType`, fields})
};
};
const reservation = createTypes({
name: "Reservation",
fields: () => ({
hotelId: { type: IntType },
rooms: { type: new List(RoomType) },
totalCost: { type: new NonNull(CurrencyType) },
status: { type: new NonNull(ReservationStatusType) }
})
});
// now you can use:
// reservation.inputType
// reservation.objectType
this is something that i did for my project (works good):
const RelativeTemplate = name => {
return {
name: name,
fields: () => ({
name: { type: GraphQLString },
reference: { type: GraphQLString }
})
};
};
const RelativeType = {
input: new GraphQLInputObjectType(RelativeTemplate("RelativeInput")),
output: new GraphQLObjectType(RelativeTemplate("RelativeOutput"))
};

Resources