@morphql/server 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,15 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .gitignore
5
+ *.md
6
+ .env*
7
+ .docker
8
+ Dockerfile*
9
+ docker-compose*
10
+ .eslintrc*
11
+ .prettier*
12
+ jest.config*
13
+ tsconfig*.json
14
+ test
15
+ coverage
package/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all"
4
+ }
package/Dockerfile ADDED
@@ -0,0 +1,63 @@
1
+ # Stage 1: Builder
2
+ FROM node:24-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy root package files
7
+ COPY package*.json ./
8
+
9
+ # Copy workspace package files (maintaining structure)
10
+ COPY packages/core/package*.json ./packages/core/
11
+ COPY packages/server/package*.json ./packages/server/
12
+
13
+ # Install ALL dependencies (including devDeps for building)
14
+ RUN npm ci
15
+
16
+ # Copy TypeScript configs
17
+ COPY tsconfig*.json ./
18
+ COPY packages/core/tsconfig*.json ./packages/core/
19
+ COPY packages/server/tsconfig*.json ./packages/server/
20
+ COPY packages/server/nest-cli.json ./packages/server/
21
+
22
+ # Copy source code for both packages
23
+ COPY packages/core/src ./packages/core/src
24
+ COPY packages/server/src ./packages/server/src
25
+ COPY packages/server/test ./packages/server/test
26
+
27
+ # Build core first (server depends on it)
28
+ RUN npm run build --workspace=@morphql/core
29
+
30
+ # Build server
31
+ RUN npm run build --workspace=server
32
+
33
+ # Prune dev dependencies
34
+ RUN npm prune --omit=dev
35
+
36
+ # Stage 2: Production
37
+ FROM node:24-alpine AS production
38
+
39
+ WORKDIR /app
40
+
41
+ # Copy root metadata
42
+ COPY package*.json ./
43
+
44
+ # Copy the entire built workspace from builder
45
+ # This includes the pruned node_modules which already has the correct links structure
46
+ COPY --from=builder /app/node_modules ./node_modules
47
+ COPY --from=builder /app/packages ./packages
48
+
49
+ # Create non-root user
50
+ RUN addgroup -g 1001 -S nodejs && \
51
+ adduser -S nestjs -u 1001
52
+
53
+ USER nestjs
54
+
55
+ ENV NODE_ENV=production
56
+ EXPOSE 3000
57
+
58
+ # Set WORKDIR to server for correct execution context
59
+ WORKDIR /app/packages/server
60
+
61
+ # Execute using local package script
62
+ CMD ["npm", "run", "start:prod"]
63
+ #CMD ["tail", "-f", "/dev/null"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniele Traverso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # MorphQL Server
2
+
3
+ A high-performance, stateless NestJS API for the MorphQL transformation engine.
4
+
5
+ ## Overview
6
+
7
+ This server provides a RESTful interface to compile and execute Morph Query Language (MorphQL) transformations. Built with NestJS, it's designed to be a lightweight, scalable microservice that can be deployed in containerized environments.
8
+
9
+ ### Features
10
+
11
+ - 🚀 **Stateless Execution**: Designed for horizontal scaling
12
+ - 🔄 **Isomorphic Engine**: Run the exact same transformations as the client-side library
13
+ - ⚡ **Redis Caching**: Built-in compiled query caching for high-throughput scenarios
14
+ - 🐳 **Docker Ready**: Production-optimized multi-stage container images
15
+ - 🔐 **API Key Authentication**: Optional security via `X-API-KEY` header
16
+ - 📊 **Swagger Documentation**: Interactive API docs at `/api`
17
+ - 🏥 **Health Checks**: Liveness and readiness endpoints for orchestration
18
+
19
+ ## Quick Start
20
+
21
+ ### Docker Compose (Recommended)
22
+
23
+ ```bash
24
+ # Start server + Redis
25
+ docker compose up -d
26
+
27
+ # View logs
28
+ docker compose logs -f server
29
+
30
+ # Stop services
31
+ docker compose down
32
+ ```
33
+
34
+ The server will be available at `http://localhost:3000` with Swagger docs at `http://localhost:3000/api`.
35
+
36
+ ### Development Mode
37
+
38
+ ```bash
39
+ # From monorepo root
40
+ npm run server
41
+
42
+ # Or from packages/server
43
+ npm run start:dev
44
+ ```
45
+
46
+ ## API Reference
47
+
48
+ All endpoints are prefixed with `/v1`. Full interactive documentation is available at `/api` when the server is running.
49
+
50
+ ### 1. Execute Transformation
51
+
52
+ Compile and execute a query against data in a single request.
53
+
54
+ **Endpoint**: `POST /v1/execute`
55
+
56
+ **Request**:
57
+
58
+ ```json
59
+ {
60
+ "query": "from json to json transform set firstName = split(fullName, ' ')[0]",
61
+ "data": { "fullName": "John Doe" }
62
+ }
63
+ ```
64
+
65
+ **Response**:
66
+
67
+ ```json
68
+ {
69
+ "success": true,
70
+ "result": { "firstName": "John" },
71
+ "executionTime": 2.5
72
+ }
73
+ ```
74
+
75
+ **Example with curl**:
76
+
77
+ ```bash
78
+ curl -X POST http://localhost:3000/v1/execute \
79
+ -H "Content-Type: application/json" \
80
+ -d '{
81
+ "query": "from json to json transform set name = fullName",
82
+ "data": { "fullName": "Jane Smith" }
83
+ }'
84
+ ```
85
+
86
+ ### 2. Compile Query
87
+
88
+ Get the generated JavaScript code for a query without executing it.
89
+
90
+ **Endpoint**: `POST /v1/compile`
91
+
92
+ **Request**:
93
+
94
+ ```json
95
+ {
96
+ "query": "from json to xml transform set name = fullName"
97
+ }
98
+ ```
99
+
100
+ **Response**:
101
+
102
+ ```json
103
+ {
104
+ "success": true,
105
+ "code": "function(source) { /* generated code */ }"
106
+ }
107
+ ```
108
+
109
+ ### 3. Health Checks
110
+
111
+ **Liveness**: `GET /v1/health`
112
+
113
+ ```json
114
+ { "status": "ok", "timestamp": "2026-01-20T00:00:00.000Z" }
115
+ ```
116
+
117
+ **Readiness**: `GET /v1/health/ready`
118
+
119
+ - Returns `200` if service and Redis (if configured) are ready
120
+ - Returns `503` if Redis is configured but unavailable
121
+
122
+ ## Configuration
123
+
124
+ Configure the server via environment variables:
125
+
126
+ | Variable | Description | Default | Required |
127
+ | -------------- | ------------------------------------ | ------- | -------- |
128
+ | `PORT` | Server port | `3000` | No |
129
+ | `NODE_ENV` | Environment mode | - | No |
130
+ | `REDIS_HOST` | Redis hostname for caching | - | No |
131
+ | `REDIS_PORT` | Redis port | `6379` | No |
132
+ | `REDIS_PREFIX` | Cache key prefix | `morphql:` | No |
133
+ | `API_KEY` | Optional API key for auth | - | No |
134
+ | `API_KEY_FILE` | Optional API key file (for secrets) | - | No |
135
+
136
+ **Note**: If `REDIS_HOST` is not set, the server runs without caching (queries are compiled on every request).
137
+
138
+ ## Authentication
139
+
140
+ The server supports optional API key authentication via the `X-API-KEY` header.
141
+
142
+ **Enable authentication**:
143
+
144
+ ```bash
145
+ # Set API_KEY environment variable
146
+ export API_KEY=your-secret-key
147
+
148
+ # Or in docker-compose.yml
149
+ environment:
150
+ - API_KEY=your-secret-key
151
+ ```
152
+
153
+ **Making authenticated requests**:
154
+
155
+ ```bash
156
+ curl -X POST http://localhost:3000/v1/execute \
157
+ -H "X-API-KEY: your-secret-key" \
158
+ -H "Content-Type: application/json" \
159
+ -d '{"query": "...", "data": {...}}'
160
+ ```
161
+
162
+ If `API_KEY` is not set, all requests are allowed (useful for development).
163
+
164
+ ## Docker Deployment
165
+
166
+ ### Building the Image
167
+
168
+ ```bash
169
+ # From monorepo root
170
+ docker build -f packages/server/Dockerfile -t morphql-server .
171
+ ```
172
+
173
+ ### Running with Docker
174
+
175
+ ```bash
176
+ # Without Redis
177
+ docker run -p 3000:3000 morphql-server
178
+
179
+ # With Redis
180
+ docker run -p 3000:3000 \
181
+ -e REDIS_HOST=redis.example.com \
182
+ -e REDIS_PORT=6379 \
183
+ morphql-server
184
+ ```
185
+
186
+ ### Docker Compose Production
187
+
188
+ The included `docker-compose.yml` provides a production-ready setup with:
189
+
190
+ - NestJS server with health checks
191
+ - Redis for query caching
192
+ - Persistent Redis data volume
193
+ - Automatic restart policies
194
+
195
+ ## Development
196
+
197
+ ### Available Scripts
198
+
199
+ | Command | Description |
200
+ | --------------------- | ------------------------ |
201
+ | `npm run start` | Start in production mode |
202
+ | `npm run start:dev` | Start with hot-reload |
203
+ | `npm run start:debug` | Start with debugger |
204
+ | `npm run build` | Build for production |
205
+ | `npm run test` | Run unit tests |
206
+ | `npm run test:e2e` | Run end-to-end tests |
207
+ | `npm run lint` | Lint and fix code |
208
+
209
+ ### Project Structure
210
+
211
+ ```
212
+ packages/server/
213
+ ├── src/
214
+ │ ├── main.ts # Application entry point
215
+ │ ├── app.module.ts # Root module
216
+ │ ├── morph.controller.ts # API endpoints
217
+ │ └── auth.guard.ts # API key authentication
218
+ ├── test/ # E2E tests
219
+ ├── Dockerfile # Multi-stage production build
220
+ ├── docker-compose.yml # Local deployment stack
221
+ └── package.json
222
+ ```
223
+
224
+ ## Performance
225
+
226
+ - **Caching**: When Redis is enabled, compiled queries are cached indefinitely (queries are deterministic)
227
+ - **Stateless**: Each request is independent, enabling horizontal scaling
228
+ - **Async**: All endpoints use async/await for non-blocking I/O
229
+
230
+ ## Monitoring
231
+
232
+ The server provides structured logging via NestJS:
233
+
234
+ - Request routing and mapping on startup
235
+ - Error logging with stack traces
236
+ - Performance metrics in `executionTime` field
237
+
238
+ For production monitoring, consider:
239
+
240
+ - Health check endpoints for Kubernetes/Docker Swarm
241
+ - Redis monitoring for cache hit rates
242
+ - Application Performance Monitoring (APM) tools
243
+
244
+ ## License
245
+
246
+ MIT
@@ -0,0 +1,32 @@
1
+ services:
2
+ server:
3
+ build:
4
+ context: ../..
5
+ dockerfile: packages/server/Dockerfile
6
+ ports:
7
+ - "3000:3000"
8
+ environment:
9
+ - NODE_ENV=production
10
+ - REDIS_HOST=redis
11
+ - REDIS_PORT=6379
12
+ depends_on:
13
+ redis:
14
+ condition: service_healthy
15
+ restart: unless-stopped
16
+
17
+ redis:
18
+ image: redis:7-alpine
19
+ ports:
20
+ - "6379:6379"
21
+ volumes:
22
+ - redis-data:/data
23
+ command: redis-server --appendonly yes
24
+ healthcheck:
25
+ test: ["CMD", "redis-cli", "ping"]
26
+ interval: 5s
27
+ timeout: 3s
28
+ retries: 5
29
+ restart: unless-stopped
30
+
31
+ volumes:
32
+ redis-data:
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import eslint from '@eslint/js';
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4
+ import globals from 'globals';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: ['eslint.config.mjs'],
10
+ },
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommendedTypeChecked,
13
+ eslintPluginPrettierRecommended,
14
+ {
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.jest,
19
+ },
20
+ sourceType: 'commonjs',
21
+ parserOptions: {
22
+ projectService: true,
23
+ tsconfigRootDir: import.meta.dirname,
24
+ },
25
+ },
26
+ },
27
+ {
28
+ rules: {
29
+ '@typescript-eslint/no-explicit-any': 'off',
30
+ '@typescript-eslint/no-floating-promises': 'warn',
31
+ '@typescript-eslint/no-unsafe-argument': 'warn',
32
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
33
+ },
34
+ },
35
+ );
package/nest-cli.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@morphql/server",
3
+ "version": "0.1.3",
4
+ "description": "Stateless Transformation Engine API for MorphQL",
5
+ "author": "Hyperwindmill",
6
+ "private": false,
7
+ "license": "MIT",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "build": "nest build",
13
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
14
+ "start": "nest start",
15
+ "start:dev": "nest start --watch",
16
+ "start:debug": "nest start --debug --watch",
17
+ "start:prod": "node dist/main.js",
18
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:cov": "vitest run --coverage",
22
+ "test:e2e": "vitest run --config ./vitest.config.ts"
23
+ },
24
+ "dependencies": {
25
+ "@nestjs/common": "^11.0.1",
26
+ "@nestjs/core": "^11.0.1",
27
+ "@nestjs/platform-express": "^11.0.1",
28
+ "@nestjs/swagger": "^11.2.5",
29
+ "@morphql/core": "^0.1.3",
30
+ "ioredis": "^5.9.2",
31
+ "reflect-metadata": "^0.2.2",
32
+ "rxjs": "^7.8.1"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/eslintrc": "^3.2.0",
36
+ "@eslint/js": "^9.18.0",
37
+ "@nestjs/cli": "^11.0.0",
38
+ "@nestjs/schematics": "^11.0.0",
39
+ "@nestjs/testing": "^11.0.1",
40
+ "@swc/core": "^1.10.9",
41
+ "@types/express": "^5.0.0",
42
+ "@types/node": "^22.10.7",
43
+ "@types/supertest": "^6.0.2",
44
+ "eslint": "^9.18.0",
45
+ "eslint-config-prettier": "^10.0.1",
46
+ "eslint-plugin-prettier": "^5.2.2",
47
+ "globals": "^16.0.0",
48
+ "prettier": "^3.4.2",
49
+ "source-map-support": "^0.5.21",
50
+ "supertest": "^7.0.0",
51
+ "ts-loader": "^9.5.2",
52
+ "ts-node": "^10.9.2",
53
+ "tsconfig-paths": "^4.2.0",
54
+ "typescript": "^5.7.3",
55
+ "typescript-eslint": "^8.20.0",
56
+ "unplugin-swc": "^1.5.1",
57
+ "vitest": "^3.0.3"
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { MorphController } from './morph.controller.js';
3
+
4
+ @Module({
5
+ imports: [],
6
+ controllers: [MorphController],
7
+ providers: [],
8
+ })
9
+ export class AppModule {}
@@ -0,0 +1,56 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ Injectable,
5
+ UnauthorizedException,
6
+ } from '@nestjs/common';
7
+ import { Request } from 'express';
8
+ import * as fs from 'fs';
9
+
10
+ @Injectable()
11
+ export class ApiKeyGuard implements CanActivate {
12
+ private apiKey: string | null = null;
13
+
14
+ constructor() {
15
+ this.loadApiKey();
16
+ }
17
+
18
+ private loadApiKey() {
19
+ // 1. Try env var
20
+ if (process.env.API_KEY) {
21
+ this.apiKey = process.env.API_KEY;
22
+ return;
23
+ }
24
+
25
+ // 2. Try file (Docker Swarm Secret)
26
+ if (process.env.API_KEY_FILE) {
27
+ try {
28
+ if (fs.existsSync(process.env.API_KEY_FILE)) {
29
+ this.apiKey = fs
30
+ .readFileSync(process.env.API_KEY_FILE, 'utf8')
31
+ .trim();
32
+ }
33
+ } catch (e) {
34
+ console.error(
35
+ `Failed to load API key from file ${process.env.API_KEY_FILE}`,
36
+ e,
37
+ );
38
+ }
39
+ }
40
+ }
41
+
42
+ canActivate(context: ExecutionContext): boolean {
43
+ if (!this.apiKey) {
44
+ return true; // No API Key configured, allow all
45
+ }
46
+
47
+ const request = context.switchToHttp().getRequest<Request>();
48
+ const requestKey = request.headers['x-api-key'];
49
+
50
+ if (requestKey === this.apiKey) {
51
+ return true;
52
+ }
53
+
54
+ throw new UnauthorizedException('Invalid or missing API Key');
55
+ }
56
+ }
package/src/main.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { NestFactory } from '@nestjs/core';
2
+ import { AppModule } from './app.module.js';
3
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4
+
5
+ async function bootstrap() {
6
+ const app = await NestFactory.create(AppModule);
7
+
8
+ const config = new DocumentBuilder()
9
+ .setTitle('MorphQL API')
10
+ .setDescription('Stateless Transformation Engine')
11
+ .setVersion('1.0')
12
+ .addApiKey({ type: 'apiKey', name: 'X-API-KEY', in: 'header' }, 'X-API-KEY')
13
+ .build();
14
+ const document = SwaggerModule.createDocument(app, config);
15
+ SwaggerModule.setup('api', app, document);
16
+
17
+ await app.listen(process.env.PORT ?? 3000);
18
+ }
19
+ bootstrap();
@@ -0,0 +1,153 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ import {
4
+ Controller,
5
+ Post,
6
+ Get,
7
+ Body,
8
+ UseGuards,
9
+ BadRequestException,
10
+ InternalServerErrorException,
11
+ ServiceUnavailableException,
12
+ } from '@nestjs/common';
13
+ import { ApiKeyGuard } from './auth.guard.js';
14
+ import { compile } from '@morphql/core';
15
+ import { RedisCache } from '@morphql/core/cache-services';
16
+ import {
17
+ ApiTags,
18
+ ApiOperation,
19
+ ApiProperty,
20
+ ApiResponse,
21
+ ApiHeader,
22
+ } from '@nestjs/swagger';
23
+
24
+ // Initialize cache if configured
25
+ const redisHost = process.env.REDIS_HOST;
26
+ const cache = redisHost
27
+ ? new RedisCache({
28
+ host: redisHost,
29
+ port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379,
30
+ prefix: process.env.REDIS_PREFIX || 'morphql:',
31
+ })
32
+ : undefined;
33
+
34
+ export class ExecuteDto {
35
+ @ApiProperty({
36
+ description: 'The MorphQL query string',
37
+ example: 'from json to json transform set name = split(fullName, " ")',
38
+ })
39
+ query!: string;
40
+
41
+ @ApiProperty({
42
+ description: 'The source data to transform',
43
+ example: { fullName: 'John Doe' },
44
+ })
45
+ data!: Record<string, unknown>;
46
+ }
47
+
48
+ export class CompileDto {
49
+ @ApiProperty({
50
+ description: 'The MorphQL query string',
51
+ example: 'from json to json transform set name = split(fullName, " ")',
52
+ })
53
+ query!: string;
54
+ }
55
+
56
+ export class ExecuteResponseDto {
57
+ @ApiProperty()
58
+ success!: boolean;
59
+
60
+ @ApiProperty()
61
+ result!: unknown;
62
+
63
+ @ApiProperty()
64
+ executionTime!: number;
65
+ }
66
+
67
+ export class CompileResponseDto {
68
+ @ApiProperty()
69
+ success!: boolean;
70
+
71
+ @ApiProperty()
72
+ code!: string;
73
+ }
74
+
75
+ @ApiTags('Morph Engine')
76
+ @ApiHeader({
77
+ name: 'X-API-KEY',
78
+ description: 'Optional API Key for authentication',
79
+ required: false,
80
+ })
81
+ @Controller('v1')
82
+ @UseGuards(ApiKeyGuard)
83
+ export class MorphController {
84
+ @Post('execute')
85
+ @ApiOperation({ summary: 'Execute a transformation' })
86
+ @ApiResponse({ status: 200, type: ExecuteResponseDto })
87
+ async execute(@Body() body: ExecuteDto): Promise<ExecuteResponseDto> {
88
+ if (!body.query || !body.data) {
89
+ throw new BadRequestException('Missing query or data');
90
+ }
91
+
92
+ try {
93
+ const start = performance.now();
94
+ const engine = await compile(body.query, { cache });
95
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
96
+ const result = await engine(body.data);
97
+ const end = performance.now();
98
+
99
+ return {
100
+ success: true,
101
+ result,
102
+ executionTime: end - start,
103
+ };
104
+ } catch (e: unknown) {
105
+ console.error('Execute Error:', e);
106
+ const message =
107
+ e instanceof Error ? e.message : 'Unknown compilation error';
108
+ throw new InternalServerErrorException(message);
109
+ }
110
+ }
111
+
112
+ @Post('compile')
113
+ @ApiOperation({ summary: 'Compile MorphQL to JavaScript' })
114
+ @ApiResponse({ status: 200, type: CompileResponseDto })
115
+ async compile(@Body() body: CompileDto): Promise<CompileResponseDto> {
116
+ if (!body.query) {
117
+ throw new BadRequestException('Missing query');
118
+ }
119
+
120
+ try {
121
+ const engine = await compile(body.query, { cache });
122
+ return {
123
+ success: true,
124
+ code: engine.code,
125
+ };
126
+ } catch (e: unknown) {
127
+ const message =
128
+ e instanceof Error ? e.message : 'Unknown compilation error';
129
+ throw new InternalServerErrorException(message);
130
+ }
131
+ }
132
+
133
+ @Get('health')
134
+ @ApiOperation({ summary: 'Liveness check' })
135
+ @ApiResponse({ status: 200, description: 'Service is alive' })
136
+ health() {
137
+ return { status: 'ok', timestamp: new Date().toISOString() };
138
+ }
139
+
140
+ @Get('health/ready')
141
+ @ApiOperation({ summary: 'Readiness check' })
142
+ @ApiResponse({ status: 200, description: 'Service is ready' })
143
+ @ApiResponse({ status: 503, description: 'Service is not ready' })
144
+ async ready() {
145
+ if (cache && redisHost) {
146
+ const isRedisOk = await cache.ping();
147
+ if (!isRedisOk) {
148
+ throw new ServiceUnavailableException('Redis cache is unavailable');
149
+ }
150
+ }
151
+ return { status: 'ready', timestamp: new Date().toISOString() };
152
+ }
153
+ }
@@ -0,0 +1,83 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ import { Test, TestingModule } from '@nestjs/testing';
5
+ import { INestApplication } from '@nestjs/common';
6
+ import request from 'supertest';
7
+ import { App } from 'supertest/types.js';
8
+ import { AppModule } from './../src/app.module.js';
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+
11
+ describe('MorphController (e2e)', () => {
12
+ let app: INestApplication<App>;
13
+
14
+ beforeEach(async () => {
15
+ const moduleFixture: TestingModule = await Test.createTestingModule({
16
+ imports: [AppModule],
17
+ }).compile();
18
+
19
+ app = moduleFixture.createNestApplication();
20
+ await app.init();
21
+ });
22
+
23
+ describe('Health', () => {
24
+ it('/v1/health (GET)', () => {
25
+ return request(app.getHttpServer())
26
+ .get('/v1/health')
27
+ .expect(200)
28
+ .expect((res) => {
29
+ expect(res.body.status).toBe('ok');
30
+ });
31
+ });
32
+
33
+ it('/v1/health/ready (GET)', () => {
34
+ // Note: Redis might not be running in this test environment,
35
+ // but the controller handles it gracefully if not configured.
36
+ return request(app.getHttpServer())
37
+ .get('/v1/health/ready')
38
+ .expect(200)
39
+ .expect((res) => {
40
+ expect(res.body.status).toBe('ready');
41
+ });
42
+ });
43
+ });
44
+
45
+ const validQuery =
46
+ 'from json to json transform set name = "Hello " + fullName';
47
+ const testData = { fullName: 'John Doe' };
48
+
49
+ it('/v1/compile (POST)', () => {
50
+ return request(app.getHttpServer())
51
+ .post('/v1/compile')
52
+ .send({ query: validQuery })
53
+ .expect(201)
54
+ .expect((res) => {
55
+ expect(res.body.success).toBe(true);
56
+ expect(res.body.code).toBeDefined();
57
+ expect(res.body.code).toContain('function');
58
+ });
59
+ });
60
+
61
+ it('/v1/execute (POST)', () => {
62
+ return request(app.getHttpServer())
63
+ .post('/v1/execute')
64
+ .send({ query: validQuery, data: testData })
65
+ .expect(201)
66
+ .expect((res) => {
67
+ expect(res.body.success).toBe(true);
68
+ const result =
69
+ typeof res.body.result === 'string'
70
+ ? JSON.parse(res.body.result)
71
+ : res.body.result;
72
+ expect(result).toEqual({ name: 'Hello John Doe' });
73
+ expect(res.body.executionTime).toBeGreaterThanOrEqual(0);
74
+ });
75
+ });
76
+
77
+ it('/v1/execute (POST) - Missing Data', () => {
78
+ return request(app.getHttpServer())
79
+ .post('/v1/execute')
80
+ .send({ query: validQuery })
81
+ .expect(400);
82
+ });
83
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "nodenext",
4
+ "moduleResolution": "nodenext",
5
+ "resolvePackageJsonExports": true,
6
+ "esModuleInterop": true,
7
+ "isolatedModules": true,
8
+ "declaration": true,
9
+ "removeComments": true,
10
+ "emitDecoratorMetadata": true,
11
+ "experimentalDecorators": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "target": "ES2023",
14
+ "sourceMap": true,
15
+ "outDir": "./dist",
16
+ "baseUrl": "./",
17
+ "incremental": true,
18
+ "skipLibCheck": true,
19
+ "strictNullChecks": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "noImplicitAny": true,
22
+ "strictBindCallApply": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "types": ["vitest/globals", "node"]
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ import swc from 'unplugin-swc';
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ root: './',
8
+ include: ['**/*.e2e-spec.ts', '**/*.spec.ts'],
9
+ },
10
+ plugins: [
11
+ // This is required to build the test files with SWC
12
+ swc.vite({
13
+ module: { type: 'es6' },
14
+ }),
15
+ ],
16
+ });