Sequelize async beforeCreate hook giving me an error while setting many to many association - async-await

I am using sequelize-typescript in this app and I have a model that currently is trying to hash a password before saving it
account.model.ts
#Table({
freezeTableName: true,
tableName: 'accounts',
})
export default class Account extends Model {
// ... other fields
#Length({ min: 0, max: 255 })
#Column({
comment:
'optional for social login users and compulsory for users signing up with an email password combination',
})
password: string;
// ...more fields
#BeforeUpdate
#BeforeCreate
static async hashPassword(instance: Account) {
instance.password = await buildHash(instance.password);
}
}
My buildHash function is asynchronous in nature and looks like this
import hasher from 'argon2';
const buildHash = (item) => hasher.hash(item);
I am getting an accountId not exists error when I try setting users on the account object
account.service.ts
static async create(createBody: {
email: string;
authenticationTypeId: string;
emailVerified?: boolean;
isPrimary?: boolean;
password?: string;
pictureUrl?: string;
socialAccountId?: string;
username?: string;
users?: User[];
}) {
console.log(createBody);
const createdAccount: Account = new Account(createBody);
console.log(createdAccount);
if (createBody.users) createdAccount.$set('users', createBody.users);
return createdAccount.save();
}
Very specifically it has an error at this line saying accountId does not exist
if (createBody.users) createdAccount.$set('users', createBody.users);
If I replace my hasher with a sync function from bcrypt, it works perfectly
import hasher from 'bcrypt';
const buildHash = (item) => hasher.hashSync(item, 12);
The model hook changed appropriately when using bcrypt
#BeforeUpdate
#BeforeCreate
static hashPassword(instance: Account) {
instance.password = buildHash(instance.password);
}
Unfortunately argon2 doesnt have a SYNC function and I dont want to change my hashing algorithm back to bcrypt
Can someone kindly tell me how I can wait for the new Account to be created before attempting to update the many to many relationship?

Related

Can't insert into table with RLS despite being logged in?

I want to add a row to a profiles table in the public schema immediately after the user has signed up and their auth user has been created in the database.
The user gets created successfully, and I can see the details of the user in the returned user object from the supabase.auth.signUp function.
However when I try to do a manual insert to the table, I get an error back saying the RLS on the table is being violated. However the user is logged in and the uid is correct, so why won't it let me insert data?
async function handleSubmit(e: any) {
e.preventDefault()
const email = emailRef.current.value;
const password = passwordRef.current.value;
// const navigate = useNavigate();
const {user, error} = await signUp({email, password});
if(error){
alert("Error signing in");
} else {
await addUserDetails({
uuid: user.id,
firstName: firstNameRef.current.value,
lastName: lastNameRef.current.value
});
navigate("/dashboard");
}
}
return //rest of component
}
export async function addUserDetails({
uuid, firstName, lastName
}){
const {data, error } = await supabase.from("profiles").insert([{
id: uuid,
first_name: firstName,
last_name: lastName
}])
}
RLS on table
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
Try this:
const {data, error } = await supabase.from("profiles").insert([{
id: uuid,
first_name: firstName,
last_name: lastName
}],{ returning: "minimal" })
I was stuck on this for 2 days. It turns out it's because I was running Supabase in a Node test environment, and Supabase silently fails to setup a session and user id when not in a browser environment like Chromium or jsdom.
You can fix it by using a browser environment like jsdom for your testing environment or just using Playwright.
Rough example:
// #vitest-environment jsdom
test('use jsdom in this test file', () => {
const expectedName = 'Sam';
await supabase.from("profiles").insert([{
id: uuid,
first_name: expectedName
}]);
const { data: profile } = await supabase.from("profiles")
.select()
.eq( 'id', uuid )
expect( profile.first_name ).toEqual( expectedName )
});
In Vitest:
https://vitest.dev/config/#environment
In Jest:
https://jestjs.io/docs/configuration#testenvironment-string

GraphQL - defining types

I'm trying to follow Ben Awad's lireddit tutorial.
At the time he made the tutorial, there may have been different inferences about Field types.
I'm trying to add a Field to my relationship attribute (adding creator Field to a Post), so that I can then access creator attributes on that post record.
Ben does this as follows:
#Field()
#ManyToOne(() => User, (user) => user.posts)
creator: User;
That worked for him. When I try this, I get an error that says:
throw new errors_1.NoExplicitTypeError(prototype.constructor.name, propertyKey,
parameterIndex, argName);
NoExplicitTypeError: Unable to infer GraphQL type from TypeScript
reflection system. You need to provide explicit type for 'creator' of
'Post' class.
When I look at the GraphQL docs for how to provide an explicit type for creator, I can't find a similar example (simple enough for me to decipher a principle that I can apply).
I'm confused by the docs, because the have the following example:
Can anyone see what I need to do to ask for the field to be recognised as an object that I can read from?
#ObjectType()
class Rate {
#Field(type => Int)
value: number;
#Field()
date: Date;
user: User;
}
I think they use user: User the same way I use creator: User. Is there a reason that Field() can't have the same thing as ObjectType()?
I tried:
#Field(() => [User])
#ManyToOne(() => User, (user) => user.posts)
creator: User;
This doesn't give any errors (neither does the code the way Ben has it), until I get to the playground, in which case, I can't return the user data - so clearly it's wrong. It also isn't clear whether the array means the array of attributes on the user object, or an array of users (which would also be wrong). I can see from the GraphQL docs that it should be possible to define a field attribute as an object type, but I can't find an example showing how to do that.
I have seen this post, which looks like a similar problem, but I can't see from the suggested answers, how to apply those ideas to this problem.
I have seen this post, which has a similar problem, and is answered with a reference to an example that shows how to write resolvers that find relations, but my resolver already worked to find the creatorId, so I think maybe I'm not looking in the right place for an answer.
In my post resolver, I have:
import {
Resolver,
Query,
Arg,
Mutation,
InputType,
Field,
Ctx,
UseMiddleware,
Int,
FieldResolver,
Root,
ObjectType,
} from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
import { isAuth } from "../middleware/isAuth";
import { getConnection } from "typeorm";
#InputType()
class PostInput {
#Field()
title: string;
#Field()
text: string;
}
#ObjectType()
class PaginatedPosts {
#Field(() => [Post])
posts: Post[];
#Field()
hasMore: boolean;
}
#Resolver(Post)
export class PostResolver {
#FieldResolver(() => String)
textSnippet(#Root() post: Post) {
return post.text.slice(0, 50);
}
#Query(() => PaginatedPosts)
async posts(
#Arg("limit", () => Int) limit: number,
#Arg("cursor", () => String, { nullable: true }) cursor: string | null
): Promise<PaginatedPosts> {
// 20 -> 21
const realLimit = Math.min(50, limit);
const reaLimitPlusOne = realLimit + 1;
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
if (cursor) {
qb.where('"createdAt" < :cursor', {
cursor: new Date(parseInt(cursor)),
});
}
const posts = await qb.getMany();
return {
posts: posts.slice(0, realLimit),
hasMore: posts.length === reaLimitPlusOne,
};
}
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id);
}
#Mutation(() => Post)
#UseMiddleware(isAuth)
async createPost(
#Arg("input") input: PostInput,
#Ctx() { req }: MyContext
): Promise<Post> {
return Post.create({
...input,
creatorId: req.session.userId,
}).save();
}
#Mutation(() => Post, { nullable: true })
async updatePost(
#Arg("id") id: number,
#Arg("title", () => String, { nullable: true }) title: string
): Promise<Post | null> {
const post = await Post.findOne(id);
if (!post) {
return null;
}
if (typeof title !== "undefined") {
await Post.update({ id }, { title });
}
return post;
}
#Mutation(() => Boolean)
async deletePost(#Arg("id") id: number): Promise<boolean> {
await Post.delete(id);
return true;
}
}
First of all, creator should be of a User type, and not a list of users, i.e.
#Field(() => User)
#ManyToOne(() => User, (user) => user.posts)
creator: User;
When you are retrieving a post, you should include a relation in your query, so the User entity is also loaded:
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id, { relations: "creator" });
}
Also, when you're using query builder to fetch posts, you should add User entities:
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.leftJoinAndSelect("p.creator", "p_creator")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
Bonus note:
There's a common problem of over-fetching the data in GraphQL, so queries can become slow with time.
In that manner, you could also consider moving the creator field to FieldResolver, so it's retrieved from the database only if it's requested. In case you do that, one other good practice with ManyToOne relations is to use a dataloader, so if you, for example, load 10 posts from the same creator, you'll end up with only one fetching operation of that creator instead of 10 requests to the database. There's a great tutorial and explanation provided by Ben Awad too: https://www.youtube.com/watch?v=uCbFMZYQbxE.
This isn't necessary for this tutorial in particular, but it's a must-know if you're building some serious app.

How to validate a DTO fields?

I have an endpoint with no entry params:
async myendpoint(): Promise<any> {
const customer = await this.customerService.findOne(1);
if (customer) {
return await this.customerService.mapToDestination(customer);
}...
}
Then I have my method mapToDestination where I simply assign vars:
async mapToDestination(customer: Customer): Promise<DestinationDto> {
const destination: DestinationDto = {
lastname: customer.lastname,
firstname: customer.firstname,...
Finally, I have my DTO:
import {IsEmail, IsNotEmpty, IsOptional, IsNumber, IsBoolean, IsString, IsDate, MaxLength, Length, NotEquals} from 'class-validator';
import {ApiProperty} from '#nestjs/swagger';
export class DestinationDto {
#IsString()
#IsNotEmpty()
#MaxLength(32)
lastname: string;
#IsString()
#IsNotEmpty()
#MaxLength(20)
firstname: string; ...
I would like my DTO fields to be validated automatically following the decorators when I'm mapping it in my mapToDestination() method. I look through the web and the official documentation and I gave a try to Validators (ValidationPipe) but it does not seem to be my need as it validates the endpoint entry params.
Please, could you explain to me how to achieve this automatic validation? Thanks in advance.
I won't be "automatic" but you could instantiate your own instance of the validator from class validator and use it against the DTO in your service. Otherwise, it won't ever happen automatically because as you said, the ValidationPipe only works on the entry of the endpoint.
Example
Inside of mapToDestination so long as customer is an instance of DestinationDTO` you can have something like this:
#Injectable()
export class CustomerService {
async mapToDestination(customer: DestinationDTO) {
const errors = await validate(customer);
if (errors) {
throw new BadRequestException('Some Error Message');
}
...
}
...
}

setup multiple return types for the NestJs GraphQL Query decorator

I want to create a GraphQL API using NestJs. As far as I understood I won't be throwing HTTP exceptions for invalid requests anymore. Therefore I think I have to create my own "error codes" I can send back to the client. So given this basic example
#ObjectType()
export class ErrorResponse {
#Field()
message: string;
}
I have a service function to return a user by its ID and I extended the return type to return an error object if the request was invalid.
public async getUserById(id: number): Promise<ErrorResponse | User> {
const user: User = await this.usersRepository.findOne(id);
if (!user) {
const errorResponse: ErrorResponse = new ErrorResponse();
errorResponse.message = `User with ID ${id} does not exist`;
return errorResponse;
}
return user;
}
The resolver originally was something like
#Query(() => User)
public async user(#Args('id') id: number): Promise<ErrorResponse | User> {
return this.usersService.getUserById(id);
}
but as mentioned above it's also possible to return a ErrorResponse if the id does not exist. How can I design the Query decorator to provide multiple return types?
#Query(() => ErrorResponse | User)
won't do the trick and shows up with this error
The left-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint' or an enum type.ts(2362)
This is the solution that i came up for a similar situation.
GraphQL expects single return ObjectType.
First i created a common Object
#ObjectType()
export class MutationResult {
#Field({ nullable: true })
success?: boolean;
#Field({ nullable: true })
error?: boolean;
}
Then in the user module i created 2 objects types - User and UserResponse. On UserResponse i extened the common MutationResult Object
#ObjectType()
export class User {
#Field(type => ID)
id: string;
#Field()
name: string;
}
#ObjectType()
export class UserResponse extends MutationResult {
#Field()
result: User;
}
Now in query you can do this
mutation {
addUser(name: "Test") {
success,
error,
result {
name
}
}
}
If both ErrorResponse and User are an #ObjectType, you just need to "merge" them together using createUnionType.
https://docs.nestjs.com/graphql/unions-and-enums
Answer by Michal seems to be working, but the link is redirecting to some spam post. Below link is official documentation for nestjs:
https://docs.nestjs.com/graphql/unions-and-enums

GraphQL subscriptions: Error on calling apolloClient.subscribe

I think I have the backend subscription setup correctly. I am using angular on the client side, when I try to call subscribe I got an error
passwordUpdatedSubscription = gql`
subscription passwordUpdated{passwordUpdated{name password}}
`;
// Apollo Subscription
var subscription = this.apollo.subscribe({
query: this.passwordUpdatedSubscription
});
subscription.subscribe(
{
next(data) {
console.log(data);
},
error(err) { console.error('err', err); },
}
);
And then this is the error appears in the console
{"type":"subscription_fail","id":0,"payload":{"errors":[{"message":"Cannot read property 'subscribe' of undefined"}]}}
Maybe I am missing something on the backend? Do I need to define the setupFunctions in the SubscriptionManager?
This is my SubscriptionManager
const sub = require('graphql-subscriptions');
const pubSub = new sub.PubSub();
const manager = new sub.SubscriptionManager({
schema,
pubSub
});
This is my schema in graphQL
const graphql = require('graphql');
var schema = graphql.buildSchema(`
type Subscription {
passwordUpdated: User
}
type Mutation {
setMessage(message: String): String,
updateUserPassword(userName: String, password: String): User!
}
type Query {
getMessage: String,
getUsers: [User],
findUsers(userName: String): [User]
}
type User {
name: String,
password: String
}
`);
Yes you are missing the setup function. You could take a look at this links graphql subscription docu or example.
Your subscription manager could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
passwordUpdated: (options, args) => ({ // name of your graphQL subscription
passwordUpdatedChannel: { // name of your pubsub publish-tag
filter: () => {
return true
},
},
}),
},
});
When you call the pubsub publish function you have to write it like this pubsub.publish("passwordUpdatedChannel").
Sidenode: You might want to add the id of the user that has the password changed to the subscription. If you do that you can add it to the filter option, could look like this filter: (user) => {return user.id === args.userId}

Resources