Skip to content

Commit

Permalink
feat: add basepath option (#316)
Browse files Browse the repository at this point in the history
* feat: add basepath option

* test(cli): update cli tests with basepath

* refactor: simplify and improve the creation of App with a basepath
  • Loading branch information
matteo-cristino authored Aug 27, 2024
1 parent 385dfe2 commit 23416d1
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 14 deletions.
25 changes: 24 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,39 @@ program
.default('/docs')
)
.addOption(
new Option('--hostname <string>', 'Provide the hostname to serve the server')
new Option('--hostname <string>', 'specify the hostname to serve the server')
.env('HOSTNAME')
.default('0.0.0.0')
.argParser((h) => {
if (h.includes('/')) {
L.error(`${bad} ${h} is not a valid hostname, subpath should be specified using --basepath option`);
process.exit(0);
}
return h;
})
)
.addOption(
new Option('--template <file>', 'Provide the html template for the applets').default(
'./applet_template.html'
)
)
.addOption(new Option('-D, --debug', 'debug').env('DEBUG').default(false).argParser(Boolean))
.addOption(new Option('--basepath <string>', 'specify the basepath for all APIs')
.env('BASEPATH')
.default('')
.argParser((b) => {
if ( b === '') return b;
if (!b.startsWith('/')) {
L.error(`${bad} ${b} is not a valid subpath, should start with a '/'`);
process.exit(0);
}
if(b.endsWith('/')) {
L.error(`${bad} ${b} is not a valid subpath, should not end with a '/'`);
process.exit(0);
}
return b;
})
)
.version(data.version, '-v, --version')
.addHelpText(
'after',
Expand Down
16 changes: 7 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@ import mime from 'mime';
import path from 'path';
import { IMeta } from 'tslog';
import {
App,
HttpResponse,
TemplatedApp,
us_listen_socket,
us_listen_socket_close,
us_socket_local_port,
LIBUS_LISTEN_EXCLUSIVE_PORT
} from 'uWebSockets.js';
Expand All @@ -31,7 +28,7 @@ import { SlangroomManager } from './slangroom.js';
import { formatContract } from './fileUtils.js';
import { getSchema, getQueryParams, prettyChain, newMetadata } from './utils.js';
import { forbidden, notFound, unprocessableEntity, internalServerError } from './responseUtils.js';
import { generateRoute, runPrecondition } from './routeUtils.js';
import { createAppWithBasePath, generateRoute, runPrecondition } from './routeUtils.js';

import { execute as slangroomChainExecute } from '@dyne/slangroom-chain';
dotenv.config();
Expand Down Expand Up @@ -77,7 +74,7 @@ const setupProm = async (app: TemplatedApp) => {
};

const ncrApp = async () => {
const app = App()
const app = createAppWithBasePath(config.basepath)
.get('/', (res, req) => {
const files = Dir.paths.map((f) => `http://${req.getHeader('host')}${f}`);
res
Expand All @@ -93,16 +90,17 @@ const ncrApp = async () => {
await Promise.all(
Dir.files.map(async (endpoints) => {
const { path, metadata } = endpoints;
const prefixedPath = config.basepath + path;
if (definition.paths && !metadata.hidden && !metadata.hideFromOpenapi) {
const schema = await getSchema(endpoints);
if (schema)
definition.paths[path] = generatePath(
definition.paths[prefixedPath] = generatePath(
endpoints.contract ?? prettyChain(endpoints.chain),
schema,
metadata
);
definition.paths[path + '/raw'] = generateRawPath();
definition.paths[path + '/app'] = generateAppletPath();
definition.paths[prefixedPath + '/raw'] = generateRawPath();
definition.paths[prefixedPath + '/app'] = generateAppletPath();
}
})
);
Expand Down Expand Up @@ -241,7 +239,7 @@ Dir.ready(async () => {
if (socket) {
const port = us_socket_local_port(socket);
listen_socket = socket;
L.info(`Swagger UI is running on http://${config.hostname}:${port}/docs`);
L.info(`Swagger UI is running on http://${config.hostname}:${port}${config.basepath}${config.openapiPath}`);
} else {
L.error('Port already in use ' + config.port);
throw new Error('Port already in use ' + config.port);
Expand Down
3 changes: 2 additions & 1 deletion src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Type } from '@sinclair/typebox';
import { OpenAPIV3_1 } from 'openapi-types';
import p from '../package.json' with { type: 'json' };
import { JSONSchema, Metadata } from './types.js';
import { config } from './cli.js';

export function generateRawPath(): OpenAPIV3_1.PathItemObject {
return {
Expand Down Expand Up @@ -169,7 +170,7 @@ export const openapiTemplate = `
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: '/oas.json',
url: '${config.basepath}/oas.json',
dom_id: '#swagger-ui'
});
};
Expand Down
76 changes: 73 additions & 3 deletions src/routeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import fs from 'fs';
import _ from 'lodash';
import { HttpResponse, TemplatedApp } from 'uWebSockets.js';
import { App, HttpRequest, HttpResponse, TemplatedApp } from 'uWebSockets.js';
import { execute as slangroomChainExecute } from '@dyne/slangroom-chain';

import { reportZenroomError } from './error.js';
Expand All @@ -15,6 +15,76 @@ import { forbidden, methodNotAllowed, notFound, unprocessableEntity } from './re
import { getSchema, validateData, getQueryParams } from './utils.js';
import { template as proctoroom } from './applets.js';

//

type MethodNames =
| 'get'
| 'post'
| 'options'
| 'del'
| 'patch'
| 'put'
| 'head'
| 'connect'
| 'trace'
| 'any'
| 'ws';

// type MethodNames = 'get' | 'post' | 'options' | 'del' | 'patch' | 'put' | 'head' | 'connect' | 'trace' | 'any' | 'ws';

export const createAppWithBasePath = (basepath: string): TemplatedApp => {
const app = App();

const wrapPatternMethod = (methodName: MethodNames) => {
const originalMethod = app[methodName].bind(app);
return (pattern: string, ...args: any[]): TemplatedApp => {
const prefixedPattern = `${basepath}${pattern}`;
originalMethod(prefixedPattern, ...args);
return wrappedApp; // Return the wrapped app instance for chaining
};
};

const wrapMethod = (methodName: keyof TemplatedApp) => {
return (...args: any[]): TemplatedApp => {
(app as any)[methodName](...args);
return wrappedApp;
};
};

const wrappedApp: TemplatedApp = {
numSubscribers: app.numSubscribers.bind(app),
publish: app.publish.bind(app),

listen: (...args: any[]) => wrapMethod('listen')(...args),
listen_unix: (...args: any[]) => wrapMethod('listen_unix')(...args),
publish: (...args: any[]) => wrapMethod('publish')(...args),
numSubscribers: (...args: any[]) => wrapMethod('numSubscribers')(...args),
addServerName: (...args: any[]) => wrapMethod('addServerName')(...args),
domain: (...args: any[]) => wrapMethod('domain')(...args),
removeServerName: (...args: any[]) => wrapMethod('removeServerName')(...args),
missingServerName: (...args: any[]) => wrapMethod('missingServerName')(...args),
filter: (...args: any[]) => wrapMethod('filter')(...args),
close: (...args: any[]) => wrapMethod('close')(...args),

// Pattern-based methods with basepath wrapping
get: wrapPatternMethod('get'),
post: wrapPatternMethod('post'),
options: wrapPatternMethod('options'),
del: wrapPatternMethod('del'),
patch: wrapPatternMethod('patch'),
put: wrapPatternMethod('put'),
head: wrapPatternMethod('head'),
connect: wrapPatternMethod('connect'),
trace: wrapPatternMethod('trace'),
any: wrapPatternMethod('any'),
ws: wrapPatternMethod('ws'),
};

return wrappedApp;
}

//

const L = config.logger;
const emoji = {
add: '📜',
Expand Down Expand Up @@ -294,10 +364,10 @@ export const generateRoute = async (
schema: JSON.stringify(schema),
title: path || 'Welcome 🥳 to ',
description: contract,
endpoint: `${path}`
endpoint: `${config.basepath}${path}`
});

res.writeStatus('200 OK').writeHeader('Content-Type', 'text/html').end(result);
});
L.info(`${emoji[action]} ${path}`);
L.info(`${emoji[action]} ${config.basepath}${path}`);
};
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Config {
template: string;
logger: Logger<ILogObj>;
publicDirectory: string | undefined;
basepath: string;
}

export interface Endpoints {
Expand Down
1 change: 1 addition & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { program } from '../src/cli';

const res = {
basepath: '',
debug: false,
hostname: '0.0.0.0',
openapiPath: '/docs',
Expand Down

0 comments on commit 23416d1

Please sign in to comment.