Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/cat-rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"type": "module",
"module": "./dist/main.js",
"dependencies": {
"@auralis/core": "workspace:*"
"@auralis/core": "workspace:*",
"@auralis/openapi": "workspace:*"
},
"devDependencies": {
"@types/supertest": "6.0.3",
Expand Down
6 changes: 5 additions & 1 deletion examples/cat-rest-api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { AuralisFactory } from "@auralis/core";
import { OpenAPI } from "@auralis/openapi";

export const app = await AuralisFactory.create();
const app = await AuralisFactory.create();
app.use(OpenAPI, {});

export { app };
4 changes: 2 additions & 2 deletions examples/cat-rest-api/src/cats/cat.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ describe("CatController /cats", () => {

it("POST /", async () => {
const response = await supertest(app.server).post("/cats").send({
name: "Mr Kettles",
name: "Mr Kittles",
age: 5,
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
id: expect.any(String),
name: "Mr Kettles",
name: "Mr Kittles",
age: 5,
});
});
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/auralis.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Auralis } from "./auralis.ts";

export interface AuralisPlugin<TPluginOptions extends object = never> {
readonly name: string;
register(app: Auralis, options?: TPluginOptions): void;
}

export function definePlugin<TPluginOptions extends object = never>(
plugin: AuralisPlugin<TPluginOptions>
): AuralisPlugin<TPluginOptions> {
return plugin;
}
227 changes: 136 additions & 91 deletions packages/core/src/auralis.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { glob } from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import type { IncomingMessage, ServerResponse } from "node:http";
import { createServer } from "node:http";
import type { Server } from "node:net";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { AuralisPlugin } from "./auralis.plugin.ts";
import type { HttpMethod } from "./decorators/http-method.decorator.ts";
import { AuralisResponseError } from "./errors/auralis-response.error.ts";
import { InternalServerError } from "./errors/internal-server-response.error.ts";
Expand Down Expand Up @@ -45,41 +46,56 @@
handlers?: Map<Function, HandlerMetadata>;
}

interface AuralisControllerHandler {
controller: Constructor;
fn: Function;
name: string;
method: HttpMethod;
path: string;
responseHeaders?: Record<string, string>;
pathVariables?: Map<
string,
{
type: Function;
index: number;
}
>;
requestBody?: {
paramName: string;
type: Constructor;
index: number;
};
passRequest?: {
paramName: string;
index: number;
};
passResponse?: {
paramName: string;
index: number;
};
}

export interface AuralisPluginHandler {
name: string;
method: HttpMethod;
path: string;
fn: (req: IncomingMessage, res: ServerResponse) => Promise<void> | void;
}

export type AuralisHandler = AuralisControllerHandler | AuralisPluginHandler;

export class Auralis {
static [AURALIS_REGISTRY_SYMBOL]: Map<Constructor, ControllerMetadata> =
new Map();

#handlers: Array<{
controller: Constructor;
fn: Function;
name: string;
method: HttpMethod;
path: string;
responseHeaders?: Record<string, string>;
pathVariables?: Map<
string,
{
type: Function;
index: number;
}
>;
requestBody?: {
paramName: string;
type: Constructor;
index: number;
};
passRequest?: {
paramName: string;
index: number;
};
passResponse?: {
paramName: string;
index: number;
};
}> = [];
#handlers: AuralisHandler[] = [];

#server?: Server;

get handlers(): ReadonlyArray<AuralisHandler> {
return this.#handlers;
}

get server(): Server {
if (!this.#server) {
throw new Error("[Auralis]: Server is not initialized");
Expand Down Expand Up @@ -185,83 +201,87 @@
console.debug("[Auralis]: Found handler for", req.method, req.url);
}

const parametersForHandler: unknown[] = [];

// Extract path variables if they exist
if (handlerRef.pathVariables) {
const regexPattern = handlerRef.path.replaceAll(
/:(\w+)/g,
(_, name) => `(?<${name as string}>[^/]+)`
);
const regex = new RegExp(regexPattern);
const match = regex.exec(req.url!);

if (match) {
if (process.env.AURALIS_DEBUG) {
console.debug("[Auralis]: Extracted path variables", match);
}

for (const [
pathVariableName,
pathVariableRef,
] of handlerRef.pathVariables) {
parametersForHandler[pathVariableRef.index] =
pathVariableRef.type(match.groups![pathVariableName]);
if ("controller" in handlerRef) {
const parametersForHandler: unknown[] = [];

// Extract path variables if they exist
if (handlerRef.pathVariables) {
const regexPattern = handlerRef.path.replaceAll(
/:(\w+)/g,
(_, name) => `(?<${name as string}>[^/]+)`
);
const regex = new RegExp(regexPattern);
const match = regex.exec(req.url!);

Check warning on line 214 in packages/core/src/auralis.ts

View workflow job for this annotation

GitHub Actions / Lint: node-24, ubuntu-latest

Forbidden non-null assertion

if (match) {
if (process.env.AURALIS_DEBUG) {
console.debug("[Auralis]: Extracted path variables", match);
}

for (const [
pathVariableName,
pathVariableRef,
] of handlerRef.pathVariables) {
parametersForHandler[pathVariableRef.index] =
pathVariableRef.type(match.groups![pathVariableName]);

Check warning on line 226 in packages/core/src/auralis.ts

View workflow job for this annotation

GitHub Actions / Lint: node-24, ubuntu-latest

Forbidden non-null assertion
}
}
}
}

// Extract request body if it exists
if (handlerRef.requestBody) {
const { type, index } = handlerRef.requestBody;

const rawBody = await new Promise<string>((resolve) => {
let data = "";
req
.on("data", (chunk) => {
data += chunk as string;
})
.on("end", () => {
resolve(data);
});
});

parametersForHandler[index] = new type(JSON.parse(rawBody));
}
// Extract request body if it exists
if (handlerRef.requestBody) {
const { type, index } = handlerRef.requestBody;

const rawBody = await new Promise<string>((resolve) => {
let data = "";
req
.on("data", (chunk) => {
data += chunk as string;
})
.on("end", () => {
resolve(data);
});
});

parametersForHandler[index] = new type(JSON.parse(rawBody));
}

if (handlerRef.responseHeaders) {
for (const [name, value] of Object.entries(
handlerRef.responseHeaders
)) {
res.setHeader(name, value);
if (handlerRef.responseHeaders) {
for (const [name, value] of Object.entries(
handlerRef.responseHeaders
)) {
res.setHeader(name, value);
}
}
}

if (handlerRef.passRequest) {
const { index } = handlerRef.passRequest;
parametersForHandler[index] = req;
}
if (handlerRef.passRequest) {
const { index } = handlerRef.passRequest;
parametersForHandler[index] = req;
}

if (handlerRef.passResponse) {
const { index } = handlerRef.passResponse;
parametersForHandler[index] = res;
}
if (handlerRef.passResponse) {
const { index } = handlerRef.passResponse;
parametersForHandler[index] = res;
}

const controllerInstance = new handlerRef.controller();
const boundFn = handlerRef.fn.bind(controllerInstance);
const controllerInstance = new handlerRef.controller();
const boundFn = handlerRef.fn.bind(controllerInstance);

const responseBody = await boundFn(...parametersForHandler);
const responseBody = await boundFn(...parametersForHandler);

if (!res.writableEnded && responseBody) {
res.write(JSON.stringify(responseBody));
}
if (!res.writableEnded && responseBody) {
res.write(JSON.stringify(responseBody));
}

if (!res.headersSent && responseBody === void 0) {
res.statusCode = 204;
if (!res.headersSent && responseBody === void 0) {
res.statusCode = 204;
}
} else {
await handlerRef.fn(req, res);
}
} else {
const notFoundResponse = new NotFoundResponseError(
`No handler found for ${req.method!} ${req.url!}`

Check warning on line 284 in packages/core/src/auralis.ts

View workflow job for this annotation

GitHub Actions / Lint: node-24, ubuntu-latest

Forbidden non-null assertion

Check warning on line 284 in packages/core/src/auralis.ts

View workflow job for this annotation

GitHub Actions / Lint: node-24, ubuntu-latest

Forbidden non-null assertion
);
notFoundResponse.handle(res);
}
Expand All @@ -285,6 +305,18 @@
});
}

use<TPluginOptions extends object>(
plugin: AuralisPlugin<TPluginOptions>,
options?: TPluginOptions
): this {
if (process.env.AURALIS_DEBUG) {
console.debug("[Auralis] Using plugin:", plugin.name);
}

plugin.register(this, options);
return this;
}

// eslint-disable-next-line @typescript-eslint/require-await
async listen(port: number): Promise<void> {
this.#server?.listen(port);
Expand Down Expand Up @@ -316,6 +348,19 @@

return `http://${host}:${address.port.toString()}`;
}

addPluginHandler(
metadata: Pick<AuralisPluginHandler, "name" | "method" | "path">,
fn: AuralisPluginHandler["fn"]
): this {
this.#handlers.push({
name: metadata.name,
method: metadata.method,
path: metadata.path,
fn,
});
return this;
}
}

function pathMatches(path: string, req: IncomingMessage): boolean {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { definePlugin } from "./auralis.plugin.ts";
export type { AuralisPlugin } from "./auralis.plugin.ts";
export type { Auralis } from "./auralis.ts";
export { Controller } from "./decorators/controller.decorator.ts";
export { Delete } from "./decorators/delete.decorator.ts";
Expand Down
14 changes: 14 additions & 0 deletions packages/openapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[![NPM package](https://img.shields.io/npm/v/@auralis/openapi.svg)](https://www.npmjs.com/package/@auralis/openapi)
[![Downloads](https://img.shields.io/npm/dt/@auralis/openapi.svg)](https://www.npmjs.com/package/@auralis/openapi)
[![Build Status](https://github.com/auralisjs/auralis/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/auralisjs/auralis/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/github/license/auralisjs/auralis.svg)](https://github.com/auralisjs/auralis/blob/main/LICENSE)

# @auralis/openapi

`@auralis/openapi` is a plugin for the Auralis framework that adds support for OpenAPI specifications.

# UNDER CONSTRUCTION

Right now there is no stable version of Auralis available. We just working on it.

<img src="https://chronicle-brightspot.s3.amazonaws.com/6a/c4/00e4ab3143f7e0cf4d9fd33aa00b/constructocat2.jpg" width="400px" />
Loading
Loading