diff --git a/examples/basic-server/src/index.ts b/examples/basic-server/src/index.ts
index ac8a1f1..590005f 100644
--- a/examples/basic-server/src/index.ts
+++ b/examples/basic-server/src/index.ts
@@ -1,26 +1,26 @@
-import { prismy, res, router } from 'prismy'
+import { prismy, Result, router } from 'prismy'
export const rootHandler = prismy([], () => {
- return res(
+ return Result(
[
'',
'
',
'Root Page
',
'Go to /test',
'',
- ].join('')
+ ].join(''),
)
})
const testHandler = prismy([], () => {
- return res(
+ return Result(
[
'',
'',
'Test Page
',
'Go to Root',
'',
- ].join('')
+ ].join(''),
)
})
diff --git a/examples/file-upload/specs/file.spec.ts b/examples/file-upload/specs/file.spec.ts
new file mode 100644
index 0000000..3c5a0ac
--- /dev/null
+++ b/examples/file-upload/specs/file.spec.ts
@@ -0,0 +1,226 @@
+import { testServerManager } from '../../../specs/helpers'
+import {
+ ErrorResult,
+ getPrismyContext,
+ Middleware,
+ prismy,
+ Result,
+} from '../../../src'
+import { MultipartBodySelector } from './file'
+import path from 'path'
+import { File } from 'buffer'
+import fs from 'fs'
+import { nanoid } from 'nanoid'
+import Formidable from 'formidable'
+
+const testBoundary = `------test-boundary-${nanoid()}`
+const testUploadRoot = path.join(process.cwd(), 'file-test-dest')
+
+beforeAll(async () => {
+ await testServerManager.start()
+})
+
+afterAll(async () => {
+ await testServerManager.close()
+ fs.rmSync(testUploadRoot, { recursive: true })
+ fs.mkdirSync(testUploadRoot)
+})
+
+describe('MultipartBodySelector', () => {
+ it('parse a file and a field (FormData)', async () => {
+ const handler = MultipartTestHandler()
+
+ const formData = new FormData()
+ const fileBuffer = fs.readFileSync(
+ path.join(process.cwd(), 'specs/dummyFiles/smallFile.txt'),
+ )
+ const fileBlob = new Blob([fileBuffer])
+ const file = new File([fileBlob], 'smallFile.txt')
+ formData.append('testFile', file)
+ formData.append('testField', 'testValue')
+
+ const response = await testServerManager.loadRequestListenerAndCall(
+ handler,
+ '/',
+ {
+ method: 'post',
+ body: formData,
+ },
+ )
+
+ expect(response).toMatchObject({
+ statusCode: 200,
+ })
+ expect(JSON.parse(response.body)).toMatchObject({
+ fields: {
+ testField: 'testValue',
+ },
+ files: {
+ testFile: [
+ {
+ filepath: expect.stringMatching(
+ new RegExp(testUploadRoot + '/[A-z0-9]+'),
+ ),
+ mimetype: 'application/octet-stream',
+ newFilename: expect.stringMatching('[A-z0-9]+'),
+ originalFilename: 'smallFile.txt',
+ size: file.size,
+ },
+ ],
+ },
+ })
+ })
+
+ it('parse a file and a field (Raw)', async () => {
+ const handler = MultipartTestHandler()
+
+ const response = await testServerManager.loadRequestListenerAndCall(
+ handler,
+ '/',
+ {
+ method: 'post',
+ headers: {
+ 'content-type': `multipart/form-data; boundary=${testBoundary}`,
+ },
+ body: [
+ '--' + testBoundary,
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '--' + testBoundary,
+ 'Content-Disposition: form-data; ' +
+ 'name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '--' + testBoundary + '--',
+ ].join('\r\n'),
+ },
+ )
+
+ expect(JSON.parse(response.body)).toMatchObject({
+ fields: [
+ {
+ name: 'file_name_0',
+ value: 'super alpha file',
+ info: {
+ encoding: '7bit',
+ mimeType: 'text/plain',
+ nameTruncated: false,
+ valueTruncated: false,
+ },
+ },
+ ],
+ files: [
+ {
+ filePath: expect.stringMatching(
+ new RegExp(
+ `${testUploadRoot}/prismy-temp-dir-[A-z0-9_-]+/prismy-upload-[A-z0-9_-]+`,
+ ),
+ ),
+ info: {
+ encoding: '7bit',
+ filename: '1k_a.dat',
+ mimeType: 'application/octet-stream',
+ },
+ name: 'upload_file_0',
+ },
+ ],
+ })
+ expect(response).toMatchObject({
+ statusCode: 200,
+ })
+ })
+
+ it('throws when body is empty', async () => {
+ const handler = MultipartTestHandler()
+
+ const response = await testServerManager.loadRequestListenerAndCall(
+ handler,
+ '/',
+ {
+ method: 'post',
+ headers: {
+ 'content-type': `multipart/form-data; boundary=${testBoundary}`,
+ },
+ body: '',
+ },
+ )
+
+ expect(JSON.parse(response.body)).toMatchObject({
+ error: expect.stringContaining('Error: Unexpected end of form'),
+ })
+ expect(response).toMatchObject({
+ statusCode: 400,
+ })
+ })
+
+ it('throws if field size hits limit', async () => {
+ const handler = MultipartTestHandler({
+ maxFileSize: 13,
+ maxFieldsSize: 3,
+ })
+
+ const response = await testServerManager.loadRequestListenerAndCall(
+ handler,
+ '/',
+ {
+ method: 'post',
+ headers: {
+ 'content-type': `multipart/form-data; boundary=${testBoundary}`,
+ },
+ body: [
+ '--' + testBoundary,
+ 'Content-Disposition: form-data; name="file_name_0"',
+ '',
+ 'super alpha file',
+ '--' + testBoundary,
+ 'Content-Disposition: form-data; ' +
+ 'name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '--' + testBoundary + '--',
+ ].join('\r\n'),
+ },
+ )
+
+ const jsonBody = JSON.parse(response.body)
+ expect(jsonBody).toMatchObject({
+ error: expect.stringContaining('Error: options.maxFieldsSize'),
+ })
+ expect(response).toMatchObject({
+ statusCode: 413,
+ })
+ })
+})
+
+const errorDataMap = new WeakMap()
+function MultipartTestHandler(options: Formidable.Options = {}) {
+ const multipartBodySelector = MultipartBodySelector({
+ uploadDir: testUploadRoot,
+ ...options,
+ })
+ return prismy(
+ [multipartBodySelector],
+ (body) => {
+ return Result(body)
+ },
+ [
+ Middleware([], (next) => async () => {
+ try {
+ return await next()
+ } catch (error: any) {
+ const context = getPrismyContext()
+ return ErrorResult(
+ error.statusCode != null ? error.statusCode : 500,
+ {
+ error: error.stack,
+ data: errorDataMap.get(context),
+ },
+ )
+ }
+ }),
+ ],
+ )
+}
diff --git a/examples/file-upload/specs/file.ts b/examples/file-upload/specs/file.ts
new file mode 100644
index 0000000..5311fe7
--- /dev/null
+++ b/examples/file-upload/specs/file.ts
@@ -0,0 +1,93 @@
+import { getPrismyContext } from '../prismy'
+import { createPrismySelector, PrismySelector } from './createSelector'
+import { createError } from '../error'
+import Formidable, { errors as formidableErrors, File } from 'formidable'
+
+function createMultipartBodyReader(
+ options: Formidable.Options & { multivaluedFields?: MVF[] } = {},
+): () => Promise<{
+ fields: Omit<{ [key: string]: undefined | string }, MVF> & {
+ [key in MVF]: string[] | undefined
+ }
+ files: { [key: string]: File[] | undefined }
+}> {
+ return async () => {
+ const context = getPrismyContext()
+ const contentType = context.req.headers['content-type']
+ if (!isContentTypeMultipart(contentType)) {
+ throw createError(
+ 400,
+ `Content type must be multipart/form-data. (Current: ${contentType})`,
+ )
+ }
+ const { multivaluedFields = [], ...otherOptions } = options
+ const form = Formidable(otherOptions)
+ try {
+ const [fields, files] = await form.parse(context.req)
+ return {
+ fields: Object.entries(fields).reduce(
+ (obj, [key, value]) => {
+ if (value == null) {
+ return obj
+ }
+ if (multivaluedFields.indexOf(key as any) > -1) {
+ obj[key] = value
+ } else {
+ obj[key] = value[0]
+ }
+
+ return obj
+ },
+ {} as Omit<{ [key: string]: undefined | string }, MVF> & {
+ [key in MVF]: string[] | undefined
+ },
+ ),
+ files,
+ }
+ } catch (error: any) {
+ switch (error.code) {
+ case formidableErrors.biggerThanMaxFileSize:
+ throw createError(413, error.message, error)
+ }
+ console.log(error)
+ throw error
+ }
+ }
+}
+
+export function MultipartBodySelector(
+ options: Formidable.Options & { multivaluedFields?: MVF[] } = {},
+): PrismySelector<{
+ fields: Omit<{ [key: string]: undefined | string }, MVF> & {
+ [key in MVF]: string[] | undefined
+ }
+ files: { [key: string]: File[] | undefined }
+}> {
+ return createPrismySelector(async () => {
+ return createMultipartBodyReader(options)()
+ })
+}
+export function MultipartBodyReaderSelector(
+ options: Formidable.Options & { multivaluedFields?: MVF[] } = {},
+): PrismySelector<
+ () => Promise<{
+ fields: Omit<{ [key: string]: undefined | string }, MVF> & {
+ [key in MVF]: string[] | undefined
+ }
+ files: { [key: string]: File[] | undefined }
+ }>
+> {
+ return createPrismySelector(async () => {
+ return createMultipartBodyReader(options)
+ })
+}
+
+function isContentTypeMultipart(contentType: string | undefined) {
+ if (contentType == null) {
+ return false
+ }
+ if (contentType.startsWith('multipart/form-data')) {
+ return true
+ }
+ return false
+}
diff --git a/examples/file-upload/specs/files.ts b/examples/file-upload/specs/files.ts
new file mode 100644
index 0000000..1443a6f
--- /dev/null
+++ b/examples/file-upload/specs/files.ts
@@ -0,0 +1,7 @@
+import { MultipartBodySelector } from '../../../src/selectors/file'
+import { expectType } from '../../../specs/helpers'
+
+const selector = MultipartBodySelector({ multivaluedFields: ['hello'] })
+const body = await selector.resolve()
+expectType(body.fields.hello)
+expectType(body.fields.noHello)
diff --git a/examples/with-session/src/index.ts b/examples/with-session/src/index.ts
index 3829142..ab33ec4 100644
--- a/examples/with-session/src/index.ts
+++ b/examples/with-session/src/index.ts
@@ -1,4 +1,4 @@
-import { prismy, createUrlEncodedBodySelector, redirect, res } from 'prismy'
+import { prismy, createUrlEncodedBodySelector, redirect, Result } from 'prismy'
import { methodRouter } from 'prismy-method-router'
import createSession from 'prismy-session'
import JWTCookieStrategy from 'prismy-session-strategy-jwt-cookie'
@@ -9,17 +9,17 @@ interface SessionData {
const { sessionSelector, sessionMiddleware } = createSession(
new JWTCookieStrategy({
- secret: 'RANDOM_HASH'
- })
+ secret: 'RANDOM_HASH',
+ }),
)
const urlEncodedBodySelector = createUrlEncodedBodySelector()
export default methodRouter(
{
- get: prismy([sessionSelector], session => {
+ get: prismy([sessionSelector], (session) => {
const { data } = session
- return res(
+ return Result(
[
'',
'',
@@ -28,15 +28,15 @@ export default methodRouter(
'',
'',
'',
- ''
- ].join('')
+ '