Skip to content

Commit

Permalink
feat(service): bring in base service builder
Browse files Browse the repository at this point in the history
  • Loading branch information
DawidWraga committed Mar 24, 2024
1 parent c3c76bb commit 84d2671
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 52 deletions.
6 changes: 5 additions & 1 deletion packages/service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@davstack/eslint-config": "workspace:*",
"@davstack/tsconfig": "workspace:*",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"eslint": "^8.57.0",
"@davstack/eslint-config": "workspace:*",
"react": "^18.2.0",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@trpc/server": "11.0.0-next.320",
"zod": "^3.22.4"
}
}
4 changes: 1 addition & 3 deletions packages/service/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export { toSlug } from "./toSlug";
export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
export { usePrevious } from "./usePrevious";
export * from './service';
213 changes: 213 additions & 0 deletions packages/service/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {
z,
infer as zInfer,
ZodObject,
ZodRawShape,
ZodSchema,
ZodTypeAny,
} from 'zod';

import { Simplify } from './utils/type-utils';

// TODO: refactor this (careful, used in create-router-from-services.ts)
// make it so that different ctx can be passed in
export type MyContext = Simplify<{
user: {
id: string;
};
}>;

// Generic type for resolver functions
export type Resolver<
TInputSchema extends ZodTypeAny | undefined,
TOutputSchema extends ZodTypeAny | undefined | unknown,
> = (opts: {
input: TInputSchema extends ZodTypeAny ? zInfer<TInputSchema> : null;
ctx: MyContext;
}) => Promise<TOutputSchema extends ZodTypeAny ? zInfer<TOutputSchema> : void>;

// Define the builder interface capturing generic types for input and output

type ZodSchemaOrRawShape = ZodSchema<any> | ZodRawShape;
type InferZodSchemaOrRawShape<T extends ZodSchemaOrRawShape> =
T extends ZodRawShape ? ZodObject<T> : T;

export interface ServiceBuilder<
TInputSchema extends ZodTypeAny | undefined,
TOutputSchema extends ZodTypeAny | undefined,
TType extends 'mutation' | 'query' | undefined,
TAccess extends 'public' | 'authed' | undefined = 'authed',
> {
// allows for objects to be passed in without having to call z.object
input: <TNewInputSchema extends ZodSchemaOrRawShape>(
schema: TNewInputSchema
) => ServiceBuilder<
InferZodSchemaOrRawShape<TNewInputSchema>, // handle zod object or raw shape
TOutputSchema,
TType,
TAccess
>;
output: <TNewOutput extends ZodTypeAny>(
schema: TNewOutput
) => ServiceBuilder<TInputSchema, TNewOutput, TType, TAccess>;
access: <TNewAccess extends 'public' | 'authed'>(
access: TNewAccess
) => ServiceBuilder<TInputSchema, TOutputSchema, TType, TNewAccess>;
mutation: <TResolver extends Resolver<TInputSchema, TOutputSchema>>(
resolver: TResolver
) => Service<TResolver, TInputSchema, TOutputSchema, 'mutation', TAccess>;
query: <TResolver extends Resolver<TInputSchema, TOutputSchema>>(
resolver: TResolver
) => Service<TResolver, TInputSchema, TOutputSchema, 'query', TAccess>;
}

export type Service<
TResolver extends Resolver<any, any>,
TInputSchema extends ZodTypeAny | undefined,
TOutputSchema extends ZodTypeAny | undefined | unknown,
TType extends 'mutation' | 'query' | undefined,
TAccess extends 'public' | 'authed' | undefined = 'authed',
> = ServiceDef<TResolver, TInputSchema, TOutputSchema, TType, TAccess> & {
(
ctx: MyContext,
input: TInputSchema extends ZodTypeAny ? zInfer<TInputSchema> : void
): ReturnType<TResolver>;
};

export type ServiceDef<
TResolver extends Resolver<any, any>,
TInputSchema extends ZodTypeAny | undefined,
TOutputSchema extends ZodTypeAny | undefined | unknown,
TType extends 'mutation' | 'query' | undefined,
TAccess extends 'public' | 'authed' | undefined,
> = {
inputSchema: TInputSchema;
outputSchema: TOutputSchema;
resolver: TResolver;
type: TType;
accessLevel: TAccess;
};

const initlaDef = {
inputSchema: undefined,
outputSchema: undefined,
resolver: undefined,
type: undefined,
accessLevel: 'authed' as const,
};

export function service() {
const def: ServiceDef<any, any, any, any, any> = { ...initlaDef };

const builder: ServiceBuilder<undefined, any, any> = {
access: function <TNewAccess extends 'public' | 'authed'>(
access: TNewAccess
) {
def.accessLevel = access;
return this as unknown as ServiceBuilder<undefined, any, any, TNewAccess>;
},
// allows for objects to be passed in without having to call z.object
input: function <TNewInputSchema extends ZodSchemaOrRawShape>(
schema: TNewInputSchema
) {
if (schema instanceof ZodSchema) {
def.inputSchema = schema;
} else {
def.inputSchema = z.object(schema);
}

return this as unknown as ServiceBuilder<
InferZodSchemaOrRawShape<TNewInputSchema>, // handle zod object or raw shape
undefined,
any
>;
},
output: function <TNewOutput extends ZodTypeAny>(schema: TNewOutput) {
def.outputSchema = schema;
return this as unknown as ServiceBuilder<undefined, TNewOutput, any>;
},
mutation: function <TResolver extends Resolver<any, any>>(
resolver: TResolver
) {
const newDef = {
...def,
resolver,
type: 'mutation',
} as ServiceDef<any, any, any, 'mutation', any>;
// console.log("MUTATION: ", newDef);
return createResolver(newDef) as unknown as Service<
TResolver,
any,
any,
'mutation'
>;
},
query: function <TResolver extends Resolver<any, any>>(
resolver: TResolver
) {
const newDef = {
...def,
resolver,
type: 'query',
} as ServiceDef<TResolver, any, any, 'query', any>;

// console.log("QUERY: ", newDef);
return createResolver(newDef) as unknown as Service<
TResolver,
undefined,
any,
'query'
>;
},
};

return builder;
}

export function createResolver<
TResolver extends Resolver<any, any>,
TInputSchema extends ZodTypeAny | undefined,
TOutputSchema extends ZodTypeAny | undefined,
TType extends 'mutation' | 'query',
TAccess extends 'public' | 'authed' | undefined,
>(def: ServiceDef<TResolver, TInputSchema, TOutputSchema, TType, TAccess>) {
/**
* Calls the resolver function without parsing input/output
* Useful for calling the resolver when the input/output is already parsed
*/
const callerWithoutParser = async (
ctx: MyContext,
input: TInputSchema extends ZodTypeAny ? zInfer<TInputSchema> : null
) => {
if (!def.resolver) {
throw new Error('Resolver not defined');
}
return def.resolver({ input, ctx });
};

/**
* invokes the resolver without parsing input/output
* Useful for safe calling the resolver directly
*/
const callerWithParser = async (ctx: MyContext, input: any) => {
const isOnlyAuthed = def.accessLevel === 'authed';
if (isOnlyAuthed) {
const hasId =
ctx.user?.id !== undefined &&
ctx.user.id !== null &&
ctx.user.id !== '';
if (!hasId) throw new Error('User is not logged in');
}

const maybeParsedInput = def.inputSchema
? def.inputSchema.parse(input)
: input;
const result = await callerWithoutParser(ctx, maybeParsedInput);
const maybeParsedOutput = def.outputSchema
? def.outputSchema.parse(result)
: result;
return maybeParsedOutput;
};

return Object.assign(callerWithParser, def);
}
18 changes: 0 additions & 18 deletions packages/service/src/toSlug.ts

This file was deleted.

13 changes: 0 additions & 13 deletions packages/service/src/useIsomorphicLayoutEffect.tsx

This file was deleted.

17 changes: 0 additions & 17 deletions packages/service/src/usePrevious.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/service/src/utils/type-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @link https://github.com/ianstormtaylor/superstruct/blob/7973400cd04d8ad92bbdc2b6f35acbfb3c934079/src/utils.ts#L323-L325
*/
export type Simplify<T> = T extends any[] | Date
? T
: { [K in keyof T]: T[K] } & {};
Loading

0 comments on commit 84d2671

Please sign in to comment.