6 Commits
0.1.3 ... 0.1.5

Author SHA1 Message Date
226cb0193b release(0.1.5)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-04-10 05:10:26 -05:00
aca4c8aa4d Add support for parsing JSON bodies
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-10 05:09:32 -05:00
adf21e67ef fix(extollo/extollo#4): remove lockfile before build on dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-30 22:34:57 -05:00
8e65a5f669 fix(extollo/extollo#4): explicitly define return type to fix build error
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-30 22:19:29 -05:00
cab2967cf6 fix(extollo/extollo#4): add prepare script to package.json and reference modules by git repo
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-30 22:15:57 -05:00
a4edecee00 enhancement(extollo/extollo#2): add kernel module for parsing request body contents, uploading files
Some checks failed
continuous-integration/drone/push Build is failing
2021-03-30 21:15:39 -05:00
8 changed files with 228 additions and 18 deletions

View File

@@ -12,6 +12,14 @@ steps:
when:
event: pull_request
- name: remove lockfile
image: glmdev/node-pnpm:latest
commands:
- rm -rf pnpm-lock.yaml
when:
event:
exclude: tag
- name: build module
image: glmdev/node-pnpm:latest
commands:

View File

@@ -1,6 +1,6 @@
{
"name": "@extollo/lib",
"version": "0.1.3",
"version": "0.1.5",
"description": "The framework library that lifts up your code.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -8,11 +8,13 @@
"lib": "lib"
},
"dependencies": {
"@extollo/di": "^0.4.4",
"@extollo/util": "^0.3.2",
"@extollo/di": "^0.4.5",
"@extollo/util": "^0.3.3",
"@types/busboy": "^0.2.3",
"@types/negotiator": "^0.6.1",
"@types/node": "^14.14.37",
"@types/pug": "^2.0.4",
"busboy": "^0.3.1",
"colors": "^1.4.0",
"dotenv": "^8.2.0",
"negotiator": "^0.6.2",
@@ -23,7 +25,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"app": "tsc && node lib/index.js"
"app": "tsc && node lib/index.js",
"prepare": "pnpm run build"
},
"files": [
"lib/**/*"

50
pnpm-lock.yaml generated
View File

@@ -1,9 +1,11 @@
dependencies:
'@extollo/di': 0.4.4
'@extollo/util': 0.3.2
'@extollo/di': 0.4.5
'@extollo/util': 0.3.3
'@types/busboy': 0.2.3
'@types/negotiator': 0.6.1
'@types/node': 14.14.37
'@types/pug': 2.0.4
busboy: 0.3.1
colors: 1.4.0
dotenv: 8.2.0
negotiator: 0.6.2
@@ -31,16 +33,16 @@ packages:
dev: false
resolution:
integrity: sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
/@extollo/di/0.4.4:
/@extollo/di/0.4.5:
dependencies:
'@extollo/util': 0.3.2
'@extollo/util': 0.3.3
'@types/node': 14.14.37
reflect-metadata: 0.1.13
typescript: 4.2.3
dev: false
resolution:
integrity: sha512-kdljHQPxunw3KmDPxuXM7u6MwwnPvOvKGGoL0ItnMxCMHUQyBJSc+NkkRhgFnqr3WHEzTNfaYVg6hRVTvs3SOA==
/@extollo/util/0.3.2:
integrity: sha512-PDYzYtFesHgdyhQKmAw1z+U13QLBOFgmrvjZgFls1Em+/P11PKWE43Ar3qXSLSIgPhrXU7grYdSyU8EGLBsekw==
/@extollo/util/0.3.3:
dependencies:
'@types/mkdirp': 1.0.1
'@types/node': 14.14.37
@@ -55,7 +57,13 @@ packages:
uuid: 8.3.2
dev: false
resolution:
integrity: sha512-cL50wHrIiRHn6W3niQZftYDgFB8K8x0dxbJPZtnt4/iy32m1aWxEx9UL8Ttldas6zDt4Ws1zBp0fInSKOKcQnQ==
integrity: sha512-qjUVSeHaOIwKcI4n1x4/t5k6Xqej/sL/0J0S3WG3s/EpRycXrAjog53/YsmQXCQcGaj7Ir5wJmLLHawZUWUfyQ==
/@types/busboy/0.2.3:
dependencies:
'@types/node': 14.14.37
dev: false
resolution:
integrity: sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU=
/@types/glob/7.1.3:
dependencies:
'@types/minimatch': 3.0.4
@@ -142,10 +150,10 @@ packages:
node: '>= 10.0.0'
resolution:
integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
/balanced-match/1.0.0:
/balanced-match/1.0.2:
dev: false
resolution:
integrity: sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
/bcrypt-pbkdf/1.0.2:
dependencies:
tweetnacl: 0.14.5
@@ -154,7 +162,7 @@ packages:
integrity: sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
/brace-expansion/1.1.11:
dependencies:
balanced-match: 1.0.0
balanced-match: 1.0.2
concat-map: 0.0.1
dev: false
resolution:
@@ -163,6 +171,14 @@ packages:
dev: false
resolution:
integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
/busboy/0.3.1:
dependencies:
dicer: 0.3.0
dev: false
engines:
node: '>=4.5.0'
resolution:
integrity: sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
/call-bind/1.0.2:
dependencies:
function-bind: 1.1.1
@@ -197,6 +213,14 @@ packages:
dev: false
resolution:
integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
/dicer/0.3.0:
dependencies:
streamsearch: 0.1.2
dev: false
engines:
node: '>=4.5.0'
resolution:
integrity: sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
/diff/4.0.2:
dev: false
engines:
@@ -578,11 +602,13 @@ packages:
resolution:
integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
specifiers:
'@extollo/di': ^0.4.4
'@extollo/util': ^0.3.2
'@extollo/di': ^0.4.5
'@extollo/util': ^0.3.3
'@types/busboy': ^0.2.3
'@types/negotiator': ^0.6.1
'@types/node': ^14.14.37
'@types/pug': ^2.0.4
busboy: ^0.3.1
colors: ^1.4.0
dotenv: ^8.2.0
negotiator: ^0.6.2

View File

@@ -0,0 +1,136 @@
import {HTTPKernelModule} from "../HTTPKernelModule"
import {HTTPKernel} from "../HTTPKernel"
import * as Busboy from "busboy"
import {Request} from "../../lifecycle/Request"
import {infer, uuid_v4} from "@extollo/util"
import {Files} from "../../../service/Files"
import {Config} from "../../../service/Config"
import {Logging} from "../../../service/Logging"
import {Injectable, Inject, Container} from "@extollo/di"
@Injectable()
export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
static register(kernel: HTTPKernel) {
const files = <Files> Container.getContainer().make(Files)
const logging = <Logging> Container.getContainer().make(Logging)
if ( !files.hasFilesystem() ) {
logging.warn(`No default filesystem is configured. This means request files will not be uploaded.`)
}
kernel.register(this).first()
}
@Inject()
protected readonly files!: Files
@Inject()
protected readonly config!: Config
@Inject()
protected readonly logging!: Logging
public async apply(request: Request): Promise<Request> {
const contentType = request.getHeader('content-type')
const contentTypes = (Array.isArray(contentType) ? contentType : [contentType])
.filter(Boolean).map(x => x!.toLowerCase())
if ( !contentType ) return request
if (
contentTypes.includes('multipart/form-data')
|| contentTypes.includes('application/x-www-form-urlencoded')
) {
return this.applyBusboy(request)
}
if ( contentTypes.includes('application/json') ) {
return this.applyJSON(request)
}
return request
}
public async applyJSON(request: Request): Promise<Request> {
await new Promise<void>((res, rej) => {
let data = ''
request.toNative().on('data', chunk => {
data += chunk
})
request.toNative().on('end', () => {
try {
const body = JSON.parse(data)
for ( const key in body ) {
if ( !body.hasOwnProperty(key) ) continue
request.parsedInput[key] = body[key]
}
res()
} catch (e) {
rej(e)
}
})
})
return request
}
public async applyBusboy(request: Request): Promise<Request> {
const config = this.config.get('server.uploads', {})
await new Promise<void>((res, rej) => {
const busboy = new Busboy({
headers: request.toNative().headers,
})
busboy.on('field', (field, val) => {
request.parsedInput[field] = infer(val)
})
busboy.on('file', async (field, file, filename, encoding, mimetype) => {
if ( !this.files.hasFilesystem() ) return
if ( !config?.enable ) {
this.logging.warn(`Skipping uploaded file '${filename}' because uploading is disabled. Set the server.uploads.enable config to allow uploads.`)
file.resume()
return
}
if ( config?.filter?.mimetype ) {
const rex = new RegExp(config.filter.mimetype)
if ( !rex.test(mimetype) ) {
this.logging.debug(`Skipping uploaded file '${filename}' because the mimetype does not match the configured filter: (${config.filter.mimetype} -> ${mimetype})`)
file.resume()
return
}
}
if ( config?.filter?.filename ) {
const rex = new RegExp(config.filter.filename)
if ( !rex.test(filename) ) {
this.logging.debug(`Skipping uploaded file '${filename}' because the file name does not match the configured filter: (${config.filter.filename} -> ${filename})`)
file.resume()
return
}
}
const fs = this.files.getFilesystem()
const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid_v4()}`
this.logging.verbose(`Uploading file in field ${field} to ${fs.getPrefix()}${storePath}`)
file.pipe(await fs.putStoreFileAsStream({ storePath })) // FIXME might need to revisit this to ensure we don't res() before pipe finishes
request.uploadedFiles[field] = request.parsedInput[field] = fs.getPath(storePath)
})
busboy.on('finish', () => {
this.logging.debug(`Parsed body input: ${JSON.stringify(request.parsedInput)}`)
res()
})
busboy.on('error', rej)
request.toNative().pipe(busboy)
})
return request
}
}

View File

@@ -1,5 +1,5 @@
import {Injectable, ScopedContainer, Container} from "@extollo/di"
import {infer} from "@extollo/util"
import {infer, UniversalPath} from "@extollo/util"
import {IncomingMessage, ServerResponse} from "http"
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
import {TLSSocket} from "tls";
@@ -84,6 +84,12 @@ export class Request extends ScopedContainer {
/** The media types accepted by the client. */
public readonly mediaTypes: string[];
/** Input parsed from the request */
public readonly parsedInput: {[key: string]: any} = {}
/** Files parsed from the request. */
public readonly uploadedFiles: {[key: string]: UniversalPath} = {}
constructor(
/** The native Node.js request. */
protected clientRequest: IncomingMessage,
@@ -162,11 +168,22 @@ export class Request extends ScopedContainer {
* @param key
*/
public input(key: string) {
if ( key in this.parsedInput ) {
return this.parsedInput[key]
}
if ( key in this.query ) {
return this.query[key]
}
}
/**
* Get the UniversalPath instance for a file uploaded in the given field on the request.
*/
public file(key: string): UniversalPath | undefined {
return this.uploadedFiles[key]
}
/**
* Returns true if the request accepts the given media type.
* @param type - a mimetype, or the short forms json, xml, or html

View File

@@ -1,6 +1,7 @@
import {Request} from "./Request";
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util"
import {ServerResponse} from "http"
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
/**
* Error thrown when the server tries to re-send headers after they have been sent once.
@@ -78,7 +79,7 @@ export class Response {
}
/** Get the HTTPCookieJar for the client. */
public get cookies() {
public get cookies(): HTTPCookieJar {
return this.request.cookies
}

View File

@@ -89,6 +89,23 @@ export class Files extends Unit {
}
await Promise.all(promises)
// Once they have initialized, register the DI token for the default filesystem
this.app().registerProducer(Filesystem, () => {
// This will throw FilesystemDoesNotExistError if no default filesystem was created
// Such behavior is desired as it is clearer than an invalid injection error, e.g.
return this.getFilesystem()
})
// If file uploads are enabled, ensure that the default upload prefix exists
if ( this.defaultFilesystem ) {
const upload = this.config.get('server.uploads', {})
if ( upload?.enable && upload?.filesystemPrefix ) {
await this.defaultFilesystem.mkdir({
storePath: upload.filesystemPrefix,
})
}
}
}
async down() {

View File

@@ -14,6 +14,7 @@ import {ExecuteResolvedRouteHandlerHTTPModule} from "../http/kernel/module/Execu
import {error} from "../http/response/ErrorResponseFactory";
import {ExecuteResolvedRoutePreflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule";
import {ExecuteResolvedRoutePostflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule";
import {ParseIncomingBodyHTTPModule} from "../http/kernel/module/ParseIncomingBodyHTTPModule";
/**
* Application unit that starts the HTTP/S server, creates Request and Response objects
@@ -42,6 +43,7 @@ export class HTTPServer extends Unit {
ExecuteResolvedRouteHandlerHTTPModule.register(this.kernel)
ExecuteResolvedRoutePreflightHTTPModule.register(this.kernel)
ExecuteResolvedRoutePostflightHTTPModule.register(this.kernel)
ParseIncomingBodyHTTPModule.register(this.kernel)
await new Promise<void>((res, rej) => {
this.server = createServer(this.handler)