I have to write a javascript function which can validate data according to this kind of schema. How should i go about this? Can i use json schema for this. That does'nt work for fucnctions so how should i solve this? Is there any native js way to go about this?
const userSchema = {
name: {
fn: () => String,
option: { default: 'Abhi', enum: ['John', 'Rick', 'Dan'] }
},
phone: {
fn: () => Number,
option: {}
},
cart: {
fn: () => [
{
id: {
fn: () => String,
option: {}
},
count: {
fn: () => Number,
option: {}
}
}
],
option: {}
},
address: {
contactName: {
fn: () => String,
option: {}
},
detailAddress: {
line1: {
fn: () => String,
option: {}
},
line2: {
fn: () => String,
option: {}
},
line3: {
fn: () => String,
option: {}
}
},
pin: {
fn: () => Number,
option: {}
},
country: {
fn: () => String,
option: {}
}
}
};
const user = {
name: 'Ric',
phone: 6610592314,
address: {
contactName: 'Bitu',
detailAddress: {
line1: 'Colony: 2/A',
line2: 'Q. No.: 3-018',
line3: 'Near IT store'
},
pin: 770017,
country: 'India'
},
cart: [
{
id: 'newId',
count: 2
}
]
};
Joi is a good library for this. Flow and TypeScript also work at compile-time, but won't help you with runtime validation. Joi's function validation isn't perfect, but there's a workaround for argument validation here.
I've been working lately on a JS Runtime Scheme Validation Tool, creatively called Types.
It is still at its early stages (powerful features implementations are expected in the near future), but I think it could fit your needs. Currently it is also able to handle circular schemes.
To install it, do yarn add #codistica/types or npm i #codistica/types.
Then, here you have an usage example (documentation is still work in progress):
import {Types} from '#codistica/types';
const myFunctionSchema = new Types({
argA: {type: '!undefined'},
argB: {type: 'Function'},
argC: {
type: 'Object',
def: {
propA: {type: 'number', min: 0, max: 20, def: 10},
propB: {type: 'boolean', def: false},
propC: {type: ['Function', 'null'], def: null},
propD: {type: 'Array<number>', def: [0, 1, 2]}
}
}
});
function myFunction(argA, argB, argC) {
({argA, argB, argC} = myFunctionSchema.validate({
argA,
argB,
argC
}));
if (!myFunctionSchema.isValid()) {
return;
}
}
export {myFunction};
If you would like to give it a try, I would be glad to assist you in any possible way. The above example is really an underestimation. Maybe you could also give us some useful feedback. Please let me know.
Related
Explanation
I'm working with react-select and want to generate the options array which would be passed to the react-select component.
The options array is of the type:
type TOptions = { value: string; label: string }[]
I get the data for the options array from an API. The data will have a structure like:
{
name: string;
slug: string;
id: number;
}[]
So, I created a helper function to transform the data into the options array format.
const generateSelectOptions = (
data: Record<string, string>[],
field: { valueKey: string; labelKey: string }
) => {
const options = data.map((data) => ({
value: data[field.valueKey],
label: data[field.labelKey],
}));
// TODO: create a better type check
return options as TOptions;
};
This function will have get two params,
data - data received from the API
field - which is an object that would contain the keys, valueKey and labelKey from the data object that would be mapped appropriately to the options array.
The function I created works fine, but I have manually asserted the return type of the function as TOptions.
Example
const data = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options = generateSelectOptions(data, {
valueKey: "slug",
labelKey: "name",
});
// now options would be
options = [
{
label: "Holy Holy Holy",
value: "holy-holy-holy",
},
{
label: "Amazing Grace",
value: "amazing-grace",
},
];
Question
Now, I'm thinking of a better way to type generateSelectOptions function, where when calling the function, as soon as I give the first argument data, the fieldKeys object which would be the second argument should automatically get type inference, as the fieldKeys - valueKey and labelKey can only be of the type keyof data
Is there a way to achieve this?
I would really appreciate some help here. Thanks
Sure, use a generic type parameter which will be inferred from the first argument, the second argument will then be validated against it:
function getOptions<T>(
data: T[],
field: { valueKey: keyof T }
) { /*...*/ }
type TOptions = { value: string; label: string}[];
function generateSelectOptions<T extends Record<string, unknown>, KeyField extends keyof T, LabelField extends keyof T>(data: T[], field: {valueKey: KeyField, labelKey: LabelField}): TOptions {
return data.map(entry => ({
label: String(entry[field.labelKey]),
value: String(entry[field.valueKey])
}))
}
const data = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options: TOptions = generateSelectOptions(data, {valueKey: 'id', labelKey: 'name'});
Obviously, without casting to a string, the values of your data record contain arbitrary values (e.g. strings and numbers). If you only want the label to be a number, you need to cast everything you might find there to a string (e.g. with String(value).
You could use generics, create a type for your data and pass it into your generateSelectOptions function.
You would still need to cast values that you will get from data[field.valueKey] and data[field.labelKey] to a string to match TOptions type
This is what I came up so far and if you will try to replace valueKey and labelKey values to something else rather than slug or name or id your IDE will give you a type hint
type TOption = { value: string; label: string };
const generateSelectOptions = <T>(
data: T[],
field: { valueKey: keyof T; labelKey: keyof T }
): TOption[] => {
const options = data.map<TOption>((data) => ({
value: data[field.valueKey] as string,
label: data[field.labelKey] as string,
}));
return options;
};
type DataType = { name: string; slug: string; id: number };
const data: DataType[] = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options = generateSelectOptions<DataType>(data, {
valueKey: "slug",
labelKey: "name",
});
console.log(options);
you could even add some extra layer of validation on top of the options that gets generated using type guard, but I think that should not be neccessary
const isOptionGuard = (option: any): option is TOption => "value" in option && "label" in option;
const isValidType = options.every(isOptionGuard);
console.log(isValidType)
if (!isValidType) throw Error("GenerateSelectOptions failed");
I am relatively new to typescript and need some advice on how best to type an object structure like the one below. I want to be able to specify which item ids are allowed to be in a section based on a section id.
I realize this question might be vague, but really what I am looking for is for someone to point me in the right direction. If I need to totally re-type this object in a different way using some other typescript syntax, that is fine too. At this point, I am not sure what to Google or what I need to study about typescript to understand the best way to type an object like this.
Any help would be appreciated.
type ItemBase = {
common: string | number
}
type Item = ItemBase & ({
id: 'item1',
custom1: string
} | {
id: 'item2',
custom1: number,
custom2: string,
})
type SectionBase = {
common: string | number
}
type Section = SectionBase & ({
id: 'section1',
items: {
[key in Section1ItemIds]: Item
},
} | {
id: 'section2',
items: {
[key in Section2ItemIds]: Item
},
})
type Section1ItemIds = 'item1';
type Section2ItemIds = 'item1' | 'item2';
type Group = {
sections: {
[key in Section['id']]: Section
}
}
const group: Group = {
sections: {
section1: {
id: 'section1',
common: '',
items: {
item1: {
id: 'item1',
common: '',
custom1: ''
}
}
},
section2: {
id: 'section2',
common: 0,
items: {
item1: {
id: 'item1',
common: '',
custom1: ''
},
item2: {
id: 'item2',
common: '',
custom1: 0,
custom2: ''
}
}
}
}
}
const sectionIds = Object.keys(group.sections) as Array<keyof typeof group.sections>;
const sectionId = sectionIds[Math.floor(Math.random()*sectionIds.length)]
const section: Section = group[sectionId];
const getItemIds = (): (keyof typeof section.items)[] => {
switch(section.id){
case 'section1': return ['item1'];
case 'section2': return ['item1','item2'];
}
}
My application is in react and redux technology.
I created a helper that formats my account bill and money, like this:
export const formatBill = (bill) => ({
...bill,
amountMoney: bill.amountMoney?.toLocaleString(undefined, {
minimumFractionDigits: 2,
}),
accountBillNumber: bill.accountBillNumber
.replace(/(^\d{2}|\d{4})+?/g, '$1 ')
.trim(),
});
Thanks to this, I can do something in the redux reducer and it works great:
...
case GET_BILLS_SUCCESS:
draft.bills = action.bills.map(formatBill);
break;
...
Now I have a problem because I would like to use this function for the transfer history. The problem is that my payload doesn't look so friendly. It's similar to this:
transactions: {
data: [
{
amountMoney: '10.00',
recipientBill: {
uuid: '8b9cef86-3987-4a71-badc-996caca8d8e0',
accountBillNumber: '19282292972339385206612752',
currency: {
name: 'PLN',
},
user: {
uuid: 'c58a9b81-2d16-4b07-804d-0b1fc34381ed',
}
},
senderBill: {
uuid: '6308be25-5bde-4f63-83d1-7bd47a829e61',
accountBillNumber: '76282292974456140174811708',
currency: {
name: 'USD',
},
user: {
uuid: 'af031321-f64b-4e91-948b-0c8c8ac4804c',
}
}
}
],
meta: {
page: 1,
take: 10,
itemCount: 1,
pageCount: 1
}
};
so now I would like to formatBill() the bill in recipientBill.accountBillNumber and senderBill.accountBillNumber How can I save this nicely in the redux reducer? I tried to solve this problem similar to this, but this is not the right solution:
case GET_TRANSACTION_HISTORY_SUCCESS:
draft.transactions.data = [
action.transactions.data.map((transaction) =>
formatBill(transaction.recipientBill),
),
action.transactions.meta,
];
break;
I've been working on a React/Redux application for building a quote. A gross simplification of my state would look something like this:
{
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
}
and my reducers would be sliced something like this:
const appReducer = combineReducers<GlobalState>({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
I've just recently gotten a requirements where I would essentially need to be able to render the entire app multiple times on a single page (basically show 1 or more quotes for different accounts on a single page). So a single state would now need to look something like this:
{
quotes: {
"0": {
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
},
"1": {
account: { name: 'billy jean' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: '', vip: false }
},
}
}
But obviously this new state shape doesn't really work with how I've sliced my reducers. Also, seems like I'd have to refactor all my actions so that I know which quote they should be operating on? For example, if I had an action like this:
{
type: 'UPDATE_PRICE'
payload: { productId: 123, newPrice: 15 }
}
Seems like the product 123 on both quotes would be updated.
Maybe there is instead some way I can just render the entire app on the page without having to refactor my entire state? I'm not sure what my best approach would be that wouldn't requirement me to rewrite large portions of the app.
This should give you the idea. It's basically using one reducer inside another one. As simple as using a function within another function body. You can run it on runkit.com as well.
const { createStore, combineReducers } from 'redux';
const UPDATE_ACCOUNT = 'app/updat-account';
const ADD_QUOTE = 'quote/add-quote';
const appActions = {
updateAcount: (q_id, a) => ({ type: UPDATE_ACCOUNT, payload: { q_id, name: a }}),
};
const quoteActions = {
addQuote: q_id => ({ type: ADD_QUOTE, payload: q_id }),
};
const accountReducer = (app = {}, action) => {
const { type, payload } = action;
switch (type) {
case UPDATE_ACCOUNT:
return { ...app, name: payload.name }
default:
return app;
}
};
const appReducer = combineReducers({
account: accountReducer,
lineItems: (app ={}, action) => app, // just a placeholder
modifiers: (app ={}, action) => app, // just a placeholder
});
const quoteReducer = (state = {}, action) => {
const { type, payload } = action;
switch (type) {
case ADD_QUOTE:
return { ...state, [payload]: {} };
case UPDATE_ACCOUNT: {
const app = state[payload.q_id];
return app
? { ...state, [payload.q_id]: appReducer(state[payload.q_id], action) }
: state;
}
default:
return state;
}
}
const store = createStore(quoteReducer);
store.dispatch(quoteActions.addQuote(3));
store.dispatch(quoteActions.addQuote(2));
store.dispatch(appActions.updateAcount(3, 'apple'));
store.dispatch(appActions.updateAcount(4, 'orange')); // non-existent quote
store.getState():
/**
{
"2": {},
"3": {
"account": {
"name": "apple"
},
"lineItems": {},
"modifiers": {}
}
}
*/
Just wanted to add my specific answer here..
Basically I added a new root reducer as norbertpy suggested. However, I also had to add a parameter quoteId to each action to specify which quote the action originated from and should operate on. This was the most time consuming part of the refactor as now each component that dispatches actions must have access to the quote key.
Reducer
const quoteReducer = combineReducers({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
const rootReducer = (state = {quotes: []}, action) => {
const newQuoteState = quoteReducer(state.quotes[action.quoteId], action);
const newQuotes = {...state.quotes};
newQuotes[action.quoteId] = newQuoteState;
return {...state, ...{quotes: newQuotes}};
};
Action
{
type: 'UPDATE_PRICE'
quoteId: '0',
payload: { productId: 123, newPrice: 15 }
}
I apologize in advance for the complex example here; I tried to trim it down as much as I could to illustrate what I try to achieve
I have a complex structure that I need to traverse and transform based on some conditions; Here's an (short) example of the structure that should cover most scenarios:
{ PROP1: {
metadata: Object, // somewhere deeper in metadata I have a `value` key
parent: { $ref: String },
employee: {
parent: { $ref: String },
id: String,
metadata: Object,
products: {
metadata: Object,
model: { $ref: String },
type: 'array',
value: ["String", "String" , "String"] ,
[...]
},
model: {
id: String,
email: {
metadata: Object,
value: 'a#b.com',
type: 'string',
validity: Object,
[...]
},
name: {
firstName: {
metadata: Object,
value: 'John',
type: String,
validity: Object,
[...]
},
lastName: {
metadata: Object,
value: 'Smith',
type: String,
validity: Object,
[...]
},
}
},
operations: {
id: String,
items: [
{ action: {value: "UPDATE", model: {$ref: String }, [...] },
{...}
],
metadata: Object,
[...]
}
}
},
PROP2: {
// similar as PROP1
},
[... and so on ...]
}
I basically need to clean that up before sending it to the backend;
Whenever a value contains $ref, I don't want the key/val pair (e.g.: PROP1.parent is of no use and can be omitted)
whenever a value contains value, I need to omit everything else and move the value of value as the value of key (e.g.: PROP1.employee.products should equal ['String', 'String', 'String'])
keys like id, metadata, validity (etc) can be completely omitted regardless of its content
So the end result should be along those lines:
{ PROP1: {
employee: {
products: ['item','item','item'],
model: {
email: 'a#b.com',
name: { firstName: 'John', lastName: 'Smith'},
},
operations: [
{action: 'UPDATE'}
]
}
},
PROP2: { ... }
}
I tried lots of different approaches using different lodash methods but couldn't wrap my head around this...
Any help will be greatly appreciated
Thanks
In pseudo code, try something like this. Implement the specifics and post more info when you run into trouble.
var ignoreKeyArray = ["id", ...] // keys to ignore and remove
var newJSON = "{}";
for (property in JSON) {
var newProp = parseJSON(key, property);
insert newProp in newJSON object
}
var parseJSON = function (key, jsonBlob) {
if (key in ignoreKeyArray || jsonBlob contains value "$ref")
return "";
var jsonOut = key + ": {}";
for (child in jsonBlob) {
add parseJSON(child) to jsonOut;
}
return jsonOut;
}
If you have any questions, comment so I can extend the answer and clarify.