I'm using the T3 stack (TypeScript, tRPC, Prisma, Next, etc) and I want to either load data from my database using a tRPC query, or use an empty array in my Zustand store. I keep getting an error saying:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug
and fix this problem. error - TypeError: Cannot read properties of
null (reading 'useContext')
(node:internal/process/task_queues:96:5) { page: '/' }
here's the code generating the error:
type Image = {
url: string;
prompt: string;
};
interface UserState {
images: Image[] | [];
isLoading: boolean;
addImage: (url: string, prompt: string) => void;
removeImage: (url: string, prompt: string) => void;
setLoading: () => void;
}
export const useStore = create<UserState>((set) => {
const { data: sessionData } = useSession();
const dbImages = trpc.images.list.useQuery({
limit: 20,
userId: sessionData?.user?.id ?? "",
}).data?.items;
return {
// initial state
images: dbImages ? dbImages : [],
isLoading: false,
// methods for manipulating state
addImage: (url, prompt) => {
set((state) => ({
images: [
...state.images,
{
url: url,
prompt: prompt,
} as Image,
],
}));
},
removeImage: (url: string) => {
set((state) => ({
images: state.images?.filter((x) => x.url !== url),
}));
},
setLoading: () => {
set((state) => ({
isLoading: !state.isLoading,
}));
},
};
});
What am I doing wrong here? I'm still in the learning phases and would appreciate best practices, etc.
Related
I'm trying to send a metadata from the server side gprc using NestJS framework. On the official NestJS guide here: it shows a example of doing the server side grpc metadata:
#Controller()
export class HeroesService {
#GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any>): Hero {
const serverMetadata = new Metadata();
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
serverMetadata.add('Set-Cookie', 'yummy_cookie=choco');
call.sendMetadata(serverMetadata);
return items.find(({ id }) => id === data.id);
}
}
On my code, I wrote a similar code:
#GrpcMethod('ClinicService', 'GetCustomerClinicNames')
async GetCustomerClinicNames_GRPC(data: CustomerClinicID, call:ServerUnaryCall<Any,Any>) {
const metadata = new Metadata();
const result = await this.clinicService.GetClinicName(Number(data.customer_id));
console.log(result);
metadata.add('context', "Jello")
call.sendMetadata(metadata)
return { response: result};
}
However, it gives me an error says:
[Nest] 53188 - 04/06/2022, 5:17:50 PM ERROR [RpcExceptionsHandler] call.sendMetadata is not a function
TypeError: call.sendMetadata is not a function
at ClinicController.GetCustomerClinicNames_GRPC (C:\Users\Vibrant\Desktop\core-samples\src\clinic\clinic.controller.ts:118:10)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at C:\Users\Vibrant\Desktop\core-samples\node_modules\#nestjs\microservices\context\rpc-proxy.js:11:32
But I think the .sendMetadata is indeed a function. Where am I wrong about this?
For the sendMetadata Function, it is defined as:
export declare type ServerSurfaceCall = {
cancelled: boolean;
readonly metadata: Metadata;
getPeer(): string;
sendMetadata(responseMetadata: Metadata): void;
getDeadline(): Deadline;
} & EventEmitter;
export declare type ServerUnaryCall<RequestType, ResponseType> = ServerSurfaceCall & {
request: RequestType;
};
So apparently you need to keep the metadata parameter in your async GetCustomerClinicNames_GRPC. I tried with and without and it only worked when I had it.
so for example, I tried:
#GrpcMethod('HeroesService', 'FindOne')
findOne(data: any, call: ServerUnaryCall<any, any>): any
and that didn't work, then I tried:
#GrpcMethod('HeroesService', 'FindOne')
findOne(data: any, _metadata: Metadata, call: ServerUnaryCall<any, any>): any
and it worked by doing something like:
#GrpcMethod('HeroesService', 'FindOne')
findOne(data: any, _metadata: Metadata, call: ServerUnaryCall<any, any>): any {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
const metadata = new Metadata();
metadata.add('context', 'Jello');
call.sendMetadata(metadata);
return items.find(({ id }) => id === data.id);
}
I'm not sure why though, since we are using named arguments.
I want to add a custom schema formatter for fastify.
import fastify from 'fastify'
import AjvCompiler from '#fastify/ajv-compiler'
const ajvFormatter = AjvCompiler(ajv);
ajvFormatter.addFormat('new-format', /hello/);
const app = fastify({
schemaController: {
compilersFactory: {
buildValidator: ajvFormatter
}
}
})
I add the format but still gives the error:
Failed building the validation schema for POST: /hey, due to error unknown format
"new-format" ignored in schema at path
I guess latest fastify does not support this functionality.
You are using in the wrong way the #fastify/ajv-compiler module. It does not accept an ajv input parameter at all. not it exports an addFormat method.
You need to use the customOption option:
const fastify = require('fastify')
const app = fastify({
logger: true,
ajv: {
customOptions: {
formats: {
'new-format': /hello/,
},
},
},
})
app.get(
'/:hello',
{
schema: {
params: {
type: 'object',
properties: {
hello: {
type: 'string',
format: 'new-format',
},
},
},
},
},
async (request, reply) => {
return request.params
}
)
app.listen(8080, '0.0.0.0')
// curl http://127.0.0.1:8080/hello
// curl http://127.0.0.1:8080/hello-foo
I am working with xstate with Nextjs. Now I am stuck somewhere.
import { assign, createMachine, interpret } from "xstate";
export interface toggleAuth {
isAuthenticated: boolean;
user: {
name: string | undefined;
};
}
// console.log(getCachedData());
export const authMachine = createMachine<toggleAuth>({
id: "auth",
initial: "unauthenticated",
context: {
isAuthenticated: false,
user: {
name: undefined,
},
},
states: {
authenticated: {
on: {
toggle: {
target: "unauthenticated",
},
},
entry: assign({
user: (ctx) => (ctx.user = { name: "Pranta" }),
isAuthenticated: (ctx) => (ctx.isAuthenticated = true),
}),
},
unauthenticated: {
on: {
toggle: {
target: "authenticated",
},
},
entry: assign({
user: (ctx) => (ctx.user = { name: undefined }),
isAuthenticated: (ctx) => (ctx.isAuthenticated = false),
}),
},
},
});
const service = interpret(authMachine);
service.onTransition((state) => console.log(state));
So I was watching the docs. According to them, whenever I transition from unauthenticated to authenticated and authenticated to unauthenticated, it should console log it for me. But it doesn't. It does only one time. What's happening here. Also, is it okay to define my machine like this? Thanks in advance.
It's not logging because you're not changing state; no event is ever being sent.
Please re-read the documentation on assigning to context - you are mutating context instead of assigning new values; the assigners should always be pure.
If you want to see the state change, you need to send a toggle event in this case:
service.send('toggle');
Also, there is no need for isAuthenticated; this is redundant, since that state is represented by the finite state (state.value) of your machine.
I'm using NestJS 7.0.2 and have globally enabled validation pipes via app.useGlobalPipes(new ValidationPipe());.
I'd like to be able to have a unit test that verifies that errors are being thrown if the improperly shaped object is provided, however the test as written still passes. I've seen that one solution is to do this testing in e2e via this post, but I'm wondering if there is anything I'm missing that would allow me to do this in unit testing.
I have a very simple controller with a very simple DTO.
Controller
async myApi(#Body() myInput: myDto): Promise<myDto | any> {
return {};
}
DTO
export class myDto {
#IsNotEmpty()
a: string;
#IsNotEmpty()
b: string | Array<string>
}
Spec file
describe('generate', () => {
it('should require the proper type', async () => {
const result = await controller.generate(<myDto>{});
// TODO: I expect a validation error to occur here so I can test against it.
expect(result).toEqual({})
})
})
It also fails if I do not coerce the type of myDto and just do a ts-ignore on a generic object.
Just test your DTO with ValidationPipe:
it('validate DTO', async() => {
let target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true });
const metadata: ArgumentMetadata = {
type: 'body',
metatype: myDto,
data: ''
};
await target.transform(<myDto>{}, metadata)
.catch(err => {
expect(err.getResponse().message).toEqual(["your validation error"])
})
});
You can find here complete test examples for ValidationPipe in Nestjs code repository
To test custom ValidationPipe:
let target = new ValidationDob();
const metadata: ArgumentMetadata = {
type: 'query',
metatype: GetAgeDto,
data: '',
};
it('should throw error when dob is invalid', async () => {
try {
await target.transform(<GetAgeDto>{ dob: 'null' }, metadata);
expect(true).toBe(false);
} catch (err) {
expect(err.getResponse().message).toEqual('Invalid dob timestamp');
}
});
How could I check if user has permission to see or query something? I have no idea how to do this.
In args? How would that even work?
In resolve()? See if user has permission and somehow
eliminate/change some of the args?
Example:
If user is "visitor", he can only see public posts, "admin" can see everything.
const userRole = 'admin'; // Let's say this could be "admin" or "visitor"
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => {
return {
posts: {
type: new GraphQLList(Post),
args: {
id: {
type: GraphQLString
},
title: {
type: GraphQLString
},
content: {
type: GraphQLString
},
status: {
type: GraphQLInt // 0 means "private", 1 means "public"
},
},
// MongoDB / Mongoose magic happens here
resolve(root, args) {
return PostModel.find(args).exec()
}
}
}
}
})
Update - Mongoose model looks something like this:
import mongoose from 'mongoose'
const postSchema = new mongoose.Schema({
title: {
type: String
},
content: {
type: String
},
author: {
type: mongoose.Schema.Types.ObjectId, // From user model/collection
ref: 'User'
},
date: {
type: Date,
default: Date.now
},
status: {
type: Number,
default: 0 // 0 -> "private", 1 -> "public"
},
})
export default mongoose.model('Post', postSchema)
You can check a user's permission in the resolve function or in the model layer. Here are the steps you have to take:
Authenticate the user before executing the query. This is up to your server and usually happens outside of graphql, for example by looking at the cookie that was sent along with the request. See this Medium post for more details on how to do this using Passport.js.
Add the authenticated user object or user id to the context. In express-graphql you can do it via the context argument:
app.use('/graphql', (req, res) => {
graphqlHTTP({ schema: Schema, context: { user: req.user } })(req, res);
}
Use the context inside the resolve function like this:
resolve(parent, args, context){
if(!context.user.isAdmin){
args.isPublic = true;
}
return PostModel.find(args).exec();
}
You can do authorization checks directly in resolve functions, but if you have a model layer, I strongly recommend implementing it there by passing the user object to the model layer. That way your code will be more modular, easier to reuse and you don't have to worry about forgetting some checks in a resolver somewhere.
For more background on authorization, check out this post (also written by myself):
Auth in GraphQL - part 2
One approach that has helped us solve authorization at our company is to think about resolvers as a composition of middleware. The above example is great but it will become unruly at scale especially as your authorization mechanisms get more advanced.
An example of a resolver as a composition of middleware might look something like this:
type ResolverMiddlewareFn =
(fn: GraphQLFieldResolver) => GraphQLFieldResolver;
A ResolverMiddlewareFn is a function that takes a GraphQLFieldResolver and and returns a GraphQLFieldResolver.
To compose our resolver middleware functions we will use (you guessed it) the compose function! Here is an example of compose implemented in javascript, but you can also find compose functions in ramda and other functional libraries. Compose lets us combine simple functions to make more complicated functions.
Going back to the GraphQL permissions problem lets look at a simple example.
Say that we want to log the resolver, authorize the user, and then run the meat and potatoes. Compose lets us combine these three pieces such that we can easily test and re-use them across our application.
const traceResolve =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
const start = new Date().getTime();
const result = await fn(obj, args, context, info);
const end = new Date().getTime();
console.log(`Resolver took ${end - start} ms`);
return result;
};
const isAdminAuthorized =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
if (!context.user.isAdmin) {
throw new Error('User lacks admin authorization.');
}
return await fn(obj, args, context, info);
}
const getPost = (obj: any, args: any, context: any, info: any) => {
return PostModel.find(args).exec();
}
const getUser = (obj: any, args: any, context: any, info: any) => {
return UserModel.find(args).exec();
}
// You can then define field resolve functions like this:
postResolver: compose(traceResolve, isAdminAuthorized)(getPost)
// And then others like this:
userResolver: compose(traceResolve, isAdminAuthorized)(getUser)