Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-buckets-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@baseplate-dev/fastify-generators': patch
---

Use Zod schema defined in mutations instead of restrictObjectNulls to allow for cleaner mutations and validation
5 changes: 5 additions & 0 deletions .changeset/twelve-readers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@baseplate-dev/fastify-generators': patch
---

Add support for validation plugin in Pothos
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"@baseplate-dev/core-generators#node/node:package-json": "package.json",
"@baseplate-dev/core-generators#node/prettier:prettier-config": ".prettierrc",
"@baseplate-dev/core-generators#node/prettier:prettier-ignore": ".prettierignore",
"@baseplate-dev/core-generators#node/ts-utils:normalize-types": "src/utils/normalize-types.ts",
"@baseplate-dev/core-generators#node/ts-utils:nulls": "src/utils/nulls.ts",
"@baseplate-dev/core-generators#node/ts-utils:string": "src/utils/string.ts",
"@baseplate-dev/core-generators#node/typescript:tsconfig": "tsconfig.json",
"@baseplate-dev/core-generators#node/vitest:global-setup": "src/tests/scripts/global-setup.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@pothos/plugin-relay": "4.6.2",
"@pothos/plugin-simple-objects": "4.1.3",
"@pothos/plugin-tracing": "1.1.0",
"@pothos/plugin-validation": "4.2.0",
"@pothos/tracing-sentry": "1.1.1",
"@prisma/adapter-pg": "6.17.1",
"@prisma/client": "6.17.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { queryFromInfo } from '@pothos/plugin-prisma';

import { builder } from '@src/plugins/graphql/builder.js';
import { restrictObjectNulls } from '@src/utils/nulls.js';

import {
createUser,
Expand All @@ -10,13 +9,15 @@ import {
} from '../services/user.data-service.js';
import { userObjectType } from './user.object-type.js';

const createUserDataInputType = builder.inputType('CreateUserData', {
fields: (t) => ({
email: t.string(),
name: t.string(),
emailVerified: t.boolean(),
}),
});
const createUserDataInputType = builder
.inputType('CreateUserData', {
fields: (t) => ({
email: t.string(),
name: t.string(),
emailVerified: t.boolean(),
}),
})
.validate(createUser.$dataSchema);

builder.mutationField('createUser', (t) =>
t.fieldWithInputPayload({
Expand All @@ -27,7 +28,7 @@ builder.mutationField('createUser', (t) =>
authorize: ['admin'],
resolve: async (root, { input: { data } }, context, info) => {
const user = await createUser({
data: restrictObjectNulls(data, ['emailVerified']),
data,
context,
query: queryFromInfo({ context, info, path: ['user'] }),
});
Expand All @@ -36,13 +37,15 @@ builder.mutationField('createUser', (t) =>
}),
);

const updateUserDataInputType = builder.inputType('UpdateUserData', {
fields: (t) => ({
email: t.string(),
name: t.string(),
emailVerified: t.boolean(),
}),
});
const updateUserDataInputType = builder
.inputType('UpdateUserData', {
fields: (t) => ({
email: t.string(),
name: t.string(),
emailVerified: t.boolean(),
}),
})
.validate(updateUser.$dataSchema);

builder.mutationField('updateUser', (t) =>
t.fieldWithInputPayload({
Expand All @@ -55,7 +58,7 @@ builder.mutationField('updateUser', (t) =>
resolve: async (root, { input: { id, data } }, context, info) => {
const user = await updateUser({
where: { id },
data: restrictObjectNulls(data, ['emailVerified']),
data,
context,
query: queryFromInfo({ context, info, path: ['user'] }),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { PothosFieldWithInputPayloadPlugin } from './index.js';
import type {
MutationWithInputPayloadOptions,
OutputShapeFromFields,
PayloadFieldRef,
} from './types.js';

declare global {
Expand Down Expand Up @@ -56,10 +57,7 @@ declare global {
payload: RootFieldBuilder<Types, unknown, 'PayloadObject'>;
fieldWithInputPayload: <
InputFields extends InputFieldMap,
PayloadFields extends Record<
string,
FieldRef<Types, unknown, 'PayloadObject'>
>,
PayloadFields extends Record<string, PayloadFieldRef<Types, unknown>>,
ResolveShape,
ResolveReturnShape,
Args extends InputFieldMap = Record<never, never>,
Expand All @@ -79,7 +77,7 @@ declare global {
ShapeFromTypeParam<
Types,
ObjectRef<Types, OutputShapeFromFields<PayloadFields>>,
false
Types['DefaultFieldNullability']
>
>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {

import { capitalizeString } from '@src/utils/string.js';

import type { PayloadFieldRef } from './types.js';

const rootBuilderProto =
RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder<
SchemaTypes,
Expand All @@ -27,11 +29,11 @@ rootBuilderProto.fieldWithInputPayload = function fieldWithInputPayload({
// expose all fields of payload by default
const payloadFields = (): Record<
string,
FieldRef<SchemaTypes, unknown, 'PayloadObject'>
PayloadFieldRef<SchemaTypes, unknown>
> => {
for (const key of Object.keys(payload)) {
payload[key].onFirstUse((cfg) => {
if (cfg.kind === 'Object') {
if (cfg.kind === 'Object' && !cfg.resolve) {
cfg.resolve = (parent) =>
(parent as Record<string, unknown>)[key] as Readonly<unknown>;
}
Expand All @@ -56,16 +58,19 @@ rootBuilderProto.fieldWithInputPayload = function fieldWithInputPayload({
type: payloadRef,
nullable: false,
...fieldOptions,
} as never);
} as never) as FieldRef<SchemaTypes, never, 'Mutation'>;

fieldRef.onFirstUse((config) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fieldRef.onFirstUse((config: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
const capitalizedName = capitalizeString(config.name);
const inputName = `${capitalizedName}Input`;
const payloadName = `${capitalizedName}Payload`;

if (inputRef) {
inputRef.name = inputName;
this.builder.inputType(inputRef, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
description: `Input type for ${config.name} mutation`,
fields: () => input,
});
Expand All @@ -74,6 +79,7 @@ rootBuilderProto.fieldWithInputPayload = function fieldWithInputPayload({
payloadRef.name = payloadName;
this.builder.objectType(payloadRef, {
name: payloadName,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
description: `Payload type for ${config.name} mutation`,
fields: payloadFields,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import type {
SchemaTypes,
} from '@pothos/core';

export type PayloadFieldRef<Types extends SchemaTypes, T> = Omit<
FieldRef<Types, T, 'PayloadObject'>,
'validate'
>;

export type OutputShapeFromFields<Fields extends FieldMap> =
NullableToOptional<{
[K in keyof Fields]: Fields[K] extends GenericFieldRef<infer T> ? T : never;
Expand All @@ -23,7 +28,7 @@ export type MutationWithInputPayloadOptions<
Kind extends FieldKind,
Args extends InputFieldMap,
InputFields extends InputFieldMap,
PayloadFields extends Record<string, FieldRef<Types, unknown, 'Object'>>,
PayloadFields extends Record<string, PayloadFieldRef<Types, unknown>>,
ResolveShape,
ResolveReturnShape,
> = Omit<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PrismaPlugin from '@pothos/plugin-prisma';
import RelayPlugin from '@pothos/plugin-relay';
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects';
import TracingPlugin, { isRootField } from '@pothos/plugin-tracing';
import ValidationPlugin from '@pothos/plugin-validation';
import { createSentryWrapper } from '@pothos/tracing-sentry';

import type PrismaTypes from '@src/generated/prisma/pothos-prisma-types.js';
Expand Down Expand Up @@ -57,6 +58,7 @@ export const builder = new SchemaBuilder<{
pothosStripQueryMutationPlugin,
RelayPlugin,
SimpleObjectsPlugin,
ValidationPlugin,
],
prisma: {
client: prisma,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Result } from '@prisma/client/runtime/client';

import { z } from 'zod';

import type { Prisma } from '@src/generated/prisma/client.js';

import { prisma } from '@src/services/prisma.js';
Expand All @@ -20,6 +22,7 @@ import type {
InferFieldsOutput,
InferFieldsUpdateOutput,
InferInput,
InferInputSchema,
OperationContext,
OperationHooks,
PrismaTransaction,
Expand Down Expand Up @@ -205,6 +208,49 @@ export async function transformFields<
return { data: transformedData, hooks };
}

/**
* =========================================
* Schema Generation Utilities
* =========================================
*/

/**
* Generates a Zod schema for create operations from field definitions.
*
* Extracts the Zod schema from each field definition and combines them
* into a single object schema. This schema can be used for validation
* in GraphQL resolvers, REST endpoints, tRPC procedures, or OpenAPI documentation.
*
* @template TFields - Record of field definitions
* @param fields - Field definitions to extract schemas from
* @returns Zod object schema with all fields required
*
* @example
* ```typescript
* const fields = {
* name: scalarField(z.string()),
* email: scalarField(z.string().email()),
* };
*
* const schema = generateCreateSchema(fields);
* // schema is z.object({ name: z.string(), email: z.string().email() })
*
* // Use for validation
* const validated = schema.parse({ name: 'John', email: '[email protected]' });
* ```
*/
export function generateCreateSchema<
TFields extends Record<string, AnyFieldDefinition>,
>(fields: TFields): InferInputSchema<TFields> {
const shape = Object.fromEntries(
Object.entries(fields).map(([key, field]) => [key, field.schema]),
) as {
[K in keyof TFields]: TFields[K]['schema'];
};

return z.object(shape);
}

/**
* =========================================
* Create Operation
Expand Down Expand Up @@ -296,6 +342,15 @@ export interface CreateOperationInput<
context: ServiceContext;
}

type CreateOperationFunction<
TModelName extends ModelPropName,
TFields extends Record<string, AnyFieldDefinition>,
> = (<TQueryArgs extends ModelQuery<TModelName>>(
input: CreateOperationInput<TModelName, TFields, TQueryArgs>,
) => Promise<GetPayload<TModelName, TQueryArgs>>) & {
$dataSchema: InferInputSchema<TFields>;
};

/**
* Defines a type-safe create operation for a Prisma model.
*
Expand Down Expand Up @@ -349,14 +404,14 @@ export function defineCreateOperation<
>,
>(
config: CreateOperationConfig<TModelName, TFields, TPrepareResult>,
): <TQueryArgs extends ModelQuery<TModelName>>(
input: CreateOperationInput<TModelName, TFields, TQueryArgs>,
) => Promise<GetPayload<TModelName, TQueryArgs>> {
return async <TQueryArgs extends ModelQuery<TModelName>>({
): CreateOperationFunction<TModelName, TFields> {
const createOperation = async <TQueryArgs extends ModelQuery<TModelName>>({
data,
query,
context,
}: CreateOperationInput<TModelName, TFields, TQueryArgs>) => {
}: CreateOperationInput<TModelName, TFields, TQueryArgs>): Promise<
GetPayload<TModelName, TQueryArgs>
> => {
// Throw error if query select is provided since we will not necessarily have a full result to return
if (query?.select) {
throw new Error(
Expand Down Expand Up @@ -451,6 +506,8 @@ export function defineCreateOperation<
return result as GetPayload<TModelName, TQueryArgs>;
});
};
createOperation.$dataSchema = generateCreateSchema(config.fields);
return createOperation;
}

/**
Expand Down Expand Up @@ -551,6 +608,16 @@ export interface UpdateOperationInput<
context: ServiceContext;
}

type UpdateOperationFunction<
TModelName extends ModelPropName,
TFields extends Record<string, AnyFieldDefinition>,
> = (<TQueryArgs extends ModelQuery<TModelName>>(
input: UpdateOperationInput<TModelName, TFields, TQueryArgs>,
) => Promise<GetPayload<TModelName, TQueryArgs>>) & {
$dataSchema: z.ZodObject<{
[k in keyof TFields]: z.ZodOptional<TFields[k]['schema']>;
}>;
};
/**
* Defines a type-safe update operation for a Prisma model.
*
Expand Down Expand Up @@ -605,15 +672,15 @@ export function defineUpdateOperation<
>,
>(
config: UpdateOperationConfig<TModelName, TFields, TPrepareResult>,
): <TQueryArgs extends ModelQuery<TModelName>>(
input: UpdateOperationInput<TModelName, TFields, TQueryArgs>,
) => Promise<GetPayload<TModelName, TQueryArgs>> {
return async <TQueryArgs extends ModelQuery<TModelName>>({
): UpdateOperationFunction<TModelName, TFields> {
const updateOperation = async <TQueryArgs extends ModelQuery<TModelName>>({
where,
data: inputData,
query,
context,
}: UpdateOperationInput<TModelName, TFields, TQueryArgs>) => {
}: UpdateOperationInput<TModelName, TFields, TQueryArgs>): Promise<
GetPayload<TModelName, TQueryArgs>
> => {
// Throw error if query select is provided since we will not necessarily have a full result to return
if (query?.select) {
throw new Error(
Expand Down Expand Up @@ -728,6 +795,8 @@ export function defineUpdateOperation<
return result as GetPayload<TModelName, TQueryArgs>;
});
};
updateOperation.$dataSchema = generateCreateSchema(config.fields).partial();
return updateOperation;
}

/**
Expand Down
Loading