@morphql/server 0.1.3 → 0.1.6

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 DELETED
@@ -1,15 +0,0 @@
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 DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "singleQuote": true,
3
- "trailingComma": "all"
4
- }
package/eslint.config.mjs DELETED
@@ -1,35 +0,0 @@
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 DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/nest-cli",
3
- "collection": "@nestjs/schematics",
4
- "sourceRoot": "src",
5
- "compilerOptions": {
6
- "deleteOutDir": true
7
- }
8
- }
package/src/app.module.ts DELETED
@@ -1,9 +0,0 @@
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 {}
package/src/auth.guard.ts DELETED
@@ -1,56 +0,0 @@
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 DELETED
@@ -1,19 +0,0 @@
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();
@@ -1,153 +0,0 @@
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
- }
@@ -1,83 +0,0 @@
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
- });
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
- }
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
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
- }
package/vitest.config.ts DELETED
@@ -1,16 +0,0 @@
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
- });