feat: RESTful style server API handler (#405)

Co-authored-by: Jawad <jawad.stouli@gmail.com>
This commit is contained in:
Yiming 2023-05-09 20:00:38 -07:00 committed by GitHub
parent 86c57fd35b
commit f07ccdded0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 5861 additions and 2275 deletions

View file

@ -168,7 +168,8 @@ async function handleRequest(
message: err.message,
});
} else {
logError(options, undefined, (err as Error).message);
const _err = err as Error;
logError(options, undefined, _err.message + (_err.stack ? '\n' + _err.stack : ''));
res.status(500).send({
message: (err as Error).message,
});

View file

@ -2,3 +2,4 @@ export * from './omit';
export * from './password';
export * from './policy';
export * from './preset';
export * from './utils';

View file

@ -18,7 +18,13 @@ import { getVersion } from '../../version';
import { resolveField } from '../model-meta';
import { NestedWriteVisitor, VisitorContext } from '../nested-write-vistor';
import { ModelMeta, PolicyDef, PolicyFunc } from '../types';
import { enumerate, getModelFields, prismaClientKnownRequestError, prismaClientUnknownRequestError } from '../utils';
import {
enumerate,
getIdFields,
getModelFields,
prismaClientKnownRequestError,
prismaClientUnknownRequestError,
} from '../utils';
import { Logger } from './logger';
/**
@ -858,15 +864,7 @@ export class PolicyUtil {
* Gets "id" field for a given model.
*/
getIdFields(model: string) {
const fields = this.modelMeta.fields[lowerCaseFirst(model)];
if (!fields) {
throw this.unknownError(`Unable to load fields for ${model}`);
}
const result = Object.values(fields).filter((f) => f.isId);
if (result.length === 0) {
throw this.unknownError(`model ${model} does not have an id field`);
}
return result;
return getIdFields(this.modelMeta, model);
}
/**

View file

@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { AUXILIARY_FIELDS } from '@zenstackhq/sdk';
import { lowerCaseFirst } from 'lower-case-first';
import path from 'path';
import * as util from 'util';
import { DbClientContract } from '../types';
import { ModelMeta } from './types';
/**
* Wraps a value into array if it's not already one
@ -19,6 +21,21 @@ export function getModelFields(data: object) {
return data ? Object.keys(data).filter((f) => !AUXILIARY_FIELDS.includes(f)) : [];
}
/**
* Gets id fields for the given model.
*/
export function getIdFields(modelMeta: ModelMeta, model: string) {
const fields = modelMeta.fields[lowerCaseFirst(model)];
if (!fields) {
throw new Error(`Unable to load fields for ${model}`);
}
const result = Object.values(fields).filter((f) => f.isId);
if (result.length === 0) {
throw new Error(`model ${model} does not have an id field`);
}
return result;
}
/**
* Array or scalar
*/
@ -35,6 +52,9 @@ export function enumerate<T>(x: Enumerable<T>) {
}
}
/**
* Formats an object for pretty printing.
*/
export function formatObject(value: unknown) {
return util.formatWithOptions({ depth: 10 }, value);
}

View file

@ -27,14 +27,19 @@
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"change-case": "^4.1.2",
"lower-case-first": "^2.0.2",
"tiny-invariant": "^1.3.1",
"ts-japi": "^1.8.0",
"upper-case-first": "^2.0.2",
"url-pattern": "^1.0.3",
"zod": "3.21.1",
"zod-validation-error": "^0.2.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/lower-case-first": "^1.0.1",
"@types/supertest": "^2.0.12",
"@types/upper-case-first": "^1.1.2",
"@zenstackhq/testtools": "workspace:*",

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
import {
DbOperations,
isPrismaClientKnownRequestError,
isPrismaClientUnknownRequestError,
isPrismaClientValidationError,
} from '@zenstackhq/runtime';
import { ModelZodSchema } from '@zenstackhq/runtime/zod';
import { LoggerConfig, RequestContext, Response } from '../types';
import { logError, stripAuxFields, zodValidate } from '../utils';
/**
* Request handler options
*/
export type Options = {
/**
* Logging configuration. Set to `null` to disable logging.
* If unset or set to `undefined`, log will be output to console.
*/
logger?: LoggerConfig | null;
/**
* Zod schemas for validating create and update payloads. By default
* loaded from the standard output location of the `@zenstackhq/zod`
* plugin. You can pass it in explicitly if you configured the plugin
* to output to a different location.
*/
zodSchemas?: ModelZodSchema;
};
/**
* Prisma RPC style API request handler that mirrors the Prisma Client API
*/
class RequestHandler {
constructor(private readonly options: Options = {}) {}
async handleRequest({ prisma, method, path, query, requestBody }: RequestContext): Promise<Response> {
const parts = path.split('/').filter((p) => !!p);
const op = parts.pop();
const model = parts.pop();
if (parts.length !== 0 || !op || !model) {
return { status: 400, body: { message: 'invalid request path' } };
}
method = method.toUpperCase();
const dbOp = op as keyof DbOperations;
let args: unknown;
let resCode = 200;
switch (dbOp) {
case 'create':
case 'createMany':
case 'upsert':
if (method !== 'POST') {
return { status: 400, body: { message: 'invalid request method, only POST is supported' } };
}
if (!requestBody) {
return { status: 400, body: { message: 'missing request body' } };
}
args = requestBody;
// TODO: upsert's status code should be conditional
resCode = 201;
break;
case 'findFirst':
case 'findUnique':
case 'findMany':
case 'aggregate':
case 'groupBy':
case 'count':
if (method !== 'GET') {
return { status: 400, body: { message: 'invalid request method, only GET is supported' } };
}
try {
args = query?.q ? this.unmarshal(query.q as string) : {};
} catch {
return { status: 400, body: { message: 'query param must contain valid JSON' } };
}
break;
case 'update':
case 'updateMany':
if (method !== 'PUT' && method !== 'PATCH') {
return {
status: 400,
body: { message: 'invalid request method, only PUT AND PATCH are supported' },
};
}
if (!requestBody) {
return { status: 400, body: { message: 'missing request body' } };
}
args = requestBody;
break;
case 'delete':
case 'deleteMany':
if (method !== 'DELETE') {
return { status: 400, body: { message: 'invalid request method, only DELETE is supported' } };
}
try {
args = query?.q ? this.unmarshal(query.q as string) : {};
} catch {
return { status: 400, body: { message: 'query param must contain valid JSON' } };
}
break;
default:
return { status: 400, body: { message: 'invalid operation: ' + op } };
}
if (this.options.zodSchemas) {
const { data, error } = zodValidate(this.options.zodSchemas, model, dbOp, args);
if (error) {
return { status: 400, body: { message: error } };
} else {
args = data;
}
}
try {
if (!prisma[model]) {
return { status: 400, body: { message: `unknown model name: ${model}` } };
}
const result = await prisma[model][dbOp](args);
stripAuxFields(result);
return { status: resCode, body: result };
} catch (err) {
if (isPrismaClientKnownRequestError(err)) {
logError(this.options.logger, err.code, err.message);
if (err.code === 'P2004') {
// rejected by policy
return {
status: 403,
body: {
prisma: true,
rejectedByPolicy: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
};
} else {
return {
status: 400,
body: {
prisma: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
};
}
} else if (isPrismaClientUnknownRequestError(err) || isPrismaClientValidationError(err)) {
logError(this.options.logger, err.message);
return {
status: 400,
body: {
prisma: true,
message: err.message,
},
};
} else {
const _err = err as Error;
logError(this.options.logger, _err.message + (_err.stack ? '\n' + _err.stack : ''));
return {
status: 400,
body: {
message: (err as Error).message,
},
};
}
}
}
private unmarshal(value: string) {
return JSON.parse(value);
}
}
export default function makeHandler(options?: Options) {
const handler = new RequestHandler(options);
return handler.handleRequest.bind(handler);
}

View file

@ -0,0 +1,48 @@
import { DbClientContract } from '@zenstackhq/runtime';
type LoggerMethod = (message: string, code?: string) => void;
/**
* Logger config.
*/
export type LoggerConfig = {
debug?: LoggerMethod;
info?: LoggerMethod;
warn?: LoggerMethod;
error?: LoggerMethod;
};
/**
* Options for initializing an API endpoint request handler.
* @see requestHandler
*/
export type RequestHandlerOptions = {
/**
* Logger configuration. By default log to console. Set to null to turn off logging.
*/
logger?: LoggerConfig | null;
};
/**
* API request context
*/
export type RequestContext = {
prisma: DbClientContract;
method: string;
path: string;
query?: Record<string, string | string[]>;
requestBody?: unknown;
};
/**
* API response
*/
export type Response = {
status: number;
body: unknown;
};
/**
* API request handler function
*/
export type HandleRequestFn = (req: RequestContext) => Promise<Response>;

View file

@ -0,0 +1,69 @@
import { DbOperations } from '@zenstackhq/runtime';
import type { ModelZodSchema } from '@zenstackhq/runtime/zod';
import { upperCaseFirst } from 'upper-case-first';
import { fromZodError } from 'zod-validation-error';
import { AUXILIARY_FIELDS } from '@zenstackhq/sdk';
import { LoggerConfig } from './types';
export function getZodSchema(zodSchemas: ModelZodSchema, model: string, operation: keyof DbOperations) {
if (zodSchemas[model]) {
return zodSchemas[model][operation];
} else if (zodSchemas[upperCaseFirst(model)]) {
return zodSchemas[upperCaseFirst(model)][operation];
} else {
return undefined;
}
}
export function zodValidate(
zodSchemas: ModelZodSchema | undefined,
model: string,
operation: keyof DbOperations,
args: unknown
) {
const zodSchema = zodSchemas && getZodSchema(zodSchemas, model, operation);
if (zodSchema) {
const parseResult = zodSchema.safeParse(args);
if (parseResult.success) {
return { data: parseResult.data, error: undefined };
} else {
return { data: undefined, error: fromZodError(parseResult.error).message };
}
} else {
return { data: args, error: undefined };
}
}
export function logError(logger: LoggerConfig | undefined | null, message: string, code?: string) {
if (logger === undefined) {
console.error(`@zenstackhq/server: error ${code ? '[' + code + ']' : ''}, ${message}`);
} else if (logger?.error) {
logger.error(message, code);
}
}
export function logWarning(logger: LoggerConfig | undefined | null, message: string, code?: string) {
if (logger === undefined) {
console.warn(`@zenstackhq/server: error ${code ? '[' + code + ']' : ''}, ${message}`);
} else if (logger?.warn) {
logger.warn(message, code);
}
}
/**
* Recursively strip auxiliary fields from the given data.
*/
export function stripAuxFields(data: unknown) {
if (Array.isArray(data)) {
return data.forEach(stripAuxFields);
} else if (data && typeof data === 'object') {
for (const [key, value] of Object.entries(data)) {
if (AUXILIARY_FIELDS.includes(key)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (data as any)[key];
} else {
stripAuxFields(value);
}
}
}
}

View file

@ -2,7 +2,8 @@
import { DbClientContract } from '@zenstackhq/runtime';
import { getModelZodSchemas, ModelZodSchema } from '@zenstackhq/runtime/zod';
import type { Handler, Request, Response } from 'express';
import { handleRequest, LoggerConfig } from '../openapi';
import RPCAPIHandler from '../api/rpc';
import { HandleRequestFn, LoggerConfig } from '../api/types';
/**
* Express middleware options
@ -23,33 +24,38 @@ export interface MiddlewareOptions {
* (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation.
*/
zodSchemas?: ModelZodSchema | boolean;
/**
* Api request handler function
*/
handler?: HandleRequestFn;
}
/**
* Creates an Express middleware for handling CRUD requests.
*/
const factory = (options: MiddlewareOptions): Handler => {
let schemas: ModelZodSchema | undefined;
let zodSchemas: ModelZodSchema | undefined;
if (typeof options.zodSchemas === 'object') {
schemas = options.zodSchemas;
zodSchemas = options.zodSchemas;
} else if (options.zodSchemas === true) {
schemas = getModelZodSchemas();
zodSchemas = getModelZodSchemas();
}
const requestHandler = options.handler || RPCAPIHandler({ logger: options.logger, zodSchemas });
return async (request, response) => {
const prisma = (await options.getPrisma(request, response)) as DbClientContract;
if (!prisma) {
throw new Error('unable to get prisma from request context');
}
const r = await handleRequest({
const r = await requestHandler({
method: request.method,
path: request.path,
query: request.query as Record<string, string | string[]>,
query: request.query as Record<string, string>,
requestBody: request.body,
prisma,
logger: options.logger,
zodSchemas: schemas,
});
response.status(r.status).json(r.body);

View file

@ -3,7 +3,8 @@ import { DbClientContract } from '@zenstackhq/runtime';
import { getModelZodSchemas, ModelZodSchema } from '@zenstackhq/runtime/zod';
import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import { handleRequest, LoggerConfig } from '../openapi';
import RPCApiHandler from '../api/rpc';
import { HandleRequestFn, LoggerConfig } from '../api/types';
/**
* Fastify plugin options
@ -29,6 +30,11 @@ export interface PluginOptions {
* (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation.
*/
zodSchemas?: ModelZodSchema | boolean;
/**
* Api request handler function
*/
api: HandleRequestFn;
}
/**
@ -50,6 +56,8 @@ const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, d
schemas = getModelZodSchemas();
}
const requestHanler = options.api ?? RPCApiHandler({ logger: options.logger, zodSchemas: schemas });
fastify.all(`${prefix}/*`, async (request, reply) => {
const prisma = (await options.getPrisma(request, reply)) as DbClientContract;
if (!prisma) {
@ -57,14 +65,12 @@ const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, d
}
const query = request.query as Record<string, string>;
const response = await handleRequest({
const response = await requestHanler({
method: request.method,
path: (request.params as any)['*'],
query,
requestBody: request.body,
prisma,
logger: options.logger,
zodSchemas: schemas,
});
reply.status(response.status).send(response.body);

View file

@ -1,246 +0,0 @@
import {
DbClientContract,
DbOperations,
isPrismaClientKnownRequestError,
isPrismaClientUnknownRequestError,
isPrismaClientValidationError,
} from '@zenstackhq/runtime';
import type { ModelZodSchema } from '@zenstackhq/runtime/zod';
import { upperCaseFirst } from 'upper-case-first';
import { fromZodError } from 'zod-validation-error';
import { stripAuxFields } from './utils';
type LoggerMethod = (message: string, code?: string) => void;
/**
* Logger config.
*/
export type LoggerConfig = {
debug?: LoggerMethod;
info?: LoggerMethod;
warn?: LoggerMethod;
error?: LoggerMethod;
};
/**
* Options for initializing a Next.js API endpoint request handler.
* @see requestHandler
*/
export type RequestHandlerOptions = {
/**
* Logger configuration. By default log to console. Set to null to turn off logging.
*/
logger?: LoggerConfig | null;
};
/**
* OpenApi request context.
*/
export type RequestContext = {
method: string;
path: string;
query?: Record<string, string | string[]>;
requestBody?: unknown;
prisma: DbClientContract;
logger?: LoggerConfig;
zodSchemas?: ModelZodSchema;
};
/**
* OpenApi response.
*/
export type Response = {
status: number;
body: unknown;
};
function getZodSchema(zodSchemas: ModelZodSchema, model: string, operation: keyof DbOperations) {
if (zodSchemas[model]) {
return zodSchemas[model][operation];
} else if (zodSchemas[upperCaseFirst(model)]) {
return zodSchemas[upperCaseFirst(model)][operation];
} else {
return undefined;
}
}
function zodValidate(
zodSchemas: ModelZodSchema | undefined,
model: string,
operation: keyof DbOperations,
args: unknown
) {
const zodSchema = zodSchemas && getZodSchema(zodSchemas, model, operation);
if (zodSchema) {
const parseResult = zodSchema.safeParse(args);
if (parseResult.success) {
return { data: parseResult.data, error: undefined };
} else {
return { data: undefined, error: fromZodError(parseResult.error).message };
}
} else {
return { data: args, error: undefined };
}
}
/**
* Handles OpenApi requests
*/
export async function handleRequest({
method,
path,
query,
requestBody,
prisma,
logger,
zodSchemas,
}: RequestContext): Promise<Response> {
const parts = path.split('/').filter((p) => !!p);
const op = parts.pop();
const model = parts.pop();
if (parts.length !== 0 || !op || !model) {
return { status: 400, body: { message: 'invalid request path' } };
}
method = method.toUpperCase();
const dbOp = op as keyof DbOperations;
let args: unknown;
let resCode = 200;
switch (dbOp) {
case 'create':
case 'createMany':
case 'upsert':
if (method !== 'POST') {
return { status: 400, body: { message: 'invalid request method, only POST is supported' } };
}
if (!requestBody) {
return { status: 400, body: { message: 'missing request body' } };
}
args = requestBody;
// TODO: upsert's status code should be conditional
resCode = 201;
break;
case 'findFirst':
case 'findUnique':
case 'findMany':
case 'aggregate':
case 'groupBy':
case 'count':
if (method !== 'GET') {
return { status: 400, body: { message: 'invalid request method, only GET is supported' } };
}
try {
args = query?.q ? unmarshal(query.q as string) : {};
} catch {
return { status: 400, body: { message: 'query param must contain valid JSON' } };
}
break;
case 'update':
case 'updateMany':
if (method !== 'PUT' && method !== 'PATCH') {
return { status: 400, body: { message: 'invalid request method, only PUT AND PATCH are supported' } };
}
if (!requestBody) {
return { status: 400, body: { message: 'missing request body' } };
}
args = requestBody;
break;
case 'delete':
case 'deleteMany':
if (method !== 'DELETE') {
return { status: 400, body: { message: 'invalid request method, only DELETE is supported' } };
}
try {
args = query?.q ? unmarshal(query.q as string) : {};
} catch {
return { status: 400, body: { message: 'query param must contain valid JSON' } };
}
break;
default:
return { status: 400, body: { message: 'invalid operation: ' + op } };
}
if (zodSchemas) {
const { data, error } = zodValidate(zodSchemas, model, dbOp, args);
if (error) {
return { status: 400, body: { message: error } };
} else {
args = data;
}
}
try {
if (!prisma[model]) {
return { status: 400, body: { message: `unknown model name: ${model}` } };
}
const result = await prisma[model][dbOp](args);
stripAuxFields(result);
return { status: resCode, body: result };
} catch (err) {
if (isPrismaClientKnownRequestError(err)) {
logError(logger, err.code, err.message);
if (err.code === 'P2004') {
// rejected by policy
return {
status: 403,
body: {
prisma: true,
rejectedByPolicy: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
};
} else {
return {
status: 400,
body: {
prisma: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
};
}
} else if (isPrismaClientUnknownRequestError(err) || isPrismaClientValidationError(err)) {
logError(logger, err.message);
return {
status: 400,
body: {
prisma: true,
message: err.message,
},
};
} else {
const _err = err as Error;
logError(logger, _err.message + (_err.stack ? '\n' + _err.stack : ''));
return {
status: 400,
body: {
message: (err as Error).message,
},
};
}
}
}
function unmarshal(value: string) {
return JSON.parse(value);
}
function logError(logger: LoggerConfig | undefined | null, message: string, code?: string) {
if (logger === undefined) {
console.error(`@zenstackhq/server: error ${code ? '[' + code + ']' : ''}, ${message}`);
} else if (logger?.error) {
logger.error(message, code);
}
}

View file

@ -1,19 +0,0 @@
import { AUXILIARY_FIELDS } from '@zenstackhq/sdk';
/**
* Recursively strip auxiliary fields from the given data.
*/
export function stripAuxFields(data: unknown) {
if (Array.isArray(data)) {
return data.forEach(stripAuxFields);
} else if (data && typeof data === 'object') {
for (const [key, value] of Object.entries(data)) {
if (AUXILIARY_FIELDS.includes(key)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (data as any)[key];
} else {
stripAuxFields(value);
}
}
}
}

View file

@ -0,0 +1,56 @@
/// <reference types="@types/jest" />
import { loadSchema } from '@zenstackhq/testtools';
import bodyParser from 'body-parser';
import express from 'express';
import request from 'supertest';
import { ZenStackMiddleware } from '../../../src/express';
import { makeUrl, schema } from '../../utils';
import RESTAPIHandler from '../../../src/api/rest';
describe('Express adapter tests', () => {
it('run middleware', async () => {
const { prisma, zodSchemas, modelMeta } = await loadSchema(schema);
const app = express();
app.use(bodyParser.json());
app.use(
'/api',
ZenStackMiddleware({
getPrisma: () => prisma,
zodSchemas,
handler: RESTAPIHandler({ endpoint: 'http://localhost/api', modelMeta }),
})
);
let r = await request(app).get(makeUrl('/api/post/1'));
expect(r.status).toBe(404);
r = await request(app)
.post('/api/user')
.send({
data: {
type: 'user',
attributes: {
id: 'user1',
email: 'user1@abc.com',
},
},
});
expect(r.status).toBe(201);
expect(r.body).toMatchObject({
jsonapi: { version: '1.0' },
data: { type: 'user', id: 'user1', attributes: { email: 'user1@abc.com' } },
});
r = await request(app)
.put('/api/user/user1')
.send({ data: { type: 'user', attributes: { email: 'user1@def.com' } } });
expect(r.status).toBe(200);
expect(r.body.data.attributes.email).toBe('user1@def.com');
r = await request(app).delete(makeUrl('/api/user/user1', { where: { id: 'user1' } }));
expect(r.status).toBe(204);
});
});

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@ import { loadSchema } from '@zenstackhq/testtools';
import bodyParser from 'body-parser';
import express from 'express';
import request from 'supertest';
import { ZenStackMiddleware } from '../src/express';
import { makeUrl, schema } from './utils';
import { ZenStackMiddleware } from '../../../src/express';
import { makeUrl, schema } from '../../utils';
describe('Express adapter tests', () => {
it('run plugin', async () => {

View file

@ -2,8 +2,9 @@
import { loadSchema } from '@zenstackhq/testtools';
import fastify from 'fastify';
import { ZenStackFastifyPlugin } from '../src/fastify';
import { makeUrl, schema } from './utils';
import { ZenStackFastifyPlugin } from '../../../src/fastify';
import { makeUrl, schema } from '../../utils';
import Prisma from '../../../src/api/rpc';
describe('Fastify adapter tests', () => {
it('run plugin', async () => {
@ -14,6 +15,7 @@ describe('Fastify adapter tests', () => {
prefix: '/api',
getPrisma: () => prisma,
zodSchemas,
api: Prisma(),
});
let r = await app.inject({
@ -121,6 +123,7 @@ describe('Fastify adapter tests', () => {
prefix: '/api',
getPrisma: () => prisma,
zodSchemas,
api: Prisma(),
});
let r = await app.inject({

View file

@ -2,18 +2,26 @@
/// <reference types="@types/jest" />
import { loadSchema } from '@zenstackhq/testtools';
import { handleRequest } from '../src/openapi';
import { schema } from './utils';
import RPCAPIHandler from '../../../src/api/rpc';
import { schema } from '../../utils';
describe('OpenAPI server tests', () => {
let prisma: any;
let zodSchemas: any;
beforeAll(async () => {
const params = await loadSchema(schema);
prisma = params.prisma;
zodSchemas = params.zodSchemas;
});
it('crud', async () => {
const { prisma, zodSchemas } = await loadSchema(schema);
const handleRequest = RPCAPIHandler();
let r = await handleRequest({
method: 'get',
path: '/post/findMany',
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect(r.body).toHaveLength(0);
@ -36,7 +44,6 @@ describe('OpenAPI server tests', () => {
},
},
prisma,
zodSchemas,
});
expect(r.status).toBe(201);
expect(r.body).toEqual(
@ -58,7 +65,6 @@ describe('OpenAPI server tests', () => {
method: 'get',
path: '/post/findMany',
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect(r.body).toHaveLength(2);
@ -68,7 +74,6 @@ describe('OpenAPI server tests', () => {
path: '/post/findMany',
query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect(r.body).toHaveLength(1);
@ -78,7 +83,6 @@ describe('OpenAPI server tests', () => {
path: '/user/update',
requestBody: { where: { id: 'user1' }, data: { email: 'user1@def.com' } },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect((r.body as any).email).toBe('user1@def.com');
@ -88,7 +92,6 @@ describe('OpenAPI server tests', () => {
path: '/post/count',
query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect(r.body).toBe(1);
@ -98,7 +101,6 @@ describe('OpenAPI server tests', () => {
path: '/post/aggregate',
query: { q: JSON.stringify({ _sum: { viewCount: true } }) },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect((r.body as any)._sum.viewCount).toBe(3);
@ -108,7 +110,6 @@ describe('OpenAPI server tests', () => {
path: '/post/groupBy',
query: { q: JSON.stringify({ by: ['published'], _sum: { viewCount: true } }) },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect(r.body).toEqual(
@ -123,14 +124,13 @@ describe('OpenAPI server tests', () => {
path: '/user/deleteMany',
query: { q: JSON.stringify({ where: { id: 'user1' } }) },
prisma,
zodSchemas,
});
expect(r.status).toBe(200);
expect((r.body as any).count).toBe(1);
});
it('validation error', async () => {
const { prisma, zodSchemas } = await loadSchema(schema);
let handleRequest = RPCAPIHandler();
// without validation
let r = await handleRequest({
@ -141,12 +141,13 @@ describe('OpenAPI server tests', () => {
expect(r.status).toBe(400);
expect((r.body as any).message).toContain('Argument where is missing');
handleRequest = RPCAPIHandler({ zodSchemas });
// with validation
r = await handleRequest({
method: 'get',
path: '/post/findUnique',
prisma,
zodSchemas,
});
expect(r.status).toBe(400);
expect((r.body as any).message).toContain('Validation error');
@ -157,7 +158,6 @@ describe('OpenAPI server tests', () => {
path: '/post/create',
requestBody: { data: {} },
prisma,
zodSchemas,
});
expect(r.status).toBe(400);
expect((r.body as any).message).toContain('Validation error');
@ -165,7 +165,7 @@ describe('OpenAPI server tests', () => {
});
it('invalid path or args', async () => {
const { prisma } = await loadSchema(schema);
const handleRequest = RPCAPIHandler();
let r = await handleRequest({
method: 'get',

View file

@ -103,8 +103,6 @@ export async function loadSchema(
throw new Error('Could not find workspace root');
}
console.log('Workspace root:', root);
const pkgContent = fs.readFileSync(path.join(__dirname, 'package.template.json'), { encoding: 'utf-8' });
fs.writeFileSync(path.join(projectRoot, 'package.json'), pkgContent.replaceAll('<root>', root));

File diff suppressed because it is too large Load diff