How to allow null, but forbid undefined? - class-validator

e.g. for database rows, we may need nullable properties that must not be undefined:
class DbRow {
#IsNumber()
id!: number;
#IsNumber()
numNullable!: number | null;
}
So numNullable can be a number or null - but it must never be undefined.
How can we express this in class-validator?
adding #Optional() does not work, because that would also allow undefined
I also had no luck with a custom validator

It turns out that this is possible by using conditional validation ValidateIf:
class DbRow {
#IsNumber()
id!: number;
#IsNumber()
#ValidateIf((object, value) => value !== null)
numNullable!: number | null;
}
Here is a stackblitz example

Here is my solution:
import { ValidationOptions, ValidateIf } from 'class-validator';
export function IsNullable(validationOptions?: ValidationOptions) {
return ValidateIf((_object, value) => value !== null, validationOptions);
}
Usage
import { plainToClass } from 'class-transformer';
import { IsNumber, validateSync } from 'class-validator';
import { IsNullable } from 'src/common/utils/is-nullable.decorator';
class SampleDto {
#IsNullable()
#IsNumber()
foo: number | null;
}
describe('IsNullable', () => {
it('should disable other validators when given property is null', () => {
expect(validateSync(plainToClass(SampleDto, { foo: null }))).toEqual([]);
});
it('should allow other validators to work when given property is not null', () => {
expect(validateSync(plainToClass(SampleDto, { foo: 1 }))).toEqual([]);
expect(validateSync(plainToClass(SampleDto, { foo: '1' }))[0].constraints.isNumber).toMatch('foo must be a number');
});
it('should not allow undefined', () => {
expect(validateSync(plainToClass(SampleDto, { foo: undefined })).length).toBeGreaterThan(0);
});
});

This is an extended version of IsOptional exported from class-validator.
import {
ValidationOptions,
ValidateIf,
IsOptional as IsOptionalValidator,
} from 'class-validator';
/**
* Checks if value is missing and if so, ignores all validators.
*
* #param nullable If `true`, all other validators will be skipped even when the value is `null`. `false` by default.
* #param validationOptions {#link ValidationOptions}
*
* #see IsOptional exported from `class-validator.
*/
export function IsOptional(
nullable = false,
validationOptions?: ValidationOptions,
) {
if (nullable) {
return IsOptionalValidator(validationOptions);
}
return ValidateIf((ob: any, v: any) => {
return v !== undefined;
}, validationOptions);
}

That's the limitation of the library, it doesn't allow condition branching.
The best way is to write your own validator that allows only nulls.

Related

correctly set params in expo bottomtabs

Having a hard time understanding this newest expo bottom tabs
I dont see an initital params on the node_module for the bottomtabs or any params property... has anyone done this? essentially we have component for two bottom tabs and a different effect depending on that tab.
So 1. Can we pass Params into bottomTabs? 2. if so how?
error we get with TS is:
The expected type comes from property 'initialParams' which is declared here on type 'IntrinsicAttributes & RouteConfig<RootTabParamList, "TabThree", TabNavigationState, BottomTabNavigationOptions, BottomTabNavigationEventMap>'
<BottomTab.Screen
name="Episodes"
component={EpisodesScreen}
initialParams={{
type: "episodes",
}}
options={{
title: 'Episodes',
tabBarIcon: ({ color }) => <TabBarFeatherIcon name="headphones" color={color} />,
}}
/>
<BottomTab.Screen
name="TabThree"
component={EpisodesScreen}
initialParams={{
type: "quickGuides",
displayType: "grid",
}}
from the node_module::
import {
createNavigatorFactory,
DefaultNavigatorOptions,
ParamListBase,
TabActionHelpers,
TabNavigationState,
TabRouter,
TabRouterOptions,
useNavigationBuilder,
} from '#react-navigation/native';
import * as React from 'react';
import warnOnce from 'warn-once';
import type {
BottomTabNavigationConfig,
BottomTabNavigationEventMap,
BottomTabNavigationOptions,
} from '../types';
import BottomTabView from '../views/BottomTabView';
type Props = DefaultNavigatorOptions<
ParamListBase,
TabNavigationState<ParamListBase>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap
> &
TabRouterOptions &
BottomTabNavigationConfig;
function BottomTabNavigator({
initialRouteName,
backBehavior,
children,
screenListeners,
screenOptions,
sceneContainerStyle,
...restWithDeprecated
}: Props) {
const {
// #ts-expect-error: lazy is deprecated
lazy,
// #ts-expect-error: tabBarOptions is deprecated
tabBarOptions,
...rest
} = restWithDeprecated;
let defaultScreenOptions: BottomTabNavigationOptions = {};
if (tabBarOptions) {
Object.assign(defaultScreenOptions, {
tabBarHideOnKeyboard: tabBarOptions.keyboardHidesTabBar,
tabBarActiveTintColor: tabBarOptions.activeTintColor,
tabBarInactiveTintColor: tabBarOptions.inactiveTintColor,
tabBarActiveBackgroundColor: tabBarOptions.activeBackgroundColor,
tabBarInactiveBackgroundColor: tabBarOptions.inactiveBackgroundColor,
tabBarAllowFontScaling: tabBarOptions.allowFontScaling,
tabBarShowLabel: tabBarOptions.showLabel,
tabBarLabelStyle: tabBarOptions.labelStyle,
tabBarIconStyle: tabBarOptions.iconStyle,
tabBarItemStyle: tabBarOptions.tabStyle,
tabBarLabelPosition:
tabBarOptions.labelPosition ??
(tabBarOptions.adaptive === false ? 'below-icon' : undefined),
tabBarStyle: [
{ display: tabBarOptions.tabBarVisible ? 'none' : 'flex' },
defaultScreenOptions.tabBarStyle,
],
});
(
Object.keys(defaultScreenOptions) as (keyof BottomTabNavigationOptions)[]
).forEach((key) => {
if (defaultScreenOptions[key] === undefined) {
// eslint-disable-next-line #typescript-eslint/no-dynamic-delete
delete defaultScreenOptions[key];
}
});
warnOnce(
tabBarOptions,
`Bottom Tab Navigator: 'tabBarOptions' is deprecated. Migrate the options to
'screenOptions' instead.\n\nPlace the following in 'screenOptions' in your code to keep
current behavior:\n\n${JSON.stringify(
defaultScreenOptions,
null,
2
)}\n\nSee https://reactnavigation.org/docs/bottom-tab-navigator#options for more
details.`
);
}
if (typeof lazy === 'boolean') {
defaultScreenOptions.lazy = lazy;
warnOnce(
true,
`Bottom Tab Navigator: 'lazy' in props is deprecated. Move it to 'screenOptions'
instead.\n\nSee https://reactnavigation.org/docs/bottom-tab-navigator/#lazy for more
details.`
);
}
const { state, descriptors, navigation, NavigationContent } =
useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap
>(TabRouter, {
initialRouteName,
backBehavior,
children,
screenListeners,
screenOptions,
defaultScreenOptions,
});
return (
<NavigationContent>
<BottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
sceneContainerStyle={sceneContainerStyle}
/>
</NavigationContent>
);
}
export default createNavigatorFactory<
TabNavigationState<ParamListBase>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap,
typeof BottomTabNavigator
>(BottomTabNavigator);
only way i found to get my componenet to render on two different routes from the bottom tabs is to use the useNavigationState
import { useNavigationState } from "#react-navigation/native"
made a constant to check the route name and then on use effect we check the case...
const screenName = useNavigationState((state) =>
state.routes[state.index].name)
const type = screenName
useEffect(() => {
switch (type) {
case "Episodes":
setTitle("Episodes")
setIsLoading(false)
break
case "quickGuides":
setTitle("Quick Guides")
setIsLoading(false)
break
}
}, [])

getted data is only null in apollo-client / apollo-server & useSubscription

I try use pubsub in apollo server & apollo client. but subscribed data is only null.
client dependency
"#apollo/react-hooks": "^3.1.5",
"apollo-boost": "^0.4.9",
"apollo-link-ws": "^1.0.20",
"graphql": "^15.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"styled-components": "^5.1.1",
"subscriptions-transport-ws": "^0.9.16",
"typescript": "~3.7.2"
server dependency
"apollo-server": "^2.14.1",
"graphql": "^15.0.0",
"merge-graphql-schemas": "^1.7.8",
"ts-node": "^8.10.2",
"tsconfig-paths": "^3.9.0",
"typescript": "^3.9.3"
// apolloClient.ts
import { ApolloClient, HttpLink, InMemoryCache, split } from 'apollo-boost'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/graphql',
options: {
reconnect: true
}
})
const httpLink = new HttpLink({
uri: 'http://localhost:4000'
})
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
)
const cache = new InMemoryCache()
const client = new ApolloClient({
cache: cache,
link: link,
})
export default client
// subscribe.ts
const ON_PUT_UNIT = gql`
subscription onPutUnit($code: String!) {
onPutUnit(code: $code)
}
`
const onPutResult = useSubscription(
ON_PUT_UNIT,
{ variables: {
code: code,
}}
)
// in is only null!!
console.log('subscribe', onPutResult)
-server-
onPutUnit.ts
type Subscription {
onPutUnit(code: String!): Room
}
import { pubsub } from '#src/index'
const { withFilter } = require('apollo-server')
export default {
Subscription: {
onPutUnit: {
subscribe: withFilter(
() => pubsub.asyncIterator(['PUT_UNIT']),
(payload: any, variables: any) => {
// no problem in payload & variable data
return payload.code === variables.code
}
)
}
},
}
putUnit.ts
type Mutation {
putUnit(code: String!, x: Int!, y: Int!, userName: String!): Room!
}
export default {
Mutation: {
putUnit: async (_: any, args: args) => {
const { code, x, y, userName } = args
const room = findRoom(code)
console.log(room) // no problem. normal data.
pubsub.publish('PUT_UNIT', room)
return room
},
},
}
Is it some problem? subscribe event is normally reached to client when publish. but data is is only null. I can't fine the reason.
You only specified a subscribe function for onPutUnit, without specifying a resolve function. That means the field utilizes the default resolver.
The default resolver just looks for a property with the same name as the field on the parent object (the first parameter passed to the resolver) and returns that. If there is no property on the parent object with the same name as the field, then the field resolves to null. The parent object is the value the parent field resolved to. For example, if we have a query like this:
{
user {
name
}
}
whatever the resolver for user returns will be the parent value provided to the resolver for name (if user returns a Promise, it's whatever the Promise resolved to).
But what about user? It has no parent field because it's a root field. In this case, user is passed the rootValue you set when initializing the ApolloServer (or {} if you didn't).
With subscriptions, this works a bit differently because whatever value you publish is actually passed to the resolver as the root value. That means you can take advantage of the default resolver by publishing an object with a property that matches the field name:
pubsub.publish('PUT_UNIT', { onPutUnit: ... })
if you don't do that, though, you'll need to provide a resolve function that transforms the payload you published. For example, if we do:
pubsub.publish('PUT_UNIT', 'FOOBAR')
Then our resolver map needs to look something like this:
const resolvers = {
Subscription: {
onPutUnit: {
subscribe: ...,
resolve: (root) => {
console.log(root) // 'FOOBAR'
// return whatever you want onPutUnit to resolve to
}
}
},
}

How to mock a call to logger.warn?

I'm practicing test-first development and I want to ensure that method in a class always calls my logger at the warn level with a message. My class is defined like so:
import { log4js } from '../config/log4js-config'
export const logger = log4js.getLogger('myClass')
class MyClass {
sum(numbers) {
const reducer = (accumulator, currentValue) => accumulator + currentValue
const retval = numbers.reduce(reducer))
if (retval < 0) {
logger.warn('The sum is less than zero!')
}
return retval
}
}
const myClass = new MyClass()
export { myClass }
My test looks like this:
import { myClass, logger } from './MyClass'
import { log4js } from '../config/log4js-config'
jest.mock('log4js')
describe('MyClass', () => {
it('logs a warn-level message if sum is negative', () => {
logger.warn = jest.fn()
logger._log = jest.fn()
myClass.sum([0, -1])
expect(logger.warn).toHaveBeenCalled() // <--- fails
expect(logger._log).toHaveBeenCalled() // <--- fails
})
})
I've also tried to mock log4js.Logger._log in the setup but that didn't seem to work either. 😕 Any suggestions are appreciated!
The thing with mocking is that you need to provide the mock, simplest method for me is through the mock factory. However i would recomend also some refactoring:
import { getLogger } from 'log4js'
export const logger = getLogger('myClass')
logger.level = 'debug'
// export the class itself to avoid memory leaks
export class MyClass {
// would consider even export just the sum function
sum(numbers) {
const reducer = (accumulator, currentValue) => accumulator + currentValue
const retval = numbers.reduce(reducer))
if (retval < 0) {
logger.warn('The sum is less than zero!')
}
return retval
}
}
import log4js from 'log4js';
import { MyClass } from "./class";
jest.mock('log4js', () => {
// using the mock factory we mimic the library.
// this mock function is outside the mockImplementation
// because we want to check the same mock in every test,
// not create a new one mock every log4js.getLogger()
const warn = jest.fn()
return {
getLogger: jest.fn().mockImplementation(() => ({
level: jest.fn(),
warn,
})),
}
})
beforeEach(() => {
// reset modules to avoid leaky scenarios
jest.resetModules()
})
// this is just some good habits, if we rename the module
describe(MyClass, () => {
it('logs a warn-level message if sum is negative', () => {
const myClass = new MyClass()
myClass.sum([0, -1])
// now we can check the mocks
expect(log4js.getLogger).toHaveBeenCalledTimes(1) // <--- passes
// check exactly the number of calls to be extra sure
expect(log4js.getLogger().warn).toHaveBeenCalledTimes(1) // <--- passes
})
})
Maybe simply spying on logger methods can do the trick
import { myClass, logger } from './MyClass'
describe('MyClass', () => {
it('logs a warn-level message if sum is negative', () => {
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
const _logSpy = jest.spyOn(logger, '_log').mockImplementation(() => {});
myClass.sum([0, -1])
expect(warnSpy).toHaveBeenCalled()
expect(_logSpy).toHaveBeenCalled()
})
})

Webpack 4 Plugin: Add module and get result from loader

I am making a Webpack 4 plugin for fun and to try to understand its internals. The idea is simple:
Parse an HTML template file into a tree;
Get the asset paths from <img src="..."> and <link href="...">;
Add the assets to dependencies to load them through the file-loader;
Get the path emitted from file-loader(which might include a hash)and fix the nodes in the tree;
Emit the final HTML string into a file.
So far, I am stuck at step 4. Parsing the template and extracting the asset paths was easy thanks to parse5, to load the assets, I used the PrefetchPlugin but now I don't know how to get the result from file-loader.
I need to load the result because it generates a hash and might change the location of the asset:
{
exclude: /\.(css|jsx?|mjs)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]?[sha512:hash:base64:8]`',
},
}],
}
Not only that, but I want to use the url-loader later which might generate the asset encoded. I am trying to get the result from the loader at tapAfterCompile.
The current source code for the plugin is as follows:
import debug from 'debug'
import prettyFormat from 'pretty-format'
import validateOptions from 'schema-utils'
import {dirname, resolve} from 'path'
import {html as beautifyHtml} from 'js-beautify'
import {minify as minifyHtml} from 'html-minifier'
import {parse, serialize} from 'parse5'
import {PrefetchPlugin} from 'webpack'
import {readFileSync} from 'fs'
let log = debug('bb:config:webpack:plugin:html')
const PLUGIN_NAME = 'HTML Plugin'
/**
* This schema is used to validate the plugin’s options, right now, all it does
* is requiring the template property.
*/
const OPTIONS_SCHEMA = {
additionalProperties: false,
type: 'object',
properties: {
minify: {
type: 'boolean',
},
template: {
type: 'string',
},
},
required: ['template'],
}
/**
* Extract an attribute’s value from the node; Returns undefined if the
* attribute is not found.
*/
function getAttributeValue(node, attributeName) {
for (let attribute of node.attrs) {
if (attribute.name === attributeName)
return attribute.value
}
return undefined
}
/**
* Update a node’s attribute value.
*/
function setAttributeValue(node, attributeName, value) {
for (let attribute of node.attrs) {
if (attribute.name === attributeName)
attribute.value = value
}
}
/**
* Recursively walks the parsed tree. It should work in 99.9% of the cases but
* it needs to be replaced with a non recursive version.
*/
function * walk(node) {
yield node
if (!node.childNodes)
return
for (let child of node.childNodes)
yield * walk(child)
}
/**
* Actual Webpack plugin that generates an HTML from a template, add the script
* bundles and and loads any local assets referenced in the code.
*/
export default class SpaHtml {
/**
* Options passed to the plugin.
*/
options = null
/**
* Parsed tree of the template.
*/
tree = null
constructor(options) {
this.options = options
validateOptions(OPTIONS_SCHEMA, this.options, PLUGIN_NAME)
}
/**
* Webpack will call this method to allow the plugin to hook to the
* compiler’s events.
*/
apply(compiler) {
let {hooks} = compiler
hooks.afterCompile.tapAsync(PLUGIN_NAME, this.tapAfterCompile.bind(this))
hooks.beforeRun.tapAsync(PLUGIN_NAME, this.tapBeforeRun.bind(this))
}
/**
* Return the extracted the asset paths from the tree.
*/
* extractAssetPaths() {
log('Extracting asset paths...')
const URL = /^(https?:)?\/\//
const TEMPLATE_DIR = dirname(this.options.template)
for (let node of walk(this.tree)) {
let {tagName} = node
if (!tagName)
continue
let assetPath
switch (tagName) {
case 'link':
assetPath = getAttributeValue(node, 'href')
break
case 'img':
assetPath = getAttributeValue(node, 'src')
break
}
// Ignore empty paths and URLs.
if (!assetPath || URL.test(assetPath))
continue
const RESULT = {
context: TEMPLATE_DIR,
path: assetPath,
}
log(`Asset found: ${prettyFormat(RESULT)}`)
yield RESULT
}
log('Done extracting assets.')
}
/**
* Returns the current tree as a beautified or minified HTML string.
*/
getHtmlString() {
let serialized = serialize(this.tree)
// We pass the serialized HTML through the minifier to remove any
// unnecessary whitespace that could affect the beautifier. When we are
// actually trying to minify, comments will be removed too. Options can be
// found in:
//
// https://github.com/kangax/html-minifier
//
const MINIFIER_OPTIONS = {
caseSensitive: false,
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
conservativeCollapse: false,
decodeEntities: true,
html5: true,
includeAutoGeneratedTags: false,
keepClosingSlash: false,
preserveLineBreaks: false,
preventAttributesEscaping: true,
processConditionalComments: false,
quoteCharacter: '"',
removeAttributeQuotes: true,
removeEmptyAttributes: true,
removeEmptyElements: false,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
useShortDoctype: true,
}
let {minify} = this.options
if (minify) {
// Minify.
serialized = minifyHtml(serialized, {
minifyCSS: true,
minifyJS: true,
removeComments: true,
...MINIFIER_OPTIONS,
})
} else {
// Beautify.
serialized = minifyHtml(serialized, MINIFIER_OPTIONS)
serialized = beautifyHtml(serialized, {
indent_char: ' ',
indent_inner_html: true,
indent_size: 2,
sep: '\n',
unformatted: ['code', 'pre'],
})
}
return serialized
}
/**
* Load the template and parse it using Parse5.
*/
parseTemplate() {
log('Loading template...')
const SOURCE = readFileSync(this.options.template, 'utf8')
log('Parsing template...')
this.tree = parse(SOURCE)
log('Done loading and parsing template.')
}
async tapAfterCompile(compilation, done) {
console.log()
console.log()
for (let asset of compilation.modules) {
if (asset.rawRequest == 'assets/logo.svg')
console.log(asset)
}
console.log()
console.log()
// Add the template to the dependencies to trigger a rebuild on change in
// watch mode.
compilation.fileDependencies.add(this.options.template)
// Emit the final HTML.
const FINAL_HTML = this.getHtmlString()
compilation.assets['index.html'] = {
source: () => FINAL_HTML,
size: () => FINAL_HTML.length,
}
done()
}
async tapBeforeRun(compiler, done) {
this.parseTemplate()
// Add assets to the compilation.
for (let {context, path} of this.extractAssetPaths()) {
new PrefetchPlugin(context, path)
.apply(compiler)
}
done()
}
}
Found the answer, after I loaded the dependencies, I can access the generated module's source:
// Index the modules generated in the child compiler by raw request.
let byRawRequest = new Map
for (let asset of compilation.modules)
byRawRequest.set(asset.rawRequest, asset)
// Replace the template requests with the result from modules generated in
// the child compiler.
for (let {node, request} of this._getAssetRequests()) {
if (!byRawRequest.has(request))
continue
const ASSET = byRawRequest.get(request)
const SOURCE = ASSET.originalSource().source()
const NEW_REQUEST = execAssetModule(SOURCE)
setResourceRequest(node, NEW_REQUEST)
log(`Changed: ${prettyFormat({from: request, to: NEW_REQUEST})}`)
}
And execute the module's source with a VM:
function execAssetModule(code, path) {
let script = new Script(code)
let exports = {}
let sandbox = {
__webpack_public_path__: '',
module: {exports},
exports,
}
script.runInNewContext(sandbox)
return sandbox.module.exports
}

ReactJs - test multiple calls in redux-saga with expectSaga

I'm using expectSaga ('redux-saga-test-plan') to test one of my sagas and I'm wondering how to test multiple calls made within the same saga.
Sagas.js
export function* fetchSomething(arg){
const response = yield call(executeFetch, arg);
if(response.status === 200){
// trigger success action
} else if (response.status >= 400){
const errResp = yield response.json();
const errorCode = yield call(sharedUtilToExtractErrors, errResp);
yield put(
{ type: 'FETCH_FAILED', errorMessage: UI_ERR_MSG, errorCode }
);
}
}
Unit test
import { expectSaga } from 'redux-saga-test-plan';
describe('fetchSomething', () => {
// positive paths
// ..
// negative paths
it('fetches something and with status code 400 triggers FETCH_FAILED with error message and extracted error code', () => {
const serverError = { message: 'BANG BANG BABY!' };
const koResponse = new Response(
JSON.stringify(serverError),
{ status: 400, headers: { 'Content-type': 'application/json' } }
);
return expectSaga(fetchSomething)
.provide(
{
call: () => koResponse,
call: () => serverError.message,
}
)
.put({
type: 'FETCH_FAILED', errorMessage: UI_ERR_MSG, serverError.message
})
.run();
})
})
Clearly having the "call" attribute twice in the same object passed in to provide() doesn't work but also calling provide() twice doesn't do the trick. Any suggestions?
Thanks
This is how you can provide multiple calls according to the documentation:
.provide([ // this external array is actually optional
[call(executeFetch, arg), koResponse],
[call(sharedUtilToExtractErrors, serverError), serverError.message],
])
or if you're lazy and don't want to specify the arguments:
import * as matchers from 'redux-saga-test-plan/matchers';
.provide(
[matchers.call.fn(executeFetch), koResponse],
[matchers.call.fn(sharedUtilToExtractErrrors), serverError.message],
)
Neither of these two worked for me though as for some reason it was not mocking out the dependencies and still calling them caused errors.
I solved using a dynamic provider:
.provide({
// select(effect, next) { return 'something-for-a-selector' },
call(effect) {
switch(effect.fn.constructor.name) {
case executeFetch.constructor.name: return koResponse;
case sharedUtilToExtractErrors.constructor.name: return serverError.message;
default: throw new Error('Unknown function called in test');
}
}
})