diff --git a/package-lock.json b/package-lock.json index 736d600..655182c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/jest": "^27.0.1", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/type-is": "^1.6.3", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", "eslint": "^7.30.0", @@ -2150,6 +2151,15 @@ "@types/superagent": "*" } }, + "node_modules/@types/type-is": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", + "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -10950,6 +10960,15 @@ "@types/superagent": "*" } }, + "@types/type-is": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", + "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", diff --git a/package.json b/package.json index 6063dc2..7c409c9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "author": "", "private": true, - "license": "UNLICENSED", + "license": "MIT", "scripts": { "prebuild": "rimraf dist", "build": "nest build", @@ -36,6 +36,7 @@ "@types/jest": "^27.0.1", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/type-is": "^1.6.3", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", "eslint": "^7.30.0", diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..564f24a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,15 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { MiddlewareConsumer, Module } from '@nestjs/common'; +import { RawParserMiddleware } from './raw-parser.middleware'; +import { ScenesController } from './scenes/scenes.controller'; +import { MemoryService } from './storages/memory.service'; @Module({ imports: [], - controllers: [AppController], - providers: [AppService], + controllers: [ScenesController], + providers: [MemoryService], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(RawParserMiddleware).forRoutes('**'); + } +} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/main.ts b/src/main.ts index 13cad38..b77df16 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,11 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(3000); + const app = await NestFactory.create(AppModule, { + cors: true, + }); + app.setGlobalPrefix('api/v2'); + + await app.listen(8080); } bootstrap(); diff --git a/src/raw-parser.middleware.spec.ts b/src/raw-parser.middleware.spec.ts new file mode 100644 index 0000000..1ddb6c9 --- /dev/null +++ b/src/raw-parser.middleware.spec.ts @@ -0,0 +1,7 @@ +import { RawParserMiddleware } from './raw-parser.middleware'; + +describe('RawParserMiddleware', () => { + it('should be defined', () => { + expect(new RawParserMiddleware()).toBeDefined(); + }); +}); diff --git a/src/raw-parser.middleware.ts b/src/raw-parser.middleware.ts new file mode 100644 index 0000000..e2a7d19 --- /dev/null +++ b/src/raw-parser.middleware.ts @@ -0,0 +1,14 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { raw } from 'express'; +import { hasBody } from 'type-is'; + +// Excalidraw Front end doesn't send a Content Type Header +// so we tell raw parser to check if there is a body +const rawParserMiddleware = raw({ type: hasBody }); + +@Injectable() +export class RawParserMiddleware implements NestMiddleware { + use(req: any, res: any, next: () => void) { + rawParserMiddleware(req, res, next); + } +} diff --git a/src/scenes/scenes.controller.spec.ts b/src/scenes/scenes.controller.spec.ts new file mode 100644 index 0000000..513c92d --- /dev/null +++ b/src/scenes/scenes.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ScenesController } from './scenes.controller'; + +describe('ScenesController', () => { + let controller: ScenesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ScenesController], + }).compile(); + + controller = module.get(ScenesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/scenes/scenes.controller.ts b/src/scenes/scenes.controller.ts new file mode 100644 index 0000000..41ae06a --- /dev/null +++ b/src/scenes/scenes.controller.ts @@ -0,0 +1,42 @@ +import { + Body, + Controller, + Get, + Header, + Param, + Post, + Res, +} from '@nestjs/common'; +import { Response } from 'express'; +import { MemoryService } from 'src/storages/memory.service'; +import { hash, hexadecimalToDecimal } from 'src/utils'; +import { Readable } from 'stream'; + +@Controller() +export class ScenesController { + constructor(private storageService: MemoryService) {} + @Get(':id') + @Header('content-type', 'application/octet-stream') + async findOne(@Param() params, @Res() res: Response): Promise { + const data = await this.storageService.load(params.id); + + const stream = new Readable(); + stream.push(data); + stream.push(null); + stream.pipe(res); + } + + @Post() + async create(@Body() payload: Buffer) { + + const drawingHash = hash(payload); + const id = hexadecimalToDecimal(drawingHash); + + await this.storageService.save(id, payload); + + return { + id, + data: `http://localhost:8080/api/v2/${id}`, + }; + } +} diff --git a/src/storages/memory.service.spec.ts b/src/storages/memory.service.spec.ts new file mode 100644 index 0000000..6394555 --- /dev/null +++ b/src/storages/memory.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MemoryService } from './memory.service'; + +describe('MemoryService', () => { + let service: MemoryService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MemoryService], + }).compile(); + + service = module.get(MemoryService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/storages/memory.service.ts b/src/storages/memory.service.ts new file mode 100644 index 0000000..08e4055 --- /dev/null +++ b/src/storages/memory.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { StorageService } from './storageService'; + +@Injectable() +export class MemoryService implements StorageService { + + scenesMap = new Map(); + + async save(id: string, data: Buffer): Promise { + this.scenesMap.set(id, data); + return true; + } + + async load(id: string): Promise { + return this.scenesMap.get(id); + } +} diff --git a/src/storages/storageService.ts b/src/storages/storageService.ts new file mode 100644 index 0000000..01239a0 --- /dev/null +++ b/src/storages/storageService.ts @@ -0,0 +1,5 @@ +export interface StorageService { + save(id: string, data: Buffer): Promise; + + load(id: string): Promise; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..ee733e4 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,12 @@ +import { createHash } from 'crypto'; + +export function hash(buffer): string { + return createHash(`sha256`).update(buffer).digest(`hex`); +} + +// Copied from https://github.com/NMinhNguyen/excalidraw-json +export function hexadecimalToDecimal(hexadecimal: string) { + // See https://stackoverflow.com/a/53751162 + const bigInt = BigInt(`0x${hexadecimal}`); + return bigInt.toString(10); +}