Knockout-Validation multiple custom async rules - javascript
I have a domain property and I want to validate two things;
URL exists (is reachable)
URL exists in my local DB.
In order to check these things I created to async validation rules using https://github.com/Knockout-Contrib/Knockout-Validation and applied both of them on my property.
What happens is that each time the response from one of the rules comes earlier and it sets isValidating property to false and I want this property to be true until the response from my second rule came.
Custom rules:
export function enableCustomValidators() {
(ko.validation.rules as any)["urlValidationServicePath"] = {
async: true,
validator: function (url: string, baseUrl: string, callback: any) {
getRequest(url, baseUrl, callback, "true");
},
message: 'You must enter a reachable domain.',
},
(ko.validation.rules as any)["customerValidationServicePath"] = {
async: true,
validator: function (url: string, baseUrl: string, callback: any) {
getRequest(url, baseUrl, callback, "false");
},
message: "This url already exists in our system. Please contact us at hello#ve.com",
}
ko.validation.registerExtenders();
}
function getRequest(url: string, baseUrl: string, callback: any, method: string) {
var restClient = new RestClient();
restClient.downloadString(baseUrl.concat(url), (responseText) => {
method === "true" ? callback(responseText === "true" ? true : false) :
callback(responseText === "true" ? false : true);
});
}
Using of the rules:
export class CompanySetupVM extends BasePageVM {
public websiteUrl: KnockoutObservable<string> = ko.observable(undefined);
public isValidating: KnockoutObservable<boolean> = ko.observable(false);
public constructor() {
this.websiteUrl.extend({
required: {
params: true,
message: CompanySetupVM.ErrorMessageNullWebsiteUrl
},
urlValidationServicePath: CompanySetupVM.DomainValidationPath,
customerValidationServicePath: CompanySetupVM.CustomerValidationPath
});
this.isValidating = ko.computed(() => this.websiteUrl.isValidating(), this);
}
}
In cshtml:
data-bind="text: currentPage().nextButtonText, css: {'button-overlay': currentPage().isValidating(), 'button': !currentPage().isValidating()}, click: nextAction"
I've looked at the source code of knockout validation (here) and it's pretty clear that two independent async validators are not supported.
The isValidating property is set to true as soon as an async rule is begins to run and set to false again as soon as that rule finishes. Therefore, multiple async rules clash.
There is only one solution. Remove the second async validator.
You can collapse the two checks into one either on the client side or on the server side.
To do it on the client side, you would need to write a validator that runs two Ajax requests and invokes the validation callback only after both of them have returned.
To do it on the server side, you would have to run the "is reachable" and "is in DB" checks in succession before giving an overall response to the client.
Personally I would prefer changing the server side, because
it keeps the client code tidy and manageable
it saves one HTTP round-trip per check
semantically, the URL check is one thing that fail for more than one reason
it's easy to let the server send a custom validation result and -message
Besides plain true or false, the validation plugin understands responses in this format:
{isValid: false, message: "something is wrong"}
So make your server send a JSON response with the appropriate validation result and error message and your REST client download JSON instead of text.
Then all you need to do is pass the server's response directly to the validation callback.
ko.validation.rules.urlValidationServicePath = {
async: true,
validator: function (url, baseUrl, callback) {
restClient.downloadJSON(baseUrl.concat(url), callback);
},
message: 'The URL you entered is not valid.'
};
Here the message is only a default. The server's message always takes precedence over the setting in the validation rule.
Yes, as Tomalak pointed out it is not possible to have multiple async validators. But I solved it on the client side and solution is quite manageable and flexible IMHO.
The trick here is to implement different async validators as regular knockout extenders and having single async rule to call them. Here is the async rule:
interface HasAsyncValidator {
asyncValidators: Validator[];
}
interface Validator {
name: string,
validator: (params: any) => boolean | PromiseLike<any>,
params: any
}
interface KnockoutObservable<T> extends HasAsyncValidator {}
ko.validation.rules["validateAsync"] = {
validator: async (value: any, paramsAccessor: () => HasAsyncValidator, callback: (result: boolean | ValidationResult) => void) => {
const params = paramsAccessor();
if (!params || !params.asyncValidators) {
callback(true);
return;
}
try {
const results = await Promise.all(params.asyncValidators.map(v => v.validator(v.params)));
const invalidResult = results.find(r => r.isValid === false);
callback(!!invalidResult ? invalidResult : true);
} catch (error) {
callback(false);
throw error;
}
},
message: 'default message',
async: true
}
As you can see, we extended observable with asyncValidators property, which keeps all the registered validators. All that is left for the rule is just call validators (if any) and then pass result to the knockout validation callback.
Here is an example of validator as regular extender:
ko.extenders["validationRule"] = (target: any, option: any) => {
const validatorObj: Validator = {
name: "validationRule",
params: option,
validator: async (): Promise<boolean | ValidationResult> => {
const unwrappedValue = ko.unwrap(target);
const result = await callServer();
return {
isValid: result.isValid,
message: result.message
};
}
}
addOrUpdateAsyncValidator(target, validatorObj);
};
function addOrUpdateAsyncValidator(target: HasAsyncValidator, validatorObj: Validator) {
target.asyncValidators = target.asyncValidators || [];
const existingRule = target.asyncValidators.find(v => v.name == validatorObj.name);
!!existingRule
? existingRule!.params = validatorObj.params
: target.asyncValidators.push(validatorObj);
}
Note that each validator must register itself to asyncValidators property on observable.
The usage of this solution is quite straightforward:
let value = ko.observable();
value.extend({ validationRule: true, validateAsync: () => value });
Note that we should pass value accessor to the validateAsync instead of value itself. That is needed so async rule don't miss validators that can be added later.
Related
Intercept all Form / POST Events
I'd like to intercept all Form Actions / POST events send to the server to do data validation once, instead each time per page in sveltekit. I figured the best place might be hooks.server, but it only exposes the handle function, not the actions: Actions that is needed for using invalid( ... ) for returning data validation. Is there a way to return invalid(...) in hooks.server or access actions:, or is there a better way to handle this?
There is nothing particularly special about invalid. It ultimately causes a JSON response of the form: { type: 'invalid', status: number, // HTTP code data: any, // Object that will be passed to `form` property of page } So you can use the handle hook to do validation, e.g. import type { Handle } from '#sveltejs/kit'; import { json } from '#sveltejs/kit'; export const handle: Handle = async ({ event, resolve }) => { if (event.request.method == 'POST') { const errorInfo = await validate(event.request); if (errorInfo) { return json({ type: 'invalid', status: errorInfo.status, data: errorInfo.data, }, { status: errorInfo.status }); } } return await resolve(event); } async function validate(r: Request) { // `clone()` so the rest of the code that might also // try to read the request should not be affected const data = await r.clone().formData(); // Validation logic here }
How to check object type from request.body in Typescript?
I need to check the object type from the request body and then run approbiate function based on this type, I try do this in this way: export interface SomeBodyType { id: string, name: string, [etc....] } export const someFunction = async (req: Request, res: Response) => { const { body } = req.body; if (body instanceof SomeBodyType) { //run function A } else { // run function B } } but it not work, cause my SomeBodyType return me error: only refers to a type, but is being used as a value here., so, how can i check the type of the body in this case? thanks for any help! //// EDIT: thanks to #phn answer I create this generic function to check object types: export const checkObjectType = <T>(body: T): boolean => { return (body as T) !== undefined; } please, take a look and comment if this function is good
As stated in the comments already, types don't exist in runtime. You need to write a function to validate the content received in the body, one approach is from the TypeScript documentation (see code example). A more advanced and dynamic approach is to use a validation library to validate your input against a schema or contract, for example ajv interface SomeBodyType { id: string, name: string } function isBodyType(body: any): body is SomeBodyType { return (body as SomeBodyType).id !== undefined && (body as SomeBodyType).name !== undefined; } export const someFunction = (req: Request, res: Response) => { const { body } = { body: { id: '', name: '' } }; if (isBodyType(body)) { // run function A } else { // run function B } };
Jest typescript check for type
lets say i have the following interface: export interface CMSData { id: number; url: string; htmlTag: string; importJSComponent: string; componentData: ComponentAttribute[]; } Then i have a method that returns an array of this object type: public async GetContent(url: string): Promise<CMSData[]>{ const response = await super.get<ICMSContentData[]>(url, {}); try { if (response?.parsedBody) { return this.ProcessResponse(response.parsedBody); } else { this.handleHTTPError(new Error("Error")); return []; } } catch (e) { this.handleHTTPError(e); return []; } } Then i want to test that this is the case so i write the following test: import {ContentIOService} from ".."; import {CMSData} from "../IOServices/ContentIOService"; require('es6-promise').polyfill(); require('isomorphic-fetch'); test('Get Content', async () => { const service = ContentIOService.getInstance(); const data = await service.GetContent("https://1c7207fb14fd3b428c70cc406f0c27d9.m.pipedream.net"); console.log(data) expect(data).toBeInstanceOf(CMSData[]); }); However here i get the following error: 'CMSData' only refers to a type, but is being used as a value here. So how can i test that the data i get back is valid and of the right type?
If the type you're looking for is a call, the .toBeInstanceOf(Class) method accepts a parameter which MUST BE a JavaScript class constructor instead of TS type. You should let TSC check whether you receive the correct type of data at compile time. Code written inside test suites and test cases is executed at runtime, .toBeInstanceOf(Class) is a runtime check, NOT compiler time. At runtime, you may want to use expect.objectContaining(object) matches any received object that recursively matches the expected properties.
Unable to pass parameters to service hook through find function in featherjs
Featherjs find service unable to pass extra parameters through find function. In below find service passing extra params data to service. but unable to receive the value at service hook. Client code : return this.app.service('userlist').find({ query: { usersIds: { "$in" : [this.user._id]} }, paginate: false, params:{ name:'sam' } }).then(response => { }).catch(error => { console.log(error); }); Server code (Service hook ) : module.exports = function (options = {}) { return async function dogetUsr (context) { const { data } = context; console.log('Client Param data -->',context.params.name); return context; }; }; params data not receiving at server --> params:{ name:'sam' } Output at server/service hook : Client Param data -->undefined
For security reasons, only params.query is passed between the client and the server. In general I wouldn't recommend letting the client disable pagination unless you are guaranteed to only get a few (less than 100) records. Otherwise requests with many records can cause major issues on both sides. If it is still something you need, you can use the disablePagination hook which lets you set the $limit to -1 if you want to disable pagination: const { disablePagination } = require('feathers-hooks-common'); module.exports = { before: { find: disablePagination() } };
Flow Types with Promises (Fetch's)
I created a Fetch function to consume a JSON API and have defined types for the JSON object. I am confused about how to define the return type for the getCurrentJobAPI function since I do a bunch of .then() afterwards. Is the return value the last .then()? In my code, the last .then() is a setState, so what would the type be for that? getCurrentJobAPI = (): {} => { const url: string = `dummy_url&job_id=${this.props.currentJob}`; return fetch(url, {credentials: 'include'}) .then((response) => { return response.json(); }) .then((json: CurrentJob) => { console.log(json); const location = json.inventoryJob.location; const ref_note = json.inventoryJob.note; const id = json.inventoryJob.id; const models = json.inventoryJobDetails.map((j) => { return Object.assign({}, { code: j.code, qty: j.qty }) }); this.setState({ currentCodes: models, location: location, ref_note: ref_note, id: id}) return json }) .then((json: CurrentJob) => { const barcodes = json.inventoryJob.history; if (barcodes.length > 0) { this.setState({apiBarcodes: barcodes}) } this.calculateRows(); this.insertApiBarcodes(); this.setState({ initialLoad: true }); }) }; UPDATE: Although I understand that I am supposed to define Promise<type> as the return value of getCurrentJobAPI (see Gilad's answer and comments), I am still unsure why I can't write Promise<CurrentJob> if the Fetch resolves as the JSON response. [I have condensed my .then() statements per loganfsmyth's recommondation.] Here are the type definitions for CurrentJob: type Job = { user_id: number, status: 'open' | 'closed', location: 'string', history: {[number]: string}[], note: string, } & CommonCurrentJob; type JobDetails = { iaj_id: number, code: number, } & CommonCurrentJob; type CommonCurrentJob = { id: number, qty: number, qty_changed: number, created_at: string, updated_at: string }
So first off, a disclaimer, I am a TypeScript user but I find that this question is actually applicable to both languages and has the same answer. I created a Fetch function to consume a JSON API and have defined types for the JSON object. I am confused about how to define the return type for the getCurrentJobAPI function since I do a bunch of .then() afterwards. Is the return value the last .then()? In my code, the last .then() is a setState, so what would the type be for that? TL;DR: Promise<void> (see note). As you suspect, this is in fact the return type of the last top-level .then in the promise chain. Now lets dig a bit deeper Here is your example, reworked very slightly to leverage type inference instead of annotating callback parameters that are declared as any by their receivers. As an aside, these callback parameter annotations amount to unsafe implicit casts, or type assertions as we call them in TypeScript, and they lie about the shape of the code. They look like this declare function takesFn(fn: (args: any) => any): void; So I have minimized these since they form a subtle trap // #flow import React from 'react'; type CurrentJob = { inventoryJob: Job, inventoryJobDetails: JobDetails[] } export default class A extends React.Component<{currentJob:JobDetails}, any> { getCurrentJobAPI: () => Promise<void> = () => { const url = `dummy_url&job_id=${String(this.props.currentJob)}`; return fetch(url, {credentials: 'include'}) .then(response => { return (response : {json(): any}).json(); }) // --> Promise<any> .then(json => { const currentJob = (json: CurrentJob); // make the assumption explicit. console.log(currentJob); const {location, id, note: ref_note} = currentJob.inventoryJob; const currentCodes = currentJob.inventoryJobDetails .map(({code, qty}) => ({ code, qty })); this.setState({currentCodes, location, ref_note, id}); return currentJob; }) // --> Promise<CurrentJob> .then(currentJob => { const apiBarcodes = currentJob.inventoryJob.history; if (apiBarcodes.length > 0) { this.setState({apiBarcodes}); } this.setState({initialLoad: true}); }); // --> Promise<void> }; } So I am making assertions about the promises in each then call above but those assertions are all validated by type inference with the exception of the initial type cast on the response value. As further evidence, if we remove the type declaration from the getCurrentJobAPI property of A, flow will infer that its type is in fact Promise<void>. Bonus: simplifying with async/await. I've used several ESNext features above to shorten the code and make it a bit more pleasant, but we can leverage a specific feature, async/await to make it easier to understand control flow and types in Promise based code. Consider this revision. // #flow import React from 'react'; type CurrentJob = { inventoryJob: Job, inventoryJobDetails: JobDetails[] } export default class A extends React.Component<{currentJob:JobDetails}, any> { getCurrentJobAPI = async () => { const url = `dummy_url&job_id=${String(this.props.currentJob)}`; const response = await fetch(url, {credentials: 'include'}); const json = await response.json(); const currentJob = (json: CurrentJob); // make the assumption explicit. console.log(currentJob); const {location, id, note: ref_note} = currentJob.inventoryJob; const currentCodes = currentJob.inventoryJobDetails.map(({code, qty}) => ({ code, qty })); this.setState({currentCodes, location, ref_note, id}); const apiBarcodes = currentJob.inventoryJob.history; if (apiBarcodes.length > 0) { this.setState({apiBarcodes}); } this.setState({initialLoad: true}); }; } Clearly, this is a void function. It has no return statements. However, as an async function, it inherently returns a Promise, just as it did when written as an explicit Promise chain. Note: void is a construct that has been found useful in Flow and TypeScript to represent the semantic intent of function that do not return values but in reality such functions actually return undefined because, well, this is JavaScript. Flow does not seem to recognize undefined as a type, but under TypeScript, the function could equally be annotated as returning Promise<undefined>. Irregardless, Promise<void> is preferable thanks to the clarity of intent it provides. Remarks: I worked through this using a combination of https://flow.org/try and the flow binary for Windows. The experience on Windows is really terrible and hopefully it will improve.
When chaining then's, the result will always be a promise. When calling then, the return value is another promise, otherwise chaining then's wouldn't have been possible. You can see that easily by using console.log() surrounding the entire chain.