Skip to content

Commit 49d9645

Browse files
authored
feat(server): add ApiHandlerService to handle request with NestJS (#2099)
1 parent 506d958 commit 49d9645

File tree

9 files changed

+345
-51
lines changed

9 files changed

+345
-51
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { DbClientContract } from '@zenstackhq/runtime';
2+
import { HttpException, Inject, Injectable, Scope } from "@nestjs/common";
3+
import { HttpAdapterHost, REQUEST } from "@nestjs/core";
4+
import { loadAssets } from "../shared";
5+
import { RPCApiHandler } from '../api/rpc';
6+
import { ENHANCED_PRISMA } from "./zenstack.constants";
7+
import { ApiHandlerOptions } from './interfaces';
8+
9+
/**
10+
* The ZenStack API handler service for NestJS. The service is used to handle API requests
11+
* and forward them to the ZenStack API handler. It is platform agnostic and can be used
12+
* with any HTTP adapter.
13+
*/
14+
@Injectable({ scope: Scope.REQUEST })
15+
export class ApiHandlerService {
16+
constructor(
17+
private readonly httpAdapterHost: HttpAdapterHost,
18+
@Inject(ENHANCED_PRISMA) private readonly prisma: DbClientContract,
19+
@Inject(REQUEST) private readonly request: unknown
20+
) { }
21+
22+
async handleRequest(options?: ApiHandlerOptions): Promise<unknown> {
23+
const { modelMeta, zodSchemas } = loadAssets(options || {});
24+
const requestHandler = options?.handler || RPCApiHandler();
25+
const hostname = this.httpAdapterHost.httpAdapter.getRequestHostname(this.request);
26+
const requestUrl = this.httpAdapterHost.httpAdapter.getRequestUrl(this.request);
27+
// prefix with http:// to make a valid url accepted by URL constructor
28+
const url = new URL(`http://${hostname}${requestUrl}`);
29+
const method = this.httpAdapterHost.httpAdapter.getRequestMethod(this.request);
30+
const path = options?.baseUrl && url.pathname.startsWith(options.baseUrl) ? url.pathname.slice(options.baseUrl.length) : url.pathname;
31+
const searchParams = url.searchParams;
32+
const query = Object.fromEntries(searchParams);
33+
const requestBody = (this.request as { body: unknown }).body;
34+
35+
const response = await requestHandler({
36+
method,
37+
path,
38+
query,
39+
requestBody,
40+
prisma: this.prisma,
41+
modelMeta,
42+
zodSchemas,
43+
logger: options?.logger,
44+
});
45+
46+
// handle handler error
47+
// if response code >= 400 throw nestjs HttpException
48+
// the error response will be generated by nestjs
49+
// caller can use try/catch to deal with this manually also
50+
if (response.status >= 400) {
51+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52+
throw new HttpException(response.body as Record<string, any>, response.status)
53+
}
54+
return response.body
55+
}
56+
}

packages/server/src/nestjs/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './zenstack.module';
2+
export * from './api-handler.service';
3+
export * from './zenstack.constants';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { AdapterBaseOptions } from "../../types";
2+
3+
export interface ApiHandlerOptions extends AdapterBaseOptions {
4+
/**
5+
* The base URL for the API handler. This is used to determine the base path for the API requests.
6+
* If you are using the ApiHandlerService in a route with a prefix, you should set this to the prefix.
7+
*
8+
* e.g.
9+
* without baseUrl(API handler default route):
10+
* - RPC API handler: [model]/findMany
11+
* - RESTful API handler: /:type
12+
*
13+
* with baseUrl(/api/crud):
14+
* - RPC API handler: /api/crud/[model]/findMany
15+
* - RESTful API handler: /api/crud/:type
16+
*/
17+
baseUrl?: string;
18+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './zenstack-module-options.interface'
2+
export * from './api-handler-options.interface'
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { FactoryProvider, ModuleMetadata, Provider } from "@nestjs/common";
2+
3+
/**
4+
* ZenStack module options.
5+
*/
6+
export interface ZenStackModuleOptions {
7+
/**
8+
* A callback for getting an enhanced `PrismaClient`.
9+
*/
10+
getEnhancedPrisma: (model?: string | symbol) => unknown;
11+
}
12+
13+
/**
14+
* ZenStack module async registration options.
15+
*/
16+
export interface ZenStackModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
17+
/**
18+
* Whether the module is global-scoped.
19+
*/
20+
global?: boolean;
21+
22+
/**
23+
* The token to export the enhanced Prisma service. Default is {@link ENHANCED_PRISMA}.
24+
*/
25+
exportToken?: string;
26+
27+
/**
28+
* The factory function to create the enhancement options.
29+
*/
30+
useFactory: (...args: unknown[]) => Promise<ZenStackModuleOptions> | ZenStackModuleOptions;
31+
32+
/**
33+
* The dependencies to inject into the factory function.
34+
*/
35+
inject?: FactoryProvider['inject'];
36+
37+
/**
38+
* Extra providers to facilitate dependency injection.
39+
*/
40+
extraProviders?: Provider[];
41+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* The default token used to export the enhanced Prisma service.
3+
*/
4+
export const ENHANCED_PRISMA = 'ENHANCED_PRISMA';

packages/server/src/nestjs/zenstack.module.ts

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,6 @@
1-
import { Module, type DynamicModule, type FactoryProvider, type ModuleMetadata, type Provider } from '@nestjs/common';
2-
3-
/**
4-
* The default token used to export the enhanced Prisma service.
5-
*/
6-
export const ENHANCED_PRISMA = 'ENHANCED_PRISMA';
7-
8-
/**
9-
* ZenStack module options.
10-
*/
11-
export interface ZenStackModuleOptions {
12-
/**
13-
* A callback for getting an enhanced `PrismaClient`.
14-
*/
15-
getEnhancedPrisma: (model?: string | symbol ) => unknown;
16-
}
17-
18-
/**
19-
* ZenStack module async registration options.
20-
*/
21-
export interface ZenStackModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
22-
/**
23-
* Whether the module is global-scoped.
24-
*/
25-
global?: boolean;
26-
27-
/**
28-
* The token to export the enhanced Prisma service. Default is {@link ENHANCED_PRISMA}.
29-
*/
30-
exportToken?: string;
31-
32-
/**
33-
* The factory function to create the enhancement options.
34-
*/
35-
useFactory: (...args: unknown[]) => Promise<ZenStackModuleOptions> | ZenStackModuleOptions;
36-
37-
/**
38-
* The dependencies to inject into the factory function.
39-
*/
40-
inject?: FactoryProvider['inject'];
41-
42-
/**
43-
* Extra providers to facilitate dependency injection.
44-
*/
45-
extraProviders?: Provider[];
46-
}
1+
import { Module, type DynamicModule } from '@nestjs/common';
2+
import { ENHANCED_PRISMA } from './zenstack.constants';
3+
import { ZenStackModuleAsyncOptions } from './interfaces';
474

485
/**
496
* The ZenStack module for NestJS. The module exports an enhanced Prisma service,

0 commit comments

Comments
 (0)