├── .gitignore
├── LICENSE
├── README.md
├── examples
├── express.js
├── index.html
├── index.js
├── koa.js
├── mediumArticle20180926
│ ├── index.html
│ └── index.js
├── package.json
└── testmarkdown.md
├── index.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Simon Y. Blackwell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anywhichway/fos/adb6951dd0a9644835bbe413a60a6db0528260e6/README.md
--------------------------------------------------------------------------------
/examples/express.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | fosify = require("../index.js").fosify,
3 | app = express();
4 |
5 | app.locals.serverName = "Express FOS";
6 | const api = {
7 | echo:arg => arg,
8 | upper:arg => arg.toUpperCase(),
9 | f:() => () => true,
10 | locals:(key) => key ? app.locals[key] : undefined
11 | // direct URL http://localhost:3000/fos/locals?arguments=["serverName"]
12 | };
13 | fosify(app,api,{allow:"*",name:"F"});
14 | app.use(express.static(__dirname + "/"));
15 |
16 | app.listen(3000, () => console.log("Express FOS listening on 3000"))
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 | FOS Example - open debugger to see results
15 |
16 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | const md = require('markdown-it')({
2 | html: true,
3 | linkify: true,
4 | typographer: true
5 | });
6 |
7 | const FOS = require("../index.js"),
8 | server = new FOS(
9 | {
10 | echo: arg => arg,
11 | upper: arg => arg.toUpperCase(),
12 | f: () => () => true
13 | },
14 | {allow: "*", log: true},
15 | );
16 | server.use((request, response, next) => {
17 | next();
18 | }, (request, response, next) => {
19 | next("route");
20 | }, (request, response, next) => {
21 | console.log("should not be here");
22 | next();
23 | });
24 | server.use(/\/hello/g, async (request, response, next) => {
25 | console.log("RegExp", request.url);
26 | });
27 | server.param("id", async (request, response, next, value) => {
28 | console.log(request.url, value);
29 | });
30 | server.use("/hello/there/:id", (request, response, next) => {
31 | console.log(request.url, "to long");
32 | next();
33 | });
34 | server.use("/hello/:id", async (request, response, next) => {
35 | response.end("hi!");
36 | });
37 | server.static("/", {
38 | mimeTypes: {
39 | md: {
40 | contentType: "text/html",
41 | transform: (content) => md.render(content.toString())
42 | }
43 | }
44 | });
45 | server.listen(3000);
--------------------------------------------------------------------------------
/examples/koa.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa'),
2 | fosify = require("../index.js").fosify,
3 | app = new Koa();
4 |
5 | app.locals = {serverName: "Koa FOS"};
6 | const api = {
7 | echo:arg => arg,
8 | upper:arg => arg.toUpperCase(),
9 | f:() => () => true,
10 | locals:(key) => key ? app.locals[key] : undefined
11 | // direct URL http://localhost:3000/fos/locals?arguments=["serverName"]
12 | };
13 |
14 | app.use(require('koa-static')(__dirname + "/"));
15 | fosify(app,api,{allow:"*",name:"F"});
16 | app.listen(3000,() => console.log("Koa FOS server listening on 3000"));
--------------------------------------------------------------------------------
/examples/mediumArticle20180926/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | Hello!
8 |
--------------------------------------------------------------------------------
/examples/mediumArticle20180926/index.js:
--------------------------------------------------------------------------------
1 | const FOS = require("../../index.js"),
2 | cookieParser = require('cookie-parser'),
3 | app = new FOS({},{allow:"*"});
4 | app.use(cookieParser());
5 | app.route("/hi").get((request,response,next) => { response.end("Hi!"); next(); });
6 | app.route("/").get(async (request) => { console.log("Cookies",JSON.stringify(request.cookies)); });
7 | app.static("/");
8 | app.listen(3000)
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fos-express example",
3 | "version": "v0.0.1",
4 | "description": "ExpressJS Function Oriented Server",
5 | "engines": {
6 | "node": "10.0.0"
7 | },
8 | "license": "MIT",
9 | "scripts": {},
10 | "repository": {},
11 | "keywords": [],
12 | "author": "Simon Y. Blackwell (http://www.github.com/anywhichway)",
13 | "bugs": {},
14 | "devDependencies": {},
15 | "dependencies": {
16 | "koa-static": "^5.0.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/testmarkdown.md:
--------------------------------------------------------------------------------
1 | You have been marked!
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var createServer,
3 | URL;
4 | if (typeof (module) !== "undefined") {
5 | createServer = require("http").createServer;
6 | URL = require("url").URL;
7 | } else { // not yet supported/tested
8 | class ServerResponse {
9 | constructor() {
10 | this.promise = new Promise(resolve => this.resolve = resolve);
11 | this.promise.statusCode = 200;
12 | this.promise.statusMessage = "";
13 | this.promise.headers = {};
14 | this.promise.body = "";
15 | Object.keys(ServerResponse.prototype).forEach(key => Object.defineProperty(this.promise, key, {
16 | enumerable: false,
17 | configurable: true,
18 | writable: true,
19 | value: ServerResponse.prototype[key]
20 | }))
21 | return this.promise;
22 | }
23 |
24 | addTrailers() {
25 | } // Adds HTTP trailing headers
26 | end(text = "") {
27 | this.body += text;
28 | this.finished = true;
29 | this.resolve();
30 | } // Signals that the the server should consider that the response is complete
31 | // finished Returns true if the response is complete, otherwise false
32 | getHeader(key) {
33 | return this.headers[key];
34 | } // Returns the value of the specified header
35 | // headersSent // Returns true if headers were sent, otherwise false
36 | removeHeader(key) {
37 | delete this.headers[key];
38 | } // Removes the specified header
39 | // sendDate set to false if the Date header should not be sent in the response. Default true
40 | setHeader(key, value) {
41 | this.headers[key] = value;
42 | } // Sets the specified header
43 | setTimeout() {
44 | } // Sets the timeout value of the socket to the specified number of milliseconds
45 | // statusCode Sets the status code that will be sent to the client
46 | // statusMessage Sets the status message that will be sent to the client
47 | write(text) {
48 | this.headersSent = true;
49 | this.body += text;
50 | } // Sends text, or a text stream, to the client
51 | //writeContinue() {} // Sends a HTTP Continue message to the client
52 | writeHead() {
53 | this.headersSent = true;
54 | } // Sends status and response headers to the client
55 | }
56 |
57 | createServer = async handler => {
58 | self.addEventListener('fetch', async event => {
59 | const request = Object.assign({}, event.request),
60 | response = new ServerResponse();
61 | request.headers = new Proxy(request.headers, {get: (target, property) => target.get(property)});
62 | handler(request, response);
63 | await response;
64 | event.respondWith(new Response(response.body, {
65 | status: response.status,
66 | statusText: response.statusText || response.statusMessage,
67 | headers: response.headers
68 | }));
69 | });
70 | }
71 | }
72 |
73 | function fromJSON(json, functions) {
74 | return JSON.parse(json, (_, value) => {
75 | if (value === '@NaN') return NaN;
76 | if (value === '@Infinity') return Infinity;
77 | if (value === '@-Infinity') return -Infinity;
78 | if (value === '@undefined') return undefined;
79 | if (typeof (value) === "string") {
80 | if (value.indexOf("Date@") === 0) return new Date(parseInt(value.substring(5)))
81 | if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/.test(value)) {
82 | return new Date(value);
83 | }
84 | if (functions && value.indexOf("Function@") === 0) return Function("return " + value.substring(9))();
85 | }
86 | return value;
87 | });
88 | }
89 |
90 | async function runCallback(cb, request, response, value) {
91 | if (typeof (cb) === "string") return true; // it is actually a path
92 | return new Promise(resolve => {
93 | const result = cb(request, response, resolve, value);
94 | if (result && typeof (result) === "object" && result instanceof Promise) {
95 | result.then(result => resolve(result))
96 | }
97 | });
98 | }
99 |
100 | function toJSON(value) {
101 | return JSON.stringify(value, (_, value) => {
102 | if (value !== value || value === Infinity || value === -Infinity || value === undefined || (typeof (value) === "number" && isNaN(value))) return `@${value}`;
103 | const type = typeof (value);
104 | if (type === "function") return "Function@" + value;
105 | if (value && type === "object" && value instanceof Date) return "Date@" + value.getTime();
106 | return value;
107 | });
108 | }
109 |
110 | function toScript(object, {server}, parent = "") {
111 | function fos(options) {
112 | fos._options = options;
113 | return fos;
114 | };
115 | const handlers = "{" + Object.keys(object).reduce((accum, key, index, array) => {
116 | const value = object[key],
117 | type = typeof (value),
118 | proto = Object.getPrototypeOf(value);
119 | if (type === "function") { // headers available as fos._headers
120 | accum += `"${key}": (...args) => {
121 | return fetch("${server}/fos/${parent}${key}?arguments="+encodeURIComponent(toJSON(args)),fos._options)
122 | .then(response => response.text().then(text => { delete fos._options; if(response.ok) { return text; } throw new Error(response.status + " " + text); }))
123 | .then(text => fromJSON(text,true));
124 | }`
125 | } else if (value && type === "object") {
126 | accum += `"${key}":` + toScript(value, {server}, `${parent}${key}.`);
127 | }
128 | if (index < array.length - 1) {
129 | accum += ",";
130 | }
131 | return accum;
132 | }, "") + "}";
133 | return `(() => {
134 | ${toJSON};
135 | ${fromJSON};
136 | var fos = Object.assign(${fos},${handlers});
137 | return fos;
138 | })()`;
139 | }
140 |
141 | const VERBS = ["all", "delete", "get", "head", "patch", "post", "put"];
142 |
143 | class FOS {
144 | constructor(functions, {allow, name="FOS", before, after, done, middleware,log} = {}) {
145 | this.functions = functions;
146 | this.settings = {log};
147 | this.routes = [];
148 | this.locals = {};
149 | this.engines = {};
150 | this.generators = {};
151 | const handler = async (request, response, complete = Promise.resolve()) => {
152 | request.fos = this;
153 | response.locals = Object.assign({}, this.locals);
154 | const url = new URL(request.url, (request.secure ? "https://" : "http://") + request.headers.host);
155 | if (!request.subdomains) {
156 | request.subdomains = [];
157 | const parts = request.headers.host.split(".");
158 | if (parts.length > 2) {
159 | parts.pop();
160 | parts.pop();
161 | request.subdomains.push(parts.join("."));
162 | }
163 | }
164 | request = new Proxy(request, {
165 | get: (target, property) => {
166 | if (url[property]) {
167 | return url[property];
168 | }
169 | if (property === "path") {
170 | return url.pathname;
171 | }
172 | if (property === "query") {
173 | return new Proxy(url.searchParams, {get: (target, property) => typeof (target.get) === "function" ? target.get(property) : target[property]});
174 | }
175 | if (property === "ip") {
176 | return (req.headers['x-forwarded-for'] || '').split(',').pop() ||
177 | req.connection.remoteAddress ||
178 | req.socket.remoteAddress ||
179 | req.connection.socket.remoteAddress
180 | }
181 | return target[property];
182 | }
183 | });
184 | if (allow) {
185 | response.setHeader("Access-Control-Allow-Origin", allow);
186 | }
187 | if (before) {
188 | await before({request, response});
189 | }
190 | if (!response.headersSent) {
191 | //console.log(request.pathname,request.query.arguments);
192 | let result;
193 | if (request.pathname === "/fos") {
194 | response.statusCode = 200;
195 | response.setHeader("Content-Type", "text/javascript");
196 | response.end(`${name ? "const " + name + " = " : ""}${toScript(this.functions, {server: request.protocol + "//" + request.host})};`);
197 | return;
198 | }
199 | if (request.pathname.indexOf("/fos/") === 0) {
200 | response.statusCode = 200;
201 | const parts = request.pathname.substring(5).split(".");
202 | let node = this.functions,
203 | key;
204 | while ((key = parts.shift()) && (node = node[key])) {
205 | if (parts.length === 0) {
206 | try {
207 | //console.log(request.query.arguments,fromJSON(request.query["arguments"]))
208 | result = await node.apply({
209 | request,
210 | response
211 | }, fromJSON(request.query["arguments"]));
212 | //console.log(result,node);
213 | if (after) {
214 | result = await after({result, request, response});
215 | }
216 | if (!response.headersSent) {
217 | response.end(toJSON(result));
218 | } else {
219 | response.end();
220 | }
221 | } catch (err) {
222 | response.statusCode = 500;
223 | response.end(err.message);
224 | }
225 | if (done) {
226 | done({result, request, response});
227 | }
228 | return;
229 | }
230 | }
231 | } else if (this.functions.request) { // request hander added
232 | await this.functions.request.call({request, response}, request.pathname);
233 | if (response.finished) return;
234 | }
235 | if (middleware) {
236 | complete();
237 | } else {
238 | response.statusCode = 404;
239 | response.end("Not Found");
240 | }
241 | }
242 | if (done) {
243 | done({result, request, response});
244 | }
245 | };
246 | if (middleware) {
247 | return handler;
248 | }
249 | this.server = createServer(handler);
250 | }
251 |
252 | disable(key) {
253 | this.set(key, false);
254 | }
255 |
256 | disabled(key) {
257 | return this.get(key) === false;
258 | }
259 |
260 | enable(key) {
261 | this.set(key, true);
262 | }
263 |
264 | enabled(key) {
265 | return this.get(key) === true;
266 | }
267 |
268 | engine(extension, renderFunction) {
269 | this.engines[extension] = renderFunction;
270 | }
271 |
272 | listen(port) {
273 | this.server.listen(port, err => {
274 | if (err) {
275 | console.log(err);
276 | }
277 | console.log(`A FOS is listening on ${port}`);
278 | })
279 | }
280 |
281 | param(params, callback) {
282 | if (!this.params) {
283 | this.params = {};
284 | }
285 | if (Array.isArray(params)) {
286 | params.slice().forEach(param => this.params[`:${param}`] = callback);
287 | } else {
288 | this.params[`:${params}`] = callback;
289 | }
290 | }
291 |
292 | set(name, value) {
293 | this.settings[name] = value;
294 | }
295 |
296 | route(path) {
297 | if (!this.functions.request) {
298 | this.functions.request = FOS.request;
299 | }
300 | const route = {path, all: [], delete: [], get: [], head: [], patch: [], post: [], put: []};
301 | this.routes.push(route);
302 | const proxy = new Proxy(route, {
303 | get(target, property) {
304 | if (!VERBS.includes(property)) throw new Error(`${property} is not an HTTP verb or 'all'`);
305 | return (...callbacks) => {
306 | target[property] = callbacks;
307 | return proxy;
308 | }
309 | }
310 | });
311 | return proxy;
312 | }
313 |
314 | static(path, {location = "", defaultFile = "index.html", mimeTypes = {}} = {}) {
315 | mimeTypes = {html: "text/html", js: "application/javascript", ...mimeTypes};
316 | let fs,
317 | normalizePath,
318 | resolvePath;
319 | try {
320 | if (typeof (require) === "function") {
321 | fs = require("fs");
322 | normalizePath = require("path").normalize;
323 | const process = require("process"),
324 | resolve = require("path").resolve;
325 | resolvePath = (...args) => resolve(process.cwd(), ...args);
326 | console.log(process.cwd())
327 | }
328 | } catch (e) {
329 | ;
330 | }
331 | if (!fs || !resolvePath || !normalizePath) return; // no-op if not on server where require("fs") works
332 | this.route(path).get(async (request, response) => {
333 | if (response.headersSent) return;
334 | let url = request.pathname.substring(path.length);
335 | if (url.length === 0) url = defaultFile;
336 | else if (url[url.length - 1] === "/") url += defaultFile;
337 | const extension = url.split(".").pop();
338 | return new Promise(resolve => {
339 | //console.log(normalizePath(location + "/" + url));
340 | const path = resolvePath(normalizePath(location + "/" + url).slice(1));
341 | if(this.settings.log) console.log(path);
342 | fs.readFile(path, async (err, data) => {
343 | if (err) {
344 | response.writeHead(404, {'Content-Type': 'text/plain'});
345 | response.write("Not Found");
346 | } else {
347 | const type = mimeTypes[extension];
348 | if (type) {
349 | const {
350 | contentType = `application/${extension}`,
351 | transform = value => value
352 | } = typeof type === "string" ? {contentType: type} : type;
353 | response.writeHead(200, {'Content-Type': contentType});
354 | response.write(await transform(data, request, response));
355 | } else {
356 | response.writeHead(200);
357 | response.write(data);
358 | }
359 | }
360 | response.end();
361 | resolve();
362 | });
363 | });
364 | });
365 | }
366 |
367 | use(pathOrCallback, ...callbacks) {
368 | if (typeof (pathOrCallback) === "function") {
369 | callbacks.unshift(pathOrCallback);
370 | pathOrCallback = () => true;
371 | }
372 | this.route(pathOrCallback).all(async (request, response, next) => {
373 | for (const cb of callbacks) {
374 | if ("route" === await runCallback(cb, request, response)) break;
375 | }
376 | next();
377 | });
378 | return this;
379 | }
380 |
381 | static fosify(app, functions, options) {
382 | options = Object.assign({}, options, {middleware: true});
383 | const fos = new FOS(functions, options);
384 | if (app.context && app.callback) { // Koa app has these, Express does not
385 | app.use(async (ctx, next) => {
386 | await next();
387 | if (ctx.request.path === "/fos" || /\/fos\/.*/.test(ctx.request.path)) {
388 | fos(ctx.request.req, ctx.response.res);
389 | }
390 | })
391 | } else {
392 | app.route("/fos").get(fos);
393 | app.route(/\/fos\/.*/).get(fos);
394 | }
395 | }
396 |
397 | static async request(path) {
398 | const {request, response} = this,
399 | fos = request.fos,
400 | uparts = path.split("/"),
401 | params = Object.assign({}, fos.params);
402 | for (const route of fos.routes) {
403 | const path = route.path,
404 | type = typeof (path),
405 | pparts = type === "string" ? path.split("/") : [];
406 | let result;
407 | for (let i = 0; i < pparts.length && i < uparts.length; i++) {
408 | const ppart = pparts[i],
409 | upart = uparts[i];
410 | if (params[ppart]) {
411 | result = await runCallback(params[ppart], request, response, uparts[i]);
412 | delete params[ppart];
413 | if (result === "route") break;
414 | } else if (ppart && ppart !== upart) {
415 | //result="route";
416 | break;
417 | }
418 | }
419 | if (result === "route" || pparts.length > uparts.length) continue;
420 | if ((type === "function" && path(request)) || (type === "object" && path instanceof RegExp && path.test(request.pathname)) || (type === "string" && path.indexOf(request.pathname.substring(0, path.length) === 0))) {
421 | for (const callback of route.all) {
422 | result = await runCallback(callback, request, response);
423 | if (result === "route") {
424 | result = null;
425 | break;
426 | }
427 | if (result === "done") {
428 | return;
429 | }
430 | }
431 | const verb = request.method.toLowerCase();
432 | if (route[verb]) {
433 | for (const callback of route[verb]) {
434 | result = await runCallback(callback, request, response);
435 | if (result === "route") {
436 | result = null;
437 | break;
438 | }
439 | if (result === "done") {
440 | return;
441 | }
442 | }
443 | }
444 | }
445 | }
446 | }
447 | }
448 |
449 | VERBS.forEach(key => {
450 | FOS.prototype[key] = function (path, ...callbacks) {
451 | return this.route(path)[key](...callbacks);
452 | }
453 | });
454 | FOS.prototype.get = function (pathOrKey, ...callbacks) {
455 | if (typeof (pathOrKey) === "string" && callbacks.length === 0) {
456 | return this.settings[pathOrKey];
457 | }
458 | return this.route(pathOrKey).get(...callbacks);
459 | }
460 |
461 | module.exports = FOS;
462 |
463 | }).call(this);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fos",
3 | "version": "v0.0.10a",
4 | "description": "Function Oriented Server: The easy way to expose JavaScript APIs to clients as micro-services.",
5 | "engines": {
6 | "node": ">=10.0.0"
7 | },
8 | "license": "MIT",
9 | "scripts": {},
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/anywhichway/fos.git"
13 | },
14 | "keywords": [],
15 | "author": "Simon Y. Blackwell (http://www.github.com/anywhichway)",
16 | "bugs": {
17 | "url": "https://github.com/anywhichway/fos/issues"
18 | },
19 | "homepage": "https://github.com/anywhichway/fos#readme",
20 | "devDependencies": {
21 | "cookie-parser": "^1.4.3",
22 | "markdown-it": "^13.0.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------