├── examples ├── now-sushi │ ├── nodemon.json │ ├── types.d.ts │ ├── src │ │ ├── backend │ │ │ ├── all.ts │ │ │ └── get-sushi.ts │ │ └── frontend │ │ │ ├── layout.ts │ │ │ ├── index.ts │ │ │ ├── sushi │ │ │ └── index.ts │ │ │ └── style.css │ ├── package.json │ ├── now.json │ ├── readme.md │ └── tsconfig.json └── hello-world │ ├── hello-world.ts │ ├── hello-world.js │ ├── package.json │ ├── index.js │ └── readme.md ├── fixtures ├── address-book │ ├── config.json │ └── address-book.ts ├── hello-world.ts ├── void.ts ├── rest-params.ts ├── date.ts ├── http-request.ts ├── es6.js ├── http-response.ts ├── http-response-p.ts ├── medium.ts ├── power.ts └── double.ts ├── .travis.yml ├── logo.png ├── packages ├── fts │ ├── src │ │ ├── index.ts │ │ ├── cli.ts │ │ ├── parser.test.ts │ │ ├── types.ts │ │ └── parser.ts │ ├── tsconfig.json │ ├── readme.md │ └── package.json ├── now-fts │ ├── fixtures │ │ ├── 00-hello-world │ │ │ ├── package.json │ │ │ ├── index.ts │ │ │ ├── now.json │ │ │ └── tsconfig.json │ │ └── 01-sushi │ │ │ ├── package.json │ │ │ ├── types.d.ts │ │ │ ├── now.json │ │ │ ├── src │ │ │ └── backend │ │ │ │ ├── all.ts │ │ │ │ └── get-sushi.ts │ │ │ └── tsconfig.json │ ├── package.json │ ├── launcher.js │ ├── index.test.js │ ├── readme.md │ └── index.js ├── fts-core │ ├── src │ │ ├── index.ts │ │ ├── package.ts │ │ ├── http-response.ts │ │ └── context.ts │ ├── tsconfig.json │ ├── package.json │ └── readme.md ├── fts-http │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── parse-endpoint.ts │ │ ├── cors.test.ts │ │ ├── require-handler-function.ts │ │ ├── server.ts │ │ ├── http-context.ts │ │ ├── handler.test.ts │ │ └── handler.ts │ ├── tsconfig.json │ ├── readme.md │ └── package.json ├── fts-validator │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ └── src │ │ ├── index.test.ts │ │ └── index.ts ├── fts-dev │ ├── tsconfig.json │ ├── src │ │ ├── cli.ts │ │ └── index.ts │ ├── package.json │ └── readme.md └── fts-http-client │ ├── tsconfig.json │ ├── package.json │ ├── src │ ├── index.test.ts │ ├── index.ts │ └── fixtures.json │ └── readme.md ├── .snapshots └── packages │ └── fts │ └── src │ ├── parser.test.ts.snap │ └── parser.test.ts.md ├── .prettierignore ├── lerna.json ├── .editorconfig ├── .npmignore ├── tsconfig.json ├── .vscode ├── settings.json ├── tasks.json ├── launch.json └── debug-ts.js ├── tslint.json ├── .gitignore ├── scripts └── generate-fixture-definitions.js ├── tsconfig.base.json ├── roadmap.md ├── package.json └── readme.md /examples/now-sushi/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts,css" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/address-book/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "get": false 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - 12 5 | - 10 6 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/functional-typescript/HEAD/logo.png -------------------------------------------------------------------------------- /fixtures/hello-world.ts: -------------------------------------------------------------------------------- 1 | export default (name = 'World') => { 2 | return `Hello ${name}!` 3 | } 4 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world.ts: -------------------------------------------------------------------------------- 1 | export default (name = 'World') => { 2 | return `Hello ${name}!` 3 | } 4 | -------------------------------------------------------------------------------- /packages/fts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'fts-core' 2 | export * from './parser' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /fixtures/void.ts: -------------------------------------------------------------------------------- 1 | type VoidAlias = void 2 | 3 | export function noop(): VoidAlias { 4 | console.log('noop') 5 | } 6 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/00-hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "now-fts-fixture-sushi", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /packages/fts-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context' 2 | export * from './http-response' 3 | export * from './package' 4 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/00-hello-world/index.ts: -------------------------------------------------------------------------------- 1 | export default (name = 'World') => { 2 | return `Hello ${name}!` 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/rest-params.ts: -------------------------------------------------------------------------------- 1 | export default (foo: string, ...params: any[]) => { 2 | return JSON.stringify({ foo, params }) 3 | } 4 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/00-hello-world/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { "src": "index.ts", "use": "now-fts" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/00-hello-world/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "moduleResolution": "node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.snapshots/packages/fts/src/parser.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/functional-typescript/HEAD/.snapshots/packages/fts/src/parser.test.ts.snap -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json 3 | 4 | # auto-generated files 5 | fixtures.json 6 | .snapshots/ 7 | build/ -------------------------------------------------------------------------------- /packages/fts-core/src/package.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | const pkg: any = require('../package.json') 3 | /* tslint:enable */ 4 | 5 | export const version: string = pkg.version 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "version": "1.4.0", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "packages": [ 7 | "packages/*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | end_of_line = lf 7 | indent_style = space 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /examples/now-sushi/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Sushi { 2 | type: 'maki' | 'temaki' | 'uramaki' | 'nigiri' | 'sashimi' 3 | title: string 4 | description: string 5 | pictureURL: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/fts-http/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-context' 2 | export * from './handler' 3 | export * from './require-handler-function' 4 | export * from './server' 5 | export * from './types' 6 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Sushi { 2 | type: 'maki' | 'temaki' | 'uramaki' | 'nigiri' | 'sashimi' 3 | title: string 4 | description: string 5 | pictureURL: string 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/date.ts: -------------------------------------------------------------------------------- 1 | type DateAlias = Date 2 | 3 | /** 4 | * @param date1 First date 5 | */ 6 | export function playdate(date1: Date, date2: DateAlias): Date { 7 | return new Date(date2.getTime() - date1.getTime()) 8 | } 9 | -------------------------------------------------------------------------------- /packages/fts-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/fts-validator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | examples 4 | tsconfig.json 5 | tsconfig.module.json 6 | tslint.json 7 | .travis.yml 8 | .github 9 | .prettierignore 10 | .vscode 11 | build/docs 12 | **/*.test.* 13 | coverage 14 | .nyc_output 15 | *.log 16 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/backend/**/*.ts", 6 | "use": "now-fts" 7 | } 8 | ], 9 | "routes": [{ "src": "/(.*)", "dest": "/src/backend/$1" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/fts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src"], 9 | "references": [{ "path": "../fts-core" }] 10 | } 11 | -------------------------------------------------------------------------------- /examples/now-sushi/src/backend/all.ts: -------------------------------------------------------------------------------- 1 | // This could be a DB query. 2 | export const availableTypesOfSushi = [ 3 | 'maki', 4 | 'temaki', 5 | 'uramaki', 6 | 'nigiri', 7 | 'sashimi' 8 | ] 9 | 10 | export function all() { 11 | return availableTypesOfSushi 12 | } 13 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/src/backend/all.ts: -------------------------------------------------------------------------------- 1 | // This could be a DB query. 2 | export const availableTypesOfSushi = [ 3 | 'maki', 4 | 'temaki', 5 | 'uramaki', 6 | 'nigiri', 7 | 'sashimi' 8 | ] 9 | 10 | export function all() { 11 | return availableTypesOfSushi 12 | } 13 | -------------------------------------------------------------------------------- /packages/fts-dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src"], 9 | "references": [{ "path": "../fts" }, { "path": "../fts-http" }] 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/http-request.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from 'fts-core' 2 | import { HttpContext } from 'fts-http' 3 | 4 | export default (body: Buffer, context: HttpContext): HttpResponse => { 5 | return { 6 | headers: { 'Content-Type': context.contentType }, 7 | statusCode: 200, 8 | body 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world.js: -------------------------------------------------------------------------------- 1 | // NOTE: this file was previously compiled from `hello-world.ts` to keep the example simple 2 | 'use strict' 3 | exports.__esModule = true 4 | exports['default'] = function(name) { 5 | if (name === void 0) { 6 | name = 'World' 7 | } 8 | return 'Hello ' + name + '!' 9 | } 10 | -------------------------------------------------------------------------------- /packages/fts-core/src/http-response.ts: -------------------------------------------------------------------------------- 1 | interface OutgoingHttpHeaders { 2 | [header: string]: string 3 | } 4 | 5 | /** 6 | * Fallback to allow raw HTTP responses that are not type-checked. 7 | */ 8 | export interface HttpResponse { 9 | statusCode: number 10 | headers: OutgoingHttpHeaders 11 | body: Buffer 12 | } 13 | -------------------------------------------------------------------------------- /packages/fts-http-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src", "src/**/*.json"], 9 | "references": [{ "path": "../fts" }, { "path": "../fts-validator" }] 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example ES6 JavaScript function commented with jsdoc. 3 | * 4 | * @param {string} foo Description of foo 5 | * @param {number} [bar=5] Description of bar 6 | * 7 | * @returns {string} 8 | */ 9 | export default async function example(foo, bar) { 10 | return JSON.stringify({ foo, bar }, null, 2) 11 | } 12 | -------------------------------------------------------------------------------- /packages/fts-core/src/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Optional context information and utilities for FTS functions. 3 | */ 4 | export class Context { 5 | /** Version of the FTS handler that is invoking the function */ 6 | public readonly version: string 7 | 8 | constructor(version: string) { 9 | this.version = version 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/fts-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build" 7 | }, 8 | "include": ["src"], 9 | "references": [ 10 | { "path": "../fts" }, 11 | { "path": "../fts-core" }, 12 | { "path": "../fts-validator" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true 4 | }, 5 | "files": [], 6 | "include": [], 7 | "references": [ 8 | { "path": "./packages/fts" }, 9 | { "path": "./packages/fts-validator" }, 10 | { "path": "./packages/fts-http" }, 11 | { "path": "./packages/fts-http-client" }, 12 | { "path": "./packages/fts-dev" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "npm.enableScriptExplorer": true, 5 | "npm.packageManager": "yarn", 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.DS_Store": true, 9 | "**/node_modules/": true, 10 | "**/build/": true, 11 | "coverage": true, 12 | "coverage.lcov": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/fts-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-core", 3 | "version": "1.4.0", 4 | "description": "Common types for Functional TypeScript.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": [true, "never-prefix"], 5 | "no-implicit-dependencies": false, 6 | "max-classes-per-file": false, 7 | "member-access": false, 8 | "no-console": false, 9 | "no-shadowed-variable": false, 10 | "object-literal-sort-keys": false, 11 | "prefer-conditional-expression": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/http-response.ts: -------------------------------------------------------------------------------- 1 | import * as FTS from 'fts-core' 2 | 3 | // 1x1 png from http://www.1x1px.me/ 4 | const image = 5 | 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' 6 | 7 | export default (): FTS.HttpResponse => { 8 | return { 9 | headers: { 'Content-Type': 'image/png' }, 10 | statusCode: 200, 11 | body: Buffer.from(image, 'base64') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/fts-http/src/types.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | import http from 'http' 3 | 4 | export type HttpHandler = ( 5 | req: http.IncomingMessage, 6 | res: http.ServerResponse 7 | ) => void 8 | 9 | export interface HttpHandlerOptions { 10 | cors?: cors.CorsOptions 11 | } 12 | 13 | export interface HttpServerOptions { 14 | silent: boolean 15 | serve(fn: HttpHandler): http.Server 16 | } 17 | 18 | export type Func = (...args: any[]) => any 19 | -------------------------------------------------------------------------------- /packages/now-fts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "now-fts", 3 | "version": "1.3.11", 4 | "license": "MIT", 5 | "dependencies": { 6 | "@now/node-bridge": "^0.1.10", 7 | "execa": "^1.0.0", 8 | "fs-extra": "7.0.1" 9 | }, 10 | "devDependencies": { 11 | "@now/build-utils": "^0.4.32" 12 | }, 13 | "peerDependencies": { 14 | "@now/build-utils": ">=0.0.1" 15 | }, 16 | "gitHead": "9c95df897ffde29865d5b9190aa7922da780f32e" 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | .cache 18 | .nyc_output 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | lerna-debug.log* 25 | lerna-error.log* 26 | 27 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /examples/now-sushi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-sushi-frontend", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "nodemon --exec \"ts-node $1\"" 9 | }, 10 | "devDependencies": { 11 | "@types/node-fetch": "^2.1.4", 12 | "nodemon": "^1.18.9", 13 | "ts-node": "^7.0.1", 14 | "typescript": "^3.2.4" 15 | }, 16 | "dependencies": { 17 | "node-fetch": "^2.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-example-hello-world", 3 | "description": "Simple Hello World example for Functional Typescript", 4 | "repository": "transitive-bullshit/functional-typescript", 5 | "author": "Saasify ", 6 | "license": "MIT", 7 | "private": true, 8 | "engines": { 9 | "node": ">=10" 10 | }, 11 | "dependencies": { 12 | "fts": "link:../../packages/fts", 13 | "fts-http": "link:../../packages/fts-http" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/http-response-p.ts: -------------------------------------------------------------------------------- 1 | import * as FTS from 'fts-core' 2 | 3 | // 1x1 png from http://www.1x1px.me/ 4 | const image = 5 | 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' 6 | 7 | export default async function fixtureHttpResponseP(): Promise< 8 | FTS.HttpResponse 9 | > { 10 | return Promise.resolve({ 11 | headers: { 'Content-Type': 'image/png' }, 12 | statusCode: 200, 13 | body: Buffer.from(image, 'base64') 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/medium.ts: -------------------------------------------------------------------------------- 1 | enum Color { 2 | Red, 3 | Green, 4 | Blue 5 | } 6 | 7 | interface Nala { 8 | numbers?: number[] 9 | color: Color 10 | } 11 | 12 | /** 13 | * This is an example description for an example function. 14 | * 15 | * @param foo - Example describing string `foo`. 16 | * @returns Description of return value. 17 | */ 18 | export default async function Example( 19 | foo: string, 20 | bar: number = 5, 21 | nala?: Nala 22 | ): Promise { 23 | return JSON.stringify({ foo, bar, nala }) 24 | } 25 | -------------------------------------------------------------------------------- /packages/now-fts/launcher.js: -------------------------------------------------------------------------------- 1 | const { Server } = require('http') 2 | const { Bridge } = require('./bridge.js') 3 | 4 | const bridge = new Bridge() 5 | bridge.port = 3000 6 | let listener 7 | 8 | try { 9 | if (!process.env.NODE_ENV) { 10 | process.env.NODE_ENV = 'production' 11 | } 12 | 13 | // PLACEHOLDER 14 | } catch (error) { 15 | console.error(error) 16 | bridge.userError = error 17 | } 18 | 19 | const server = new Server(listener) 20 | server.listen(bridge.port) 21 | 22 | exports.launcher = bridge.launcher 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "TypeScript watch", 6 | "type": "shell", 7 | "command": "./node_modules/.bin/tsc", 8 | "args": [ 9 | "--build", 10 | "tsconfig.json", 11 | "--watch" 12 | ], 13 | "isBackground": true, 14 | "problemMatcher": [ 15 | "$tsc-watch" 16 | ], 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | }, 21 | "presentation": { 22 | "reveal": "never", 23 | "panel": "dedicated" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/fts-validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-validator", 3 | "version": "1.4.0", 4 | "description": "Schema validation and encoding / decoding for Functional TypeScript.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "dependencies": { 14 | "ajv": "^6.7.0", 15 | "clone-deep": "^4.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/now-sushi/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-typescript-sushi", 3 | "version": 2, 4 | "builds": [ 5 | { 6 | "src": "src/backend/**/*.ts", 7 | "use": "now-fts" 8 | }, 9 | { 10 | "src": "src/frontend/**/*.ts", 11 | "use": "@now/node@canary" 12 | } 13 | ], 14 | "env": { 15 | "IS_NOW": "true" 16 | }, 17 | "routes": [ 18 | { "src": "/api/(.*)", "dest": "/src/backend/$1.ts" }, 19 | { "src": "/sushi/(.*)", "dest": "/src/frontend/sushi?type=$1" }, 20 | { "src": "/(.*)", "dest": "/src/frontend/$1" } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/power.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Raise the value of the first parameter to the power of the second using the es7 `**` operator. 3 | * 4 | * ### Example (es module) 5 | * ```js 6 | * import { power } from 'typescript-starter' 7 | * console.log(power(2,3)) 8 | * // => 8 9 | * ``` 10 | * 11 | * ### Example (commonjs) 12 | * ```js 13 | * var power = require('typescript-starter').power; 14 | * console.log(power(2,3)) 15 | * // => 8 16 | * ``` 17 | */ 18 | export function power(base: number, exponent: number): number { 19 | // This is a proposed es7 operator, which should be transpiled by Typescript 20 | return base ** exponent 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/double.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Multiplies a value by 2. (Also a full example of Typedoc's functionality.) 3 | * 4 | * ### Example (es module) 5 | * ```js 6 | * import { double } from 'typescript-starter' 7 | * console.log(double(4)) 8 | * // => 8 9 | * ``` 10 | * 11 | * ### Example (commonjs) 12 | * ```js 13 | * var double = require('typescript-starter').double; 14 | * console.log(double(4)) 15 | * // => 8 16 | * ``` 17 | * 18 | * @param value Comment describing the `value` parameter. 19 | * @returns Comment describing the return type. 20 | * @anotherNote Some other value. 21 | */ 22 | export function double(value: number): number { 23 | return value * 2 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/address-book/address-book.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * #TopLevel 3 | */ 4 | export function Nala(address: AddressBook): string { 5 | return 'TODO' 6 | } 7 | 8 | class AddressBook { 9 | /** 10 | * A dictionary of Contacts, indexed by unique ID 11 | */ 12 | contacts: { [id: string]: Contact } 13 | } 14 | 15 | class Contact { 16 | firstName: string 17 | lastName?: string 18 | 19 | birthday?: Date 20 | 21 | title?: 'Mr.' | 'Mrs.' | 'Ms.' | 'Prof.' 22 | 23 | emails: string[] 24 | phones: PhoneNumber[] 25 | 26 | /** @TJS-type integer */ 27 | highScore: number 28 | } 29 | 30 | /** 31 | * A Contact's phone number. 32 | */ 33 | class PhoneNumber { 34 | number: string 35 | 36 | /** An optional label (e.g. "mobile") */ 37 | label?: string 38 | } 39 | -------------------------------------------------------------------------------- /packages/fts/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander' 4 | import { generateDefinition } from '.' 5 | 6 | export async function exec(argv: string[]) { 7 | program 8 | .name('fts') 9 | .usage('[options] ') 10 | .option('-p, --project ', "Path to 'tsconfig.json'.") 11 | .parse(argv) 12 | 13 | let file: string 14 | if (program.args.length === 1) { 15 | file = program.args[0] 16 | } else { 17 | console.error('invalid arguments') 18 | program.help() 19 | process.exit(1) 20 | } 21 | 22 | const definition = await generateDefinition(file) 23 | console.log(JSON.stringify(definition, null, 2)) 24 | } 25 | 26 | exec(process.argv).catch((err) => { 27 | console.error(err) 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/fts-dev/src/cli.ts: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import { createDevServer } from '.' 3 | 4 | export async function exec(argv: string[]) { 5 | program 6 | .name('fts') 7 | .usage('[options] ') 8 | .option( 9 | '-P, --port ', 10 | 'Port to listen on', 11 | (s) => parseInt(s, 10), 12 | 3000 13 | ) 14 | .parse(argv) 15 | 16 | let file: string 17 | if (program.args.length === 1) { 18 | file = program.args[0] 19 | } else { 20 | console.error('invalid arguments') 21 | program.help() 22 | process.exit(1) 23 | } 24 | 25 | await createDevServer(file, { 26 | port: program.port 27 | }) 28 | } 29 | 30 | exec(process.argv).catch((err) => { 31 | console.error(err) 32 | process.exit(1) 33 | }) 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Debug Current File", 7 | "program": "${file}", 8 | "outFiles": ["${workspaceFolder}/packages/*/build/**/*.js"], 9 | "skipFiles": [ 10 | "/**/*.js", 11 | "${workspaceFolder}/node_modules/**/*.js" 12 | ], 13 | "runtimeArgs": ["--nolazy"], 14 | "smartStep": true 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Debug Current AVA Test", 20 | "program": "${workspaceRoot}/.vscode/debug-ts.js", 21 | "args": ["${file}", "--serial", "--verbose", "--no-color"], 22 | "skipFiles": ["/**/*.js"], 23 | "runtimeArgs": ["--nolazy"], 24 | "smartStep": true 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/fts/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | # Functional TypeScript 6 | 7 | > TypeScript standard for rock solid serverless functions. 8 | 9 | [![NPM](https://img.shields.io/npm/v/fts.svg)](https://www.npmjs.com/package/fts) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 10 | 11 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript). 12 | 13 | ## License 14 | 15 | MIT © [Saasify](https://saasify.sh) 16 | -------------------------------------------------------------------------------- /packages/fts-http-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-http-client", 3 | "version": "1.4.0", 4 | "description": "HTTP client for Functional TypeScript.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "keywords": [ 14 | "typescript", 15 | "serverless", 16 | "lambda", 17 | "function", 18 | "functional", 19 | "faas", 20 | "rpc", 21 | "http", 22 | "client", 23 | "jsonrpc", 24 | "json", 25 | "jsonschema", 26 | "stdlib" 27 | ], 28 | "dependencies": { 29 | "fts": "^1.4.0", 30 | "fts-validator": "^1.4.0", 31 | "isomorphic-unfetch": "^3.0.0" 32 | }, 33 | "gitHead": "9c95df897ffde29865d5b9190aa7922da780f32e" 34 | } 35 | -------------------------------------------------------------------------------- /packages/fts-core/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | > Common types for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 6 | 7 | [![NPM](https://img.shields.io/npm/v/fts-core.svg)](https://www.npmjs.com/package/fts-core) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 8 | 9 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript). 10 | 11 | ## License 12 | 13 | MIT © [Saasify](https://saasify.sh) 14 | -------------------------------------------------------------------------------- /packages/fts-http/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | > HTTP support for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 6 | 7 | [![NPM](https://img.shields.io/npm/v/fts-http.svg)](https://www.npmjs.com/package/fts-http) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 8 | 9 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript). 10 | 11 | ## License 12 | 13 | MIT © [Saasify](https://saasify.sh) 14 | -------------------------------------------------------------------------------- /packages/fts/src/parser.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import test from 'ava' 3 | import globby from 'globby' 4 | import path from 'path' 5 | import { generateDefinition } from '.' 6 | 7 | // const fixtures = ['./fixtures/http-response.ts'] 8 | const fixtures = globby.sync('./fixtures/**/*.{js,ts}') 9 | const ajv = new Ajv() 10 | 11 | for (const fixture of fixtures) { 12 | const { name } = path.parse(fixture) 13 | 14 | test(name, async (t) => { 15 | const definition = await generateDefinition(fixture) 16 | t.truthy(definition) 17 | 18 | t.true(Array.isArray(definition.params.order)) 19 | t.true(ajv.validateSchema(definition.params.schema)) 20 | t.is(ajv.errors, null) 21 | 22 | t.true(ajv.validateSchema(definition.returns.schema)) 23 | t.is(ajv.errors, null) 24 | 25 | // package version updates shouldn't affect snapshots 26 | delete definition.version 27 | 28 | t.snapshot(definition) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /packages/fts-validator/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | > Schema validation and encoding / decoding for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 6 | 7 | [![NPM](https://img.shields.io/npm/v/fts-validator.svg)](https://www.npmjs.com/package/fts-validator) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 8 | 9 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript). 10 | 11 | ## License 12 | 13 | MIT © [Saasify](https://saasify.sh) 14 | -------------------------------------------------------------------------------- /examples/now-sushi/readme.md: -------------------------------------------------------------------------------- 1 | # Now + TypeScript 2 | 3 | > 🚀 This is deployed with [Now 2](https://zeit.co/now). Please feel free to enjoy the [live demo](https://typescript-sushi.now.sh). 4 | 5 | This example, a sushi information application, uses TypeScript and shares a type definition across frontend and backend in this [majestic monorepo](https://zeit.co/blog/now-2#the-majestic-monorepo). 6 | 7 | ## Getting Started 8 | 9 | 1. Clone the repo 10 | 2. `cd now-examples/typescript` 11 | 3. `yarn` (optionally `npm i`) 12 | 4. `yarn dev src/frontend/index.ts` to run the front page lambda locally, or 13 | - `yarn dev src/frontend/sushi.ts` to run the sushi detail lambda locally, or 14 | - `yarn dev path/to/ts/lambda.ts` to run a TS Lambda locally. 15 | 16 | ## More Info 17 | 18 | We have written [a comprehensive blog post](https://zeit.co/blog/scalable-apps-with-typescript-and-now-2) that highlights the code in this project in detail. We recommend having a look at it. 19 | -------------------------------------------------------------------------------- /packages/fts-dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-dev", 3 | "version": "1.4.0", 4 | "description": "Dev Server for Functional TypeScript.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "bin": { 11 | "fts-dev": "build/cli.js" 12 | }, 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "serverless", 19 | "lambda", 20 | "function", 21 | "functional", 22 | "faas", 23 | "rpc", 24 | "http", 25 | "server", 26 | "dev", 27 | "client", 28 | "jsonrpc", 29 | "json", 30 | "jsonschema", 31 | "stdlib" 32 | ], 33 | "dependencies": { 34 | "commander": "^2.19.0", 35 | "fts": "^1.4.0", 36 | "fts-http": "^1.4.0" 37 | }, 38 | "gitHead": "9c95df897ffde29865d5b9190aa7922da780f32e" 39 | } 40 | -------------------------------------------------------------------------------- /examples/now-sushi/src/frontend/layout.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join } from 'path' 3 | 4 | export default (content: string) => ` 5 | 6 | 7 | FTS TypeScript Sushi 8 | 9 | 10 | 13 | 14 | 15 |
16 | ${content} 17 |
18 |
19 | This project demonstrates how to use TypeScript with \`now\`. Read more about how it works or get the source code and play. 20 |
21 | ` 22 | -------------------------------------------------------------------------------- /packages/fts-dev/src/index.ts: -------------------------------------------------------------------------------- 1 | import { generateDefinition } from 'fts' 2 | import { createHttpHandler, createHttpServer } from 'fts-http' 3 | import http from 'http' 4 | import path from 'path' 5 | import tempy from 'tempy' 6 | 7 | export class DevServerOptions { 8 | port: number = 3000 9 | } 10 | 11 | export async function createDevServer( 12 | file: string, 13 | options: Partial = {} 14 | ): Promise { 15 | const opts = { 16 | ...new DevServerOptions(), 17 | ...options 18 | } 19 | 20 | file = path.resolve(file) 21 | const { name } = path.parse(file) 22 | const outDir = tempy.directory() 23 | const definition = await generateDefinition(file, { 24 | compilerOptions: { 25 | outDir 26 | }, 27 | emit: true 28 | }) 29 | console.log(definition) 30 | 31 | const jsFilePath = path.join(outDir, `${name}.js`) 32 | const handler = createHttpHandler(definition, jsFilePath) 33 | 34 | return createHttpServer(handler, opts.port, { silent: false }) 35 | } 36 | -------------------------------------------------------------------------------- /packages/fts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts", 3 | "version": "1.4.0", 4 | "description": "TypeScript standard for rock solid serverless functions.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "bin": { 11 | "fts": "build/cli.js" 12 | }, 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "keywords": [ 17 | "typescript", 18 | "serverless", 19 | "lambda", 20 | "function", 21 | "functional", 22 | "faas", 23 | "rpc", 24 | "jsonrpc", 25 | "json", 26 | "jsonschema", 27 | "stdlib" 28 | ], 29 | "dependencies": { 30 | "ajv": "^6.7.0", 31 | "commander": "^2.19.0", 32 | "doctrine": "^3.0.0", 33 | "fs-extra": "^7.0.1", 34 | "tempy": "^0.2.1", 35 | "ts-morph": "^1.3.4", 36 | "typescript-json-schema": "^0.34.0" 37 | }, 38 | "gitHead": "9c95df897ffde29865d5b9190aa7922da780f32e" 39 | } 40 | -------------------------------------------------------------------------------- /packages/fts-http-client/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import nock from 'nock' 3 | import { createHttpClient } from '.' 4 | import fixtures from './fixtures.json' 5 | 6 | const hostname = 'https://nala.com' 7 | const path = '/123' 8 | const url = `${hostname}${path}` 9 | 10 | test('hello-world', async (t) => { 11 | const definition = fixtures['hello-world'] 12 | const client = createHttpClient(definition, url) 13 | 14 | { 15 | nock(hostname) 16 | .post(path, { name: 'World' }) 17 | .reply(200, '"Hello World!"') 18 | 19 | const result = await client() 20 | t.is(result, 'Hello World!') 21 | } 22 | 23 | { 24 | nock(hostname) 25 | .post(path, { name: 'Foo' }) 26 | .reply(200, '"Hello Foo!"') 27 | 28 | const result = await client({ name: 'Foo' }) 29 | t.is(result, 'Hello Foo!') 30 | } 31 | 32 | { 33 | nock(hostname) 34 | .post(path, { name: 'Bar' }) 35 | .reply(200, '"Hello Bar!"') 36 | 37 | const result = await client('Bar') 38 | t.is(result, 'Hello Bar!') 39 | } 40 | 41 | await t.throwsAsync(() => client('Foo', 'Bar')) 42 | }) 43 | -------------------------------------------------------------------------------- /examples/hello-world/index.js: -------------------------------------------------------------------------------- 1 | const fts = require('fts') 2 | const ftsHttp = require('fts-http') 3 | 4 | async function example() { 5 | const tsFilePath = './hello-world.ts' 6 | const jsFilePath = './hello-world.js' 7 | 8 | // Parse a TS file's main function export into a Definition schema. 9 | console.log('Generating definition', tsFilePath) 10 | const definition = await fts.generateDefinition(tsFilePath) 11 | console.log(JSON.stringify(definition, null, 2)) 12 | 13 | // Create a standard http handler function `(req, res) => { ... }` that will 14 | // invoke the compiled JS function, performing type checking and conversions 15 | // between http and json for the function's parameters and return value. 16 | const handler = ftsHttp.createHttpHandler(definition, jsFilePath) 17 | 18 | // Create a `micro` http server that uses our HttpHandler to respond to 19 | // incoming http requests. 20 | await ftsHttp.createHttpServer(handler, 'http://localhost:3000') 21 | 22 | // You could alternatively use your `handler` with any Node.js server 23 | // framework, such as express, koa, @now/node, etc. 24 | } 25 | 26 | example().catch((err) => { 27 | console.error(err) 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/now-sushi/src/frontend/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage, ServerResponse } from 'http' 2 | import * as fetch from 'node-fetch' 3 | 4 | import { Sushi } from '../../types' 5 | import layout from './layout' 6 | 7 | const handler = async (_: IncomingMessage, res: ServerResponse) => { 8 | const sushiResponse = await fetch.default( 9 | 'https://fts-typescript-sushi.now.sh/api/all' 10 | ) 11 | const sushiList: Array = await sushiResponse.json() 12 | 13 | res.writeHead(200, { 'Content-Type': 'text/html' }) 14 | res.end( 15 | layout(`

TypeScript Sushi API

16 |
17 |
18 |
19 |
20 |
21 |
22 |

Learn more about...

23 |
    24 | ${sushiList 25 | .map( 26 | (name) => 27 | `
  • ${name}
  • ` 28 | ) 29 | .join('\n')} 30 |

31 |
32 | Sushi animation by yumeeeei.`) 33 | ) 34 | } 35 | 36 | if (!process.env.IS_NOW) { 37 | createServer(handler).listen(3000) 38 | } 39 | 40 | export default handler 41 | -------------------------------------------------------------------------------- /packages/fts-http/src/parse-endpoint.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | 3 | const defaultPort = 3000 4 | const defaultHostname = 'localhost' 5 | 6 | export function parseEndpoint(endpointOrPort?: string | number): any[] { 7 | if (endpointOrPort === undefined) { 8 | return [defaultPort, defaultHostname] 9 | } else if (typeof endpointOrPort === 'number') { 10 | return [endpointOrPort, defaultHostname] 11 | } 12 | 13 | const endpoint = endpointOrPort as string 14 | const url = new URL(endpoint) 15 | 16 | switch (url.protocol) { 17 | case 'pipe:': { 18 | // some special handling 19 | const cutStr = endpoint.replace(/^pipe:/, '') 20 | if (cutStr.slice(0, 4) !== '\\\\.\\') { 21 | throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`) 22 | } 23 | return [cutStr] 24 | } 25 | 26 | case 'unix:': 27 | if (!url.pathname) { 28 | throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`) 29 | } 30 | return [url.pathname] 31 | 32 | case 'tcp:': 33 | case 'http:': { 34 | const port = url.port ? parseInt(url.port, 10) : defaultPort 35 | return [port, url.hostname] 36 | } 37 | 38 | default: 39 | throw new Error(`Unknown endpoint scheme (protocol): ${url.protocol}`) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/fts-http/src/cors.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import fs from 'fs-extra' 3 | import { generateDefinition } from 'fts' 4 | import getPort from 'get-port' 5 | import globby from 'globby' 6 | import got from 'got' 7 | import path from 'path' 8 | import pify from 'pify' 9 | import tempy from 'tempy' 10 | import * as HTTP from '.' 11 | 12 | const fixtures = globby.sync('./fixtures/hello-world.ts') 13 | 14 | for (const fixture of fixtures) { 15 | const { name } = path.parse(fixture) 16 | 17 | test.serial(name, async (t) => { 18 | const outDir = tempy.directory() 19 | const definition = await generateDefinition(fixture, { 20 | compilerOptions: { 21 | outDir 22 | }, 23 | emit: true 24 | }) 25 | t.truthy(definition) 26 | 27 | const jsFilePath = path.join(outDir, `${name}.js`) 28 | const handler = HTTP.createHttpHandler(definition, jsFilePath) 29 | t.is(typeof handler, 'function') 30 | 31 | const port = await getPort() 32 | const server = await HTTP.createHttpServer(handler, port) 33 | const url = `http://localhost:${port}` 34 | 35 | const res = await got(url, { method: 'options' }) 36 | t.truthy(res) 37 | t.is(res.statusCode, 200) 38 | t.is(res.body, 'ok\n') 39 | 40 | await pify(server.close.bind(server))() 41 | await fs.remove(outDir) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/fts-dev/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | > Dev Server for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 6 | 7 | [![NPM](https://img.shields.io/npm/v/fts-dev.svg)](https://www.npmjs.com/package/fts-dev) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 8 | 9 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript) for more info on FTS in general. 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install -g fts-dev 15 | ``` 16 | 17 | ## Usage 18 | 19 | Say we have the following FTS function: 20 | 21 | ```ts 22 | // example.ts 23 | export function example(name: string, date: Date): string { 24 | return `${name}: ${date}` 25 | } 26 | ``` 27 | 28 | You can easily create a local dev server for this function with: 29 | 30 | ```bash 31 | fts-dev example.ts 32 | ``` 33 | 34 | The server defaults to port 3000. 35 | 36 | ## License 37 | 38 | MIT © [Saasify](https://saasify.sh) 39 | -------------------------------------------------------------------------------- /scripts/generate-fixture-definitions.js: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import assert from 'assert' 3 | import globby from 'globby' 4 | import path from 'path' 5 | import pMap from 'p-map' 6 | import { generateDefinition } from 'fts' 7 | 8 | const fixtures = globby.sync('../fixtures/**/*.{js,ts}', { cwd: __dirname }) 9 | .map((fixture) => path.resolve(__dirname, fixture)) 10 | 11 | async function generateFixtureDefinitions () { 12 | const definitions = { } 13 | const ajv = new Ajv() 14 | 15 | console.error(`generating fts definition for ${fixtures.length} fixtures...`) 16 | 17 | await pMap(fixtures, async (fixture) => { 18 | const { name } = path.parse(fixture) 19 | console.error(`generating fts definition for "${name}"...`, fixture) 20 | const definition = await generateDefinition(fixture) 21 | 22 | assert(Array.isArray(definition.params.order)) 23 | assert(ajv.validateSchema(definition.params.schema)) 24 | assert.equal(ajv.errors, null) 25 | 26 | assert(ajv.validateSchema(definition.returns.schema)) 27 | assert.equal(ajv.errors, null) 28 | 29 | definitions[name] = definition 30 | }, { 31 | concurrency: 4 32 | }) 33 | 34 | return definitions 35 | } 36 | 37 | generateFixtureDefinitions() 38 | .then((definitions) => { 39 | console.log(JSON.stringify(definitions, null, 2)) 40 | }) 41 | .catch((err) => { 42 | console.error(err) 43 | process.exit(1) 44 | }) 45 | -------------------------------------------------------------------------------- /.vscode/debug-ts.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const meow = require('meow') 4 | const path = require('path') 5 | 6 | const tsFile = getTSFile() 7 | const jsFile = TS2JS(tsFile) 8 | 9 | // Guard against running on non-test files 10 | if (!tsFile.endsWith('.test.ts') && !tsFile.endsWith('.spec.ts')) { 11 | const tsFileBase = path.basename(tsFile) 12 | console.error() 13 | console.error(`Error: file "${tsFileBase}" is not a valid test file.`) 14 | console.error() 15 | process.exit(1) 16 | } 17 | 18 | replaceCLIArg(tsFile, jsFile) 19 | 20 | // Ava debugger 21 | require('ava/profile') 22 | 23 | /** 24 | * Get ts file path from CLI args. 25 | * 26 | * @return string path 27 | */ 28 | function getTSFile() { 29 | const cli = meow() 30 | return cli.input[0] 31 | } 32 | 33 | /** 34 | * Get associated compiled js file path. 35 | * 36 | * @param tsFile path 37 | * @return string path 38 | */ 39 | function TS2JS(tsFile) { 40 | const tsPathObj = path.parse(tsFile) 41 | const buildDir = tsPathObj.dir.replace(/\/src\b/, '/build') 42 | 43 | return path.format({ 44 | dir: buildDir, 45 | ext: '.js', 46 | name: tsPathObj.name, 47 | root: tsPathObj.root 48 | }) 49 | } 50 | 51 | /** 52 | * Replace a value in CLI args. 53 | * 54 | * @param search value to search 55 | * @param replace value to replace 56 | * @return void 57 | */ 58 | function replaceCLIArg(search, replace) { 59 | process.argv[process.argv.indexOf(search)] = replace 60 | } 61 | -------------------------------------------------------------------------------- /packages/fts-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-http", 3 | "version": "1.4.0", 4 | "description": "HTTP Support for Functional TypeScript.", 5 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 6 | "author": "Saasify ", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "typings": "build/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "keywords": [ 14 | "typescript", 15 | "serverless", 16 | "lambda", 17 | "function", 18 | "functional", 19 | "faas", 20 | "rpc", 21 | "http", 22 | "jsonrpc", 23 | "json", 24 | "jsonschema", 25 | "stdlib" 26 | ], 27 | "dependencies": { 28 | "accepts": "^1.3.5", 29 | "content-type": "^1.0.4", 30 | "cors": "^2.8.5", 31 | "decompress-request": "^1.0.0", 32 | "file-type": "^12.4.0", 33 | "fts": "^1.4.0", 34 | "fts-core": "^1.4.0", 35 | "fts-validator": "^1.4.0", 36 | "is-stream": "^1.1.0", 37 | "micro": "^9.3.3", 38 | "micro-cors": "^0.1.1", 39 | "mime-types": "^2.1.24", 40 | "multiparty": "^4.2.1", 41 | "parseurl": "^1.3.2", 42 | "qs": "^6.6.0", 43 | "raw-body": "^2.4.1", 44 | "resolve": "^1.10.0", 45 | "type-is": "^1.6.16", 46 | "urlencoded-body-parser": "^3.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/file-type": "^10.9.1", 50 | "@types/micro-cors": "^0.1.0", 51 | "@types/multiparty": "^0.0.32", 52 | "@types/raw-body": "^2.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/now-sushi/src/frontend/sushi/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage, ServerResponse } from 'http' 2 | import * as fetch from 'node-fetch' 3 | import url from 'url' 4 | 5 | import { Sushi } from '../../../types' 6 | import layout from '../layout' 7 | 8 | const handler = async (req: IncomingMessage, res: ServerResponse) => { 9 | const { type } = url.parse(req.url || '', true).query 10 | res.writeHead(200, { 'Content-Type': 'text/html' }) 11 | 12 | try { 13 | const sushiResponse = await fetch.default( 14 | 'https://fts-typescript-sushi.now.sh/api/get-sushi?type=' + type 15 | ) 16 | const { description, pictureURL, title }: Sushi = await sushiResponse.json() 17 | 18 | res.end( 19 | layout(`

${title}

20 |
21 |
22 |
${title}
23 |
24 |
25 |

${description}

26 |
27 |
28 | 29 | Back`) 30 | ) 31 | } catch (e) { 32 | res.end( 33 | layout(`

Invalid Sushi Type

34 |
35 |
36 |

?

37 |
38 |
39 |

We don't know what you mean by \`${type}\`. Go back to the start page to see available choices.

40 |
41 |
`) 42 | ) 43 | } 44 | } 45 | 46 | if (!process.env.IS_NOW) { 47 | createServer(handler).listen(3000) 48 | } 49 | 50 | export default handler 51 | -------------------------------------------------------------------------------- /packages/now-fts/index.test.js: -------------------------------------------------------------------------------- 1 | const FileFsRef = require('@now/build-utils/file-fs-ref.js') 2 | const test = require('ava') 3 | const fs = require('fs-extra') 4 | const globby = require('globby') 5 | const path = require('path') 6 | const tempy = require('tempy') 7 | const builder = require('.') 8 | 9 | const fixturesPath = path.resolve(__dirname, 'fixtures') 10 | const fixtures = fs 11 | .readdirSync(fixturesPath) 12 | .map((fixture) => path.join(fixturesPath, fixture)) 13 | 14 | for (const fixture of fixtures) { 15 | const { name } = path.parse(fixture) 16 | 17 | test.serial(name, async (t) => { 18 | const nowConfig = await fs.readJson(path.join(fixture, 'now.json')) 19 | const builds = (nowConfig.builds || []).filter( 20 | (build) => build.use === 'now-fts' 21 | ) 22 | const entrypoints = builds 23 | .map((build) => build.src) 24 | .map((pattern) => globby.sync(pattern, { cwd: fixture })) 25 | .reduce((acc, files) => acc.concat(files), []) 26 | const sourceFiles = await globby('**/*.{js,json,ts}', { cwd: fixture }) 27 | 28 | const getContext = (entrypoint, workPath, config = {}) => ({ 29 | config, 30 | workPath, 31 | entrypoint, 32 | files: sourceFiles.reduce((files, file) => { 33 | const fsPath = path.join(fixture, file) 34 | files[file] = new FileFsRef({ fsPath }) 35 | return files 36 | }, {}) 37 | }) 38 | 39 | for (const entrypoint of entrypoints) { 40 | const workPath = tempy.directory() 41 | console.log({ fixture, entrypoint, workPath }) 42 | 43 | const context = getContext(entrypoint, workPath) 44 | const result = await builder.build(context) 45 | t.truthy(result) 46 | t.truthy(result[entrypoint]) 47 | 48 | // await fs.remove(workPath) 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /packages/fts-http/src/require-handler-function.ts: -------------------------------------------------------------------------------- 1 | import { Definition } from 'fts' 2 | import path from 'path' 3 | import resolve from 'resolve' 4 | import * as HTTP from './types' 5 | 6 | export function requireHandlerFunction( 7 | definition: Definition, 8 | file: string | object 9 | ): HTTP.Func { 10 | let entry: any 11 | 12 | if (typeof file === 'string') { 13 | let filePath = file 14 | 15 | if (!path.isAbsolute(filePath)) { 16 | const basedir = path.dirname(module.parent.parent.parent.filename) 17 | filePath = resolve.sync(file, { basedir }) 18 | } 19 | 20 | entry = require(filePath) 21 | } else { 22 | entry = file 23 | } 24 | 25 | if (!entry) { 26 | throw new Error( 27 | `FTS definition error "${ 28 | definition.title 29 | }"; empty JS module require in file "${file}."` 30 | ) 31 | } 32 | 33 | if (definition.config.defaultExport) { 34 | if (typeof entry === 'object') { 35 | entry = entry.default 36 | } 37 | } else { 38 | if (!definition.config.namedExport) { 39 | throw new Error( 40 | `FTS definition error "${ 41 | definition.title 42 | }"; must have either a defaultExport or namedExport in file "${file}."` 43 | ) 44 | } 45 | 46 | entry = entry[definition.config.namedExport] 47 | 48 | if (!entry) { 49 | throw new Error( 50 | `FTS definition error "${definition.title}"; JS export "${ 51 | definition.config.namedExport 52 | }" doesn't exist in file "${file}".` 53 | ) 54 | } 55 | } 56 | 57 | if (typeof entry !== 'function') { 58 | throw new Error( 59 | `FTS definition error "${ 60 | definition.title 61 | }"; referenced JS export is not a function in file "${file}".` 62 | ) 63 | } 64 | 65 | return entry 66 | } 67 | -------------------------------------------------------------------------------- /packages/fts-http/src/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import micro from 'micro' 3 | import { parseEndpoint } from './parse-endpoint' 4 | import { HttpHandler, HttpServerOptions } from './types' 5 | 6 | /** 7 | * Small wrapper around [micro](https://github.com/zeit/micro) for creating 8 | * an http server that wraps for a single HttpHandler function. 9 | */ 10 | export async function createHttpServer( 11 | handler: HttpHandler, 12 | endpointOrPort?: string | number, 13 | options: Partial = {} 14 | ): Promise { 15 | const opts: HttpServerOptions = { 16 | silent: false, 17 | serve: micro, 18 | ...options 19 | } 20 | const server = opts.serve(handler) 21 | const parsedEndpoint = parseEndpoint(endpointOrPort) 22 | const log = opts.silent ? noop : console.log.bind(console) 23 | 24 | return new Promise((resolve, reject) => { 25 | server.on('error', (err: Error) => { 26 | log('fts:', err.stack) 27 | reject(err) 28 | }) 29 | 30 | server.listen(...parsedEndpoint, () => { 31 | const details = server.address() 32 | 33 | registerShutdown(() => server.close()) 34 | 35 | if (typeof details === 'string') { 36 | log(`fts: Accepting connections on ${details}`) 37 | } else if (typeof details === 'object' && details.port) { 38 | log(`fts: Accepting connections on port ${details.port}`) 39 | } else { 40 | log('fts: Accepting connections') 41 | } 42 | 43 | resolve(server) 44 | }) 45 | }) 46 | } 47 | 48 | function registerShutdown(cb: () => any) { 49 | let run = false 50 | 51 | const wrapper = () => { 52 | if (!run) { 53 | run = true 54 | cb() 55 | } 56 | } 57 | 58 | process.on('SIGINT', wrapper) 59 | process.on('SIGTERM', wrapper) 60 | process.on('exit', wrapper) 61 | } 62 | 63 | function noop() { 64 | return undefined 65 | } 66 | -------------------------------------------------------------------------------- /packages/now-fts/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | # now-fts 6 | 7 | > [@zeit/now](https://zeit.co/now) [builder](https://zeit.co/docs/v2/deployments/builders/overview) for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 8 | 9 | [![NPM](https://img.shields.io/npm/v/now-fts.svg)](https://www.npmjs.com/package/now-fts) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 10 | 11 | The main benefit of `now-fts` is that you **just write TypeScript functions** and can easily deploy robust serverless functions. No dealing with HTTP, parameter validation, or data encoding / decoding! 12 | 13 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript) for info on FTS in general. 14 | 15 | ## Usage 16 | 17 | Say we have the following FTS function: 18 | 19 | ```ts 20 | // example.ts 21 | export function example(name: string, foo: number): string { 22 | return `${name}: ${foo}` 23 | } 24 | ``` 25 | 26 | You can use the `now-fts` builder to deploy it as an HTTP lambda with the following `now.json`: 27 | 28 | ```json 29 | // now.json 30 | { 31 | "version": 2, 32 | "builds": [{ "src": "example.ts", "use": "now-fts" }] 33 | } 34 | ``` 35 | 36 | Then deploy the application via the `now` command. 37 | 38 | The resulting deployment will use an `fts-http` handler to respond to HTTP requests by invoking the original `example` function, without you having to deal with HTTP, server logic, parameter checking, or data encoding / decoding. 39 | 40 | ## License 41 | 42 | MIT © [Saasify](https://saasify.sh) 43 | -------------------------------------------------------------------------------- /packages/fts/src/types.ts: -------------------------------------------------------------------------------- 1 | import doctrine from 'doctrine' 2 | import * as TS from 'ts-morph' 3 | import * as TJS from 'typescript-json-schema' 4 | 5 | /** 6 | * Core FTS function definition that fully specifies the configuration, parameters, 7 | * and return type for an FTS function. 8 | */ 9 | export interface Definition { 10 | /** Name of the function */ 11 | title: string 12 | 13 | /** Brief description of the function */ 14 | description?: string 15 | 16 | /** Language and runtime-specific information */ 17 | config: Config 18 | 19 | /** FTS version that generated this definition */ 20 | version: string 21 | 22 | // consumes: MimeTypes 23 | // produces: MimeTypes 24 | 25 | params: { 26 | /** JSON Schema describing the function parameters */ 27 | schema: TJS.Definition 28 | 29 | /** Ordering of the function parameters */ 30 | order: string[] 31 | 32 | /** Enables a fallback to disable type-checking for raw HTTP requests */ 33 | http: boolean 34 | 35 | /** Whether or not the function takes in a context parameter */ 36 | context: boolean 37 | } 38 | 39 | returns: { 40 | /** JSON Schema describing the function return type */ 41 | schema: TJS.Definition 42 | 43 | /** Whether or not the function returns an async Promise */ 44 | async: boolean 45 | 46 | /** Enables a fallback to disable type-checking for raw HTTP responses */ 47 | http: boolean 48 | } 49 | } 50 | 51 | // export type MimeTypes = string[] 52 | 53 | export interface Config { 54 | language: string 55 | defaultExport: boolean 56 | namedExport?: string 57 | } 58 | 59 | export interface DefinitionOptions { 60 | emit?: boolean 61 | emitOptions?: TS.EmitOptions 62 | compilerOptions?: TS.CompilerOptions 63 | jsonSchemaOptions?: TJS.PartialArgs 64 | } 65 | 66 | export type PartialDefinitionOptions = Partial 67 | 68 | export interface DefinitionBuilder { 69 | sourceFile: TS.SourceFile 70 | main: TS.FunctionDeclaration 71 | docs?: doctrine.Annotation 72 | title: string 73 | definition: Partial 74 | } 75 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 10 | "noEmitOnError": false, 11 | 12 | // "strict": true /* Enable all strict type-checking options. */, 13 | 14 | /* Strict Type-Checking Options */ 15 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 16 | // "strictNullChecks": true /* Enable strict null checks. */, 17 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 18 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 19 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 20 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 21 | 22 | /* Additional Checks */ 23 | "noUnusedLocals": true /* Report errors on unused locals. */, 24 | "noUnusedParameters": true /* Report errors on unused parameters. */, 25 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 26 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 27 | 28 | /* Debugging Options */ 29 | "traceResolution": false /* Report module resolution log messages. */, 30 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 31 | "listFiles": false /* Print names of files part of the compilation. */, 32 | "pretty": true /* Stylize errors and messages using color and context. */, 33 | 34 | // "lib": ["es2017", "dom"], 35 | // "types": ["node"], 36 | 37 | "baseUrl": ".", 38 | "paths": { 39 | "*" : ["types/*"] 40 | } 41 | }, 42 | "compileOnSave": false 43 | } 44 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | - [x] Function definition parser 4 | - [x] extract main function export 5 | - [x] convert main function signature to json schema 6 | - [x] add support for common jsdoc comments 7 | - [x] fix should be readonly to source files 8 | - [x] fix async / promise return types 9 | - [x] fix support for arrow function exports 10 | - [x] add support for default values 11 | - [x] add support for void return type 12 | - [x] add support for Buffer type 13 | - [x] add support for Date type 14 | - [x] add support for returning Buffer and Date types 15 | - [x] add CLI wrapper to generate function definitions 16 | - [x] add support for standard JS with jsdoc comments 17 | - [ ] add support for custom tsconfig 18 | - [x] HTTP handler to invoke a function given an FTS definition and JS file entrypoint 19 | - [x] add support for HTTP GET 20 | - [x] add support for other HTTP methods 21 | - [x] validate function parameters against json schema 22 | - [x] validate function return type against json schema 23 | - [x] add support for passing params as array 24 | - [x] add support for async function 25 | - [x] add support for http context (ip, headers, etc) 26 | - [x] add support for setting response headers 27 | - [x] add support for CORS 28 | - [x] remove support for index-based invocations 29 | - [x] HTTP server implementation 30 | - [ ] Documentation 31 | - [x] basic usage example 32 | - [x] example functions (test suite) 33 | - [ ] how to use with different serverless cloud providers 34 | - [x] Testing 35 | - [x] basic unit tests for function definition parser 36 | - [x] basic unit tests for function http handler 37 | - [x] integration tests for TS function => definition => HTTP server 38 | - [x] Publish separate packages 39 | - [x] fts 40 | - [x] fts-validator 41 | - [x] fts-core 42 | - [x] fts-http 43 | - [x] fts-http-client 44 | - [x] fts-dev 45 | - [x] now-fts 46 | - [ ] Post-MVP 47 | - [ ] support multiple source languages 48 | - [ ] support multiple transport handlers (http, grpc, thrift) 49 | - [x] now-builder for FTS functions 50 | 51 | Support my OSS work by following me on twitter twitter 52 | -------------------------------------------------------------------------------- /examples/hello-world/readme.md: -------------------------------------------------------------------------------- 1 | FTS Logo 2 | 3 | # FTS "Hello World" Example 4 | 5 | This is a simple example which transforms a "Hello World" TypeScript function into a type-safe HTTP endpoint. To do so, we perform the following steps: 6 | 7 | 1. Generate an `Definition` 8 | 2. Create an `HttpHandler` 9 | 3. Start an http server 10 | 4. Profit! 11 | 12 | ## Running 13 | 14 | In order to run this example, you first need to build the top-level `fts` package locally. 15 | 16 | Then, run: 17 | 18 | ```bash 19 | $ yarn install 20 | $ node index.js 21 | ``` 22 | 23 | Which will print out a `Definition` schema, as well as which port the server is listening on: 24 | 25 | ``` 26 | { 27 | "title": "hello", 28 | "version": "0.0.1", 29 | "config": { 30 | "language": "typescript", 31 | "defaultExport": false, 32 | "namedExport": "hello" 33 | }, 34 | "params": { 35 | "context": false, 36 | "order": ["name"], 37 | "schema": { 38 | "type": "object", 39 | "properties": { 40 | "name": { 41 | "type": "string", 42 | "default": "World" 43 | } 44 | }, 45 | "additionalProperties": false, 46 | "required": ["name"], 47 | "$schema": "http://json-schema.org/draft-07/schema#" 48 | } 49 | }, 50 | "returns": { 51 | "async": false, 52 | "schema": { 53 | "type": "object", 54 | "properties": { 55 | "result": { 56 | "type": "string" 57 | } 58 | }, 59 | "$schema": "http://json-schema.org/draft-07/schema#" 60 | } 61 | } 62 | } 63 | 64 | fts: Accepting connections on port 3000 65 | ``` 66 | 67 | Once you have the server running, you can invoke your type-safe function over HTTP: 68 | 69 | ```bash 70 | $ curl -s 'http://localhost:3000?name=GET' 71 | Hello GET! 72 | 73 | $ curl -s 'http://localhost:3000' -d 'name=POST' 74 | Hello POST! 75 | ``` 76 | 77 | Note that in this example, we're generating the FTS Definition and serving it together, but in practice we recommend that you generate these definitions during your build step, alongside your normal TS => JS compilation. The definitions should be viewed as json build artifacts that are _referenced_ at runtime in your server or serverless function. 78 | 79 | ## License 80 | 81 | MIT © [Saasify](https://saasify.sh) 82 | -------------------------------------------------------------------------------- /examples/now-sushi/src/backend/get-sushi.ts: -------------------------------------------------------------------------------- 1 | import { Sushi } from '../../types' 2 | 3 | // Imagine this is a DB query. 4 | export function getSushi(type: Sushi['type']): Sushi { 5 | switch (type) { 6 | case 'maki': 7 | return { 8 | type, 9 | description: 10 | 'Maki is a type of sushi roll that includes toasted seaweed nori rolled around vinegar-flavored rice and various fillings, including raw seafood and vegetables. The word maki means “roll.”', 11 | pictureURL: 12 | 'https://upload.wikimedia.org/wikipedia/commons/8/81/Maki_Sushi_Lunch_on_green_leaf_plate.jpg', 13 | title: 'Maki' 14 | } 15 | case 'temaki': 16 | return { 17 | type, 18 | description: 19 | 'Temaki sushi, also known as hand rolled sushi, is a popular casual Japanese food. The conelike form of temaki incorporates rice, specially prepared seaweed called nori, and a variety of fillings known as neta.', 20 | pictureURL: 21 | 'https://www.publicdomainpictures.net/pictures/270000/velka/japanese-food.jpg', 22 | title: 'Temaki' 23 | } 24 | case 'uramaki': 25 | return { 26 | type, 27 | description: 28 | 'Uramaki is a sushi roll made with rice on the outside and seaweed on the inside. Uramaki can be made with a number of fillings.', 29 | pictureURL: 30 | 'https://c1.staticflickr.com/1/772/20940700515_865e26d4a0_b.jpg', 31 | title: 'Uramaki' 32 | } 33 | case 'nigiri': 34 | return { 35 | type, 36 | description: 37 | 'Nigiri is a hand-formed ball of rice, with a slice of fish over the top. If you take out the rice, you have Sashimi! Maki is a type of roll in which the seaweed wrap is on the outside of the roll.', 38 | pictureURL: 39 | 'https://upload.wikimedia.org/wikipedia/commons/4/41/Salmon_Nigiri_Sushi_with_chopsticks%2C_2008.jpg', 40 | title: 'Nigiri' 41 | } 42 | case 'sashimi': 43 | return { 44 | type, 45 | description: 46 | 'Sashimi is a Japanese delicacy consisting of very fresh raw fish or meat sliced into thin pieces and often eaten with soy sauce.', 47 | pictureURL: 48 | 'https://c1.staticflickr.com/3/2441/3537413421_cd7cff0b70_b.jpg', 49 | title: 'Sashimi' 50 | } 51 | default: 52 | throw new Error('This sushi type does not exist.') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/src/backend/get-sushi.ts: -------------------------------------------------------------------------------- 1 | import { Sushi } from '../../types' 2 | 3 | // Imagine this is a DB query. 4 | export function getSushi(type: Sushi['type']): Sushi { 5 | switch (type) { 6 | case 'maki': 7 | return { 8 | type, 9 | description: 10 | 'Maki is a type of sushi roll that includes toasted seaweed nori rolled around vinegar-flavored rice and various fillings, including raw seafood and vegetables. The word maki means “roll.”', 11 | pictureURL: 12 | 'https://upload.wikimedia.org/wikipedia/commons/8/81/Maki_Sushi_Lunch_on_green_leaf_plate.jpg', 13 | title: 'Maki' 14 | } 15 | case 'temaki': 16 | return { 17 | type, 18 | description: 19 | 'Temaki sushi, also known as hand rolled sushi, is a popular casual Japanese food. The conelike form of temaki incorporates rice, specially prepared seaweed called nori, and a variety of fillings known as neta.', 20 | pictureURL: 21 | 'https://www.publicdomainpictures.net/pictures/270000/velka/japanese-food.jpg', 22 | title: 'Temaki' 23 | } 24 | case 'uramaki': 25 | return { 26 | type, 27 | description: 28 | 'Uramaki is a sushi roll made with rice on the outside and seaweed on the inside. Uramaki can be made with a number of fillings.', 29 | pictureURL: 30 | 'https://c1.staticflickr.com/1/772/20940700515_865e26d4a0_b.jpg', 31 | title: 'Uramaki' 32 | } 33 | case 'nigiri': 34 | return { 35 | type, 36 | description: 37 | 'Nigiri is a hand-formed ball of rice, with a slice of fish over the top. If you take out the rice, you have Sashimi! Maki is a type of roll in which the seaweed wrap is on the outside of the roll.', 38 | pictureURL: 39 | 'https://upload.wikimedia.org/wikipedia/commons/4/41/Salmon_Nigiri_Sushi_with_chopsticks%2C_2008.jpg', 40 | title: 'Nigiri' 41 | } 42 | case 'sashimi': 43 | return { 44 | type, 45 | description: 46 | 'Sashimi is a Japanese delicacy consisting of very fresh raw fish or meat sliced into thin pieces and often eaten with soy sauce.', 47 | pictureURL: 48 | 'https://c1.staticflickr.com/3/2441/3537413421_cd7cff0b70_b.jpg', 49 | title: 'Sashimi' 50 | } 51 | default: 52 | throw new Error('This sushi type does not exist.') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/fts-http-client/readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | > HTTP Client for [Functional TypeScript](https://github.com/transitive-bullshit/functional-typescript). 6 | 7 | [![NPM](https://img.shields.io/npm/v/fts-http-client.svg)](https://www.npmjs.com/package/fts-http-client) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 8 | 9 | See the main [docs](https://github.com/transitive-bullshit/functional-typescript) for more info on FTS in general. 10 | 11 | Note that this client is **optional**, as FTS HTTP endpoints may be called using any HTTP request library. 12 | 13 | The advantage to using this client library is that it performs parameter and return value validation as well as handling JSON encoding / decoding. The custom encoding / decoding is really only used for non-JSON primitive types such as `Date` and `Buffer`. 14 | 15 | ## Usage 16 | 17 | Say we have the following FTS function: 18 | 19 | ```ts 20 | export function example(name: string, date: Date): string { 21 | return `${name}: ${date}` 22 | } 23 | ``` 24 | 25 | You can invoke this function remotely with the following client code: 26 | 27 | ```ts 28 | import { createHttpClient } from 'fts-http-client' 29 | 30 | // previously generated fts definition 31 | const definition = {} 32 | 33 | // URL of an fts-http handler endpoint 34 | const url = 'https://example.com/foo' 35 | 36 | // create a client that will be used to call the remote FTS function 37 | const client = createHttpClient(definition, url) 38 | 39 | // You may either call the remote function with the same signature as the 40 | // original TS function or with an object of named parameters 41 | const result0 = await client('Foo', new Date()) 42 | const result1 = await client({ name: 'Foo', date: new Date() }) 43 | ``` 44 | 45 | ## Alternatives 46 | 47 | - Node.js 48 | - [got](https://github.com/sindresorhus/got) - set `json` option to `true` 49 | - [request](https://github.com/request/request) - set `json` option to `true` 50 | - Browser 51 | - [isomorphic-unfetch](https://github.com/developit/unfetch/tree/master/packages/isomorphic-unfetch) 52 | - [axios](https://github.com/axios/axios) 53 | - CLI 54 | - [httpie](https://httpie.org) 55 | - [curl](https://github.com/tldr-pages/tldr/blob/master/pages/common/curl.md) 56 | 57 | ## License 58 | 59 | MIT © [Saasify](https://saasify.sh) 60 | -------------------------------------------------------------------------------- /packages/fts-http-client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Definition } from 'fts' 2 | import { createValidator } from 'fts-validator' 3 | import fetch from 'isomorphic-unfetch' 4 | 5 | export function createHttpClient(definition: Definition, url: string) { 6 | const { title } = definition 7 | const requiredParams = getRequiredParams(definition.params.schema) 8 | 9 | const validator = createValidator() 10 | const paramsEncoder = validator.encoder(definition.params.schema) 11 | const returnsDecoder = validator.decoder(definition.returns.schema) 12 | 13 | return async (...args: any[]) => { 14 | let setParams = false 15 | let params: any 16 | 17 | if (args.length > definition.params.order.length) { 18 | throw new Error( 19 | `Invalid parameters to "${title}": too many parameters. Expected ${ 20 | definition.params.order.length 21 | }, received ${args.length}.` 22 | ) 23 | } 24 | 25 | if (args.length === 1 && typeof args[0] === 'object') { 26 | const arg = args[0] 27 | const firstParamName = definition.params.order[0] 28 | 29 | if (firstParamName) { 30 | const firstParam = definition.params.schema.properties[firstParamName] 31 | const isCustomType = firstParam.type === 'string' && firstParam.coerceTo 32 | let isParamsObject = true 33 | 34 | if (Buffer.isBuffer(arg) || Array.isArray(arg) || arg instanceof Date) { 35 | isParamsObject = false 36 | } else if ( 37 | firstParam.type === 'object' || 38 | firstParam.$ref || 39 | isCustomType 40 | ) { 41 | for (const param of requiredParams) { 42 | if (!arg.hasOwnProperty(param)) { 43 | isParamsObject = false 44 | } 45 | } 46 | } 47 | 48 | if (isParamsObject) { 49 | params = arg 50 | setParams = true 51 | } 52 | } 53 | } 54 | 55 | if (!setParams) { 56 | params = args.reduce((acc, param, i) => { 57 | const name = definition.params.order[i] 58 | if (name) { 59 | acc[name] = param 60 | } 61 | return acc 62 | }, {}) 63 | } 64 | 65 | paramsEncoder(params) 66 | if (paramsEncoder.errors) { 67 | const message = validator.ajv.errorsText(paramsEncoder.errors) 68 | throw new Error(`Invalid parameters: ${message}`) 69 | } 70 | 71 | const response = await fetch(url, { 72 | method: 'POST', 73 | headers: { 74 | Accept: 'application/json', 75 | 'Content-Type': 'application/json' 76 | }, 77 | body: JSON.stringify(params) 78 | }) 79 | 80 | if (!response.ok) { 81 | throw new Error(response.statusText) 82 | } 83 | 84 | const returns = { result: await response.json() } 85 | returnsDecoder(returns) 86 | 87 | return returns.result 88 | } 89 | } 90 | 91 | function getRequiredParams(schema: any): string[] { 92 | return schema.required.filter( 93 | (name: string) => !schema.properties[name].hasOwnProperty('default') 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fts-monorepo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "TypeScript standard for rock solid serverless functions.", 6 | "repository": "https://github.com/transitive-bullshit/functional-typescript", 7 | "author": "Saasify ", 8 | "license": "MIT", 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "engines": { 13 | "node": ">=10" 14 | }, 15 | "scripts": { 16 | "start": "run-s build:clean && tsc --build tsconfig.json --watch", 17 | "build": "run-s build:clean bootstrap && tsc --build tsconfig.json", 18 | "build:clean": "tsc --build tsconfig.json --clean", 19 | "fix": "run-s fix:*", 20 | "fix:prettier": "prettier '**/*.ts' --write", 21 | "fix:tslint": "tslint --fix --project .", 22 | "test": "run-s build test:*", 23 | "test:lint": "tslint --project . && prettier '**/*.ts' --list-different", 24 | "test:unit": "nyc -- ava -v", 25 | "cov": "run-s build test:unit cov:html && opn coverage/index.html", 26 | "cov:html": "nyc report --reporter=html", 27 | "doc": "run-s doc:html && opn build/docs/index.html", 28 | "doc:html": "typedoc src/ --exclude **/*.test.ts --target ES6 --mode file --out build/docs", 29 | "doc:publish": "gh-pages -m '[ci skip] Updates' -d build/docs", 30 | "clean": "git clean -dfqX -- ./node_modules **/{build,node_modules}/", 31 | "fixtures": "node -r esm ./scripts/generate-fixture-definitions.js > packages/fts-http-client/src/fixtures.json", 32 | "bootstrap": "lerna bootstrap", 33 | "publish": "lerna publish", 34 | "preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') < 0) throw new Error('fts requires yarn for development')\"", 35 | "postinstall": "run-s bootstrap" 36 | }, 37 | "dependencies": { 38 | "fts": "link:packages/fts", 39 | "fts-core": "link:packages/fts-core", 40 | "fts-dev": "link:packages/fts-dev", 41 | "fts-http": "link:packages/fts-http", 42 | "fts-http-client": "link:packages/fts-http-client", 43 | "fts-validator": "link:packages/fts-validator" 44 | }, 45 | "devDependencies": { 46 | "@types/accepts": "^1.3.5", 47 | "@types/content-type": "^1.1.2", 48 | "@types/cors": "^2.8.4", 49 | "@types/doctrine": "^0.0.3", 50 | "@types/fs-extra": "^5.0.4", 51 | "@types/get-port": "^4.0.1", 52 | "@types/globby": "^8.0.0", 53 | "@types/got": "^9.4.0", 54 | "@types/is-stream": "^1.1.0", 55 | "@types/json-schema": "^7.0.1", 56 | "@types/micro": "^7.3.3", 57 | "@types/nock": "^9.3.1", 58 | "@types/node": "^10.12.18", 59 | "@types/parseurl": "^1.3.1", 60 | "@types/pify": "^3.0.2", 61 | "@types/qs": "^6.5.1", 62 | "@types/resolve": "^0.0.8", 63 | "@types/seedrandom": "^2.4.27", 64 | "@types/tempy": "^0.2.0", 65 | "@types/type-is": "^1.6.2", 66 | "ava": "^1.1.0", 67 | "clone-deep": "^4.0.1", 68 | "codecov": "^3.1.0", 69 | "cz-conventional-changelog": "^2.1.0", 70 | "delay": "^4.3.0", 71 | "esm": "^3.1.1", 72 | "get-port": "^4.1.0", 73 | "gh-pages": "^2.0.1", 74 | "globby": "^9.0.0", 75 | "got": "^9.6.0", 76 | "husky": "^1.3.1", 77 | "json-schema-faker": "^0.5.0-rc16", 78 | "lerna": "^3.10.6", 79 | "lint-staged": "^8.1.0", 80 | "nock": "^10.0.6", 81 | "npm-run-all": "^4.1.5", 82 | "nyc": "^13.1.0", 83 | "opn-cli": "^4.0.0", 84 | "p-map": "^2.0.0", 85 | "pify": "^4.0.1", 86 | "prettier": "^1.16.0", 87 | "seedrandom": "^2.4.4", 88 | "standard-version": "^4.4.0", 89 | "tempy": "^0.2.1", 90 | "tslint": "^5.11.0", 91 | "tslint-config-prettier": "^1.17.0", 92 | "typedoc": "^0.14.2", 93 | "typescript": "3.2.4" 94 | }, 95 | "ava": { 96 | "failFast": true, 97 | "snapshotDir": "./.snapshots", 98 | "concurrency": 1, 99 | "files": [ 100 | "packages/*/build/**/*.test.js" 101 | ], 102 | "sources": [ 103 | "packages/*/build/**/*.js" 104 | ] 105 | }, 106 | "config": { 107 | "commitizen": { 108 | "path": "cz-conventional-changelog" 109 | } 110 | }, 111 | "lint-staged": { 112 | "*.{ts,js,json,md}": [ 113 | "prettier --write", 114 | "git add" 115 | ] 116 | }, 117 | "prettier": { 118 | "singleQuote": true, 119 | "jsxSingleQuote": true, 120 | "semi": false, 121 | "tabWidth": 2, 122 | "bracketSpacing": true, 123 | "jsxBracketSameLine": false, 124 | "arrowParens": "always" 125 | }, 126 | "nyc": { 127 | "exclude": [ 128 | "**/*.test.js" 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/fts-validator/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { createValidator } from '.' 3 | 4 | test('basic', async (t) => { 5 | const schema = { 6 | $schema: 'http://json-schema.org/draft-07/schema#', 7 | type: 'object', 8 | properties: { 9 | foo: { 10 | type: 'number' 11 | } 12 | } 13 | } 14 | 15 | const validator = createValidator() 16 | const decoder = validator.decoder(schema) 17 | const encoder = validator.encoder(schema) 18 | 19 | const data = { 20 | pi: 3.14159 21 | } 22 | 23 | decoder(data) 24 | t.is(decoder.errors, null) 25 | t.deepEqual(data, { pi: 3.14159 }) 26 | t.is(typeof data.pi, 'number') 27 | 28 | encoder(data) 29 | t.is(encoder.errors, null) 30 | t.deepEqual(data, { pi: 3.14159 }) 31 | t.is(typeof data.pi, 'number') 32 | }) 33 | 34 | test('decode/encode Date valid object', async (t) => { 35 | const schema = { 36 | $schema: 'http://json-schema.org/draft-07/schema#', 37 | type: 'object', 38 | properties: { 39 | foo: { 40 | type: 'string', 41 | coerceTo: 'Date' 42 | } 43 | } 44 | } 45 | 46 | const validator = createValidator() 47 | const decoder = validator.decoder(schema) 48 | const encoder = validator.encoder(schema) 49 | 50 | const data = { 51 | foo: '2019-01-19T20:42:45.310Z' 52 | } 53 | 54 | decoder(data) 55 | t.is(decoder.errors, null) 56 | t.true((data.foo as any) instanceof Date) 57 | 58 | encoder(data) 59 | t.is(encoder.errors, null) 60 | t.is(typeof data.foo, 'string') 61 | }) 62 | 63 | test('decode/encode Date valid array', async (t) => { 64 | const schema = { 65 | $schema: 'http://json-schema.org/draft-07/schema#', 66 | type: 'object', 67 | properties: { 68 | foo: { 69 | type: 'array', 70 | items: { 71 | type: 'string', 72 | coerceTo: 'Date' 73 | } 74 | } 75 | } 76 | } 77 | 78 | const validator = createValidator() 79 | const decoder = validator.decoder(schema) 80 | const encoder = validator.encoder(schema) 81 | 82 | const data = { 83 | foo: ['2019-01-19T20:42:45.310Z', '2019-01-19T20:55:23.733Z'] 84 | } 85 | 86 | decoder(data) 87 | t.is(decoder.errors, null) 88 | t.true((data.foo[0] as any) instanceof Date) 89 | t.true((data.foo[1] as any) instanceof Date) 90 | 91 | encoder(data) 92 | t.is(encoder.errors, null) 93 | t.is(typeof data.foo[0], 'string') 94 | t.is(typeof data.foo[1], 'string') 95 | }) 96 | 97 | test('decode Date invalid object', async (t) => { 98 | const schema = { 99 | $schema: 'http://json-schema.org/draft-07/schema#', 100 | type: 'object', 101 | properties: { 102 | foo: { 103 | type: 'string', 104 | coerceTo: 'Date' 105 | } 106 | } 107 | } 108 | 109 | const validator = createValidator() 110 | const decoder = validator.decoder(schema) 111 | 112 | const data = { 113 | foo: 'invalid date' 114 | } 115 | 116 | t.false(decoder(data)) 117 | }) 118 | 119 | test('decode Date invalid array', async (t) => { 120 | const schema = { 121 | $schema: 'http://json-schema.org/draft-07/schema#', 122 | type: 'object', 123 | properties: { 124 | foo: { 125 | type: 'array', 126 | items: { 127 | type: 'string', 128 | coerceTo: 'Date' 129 | } 130 | } 131 | } 132 | } 133 | 134 | const validator = createValidator() 135 | const decoder = validator.decoder(schema) 136 | 137 | const data = { 138 | foo: ['foo', 'bar'] 139 | } 140 | 141 | t.false(decoder(data)) 142 | }) 143 | 144 | test('decode Date invalid bare', async (t) => { 145 | const schema = { 146 | $schema: 'http://json-schema.org/draft-07/schema#', 147 | type: 'string', 148 | coerceTo: 'Date' 149 | } 150 | 151 | const validator = createValidator() 152 | const decoder = validator.decoder(schema) 153 | 154 | const data = '2019-01-19T20:42:45.310Z' 155 | 156 | t.throws(() => decoder(data)) 157 | }) 158 | 159 | test('decode/encode Buffer valid object', async (t) => { 160 | const schema = { 161 | $schema: 'http://json-schema.org/draft-07/schema#', 162 | type: 'object', 163 | properties: { 164 | foo: { 165 | type: 'string', 166 | coerceTo: 'Buffer' 167 | } 168 | } 169 | } 170 | 171 | const validator = createValidator() 172 | const decoder = validator.decoder(schema) 173 | const encoder = validator.encoder(schema) 174 | 175 | const data = { 176 | foo: Buffer.from('hello world').toString('base64') 177 | } 178 | 179 | decoder(data) 180 | t.is(decoder.errors, null) 181 | t.true(Buffer.isBuffer(data.foo)) 182 | t.is(data.foo.toString(), 'hello world') 183 | 184 | encoder(data) 185 | t.is(encoder.errors, null) 186 | t.is(typeof data.foo, 'string') 187 | t.is(Buffer.from(data.foo, 'base64').toString(), 'hello world') 188 | }) 189 | -------------------------------------------------------------------------------- /examples/now-sushi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["es2015"] /* Specify library files to be included in the compilation. */, 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 26 | "strictNullChecks": true /* Enable strict null checks. */, 27 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 28 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 29 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 30 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 31 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/now-fts/fixtures/01-sushi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["es2015"] /* Specify library files to be included in the compilation. */, 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 26 | "strictNullChecks": true /* Enable strict null checks. */, 27 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 28 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 29 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 30 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 31 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/fts-http/src/http-context.ts: -------------------------------------------------------------------------------- 1 | import accepts from 'accepts' 2 | import contentType from 'content-type' 3 | import { Context, version } from 'fts-core' 4 | import http from 'http' 5 | import parseUrl from 'parseurl' 6 | import qs from 'qs' 7 | import typeIs from 'type-is' 8 | import url from 'url' 9 | 10 | /** 11 | * Optional context utilities for FTS functions when invoked over http. 12 | * 13 | * Based off of [Koa](https://koajs.com/#context). 14 | * 15 | * TODO: port the rest of the jsdocs to this class. 16 | */ 17 | export class HttpContext extends Context { 18 | public readonly req: http.IncomingMessage 19 | public readonly res: http.ServerResponse 20 | public readonly querystring: string 21 | public readonly query: any 22 | protected pUrl?: url.URL 23 | protected pAccept?: accepts.Accepts 24 | 25 | constructor(req: http.IncomingMessage, res: http.ServerResponse) { 26 | super(version) 27 | 28 | this.req = req 29 | this.res = res 30 | 31 | const urlinfo = url.parse(req.url) 32 | this.querystring = urlinfo.query || '' 33 | this.query = qs.parse(this.querystring) 34 | } 35 | 36 | /** Request headers */ 37 | get headers() { 38 | return this.req.headers 39 | } 40 | 41 | /** Request URL */ 42 | get url() { 43 | return this.req.url 44 | } 45 | 46 | /** Request origin URL */ 47 | get origin() { 48 | return `${this.protocol}://${this.host}` 49 | } 50 | 51 | /** Full request URL */ 52 | get href() { 53 | // support: `GET http://example.com/foo` 54 | if (/^https?:\/\//i.test(this.url)) { 55 | return this.url 56 | } 57 | return this.origin + this.url 58 | } 59 | 60 | get method() { 61 | return this.req.method 62 | } 63 | 64 | get path() { 65 | return parseUrl(this.req).pathname 66 | } 67 | 68 | get host() { 69 | let host: string 70 | if (this.req.httpVersionMajor >= 2) { 71 | host = this.get(':authority') 72 | } 73 | if (!host) { 74 | host = this.get('Host') 75 | } 76 | if (!host) { 77 | return '' 78 | } 79 | return host.split(/\s*,\s*/, 1)[0] 80 | } 81 | 82 | get hostname() { 83 | const host = this.host 84 | if (!host) { 85 | return '' 86 | } 87 | if ('[' === host[0]) { 88 | return this.URL.hostname || '' 89 | } // IPv6 90 | return host.split(':', 1)[0] 91 | } 92 | 93 | get URL() { 94 | if (!this.pUrl) { 95 | try { 96 | this.pUrl = new URL(`${this.protocol}://${this.host}${this.url}`) 97 | } catch (err) { 98 | this.pUrl = Object.create(null) 99 | } 100 | } 101 | 102 | return this.pUrl 103 | } 104 | 105 | get socket() { 106 | return this.req.socket 107 | } 108 | 109 | get charset() { 110 | try { 111 | const { parameters } = contentType.parse(this.req) 112 | return parameters.charset || '' 113 | } catch (e) { 114 | return '' 115 | } 116 | } 117 | 118 | get contentType() { 119 | try { 120 | const { type } = contentType.parse(this.req) 121 | return type 122 | } catch (e) { 123 | return '' 124 | } 125 | } 126 | 127 | get length(): number | undefined { 128 | const len = this.get('Content-Length') 129 | if (len !== '') { 130 | return parseInt(len, 10) 131 | } else { 132 | return undefined 133 | } 134 | } 135 | 136 | get protocol() { 137 | /* tslint:disable */ 138 | // TODO: this is hacky 139 | if (this.socket['secure']) { 140 | return 'https' 141 | } 142 | /* tslint:enable */ 143 | return 'http' 144 | } 145 | 146 | get secure() { 147 | return 'https' === this.protocol 148 | } 149 | 150 | get ip() { 151 | return this.socket.remoteAddress 152 | } 153 | 154 | get accept() { 155 | if (!this.pAccept) { 156 | this.pAccept = accepts(this.req) 157 | } 158 | return this.pAccept 159 | } 160 | 161 | accepts(...args: string[]) { 162 | return this.accept.types(...args) 163 | } 164 | 165 | acceptsEncodings(...args: string[]) { 166 | return this.accept.encodings(...args) 167 | } 168 | 169 | acceptsCharsets(...args: string[]) { 170 | return this.accept.charsets(...args) 171 | } 172 | 173 | acceptsLanguages(...args: string[]) { 174 | return this.accept.languages(...args) 175 | } 176 | 177 | is(t: string) { 178 | return typeIs(this.req, [t]) 179 | } 180 | 181 | get type() { 182 | const type = this.get('Content-Type') 183 | if (!type) { 184 | return '' 185 | } 186 | return type.split(';')[0] 187 | } 188 | 189 | get(field: string): string { 190 | const req = this.req 191 | const header = field.toLowerCase() 192 | 193 | switch (header) { 194 | case 'referer': 195 | case 'referrer': 196 | return ( 197 | (req.headers.referrer as string) || 198 | (req.headers.referer as string) || 199 | '' 200 | ) 201 | default: 202 | return (req.headers[header] as string) || '' 203 | } 204 | } 205 | 206 | /** 207 | * Set header `field` to `val`. 208 | * 209 | * Examples: 210 | * 211 | * this.set('Foo', ['bar', 'baz']) 212 | * this.set('Accept', 'application/json') 213 | */ 214 | set(field: string, val: string | string[]) { 215 | if (Array.isArray(val)) { 216 | val = val.map((v) => (typeof v === 'string' ? v : String(v))) 217 | } else if (typeof val !== 'string') { 218 | val = String(val) 219 | } 220 | 221 | this.res.setHeader(field, val) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /packages/fts-validator/src/index.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import cloneDeep from 'clone-deep' 3 | 4 | const encoding = 'base64' 5 | 6 | const customCoercionTypes = { 7 | Buffer: { 8 | // decode 9 | to: (data: string): Buffer => { 10 | return Buffer.from(data, encoding) 11 | }, 12 | 13 | // encode 14 | from: (data: Buffer): string => { 15 | return data.toString(encoding) 16 | } 17 | }, 18 | 19 | Date: { 20 | // decode 21 | to: (data: string): Date => { 22 | const date = new Date(data) 23 | if (isNaN(date as any)) { 24 | throw new Error(`Invalid Date "${data}"`) 25 | } 26 | 27 | return date 28 | }, 29 | 30 | // encode 31 | from: (data: Date): string => { 32 | return data.toISOString() 33 | } 34 | } 35 | } 36 | 37 | export interface Validator { 38 | ajv: Ajv.Ajv 39 | decoder: (schema: any) => Ajv.ValidateFunction 40 | encoder: (schema: any) => Ajv.ValidateFunction 41 | } 42 | 43 | export function createValidator(opts?: any): Validator { 44 | const ajv = new Ajv({ 45 | useDefaults: true, 46 | coerceTypes: true, 47 | unknownFormats: 'ignore', 48 | ...opts 49 | }) 50 | ajv.addKeyword('coerceTo', coerceToKeyword) 51 | ajv.addKeyword('coerceFrom', coerceFromKeyword) 52 | 53 | return { 54 | ajv, 55 | decoder: (schema: any): Ajv.ValidateFunction => { 56 | const temp = cloneDeep(schema) 57 | convertSchema(temp, (schema, key) => { 58 | if (key === 'coerceFrom') { 59 | const type = schema[key] 60 | delete schema[key] 61 | schema.coerceTo = type 62 | if (type === 'date') { 63 | schema.format = 'date-time' 64 | } 65 | schema.type = 'string' 66 | } 67 | }) 68 | return ajv.compile(temp) 69 | }, 70 | encoder: (schema: any): Ajv.ValidateFunction => { 71 | const temp = cloneDeep(schema) 72 | convertSchema(temp, (schema, key) => { 73 | if (key === 'coerceTo') { 74 | const type = schema[key] 75 | delete schema[key] 76 | schema.coerceFrom = type 77 | if (type === 'date') { 78 | delete schema.format 79 | } 80 | schema.type = 'object' 81 | } 82 | }) 83 | return ajv.compile(temp) 84 | } 85 | } 86 | } 87 | 88 | function convertSchema( 89 | schema: any, 90 | transform: (schema: any, key: string) => void 91 | ): any { 92 | for (const key in schema) { 93 | if (schema.hasOwnProperty(key)) { 94 | transform(schema, key) 95 | 96 | const value = schema[key] 97 | if (typeof value === 'object') { 98 | convertSchema(value, transform) 99 | } 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Performs **decoding** during schema validation of non-JSON-primitive data 106 | * types from serialized string representations to their native JavaScript types. 107 | * 108 | * Used for converting from ISO utf8 strings to JS Date objects. 109 | * Used for converting from base64-encoded strings to JS Buffer objects. 110 | */ 111 | const coerceToKeyword: Ajv.KeywordDefinition = { 112 | type: 'string', 113 | modifying: true, 114 | errors: true, 115 | compile: (schema: any): Ajv.ValidateFunction => { 116 | const coercionType = customCoercionTypes[schema] 117 | if (!coercionType) { 118 | throw new Error(`Invalid coerceTo "${schema}"`) 119 | } 120 | 121 | const coerceToType: (data: any) => any = coercionType.to 122 | 123 | return function coerceTo( 124 | data: string, 125 | dataPath: string, 126 | parentData: object | any[], 127 | parentDataProperty: string | number 128 | ): boolean { 129 | if (!parentData || !dataPath) { 130 | // TODO: support modifying "bare" types 131 | throw new Error( 132 | `Invalid coerceTo "${schema}" must be contained in object or array` 133 | ) 134 | } 135 | 136 | try { 137 | const transformedData = coerceToType(data) 138 | if (!transformedData) { 139 | return false 140 | } 141 | 142 | parentData[parentDataProperty] = transformedData 143 | return true 144 | } catch (err) { 145 | this.errors = [err] 146 | return false 147 | } 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Performs **encoding** during schema validation of non-JSON-primitive data 154 | * types from their native JavaScript types to serialized string representations. 155 | * 156 | * Used for converting from JS Date objects to ISO utf8 strings. 157 | * Used for converting from JS Buffer objects to base64-encoded strings. 158 | */ 159 | const coerceFromKeyword: Ajv.KeywordDefinition = { 160 | type: 'object', 161 | modifying: true, 162 | errors: true, 163 | compile: (schema: any): Ajv.ValidateFunction => { 164 | const coercionType = customCoercionTypes[schema] 165 | if (!coercionType) { 166 | throw new Error(`Invalid coerceFrom "${schema}"`) 167 | } 168 | 169 | const coerceFromType: (data: any) => any = coercionType.from 170 | 171 | return function coerceFrom( 172 | data: object, 173 | dataPath: string, 174 | parentData: object | any[], 175 | parentDataProperty: string | number 176 | ): boolean { 177 | if (!parentData || !dataPath) { 178 | // TODO: support modifying "bare" types 179 | throw new Error( 180 | `Invalid coerceFrom "${schema}" must be contained in object or array` 181 | ) 182 | } 183 | 184 | try { 185 | const transformedData = coerceFromType(data) 186 | if (!transformedData) { 187 | return false 188 | } 189 | 190 | parentData[parentDataProperty] = transformedData 191 | return true 192 | } catch (err) { 193 | this.errors = [err] 194 | return false 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /packages/fts-http/src/handler.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import cloneDeep from 'clone-deep' 3 | import delay from 'delay' 4 | import fs from 'fs-extra' 5 | import { generateDefinition } from 'fts' 6 | import { createValidator } from 'fts-validator' 7 | import getPort from 'get-port' 8 | import globby from 'globby' 9 | import got from 'got' 10 | import jsf from 'json-schema-faker' 11 | import path from 'path' 12 | import pify from 'pify' 13 | import qs from 'qs' 14 | import seedrandom from 'seedrandom' 15 | import tempy from 'tempy' 16 | import * as HTTP from '.' 17 | 18 | const fixtures = globby.sync('./fixtures/http-request.ts') 19 | // const fixtures = globby.sync('./fixtures/**/*.{js,ts}') 20 | 21 | jsf.option({ 22 | alwaysFakeOptionals: true, 23 | // make values generated by json-schema-faker deterministic 24 | random: seedrandom('NzYxNDdlNjgxYzliN2FkNjFmYjBlMTI5') 25 | }) 26 | 27 | // 1x1 png from http://www.1x1px.me/ 28 | const image = 29 | 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' 30 | 31 | for (const fixture of fixtures) { 32 | const { name, dir } = path.parse(fixture) 33 | const testConfigPath = path.join(process.cwd(), dir, 'config.json') 34 | 35 | test.serial(name, async (t) => { 36 | let testConfig = { 37 | get: true, 38 | post: true 39 | } 40 | 41 | if (fs.pathExistsSync(testConfigPath)) { 42 | testConfig = { 43 | ...testConfig, 44 | ...require(testConfigPath) 45 | } 46 | } 47 | 48 | const outDir = tempy.directory() 49 | const definition = await generateDefinition(fixture, { 50 | compilerOptions: { 51 | outDir 52 | }, 53 | emit: true 54 | }) 55 | t.truthy(definition) 56 | 57 | const isRawHttpRequest = !!definition.params.http 58 | const isRawHttpResponse = !!definition.returns.http 59 | const hasContext = !!definition.params.context 60 | const supportsRest = definition.params.schema.additionalProperties 61 | const validator = createValidator() 62 | const paramsDecoder = validator.decoder(definition.params.schema) 63 | const returnsEncoder = validator.encoder(definition.returns.schema) 64 | 65 | const jsFilePath = path.join(outDir, `${name}.js`) 66 | const handler = HTTP.createHttpHandler(definition, jsFilePath, { 67 | debug: true 68 | }) 69 | t.is(typeof handler, 'function') 70 | 71 | const port = await getPort() 72 | const server = await HTTP.createHttpServer(handler, port) 73 | const url = `http://localhost:${port}` 74 | 75 | const params = await jsf.resolve(definition.params.schema) 76 | const paramsLocal = cloneDeep(params) 77 | paramsDecoder(paramsLocal) 78 | t.is(paramsDecoder.errors, null) 79 | 80 | let paramsLocalArray: any[] = [] 81 | if (isRawHttpRequest) { 82 | if (definition.params.order.length) { 83 | paramsLocalArray = [Buffer.from(image, 'base64')] 84 | } 85 | 86 | if (hasContext) { 87 | paramsLocalArray.push({ 88 | contentType: 'image/png' 89 | }) 90 | } 91 | } else { 92 | paramsLocalArray = definition.params.order.map((key) => paramsLocal[key]) 93 | 94 | if (supportsRest) { 95 | const additionalProperty = 'Hello, World!' 96 | params.additionalProperty = additionalProperty 97 | paramsLocalArray.push(['additionalProperty', additionalProperty]) 98 | } 99 | } 100 | 101 | const func = HTTP.requireHandlerFunction(definition, jsFilePath) 102 | 103 | const result = await Promise.resolve(func(...paramsLocalArray)) 104 | const expected = { result } 105 | if (!isRawHttpResponse) { 106 | returnsEncoder(expected) 107 | t.is(returnsEncoder.errors, null) 108 | } else { 109 | expected.result = (result.body || Buffer.from('')).toString() 110 | } 111 | const expectedEncoded = JSON.parse(JSON.stringify(expected)) 112 | console.log({ name, params, port, expected }) 113 | 114 | // test GET request with params as a query string 115 | // note: some fixtures will not support this type of encoding 116 | if (!definition.params.http && testConfig.get) { 117 | const query = qs.stringify(params) 118 | const temp: any = { query } 119 | if (!isRawHttpResponse) { 120 | temp.json = true 121 | } 122 | const responseGET = await got(url, temp) 123 | validateResponseSuccess(responseGET, 'GET', expectedEncoded) 124 | } 125 | 126 | // test POST request with params as a json body object 127 | if (testConfig.post) { 128 | const temp: any = { body: params } 129 | if (isRawHttpRequest) { 130 | temp.body = Buffer.from(image, 'base64') 131 | temp.headers = { 132 | 'Content-type': 'image/png' 133 | } 134 | } else if (isRawHttpResponse) { 135 | temp.body = JSON.stringify(params) 136 | temp.headers = { 137 | 'Content-type': 'application/json' 138 | } 139 | } else { 140 | temp.json = true 141 | } 142 | const responsePOST = await got.post(url, temp) 143 | validateResponseSuccess(responsePOST, 'POST', expectedEncoded) 144 | } 145 | 146 | await pify(server.close.bind(server))() 147 | await delay(500) 148 | await fs.remove(outDir) 149 | 150 | function validateResponseSuccess( 151 | res: got.Response, 152 | label: string, 153 | expected: any 154 | ) { 155 | console.log({ 156 | body: res.body, 157 | label, 158 | statusCode: res.statusCode 159 | }) 160 | t.is(res.statusCode, 200) 161 | const response = { result: res.body as any } 162 | 163 | if (result === undefined || result === null) { 164 | if (response.result === '') { 165 | response.result = result 166 | } 167 | } 168 | 169 | const responseEncoded = JSON.parse(JSON.stringify(response)) 170 | t.deepEqual(responseEncoded, expected) 171 | } 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /packages/now-fts/index.js: -------------------------------------------------------------------------------- 1 | const { createLambda } = require('@now/build-utils/lambda.js') 2 | const download = require('@now/build-utils/fs/download.js') 3 | const FileBlob = require('@now/build-utils/file-blob.js') 4 | const FileFsRef = require('@now/build-utils/file-fs-ref.js') 5 | const execa = require('execa') 6 | const fs = require('fs-extra') 7 | const glob = require('@now/build-utils/fs/glob.js') 8 | const path = require('path') 9 | const { 10 | runNpmInstall, 11 | runPackageJsonScript 12 | } = require('@now/build-utils/fs/run-user-scripts.js') 13 | 14 | const tsconfig = 'tsconfig.json' 15 | 16 | /** @typedef { import('@now/build-utils/file-ref') } FileRef */ 17 | /** @typedef {{[filePath: string]: FileRef}} Files */ 18 | 19 | /** 20 | * @typedef {Object} BuildParamsType 21 | * @property {Files} files - Files object 22 | * @property {string} entrypoint - Entrypoint specified for the builder 23 | * @property {string} workPath - Working directory for this build 24 | */ 25 | 26 | /** 27 | * @param {BuildParamsType} buildParams 28 | * @param {Object} [options] 29 | * @param {string[]} [options.npmArguments] 30 | */ 31 | async function downloadInstallAndBundle( 32 | { files, entrypoint, workPath }, 33 | { npmArguments = [] } = {} 34 | ) { 35 | const userPath = path.join(workPath, 'user') 36 | const nccPath = path.join(workPath, 'ncc') 37 | const ftsPath = path.join(workPath, 'fts') 38 | 39 | console.log() 40 | console.log('downloading user files...') 41 | const downloadedFiles = await download(files, userPath) 42 | 43 | console.log() 44 | console.log("installing dependencies for user's code...") 45 | const entrypointFsDirname = path.join(userPath, path.dirname(entrypoint)) 46 | await runNpmInstall(entrypointFsDirname, npmArguments) 47 | 48 | console.log('writing ncc package.json...') 49 | fs.outputJsonSync(path.join(nccPath, 'package.json'), { 50 | dependencies: { 51 | '@zeit/ncc': '0.11.0', 52 | typescript: '3.2.4' 53 | } 54 | }) 55 | 56 | console.log() 57 | console.log('installing dependencies for ncc...') 58 | await runNpmInstall(nccPath, npmArguments) 59 | 60 | console.log('writing fts package.json...') 61 | fs.outputJsonSync(path.join(ftsPath, 'package.json'), { 62 | dependencies: { 63 | fts: 'latest', 64 | 'fts-http': 'latest' 65 | } 66 | }) 67 | 68 | const ftsTsConfigPath = path.join(ftsPath, tsconfig) 69 | if (downloadedFiles[tsconfig]) { 70 | console.log(`copying ${tsconfig}`) 71 | fs.copySync(downloadedFiles[tsconfig].fsPath, ftsTsConfigPath) 72 | } else { 73 | console.log(`using default ${tsconfig}`) 74 | fs.outputJsonSync(ftsTsConfigPath, { 75 | compilerOptions: { 76 | target: 'es2015', 77 | moduleResolution: 'node' 78 | } 79 | }) 80 | } 81 | 82 | // TODO: for local testing purposes 83 | // console.log('linking dependencies for fts...') 84 | // execa.shellSync('yarn link fts fts-http', { cwd: ftsPath, stdio: 'inherit' }) 85 | 86 | console.log() 87 | console.log('installing dependencies for fts...') 88 | await runNpmInstall(ftsPath, npmArguments) 89 | 90 | return [downloadedFiles, nccPath, ftsPath, entrypointFsDirname] 91 | } 92 | 93 | async function generateDefinitionAndCompile({ 94 | nccPath, 95 | ftsPath, 96 | downloadedFiles, 97 | entrypoint 98 | }) { 99 | const input = downloadedFiles[entrypoint].fsPath 100 | const preparedFiles = {} 101 | 102 | console.log() 103 | console.log('generating entrypoint fts definition...') 104 | const fts = require(path.join(ftsPath, 'node_modules/fts')) 105 | const definition = await fts.generateDefinition(input) 106 | const definitionData = JSON.stringify(definition, null, 2) 107 | console.log('fts definition', definitionData) 108 | 109 | // TODO: this should be accessible via a special route 110 | // const definitionFsPath = path.join(ftsPath, 'definition.json') 111 | // preparedFiles[definitionFsPath] = new FileBlob({ data: definitionData }) 112 | 113 | const handlerPath = path.join(ftsPath, 'handler.ts') 114 | const handler = ` 115 | import * as ftsHttp from 'fts-http' 116 | import * as handler from '${input.replace('.ts', '')}' 117 | const definition = ${definitionData} 118 | export default ftsHttp.createHttpHandler(definition, handler) 119 | ` 120 | 121 | fs.writeFileSync(handlerPath, handler, 'utf8') 122 | 123 | console.log() 124 | console.log('compiling entrypoint with ncc...') 125 | const ncc = require(path.join(nccPath, 'node_modules/@zeit/ncc')) 126 | const { code, assets } = await ncc(handlerPath) 127 | const outputHandlerPath = path.join('user', 'fts-handler.js') 128 | 129 | const blob = new FileBlob({ data: code }) 130 | // move all user code to 'user' subdirectory 131 | preparedFiles[outputHandlerPath] = blob 132 | // eslint-disable-next-line no-restricted-syntax 133 | for (const assetName of Object.keys(assets)) { 134 | const { source: data, permissions: mode } = assets[assetName] 135 | const blob2 = new FileBlob({ data, mode }) 136 | 137 | preparedFiles[ 138 | path.join('user', path.dirname(entrypoint), assetName) 139 | ] = blob2 140 | } 141 | 142 | return { 143 | preparedFiles, 144 | handlerPath: outputHandlerPath 145 | } 146 | } 147 | 148 | exports.config = { 149 | maxLambdaSize: '5mb' 150 | } 151 | 152 | /** 153 | * @param {BuildParamsType} buildParams 154 | * @returns {Promise} 155 | */ 156 | exports.build = async ({ files, entrypoint, workPath }) => { 157 | const [ 158 | downloadedFiles, 159 | nccPath, 160 | ftsPath, 161 | entrypointFsDirname 162 | ] = await downloadInstallAndBundle( 163 | { files, entrypoint, workPath }, 164 | { npmArguments: ['--prefer-offline'] } 165 | ) 166 | 167 | console.log('running user script...') 168 | await runPackageJsonScript(entrypointFsDirname, 'now-build') 169 | 170 | const { preparedFiles, handlerPath } = await generateDefinitionAndCompile({ 171 | nccPath, 172 | ftsPath, 173 | downloadedFiles, 174 | entrypoint 175 | }) 176 | 177 | // setting up launcher 178 | const launcherPath = path.join(__dirname, 'launcher.js') 179 | let launcherData = await fs.readFile(launcherPath, 'utf8') 180 | 181 | launcherData = launcherData.replace( 182 | '// PLACEHOLDER', 183 | [ 184 | 'process.chdir("./user");', 185 | `listener = require("${handlerPath}");`, 186 | 'if (listener.default) listener = listener.default;' 187 | ].join(' ') 188 | ) 189 | 190 | const launcherFiles = { 191 | 'launcher.js': new FileBlob({ data: launcherData }), 192 | 'bridge.js': new FileFsRef({ fsPath: require('@now/node-bridge') }) 193 | } 194 | 195 | const lambda = await createLambda({ 196 | files: { ...preparedFiles, ...launcherFiles }, 197 | handler: 'launcher.launcher', 198 | runtime: 'nodejs8.10' 199 | }) 200 | 201 | return { [entrypoint]: lambda } 202 | } 203 | 204 | exports.prepareCache = async ({ files, entrypoint, workPath, cachePath }) => { 205 | await fs.remove(workPath) 206 | await downloadInstallAndBundle({ files, entrypoint, workPath: cachePath }) 207 | 208 | return { 209 | ...(await glob('user/node_modules/**', cachePath)), 210 | ...(await glob('user/package-lock.json', cachePath)), 211 | ...(await glob('user/yarn.lock', cachePath)), 212 | ...(await glob('ncc/node_modules/**', cachePath)), 213 | ...(await glob('ncc/package-lock.json', cachePath)), 214 | ...(await glob('ncc/yarn.lock', cachePath)), 215 | ...(await glob('fts/node_modules/**', cachePath)), 216 | ...(await glob('fts/package-lock.json', cachePath)), 217 | ...(await glob('fts/yarn.lock', cachePath)) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /examples/now-sushi/src/frontend/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100vw; 4 | height: 100vh; 5 | overflow: hidden; 6 | margin: 0; 7 | } 8 | body { 9 | display: flex; 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 11 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 12 | justify-content: center; 13 | align-items: center; 14 | background-color: #000; 15 | color: white; 16 | text-rendering: optimizeLegibility; 17 | -webkit-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | text-align: center; 20 | } 21 | 22 | body * { 23 | box-sizing: border-box; 24 | } 25 | 26 | a:link, 27 | a:visited { 28 | color: cyan; 29 | text-decoration: none; 30 | } 31 | 32 | small { 33 | display: inline-block; 34 | text-align: justify; 35 | max-width: 400px; 36 | width: 80%; 37 | font-size: 12px; 38 | color: #757575; 39 | } 40 | 41 | button, 42 | .button { 43 | display: inline-block; 44 | text-decoration: none; 45 | color: white; 46 | border: 1px solid cyan; 47 | background: transparent; 48 | font-size: 16px; 49 | padding: 8px; 50 | border-radius: 3px; 51 | width: 200px; 52 | text-transform: capitalize; 53 | cursor: pointer; 54 | } 55 | 56 | button:hover, 57 | button:active, 58 | button:focus { 59 | background: cyan; 60 | color: black; 61 | } 62 | 63 | .sushi-detail { 64 | display: grid; 65 | grid-template-columns: 100px auto; 66 | grid-gap: 16px; 67 | padding: 0 16px; 68 | align-items: center; 69 | justify-content: start; 70 | text-align: left; 71 | margin-bottom: 16px; 72 | max-width: 400px; 73 | } 74 | 75 | .image-container { 76 | width: 100px; 77 | height: 100px; 78 | background: white; 79 | border-radius: 50%; 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | overflow: hidden; 84 | } 85 | 86 | img { 87 | max-width: 100%; 88 | } 89 | 90 | p { 91 | line-height: 23px; 92 | } 93 | 94 | ul { 95 | list-style-type: none; 96 | margin: 0; 97 | padding: 0; 98 | } 99 | 100 | li + li { 101 | margin-top: 8px; 102 | } 103 | 104 | .sushi-machine { 105 | position: relative; 106 | display: flex; 107 | flex-direction: column; 108 | align-items: center; 109 | overflow-x: hidden; 110 | } 111 | .rice { 112 | width: 80px; 113 | height: 20px; 114 | background-color: #fff; 115 | border-bottom: 4px solid #ddf5f1; 116 | border-radius: 6px; 117 | transform-origin: center bottom; 118 | animation: rice 1.6s infinite; 119 | } 120 | .rice:before { 121 | content: ''; 122 | display: block; 123 | position: absolute; 124 | top: calc(100% + 4px); 125 | left: 50%; 126 | margin-left: -40px; 127 | width: 80px; 128 | height: 6px; 129 | border-radius: 3px; 130 | background-color: rgba(0, 0, 0, 0.1); 131 | } 132 | .neta { 133 | width: 88px; 134 | height: 20px; 135 | border-top: 4px solid rgba(255, 255, 255, 0.5); 136 | border-radius: 20px 80px 0 20px; 137 | background: repeating-linear-gradient( 138 | 45deg, 139 | #ff8c08, 140 | #ff8c08 10px, 141 | #ffe771 10px, 142 | #ffe771 14px 143 | ); 144 | z-index: 10; 145 | transform-origin: center -10vh; 146 | animation: neta 1.6s ease-in infinite; 147 | } 148 | .sushi { 149 | opacity: 0; 150 | position: absolute; 151 | bottom: 20px; 152 | left: 50%; 153 | margin-left: -40px; 154 | width: 80px; 155 | height: 20px; 156 | background-color: #fff; 157 | border-bottom: 4px solid #ddf5f1; 158 | border-radius: 0 0 6px 6px; 159 | animation: sushi 1.6s infinite forwards; 160 | animation-delay: 0.8s; 161 | } 162 | .sushi:before { 163 | content: ''; 164 | display: block; 165 | position: absolute; 166 | top: calc(100% + 4px); 167 | left: 50%; 168 | margin-left: -40px; 169 | width: 80px; 170 | height: 6px; 171 | border-radius: 3px; 172 | background-color: rgba(0, 0, 0, 0.1); 173 | } 174 | .sushi:after { 175 | content: ''; 176 | display: block; 177 | box-sizing: inherit; 178 | position: absolute; 179 | bottom: 100%; 180 | left: -4px; 181 | width: 88px; 182 | height: 20px; 183 | border-top: 4px solid rgba(255, 255, 255, 0.5); 184 | border-radius: 20px 80px 0 20px; 185 | background: repeating-linear-gradient( 186 | 45deg, 187 | #ff8c08, 188 | #ff8c08 10px, 189 | #ffe771 10px, 190 | #ffe771 14px 191 | ); 192 | } 193 | .table { 194 | opacity: 1; 195 | width: 100%; 196 | max-width: 400px; 197 | height: 20px; 198 | background-color: #f5eab9; 199 | border-bottom: 6px solid #fbd991; 200 | } 201 | @-moz-keyframes neta { 202 | 0% { 203 | transform: scale(0) translate(0, -5vh); 204 | } 205 | 50% { 206 | transform: scale(1) translate(0, -5vh); 207 | } 208 | 90% { 209 | transform: scale(1) translate(0, 1.5vh); 210 | } 211 | 100% { 212 | transform: scale(1) translate(0, 0); 213 | } 214 | } 215 | @-webkit-keyframes neta { 216 | 0% { 217 | transform: scale(0) translate(0, -5vh); 218 | } 219 | 50% { 220 | transform: scale(1) translate(0, -5vh); 221 | } 222 | 90% { 223 | transform: scale(1) translate(0, 1.5vh); 224 | } 225 | 100% { 226 | transform: scale(1) translate(0, 0); 227 | } 228 | } 229 | @-o-keyframes neta { 230 | 0% { 231 | transform: scale(0) translate(0, -5vh); 232 | } 233 | 50% { 234 | transform: scale(1) translate(0, -5vh); 235 | } 236 | 90% { 237 | transform: scale(1) translate(0, 1.5vh); 238 | } 239 | 100% { 240 | transform: scale(1) translate(0, 0); 241 | } 242 | } 243 | @keyframes neta { 244 | 0% { 245 | transform: scale(0) translate(0, -5vh); 246 | } 247 | 50% { 248 | transform: scale(1) translate(0, -5vh); 249 | } 250 | 90% { 251 | transform: scale(1) translate(0, 1.5vh); 252 | } 253 | 100% { 254 | transform: scale(1) translate(0, 0); 255 | } 256 | } 257 | @-moz-keyframes rice { 258 | 0% { 259 | transform: scaleY(1) translateX(288px); 260 | } 261 | 50%, 262 | 80% { 263 | transform: scaleY(1) translateX(0); 264 | } 265 | 95% { 266 | transform: scaleY(0.8) translateX(0); 267 | } 268 | 100% { 269 | transform: scaleY(1) translateX(0); 270 | } 271 | } 272 | @-webkit-keyframes rice { 273 | 0% { 274 | transform: scaleY(1) translateX(288px); 275 | } 276 | 50%, 277 | 80% { 278 | transform: scaleY(1) translateX(0); 279 | } 280 | 95% { 281 | transform: scaleY(0.8) translateX(0); 282 | } 283 | 100% { 284 | transform: scaleY(1) translateX(0); 285 | } 286 | } 287 | @-o-keyframes rice { 288 | 0% { 289 | transform: scaleY(1) translateX(288px); 290 | } 291 | 50%, 292 | 80% { 293 | transform: scaleY(1) translateX(0); 294 | } 295 | 95% { 296 | transform: scaleY(0.8) translateX(0); 297 | } 298 | 100% { 299 | transform: scaleY(1) translateX(0); 300 | } 301 | } 302 | @keyframes rice { 303 | 0% { 304 | transform: scaleY(1) translateX(288px); 305 | } 306 | 50%, 307 | 80% { 308 | transform: scaleY(1) translateX(0); 309 | } 310 | 95% { 311 | transform: scaleY(0.8) translateX(0); 312 | } 313 | 100% { 314 | transform: scaleY(1) translateX(0); 315 | } 316 | } 317 | @-moz-keyframes sushi { 318 | 0%, 319 | 49% { 320 | opacity: 0; 321 | transform: translateX(0); 322 | } 323 | 50% { 324 | opacity: 1; 325 | transform: translateX(0); 326 | } 327 | 100% { 328 | opacity: 1; 329 | transform: translateX(-288px); 330 | } 331 | } 332 | @-webkit-keyframes sushi { 333 | 0%, 334 | 49% { 335 | opacity: 0; 336 | transform: translateX(0); 337 | } 338 | 50% { 339 | opacity: 1; 340 | transform: translateX(0); 341 | } 342 | 100% { 343 | opacity: 1; 344 | transform: translateX(-288px); 345 | } 346 | } 347 | @-o-keyframes sushi { 348 | 0%, 349 | 49% { 350 | opacity: 0; 351 | transform: translateX(0); 352 | } 353 | 50% { 354 | opacity: 1; 355 | transform: translateX(0); 356 | } 357 | 100% { 358 | opacity: 1; 359 | transform: translateX(-288px); 360 | } 361 | } 362 | @keyframes sushi { 363 | 0%, 364 | 49% { 365 | opacity: 0; 366 | transform: translateX(0); 367 | } 368 | 50% { 369 | opacity: 1; 370 | transform: translateX(0); 371 | } 372 | 100% { 373 | opacity: 1; 374 | transform: translateX(-288px); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /packages/fts-http/src/handler.ts: -------------------------------------------------------------------------------- 1 | import contentType from 'content-type' 2 | import decompressRequest from 'decompress-request' 3 | import fileType from 'file-type' 4 | import fs from 'fs' 5 | import { Definition } from 'fts' 6 | import { createValidator } from 'fts-validator' 7 | import http from 'http' 8 | import { readable } from 'is-stream' 9 | import * as micro from 'micro' 10 | import microCORS = require('micro-cors') 11 | import mime from 'mime-types' 12 | import multiparty from 'multiparty' 13 | import raw = require('raw-body') 14 | import { Stream } from 'stream' 15 | import formParser from 'urlencoded-body-parser' 16 | 17 | import { HttpContext } from './http-context' 18 | import { requireHandlerFunction } from './require-handler-function' 19 | 20 | const DEV = process.env.NODE_ENV === 'development' 21 | const BODY_SIZE_LIMIT = '100mb' 22 | 23 | interface Options { 24 | debug: boolean 25 | cors: { 26 | allowMethods: string[] 27 | } 28 | } 29 | 30 | export function createHttpHandler( 31 | definition: Definition, 32 | jsFilePathOrModule: string | object, 33 | opts: Partial = {} 34 | ) { 35 | const { 36 | debug = false, 37 | cors = { 38 | allowMethods: ['GET', 'POST', 'OPTIONS', 'HEAD'] 39 | } 40 | } = opts 41 | 42 | const validator = createValidator() 43 | const validateParams = validator.decoder(definition.params.schema) 44 | const validateReturns = definition.returns.http 45 | ? null 46 | : validator.encoder(definition.returns.schema) 47 | 48 | const innerHandler = requireHandlerFunction(definition, jsFilePathOrModule) 49 | 50 | // Note: it is inconvenient but important for this handler to not be async in 51 | // order to maximize compatibility with different Node.js server frameworks. 52 | const handler = (req: http.IncomingMessage, res: http.ServerResponse) => { 53 | if (opts.debug) { 54 | if (req.method !== 'OPTIONS') { 55 | console.log(req.method, req.url, req.headers) 56 | } 57 | } 58 | 59 | const context = new HttpContext(req, res) 60 | 61 | if (context.req.method === 'OPTIONS') { 62 | send(context, 200, 'ok') 63 | return 64 | } 65 | 66 | getParams(context, definition, debug) 67 | .then((params: any) => { 68 | let args: any[] = [] 69 | 70 | if (definition.params.http) { 71 | if (definition.params.order.length) { 72 | args = [params] 73 | } 74 | 75 | if (definition.params.context) { 76 | args.push(context) 77 | } 78 | } else { 79 | const hasValidParams = validateParams(params) 80 | if (!hasValidParams) { 81 | const message = validator.ajv.errorsText(validateParams.errors) 82 | sendError(context, new Error(message), 400) 83 | return 84 | } 85 | 86 | args = definition.params.order.map((name) => params[name]) 87 | if (definition.params.context) { 88 | args.push(context) 89 | } 90 | 91 | // Push additional props into args if allowing additionalProperties 92 | if (definition.params.schema.additionalProperties) { 93 | Object.keys(params).forEach((name) => { 94 | if (definition.params.order.indexOf(name) === -1) { 95 | args.push([name, params[name]]) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | try { 102 | Promise.resolve(innerHandler(...args)) 103 | .then((result: any) => { 104 | const returns = { result } 105 | 106 | if (definition.returns.http) { 107 | // skip validation for raw http response 108 | returns.result = result.body 109 | res.statusCode = result.statusCode 110 | 111 | if (result.headers) { 112 | for (const [key, value] of Object.entries(result.headers)) { 113 | res.setHeader(key, value as string | number | string[]) 114 | } 115 | } 116 | } else { 117 | // validate return value 118 | const isValidReturnType = validateReturns(returns) 119 | if (!isValidReturnType) { 120 | const message = validator.ajv.errorsText( 121 | validateReturns.errors 122 | ) 123 | sendError(context, new Error(message), 502) 124 | return 125 | } 126 | } 127 | 128 | if (returns.result === null || returns.result === undefined) { 129 | send(context, res.statusCode || 204, returns.result) 130 | } else { 131 | send(context, res.statusCode || 200, returns.result) 132 | } 133 | }) 134 | .catch((err) => { 135 | sendError(context, err, 403) 136 | }) 137 | } catch (err) { 138 | sendError(context, err) 139 | } 140 | }) 141 | .catch((err) => { 142 | sendError(context, err) 143 | }) 144 | } 145 | 146 | return microCORS(cors)(handler) 147 | } 148 | 149 | async function getParams( 150 | context: HttpContext, 151 | definition: Definition, 152 | debug: boolean 153 | ): Promise { 154 | if (definition.params.http) { 155 | if (!definition.params.order.length) { 156 | return null 157 | } else { 158 | if (debug) { 159 | console.log( 160 | 'fts-http', 161 | 'headers', 162 | JSON.stringify(context.req.headers, null, 2) 163 | ) 164 | } 165 | 166 | return getBody(context) 167 | } 168 | } else { 169 | let params: any = {} 170 | 171 | if (context.req.method === 'GET') { 172 | params = context.query 173 | } else if (context.req.method === 'POST') { 174 | if (context.is('multipart/form-data')) { 175 | const form = new multiparty.Form() 176 | 177 | // TODO: clean this up 178 | params = await new Promise((resolve, reject) => { 179 | form.parse(context.req, (err, fields, files) => { 180 | if (err) { 181 | return reject(err) 182 | } 183 | 184 | // TODO: I believe field values are assumed to be strings but should 185 | // probably be parsed as json? 186 | 187 | for (const key in files) { 188 | if (!files.hasOwnProperty(key)) { 189 | continue 190 | } 191 | 192 | const file = files[key][0] 193 | if (!file) { 194 | continue 195 | } 196 | 197 | let v: string | Buffer = fs.readFileSync(file.path) 198 | let m = 'application/octet-stream' 199 | const ct = file.headers['content-type'] 200 | let charset: string 201 | 202 | if (ct) { 203 | const c = contentType.parse(ct) 204 | if (c) { 205 | m = c.type 206 | charset = c.parameters.charset 207 | } 208 | } else { 209 | const f = fileType(v) 210 | if (f) { 211 | m = f.mime 212 | } 213 | } 214 | 215 | if (!charset) { 216 | charset = mime.charset(m) 217 | } 218 | 219 | if (charset) { 220 | v = v.toString(charset) 221 | } 222 | 223 | fields[key] = v 224 | } 225 | 226 | resolve(fields) 227 | }) 228 | }) 229 | } else if (context.is('application/x-www-form-urlencoded')) { 230 | params = await formParser(context.req, { 231 | limit: BODY_SIZE_LIMIT 232 | }) 233 | } else { 234 | const body = await getBody(context) 235 | params = JSON.parse(body.toString('utf8')) 236 | } 237 | } else { 238 | throw micro.createError(501, 'Not implemented\n') 239 | } 240 | 241 | if (typeof params !== 'object') { 242 | throw micro.createError(400, 'Invalid parameters\n') 243 | } 244 | 245 | return params 246 | } 247 | } 248 | 249 | async function getBody(context: HttpContext): Promise { 250 | const opts: any = { 251 | limit: BODY_SIZE_LIMIT 252 | } 253 | 254 | const len = context.req.headers['content-length'] 255 | const encoding = context.req.headers['content-encoding'] || 'identity' 256 | if (len && encoding === 'identity') { 257 | opts.length = +len 258 | } 259 | 260 | return (raw(decompressRequest(context.req), opts) as unknown) as Promise< 261 | Buffer 262 | > 263 | } 264 | 265 | function send(context: HttpContext, code: number, obj: any = null) { 266 | const { res } = context 267 | res.statusCode = code 268 | 269 | if (obj === null || obj === undefined) { 270 | res.end() 271 | return 272 | } 273 | 274 | if (Buffer.isBuffer(obj)) { 275 | if (!res.getHeader('Content-Type')) { 276 | res.setHeader('Content-Type', 'application/octet-stream') 277 | } 278 | 279 | res.setHeader('Content-Length', obj.length) 280 | res.end(obj) 281 | return 282 | } 283 | 284 | if (obj instanceof Stream || readable(obj)) { 285 | if (!res.getHeader('Content-Type')) { 286 | res.setHeader('Content-Type', 'application/octet-stream') 287 | } 288 | 289 | obj.pipe(res) 290 | return 291 | } 292 | 293 | let jsonify = false 294 | let str = obj 295 | 296 | switch (typeof str) { 297 | case 'object': 298 | jsonify = true 299 | break 300 | case 'number': 301 | jsonify = true 302 | break 303 | case 'boolean': 304 | jsonify = true 305 | break 306 | case 'string': 307 | jsonify = context.accepts('text', 'json') === 'json' 308 | break 309 | default: 310 | throw micro.createError(500, 'Unexpected return type\n') 311 | } 312 | 313 | if (jsonify) { 314 | // We stringify before setting the header in case `JSON.stringify` throws 315 | // and a 500 has to be sent instead. 316 | 317 | // The `JSON.stringify` call is split into two cases as `JSON.stringify` 318 | // is optimized in V8 if called with only one argument 319 | str = DEV ? JSON.stringify(obj, null, 2) : JSON.stringify(obj) 320 | 321 | if (!res.getHeader('Content-Type')) { 322 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 323 | } 324 | } else { 325 | if (!res.getHeader('Content-Type')) { 326 | // TODO: why does this seem to be necessary when using text/plain? 327 | if (typeof str === 'string' && !str.endsWith('\n')) { 328 | str += '\n' 329 | } 330 | 331 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 332 | } 333 | } 334 | 335 | res.setHeader('Content-Length', Buffer.byteLength(str)) 336 | res.end(str) 337 | } 338 | 339 | function sendError(context: HttpContext, error: Error, statusCode?: number) { 340 | /* tslint:disable no-string-literal */ 341 | if (statusCode || error['statusCode'] === undefined) { 342 | error['statusCode'] = statusCode || 500 343 | } 344 | 345 | console.error(error) 346 | 347 | /* tslint:enable no-string-literal */ 348 | micro.sendError(context.req, context.res, error) 349 | } 350 | -------------------------------------------------------------------------------- /packages/fts/src/parser.ts: -------------------------------------------------------------------------------- 1 | // TODO: parser would be ~2x faster if we reused the underlying ts program from TS in TJS 2 | 3 | import doctrine from 'doctrine' 4 | import fs from 'fs-extra' 5 | import { version } from 'fts-core' 6 | import path from 'path' 7 | import tempy from 'tempy' 8 | import * as TS from 'ts-morph' 9 | import * as TJS from 'typescript-json-schema' 10 | import * as FTS from './types' 11 | 12 | const FTSReturns = 'FTSReturns' 13 | const FTSParams = 'FTSParams' 14 | 15 | const promiseTypeRe = /^Promise<(.*)>$/ 16 | 17 | const supportedExtensions = { 18 | js: 'javascript', 19 | jsx: 'javascript', 20 | ts: 'typescript', 21 | tsx: 'typescript' 22 | } 23 | 24 | export async function generateDefinition( 25 | file: string, 26 | options: FTS.PartialDefinitionOptions = {} 27 | ): Promise { 28 | file = path.resolve(file) 29 | const fileInfo = path.parse(file) 30 | const language = supportedExtensions[fileInfo.ext.substr(1)] 31 | 32 | if (!language) { 33 | throw new Error(`File type "${fileInfo.ext}" not supported. "${file}"`) 34 | } 35 | 36 | const outDir = tempy.directory() 37 | 38 | // initialize and compile TS program 39 | const compilerOptions = { 40 | allowJs: true, 41 | ignoreCompilerErrors: true, 42 | esModuleInterop: true, 43 | // TODO: why do we need to specify the full filename for these lib definition files? 44 | lib: ['lib.es2018.d.ts', 'lib.dom.d.ts'], 45 | target: TS.ScriptTarget.ES5, 46 | outDir, 47 | ...(options.compilerOptions || {}) 48 | } 49 | 50 | const jsonSchemaOptions = { 51 | noExtraProps: true, 52 | required: true, 53 | validationKeywords: ['coerceTo', 'coerceFrom'], 54 | ...(options.jsonSchemaOptions || {}) 55 | } 56 | 57 | const definition: Partial = { 58 | config: { 59 | defaultExport: true, 60 | language 61 | }, 62 | params: { 63 | http: false, 64 | context: false, 65 | order: [], 66 | schema: null 67 | }, 68 | returns: { 69 | async: false, 70 | http: false, 71 | schema: null 72 | }, 73 | version 74 | } 75 | 76 | const project = new TS.Project({ compilerOptions }) 77 | 78 | project.addExistingSourceFile(file) 79 | project.resolveSourceFileDependencies() 80 | 81 | const diagnostics = project.getPreEmitDiagnostics() 82 | if (diagnostics.length > 0) { 83 | console.log(project.formatDiagnosticsWithColorAndContext(diagnostics)) 84 | 85 | // TODO: throw error? 86 | } 87 | 88 | const sourceFile = project.getSourceFileOrThrow(file) 89 | const main = extractMainFunction(sourceFile, definition) 90 | 91 | if (!main) { 92 | throw new Error(`Unable to infer a main function export "${file}"`) 93 | } 94 | 95 | // extract main function type and documentation info 96 | const title = main.getName ? main.getName() : path.parse(file).name 97 | const mainTypeParams = main.getTypeParameters() 98 | definition.title = title 99 | 100 | if (mainTypeParams.length > 0) { 101 | throw new Error( 102 | `Generic Type Parameters are not supported for function "${title}"` 103 | ) 104 | } 105 | 106 | const doc = main.getJsDocs()[0] 107 | let docs: doctrine.Annotation 108 | 109 | if (doc) { 110 | const { description } = doc.getStructure() 111 | docs = doctrine.parse(description as string) 112 | if (docs.description) { 113 | definition.description = docs.description 114 | } 115 | } 116 | 117 | const builder: FTS.DefinitionBuilder = { 118 | definition, 119 | docs, 120 | main, 121 | sourceFile, 122 | title 123 | } 124 | 125 | if (options.emit) { 126 | const result = project.emit(options.emitOptions) 127 | if (result.getEmitSkipped()) { 128 | throw new Error('emit skipped') 129 | } 130 | } 131 | 132 | addParamsDeclaration(builder) 133 | addReturnTypeDeclaration(builder) 134 | 135 | // TODO: figure out a better workaround than mutating the source file directly 136 | // TODO: fix support for JS files since you can't save TS in JS 137 | const tempSourceFilePath = path.format({ 138 | dir: fileInfo.dir, 139 | ext: '.ts', 140 | name: `.${fileInfo.name}-fts` 141 | }) 142 | const tempSourceFile = sourceFile.copy(tempSourceFilePath, { 143 | overwrite: true 144 | }) 145 | await tempSourceFile.save() 146 | 147 | try { 148 | extractJSONSchemas(builder, tempSourceFilePath, jsonSchemaOptions) 149 | } finally { 150 | await fs.remove(tempSourceFilePath) 151 | } 152 | 153 | return builder.definition as FTS.Definition 154 | } 155 | 156 | /** Find main exported function declaration */ 157 | function extractMainFunction( 158 | sourceFile: TS.SourceFile, 159 | definition: Partial 160 | ): TS.FunctionDeclaration | undefined { 161 | const functionDefaultExports = sourceFile 162 | .getFunctions() 163 | .filter((f) => f.isDefaultExport()) 164 | 165 | if (functionDefaultExports.length === 1) { 166 | definition.config.defaultExport = true 167 | return functionDefaultExports[0] 168 | } else { 169 | definition.config.defaultExport = false 170 | } 171 | 172 | const functionExports = sourceFile 173 | .getFunctions() 174 | .filter((f) => f.isExported()) 175 | 176 | if (functionExports.length === 1) { 177 | const func = functionExports[0] 178 | definition.config.namedExport = func.getName() 179 | return func 180 | } 181 | 182 | if (functionExports.length > 1) { 183 | const externalFunctions = functionExports.filter((f) => { 184 | const docs = f.getJsDocs()[0] 185 | 186 | return ( 187 | docs && 188 | docs.getTags().find((tag) => { 189 | const tagName = tag.getTagName() 190 | return tagName === 'external' || tagName === 'public' 191 | }) 192 | ) 193 | }) 194 | 195 | if (externalFunctions.length === 1) { 196 | const func = externalFunctions[0] 197 | definition.config.namedExport = func.getName() 198 | return func 199 | } 200 | } 201 | 202 | // TODO: arrow function exports are a lil hacky 203 | const arrowFunctionExports = sourceFile 204 | .getDescendantsOfKind(TS.SyntaxKind.ArrowFunction) 205 | .filter((f) => TS.TypeGuards.isExportAssignment(f.getParent())) 206 | 207 | if (arrowFunctionExports.length === 1) { 208 | const func = arrowFunctionExports[0] 209 | const exportAssignment = func.getParent() as TS.ExportAssignment 210 | const exportSymbol = sourceFile.getDefaultExportSymbol() 211 | 212 | // TODO: handle named exports `export const foo = () => 'bar'` 213 | if (exportSymbol) { 214 | const defaultExportPos = exportSymbol 215 | .getValueDeclarationOrThrow() 216 | .getPos() 217 | const exportAssignmentPos = exportAssignment.getPos() 218 | 219 | // TODO: better way of comparing nodes 220 | const isDefaultExport = defaultExportPos === exportAssignmentPos 221 | 222 | if (isDefaultExport) { 223 | definition.config.defaultExport = true 224 | return (func as unknown) as TS.FunctionDeclaration 225 | } 226 | } 227 | } 228 | 229 | return undefined 230 | } 231 | 232 | function addParamsDeclaration( 233 | builder: FTS.DefinitionBuilder 234 | ): TS.ClassDeclaration { 235 | const mainParams = builder.main.getParameters() 236 | 237 | const paramsDeclaration = builder.sourceFile.addClass({ 238 | name: FTSParams 239 | }) 240 | 241 | const paramComments = {} 242 | 243 | if (builder.docs) { 244 | const paramTags = builder.docs.tags.filter((tag) => tag.title === 'param') 245 | for (const tag of paramTags) { 246 | paramComments[tag.name] = tag.description 247 | } 248 | } 249 | 250 | let httpParameterName: string 251 | 252 | for (let i = 0; i < mainParams.length; ++i) { 253 | const param = mainParams[i] 254 | const name = param.getName() 255 | const structure = param.getStructure() 256 | 257 | if (structure.isRestParameter) { 258 | builder.definition.params.schema = { additionalProperties: true } 259 | continue 260 | } 261 | 262 | // TODO: this handles alias type resolution i think... 263 | // need to test multiple levels of aliasing 264 | structure.type = param.getType().getText() 265 | 266 | if (name === 'context') { 267 | if (i !== mainParams.length - 1) { 268 | throw new Error( 269 | `Function parameter "context" must be last parameter to main function "${ 270 | builder.title 271 | }"` 272 | ) 273 | } 274 | 275 | // TODO: ensure context has valid type `FTS.Context` 276 | 277 | builder.definition.params.context = true 278 | 279 | if (mainParams.length === 1) { 280 | builder.definition.params.http = true 281 | httpParameterName = name 282 | } 283 | 284 | // ignore context in parameter aggregation 285 | continue 286 | } else if (structure.type === 'Buffer') { 287 | if (mainParams.length > 2 || i > 0) { 288 | throw new Error( 289 | `Function parameter "${name}" of type "Buffer" must not include additional parameters to main function "${ 290 | builder.title 291 | }"` 292 | ) 293 | } 294 | 295 | builder.definition.params.http = true 296 | httpParameterName = name 297 | builder.definition.params.order.push(name) 298 | 299 | // ignore Buffer in parameter aggregation 300 | continue 301 | } else { 302 | // TODO: ensure that type is valid: 303 | // not `FTS.Context` 304 | // not Promise 305 | // not Function or ArrowFunction 306 | // not RegExp 307 | } 308 | 309 | const promiseReMatch = structure.type.match(promiseTypeRe) 310 | if (promiseReMatch) { 311 | throw new Error( 312 | `Parameter "${name}" has unsupported type "${structure.type}"` 313 | ) 314 | } 315 | 316 | addPropertyToDeclaration( 317 | paramsDeclaration, 318 | structure as TS.PropertyDeclarationStructure, 319 | paramComments[name] 320 | ) 321 | builder.definition.params.order.push(name) 322 | } 323 | 324 | if ( 325 | builder.definition.params.http && 326 | builder.definition.params.order.length > 1 327 | ) { 328 | throw new Error( 329 | `Function parameter "${httpParameterName}" of type "Buffer" must not include additional parameters to main function "${ 330 | builder.title 331 | }"` 332 | ) 333 | } 334 | 335 | return paramsDeclaration 336 | } 337 | 338 | function addReturnTypeDeclaration(builder: FTS.DefinitionBuilder) { 339 | const mainReturnType = builder.main.getReturnType() 340 | let type = mainReturnType.getText() 341 | 342 | const promiseReMatch = type.match(promiseTypeRe) 343 | const isAsync = !!promiseReMatch 344 | 345 | builder.definition.returns.async = builder.main.isAsync() 346 | 347 | if (isAsync) { 348 | type = promiseReMatch[1] 349 | builder.definition.returns.async = true 350 | } 351 | 352 | if (type === 'void') { 353 | type = 'any' 354 | } 355 | 356 | if ( 357 | type.endsWith('HttpResponse') && 358 | (isAsync || mainReturnType.isInterface()) 359 | ) { 360 | builder.definition.returns.http = true 361 | } 362 | 363 | const declaration = builder.sourceFile.addInterface({ 364 | name: FTSReturns 365 | }) 366 | 367 | const jsdoc = 368 | builder.docs && 369 | builder.docs.tags.find( 370 | (tag) => tag.title === 'returns' || tag.title === 'return' 371 | ) 372 | addPropertyToDeclaration( 373 | declaration, 374 | { name: 'result', type }, 375 | jsdoc && jsdoc.description 376 | ) 377 | } 378 | 379 | function addPropertyToDeclaration( 380 | declaration: TS.ClassDeclaration | TS.InterfaceDeclaration, 381 | structure: TS.PropertyDeclarationStructure, 382 | jsdoc?: string 383 | ): TS.PropertyDeclaration | TS.PropertySignature { 384 | const isDate = structure.type === 'Date' 385 | const isBuffer = structure.type === 'Buffer' 386 | 387 | // Type coercion for non-JSON primitives like Date and Buffer 388 | if (isDate || isBuffer) { 389 | const coercionType = structure.type 390 | 391 | if (isDate) { 392 | structure.type = 'Date' 393 | } else { 394 | structure.type = 'string' 395 | } 396 | 397 | jsdoc = `${jsdoc ? jsdoc + '\n' : ''}@coerceTo ${coercionType}` 398 | } 399 | 400 | const property = declaration.addProperty(structure) 401 | 402 | if (jsdoc) { 403 | property.addJsDoc(jsdoc) 404 | } 405 | 406 | return property 407 | } 408 | 409 | function extractJSONSchemas( 410 | builder: FTS.DefinitionBuilder, 411 | file: string, 412 | jsonSchemaOptions: TJS.PartialArgs = {}, 413 | jsonCompilerOptions: any = {} 414 | ) { 415 | const compilerOptions = { 416 | allowJs: true, 417 | lib: ['es2018', 'dom'], 418 | target: 'es5', 419 | esModuleInterop: true, 420 | ...jsonCompilerOptions 421 | } 422 | 423 | const program = TJS.getProgramFromFiles( 424 | [file], 425 | compilerOptions, 426 | process.cwd() 427 | ) 428 | 429 | builder.definition.params.schema = { 430 | ...TJS.generateSchema(program, FTSParams, jsonSchemaOptions), 431 | ...(builder.definition.params.schema || {}) // Spread any existing schema params 432 | } 433 | 434 | if (!builder.definition.params.schema) { 435 | throw new Error(`Error generating params JSON schema for TS file "${file}"`) 436 | } 437 | 438 | // fix required parameters to only be those which do not have default values 439 | const { schema } = builder.definition.params 440 | schema.required = (schema.required || []).filter( 441 | (k) => schema.properties[k].default === undefined 442 | ) 443 | if (!schema.required.length) { 444 | delete schema.required 445 | } 446 | 447 | builder.definition.returns.schema = TJS.generateSchema(program, FTSReturns, { 448 | ...jsonSchemaOptions, 449 | required: false 450 | }) 451 | 452 | if (!builder.definition.returns.schema) { 453 | throw new Error( 454 | `Error generating returns JSON schema for TS file "${file}"` 455 | ) 456 | } 457 | } 458 | 459 | /* 460 | // useful for quick testing purposes 461 | if (!module.parent) { 462 | generateDefinition('./fixtures/http-request.ts') 463 | .then((definition) => { 464 | console.log(JSON.stringify(definition, null, 2)) 465 | }) 466 | .catch((err) => { 467 | console.error(err) 468 | process.exit(1) 469 | }) 470 | } 471 | */ 472 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | FTS Logo 3 | 4 | 5 | # Functional TypeScript 6 | 7 | > TypeScript standard for rock solid serverless functions. (sponsored by [Saasify](https://saasify.sh)) 8 | 9 | [![NPM](https://img.shields.io/npm/v/fts.svg)](https://www.npmjs.com/package/fts) [![Build Status](https://travis-ci.com/transitive-bullshit/functional-typescript.svg?branch=master)](https://travis-ci.com/transitive-bullshit/functional-typescript) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 10 | 11 | ## Features 12 | 13 | - **Robust**: Type-safe serverless functions! 14 | - **Simple**: Quick to setup and integrate 15 | - **Standard**: Just TypeScript + JSON Schema 16 | - **Compatible**: Supports all major serverless providers (AWS, GCP, Azure, Now, etc) 17 | - **Explicit**: Easily generate serverless function docs 18 | - **Fast**: Uses [ajv](https://github.com/epoberezkin/ajv) for schema validation 19 | - **Lightweight**: Focused http handler optimized for serverless environments 20 | - **Robust**: Used in production at [Saasify](https://saasify.sh) 21 | 22 | ## Contents 23 | 24 | - [What is Functional TypeScript (FTS)?](#what-is-functional-typescript-fts) 25 | - [Usage](#usage) 26 | - [CLI](#cli) 27 | - [Module](#module) 28 | - [Definition Format](#definition-format) 29 | - [Status](#status) 30 | - [FAQ](#faq) 31 | - [Why Serverless?](#why-serverless) 32 | - [Why FTS?](#why-fts) 33 | - [How is FTS different from other RPC standards?](#how-is-fts-different-from-other-rpc-standards) 34 | - [How is FTS related to FaaSLang?](#how-is-fts-related-to-faaslang) 35 | - [How are primitive types like Date and Buffer handled?](#how-are-primitive-types-like-date-and-buffer-handled) 36 | - [How do I use FTS with my Serverless Provider (AWS, GCP, Azure, Now, OpenWhisk, etc)?](#how-do-i-use-fts-with-my-serverless-provider-aws-gcp-azure-now-openwhisk-etc) 37 | - [Related](#related) 38 | 39 | ## What is Functional TypeScript (FTS)? 40 | 41 | FTS transforms standard TypeScript functions like this: 42 | 43 | ```ts 44 | /** 45 | * This is a basic TypeScript function. 46 | */ 47 | export function hello(name: string = 'World'): string { 48 | return `Hello ${name}!` 49 | } 50 | ``` 51 | 52 | Into type-safe serverless functions that can be called over HTTP like this (GET): 53 | 54 | ``` 55 | https://example.com/hello?name=GitHub 56 | ``` 57 | 58 | Or like this (POST): 59 | 60 | ``` 61 | { 62 | "name": "GitHub" 63 | } 64 | ``` 65 | 66 | And returns a result like this: 67 | 68 | ``` 69 | "Hello GitHub!" 70 | ``` 71 | 72 | All parameters and return values are type-checked by a standard Node.js HTTP handler, so you can invoke your TypeScript functions remotely with the same confidence as calling them directly. 73 | 74 | ## Usage 75 | 76 | You can use this package as either a CLI or as a module. 77 | 78 | ### CLI 79 | 80 | ```bash 81 | npm install -g fts 82 | ``` 83 | 84 | This will install the `fts` CLI program globally. 85 | 86 | ``` 87 | Generates an FTS Definition schema given a TS input file. 88 | 89 | Usage: fts [options] 90 | 91 | Options: 92 | -p, --project Path to 'tsconfig.json'. 93 | -h, --help output usage information 94 | ``` 95 | 96 | ### Module 97 | 98 | ```bash 99 | npm install --save fts 100 | 101 | # (optional) add support for http transport 102 | npm install --save fts-http 103 | ``` 104 | 105 | Here is an end-to-end example using HTTP ([examples/hello-world](./examples/hello-world)). 106 | 107 | ```js 108 | const fts = require('fts') 109 | const ftsHttp = require('fts-http') 110 | 111 | async function example() { 112 | const tsFilePath = './hello-world.ts' 113 | const jsFilePath = './hello-world.js' 114 | 115 | // Parse a TS file's main function export into a Definition schema. 116 | const definition = await fts.generateDefinition(tsFilePath) 117 | console.log(JSON.stringify(definition, null, 2)) 118 | 119 | // Create a standard http handler function `(req, res) => { ... }` that will 120 | // invoke the compiled JS function, performing type checking and conversions 121 | // between http and json for the function's parameters and return value. 122 | const handler = ftsHttp.createHttpHandler(definition, jsFilePath) 123 | 124 | // Create a `micro` http server that uses our HttpHandler to respond to 125 | // incoming http requests. 126 | await ftsHttp.createHttpServer(handler, 'http://localhost:3000') 127 | 128 | // You could alternatively use your `handler` with any Node.js server 129 | // framework, such as express, koa, @now/node, etc. 130 | } 131 | 132 | example().catch((err) => { 133 | console.error(err) 134 | process.exit(1) 135 | }) 136 | ``` 137 | 138 | Once you have a server running, you can invoke your type-safe function over HTTP: 139 | 140 | ```bash 141 | $ curl -s 'http://localhost:3000?name=GET' 142 | Hello GET! 143 | 144 | $ curl -s 'http://localhost:3000' -d 'name=POST' 145 | Hello POST! 146 | ``` 147 | 148 | Note that in this example, we're generating the FTS Definition and serving it together, but in practice we recommend that you generate these definitions during your build step, alongside your normal TS => JS compilation. The definitions should be viewed as json build artifacts that are _referenced_ at runtime in your server or serverless function. 149 | 150 | ## Definition Format 151 | 152 | Given our "hello world" example from earlier, FTS generates the following JSON definition that fully specifies the `hello` function export. 153 | 154 | ```json 155 | { 156 | "title": "hello", 157 | "version": "0.0.1", 158 | "config": { 159 | "language": "typescript", 160 | "defaultExport": false, 161 | "namedExport": "hello" 162 | }, 163 | "params": { 164 | "context": false, 165 | "order": ["name"], 166 | "schema": { 167 | "type": "object", 168 | "properties": { 169 | "name": { 170 | "type": "string", 171 | "default": "World" 172 | } 173 | }, 174 | "additionalProperties": false, 175 | "required": ["name"], 176 | "$schema": "http://json-schema.org/draft-07/schema#" 177 | } 178 | }, 179 | "returns": { 180 | "async": false, 181 | "schema": { 182 | "type": "object", 183 | "properties": { 184 | "result": { 185 | "type": "string" 186 | } 187 | }, 188 | "$schema": "http://json-schema.org/draft-07/schema#" 189 | } 190 | } 191 | } 192 | ``` 193 | 194 | In addition to some metadata, this definition contains a JSON Schema for the function's parameters and a JSON Schema for the function's return type. 195 | 196 | Note that this definition allows for easy **type checking**, **documentation generation**, and **automated testing** via tools like [json-schema-faker](https://github.com/json-schema-faker/json-schema-faker). 197 | 198 | ## Status 199 | 200 | FTS is stable and is actively used in production by [Saasify](https://saasify.sh). 201 | 202 | ## FAQ 203 | 204 | ### Why Serverless? 205 | 206 | Serverless functions allow your code to run on-demand and scale automatically both infinitely upwards and down to zero. They are great at minimizing cost in terms of infrastructure and engineering time, largely due to removing operational overhead and reducing the surface area for potential errors. 207 | 208 | For more information, see [Why Serverless?](https://serverless.com/learn/overview), and an excellent breakdown on the [Tradeoffs that come with Serverless](https://martinfowler.com/articles/serverless.html). 209 | 210 | ### Why FTS? 211 | 212 | The serverless space has seen such rapid growth that tooling, especially across different cloud providers, has struggled to keep up. One of the major disadvantages of using serverless functions at the moment is that each cloud provider has their own conventions and gotchas, which can quickly lead to vendor lock-in. 213 | 214 | For example, take the following Node.js serverless function defined across several cloud providers: 215 | 216 | **AWS** 217 | 218 | ```js 219 | exports.handler = (event, context, callback) => { 220 | const name = event.name || 'World' 221 | callback(null, `Hello ${name}!`) 222 | } 223 | ``` 224 | 225 | **Azure** 226 | 227 | ```js 228 | module.exports = function(context, req) { 229 | const name = req.query.name || (req.body && req.body.name) || 'World' 230 | context.res = { body: `Hello ${name}!` } 231 | context.done() 232 | } 233 | ``` 234 | 235 | **GCP** 236 | 237 | ```js 238 | const escapeHtml = require('escape-html') 239 | 240 | exports.hello = (req, res) => { 241 | const name = req.query.name || req.body.name || 'World' 242 | res.send(`Hello ${escapeHtml(name)}!`) 243 | } 244 | ``` 245 | 246 | **FTS** 247 | 248 | ```ts 249 | export function hello(name: string = 'World'): string { 250 | return `Hello ${name}!` 251 | } 252 | ``` 253 | 254 | FTS allows you to define **provider-agnostic** serverless functions while also giving you **strong type checking** and **built-in documentation** for free. 255 | 256 | ### How is FTS different from other RPC standards? 257 | 258 | Functional TypeScript is a standard for declaring and invoking remote functions. This type of invocation is known as an [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) or remote procedure call. 259 | 260 | Some other notable RPC standards include [SOAP](https://en.wikipedia.org/wiki/SOAP), [Apache Thrift](https://en.wikipedia.org/wiki/Apache_Thrift), and [gRPC](https://en.wikipedia.org/wiki/GRPC). 261 | 262 | > So how does FTS fit into this picture? 263 | 264 | First off, FTS is fully compatible with these other RPC standards, with a gRPC transport layer on the roadmap. 265 | 266 | The default HTTP handler with JSON Schema validation is the simplest way of using FTS, but it's pretty straightforward to interop with other RPC standards. For example, to use FTS with gRPC, we need to convert the JSON Schemas into protocol buffers (both of which describe the types and format of data) and add a gRPC handler which calls our compiled target JS function. Of course, there are pros and cons to using HTTP vs gRPC, with HTTP being easier to use and debug and gRPC being more efficient and scalable. 267 | 268 | The real benefit of FTS is that the remote function definitions are just standard TypeScript, without you having to worry about the complexities of gRPC, protocol buffers, or other RPC formats. **You only need to understand and write TypeScript.** 269 | 270 | Couple that with the simplicity and scalability of serverless functions, and FTS starts to become really powerful, enabling any TypeScript developer to create rock solid serverless functions easier than ever before. 271 | 272 | ### How is FTS related to FaaSLang? 273 | 274 | Functional TypeScript builds off of and shares many of the same design goals as [FaaSLang](https://github.com/faaslang/faaslang). The main difference is that FaaSLang's default implementation uses **JavaScript + JSDoc** to generate **custom schemas** for function definitions, whereas **FTS uses TypeScript** to generate **JSON Schemas** for function definitions. 275 | 276 | In our opinion, the relatively mature [JSON Schema](https://json-schema.org) specification provides a more solid and extensible base for the core schema validation layer. JSON Schema also provides interop with a large ecosystem of existing tools and languages. For example, it would be relatively simple to **extend FTS beyond TypeScript** to generate JSON Schemas from any language that is supported by [Quicktype](https://quicktype.io) (Go, Objective-C, C++, etc). 277 | 278 | FTS also exposes a standard Node.js [http handler](https://nodejs.org/api/http.html#http_event_request) for invoking FTS functions `(req, res) => { ... }`. This makes it **extremely easy to integrate with popular Node.js server frameworks** such as [express](https://expressjs.com), [koa](https://koajs.com), and [micro](https://github.com/zeit/micro). While FaaSLang could potentially be extended to support more general usage, the default implementation currently only supports a custom API gateway server... which makes me a sad panda. 🐼 279 | 280 | ### How are primitive types like Date and Buffer handled? 281 | 282 | These are both very common and useful types that are built into TypeScript and JavaScript, but they're not supported by [JSON](https://www.json.org) or [JSON Schema](htts://json-schema.org). 283 | 284 | To resolve this, FTS uses two custom JSON Schema keywords (`convertTo` and `convertFrom`) to handle encoding and decoding these types as strings. Dates are encoded as ISO utf8 strings and Buffers are encoded as base64 strings. 285 | 286 | All of these conversions are handled transparently and efficiently by the FTS wrappers _so you can just focus on writing TypeScript_. 287 | 288 | ### How do I use FTS with my Serverless Provider (AWS, GCP, Azure, Now, OpenWhisk, etc)? 289 | 290 | Great question -- this answer will be updated once we have a good answer... 😁 291 | 292 | ## Related 293 | 294 | - [Saasify](https://saasify.sh) - Uses FTS for serverless TS support. 295 | - [FaaSLang](https://github.com/faaslang/faaslang) - Similar in spirit to this project but uses JS instead of TypeScript. 296 | - [FastAPI](https://github.com/tiangolo/fastapi) - Similar functionality but for Python. 297 | - [JSON RPC](https://en.wikipedia.org/wiki/JSON-RPC) - Similar JSON RPC standard. 298 | - [AWS TypeScript Template](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-nodejs-typescript) - Default TypeScript template for AWS serverless functions. 299 | - [Serverless Plugin TypeScript](https://github.com/prisma/serverless-plugin-typescript) - Serverless plugin for zero-config Typescript support. 300 | 301 | --- 302 | 303 | - [typescript-json-schema](https://github.com/YousefED/typescript-json-schema) - Used under the hood to convert TypeScript types to [JSON Schema](https://json-schema.org). 304 | - [Quicktype](https://quicktype.io) - Very useful utility which uses JSON Schema as a common standard for converting between different type systems. 305 | 306 | ## License 307 | 308 | MIT © [Saasify](https://saasify.sh) 309 | 310 | Support my OSS work by following me on twitter twitter 311 | -------------------------------------------------------------------------------- /packages/fts-http-client/src/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello-world": { 3 | "config": { 4 | "defaultExport": true, 5 | "language": "typescript" 6 | }, 7 | "params": { 8 | "context": false, 9 | "http": false, 10 | "order": [ 11 | "name" 12 | ], 13 | "schema": { 14 | "type": "object", 15 | "properties": { 16 | "name": { 17 | "type": "string", 18 | "default": "World" 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": [ 23 | "name" 24 | ], 25 | "$schema": "http://json-schema.org/draft-07/schema#" 26 | } 27 | }, 28 | "returns": { 29 | "async": false, 30 | "http": false, 31 | "schema": { 32 | "type": "object", 33 | "properties": { 34 | "result": { 35 | "type": "string" 36 | } 37 | }, 38 | "additionalProperties": false, 39 | "$schema": "http://json-schema.org/draft-07/schema#" 40 | } 41 | }, 42 | "version": "1.0.9", 43 | "title": "hello-world" 44 | }, 45 | "date": { 46 | "config": { 47 | "defaultExport": false, 48 | "language": "typescript", 49 | "namedExport": "playdate" 50 | }, 51 | "params": { 52 | "context": false, 53 | "http": false, 54 | "order": [ 55 | "date1", 56 | "date2" 57 | ], 58 | "schema": { 59 | "type": "object", 60 | "properties": { 61 | "date1": { 62 | "description": "Enables basic storage and retrieval of dates and times.", 63 | "coerceTo": "Date", 64 | "type": "string", 65 | "format": "date-time" 66 | }, 67 | "date2": { 68 | "description": "Enables basic storage and retrieval of dates and times.", 69 | "coerceTo": "Date", 70 | "type": "string", 71 | "format": "date-time" 72 | } 73 | }, 74 | "additionalProperties": false, 75 | "required": [ 76 | "date1", 77 | "date2" 78 | ], 79 | "$schema": "http://json-schema.org/draft-07/schema#" 80 | } 81 | }, 82 | "returns": { 83 | "async": false, 84 | "http": false, 85 | "schema": { 86 | "type": "object", 87 | "properties": { 88 | "result": { 89 | "description": "Enables basic storage and retrieval of dates and times.", 90 | "coerceTo": "Date", 91 | "type": "string", 92 | "format": "date-time" 93 | } 94 | }, 95 | "additionalProperties": false, 96 | "$schema": "http://json-schema.org/draft-07/schema#" 97 | } 98 | }, 99 | "version": "1.0.9", 100 | "title": "playdate" 101 | }, 102 | "double": { 103 | "config": { 104 | "defaultExport": false, 105 | "language": "typescript", 106 | "namedExport": "double" 107 | }, 108 | "params": { 109 | "context": false, 110 | "http": false, 111 | "order": [ 112 | "value" 113 | ], 114 | "schema": { 115 | "type": "object", 116 | "properties": { 117 | "value": { 118 | "description": "Comment describing the `value` parameter.", 119 | "type": "number" 120 | } 121 | }, 122 | "additionalProperties": false, 123 | "required": [ 124 | "value" 125 | ], 126 | "$schema": "http://json-schema.org/draft-07/schema#" 127 | } 128 | }, 129 | "returns": { 130 | "async": false, 131 | "http": false, 132 | "schema": { 133 | "type": "object", 134 | "properties": { 135 | "result": { 136 | "description": "Comment describing the return type.", 137 | "type": "number" 138 | } 139 | }, 140 | "additionalProperties": false, 141 | "$schema": "http://json-schema.org/draft-07/schema#" 142 | } 143 | }, 144 | "version": "1.0.9", 145 | "title": "double", 146 | "description": "Multiplies a value by 2. (Also a full example of Typedoc's functionality.)\n\n### Example (es module)\n```js\nimport { double } from 'typescript-starter'\nconsole.log(double(4))\n// => 8\n```\n\n### Example (commonjs)\n```js\nvar double = require('typescript-starter').double;\nconsole.log(double(4))\n// => 8\n```" 147 | }, 148 | "es6": { 149 | "config": { 150 | "defaultExport": true, 151 | "language": "javascript" 152 | }, 153 | "params": { 154 | "context": false, 155 | "http": false, 156 | "order": [ 157 | "foo", 158 | "bar" 159 | ], 160 | "schema": { 161 | "type": "object", 162 | "properties": { 163 | "foo": { 164 | "description": "Description of foo", 165 | "type": "string" 166 | }, 167 | "bar": { 168 | "type": "number" 169 | } 170 | }, 171 | "additionalProperties": false, 172 | "required": [ 173 | "bar", 174 | "foo" 175 | ], 176 | "$schema": "http://json-schema.org/draft-07/schema#" 177 | } 178 | }, 179 | "returns": { 180 | "async": true, 181 | "http": false, 182 | "schema": { 183 | "type": "object", 184 | "properties": { 185 | "result": { 186 | "type": "string" 187 | } 188 | }, 189 | "additionalProperties": false, 190 | "$schema": "http://json-schema.org/draft-07/schema#" 191 | } 192 | }, 193 | "version": "1.0.9", 194 | "title": "example", 195 | "description": "Example ES6 JavaScript function commented with jsdoc." 196 | }, 197 | "http-response": { 198 | "config": { 199 | "defaultExport": true, 200 | "language": "typescript" 201 | }, 202 | "params": { 203 | "context": false, 204 | "http": false, 205 | "order": [], 206 | "schema": { 207 | "type": "object", 208 | "additionalProperties": false, 209 | "$schema": "http://json-schema.org/draft-07/schema#" 210 | } 211 | }, 212 | "returns": { 213 | "async": false, 214 | "http": true, 215 | "schema": { 216 | "type": "object", 217 | "properties": { 218 | "result": { 219 | "$ref": "#/definitions/HttpResponse" 220 | } 221 | }, 222 | "additionalProperties": false, 223 | "definitions": { 224 | "HttpResponse": { 225 | "description": "Fallback to allow raw HTTP responses that are not type-checked.", 226 | "type": "object", 227 | "properties": { 228 | "statusCode": { 229 | "type": "number" 230 | }, 231 | "headers": { 232 | "$ref": "#/definitions/OutgoingHttpHeaders" 233 | }, 234 | "body": { 235 | "description": "Raw data is stored in instances of the Buffer class.\nA Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.\nValid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex'", 236 | "type": "object", 237 | "additionalProperties": false, 238 | "patternProperties": { 239 | "^[0-9]+$": { 240 | "type": "number" 241 | } 242 | } 243 | } 244 | }, 245 | "additionalProperties": false 246 | }, 247 | "OutgoingHttpHeaders": { 248 | "type": "object", 249 | "additionalProperties": { 250 | "type": "string" 251 | } 252 | } 253 | }, 254 | "$schema": "http://json-schema.org/draft-07/schema#" 255 | } 256 | }, 257 | "version": "1.0.9", 258 | "title": "http-response" 259 | }, 260 | "medium": { 261 | "config": { 262 | "defaultExport": true, 263 | "language": "typescript" 264 | }, 265 | "params": { 266 | "context": false, 267 | "http": false, 268 | "order": [ 269 | "foo", 270 | "bar", 271 | "nala" 272 | ], 273 | "schema": { 274 | "type": "object", 275 | "properties": { 276 | "foo": { 277 | "description": "Example describing string `foo`.", 278 | "type": "string" 279 | }, 280 | "bar": { 281 | "type": "number", 282 | "default": 5 283 | }, 284 | "nala": { 285 | "$ref": "#/definitions/Nala" 286 | } 287 | }, 288 | "additionalProperties": false, 289 | "required": [ 290 | "bar", 291 | "foo" 292 | ], 293 | "definitions": { 294 | "Nala": { 295 | "type": "object", 296 | "properties": { 297 | "numbers": { 298 | "type": "array", 299 | "items": { 300 | "type": "number" 301 | } 302 | }, 303 | "color": { 304 | "$ref": "#/definitions/Color" 305 | } 306 | }, 307 | "additionalProperties": false, 308 | "required": [ 309 | "color" 310 | ] 311 | }, 312 | "Color": { 313 | "enum": [ 314 | 0, 315 | 1, 316 | 2 317 | ], 318 | "type": "number" 319 | } 320 | }, 321 | "$schema": "http://json-schema.org/draft-07/schema#" 322 | } 323 | }, 324 | "returns": { 325 | "async": true, 326 | "http": false, 327 | "schema": { 328 | "type": "object", 329 | "properties": { 330 | "result": { 331 | "description": "Description of return value.", 332 | "type": "string" 333 | } 334 | }, 335 | "additionalProperties": false, 336 | "$schema": "http://json-schema.org/draft-07/schema#" 337 | } 338 | }, 339 | "version": "1.0.9", 340 | "title": "Example", 341 | "description": "This is an example description for an example function." 342 | }, 343 | "power": { 344 | "config": { 345 | "defaultExport": false, 346 | "language": "typescript", 347 | "namedExport": "power" 348 | }, 349 | "params": { 350 | "context": false, 351 | "http": false, 352 | "order": [ 353 | "base", 354 | "exponent" 355 | ], 356 | "schema": { 357 | "type": "object", 358 | "properties": { 359 | "base": { 360 | "type": "number" 361 | }, 362 | "exponent": { 363 | "type": "number" 364 | } 365 | }, 366 | "additionalProperties": false, 367 | "required": [ 368 | "base", 369 | "exponent" 370 | ], 371 | "$schema": "http://json-schema.org/draft-07/schema#" 372 | } 373 | }, 374 | "returns": { 375 | "async": false, 376 | "http": false, 377 | "schema": { 378 | "type": "object", 379 | "properties": { 380 | "result": { 381 | "type": "number" 382 | } 383 | }, 384 | "additionalProperties": false, 385 | "$schema": "http://json-schema.org/draft-07/schema#" 386 | } 387 | }, 388 | "version": "1.0.9", 389 | "title": "power", 390 | "description": "Raise the value of the first parameter to the power of the second using the es7 `**` operator.\n\n### Example (es module)\n```js\nimport { power } from 'typescript-starter'\nconsole.log(power(2,3))\n// => 8\n```\n\n### Example (commonjs)\n```js\nvar power = require('typescript-starter').power;\nconsole.log(power(2,3))\n// => 8\n```" 391 | }, 392 | "void": { 393 | "config": { 394 | "defaultExport": false, 395 | "language": "typescript", 396 | "namedExport": "noop" 397 | }, 398 | "params": { 399 | "context": false, 400 | "http": false, 401 | "order": [], 402 | "schema": { 403 | "type": "object", 404 | "additionalProperties": false, 405 | "$schema": "http://json-schema.org/draft-07/schema#" 406 | } 407 | }, 408 | "returns": { 409 | "async": false, 410 | "http": false, 411 | "schema": { 412 | "type": "object", 413 | "properties": { 414 | "result": {} 415 | }, 416 | "additionalProperties": false, 417 | "$schema": "http://json-schema.org/draft-07/schema#" 418 | } 419 | }, 420 | "version": "1.0.9", 421 | "title": "noop" 422 | }, 423 | "address-book": { 424 | "config": { 425 | "defaultExport": false, 426 | "language": "typescript", 427 | "namedExport": "Nala" 428 | }, 429 | "params": { 430 | "context": false, 431 | "http": false, 432 | "order": [ 433 | "address" 434 | ], 435 | "schema": { 436 | "type": "object", 437 | "properties": { 438 | "address": { 439 | "$ref": "#/definitions/AddressBook" 440 | } 441 | }, 442 | "additionalProperties": false, 443 | "required": [ 444 | "address" 445 | ], 446 | "definitions": { 447 | "AddressBook": { 448 | "type": "object", 449 | "properties": { 450 | "contacts": { 451 | "description": "A dictionary of Contacts, indexed by unique ID", 452 | "type": "object", 453 | "additionalProperties": { 454 | "$ref": "#/definitions/Contact" 455 | } 456 | } 457 | }, 458 | "additionalProperties": false, 459 | "required": [ 460 | "contacts" 461 | ] 462 | }, 463 | "Contact": { 464 | "type": "object", 465 | "properties": { 466 | "firstName": { 467 | "type": "string" 468 | }, 469 | "lastName": { 470 | "type": "string" 471 | }, 472 | "birthday": { 473 | "description": "Enables basic storage and retrieval of dates and times.", 474 | "type": "string", 475 | "format": "date-time" 476 | }, 477 | "title": { 478 | "enum": [ 479 | "Mr.", 480 | "Mrs.", 481 | "Ms.", 482 | "Prof." 483 | ], 484 | "type": "string" 485 | }, 486 | "emails": { 487 | "type": "array", 488 | "items": { 489 | "type": "string" 490 | } 491 | }, 492 | "phones": { 493 | "type": "array", 494 | "items": { 495 | "$ref": "#/definitions/PhoneNumber" 496 | } 497 | }, 498 | "highScore": { 499 | "type": "integer" 500 | } 501 | }, 502 | "additionalProperties": false, 503 | "required": [ 504 | "emails", 505 | "firstName", 506 | "highScore", 507 | "phones" 508 | ] 509 | }, 510 | "PhoneNumber": { 511 | "description": "A Contact's phone number.", 512 | "type": "object", 513 | "properties": { 514 | "number": { 515 | "type": "string" 516 | }, 517 | "label": { 518 | "description": "An optional label (e.g. \"mobile\")", 519 | "type": "string" 520 | } 521 | }, 522 | "additionalProperties": false, 523 | "required": [ 524 | "number" 525 | ] 526 | } 527 | }, 528 | "$schema": "http://json-schema.org/draft-07/schema#" 529 | } 530 | }, 531 | "returns": { 532 | "async": false, 533 | "http": false, 534 | "schema": { 535 | "type": "object", 536 | "properties": { 537 | "result": { 538 | "type": "string" 539 | } 540 | }, 541 | "additionalProperties": false, 542 | "$schema": "http://json-schema.org/draft-07/schema#" 543 | } 544 | }, 545 | "version": "1.0.9", 546 | "title": "Nala", 547 | "description": "#TopLevel" 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /.snapshots/packages/fts/src/parser.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `packages/fts/src/parser.test.ts` 2 | 3 | The actual snapshot is saved in `parser.test.ts.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## address-book 8 | 9 | > Snapshot 1 10 | 11 | { 12 | config: { 13 | defaultExport: false, 14 | language: 'typescript', 15 | namedExport: 'Nala', 16 | }, 17 | description: '#TopLevel', 18 | params: { 19 | context: false, 20 | http: false, 21 | order: [ 22 | 'address', 23 | ], 24 | schema: { 25 | $schema: 'http://json-schema.org/draft-07/schema#', 26 | additionalProperties: false, 27 | definitions: { 28 | AddressBook: { 29 | additionalProperties: false, 30 | properties: { 31 | contacts: { 32 | additionalProperties: { 33 | $ref: '#/definitions/Contact', 34 | }, 35 | description: 'A dictionary of Contacts, indexed by unique ID', 36 | type: 'object', 37 | }, 38 | }, 39 | required: [ 40 | 'contacts', 41 | ], 42 | type: 'object', 43 | }, 44 | Contact: { 45 | additionalProperties: false, 46 | properties: { 47 | birthday: { 48 | description: 'Enables basic storage and retrieval of dates and times.', 49 | format: 'date-time', 50 | type: 'string', 51 | }, 52 | emails: { 53 | items: { 54 | type: 'string', 55 | }, 56 | type: 'array', 57 | }, 58 | firstName: { 59 | type: 'string', 60 | }, 61 | highScore: { 62 | type: 'integer', 63 | }, 64 | lastName: { 65 | type: 'string', 66 | }, 67 | phones: { 68 | items: { 69 | $ref: '#/definitions/PhoneNumber', 70 | }, 71 | type: 'array', 72 | }, 73 | title: { 74 | enum: [ 75 | 'Mr.', 76 | 'Mrs.', 77 | 'Ms.', 78 | 'Prof.', 79 | ], 80 | type: 'string', 81 | }, 82 | }, 83 | required: [ 84 | 'emails', 85 | 'firstName', 86 | 'highScore', 87 | 'phones', 88 | ], 89 | type: 'object', 90 | }, 91 | PhoneNumber: { 92 | additionalProperties: false, 93 | description: 'A Contact\'s phone number.', 94 | properties: { 95 | label: { 96 | description: 'An optional label (e.g. "mobile")', 97 | type: 'string', 98 | }, 99 | number: { 100 | type: 'string', 101 | }, 102 | }, 103 | required: [ 104 | 'number', 105 | ], 106 | type: 'object', 107 | }, 108 | }, 109 | properties: { 110 | address: { 111 | $ref: '#/definitions/AddressBook', 112 | }, 113 | }, 114 | required: [ 115 | 'address', 116 | ], 117 | type: 'object', 118 | }, 119 | }, 120 | returns: { 121 | async: false, 122 | http: false, 123 | schema: { 124 | $schema: 'http://json-schema.org/draft-07/schema#', 125 | additionalProperties: false, 126 | properties: { 127 | result: { 128 | type: 'string', 129 | }, 130 | }, 131 | type: 'object', 132 | }, 133 | }, 134 | title: 'Nala', 135 | } 136 | 137 | ## date 138 | 139 | > Snapshot 1 140 | 141 | { 142 | config: { 143 | defaultExport: false, 144 | language: 'typescript', 145 | namedExport: 'playdate', 146 | }, 147 | params: { 148 | context: false, 149 | http: false, 150 | order: [ 151 | 'date1', 152 | 'date2', 153 | ], 154 | schema: { 155 | $schema: 'http://json-schema.org/draft-07/schema#', 156 | additionalProperties: false, 157 | properties: { 158 | date1: { 159 | coerceTo: 'Date', 160 | description: 'Enables basic storage and retrieval of dates and times.', 161 | format: 'date-time', 162 | type: 'string', 163 | }, 164 | date2: { 165 | coerceTo: 'Date', 166 | description: 'Enables basic storage and retrieval of dates and times.', 167 | format: 'date-time', 168 | type: 'string', 169 | }, 170 | }, 171 | required: [ 172 | 'date1', 173 | 'date2', 174 | ], 175 | type: 'object', 176 | }, 177 | }, 178 | returns: { 179 | async: false, 180 | http: false, 181 | schema: { 182 | $schema: 'http://json-schema.org/draft-07/schema#', 183 | additionalProperties: false, 184 | properties: { 185 | result: { 186 | coerceTo: 'Date', 187 | description: 'Enables basic storage and retrieval of dates and times.', 188 | format: 'date-time', 189 | type: 'string', 190 | }, 191 | }, 192 | type: 'object', 193 | }, 194 | }, 195 | title: 'playdate', 196 | } 197 | 198 | ## double 199 | 200 | > Snapshot 1 201 | 202 | { 203 | config: { 204 | defaultExport: false, 205 | language: 'typescript', 206 | namedExport: 'double', 207 | }, 208 | description: `Multiplies a value by 2. (Also a full example of Typedoc's functionality.)␊ 209 | ␊ 210 | ### Example (es module)␊ 211 | ```js␊ 212 | import { double } from 'typescript-starter'␊ 213 | console.log(double(4))␊ 214 | // => 8␊ 215 | ```␊ 216 | ␊ 217 | ### Example (commonjs)␊ 218 | ```js␊ 219 | var double = require('typescript-starter').double;␊ 220 | console.log(double(4))␊ 221 | // => 8␊ 222 | ````, 223 | params: { 224 | context: false, 225 | http: false, 226 | order: [ 227 | 'value', 228 | ], 229 | schema: { 230 | $schema: 'http://json-schema.org/draft-07/schema#', 231 | additionalProperties: false, 232 | properties: { 233 | value: { 234 | description: 'Comment describing the `value` parameter.', 235 | type: 'number', 236 | }, 237 | }, 238 | required: [ 239 | 'value', 240 | ], 241 | type: 'object', 242 | }, 243 | }, 244 | returns: { 245 | async: false, 246 | http: false, 247 | schema: { 248 | $schema: 'http://json-schema.org/draft-07/schema#', 249 | additionalProperties: false, 250 | properties: { 251 | result: { 252 | description: 'Comment describing the return type.', 253 | type: 'number', 254 | }, 255 | }, 256 | type: 'object', 257 | }, 258 | }, 259 | title: 'double', 260 | } 261 | 262 | ## es6 263 | 264 | > Snapshot 1 265 | 266 | { 267 | config: { 268 | defaultExport: true, 269 | language: 'javascript', 270 | }, 271 | description: 'Example ES6 JavaScript function commented with jsdoc.', 272 | params: { 273 | context: false, 274 | http: false, 275 | order: [ 276 | 'foo', 277 | 'bar', 278 | ], 279 | schema: { 280 | $schema: 'http://json-schema.org/draft-07/schema#', 281 | additionalProperties: false, 282 | properties: { 283 | bar: { 284 | type: 'number', 285 | }, 286 | foo: { 287 | description: 'Description of foo', 288 | type: 'string', 289 | }, 290 | }, 291 | required: [ 292 | 'bar', 293 | 'foo', 294 | ], 295 | type: 'object', 296 | }, 297 | }, 298 | returns: { 299 | async: true, 300 | http: false, 301 | schema: { 302 | $schema: 'http://json-schema.org/draft-07/schema#', 303 | additionalProperties: false, 304 | properties: { 305 | result: { 306 | type: 'string', 307 | }, 308 | }, 309 | type: 'object', 310 | }, 311 | }, 312 | title: 'example', 313 | } 314 | 315 | ## hello-world 316 | 317 | > Snapshot 1 318 | 319 | { 320 | config: { 321 | defaultExport: true, 322 | language: 'typescript', 323 | }, 324 | params: { 325 | context: false, 326 | http: false, 327 | order: [ 328 | 'name', 329 | ], 330 | schema: { 331 | $schema: 'http://json-schema.org/draft-07/schema#', 332 | additionalProperties: false, 333 | properties: { 334 | name: { 335 | default: 'World', 336 | type: 'string', 337 | }, 338 | }, 339 | type: 'object', 340 | }, 341 | }, 342 | returns: { 343 | async: false, 344 | http: false, 345 | schema: { 346 | $schema: 'http://json-schema.org/draft-07/schema#', 347 | additionalProperties: false, 348 | properties: { 349 | result: { 350 | type: 'string', 351 | }, 352 | }, 353 | type: 'object', 354 | }, 355 | }, 356 | title: 'hello-world', 357 | } 358 | 359 | ## http-request 360 | 361 | > Snapshot 1 362 | 363 | { 364 | config: { 365 | defaultExport: true, 366 | language: 'typescript', 367 | }, 368 | params: { 369 | context: true, 370 | http: true, 371 | order: [ 372 | 'body', 373 | ], 374 | schema: { 375 | $schema: 'http://json-schema.org/draft-07/schema#', 376 | additionalProperties: false, 377 | type: 'object', 378 | }, 379 | }, 380 | returns: { 381 | async: false, 382 | http: true, 383 | schema: { 384 | $schema: 'http://json-schema.org/draft-07/schema#', 385 | additionalProperties: false, 386 | definitions: { 387 | HttpResponse: { 388 | additionalProperties: false, 389 | description: 'Fallback to allow raw HTTP responses that are not type-checked.', 390 | properties: { 391 | body: { 392 | additionalProperties: false, 393 | description: `Raw data is stored in instances of the Buffer class.␊ 394 | A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.␊ 395 | Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex'`, 396 | patternProperties: { 397 | '^[0-9]+$': { 398 | type: 'number', 399 | }, 400 | }, 401 | type: 'object', 402 | }, 403 | headers: { 404 | $ref: '#/definitions/OutgoingHttpHeaders', 405 | }, 406 | statusCode: { 407 | type: 'number', 408 | }, 409 | }, 410 | type: 'object', 411 | }, 412 | OutgoingHttpHeaders: { 413 | additionalProperties: { 414 | type: 'string', 415 | }, 416 | type: 'object', 417 | }, 418 | }, 419 | properties: { 420 | result: { 421 | $ref: '#/definitions/HttpResponse', 422 | }, 423 | }, 424 | type: 'object', 425 | }, 426 | }, 427 | title: 'http-request', 428 | } 429 | 430 | ## http-response 431 | 432 | > Snapshot 1 433 | 434 | { 435 | config: { 436 | defaultExport: true, 437 | language: 'typescript', 438 | }, 439 | params: { 440 | context: false, 441 | http: false, 442 | order: [], 443 | schema: { 444 | $schema: 'http://json-schema.org/draft-07/schema#', 445 | additionalProperties: false, 446 | type: 'object', 447 | }, 448 | }, 449 | returns: { 450 | async: false, 451 | http: true, 452 | schema: { 453 | $schema: 'http://json-schema.org/draft-07/schema#', 454 | additionalProperties: false, 455 | definitions: { 456 | HttpResponse: { 457 | additionalProperties: false, 458 | description: 'Fallback to allow raw HTTP responses that are not type-checked.', 459 | properties: { 460 | body: { 461 | additionalProperties: false, 462 | description: `Raw data is stored in instances of the Buffer class.␊ 463 | A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.␊ 464 | Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex'`, 465 | patternProperties: { 466 | '^[0-9]+$': { 467 | type: 'number', 468 | }, 469 | }, 470 | type: 'object', 471 | }, 472 | headers: { 473 | $ref: '#/definitions/OutgoingHttpHeaders', 474 | }, 475 | statusCode: { 476 | type: 'number', 477 | }, 478 | }, 479 | type: 'object', 480 | }, 481 | OutgoingHttpHeaders: { 482 | additionalProperties: { 483 | type: 'string', 484 | }, 485 | type: 'object', 486 | }, 487 | }, 488 | properties: { 489 | result: { 490 | $ref: '#/definitions/HttpResponse', 491 | }, 492 | }, 493 | type: 'object', 494 | }, 495 | }, 496 | title: 'http-response', 497 | } 498 | 499 | ## http-response-p 500 | 501 | > Snapshot 1 502 | 503 | { 504 | config: { 505 | defaultExport: true, 506 | language: 'typescript', 507 | }, 508 | params: { 509 | context: false, 510 | http: false, 511 | order: [], 512 | schema: { 513 | $schema: 'http://json-schema.org/draft-07/schema#', 514 | additionalProperties: false, 515 | type: 'object', 516 | }, 517 | }, 518 | returns: { 519 | async: true, 520 | http: true, 521 | schema: { 522 | $schema: 'http://json-schema.org/draft-07/schema#', 523 | additionalProperties: false, 524 | definitions: { 525 | HttpResponse: { 526 | additionalProperties: false, 527 | description: 'Fallback to allow raw HTTP responses that are not type-checked.', 528 | properties: { 529 | body: { 530 | additionalProperties: false, 531 | description: `Raw data is stored in instances of the Buffer class.␊ 532 | A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.␊ 533 | Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex'`, 534 | patternProperties: { 535 | '^[0-9]+$': { 536 | type: 'number', 537 | }, 538 | }, 539 | type: 'object', 540 | }, 541 | headers: { 542 | $ref: '#/definitions/OutgoingHttpHeaders', 543 | }, 544 | statusCode: { 545 | type: 'number', 546 | }, 547 | }, 548 | type: 'object', 549 | }, 550 | OutgoingHttpHeaders: { 551 | additionalProperties: { 552 | type: 'string', 553 | }, 554 | type: 'object', 555 | }, 556 | }, 557 | properties: { 558 | result: { 559 | $ref: '#/definitions/HttpResponse', 560 | }, 561 | }, 562 | type: 'object', 563 | }, 564 | }, 565 | title: 'fixtureHttpResponseP', 566 | } 567 | 568 | ## medium 569 | 570 | > Snapshot 1 571 | 572 | { 573 | config: { 574 | defaultExport: true, 575 | language: 'typescript', 576 | }, 577 | description: 'This is an example description for an example function.', 578 | params: { 579 | context: false, 580 | http: false, 581 | order: [ 582 | 'foo', 583 | 'bar', 584 | 'nala', 585 | ], 586 | schema: { 587 | $schema: 'http://json-schema.org/draft-07/schema#', 588 | additionalProperties: false, 589 | definitions: { 590 | Color: { 591 | enum: [ 592 | 0, 593 | 1, 594 | 2, 595 | ], 596 | type: 'number', 597 | }, 598 | Nala: { 599 | additionalProperties: false, 600 | properties: { 601 | color: { 602 | $ref: '#/definitions/Color', 603 | }, 604 | numbers: { 605 | items: { 606 | type: 'number', 607 | }, 608 | type: 'array', 609 | }, 610 | }, 611 | required: [ 612 | 'color', 613 | ], 614 | type: 'object', 615 | }, 616 | }, 617 | properties: { 618 | bar: { 619 | default: 5, 620 | type: 'number', 621 | }, 622 | foo: { 623 | description: 'Example describing string `foo`.', 624 | type: 'string', 625 | }, 626 | nala: { 627 | $ref: '#/definitions/Nala', 628 | }, 629 | }, 630 | required: [ 631 | 'foo', 632 | ], 633 | type: 'object', 634 | }, 635 | }, 636 | returns: { 637 | async: true, 638 | http: false, 639 | schema: { 640 | $schema: 'http://json-schema.org/draft-07/schema#', 641 | additionalProperties: false, 642 | properties: { 643 | result: { 644 | description: 'Description of return value.', 645 | type: 'string', 646 | }, 647 | }, 648 | type: 'object', 649 | }, 650 | }, 651 | title: 'Example', 652 | } 653 | 654 | ## power 655 | 656 | > Snapshot 1 657 | 658 | { 659 | config: { 660 | defaultExport: false, 661 | language: 'typescript', 662 | namedExport: 'power', 663 | }, 664 | description: `Raise the value of the first parameter to the power of the second using the es7 `**` operator.␊ 665 | ␊ 666 | ### Example (es module)␊ 667 | ```js␊ 668 | import { power } from 'typescript-starter'␊ 669 | console.log(power(2,3))␊ 670 | // => 8␊ 671 | ```␊ 672 | ␊ 673 | ### Example (commonjs)␊ 674 | ```js␊ 675 | var power = require('typescript-starter').power;␊ 676 | console.log(power(2,3))␊ 677 | // => 8␊ 678 | ````, 679 | params: { 680 | context: false, 681 | http: false, 682 | order: [ 683 | 'base', 684 | 'exponent', 685 | ], 686 | schema: { 687 | $schema: 'http://json-schema.org/draft-07/schema#', 688 | additionalProperties: false, 689 | properties: { 690 | base: { 691 | type: 'number', 692 | }, 693 | exponent: { 694 | type: 'number', 695 | }, 696 | }, 697 | required: [ 698 | 'base', 699 | 'exponent', 700 | ], 701 | type: 'object', 702 | }, 703 | }, 704 | returns: { 705 | async: false, 706 | http: false, 707 | schema: { 708 | $schema: 'http://json-schema.org/draft-07/schema#', 709 | additionalProperties: false, 710 | properties: { 711 | result: { 712 | type: 'number', 713 | }, 714 | }, 715 | type: 'object', 716 | }, 717 | }, 718 | title: 'power', 719 | } 720 | 721 | ## rest-params 722 | 723 | > Snapshot 1 724 | 725 | { 726 | config: { 727 | defaultExport: true, 728 | language: 'typescript', 729 | }, 730 | params: { 731 | context: false, 732 | http: false, 733 | order: [ 734 | 'foo', 735 | ], 736 | schema: { 737 | $schema: 'http://json-schema.org/draft-07/schema#', 738 | additionalProperties: true, 739 | properties: { 740 | foo: { 741 | type: 'string', 742 | }, 743 | }, 744 | required: [ 745 | 'foo', 746 | ], 747 | type: 'object', 748 | }, 749 | }, 750 | returns: { 751 | async: false, 752 | http: false, 753 | schema: { 754 | $schema: 'http://json-schema.org/draft-07/schema#', 755 | additionalProperties: false, 756 | properties: { 757 | result: { 758 | type: 'string', 759 | }, 760 | }, 761 | type: 'object', 762 | }, 763 | }, 764 | title: 'rest-params', 765 | } 766 | 767 | ## void 768 | 769 | > Snapshot 1 770 | 771 | { 772 | config: { 773 | defaultExport: false, 774 | language: 'typescript', 775 | namedExport: 'noop', 776 | }, 777 | params: { 778 | context: false, 779 | http: false, 780 | order: [], 781 | schema: { 782 | $schema: 'http://json-schema.org/draft-07/schema#', 783 | additionalProperties: false, 784 | type: 'object', 785 | }, 786 | }, 787 | returns: { 788 | async: false, 789 | http: false, 790 | schema: { 791 | $schema: 'http://json-schema.org/draft-07/schema#', 792 | additionalProperties: false, 793 | properties: { 794 | result: {}, 795 | }, 796 | type: 'object', 797 | }, 798 | }, 799 | title: 'noop', 800 | } 801 | --------------------------------------------------------------------------------