How to properly use context in tRPC? - javascript

Let's say I have a very basic API with two sets of endpoints. One set queries and mutates properties about a User, which requires a username parameter, and one set queries and mutates properties about a Post, which requires a post ID. (Let's ignore authentication for simplicity.) I don't currently see a good way to implement this in a DRY way.
What makes the most sense to me is to have a separate Context for each set of routes, like this:
// post.ts
export async function createContext(
opts?: trpcExpress.CreateExpressContextOptions
) {
// pass through post id, throw if not present
}
type Context = trpc.inferAsyncReturnType<typeof createContext>;
const router = trpc
.router()
.query("get", {
resolve(req) {
// get post from database
return post;
},
});
// similar thing in user.ts
// server.ts
const trpcRouter = trpc
.router()
.merge("post.", postRouter)
.merge("user.", userRouter);
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: trpcRouter,
createContext,
})
);
This complains about context, and I can't find anything in the tRPC docs about passing a separate context to each router when merging. Middleware doesn't seem to solve the problem either - while I can fetch the post/user in a middleware and pass it on, I don't see any way to require a certain type of input in a middleware. I would have to throw { input: z.string() } or { input: z.number() } on every query/mutation, which of course isn't ideal.
The docs and examples seem pretty lacking for this (presumably common) use case, so what's the best way forward here?

This functionality has been added in (unreleased as of writing) v10. https://trpc.io/docs/v10/procedures#multiple-input-parsers
const roomProcedure = t.procedure.input(
z.object({
roomId: z.string(),
}),
);
const appRouter = t.router({
sendMessage: roomProcedure
.input(
z.object({
text: z.string(),
}),
)
.mutation(({ input }) => {
// input: { roomId: string; text: string }
}),
});

Related

Objection JS & Postgres & Exporess returning 'TypeError: Cannot read property 'isPartial' of null' on many to many relation query

I am trying to relate table named 'products' to another table named 'tags'.
I have a many to many table titled 'products_tags'.
When running const product = await Product.relatedQuery('tags').findById(1); I receive the following error:
TypeError: Cannot read property 'isPartial' of null
at findFirstNonPartialAncestorQuery (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\relations\RelationOwner.js:194:18)
at Function.createParentReference (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\relations\RelationOwner.js:24:48)
at QueryBuilder._findOperationFactory (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\model\Model.js:819:25)
at addFindOperation (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\queryBuilder\QueryBuilder.js:1551:31)
at addImplicitOperations (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\queryBuilder\QueryBuilder.js:1539:5)
at beforeExecute (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\queryBuilder\QueryBuilder.js:1439:13)
at QueryBuilder.execute (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\queryBuilder\QueryBuilder.js:681:13)
at QueryBuilder.then (C:\Users\AmazeCPK\Documents\_Web\fractal-insight2\node_modules\objection\lib\queryBuilder\QueryBuilder.js:634:26)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
My product model:
const {Model} = require('objection');
class Product extends Model {
static get tableName() {
return 'products'
}
static get relationMappings() {
const Tag = require('./Tag');
return {
tags: {
relation: Model.ManyToManyRelation,
modelClass: Tag,
join: {
from: 'products.id',
through: {
from: 'products_tags.product_id',
to: 'products_tags.tag_id'
},
to: 'tags.id'
}
}
}
}
}
module.exports = Product;
Tags.js
const { Model } = require('objection');
class Tag extends Model {
static get tableName() {
return 'tags';
}
static get relationMappings() {
const Product = require ('./Product');
return {
products: {
relation: Model.ManyToManyRelation,
modelClass: Product,
join: {
from: 'tags.id',
through: {
from: 'products_tags.tag_id',
to: 'products_tags.product_id'
},
to: 'products.id'
}
}
}
}
}
module.exports = Tag;
I've made sure the table names and relations are correct, as well as made sure there is matching data in the database. Help is appreciated. Thank you.
edit: here is the migration setup
//...
knex.schema.createTable('tags', table => {
table.increments('id').notNullable();
table.text("name");
table.boolean("is_deleted").defaultTo(false);
table.timestamps(true, true);
}),
knex.schema.createTable('products', table => {
table.increments('id').notNullable();
table.text("name");
table.text("description");
table.float("price");
table.integer('category_id').references('id').inTable('categories');
table.boolean("can_be_personalized").defaultTo(false);
table.specificType('images', 'text ARRAY');
table.boolean("is_deleted").defaultTo(false);
table.timestamps(true, true);
}),
knex.schema.createTable('products_tags', table => {
table.increments('id').notNullable();
table.integer('product_id').references('id').inTable('products');
table.integer('tag_id').references('id').inTable('tags');
table.boolean("is_deleted").defaultTo(false);
table.timestamps(true, true);
}),
Surely you're not in need of a solution anymore, but to future readers there are three things which can cause this. I just spent the last two and a half hours debugging this same error so in order from easiest to verify up to hardest to verify, they are:
You aren't specifying a primary id for the parent model of your related query. If you're using ModelB.relatedQuery('modelARelation') then you must also call .for(modelBId) or you will get this error. (This is what happened to me because I mixed up req.query and req.params :facepalm:)
ModelB.relatedQuery('modelARelation'); // when run alone, you get this answer
...
const modelBId = req.query.modelBId;
ModelB.relatedQuery('modelARelation').for(modelBId); // works just fine as long as modelBId is properly defined.
Require loops Circular dependency loops. If ModelA relates to ModelB and ModelB relates to ModelA, make sure you are using one of the circular dependency strategies outlined in the docs.
Typos. I hate to say it but if you have gotten this far, it is almost certain that you have typos somewhere. Go eat something or have somebody else look at it. This is actually just good advice generally.
In my limited experience, this error usually comes when it cannot create the object for whatever reason. Commonly it's an exports issue, or the constructor issue.
Are you sure the filename is correct? You named it Tags.js, but your const Tag = require('./Tag');

Feathers-mongoose : Get by custom attribute in feathers-mongoose

I have a very basic feathers service which stores data in mongoose using the feathers-mongoose package. The issue is with the get functionality. My model is as follows:
module.exports = function (app) {
const mongooseClient = app.get('mongooseClient');
const { Schema } = mongooseClient;
const messages = new Schema({
message: { type: String, required: true }
}, {
timestamps: true
});
return mongooseClient.model('messages', messages);
};
When the a user runs a GET command :
curl http://localhost:3030/messages/test
I have the following requirements
This essentially tries to convert test to ObjectID. What i would
like it to do is to run a query against the message attribute
{message : "test"} , i am not sure how i can achieve this. There is
not enough documentation for to understand to write or change this
in the hooks. Can some one please help
I want to return a custom error code (http) when a row is not found or does not match some of my criterias. How can i achive this?
Thanks
In a Feathers before hook you can set context.result in which case the original database call will be skipped. So the flow is
In a before get hook, try to find the message by name
If it exists set context.result to what was found
Otherwise do nothing which will return the original get by id
This is how it looks:
async context => {
const messages = context.service.find({
...context.params,
query: {
$limit: 1,
name: context.id
}
});
if (messages.total > 0) {
context.result = messages.data[0];
}
return context;
}
How to create custom errors and set the error code is documented in the Errors API.

Use existing variables when using refetchQueries in react-apollo

I am using postsConnection query for infinite scroll. It contains variables like after.
After doing an upvote mutation, I want to refetchQueries... like this 👇
const upvote = await client.mutate({
mutation: UPVOTE_MUTATION,
variables: {
postId: this.props.post.id
},
refetchQueries: [
{ query: POST_AUTHOR_QUERY }
]
})
Above code gives error because POST_AUTHOR_QUERY accepts few variables. Here's that query 👇
export const POST_AUTHOR_QUERY = gql`
query POST_AUTHOR_QUERY($authorUsername: String! $orderBy: PostOrderByInput $after: String){
postsAuthorConnection(authorUsername: $authorUsername orderBy: $orderBy after: $after) {
....
}
}
I do not want to add variables manually. Variables are already stored in the cache. How do I reuse them while using refetchQueries???
Here are a few resources I have read about this issue 👇
https://github.com/apollographql/react-apollo/issues/817
https://github.com/apollographql/apollo-client/issues/1900
As mentioned in the issue you linked, you should be able to do the following:
import { getOperationName } from 'apollo-link'
const upvote = await client.mutate({
// other options
refetchQueries={[getOperationName(POST_AUTHOR_QUERY)]}
})
From the docs:
Please note that if you call refetchQueries with an array of strings, then Apollo Client will look for any previously called queries that have the same names as the provided strings. It will then refetch those queries with their current variables.
getOperationName simply parses the document you pass it and extracts the operation name from it. You can, of course, provide the operation name yourself as a string instead, but this way avoids issues if the operation name changes in the future or you fat finger it.
If you don't want to pull in apollo-link, you can also get this via the base graphql package (note that I use optional chaining for convenience:
import { getOperationAST } from 'graphql';
const operationName = getOperationAST(POST_AUTHOR_QUERY)?.name?.value;
// Note that this could technically return `undefined`
const upvote = await client.mutate({
mutation: UPVOTE_MUTATION,
variables: {
postId: this.props.post.id
},
refetchQueries: [operationName]
})

How can I add computed state to graph objects in React Apollo?

I really like the graphQL pattern of having components request their own data, but some data properties are expensive to compute and so I want to localize the logic (and code) to do so.
function CheaterList({ data: { PlayerList: players } }) {
return (
<ul>
{players && players.map(({ name, isCheater }) => (
<li key={name}>{name} seems to be a {isCheater ? 'cheater' : 'normal player'}</li>
))}
</ul>
);
}
export default graphql(gql`
query GetList {
PlayerList {
name,
isCheater
}
}
`)(CheaterList);
The schema looks like:
type Queries {
PlayerList: [Player]
}
type Player {
name: String,
kills: Integer,
deaths: Integer
}
And so I want to add the isCheater property to Player and have its code be:
function computeIsCheater(player: Player){
// This is a simplified version of what it actually is for the sake of the example
return player.deaths == 0 || (player.kills / player.deaths) > 20;
}
How would I do that?
Another way of phrasing this would be: how do I get the isCheater property to look as though it came from the backend? (However, if an optimistic update were applied the function should rerun on the new data)
Note: Local state management is now baked into apollo-client -- there's no need to add a separate link in order to make use of local resolvers and the #client directive. For an example of mixing local and remote fields, check out of the docs. Original answer follows.
As long as you're using Apollo 2.0, this should be possible by utilizing apollo-link-state as outlined in the docs.
Modify your client configuration to include apollo-link-state:
import { withClientState } from 'apollo-link-state';
const stateLink = withClientState({
cache, //same cache object you pass to the client constructor
resolvers: linkStateResolvers,
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([stateLink, new HttpLink()]),
});
Define your client-only resolvers:
const linkStateResolvers = {
Player: {
isCheater: (player, args, ctx) => {
return player.deaths == 0 || (player.kills / player.deaths) > 20
}
}
}
Use the #client directive in your query
export default graphql(gql`
query GetList {
PlayerList {
name
kills
deaths
isCheater #client
}
}
`)(CheaterList);
However, it appears that combining local and remote fields in a single query is currently broken. There's an open issue here that you can track.

How to set URL query params in Vue with Vue-Router

I am trying to set query params with Vue-router when changing input fields, I don't want to navigate to some other page but just want to modify url query params on the same page, I am doing like this:
this.$router.replace({ query: { q1: "q1" } })
But this also refreshes the page and sets the y position to 0, ie scrolls to the top of the page. Is this the correct way to set the URL query params or is there a better way to do it.
Edited:
Here is my router code:
export default new Router({
mode: 'history',
scrollBehavior: (to, from, savedPosition) => {
if (to.hash) {
return {selector: to.hash}
} else {
return {x: 0, y: 0}
}
},
routes: [
.......
{ path: '/user/:id', component: UserView },
]
})
Here is the example in docs:
// with query, resulting in /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
Ref: https://router.vuejs.org/en/essentials/navigation.html
As mentioned in those docs, router.replace works like router.push
So, you seem to have it right in your sample code in question. But I think you may need to include either name or path parameter also, so that the router has some route to navigate to. Without a name or path, it does not look very meaningful.
This is my current understanding now:
query is optional for router - some additional info for the component to construct the view
name or path is mandatory - it decides what component to show in your <router-view>.
That might be the missing thing in your sample code.
EDIT: Additional details after comments
Have you tried using named routes in this case? You have dynamic routes, and it is easier to provide params and query separately:
routes: [
{ name: 'user-view', path: '/user/:id', component: UserView },
// other routes
]
and then in your methods:
this.$router.replace({ name: "user-view", params: {id:"123"}, query: {q1: "q1"} })
Technically there is no difference between the above and this.$router.replace({path: "/user/123", query:{q1: "q1"}}), but it is easier to supply dynamic params on named routes than composing the route string. But in either cases, query params should be taken into account. In either case, I couldn't find anything wrong with the way query params are handled.
After you are inside the route, you can fetch your dynamic params as this.$route.params.id and your query params as this.$route.query.q1.
Without reloading the page or refreshing the dom, history.pushState can do the job.
Add this method in your component or elsewhere to do that:
addParamsToLocation(params) {
history.pushState(
{},
null,
this.$route.path +
'?' +
Object.keys(params)
.map(key => {
return (
encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
)
})
.join('&')
)
}
So anywhere in your component, call addParamsToLocation({foo: 'bar'}) to push the current location with query params in the window.history stack.
To add query params to current location without pushing a new history entry, use history.replaceState instead.
Tested with Vue 2.6.10 and Nuxt 2.8.1.
Be careful with this method!
Vue Router don't know that url has changed, so it doesn't reflect url after pushState.
Actually you can push query like this: this.$router.push({query: {plan: 'private'}})
Based on: https://github.com/vuejs/vue-router/issues/1631
Okay so i've been trying to add a param to my existing url wich already have params for a week now lol,
original url: http://localhost:3000/somelink?param1=test1
i've been trying with:
this.$router.push({path: this.$route.path, query: {param2: test2} });
this code would juste remove param1 and becomes
http://localhost:3000/somelink?param2=test2
to solve this issue i used fullPath
this.$router.push({path: this.$route.fullPath, query: {param2: test2} });
now i successfully added params over old params nd the result is
http://localhost:3000/somelink?param1=test1&param2=test2
If you are trying to keep some parameters, while changing others, be sure to copy the state of the vue router query and not reuse it.
This works, since you are making an unreferenced copy:
const query = Object.assign({}, this.$route.query);
query.page = page;
query.limit = rowsPerPage;
await this.$router.push({ query });
while below will lead to Vue Router thinking you are reusing the same query and lead to the NavigationDuplicated error:
const query = this.$route.query;
query.page = page;
query.limit = rowsPerPage;
await this.$router.push({ query });
Of course, you could decompose the query object, such as follows, but you'll need to be aware of all the query parameters to your page, otherwise you risk losing them in the resultant navigation.
const { page, limit, ...otherParams } = this.$route.query;
await this.$router.push(Object.assign({
page: page,
limit: rowsPerPage
}, otherParams));
);
Note, while the above example is for push(), this works with replace() too.
Tested with vue-router 3.1.6.
Here's my simple solution to update the query params in the URL without refreshing the page. Make sure it works for your use case.
const query = { ...this.$route.query, someParam: 'some-value' };
this.$router.replace({ query });
My solution, no refreshing the page and no error Avoided redundant navigation to current location
this.$router.replace(
{
query: Object.assign({ ...this.$route.query }, { newParam: 'value' }),
},
() => {}
)
this.$router.push({ query: Object.assign(this.$route.query, { new: 'param' }) })
You could also just use the browser window.history.replaceState API. It doesn't remount any components and doesn't cause redundant navigation.
window.history.replaceState(null, '', '?query=myquery');
More info here.
For adding multiple query params, this is what worked for me (from here https://forum.vuejs.org/t/vue-router-programmatically-append-to-querystring/3655/5).
an answer above was close … though with Object.assign it will mutate this.$route.query which is not what you want to do … make sure the first argument is {} when doing Object.assign
this.$router.push({ query: Object.assign({}, this.$route.query, { newKey: 'newValue' }) });
To set/remove multiple query params at once I've ended up with the methods below as part of my global mixins (this points to vue component):
setQuery(query){
let obj = Object.assign({}, this.$route.query);
Object.keys(query).forEach(key => {
let value = query[key];
if(value){
obj[key] = value
} else {
delete obj[key]
}
})
this.$router.replace({
...this.$router.currentRoute,
query: obj
})
},
removeQuery(queryNameArray){
let obj = {}
queryNameArray.forEach(key => {
obj[key] = null
})
this.setQuery(obj)
},
I normally use the history object for this. It also does not reload the page.
Example:
history.pushState({}, '',
`/pagepath/path?query=${this.myQueryParam}`);
The vue router keeps reloading the page on update, the best solution is
const url = new URL(window.location);
url.searchParams.set('q', 'q');
window.history.pushState({}, '', url);
With RouterLink
//With RouterLink
<router-link
:to="{name:"router-name", prams:{paramName: paramValue}}"
>
Route Text
</router-link>
//With Methods
methods(){
this.$router.push({name:'route-name', params:{paramName: paramValue}})
}
With Methods
methods(){
this.$router.push({name:'route-name', params:{paramName, paramValue}})
}
This is the equivalent using the Composition API
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({ path: 'register', query: { plan: 'private' }})
</script>
You can also use the Vue devtools just to be sure that it's working as expected (by inspecting the given route you're on) as shown here: https://stackoverflow.com/a/74136917/8816585
Update
That will meanwhile mount/unmount components. Some vanilla JS solution is still the best way to go for that purpose.

Categories