Skip to content

Commit

Permalink
Merge pull request #1 from TheAppleFreak/v2.0.0
Browse files Browse the repository at this point in the history
Added support for param transformations
  • Loading branch information
TheAppleFreak authored Jan 24, 2022
2 parents 14448d2 + b7238ca commit fc58cfb
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 45 deletions.
16 changes: 11 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# CHANGELOG

# 1.0.2 (2022/1/17)
## 2.0.0 (2022/1/24)

* Fixed package.json to actually include the build this time, for real
* Added transformation support to the validator itself, which will allow for features such as defaults and different passthrough modes for unrecognized parameters. This has several side effects that are listed below.
* **BREAKING CHANGE** - Removed the `compiledValidatorOnly` nested options object. The two settings, `strip` and `passthrough`, are now present in the top level options object.
* **BREAKING CHANGE** - The default validation mode is now `strip` as opposed to `passthrough`.

# 1.0.1 (2022/1/17)
## 1.0.2 (2022/1/17)

* Fixed package.json to actually include the build this time
* Fixed package.json and continuous integration scripts to actually include the build this time, for real

# 1.0.0 (2022/1/17)
## 1.0.1 (2022/1/17)

* Fixed package.json and continuous integration scripts to actually include the build this time

## 1.0.0 (2022/1/17)

* Initial release
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,18 @@ const broker = new ServiceBroker({

One of Zod's main features is how it can infer TypeScript types from a schema. To simplify the usage of this, there is a convenience utility called `ZodParams` that allows for easy access to the necessary data.

The `ZodParams` constructor takes one to two arguments, `schema` and optionally `options`.
The `ZodParams` constructor takes one or two arguments, `schema` and optionally `options`.

* `schema` - This is a schema object that gets passed directly into `z.object`. [For all available schema options, please look at the Zod documentation.](https://github.com/colinhacks/zod#defining-schemas) Please note that transformation options (such as `default`) are currently unavailable in the main validator; please read the below note for more information.
* `options` - This provides access to some of the different functions available on a standard Zod object. All booleans default to `false`.
* `schema` - This is a schema object that gets passed directly into `z.object`. [For all available schema options, please look at the Zod documentation.](https://github.com/colinhacks/zod#defining-schemas)
* `options` - This provides access to some of the different functions available on a standard Zod object. All booleans default to `false` except for `strip`, which is implicitly set to `true`.
* `partial` (boolean) - Shallowly makes all properties optional. ([docs](https://github.com/colinhacks/zod#partial))
* `deepPartial` (boolean) - Deeply makes all properties optional. ([docs](https://github.com/colinhacks/zod#deepPartial))
* `strict` (boolean) - Throws an error if unrecognized keys are present. ([docs](https://github.com/colinhacks/zod#strict))
* `catchall` (Zod validator) - Validates all unknown keys against this schema. Obviates `strict`. ([docs](https://github.com/colinhacks/zod#catchall))
* `compiledValidatorOnly` - This provides access to settings that transform the inputs that is currently not supported natively in the validator itself. Please read the note below for more information on this.
* `passthrough` (boolean) - Passes through unrecognized keys. Equivalent to the current default behavior in this validator.
* `strip` (boolean) - Removes unrecognized keys from the parsed input. Equivalent to Zod's default behavior.
* `strip` (boolean) - Removes unrecognized keys from the parsed input. This is Zod's default behavior and this validator's default behavior. Mutually exclusive with `passthrough` and `strict`, and will override them if set. ([docs](https://github.com/colinhacks/zod#strip))
* `strict` (boolean) - Throws an error if unrecognized keys are present. Mutually exclusive with `passthrough` and `strip`. ([docs](https://github.com/colinhacks/zod#strict))
* `passthrough` (boolean) - Passes through unrecognized keys. Mutually exclusive with `strict` and `strip`. ([docs](https://github.com/colinhacks/zod#passthrough))
* `catchall` (Zod validator) - Validates all unknown keys against this schema. Obviates `strict`, `passthrough`, and `strip`. ([docs](https://github.com/colinhacks/zod#catchall))

**Important note**: As of this writing (v1.0.0), there is currently no support for transformation options (`strip`, `passthrough`, and `default` primarily), meaning that this validator only checks to see whether the shape of the incoming data is valid and then sends it to the action as received. I'm not sure how the default implementation of fastest-validator performs its transformations; this section will be updated if and when that support can be added. For transformation options in the interim, you can access the compiled validator within the action and run `ctx.params` through it again to properly transform the input. There will be a major version update (currently v2.0.0) once it is added.
As of v2.0.0, support for object transformations is present, allowing for the use of features such as [preprocessing](https://github.com/colinhacks/zod#preprocess), [refinements](https://github.com/colinhacks/zod#refine), [transforms](https://github.com/colinhacks/zod#transform), and [defaults](https://github.com/colinhacks/zod#default).

Once constructed, there are four properties exposed on the `ZodParams` object.

Expand Down Expand Up @@ -95,6 +94,19 @@ broker.createService({

...

broker.call<returnType, typeof simpleValidator.call>({ string: "yes", number: 42 }); // calls successfully
broker.call<returnType, typeof complexValidator.call>({ object: { nestedString: "not optional", nestedBoolean: false }, unrecognizedKey: 69 }); // throws ValidationError
broker.call<
ReturnType,
typeof simpleValidator.call
>({ string: "yes", number: 42 }); // calls successfully

broker.call<
ReturnType,
typeof complexValidator.call
>({
object: {
nestedString: "not optional",
nestedBoolean: false
},
unrecognizedKey: 69
}); // throws ValidationError
```
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/src/**/*.test.ts"]
testMatch: ["**/test/**/*.ts"]
};
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "moleculer-zod-validator",
"version": "1.0.2",
"version": "2.0.0",
"description": "A validator for the Moleculer microservice framework to allow the use of Zod.",
"author": "TheAppleFreak <[email protected]>",
"main": "build/index.js",
Expand All @@ -24,7 +24,7 @@
"typescript"
],
"files": [
"build/",
"build/**/*",
"CHANGELOG.md"
],
"license": "MIT",
Expand Down
70 changes: 70 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Performs a deep mutation of an existing object to equal a given source object.
*
* Adapted from this package: https://github.com/jeremyhewett/mutate-object
* @param {object} destination - The object to mutate
* @param {object} source - The object that the destination should be mutated to equal
* @param {boolean} [preserveRootProps=true] - Whether undefined props on the root level should be deleted
*/
export function mutateObject(
destination: any,
source: any,
preserveRootProps: boolean = false,
): void {
if (isEnumerable(source)) {
const isSrcArray = Array.isArray(source);
destination =
(isEnumerable(destination) &&
Array.isArray(destination) === isSrcArray &&
destination) ||
(isSrcArray ? [] : {});

if (!preserveRootProps) {
isSrcArray
? cleanArray(destination, source)
: cleanObject(destination, source);
}

if (isSrcArray) {
source.map((value, i) => {
if (destination.length < i + 1) {
destination.push();
}
destination[i] = mutateObject(destination[i], value, false);
});
} else {
for (let i in source) {
if (source.hasOwnProperty(i)) {
destination[i] = mutateObject(
destination[i],
source[i],
false,
);
}
}
}

return destination;
}

return source;
}

function isEnumerable(value: unknown) {
return value !== null && typeof value === "object";
}

function cleanArray(oldArray: any[], newArray: any[]) {
if (newArray.length < oldArray.length) {
oldArray.splice(newArray.length, oldArray.length - newArray.length);
}
return oldArray;
}

function cleanObject(oldObj: any, newObj: any) {
for (let prop in oldObj) {
if (oldObj.hasOwnProperty(prop) && !newObj.hasOwnProperty(prop)) {
delete oldObj[prop];
}
}
}
16 changes: 6 additions & 10 deletions src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,18 @@ export class ZodParams<Schema extends z.ZodRawShape> {
deepPartial: false,
strict: false,
catchall: undefined,
compiledValidatorOnly: {
strip: false,
passthrough: false,
},
strip: false,
passthrough: false,
} as z.infer<typeof ZodParamsOptions>,
options,
);

const opts = ZodParamsOptions.parse(options);
let validator;

if (opts.compiledValidatorOnly!.strip) {
if (opts.strip) {
validator = z.object(this._rawSchema).strip();
} else if (opts.compiledValidatorOnly!.passthrough) {
} else if (opts.passthrough) {
validator = z.object(this._rawSchema).passthrough();
} else if (opts.strict) {
validator = z.object(this._rawSchema).strict();
Expand Down Expand Up @@ -78,10 +76,8 @@ const ZodParamsOptions = z
deepPartial: z.boolean(),
strict: z.boolean(),
catchall: z.any(),
compiledValidatorOnly: z.object({
passthrough: z.boolean(),
strip: z.boolean(),
}),
passthrough: z.boolean(),
strip: z.boolean(),
})
.deepPartial();

Expand Down
22 changes: 11 additions & 11 deletions src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { z } from "zod";
import { Errors, Validators } from "moleculer";

import type { ZodParamsOptionsType } from ".";
import type { ZodParamsOptionsType } from "./params";

import { mutateObject } from "./helpers";

export class ZodValidator extends Validators.Base {
constructor() {
Expand All @@ -21,22 +23,14 @@ export class ZodValidator extends Validators.Base {

let compiled;

// Uncomment when I figure out how to apply transformations from the validator
/*
if (opts.strip) {
compiled = z.object(schema).strip();
} else if (opts.strict) {
compiled = z.object(schema).strict();
} else if (opts.passthrough) {
compiled = z.object(schema).passthrough();
} else {
compiled = z.object(schema);
}
*/
if (opts.strict) {
compiled = z.object(schema).strict();
} else {
compiled = z.object(schema);
compiled = z.object(schema).strip();
}
if (opts.partial) {
compiled = compiled.partial();
Expand All @@ -48,7 +42,13 @@ export class ZodValidator extends Validators.Base {
compiled = compiled.catchall(opts.catchall);
}

compiled.parse(params);
const results = compiled.parse(params);

// params is passed by reference, meaning that we can apply transformations
// to what gets passed to the validator from here. However, simply setting
// it to a new value will just change the pointer to the object in function
// scope, so we need to actually mutate the original ourselves.
mutateObject(params, results, false);

return true;
} catch (err) {
Expand Down
60 changes: 58 additions & 2 deletions src/index.test.ts → test/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ServiceBroker, Context, Errors } from "moleculer";
import { ZodParams, ZodValidator } from ".";
import { ZodParams, ZodValidator } from "../src";
import { z } from "zod";

let broker: ServiceBroker;
Expand Down Expand Up @@ -51,6 +51,24 @@ const strictParams = new ZodParams(
strict: true,
},
);
const passthroughParams = new ZodParams(
{
stringProp: z.string(),
},
{
passthrough: true,
},
);
const stripParams = new ZodParams(
{
stringProp: z.string(),
},
{
passthrough: true,
strip: true, // should override passthrough
},
);

const catchallParams = new ZodParams(
{
stringProp: z.string(),
Expand Down Expand Up @@ -118,6 +136,18 @@ beforeAll(() => {
return ctx.params;
},
},
passthroughParams: {
params: passthroughParams.schema,
async handler(ctx: Context<typeof passthroughParams.context>) {
return ctx.params;
},
},
stripParams: {
params: stripParams.schema,
async handler(ctx: Context<typeof stripParams.context>) {
return ctx.params;
},
},
catchallParams: {
params: catchallParams.schema,
async handler(ctx: Context<typeof catchallParams.context>) {
Expand Down Expand Up @@ -248,7 +278,7 @@ describe("unrecognized parameters", () => {
expect(data).toBeUndefined();
});

test("calling oneParam with unrecognized parameters (equivalent to passthrough() from Zod)", async () => {
test("calling oneParam with unrecognized parameters (equivalent to strip: true)", async () => {
const data = await broker.call<
typeof oneParam.context,
typeof oneParam.call
Expand All @@ -258,9 +288,35 @@ describe("unrecognized parameters", () => {
unrecognizedParam: null,
});

expect(data).toEqual({ stringProp: "yes" });
});

test("calling passthroughParams with unrecognized parameters", async () => {
const data = await broker.call<
typeof passthroughParams.context,
typeof passthroughParams.call
>("test.passthroughParams", {
stringProp: "yes",
// @ts-ignore
unrecognizedParam: null,
});

expect(data).toEqual({ stringProp: "yes", unrecognizedParam: null });
});

test("calling stripParams with unrecognized parameters", async () => {
const data = await broker.call<
typeof stripParams.context,
typeof stripParams.call
>("test.stripParams", {
stringProp: "yes",
// @ts-ignore
unrecognizedParam: null,
});

expect(data).toEqual({ stringProp: "yes" });
});

test("unrecognized parameters with strict: true", async () => {
try {
await broker.call<
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"include": [
"src/**/*.ts"
],
],
"exclude": [
"node_modules/**"
]
Expand Down

0 comments on commit fc58cfb

Please sign in to comment.