Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
100 changes: 51 additions & 49 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class RequestHandler {
// error responses
private readonly errors: Record<string, { status: number; title: string; detail?: string }> = {
unsupportedModel: {
status: 400,
status: 404,
title: 'Unsupported model type',
detail: 'The model type is not supported',
},
Expand Down Expand Up @@ -158,6 +158,10 @@ class RequestHandler {
status: 400,
title: 'Invalid value for type',
},
forbidden: {
status: 403,
title: 'Operation is forbidden',
},
unknownError: {
status: 400,
title: 'Unknown error',
Expand Down Expand Up @@ -227,6 +231,9 @@ class RequestHandler {
}

method = method.toUpperCase();
if (!path.startsWith('/')) {
path = '/' + path;
}

try {
switch (method) {
Expand Down Expand Up @@ -346,8 +353,7 @@ class RequestHandler {
if (err instanceof InvalidValueError) {
return this.makeError('invalidValue', err.message);
} else {
const _err = err as Error;
return this.makeError('unknownError', `${_err.message}\n${_err.stack}`);
return this.handlePrismaError(err);
}
}
}
Expand Down Expand Up @@ -406,7 +412,7 @@ class RequestHandler {

const relationInfo = typeInfo.relationships[relationship];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, relationship);
return this.makeUnsupportedRelationshipError(type, relationship, 404);
}

let select: any;
Expand Down Expand Up @@ -482,7 +488,7 @@ class RequestHandler {

const relationInfo = typeInfo.relationships[relationship];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, relationship);
return this.makeUnsupportedRelationshipError(type, relationship, 404);
}

const args: any = {
Expand Down Expand Up @@ -688,7 +694,7 @@ class RequestHandler {

const relationInfo = typeInfo.relationships[key];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, key);
return this.makeUnsupportedRelationshipError(type, key, 400);
}

if (relationInfo.isCollection) {
Expand Down Expand Up @@ -737,7 +743,7 @@ class RequestHandler {

const relationInfo = typeInfo.relationships[relationship];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, relationship);
return this.makeUnsupportedRelationshipError(type, relationship, 404);
}

if (!relationInfo.isCollection && mode !== 'update') {
Expand All @@ -749,7 +755,6 @@ class RequestHandler {
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
select: { [typeInfo.idField]: true, [relationship]: { select: { [relationInfo.idField]: true } } },
};
let entity: any;

if (!relationInfo.isCollection) {
// zod-parse payload
Expand Down Expand Up @@ -797,11 +802,7 @@ class RequestHandler {
};
}

try {
entity = await prisma[type].update(updateArgs);
} catch (err) {
return this.handlePrismaError(err);
}
const entity: any = await prisma[type].update(updateArgs);

const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], {
linkers: {
Expand Down Expand Up @@ -866,7 +867,7 @@ class RequestHandler {

const relationInfo = typeInfo.relationships[key];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, key);
return this.makeUnsupportedRelationshipError(type, key, 400);
}

if (relationInfo.isCollection) {
Expand All @@ -890,15 +891,11 @@ class RequestHandler {
}
}

try {
const entity = await prisma[type].update(updatePayload);
return {
status: 200,
body: await this.serializeItems(type, entity),
};
} catch (err) {
return this.handlePrismaError(err);
}
const entity = await prisma[type].update(updatePayload);
return {
status: 200,
body: await this.serializeItems(type, entity),
};
}

private async processDelete(prisma: DbClientContract, type: any, resourceId: string): Promise<Response> {
Expand All @@ -907,17 +904,13 @@ class RequestHandler {
return this.makeUnsupportedModelError(type);
}

try {
await prisma[type].delete({
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
});
return {
status: 204,
body: undefined,
};
} catch (err) {
return this.handlePrismaError(err);
}
await prisma[type].delete({
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
});
return {
status: 204,
body: undefined,
};
}

//#region utilities
Expand Down Expand Up @@ -1005,6 +998,7 @@ class RequestHandler {
}

const serializer = new Serializer(model, {
version: '1.1',
idKey: ids[0].name,
linkers: {
resource: linker,
Expand Down Expand Up @@ -1241,6 +1235,7 @@ class RequestHandler {
}

const items: any[] = [];
let currType = typeInfo;

for (const [key, value] of Object.entries(query)) {
if (!value) {
Expand Down Expand Up @@ -1287,8 +1282,8 @@ class RequestHandler {

const fieldInfo =
filterKey === 'id'
? Object.values(typeInfo.fields).find((f) => f.isId)
: typeInfo.fields[filterKey];
? Object.values(currType.fields).find((f) => f.isId)
: currType.fields[filterKey];
if (!fieldInfo) {
return { filter: undefined, error: this.makeError('invalidFilter') };
}
Expand All @@ -1306,7 +1301,14 @@ class RequestHandler {
curr[fieldInfo.name] = this.makeFilterValue(fieldInfo, filterValue, filterOp);
} else {
// keep going
curr = curr[fieldInfo.name] = {};
if (fieldInfo.isArray) {
// collection filtering implies "some" operation
curr[fieldInfo.name] = { some: {} };
curr = curr[fieldInfo.name].some;
} else {
curr = curr[fieldInfo.name] = {};
}
currType = this.typeMap[lowerCaseFirst(fieldInfo.type)];
}
}
}
Expand Down Expand Up @@ -1418,7 +1420,7 @@ class RequestHandler {
const relation = parts[i];
const relationInfo = currType.relationships[relation];
if (!relationInfo) {
return { select: undefined, error: this.makeUnsupportedRelationshipError(type, relation) };
return { select: undefined, error: this.makeUnsupportedRelationshipError(type, relation, 400) };
}

currType = this.typeMap[lowerCaseFirst(relationInfo.type)];
Expand Down Expand Up @@ -1519,7 +1521,9 @@ class RequestHandler {

private handlePrismaError(err: unknown) {
if (isPrismaClientKnownRequestError(err)) {
if (err.code === 'P2025' || err.code === 'P2018') {
if (err.code === 'P2004') {
return this.makeError('forbidden');
} else if (err.code === 'P2025' || err.code === 'P2018') {
return this.makeError('notFound');
} else {
return {
Expand All @@ -1530,17 +1534,18 @@ class RequestHandler {
};
}
} else {
throw err;
const _err = err as Error;
return this.makeError('unknownError', `${_err.message}\n${_err.stack}`);
}
}

private makeError(code: keyof typeof this.errors, detail?: string) {
private makeError(code: keyof typeof this.errors, detail?: string, status?: number) {
return {
status: this.errors[code].status,
status: status ?? this.errors[code].status,
body: {
errors: [
{
status: this.errors[code].status,
status: status ?? this.errors[code].status,
code: paramCase(code),
title: this.errors[code].title,
detail: detail || this.errors[code].detail,
Expand All @@ -1551,14 +1556,11 @@ class RequestHandler {
}

private makeUnsupportedModelError(model: string) {
return this.makeError('unsupportedModel', `Model ${model} doesn't exist or doesn't have a single ID field`);
return this.makeError('unsupportedModel', `Model ${model} doesn't exist`);
}

private makeUnsupportedRelationshipError(model: string, relationship: string) {
return this.makeError(
'unsupportedRelationship',
`Relationship ${model}.${relationship} doesn't exist or its type doesn't have a single ID field`
);
private makeUnsupportedRelationshipError(model: string, relationship: string, status: number) {
return this.makeError('unsupportedRelationship', `Relationship ${model}.${relationship} doesn't exist`, status);
}

//#endregion
Expand Down
14 changes: 11 additions & 3 deletions packages/server/src/express/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,25 @@ const factory = (options: MiddlewareOptions): Handler => {

let query: Record<string, string | string[]> = {};
try {
query = buildUrlQuery(request.query, useSuperJson);
// express converts query parameters with square brackets into object
// e.g.: filter[foo]=bar is parsed to { filter: { foo: 'bar' } }
// we need to revert this behavior and reconstruct params from original URL
const url = request.protocol + '://' + request.get('host') + request.originalUrl;
const searchParams = new URL(url).searchParams;
const rawQuery: Record<string, string | string[]> = {};
for (const key of searchParams.keys()) {
const values = searchParams.getAll(key);
rawQuery[key] = values.length === 1 ? values[0] : values;
}
query = buildUrlQuery(rawQuery, useSuperJson);
} catch {
response.status(400).json(marshalToObject({ message: 'invalid query parameters' }, useSuperJson));
return;
}

try {
const url = request.protocol + '://' + request.get('host') + request.originalUrl;
const r = await requestHandler({
method: request.method,
url: new URL(url),
path: request.path,
query,
requestBody: unmarshalFromObject(request.body, useSuperJson),
Expand Down
2 changes: 0 additions & 2 deletions packages/server/src/fastify/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@ const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, d
}

try {
const url = request.protocol + '://' + request.hostname + request.url;
const response = await requestHanler({
method: request.method,
path: (request.params as any)['*'],
url: new URL(url),
query,
requestBody: unmarshalFromObject(request.body, useSuperJson),
prisma,
Expand Down
3 changes: 0 additions & 3 deletions packages/server/src/next/request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,9 @@ export default function factory(
const path = (req.query.path as string[]).join('/');

try {
const protocol = req.headers['x-forwarded-proto'] ?? 'http';
const url = `${protocol}://${req.headers['host']}${req.url}`;
const r = await requestHandler({
method: req.method!,
path,
url: new URL(url),
query,
requestBody: unmarshalFromObject(req.body, useSuperJson),
prisma,
Expand Down
1 change: 0 additions & 1 deletion packages/server/src/sveltekit/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export default function createHandler(options: HandlerOptions): Handle {
const r = await requestHanler({
method: event.request.method,
path,
url: event.url,
query,
requestBody,
prisma,
Expand Down
5 changes: 0 additions & 5 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ export type RequestContext = {
*/
path: string;

/**
* The request URL
*/
url: URL;

/**
* The query parameters
*/
Expand Down
57 changes: 0 additions & 57 deletions packages/server/tests/adapter/express-rest.test.ts

This file was deleted.

Loading