meteor-type-validation
Version:
A lightweight set of TypeScript utilities to add proper type inference and validation for your Meteor publications and methods
203 lines (196 loc) • 11.8 kB
TypeScript
import * as valibot from 'valibot';
import { GenericSchema, InferOutput, InferInput } from 'valibot';
import { Meteor, Subscription } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import Pino from 'pino';
declare module 'meteor/meteor' {
interface DefinedMethods {
}
interface DefinedPublications {
}
type MethodName = keyof DefinedMethods;
type PublicationName = keyof DefinedPublications;
type PublicationParams<TName extends PublicationName> = Parameters<DefinedPublications[TName]>;
type MethodParams<TName extends MethodName> = Parameters<DefinedMethods[TName]>;
type MethodResult<TName extends MethodName> = ReturnType<DefinedMethods[TName]>;
type PublicationResult<TName extends PublicationName> = ReturnType<DefinedPublications[TName]>;
namespace Meteor {
function subscribe<TName extends keyof DefinedPublications>(name: TName, ...params: Parameters<DefinedPublications[TName]>): Meteor.SubscriptionHandle;
function call<TName extends keyof DefinedMethods>(name: TName, ...params: [
...Parameters<DefinedMethods[TName]>,
callback?: (error?: Error, response?: ReturnType<DefinedMethods[TName]>) => void
]): void;
function callAsync<TName extends keyof DefinedMethods>(name: TName, ...params: Parameters<DefinedMethods[TName]>): Awaited<ReturnType<DefinedMethods[TName]>>;
}
}
interface MethodDefinition<TSchemas extends GenericSchema[] = GenericSchema[], TGuards extends GuardStatic[] = GuardStatic[], TExtendedContext extends ExtendedContext = ExtendedContext, TReturnType = unknown> {
schema: [...TSchemas];
guards: TGuards;
rateLimiters?: RateLimiterRule[];
method(this: ValidatedThisType<TGuards, Meteor.MethodThisType> & TExtendedContext, ...params: UnwrapSchemaOutput<TSchemas>): TReturnType;
}
interface PublicationDefinition<TSchemas extends GenericSchema[] = GenericSchema[], TGuards extends GuardStatic[] = GuardStatic[], TExtendedContext extends ExtendedContext = ExtendedContext, TReturnType = unknown> {
schema: [...TSchemas];
guards: TGuards;
rateLimiters?: RateLimiterRule[];
publish(this: ValidatedThisType<TGuards, Subscription> & TExtendedContext, ...params: UnwrapSchemaOutput<TSchemas>): TReturnType;
}
/**
* This is left empty so you can augment it with any custom context types you want to be
* injected into the `this` type of your method/publication handlers.
* Useful for loggers, profiling or adding extra request metadata.
*/
interface ExtendedContext {
}
type _ResourceThisType = (Meteor.MethodThisType | Subscription);
type BaseContext<TSelf extends _ResourceThisType = _ResourceThisType> = TSelf & ExtendedContext;
type WrappedContext<TBaseContext extends BaseContext = BaseContext> = TBaseContext & {
startTime: number;
};
type MethodDefinitionMap = {
[key in string]: MethodDefinition;
};
type PublicationDefinitionMap = {
[key in string]: PublicationDefinition;
};
type RateLimiterRule = Pick<DDPRateLimiter.Matcher, 'userId' | 'connectionId' | 'clientAddress'> & {
requestCount?: number;
intervalMs?: number;
};
/**
* Unwrap method definitions to get the method map as it would be
* fed into Meteor.methods(...)
*/
type UnwrapMethods<TMethods extends MethodDefinitionMap> = {
[key in keyof TMethods]: (...params: UnwrapSchemaInput<TMethods[key]['schema']>) => ReturnType<TMethods[key]['method']>;
};
/**
* Unwrap publications to get a record of publication handles as
* they would be added to Meteor.publish(<name>, ...)
*/
type UnwrapPublications<TPublications extends PublicationDefinitionMap> = {
[key in keyof TPublications]: (...params: UnwrapSchemaInput<TPublications[key]['schema']>) => ReturnType<TPublications[key]['publish']>;
};
/**
* Infer method/publication argument types from the provided schema.
* This is the argument's type as it is received inside the method handle.
* The input type (the type the caller should adhere to) is inferred from {@link UnwrapSchemaInput}
*/
type UnwrapSchemaOutput<TSchemas extends GenericSchema[]> = {
[key in keyof TSchemas]: InferOutput<TSchemas[key]>;
};
/**
* Argument types for the provided schemas as it should be passed by the caller of the method/publication.
*/
type UnwrapSchemaInput<TSchemas extends GenericSchema[]> = {
[key in keyof TSchemas]: InferInput<TSchemas[key]>;
};
type ValidatedThisType<TGuards extends GuardStatic[] | GuardFunction[], TThisType extends _ResourceThisType = _ResourceThisType> = TGuards extends GuardStatic[] ? ValidatedStaticThisType<TGuards> & BaseContext<TThisType> : TGuards extends GuardFunction[] ? ValidatedFnThisType<TGuards> & BaseContext<TThisType> : never;
type ValidatedStaticThisType<TGuards extends GuardStatic[]> = InstanceType<TGuards[number]>['validatedContext'];
type ValidatedFnThisType<TGuards extends GuardFunction[]> = ReturnType<TGuards[number]>;
type ResourceType = 'method' | 'publication';
interface ContextWrapper<TContext extends BaseContext = BaseContext, TType extends ResourceType = TContext extends Meteor.MethodThisType ? 'method' : TContext extends Subscription ? 'publication' : never> {
type: TType;
context: TContext;
name: string;
}
declare abstract class Guard {
readonly context: BaseContext;
protected readonly params: unknown[];
constructor(context: BaseContext, params: unknown[]);
abstract validate(): asserts this;
abstract get validatedContext(): unknown;
}
interface GuardStatic<TGuard extends Guard = Guard> {
new (...context: any): TGuard;
}
type GuardFunction<TSchemas extends GenericSchema[] = GenericSchema[]> = (request: {
context: BaseContext;
params: UnwrapSchemaOutput<TSchemas>;
}) => asserts request;
/**
* Defines a type safe method with input and context validation.
* The result of this function should be exported so that we can infer all its types globally.
*
* @example /imports/api/topics/methods.ts
* export default defineMethods({
* 'topic.create': {
* schema: [TopicSchemas.create],
* guards: [...],
* method(topic) {
* ...
* }
* }
* })
*
* @example ./server/methods.ts
* import TopicMethods from '/imports/api/topics/methods'
*
* const AllMethods = {
* ...TopicMethods,
* // ... all other methods
* }
*
* Meteor.startup(() => {
* exposeMethods(AllMethods)
* })
*
* declare module 'meteor/meteor' {
* interface DefinedMethods extends UnwrapMethods<typeof AllMethods> {}
* }
*/
declare const defineMethods: <TSchemas extends Record<keyof TGuards, valibot.GenericSchema[]>, TGuards extends Record<keyof TSchemas | keyof TResult, GuardStatic<Guard>[]>, TResult extends Record<keyof TGuards | keyof TSchemas, unknown>>(methods: { [key in keyof TGuards | keyof TSchemas | keyof TResult]: MethodDefinition<TSchemas[key], TGuards[key], {}, TResult[key]>; }) => { [key in keyof TGuards | keyof TSchemas | keyof TResult]: MethodDefinition<TSchemas[key], TGuards[key], {}, TResult[key]>; };
/**
* Defines a type safe publication input and context validation.
* The result of this method should be exported so that we can infer all its types globally.
*/
declare const definePublications: <TSchemas extends Record<keyof TGuards, valibot.GenericSchema[]>, TGuards extends Record<keyof TSchemas | keyof TResult, GuardStatic<Guard>[]>, TResult extends Record<keyof TGuards | keyof TSchemas, unknown>>(publications: { [key in keyof TGuards | keyof TSchemas | keyof TResult]: PublicationDefinition<TSchemas[key], TGuards[key], {}, TResult[key]>; }) => { [key in keyof TGuards | keyof TSchemas | keyof TResult]: PublicationDefinition<TSchemas[key], TGuards[key], {}, TResult[key]>; };
declare const exposeMethods: <TMethods extends MethodDefinitionMap>(methods: TMethods) => { [key in keyof TMethods]: (...params: Parameters<TMethods[key]["method"]>) => ReturnType<TMethods[key]["method"]>; };
declare const exposePublications: <TPublications extends PublicationDefinitionMap>(publications: TPublications) => { [key in keyof TPublications]: (...params: Parameters<TPublications[key]["publish"]>) => ReturnType<TPublications[key]["publish"]>; };
declare class MeteorTypeValidation<TAddedContext = {}, TOptionsContext extends {
logger?: Pino.Logger;
} = {}, TExtendedContext extends TAddedContext & TOptionsContext = TAddedContext & TOptionsContext> {
protected readonly options: {
extendContext?: (context: ContextWrapper) => TExtendedContext;
createLogger?: (context: ContextWrapper) => TOptionsContext['logger'];
errorHandler?: (error: unknown) => never;
};
constructor(options?: {
extendContext?: (context: ContextWrapper) => TExtendedContext;
createLogger?: (context: ContextWrapper) => TOptionsContext['logger'];
errorHandler?: (error: unknown) => never;
});
protected setupDefaultLogger(): void;
defineMethods<TSchemas extends Record<keyof TGuards, GenericSchema[]>, TGuards extends Record<keyof TSchemas | keyof TResult, GuardStatic[]>, TResult extends Record<keyof TSchemas | keyof TGuards, unknown>>(methods: {
[key in keyof TSchemas | keyof TGuards | keyof TResult]: MethodDefinition<TSchemas[key], TGuards[key], TExtendedContext, TResult[key]>;
}): { [key in keyof TGuards | keyof TSchemas | keyof TResult]: MethodDefinition<TSchemas[key], TGuards[key], TExtendedContext, TResult[key]>; };
definePublications<TSchemas extends Record<keyof TGuards, GenericSchema[]>, TGuards extends Record<keyof TSchemas | keyof TResult, GuardStatic[]>, TResult extends Record<keyof TSchemas | keyof TGuards, unknown>>(publications: {
[key in keyof TSchemas | keyof TGuards | keyof TResult]: PublicationDefinition<TSchemas[key], TGuards[key], TExtendedContext, TResult[key]>;
}): { [key in keyof TGuards | keyof TSchemas | keyof TResult]: PublicationDefinition<TSchemas[key], TGuards[key], TExtendedContext, TResult[key]>; };
exposeMethods<TMethods extends MethodDefinitionMap>(methods: TMethods): {
[key in keyof TMethods]: (...params: Parameters<TMethods[key]['method']>) => ReturnType<TMethods[key]['method']>;
};
exposePublications<TPublications extends PublicationDefinitionMap>(publications: TPublications): {
[key in keyof TPublications]: (...params: Parameters<TPublications[key]['publish']>) => ReturnType<TPublications[key]['publish']>;
};
protected loadRateLimit({ rule, type, name }: {
rule: RateLimiterRule;
type: ContextWrapper['type'];
name: string;
}): void;
protected extendContext({ type, context, name }: ContextWrapper): Promise<any> | (BaseContext<_ResourceThisType> & (TExtendedContext | undefined));
protected validateRequest({ context, definition, params }: {
context: WrappedContext;
definition: MethodDefinition | PublicationDefinition;
params: unknown[];
}): Promise<{
validatedParams: unknown[];
}>;
protected withErrorHandler(method: (...params: unknown[]) => unknown): (...params: unknown[]) => any;
protected wrapResource({ definition, name }: {
definition: MethodDefinition | PublicationDefinition;
name: string;
}): (...params: unknown[]) => any;
private parseDefinition;
}
export { type BaseContext, type ContextWrapper, type ExtendedContext, Guard, type GuardFunction, type GuardStatic, MeteorTypeValidation, type MethodDefinition, type MethodDefinitionMap, type PublicationDefinition, type PublicationDefinitionMap, type RateLimiterRule, type ResourceType, type UnwrapMethods, type UnwrapPublications, type UnwrapSchemaInput, type UnwrapSchemaOutput, type WrappedContext, type _ResourceThisType, defineMethods, definePublications, exposeMethods, exposePublications };