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

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');

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',
})
},
})

Creating nested models on the fly using types.reference in 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 :)

Prisma Not Returning Created Related Records

i want to create a new graphql api and i have an issue that i am struggling to fix.
the code is open source and can be found at: https://github.com/glitr-io/glitr-api
i want to create a mutation to create a record with relations... it seems the record is created correctly with all the expected relations, (when checking directly into the database), but the value returned by the create<YourTableName> method, is missing all the relations.
... so so i get an error on the api because "Cannot return null for non-nullable field Meme.author.". i am unable to figure out what could be wrong in my code.
the resolver looks like the following:
...
const newMeme = await ctx.prisma.createMeme({
author: {
connect: { id: userId },
},
memeItems: {
create: memeItems.map(({
type,
meta,
value,
style,
tags = []
}) => ({
type,
meta,
value,
style,
tags: {
create: tags.map(({ name = '' }) => (
{
name
}
))
}
}))
},
tags: {
create: tags.map(({ name = '' }) => (
{
name
}
))
}
});
console.log('newMeme', newMeme);
...
that value of newMeme in the console.log here (which what is returned in this resolver) is:
newMeme {
id: 'ck351j0f9pqa90919f52fx67w',
createdAt: '2019-11-18T23:08:46.437Z',
updatedAt: '2019-11-18T23:08:46.437Z',
}
where those fields returned are the auto-generated fields. so i get an error for a following mutation because i tried to get the author:
mutation{
meme(
memeItems: [{
type: TEXT
meta: "test1-meta"
value: "test1-value"
style: "test1-style"
}, {
type: TEXT
meta: "test2-meta"
value: "test2-value"
style: "test2-style"
}]
) {
id,
author {
displayName
}
}
}
can anyone see what issue could be causing this?
(as previously mentioned... the record is created successfully with all relationships as expected when checking directly into the database).
As described in the prisma docs the promise of the Prisma client functions to write data, e.g for the createMeme function, only returns the scalar fields of the object:
When creating new records in the database, the create-method takes one input object which wraps all the scalar fields of the record to be
created. It also provides a way to create relational data for the
model, this can be supplied using nested object writes.
Each method call returns a Promise for an object that contains all the
scalar fields of the model that was just created.
See: https://www.prisma.io/docs/prisma-client/basic-data-access/writing-data-JAVASCRIPT-rsc6/#creating-records
To also return the relations of the object you need to read the object again using an info fragment or the fluent api, see: https://www.prisma.io/docs/prisma-client/basic-data-access/reading-data-JAVASCRIPT-rsc2/#relations

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