Skip to content

Commit 09677d7

Browse files
committed
feat(server): add ApiHandlerService to handle request with NestJS
1 parent 82232d9 commit 09677d7

9 files changed

+112
-49
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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(private readonly httpAdapterHost: HttpAdapterHost, @Inject(ENHANCED_PRISMA) private readonly prisma: DbClientContract, @Inject(REQUEST) private readonly request: unknown) {}
17+
18+
async handleRequest(options?: ApiHandlerOptions): Promise<unknown> {
19+
const { modelMeta, zodSchemas } = loadAssets(options || {});
20+
const requestHandler = options?.handler || RPCApiHandler();
21+
const hostname = this.httpAdapterHost.httpAdapter.getRequestHostname(this.request);
22+
const requestUrl = this.httpAdapterHost.httpAdapter.getRequestUrl(this.request);
23+
// prefix with http:// to make a valid url accepted by URL constructor
24+
const url = new URL(`http://${hostname}${requestUrl}`);
25+
const method = this.httpAdapterHost.httpAdapter.getRequestMethod(this.request);
26+
const path = options?.baseUrl && url.pathname.startsWith(options.baseUrl) ? url.pathname.slice(options.baseUrl.length) : url.pathname;
27+
const searchParams = url.searchParams;
28+
const query = Object.fromEntries(searchParams);
29+
const requestBody = (this.request as {body: unknown}).body;
30+
31+
const response = await requestHandler({
32+
method,
33+
path,
34+
query,
35+
requestBody,
36+
prisma: this.prisma,
37+
modelMeta,
38+
zodSchemas,
39+
logger: options?.logger,
40+
});
41+
console.log(response)
42+
// handle handler error
43+
// if reponse code >= 400 throw nestjs HttpException
44+
// the error response will be generated by nestjs
45+
// caller can use try/catch to deal with this manually also
46+
if (response.status >= 400) {
47+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48+
throw new HttpException(response.body as Record<string, any>, response.status)
49+
}
50+
return response.body
51+
}
52+
}

packages/server/src/nestjs/index.ts

+2
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';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { AdapterBaseOptions } from "../../types";
2+
3+
export interface ApiHandlerOptions extends AdapterBaseOptions {
4+
baseUrl?: string;
5+
}
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'
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+
}
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

+3-46
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,

packages/server/tests/adapter/nestjs.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Test } from '@nestjs/testing';
22
import { loadSchema } from '@zenstackhq/testtools';
3-
import { ZenStackModule } from '../../src/nestjs';
4-
import { ENHANCED_PRISMA } from '../../src/nestjs/zenstack.module';
3+
import { ZenStackModule, ENHANCED_PRISMA } from '../../src/nestjs';
54

65
describe('NestJS adapter tests', () => {
76
const schema = `

packages/server/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"target": "ES2020",
55
"lib": ["ESNext", "DOM"],
66
"outDir": "dist",
7-
"strictPropertyInitialization": false
7+
"strictPropertyInitialization": false,
8+
"emitDecoratorMetadata": true
89
},
910
"include": ["src/**/*.ts"]
1011
}

0 commit comments

Comments
 (0)