@server/next 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "An experimental reimplementation of server.js focused on the DX",
5
5
  "homepage": "https://node-server.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
package/src/bucket.js CHANGED
@@ -7,6 +7,11 @@ import fsp from "fs/promises";
7
7
  // at the very least a read(id) and write(id, value), both returning
8
8
  // promises. If possible both are also pipeable/streamable.
9
9
  export default function (root) {
10
+ // Already a bucket, no need to do anything with it, just return it:
11
+ if (typeof root !== "string") {
12
+ return root;
13
+ }
14
+
10
15
  const absolute = (name) => {
11
16
  if (!name) throw new Error(`File name is required`);
12
17
  return path.resolve(path.join(root, name));
@@ -4,6 +4,7 @@ import parseCookies from "./parseCookies.js";
4
4
 
5
5
  export default async (request, options = {}) => {
6
6
  const ctx = {};
7
+ ctx.options = options;
7
8
  ctx.req = request;
8
9
  ctx.res = { status: null, headers: {}, cookies: {} };
9
10
  ctx.method = request.method.toLowerCase();
@@ -19,10 +20,23 @@ export default async (request, options = {}) => {
19
20
  Object.fromEntries(url.searchParams.entries())
20
21
  );
21
22
 
22
- if (request.body) {
23
- const type = ctx.headers["content-type"];
24
- ctx.body = await parseBody(request, type, options.uploads);
25
- }
23
+ await new Promise((resolve, reject) => {
24
+ const body = [];
25
+ request
26
+ .on("data", (chunk) => {
27
+ body.push(chunk);
28
+ })
29
+ .on("end", async () => {
30
+ const type = ctx.headers["content-type"];
31
+ ctx.body = await parseBody(
32
+ Buffer.concat(body).toString(),
33
+ type,
34
+ options.uploads
35
+ );
36
+ resolve();
37
+ })
38
+ .on("error", reject);
39
+ });
26
40
 
27
41
  return ctx;
28
42
  };
@@ -4,6 +4,7 @@ import parseCookies from "./parseCookies.js";
4
4
 
5
5
  export default async (request, options = {}) => {
6
6
  const ctx = {};
7
+ ctx.options = options;
7
8
  ctx.req = request;
8
9
  ctx.res = { status: null, headers: {}, cookies: {} };
9
10
  ctx.method = request.method.toLowerCase();
@@ -0,0 +1,6 @@
1
+ export default class StatusError extends Error {
2
+ constructor(msg, status = 500) {
3
+ super(msg);
4
+ this.status = status;
5
+ }
6
+ }
@@ -1,6 +1,7 @@
1
1
  import parseResponse from "../parseResponse.js";
2
2
  import pathPattern from "../pathPattern.js";
3
3
  import define from "./define.js";
4
+ import validate from "./validate.js";
4
5
 
5
6
  export default async function handleRequest(handlers, ctx) {
6
7
  for (let [matcher, ...cbs] of handlers[ctx.method]) {
@@ -11,10 +12,17 @@ export default async function handleRequest(handlers, ctx) {
11
12
  define(ctx.url, "params", () => match);
12
13
 
13
14
  for (let cb of cbs) {
14
- const out = await parseResponse(cb, ctx);
15
- if (out) return out;
15
+ try {
16
+ validate(ctx, cb);
17
+ if (typeof cb === "function") {
18
+ const out = await parseResponse(cb, ctx);
19
+ if (out) return out;
20
+ }
21
+ } catch (error) {
22
+ return new Response(error.message, { status: error.status || 500 });
23
+ }
16
24
  }
17
25
  }
18
26
 
19
- return Response("Not Found", { status: 404 });
27
+ return new Response("Not Found", { status: 404 });
20
28
  }
@@ -0,0 +1,35 @@
1
+ import StatusError from "./StatusError.js";
2
+
3
+ export default function (ctx, schema) {
4
+ if (!schema || typeof schema !== "object") return;
5
+
6
+ let base;
7
+ try {
8
+ if (typeof schema?.body === "function") {
9
+ base = "body";
10
+ schema.body(ctx.body || {});
11
+ }
12
+ if (typeof schema?.body?.parse === "function") {
13
+ base = "body";
14
+ schema.body.parse(ctx.body || {});
15
+ }
16
+ if (typeof schema?.query === "function") {
17
+ base = "query";
18
+ schema.query(ctx.url.query || {});
19
+ }
20
+ if (typeof schema?.query?.parse === "function") {
21
+ base = "query";
22
+ schema.query.parse(ctx.url.query || {});
23
+ }
24
+ } catch (error) {
25
+ if (error.constructor.name === "ZodError") {
26
+ console.log(error);
27
+ const message = error.issues
28
+ .map(({ path, message }) => `[${base}.${path.join(".")}]: ${message}`)
29
+ .sort()
30
+ .join("\n");
31
+ throw new StatusError(message, 422);
32
+ }
33
+ throw error;
34
+ }
35
+ }
@@ -9,7 +9,7 @@ export default async function parseResponse(handler, ctx) {
9
9
  if (!out && typeof out !== "string") return null;
10
10
 
11
11
  if (typeof out === "function") {
12
- out = out(ctx);
12
+ out = await out(ctx);
13
13
  }
14
14
 
15
15
  // A plain number is a status code
package/src/reply.js CHANGED
@@ -62,6 +62,17 @@ Reply.prototype.file = async function (path) {
62
62
  return status(404).send();
63
63
  };
64
64
 
65
+ Reply.prototype.view = async function (path) {
66
+ return async (ctx) => {
67
+ if (!ctx.options.views) {
68
+ throw new Error("Views not enabled");
69
+ }
70
+ const data = await ctx.options.views.read(path);
71
+ if (data) return this.type(path.split(".").pop()).send(data);
72
+ return status(404).send();
73
+ };
74
+ };
75
+
65
76
  Reply.prototype.send = function (body = "") {
66
77
  const { status = 200 } = this.res;
67
78
 
@@ -93,3 +104,4 @@ export const cookies = (...args) => new Reply().cookies(...args);
93
104
  export const send = (...args) => new Reply().send(...args);
94
105
  export const json = (...args) => new Reply().json(...args);
95
106
  export const file = (...args) => new Reply().file(...args);
107
+ export const view = (...args) => new Reply().view(...args);