diff --git a/packages/app-opine/api/cache.js b/packages/app-opine/api/cache.js index 37afabc1..84d125d8 100644 --- a/packages/app-opine/api/cache.js +++ b/packages/app-opine/api/cache.js @@ -25,8 +25,8 @@ export const createDocument = ({ params, body, cache }, res) => fork(res, 201, cache.createDoc(params.name, body.key, body.value, body.ttl)); // GET /cache/:name/:key -export const getDocument = ({ params, cache }, res) => - fork(res, 200, cache.getDoc(params.name, params.key)); +export const getDocument = ({ params, isLegacyGetEnabled, cache }, res) => + fork(res, 200, cache.getDoc(params.name, params.key, isLegacyGetEnabled)); // PUT /cache/:name/:key export const updateDocument = ({ cache, params, body, query }, res) => diff --git a/packages/app-opine/api/data.js b/packages/app-opine/api/data.js index 7cd72632..62873659 100644 --- a/packages/app-opine/api/data.js +++ b/packages/app-opine/api/data.js @@ -21,8 +21,8 @@ export const createDocument = ({ params, body, data }, res) => fork(res, 201, data.createDocument(params.db, body)); // GET /data/:db/:id -export const getDocument = ({ params, data }, res) => - fork(res, 200, data.getDocument(params.db, params.id)); +export const getDocument = ({ params, isLegacyGetEnabled, data }, res) => + fork(res, 200, data.getDocument(params.db, params.id, isLegacyGetEnabled)); // PUT /data/:db/:id export const updateDocument = ({ data, params, body }, res) => diff --git a/packages/app-opine/deps.js b/packages/app-opine/deps.js index 25bb11ce..ab6064d1 100644 --- a/packages/app-opine/deps.js +++ b/packages/app-opine/deps.js @@ -1,10 +1,10 @@ -export { json, opine, Router } from "https://deno.land/x/opine@2.1.5/mod.ts"; +export { json, opine, Router } from "https://deno.land/x/opine@2.3.3/mod.ts"; export { opineCors as cors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; -export { lookup as getMimeType } from "https://deno.land/x/media_types@v2.12.3/mod.ts"; +export { contentType as getMimeType } from "https://deno.land/std@0.167.0/media_types/mod.ts"; +// TODO: refactor off deprecated: see https://github.com/denoland/deno_std/issues/1778 export { MultipartReader } from "https://deno.land/std@0.133.0/mime/mod.ts"; -export { Buffer } from "https://deno.land/std@0.133.0/io/buffer.ts"; -export { exists } from "https://deno.land/std@0.133.0/fs/exists.ts"; +export { Buffer } from "https://deno.land/std@0.167.0/io/buffer.ts"; export { default as helmet } from "https://cdn.skypack.dev/helmet@5.0.2"; export * as R from "https://cdn.skypack.dev/ramda@0.28.0"; diff --git a/packages/app-opine/dev_deps.js b/packages/app-opine/dev_deps.js index 19436660..581aaf5b 100644 --- a/packages/app-opine/dev_deps.js +++ b/packages/app-opine/dev_deps.js @@ -2,5 +2,5 @@ export { assert, assertEquals, assertObjectMatch, -} from "https://deno.land/std@0.152.0/testing/asserts.ts"; +} from "https://deno.land/std@0.167.0/testing/asserts.ts"; export { superdeno } from "https://deno.land/x/superdeno@4.8.0/mod.ts"; diff --git a/packages/app-opine/hyper63-logo.png b/packages/app-opine/hyper63-logo.png new file mode 100644 index 00000000..cb1bb904 Binary files /dev/null and b/packages/app-opine/hyper63-logo.png differ diff --git a/packages/app-opine/lib/formData.js b/packages/app-opine/lib/formData.js index c0688546..9476c1af 100644 --- a/packages/app-opine/lib/formData.js +++ b/packages/app-opine/lib/formData.js @@ -1,4 +1,4 @@ -import { exists, MultipartReader, R } from "../deps.js"; +import { MultipartReader, R } from "../deps.js"; import { isMultipartFormData } from "../utils.js"; const { compose, nth, split } = R; @@ -15,12 +15,13 @@ export default async (req, _res, next) => { let boundary; const contentType = req.get("content-type"); - if (isMultipartFormData(contentType)) { - boundary = getBoundary(contentType); - } + if (isMultipartFormData(contentType)) boundary = getBoundary(contentType); - if (!(await exists(TMP_DIR))) { + try { await Deno.mkdir(TMP_DIR, { recursive: true }); + } catch (err) { + if (!(err instanceof Deno.errors.AlreadyExists)) throw err; + // dir exists, so do nothing } const form = await new MultipartReader(req.body, boundary).readForm({ diff --git a/packages/app-opine/lib/legacyGet.js b/packages/app-opine/lib/legacyGet.js new file mode 100644 index 00000000..6bc31aba --- /dev/null +++ b/packages/app-opine/lib/legacyGet.js @@ -0,0 +1,8 @@ +import { isTrue } from "../utils.js"; + +export default (req, _res, next) => { + if (!req.get("X-HYPER-LEGACY-GET")) return next(); + + req.isLegacyGetEnabled = isTrue(req.get("X-HYPER-LEGACY-GET")); + next(); +}; diff --git a/packages/app-opine/router.js b/packages/app-opine/router.js index b6ea2c20..fc8863ba 100644 --- a/packages/app-opine/router.js +++ b/packages/app-opine/router.js @@ -2,6 +2,7 @@ import { cors, helmet, json, opine, R } from "./deps.js"; // middleware import formData from "./lib/formData.js"; +import legacyGet from "./lib/legacyGet.js"; import { isMultipartFormData } from "./utils.js"; @@ -52,7 +53,7 @@ export function hyperRouter(services) { app.delete("/data/:db", bindCore, data.removeDb); app.get("/data/:db", bindCore, data.listDocuments); app.post("/data/:db", json({ limit: "8mb" }), bindCore, data.createDocument); - app.get("/data/:db/:id", bindCore, data.getDocument); + app.get("/data/:db/:id", bindCore, legacyGet, data.getDocument); app.put( "/data/:db/:id", json({ limit: "8mb" }), @@ -71,7 +72,7 @@ export function hyperRouter(services) { app.get("/cache/:name/_query", bindCore, cache.queryStore); app.post("/cache/:name/_query", bindCore, cache.queryStore); app.post("/cache/:name", json(), bindCore, cache.createDocument); - app.get("/cache/:name/:key", bindCore, cache.getDocument); + app.get("/cache/:name/:key", bindCore, legacyGet, cache.getDocument); app.put("/cache/:name/:key", json(), bindCore, cache.updateDocument); app.delete("/cache/:name/:key", bindCore, cache.deleteDocument); diff --git a/packages/app-opine/scripts/test.sh b/packages/app-opine/scripts/test.sh index 09f09ecd..5ea0cdfe 100755 --- a/packages/app-opine/scripts/test.sh +++ b/packages/app-opine/scripts/test.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -deno lint && deno fmt --check && deno test -A --unstable \ No newline at end of file +deno lint && deno fmt --check && deno test -A --no-lock --no-check --unstable \ No newline at end of file diff --git a/packages/app-opine/test/cache_test.js b/packages/app-opine/test/cache_test.js new file mode 100644 index 00000000..281eb1a9 --- /dev/null +++ b/packages/app-opine/test/cache_test.js @@ -0,0 +1,53 @@ +// TODO: Tyler. Probably better way to do this +import { crocks } from "../deps.js"; +import { assert, assertEquals, superdeno } from "../dev_deps.js"; + +import build from "../mod.js"; + +Deno.env.set("DENO_ENV", "test"); + +const app = build({ + cache: { + getDoc: (name, key, isLegacyGetEnabled) => + crocks.Async.Resolved({ + ok: true, + doc: { _id: key, name, isLegacyGetEnabled }, + }), + }, + middleware: [], +}); + +Deno.test("cache", async (t) => { + await t.step("GET /cache/:name/:id", async (t) => { + await t.step("should pass the correct values", async () => { + const withLegacy = await superdeno(app) + .get("/cache/movies/key") + .set("X-HYPER-LEGACY-GET", true); + + assert(withLegacy.body.ok); + assertEquals(withLegacy.body.doc._id, "key"); + assertEquals(withLegacy.body.doc.name, "movies"); + assert(withLegacy.body.doc.isLegacyGetEnabled); + + const withoutLegacy = await superdeno(app) + .get("/cache/movies/key"); + + assert(withoutLegacy.body.ok); + assertEquals(withoutLegacy.body.doc._id, "key"); + assertEquals(withoutLegacy.body.doc.name, "movies"); + // Will cause the default in core to be used + assertEquals(withoutLegacy.body.doc.isLegacyGetEnabled, undefined); + + const withLegacyDisabled = await superdeno(app) + .get("/cache/movies/key") + .set("X-HYPER-LEGACY-GET", false); + + assert(withLegacyDisabled.body.ok); + assertEquals(withLegacyDisabled.body.doc._id, "key"); + assertEquals(withLegacyDisabled.body.doc.name, "movies"); + assertEquals(withLegacyDisabled.body.doc.isLegacyGetEnabled, false); + }); + }); + + // TODO: add more test coverage here +}); diff --git a/packages/app-opine/test/crawler-delete_test.js b/packages/app-opine/test/crawler-delete_test.js deleted file mode 100644 index 00bd26e8..00000000 --- a/packages/app-opine/test/crawler-delete_test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { crocks } from "../deps.js"; -import { assertEquals, superdeno } from "../dev_deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - crawler: { - remove: () => crocks.Async.Resolved({ ok: true }), - }, - middleware: [], -}); - -Deno.test("DELETE /crawler/test/spider", async () => { - const res = await superdeno(app) - .delete("/crawler/test/spider") - .send(); - - assertEquals(res.body.ok, true); -}); diff --git a/packages/app-opine/test/crawler-get_test.js b/packages/app-opine/test/crawler-get_test.js deleted file mode 100644 index c910c368..00000000 --- a/packages/app-opine/test/crawler-get_test.js +++ /dev/null @@ -1,36 +0,0 @@ -import { crocks } from "../deps.js"; -import { assertEquals, superdeno } from "../dev_deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - crawler: { - get: () => - crocks.Async.Resolved({ - id: "test-spider", - app: "test", - name: "spider", - source: "https://example.com", - depth: 2, - script: "", - target: { - url: "https://jsonplaceholder.typicode.com/posts", - sub: "1234", - aud: "https://example.com", - secret: "secret", - }, - notify: "https://example.com", - }), - }, - middleware: [], -}); - -Deno.test("GET /crawler/test/spider", async () => { - const res = await superdeno(app) - .get("/crawler/test/spider") - .send(); - - assertEquals(res.body.id, "test-spider"); -}); diff --git a/packages/app-opine/test/crawler-start_test.js b/packages/app-opine/test/crawler-start_test.js deleted file mode 100644 index af0b861e..00000000 --- a/packages/app-opine/test/crawler-start_test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { crocks } from "../deps.js"; -import { assertEquals, superdeno } from "../dev_deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - crawler: { - start: () => crocks.Async.Resolved({ ok: true }), - }, - middleware: [], -}); - -Deno.test("POST /crawler/test/spider/_start", async () => { - const res = await superdeno(app) - .post("/crawler/test/spider/_start") - .send(); - - assertEquals(res.body.ok, true); -}); diff --git a/packages/app-opine/test/crawler-upsert_test.js b/packages/app-opine/test/crawler-upsert_test.js deleted file mode 100644 index c8c9b1de..00000000 --- a/packages/app-opine/test/crawler-upsert_test.js +++ /dev/null @@ -1,33 +0,0 @@ -import { crocks } from "../deps.js"; -import { assertEquals, superdeno } from "../dev_deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - crawler: { - upsert: () => crocks.Async.Resolved({ ok: true }), - }, - middleware: [], -}); - -Deno.test("PUT /crawler/test/spider", async () => { - const res = await superdeno(app) - .put("/crawler/test/spider") - .set("Content-Type", "application/json") - .send({ - source: "https://example.com", - depth: 2, - script: "", - target: { - url: "https://jsonplaceholder.typicode.com/posts", - sub: "1234", - aud: "https://example.com", - secret: "secret", - }, - notify: "https://example.com", - }); - - assertEquals(res.body.ok, true); -}); diff --git a/packages/app-opine/test/crawler_test.js b/packages/app-opine/test/crawler_test.js new file mode 100644 index 00000000..45c2081d --- /dev/null +++ b/packages/app-opine/test/crawler_test.js @@ -0,0 +1,94 @@ +// TODO: Tyler. Probably better way to do this +import { crocks } from "../deps.js"; +import { assert, assertEquals, superdeno } from "../dev_deps.js"; + +import build from "../mod.js"; + +Deno.env.set("DENO_ENV", "test"); + +const app = build({ + crawler: { + remove: (bucket, name) => crocks.Async.Resolved({ ok: true, bucket, name }), + get: (bucket, name) => + crocks.Async.Resolved({ + id: "test-spider", + app: "test", + source: "https://example.com", + depth: 2, + script: "", + target: { + url: "https://jsonplaceholder.typicode.com/posts", + sub: "1234", + aud: "https://example.com", + secret: "secret", + }, + notify: "https://example.com", + bucket, + name, + }), + start: (bucket, name) => crocks.Async.Resolved({ ok: true, bucket, name }), + upsert: ({ app, name }) => + crocks.Async.Resolved({ ok: true, bucket: app, name }), + }, + middleware: [], +}); + +Deno.test("crawler", async (t) => { + await t.step("GET /crawler/:bucket/:name", async (t) => { + await t.step("GET /crawler/test/spider", async () => { + const res = await superdeno(app) + .get("/crawler/test/spider") + .send(); + + assertEquals(res.body.id, "test-spider"); + assertEquals(res.body.bucket, "test"); + assertEquals(res.body.name, "spider"); + }); + }); + + await t.step("DELETE /crawler/:bucket/:name", async (t) => { + await t.step("should pass the correct values", async () => { + const res = await superdeno(app) + .delete("/crawler/test/spider") + .send(); + + assert(res.body.ok); + assertEquals(res.body.bucket, "test"); + assertEquals(res.body.name, "spider"); + }); + }); + + await t.step("POST /crawler/:bucket/:name/_start", async (t) => { + await t.step("should pass the correct values", async () => { + const res = await superdeno(app) + .post("/crawler/test/spider/_start") + .send(); + + assert(res.body.ok); + assertEquals(res.body.bucket, "test"); + assertEquals(res.body.name, "spider"); + }); + }); + + await t.step("PUT /crawler/:bucket/:name", async () => { + const res = await superdeno(app) + .put("/crawler/test/spider") + .set("Content-Type", "application/json") + .send({ + source: "https://example.com", + depth: 2, + script: "", + target: { + url: "https://jsonplaceholder.typicode.com/posts", + sub: "1234", + aud: "https://example.com", + secret: "secret", + }, + notify: "https://example.com", + }); + + assertEquals(res.body.ok, true); + assertEquals(res.body.bucket, "test"); + assertEquals(res.body.name, "spider"); + }); +}); diff --git a/packages/app-opine/test/data-bulk_test.js b/packages/app-opine/test/data-bulk_test.js deleted file mode 100644 index 40a536eb..00000000 --- a/packages/app-opine/test/data-bulk_test.js +++ /dev/null @@ -1,23 +0,0 @@ -// TODO: Tyler. Probably better way to do this -import { crocks } from "../deps.js"; -import { assertEquals, superdeno } from "../dev_deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - data: { - bulkDocuments: () => crocks.Async.Resolved({ ok: true, results: [] }), - }, - middleware: [], -}); - -Deno.test("POST /data/movies/_bulk", async () => { - const res = await superdeno(app) - .post("/data/movies/_bulk") - .set("Content-Type", "application/json") - .send([{ id: "1", type: "movie" }]); - - assertEquals(res.body.ok, true); -}); diff --git a/packages/app-opine/test/data-list_test.js b/packages/app-opine/test/data-list_test.js deleted file mode 100644 index 4f6313ab..00000000 --- a/packages/app-opine/test/data-list_test.js +++ /dev/null @@ -1,27 +0,0 @@ -import { assert, superdeno } from "../dev_deps.js"; -import { crocks } from "../deps.js"; - -import build from "../mod.js"; - -Deno.env.set("DENO_ENV", "test"); - -const app = build({ - data: { - listDocuments: (_info, _options) => { - return crocks.Async.Resolved({ ok: true, docs: [] }); - }, - }, - middleware: [], -}); - -Deno.test("GET /data/movies?limit=2", async () => { - const res = await superdeno(app) - .get("/data/movies?limit=2"); - - assert(res.body.ok); - - const res2 = await superdeno(app) - .get("/data/movies"); - - assert(res2.body.ok); -}); diff --git a/packages/app-opine/test/data_test.js b/packages/app-opine/test/data_test.js new file mode 100644 index 00000000..5088e5aa --- /dev/null +++ b/packages/app-opine/test/data_test.js @@ -0,0 +1,91 @@ +// TODO: Tyler. Probably better way to do this +import { crocks } from "../deps.js"; +import { assert, assertEquals, superdeno } from "../dev_deps.js"; + +import build from "../mod.js"; + +Deno.env.set("DENO_ENV", "test"); + +const app = build({ + data: { + getDocument: (db, id, isLegacyGetEnabled) => + crocks.Async.Resolved({ + ok: true, + doc: { _id: id, db, isLegacyGetEnabled }, + }), + bulkDocuments: (db, body) => + crocks.Async.Resolved({ ok: true, db, results: body }), + listDocuments: (db, query) => { + return crocks.Async.Resolved({ ok: true, db, query, docs: [] }); + }, + }, + middleware: [], +}); + +Deno.test("data", async (t) => { + await t.step("GET /data/:db/:id", async (t) => { + await t.step("should pass the correct values", async () => { + const withLegacy = await superdeno(app) + .get("/data/movies/1") + .set("X-HYPER-LEGACY-GET", true); + + assert(withLegacy.body.ok); + assertEquals(withLegacy.body.doc._id, "1"); + assertEquals(withLegacy.body.doc.db, "movies"); + assert(withLegacy.body.doc.isLegacyGetEnabled); + + const withoutLegacy = await superdeno(app) + .get("/data/movies/1"); + + assert(withoutLegacy.body.ok); + assertEquals(withoutLegacy.body.doc._id, "1"); + assertEquals(withoutLegacy.body.doc.db, "movies"); + // Will cause the default in core to be used + assertEquals(withoutLegacy.body.doc.isLegacyGetEnabled, undefined); + + const withLegacyDisabled = await superdeno(app) + .get("/data/movies/1") + .set("X-HYPER-LEGACY-GET", false); + + assert(withLegacyDisabled.body.ok); + assertEquals(withLegacyDisabled.body.doc._id, "1"); + assertEquals(withLegacyDisabled.body.doc.db, "movies"); + assertEquals(withLegacyDisabled.body.doc.isLegacyGetEnabled, false); + }); + }); + + await t.step("POST /data/movies/_bulk", async (t) => { + await t.step("should pass the correct values", async () => { + const res = await superdeno(app) + .post("/data/movies/_bulk") + .set("Content-Type", "application/json") + .send([{ id: "1", type: "movie" }]); + + assert(res.body.ok); + assertEquals(res.body.db, "movies"); + assertEquals(res.body.results, [{ id: "1", type: "movie" }]); + }); + }); + + await t.step("GET /data/movies", async (t) => { + await t.step("query parmas ?limit=2", async () => { + const res = await superdeno(app) + .get("/data/movies?limit=2"); + + assert(res.body.ok); + assertEquals(res.body.db, "movies"); + assertEquals(res.body.query, { limit: "2" }); + }); + + await t.step("no query params", async () => { + const res = await superdeno(app) + .get("/data/movies"); + + assert(res.body.ok); + assertEquals(res.body.db, "movies"); + assertEquals(res.body.query, {}); + }); + }); + + // TODO: add more test coverage here +}); diff --git a/packages/app-opine/test/formData_test.js b/packages/app-opine/test/formData_test.js new file mode 100644 index 00000000..00d2cf40 --- /dev/null +++ b/packages/app-opine/test/formData_test.js @@ -0,0 +1,19 @@ +import { opine } from "../deps.js"; +import { assert, superdeno } from "../dev_deps.js"; + +import formData from "../lib/formData.js"; + +Deno.test("formData", async (t) => { + const app = opine(); + app.put("/foo/bar", formData, (req, res) => { + res.json({ form: !!req.form }); + }); + + await t.step("should parse the FormData", async () => { + const on = await superdeno(app) + .put("/foo/bar") + .attach("file", Deno.readFileSync("hyper63-logo.png"), "image.png"); + + assert(on.body.form); + }); +}); diff --git a/packages/app-opine/test/legacyGet_test.js b/packages/app-opine/test/legacyGet_test.js new file mode 100644 index 00000000..1d441af2 --- /dev/null +++ b/packages/app-opine/test/legacyGet_test.js @@ -0,0 +1,29 @@ +import { opine } from "../deps.js"; +import { assert, superdeno } from "../dev_deps.js"; + +import legacyGet from "../lib/legacyGet.js"; + +Deno.test("legacyGet", async (t) => { + const app = opine(); + app.get("/foo/bar", legacyGet, (req, res) => { + res.json({ isLegacyGetEnabled: req.isLegacyGetEnabled }); + }); + + await t.step("should extract the legacyGet header flag", async () => { + const on = await superdeno(app) + .get("/foo/bar") + .set("x-hyper-legacy-get", true); + assert(on.body.isLegacyGetEnabled); + + const off = await superdeno(app) + .get("/foo/bar") + .set("x-hyper-legacy-get", false); + assert(!off.body.isLegacyGetEnabled); + }); + + await t.step("should not set legacyGet header flag", async () => { + const on = await superdeno(app) + .get("/foo/bar"); + assert(on.body.isLegacyGetEnabled === undefined); + }); +}); diff --git a/packages/app-opine/test/mod_test.js b/packages/app-opine/test/mod_test.js index 7cdd0e4f..feed12d3 100644 --- a/packages/app-opine/test/mod_test.js +++ b/packages/app-opine/test/mod_test.js @@ -9,25 +9,27 @@ const app = build({ middleware: [], }); -Deno.test("GET /", async () => { - const res = await superdeno(app) - .get("/") - .expect(200); +Deno.test("mod", async (t) => { + await t.step("GET /", async () => { + const res = await superdeno(app) + .get("/") + .expect(200); - assertEquals(res.body.name, "hyper"); -}); + assertEquals(res.body.name, "hyper"); + }); -Deno.test("GET /foobarbaz", async () => { - await superdeno(app) - .get("/foobarbaz") - .expect(404); -}); + await t.step("GET /foobarbaz", async () => { + await superdeno(app) + .get("/foobarbaz") + .expect(404); + }); -/* -Deno.test("GET /graphql", async () => { - await superdeno(app) - .get("/graphql") - .set("Accept", "text/html") // ask for the playground - .expect(200); + /* + Deno.test("GET /graphql", async () => { + await superdeno(app) + .get("/graphql") + .set("Accept", "text/html") // ask for the playground + .expect(200); + }); + */ }); -*/ diff --git a/packages/app-opine/test/utils_test.js b/packages/app-opine/test/utils_test.js index c98eefb5..c5f38f0d 100644 --- a/packages/app-opine/test/utils_test.js +++ b/packages/app-opine/test/utils_test.js @@ -17,79 +17,97 @@ const env = Deno.env.get("DENO_ENV"); const cleanup = () => env ? Deno.env.set("DENO_ENV", env) : Deno.env.delete("DENO_ENV"); -Deno.test("fork", async (t) => { - await t.step("should sanitize errors on both branches", async () => { - Deno.env.set("DENO_ENV", "production"); - - // resolved success - await fork(res, 200, Async.Resolved({ ok: true })); - assert(result.ok); - - // resolved error - await fork(res, 200, Async.Resolved({ ok: false, originalErr: "foobar" })); - assertEquals(result.ok, false); - assert(!result.originalErr); - - // rejected error (fatal) - await fork(res, 200, Async.Rejected({ ok: false, originalErr: "foobar" })); - assertEquals(result, "Internal Server Error"); - - cleanup(); +Deno.test("utils", async (t) => { + await t.step("fork", async (t) => { + await t.step("should sanitize errors on both branches", async () => { + Deno.env.set("DENO_ENV", "production"); + + // resolved success + await fork(res, 200, Async.Resolved({ ok: true })); + assert(result.ok); + + // resolved error + await fork( + res, + 200, + Async.Resolved({ ok: false, originalErr: "foobar" }), + ); + assertEquals(result.ok, false); + assert(!result.originalErr); + + // rejected error (fatal) + await fork( + res, + 200, + Async.Rejected({ ok: false, originalErr: "foobar" }), + ); + assertEquals(result, "Internal Server Error"); + + cleanup(); + }); + + await t.step("should NOT sanitize errors on both branches", async () => { + Deno.env.set("DENO_ENV", "foo"); + + // resolved success + await fork(res, 200, Async.Resolved({ ok: true })); + assert(result.ok); + + // resolved error + await fork( + res, + 200, + Async.Resolved({ ok: false, originalErr: "foobar" }), + ); + assertEquals(result.ok, false); + assert(result.originalErr); + + // rejected error (fatal) + await fork( + res, + 200, + Async.Rejected({ ok: false, originalErr: "foobar" }), + ); + assertEquals(result.ok, false); + assert(result.originalErr); + + cleanup(); + }); }); - await t.step("should NOT sanitize errors on both branches", async () => { - Deno.env.set("DENO_ENV", "foo"); - - // resolved success - await fork(res, 200, Async.Resolved({ ok: true })); - assert(result.ok); - - // resolved error - await fork(res, 200, Async.Resolved({ ok: false, originalErr: "foobar" })); - assertEquals(result.ok, false); - assert(result.originalErr); - - // rejected error (fatal) - await fork(res, 200, Async.Rejected({ ok: false, originalErr: "foobar" })); - assertEquals(result.ok, false); - assert(result.originalErr); - - cleanup(); + await t.step("isMultipartFormData", async (t) => { + await t.step( + "should return true if header indicates multipart/form-data", + () => { + assert(isMultipartFormData("multipart/form-data")); + assert(!isMultipartFormData("application/json")); + assert(!isMultipartFormData()); + }, + ); }); -}); - -Deno.test("isMultipartFormData", async (t) => { - await t.step( - "should return true if header indicates multipart/form-data", - () => { - assert(isMultipartFormData("multipart/form-data")); - assert(!isMultipartFormData("application/json")); - assert(!isMultipartFormData()); - }, - ); -}); -Deno.test("isFile", async (t) => { - await t.step( - "should return true if path points to a file", - () => { - assert(isFile("/foo.jpg")); - assert(!isFile("/foo")); - assert(!isFile()); - }, - ); -}); + await t.step("isFile", async (t) => { + await t.step( + "should return true if path points to a file", + () => { + assert(isFile("/foo.jpg")); + assert(!isFile("/foo")); + assert(!isFile()); + }, + ); + }); -Deno.test("isTrue", async (t) => { - await t.step( - "should return true if value is true-like", - () => { - assert(isTrue(true)); - assert(isTrue("true")); - assert(!isTrue("t")); - assert(!isTrue(false)); - assert(!isTrue("false")); - assert(!isTrue("")); - }, - ); + await t.step("isTrue", async (t) => { + await t.step( + "should return true if value is true-like", + () => { + assert(isTrue(true)); + assert(isTrue("true")); + assert(!isTrue("t")); + assert(!isTrue(false)); + assert(!isTrue("false")); + assert(!isTrue("")); + }, + ); + }); }); diff --git a/packages/core/dev_deps.js b/packages/core/dev_deps.js index 2e557696..442d9c77 100644 --- a/packages/core/dev_deps.js +++ b/packages/core/dev_deps.js @@ -3,8 +3,8 @@ export { assertEquals, assertObjectMatch, assertThrows, -} from "https://deno.land/std@0.152.0/testing/asserts.ts"; +} from "https://deno.land/std@0.167.0/testing/asserts.ts"; export { encode as base64Encode, -} from "https://deno.land/std@0.152.0/encoding/base64.ts"; +} from "https://deno.land/std@0.167.0/encoding/base64.ts"; diff --git a/packages/core/lib/cache/doc.js b/packages/core/lib/cache/doc.js index 8b0f7245..5e8291bb 100644 --- a/packages/core/lib/cache/doc.js +++ b/packages/core/lib/cache/doc.js @@ -1,6 +1,5 @@ import { crocks, ms, R } from "../../deps.js"; - -import { apply, is, of, triggerEvent } from "../utils/mod.js"; +import { apply, is, legacyGet, of, triggerEvent } from "../utils/mod.js"; const { compose, identity, ifElse, isNil, lensProp, prop, over, omit } = R; const { hasProp } = crocks; @@ -38,7 +37,8 @@ export const get = (store, key) => of({ store, key }) .chain(is(validKey, INVALID_KEY)) .chain(apply("getDoc")) - .chain(triggerEvent("CACHE:GET")); + .chain(triggerEvent("CACHE:GET")) + .chain(legacyGet("CACHE:GET")); // .chain(is(validResult, INVALID_RESULT)); /** diff --git a/packages/core/lib/cache/doc_test.js b/packages/core/lib/cache/doc_test.js index 05ad0f26..3a5c48da 100644 --- a/packages/core/lib/cache/doc_test.js +++ b/packages/core/lib/cache/doc_test.js @@ -1,5 +1,5 @@ // deno-lint-ignore-file no-unused-vars -import { assertEquals } from "../../dev_deps.js"; +import { assert, assertEquals } from "../../dev_deps.js"; import * as doc from "./doc.js"; @@ -15,7 +15,6 @@ const mockService = { const fork = (m) => () => { m.fork( (e) => { - console.log("ERROR", e.message); assertEquals(false, true); }, () => assertEquals(true, true), @@ -53,6 +52,110 @@ test( fork(doc.get("store", "key-1234").runWith({ svc: mockService, events })), ); +test("get cache doc - legacyGet", async (t) => { + await t.step("enabled", async (t) => { + await t.step( + "should passthrough a legacyGet response", + fork( + doc.get("foo", "key").map((res) => { + assert(res.hello); + }).runWith({ svc: mockService, events, isLegacyGetEnabled: true }), + ), + ); + + await t.step( + "should map to legacyGet response for backwards compatibility", + fork( + doc.get("foo", "key").map((res) => { + assert(res.hello); + }).runWith({ + svc: { + ...mockService, + getDoc({ store, key }) { + // NOT legacyGet response + return Promise.resolve({ ok: true, doc: { hello: "world" } }); + }, + }, + events, + isLegacyGetEnabled: true, + }), + ), + ); + + await t.step( + "should passthrough a hyper error shape", + fork( + doc.get("foo", "err") + .map((res) => { + assert(!res.ok); + }).runWith({ + svc: { + ...mockService, + getDoc({ store, key }) { + // NOT legacyGet response + return Promise.resolve({ ok: false, msg: "oops" }); + }, + }, + events, + isLegacyGetEnabled: true, + }), + ), + ); + }); + + await t.step("disabled", async (t) => { + await t.step( + "should passthrough a get response", + fork( + doc.get("foo", "key").map((res) => { + assert(res.ok); + assert(res.doc.hello); + }).runWith({ + svc: { + ...mockService, + getDoc({ store, key }) { + // NOT legacyGet response + return Promise.resolve({ ok: true, doc: { hello: "world" } }); + }, + }, + events, + isLegacyGetEnabled: false, + }), + ), + ); + + await t.step( + "should map to get response for forwards compatibility", + fork( + doc.get("foo", "key").map((res) => { + assert(res.ok); + assert(res.doc.hello); + }).runWith({ svc: mockService, events, isLegacyGetEnabled: false }), + ), + ); + + await t.step( + "should passthrough a hyper error shape", + fork( + doc.get("foo", "err") + .map((res) => { + assert(!res.ok); + }).runWith({ + svc: { + ...mockService, + getDoc({ store, key }) { + // NOT legacyGet response + return Promise.resolve({ ok: false, msg: "oops" }); + }, + }, + events, + isLegacyGetEnabled: false, + }), + ), + ); + }); +}); + test( "update cache document", fork( diff --git a/packages/core/lib/cache/mod.js b/packages/core/lib/cache/mod.js index 15a37e50..794ad5f5 100644 --- a/packages/core/lib/cache/mod.js +++ b/packages/core/lib/cache/mod.js @@ -39,10 +39,13 @@ export default function ({ cache, events }) { /** * @param {string} store * @param {string} key + * @param {boolean?} isLegacyGetEnabled * @returns {Async} + * + * TODO: LEGACY_GET: set to false on next major version. Then remove on next major version */ - const getDoc = (store, key) => - doc.get(store, key).runWith({ svc: cache, events }); + const getDoc = (store, key, isLegacyGetEnabled = true) => + doc.get(store, key).runWith({ svc: cache, events, isLegacyGetEnabled }); /** * @param {string} name diff --git a/packages/core/lib/data/doc.js b/packages/core/lib/data/doc.js index ff42f465..d017857d 100644 --- a/packages/core/lib/data/doc.js +++ b/packages/core/lib/data/doc.js @@ -1,23 +1,20 @@ import { cuid, R } from "../../deps.js"; -import { apply, is, of, triggerEvent } from "../utils/mod.js"; +import { apply, legacyGet, of, triggerEvent } from "../utils/mod.js"; const { defaultTo } = R; -// const INVALID_ID_MSG = 'doc id is not valid' -const INVALID_RESPONSE = "response is not valid"; - export const create = (db, doc) => of(doc) .map(defaultTo({})) .map((doc) => ({ db, id: doc._id || cuid(), doc })) .chain(apply("createDocument")) - .chain(triggerEvent("DATA:CREATE")) - .chain(is(validResponse, INVALID_RESPONSE)); + .chain(triggerEvent("DATA:CREATE")); export const get = (db, id) => of({ db, id }) .chain(apply("retrieveDocument")) - .chain(triggerEvent("DATA:GET")); + .chain(triggerEvent("DATA:GET")) + .chain(legacyGet("DATA:GET")); export const update = (db, id, doc) => of({ db, id, doc }) @@ -28,7 +25,3 @@ export const remove = (db, id) => of({ db, id }) .chain(apply("removeDocument")) .chain(triggerEvent("DATA:DELETE")); - -function validResponse() { - return true; -} diff --git a/packages/core/lib/data/doc_test.js b/packages/core/lib/data/doc_test.js index e20c61c3..af932da6 100644 --- a/packages/core/lib/data/doc_test.js +++ b/packages/core/lib/data/doc_test.js @@ -12,6 +12,7 @@ const mock = { return Promise.resolve({ ok: false }); } + // legacy get response return Promise.resolve({ _id: id }); }, updateDocument({ db, id, doc }) { @@ -103,6 +104,89 @@ test( ), ); +test("get document - legacyGet", async (t) => { + await t.step("enabled", async (t) => { + await t.step( + "should passthrough a legacyGet response", + fork( + doc.get("foo", "1").map((res) => { + assert(res._id); + }).runWith({ svc: mock, events, isLegacyGetEnabled: true }), + ), + ); + + await t.step( + "should map to legacyGet response for backwards compatibility", + fork( + doc.get("foo", "1").map((res) => { + assert(res._id); + }).runWith({ + svc: { + ...mock, + retrieveDocument({ db, id }) { + // NOT legacyGet response + return Promise.resolve({ ok: true, doc: { _id: id } }); + }, + }, + events, + isLegacyGetEnabled: true, + }), + ), + ); + + await t.step( + "should passthrough a hyper error shape", + fork( + doc.get("foo", "err") + .map((res) => { + assert(!res.ok); + }).runWith({ svc: mock, events, isLegacyGetEnabled: true }), + ), + ); + }); + + await t.step("disabled", async (t) => { + await t.step( + "should passthrough a get response", + fork( + doc.get("foo", "1").map((res) => { + assert(res.ok); + assert(res.doc._id); + }).runWith({ + svc: { + ...mock, + retrieveDocument({ db, id }) { + return Promise.resolve({ ok: true, doc: { _id: id } }); + }, + }, + events, + isLegacyGetEnabled: false, + }), + ), + ); + + await t.step( + "should map to get response for forwards compatibility", + fork( + doc.get("foo", "1").map((res) => { + assert(res.ok); + assert(res.doc._id); + }).runWith({ svc: mock, events, isLegacyGetEnabled: false }), + ), + ); + + await t.step( + "should passthrough a hyper error shape", + fork( + doc.get("foo", "err") + .map((res) => { + assert(!res.ok); + }).runWith({ svc: mock, events, isLegacyGetEnabled: false }), + ), + ); + }); +}); + test( "update document", fork( diff --git a/packages/core/lib/data/mod.js b/packages/core/lib/data/mod.js index 53620459..d37b689d 100644 --- a/packages/core/lib/data/mod.js +++ b/packages/core/lib/data/mod.js @@ -27,10 +27,13 @@ export default function ({ data, events }) { /** * @param {string} db * @param {string} id + * @param {boolean?} isLegacyGetEnabled * @returns {Async} + * + * TODO: LEGACY_GET: set to false on next major version. Then remove on next major version */ - const getDocument = (db, id) => - doc.get(db, id).runWith({ svc: data, events }); + const getDocument = (db, id, isLegacyGetEnabled = true) => + doc.get(db, id).runWith({ svc: data, events, isLegacyGetEnabled }); /** * @param {string} db diff --git a/packages/core/lib/utils/mod.js b/packages/core/lib/utils/mod.js index 30ed205b..68d6e793 100644 --- a/packages/core/lib/utils/mod.js +++ b/packages/core/lib/utils/mod.js @@ -85,3 +85,48 @@ export const triggerEvent = (event) => (data) => * constructor for an AsyncReader monad */ export const of = ReaderAsync.of; + +/** + * Given the result of a call into a service (data), + * if legacyGet is enabled, then map the result to + * the legacyGet shape. + * + * Otherwise, map the result to a "hyper shape". + * + * NOTE: this isn't full proof, due to conflation of + * types described in https://github.com/hyper63/hyper/issues/531 + * but it get's us most of the way there, and provides an avenue for adapters + * to gradually update to a "hyper shape" + * + * @param {string} service + * @returns {AsyncReader} + */ +export const legacyGet = (service) => (data) => + ask(({ isLegacyGetEnabled }) => { + if (isHyperErr(data)) return Async.Resolved(data); + + if (isLegacyGetEnabled) { + // Can use this to monitor usage of legacy + console.warn(`LEGACY_GET: ${service}`); + console.log(data); + /** + * If the adapter returned a legacy get shape, just return it. + * Otherwise, extract the doc from the response to create the legacy get shape. + * + * This will allows adapters to independently migrate to return a hyper shape, + * without breaking legacy for consumers that still want it enabled + */ + return Async.Resolved(data.doc || data); + } + + /** + * If the adapter returned a hyper shape, just return it. Otherwise, create + * the hyper shape. + * + * This will allow adapters to gradually migrate to return hyper shape, + * without breaking "non-legacy" for consumers that want legacy disabled + */ + return Async.Resolved( + data.ok && data.doc ? data : { ok: true, doc: data }, + ); + }).chain(lift); diff --git a/packages/core/mod_test.js b/packages/core/mod_test.js index cb3736c5..4d84d9c1 100644 --- a/packages/core/mod_test.js +++ b/packages/core/mod_test.js @@ -1,5 +1,8 @@ import opine from "https://x.nest.land/hyper-app-opine@2.0.0/mod.js"; -import dndb from "https://x.nest.land/hyper-adapter-dndb@2.0.0/mod.js"; +import { + default as pouchdb, + PouchDbAdapterTypes, +} from "https://x.nest.land/hyper-adapter-pouchdb@0.1.6/mod.js"; import memory from "https://x.nest.land/hyper-adapter-memory@1.2.6/mod.js"; import { superdeno } from "https://deno.land/x/superdeno@4.7.2/mod.ts"; @@ -13,7 +16,10 @@ Deno.env.set("DENO_ENV", "test"); const config = { app: opine, adapters: [ - { port: "data", plugins: [dndb("./data/foo.db")] }, + { + port: "data", + plugins: [pouchdb({ storage: PouchDbAdapterTypes.memory })], + }, { port: "cache", plugins: [memory()] }, ], middleware: [], diff --git a/packages/core/scripts/test.sh b/packages/core/scripts/test.sh index 24862417..3006bbcc 100755 --- a/packages/core/scripts/test.sh +++ b/packages/core/scripts/test.sh @@ -2,5 +2,5 @@ deno lint && \ deno fmt --check && \ -deno test --unstable --allow-env --allow-read --allow-net --no-check=remote mod_test.js lib/**/*_test.js +deno test --unstable --allow-env --allow-read --allow-sys --allow-net --no-check=remote mod_test.js lib/**/*_test.js