Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for AJV Keywords + conditional schemas #8

Open
wants to merge 2 commits into
base: feat/esm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"accepts": "^1.3.8",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0",
"busboy": "^1.6.0",
"connect": "^3.7.0",
"content-type": "^1.0.4",
Expand Down
24 changes: 17 additions & 7 deletions src/plugin.router/router.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ajv from "ajv"
import addFormats from "ajv-formats"
import addKeywords from "ajv-keywords"
import { pathToRegexp } from "path-to-regexp"
import { count, reduce, findWith, merge, pluck, is, isEmpty } from "@asd14/m"

Expand Down Expand Up @@ -35,6 +36,8 @@ export default {
],
})

addKeywords(ajv, ["regexp", "transform"])

const { default: defaultRouteSchema } = await import("./default.schema.js")
const routes = []

Expand Down Expand Up @@ -100,6 +103,19 @@ export default {
}) => {
const keys = []

// If conditional schemas or other complex scenarios are found,
// defer to the schema itself instead of trying to expect certain keys
const schemaToValidate =
is(schema.properties) && is(schema.if)
? pluck(["properties", "type", "if", "then", "else"])(schema)
: {
type: "object",
properties: merge(
defaultRouteSchema,
pluck(["headers", "params", "query", "body"])(schema)
),
}

routes.push({
method,
path,
Expand All @@ -109,13 +125,7 @@ export default {
...rest,
pathParamsKeys: keys,
pathRegExp: pathToRegexp(path, keys),
validate: ajv.compile({
type: "object",
properties: merge(
defaultRouteSchema,
pluck(["headers", "params", "query", "body"])(schema)
),
}),
validate: ajv.compile(schemaToValidate),
})
},

Expand Down
56 changes: 54 additions & 2 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ test("blocks :: init with defaults", async t => {
routes: [
await import("./routes/no-schema.route.js"),
await import("./routes/with-schema.route.js"),
await import("./routes/with-keywords.route.js"),
await import("./routes/with-conditional-schema.route.js"),
await import("./routes/no-authenticate.route.js"),
await import("./routes/no-authorize.route.js"),
await import("./routes/dont-authenticate.route.js"),
Expand All @@ -57,8 +59,8 @@ test("blocks :: init with defaults", async t => {

t.deepEquals(
plugins.Router.count(),
12,
"given [11 custom routes] should [load default /ping and all custom]"
14,
"given [13 custom routes] should [load default /ping and all custom]"
)

t.deepEquals(
Expand Down Expand Up @@ -225,6 +227,56 @@ test("blocks :: init with defaults", async t => {
"given [form encoded body and content type] should [parse body with qs]"
)

t.deepEqual(
await POST(`${API_URL}/with-keywords`, {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: "title=%20UP%20CASED%20&foo=foobar",
}),
{
message: "Hello Plugin World!",
query: {},
params: {},
body: { foo: "foobar", title: "up cased" },
},
"given [form encoded with custom keywords] should [transform and validate data inside schema with added functionalities from ajv-keywords]",
)

t.deepEqual(
await POST(`${API_URL}/with-conditional-schema`, {
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-api-version": "1.0.0",
},
body: "foo=1",
}),
{
message: "Hello Plugin World!",
query: {},
params: {},
body: { foo: "1" },
},
"given [a versioned schema based on header] should [use one schema or the other based on the API header value]",
)

t.deepEqual(
await POST(`${API_URL}/with-conditional-schema`, {
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-api-version": "2.4.0",
},
body: "foo=1",
}),
{
message: "Hello Plugin World!",
query: {},
params: {},
body: { foo: 1 },
},
"given [a versioned schema based on header] should [use one schema or the other based on the API header value]",
)

t.deepEqual(
await MULTIPART(`${API_URL}/upload`, {
body: {
Expand Down
19 changes: 19 additions & 0 deletions tests/routes/with-conditional-schema.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import schema from "./with-conditional-schema.schema.js"

export default {
method: "POST",
path: "/with-conditional-schema",
schema,
authenticate: (/* plugins */) => (/* req */) => true,
authorize: (/* plugins */) => (/* req */) => true,
action:
({ Good }) =>
({ ctx }) => {
return {
message: Good.getMessage(),
params: ctx.params,
query: ctx.query,
body: ctx.body,
}
},
}
56 changes: 56 additions & 0 deletions tests/routes/with-conditional-schema.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export default {
type: "object",
properties: {
headers: {
type: "object",
required: ["x-content-type"],
properties: {
"x-content-type": {
enum: ["application/x-www-form-urlencoded"],
},
"x-api-version": { type: "string" },
},
},
},
if: {
properties: {
headers: {
type: "object",
properties: {
"x-api-version": {
type: "string",
// <= 2.3.0
pattern:
"^([01]\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)|2\\.[0-2]\\.(?:0|[1-9]\\d*)|2\\.3\\.0)$",
},
},
},
},
},
then: {
properties: {
body: {
type: "object",
// additionalProperties: false,
properties: {
foo: {
type: "string",
},
},
},
},
},
else: {
properties: {
body: {
type: "object",
// additionalProperties: false,
properties: {
foo: {
type: "number",
},
},
},
},
},
}
19 changes: 19 additions & 0 deletions tests/routes/with-keywords.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import schema from "./with-keywords.schema.js"

export default {
method: "POST",
path: "/with-keywords",
schema,
authenticate: (/* plugins */) => (/* req */) => true,
authorize: (/* plugins */) => (/* req */) => true,
action:
({ Good }) =>
({ ctx }) => {
return {
message: Good.getMessage(),
params: ctx.params,
query: ctx.query,
body: ctx.body,
}
},
}
26 changes: 26 additions & 0 deletions tests/routes/with-keywords.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default {
headers: {
type: "object",
required: ["x-content-type"],
properties: {
"x-content-type": {
enum: ["application/x-www-form-urlencoded"],
},
},
},

body: {
type: "object",
// additionalProperties: false,
properties: {
foo: {
type: "string",
regexp: "/foo/i",
},
title: {
type: "string",
transform: ["trim", "toLowerCase"],
},
},
},
}