grubba-rpc
v0.12.8
Published
RPC e2e safe
Downloads
184
Readme
RPC
What is this package?
Inspired on zodern:relay
This package provides functions for building E2E type-safe RPCs. The functions are:
- crateMethod
- createPublication
- createModule
- createClient
How to download it?
meteor npm i grubba-rpc @tanstack/react-query zod
How to use it?
import {
ReturnMethod, // <- Type
ReturnSubscription, // <- Type
Config, // <- Type
SubscriptionCallbacks, // <- Type
createMethod, // <- function
createPublication // <- function
createModule, // <- function
createClient, // <- function
} from 'grubba-rpc';
createMethod
const test1 = createMethod('name', z.any(), () => 'str');
const result = await test1();
// ˆ? is string and their value is 'str'
For semantics uses you can as well use the methods below with the same output as createMethod:
const joinStr = createMutation(
'join',
z.object({ foo: z.string(), bar: z.string() }),
({ foo, bar }) => foo + bar);
const result = await joinStr({ foo: 'foo', bar: 'bar' });
// ˆ? is string and their value is 'foobar'
const query = createQuery(
'query',
z.object({ _id: z.string() }),
async ({ _id }) => {
const someData = await DB.findOne(_id);
const otherData = await DB.find({ _id: { $ne: someData._id } }).fetchAsync();
return { someData, otherData };
});
const result = await query({ _id: 'id' });
// ˆ? is string and their value is the item you was querying
example of use
createMethod accepts 4 arguments:
- name: string
- schema: ZodSchema (validator)
- handler (optional): function that receives the arguments of the method and returns the result
- config (optional): object with the following properties:
type Config<S, T> = {
rateLimit?: {
interval: number,
limit: number
},
hooks?: {
onBeforeResolve?: Array<(raw: unknown, parsed: S,) => void>;
onAfterResolve?: Array<(raw: Maybe<T>, parsed: S, result: T) => void>;
onErrorResolve?: Array<(err: Meteor.Error | Error | unknown, raw: Maybe<T>, parsed: S) => void>;
}
}
createPublication
const publication = createPublication('findRooms', z.object({ level: z.number() }), ({ level }) => Rooms.find({ level: level }));
const result = publication({ level: 1 }, (rooms) => console.log(rooms));
// ˆ? subscription
example of use
createPublication accepts 4 arguments:
- name: string
- schema: ZodSchema (validator)
- handler (optional): function that is being published
- config (optional): object with the following properties:
note that subscription returns the subscription handler the same way as Meteor.publish
type Config<S, T> = {
rateLimit?: {
interval: number,
limit: number
},
hooks?: {
onBeforeResolve?: Array<(raw: unknown, parsed: S,) => void>;
onAfterResolve?: Array<(raw: Maybe<T>, parsed: S, result: T) => void>;
onErrorResolve?: Array<(err: Meteor.Error | Error | unknown, raw: Maybe<T>, parsed: S) => void>;
}
}
Advanced usage
you can take advantage of the hooks to add custom logic to your methods and publications
const fn = createMethod('name', z.any(), () => 'str', {
hooks: {
onBeforeResolve: [
(raw, parsed) => {
console.log('before resolve', raw, parsed);
}
],
onAfterResolve: [
(raw, parsed, result) => {
console.log('after resolve', raw, parsed, result);
}
],
onErrorResolve: [
(err, raw, parsed) => {
console.log('error resolve', err, raw, parsed);
}
]
}
});
// valid ways as well
fn.addErrorResolveHook((err, raw, parsed) => {
console.log('error resolve', err, raw, parsed);
});
fn.addBeforeResolveHook((raw, parsed) => {
console.log('before resolve', raw, parsed);
});
fn.addAfterResolveHook((raw, parsed, result) => {
console.log('after resolve', raw, parsed, result);
});
const result = await fn();
Using safe methods
check this example that illustrates this 'secure way' of using safe methods, as it is not bundled in the client
import { createMethod } from 'grubba-rpc'
import { z } from "zod";
const DescriptionValidator = z.object({ description: z.string() });
// tasks.mutations.ts
// it expects the return type to be a void
export const insert = createMethod('task.insert', DescriptionValidator).expect<void>();
// tasks.mutations.js
// If you are using javascript, you can use the following syntax
export const insert = createMethod('task.insert', DescriptionValidator).expect(z.void());
// or you can use other name such as:
export const insert = createMethod('task.insert', DescriptionValidator).returns(z.void());
// ---------
// tasks.methods.ts
import { insert } from './tasks.mutations.ts'
insertTask = ({ description }) => {
TasksCollection.insert({
description,
userId: Meteor.userId(),
createdAt: new Date(),
});
};
insert.setResolver(insertTask);
// ---------
// client.ts
import { insert } from './tasks.mutations.ts'
insert({ description: 'test' });
//^? it return void and it will run
// if resolver is not set it will throw an error
createModule
const Tasks = createModule('tasks', {insert, remove, setChecked}).build();
const foo = createModule('foo')
.addMethod('bar', z.string(), () => 'bar' as const)
.addMethod('baz', z.string(), () => 'baz')
.addQuery('get', z.string(), () => 'get')
.addSubmodule('task', Tasks)
.build();
const k = await foo.bar();
// ?^ 'bar'
Examples?
in the examples folder you can find a simple example of how to use this package it uses simpletasks as a base
for downloading it you can do the command below or just access this link
git clone https://github.com/Grubba27/meteor-rpc-template.git
React focused API
For now, only works for methods
const test1 = createMethod('name', z.any(), () => 'str');
// in react context / component.tsx
const { data } = test1.useQuery();
// works like useSuspenseQuery from https://tanstack.com/query/latest/docs/react/reference/useSuspenseQuery
// or you can use for mutation
const { mutate } = test1.useMutation();
// uses the https://tanstack.com/query/v4/docs/react/reference/useMutation under the hood
method.useQuery
This uses the same api as useSuspenseQuery
method.useMutation
This uses the same api as useMutation
Using in the client
When using in the client you should use the createModule
and build
methods to create a module that will be used in the client
and be sure that you are exporting the type of the module
You should only create one client in your application
You can have something like api.ts
that will export the client and the type of the client
// server.ts
const otherModule = createModule()
.addMethod('bar', z.string(), () => 'bar')
.build();
const server = createModule()
.addMethod('foo', z.string(), () => 'foo')
.addMethod('bar', z.string(), () => 'bar')
.addSubmodule('other', otherModule)
.build();
export type Server = typeof server;
// client.ts
const app = createClient<Server>();
app.foo("str") // <--- This is type safe
app.other.bar("str") // <--- This is type safe