Joi, add extension type with argument - javascript

I want to add splitArray type to Joi which converts string to an array using method Array.split. As you know, Array.split can split by argument, and I want to pass this argument during creation of the schema and not during it's usage.
So, currently I have this extension that allows me to split string to an array during validation with the use of helpers:
const splitArrayExt = joi => ({
base: joi.array(),
coerce: (value, helpers) => ({
value: value.split ? value.split(helpers.prefs.split ? helpers.prefs.split : /\s+/) : value
}),
type: 'splitArray',
)}
The problems is - I have to pass { split: <ANOTHER SPLIT ARG> } every time I call validate.
Is it possible to just do something like this:
const JoiBase = require('joi');
...
const Joi = JoiBase.extend(splitArrayExt);
const schema1 = Joi.splitArray().splitBy('|').items(Joi.string())
const schema2 = Joi.splitArray().splitBy(',').items(Joi.string())
schema1.validate('A|B,C') // values: [ 'A', 'B,C' ]
schema2.validate('A|B,C') // values: [ 'A|B', 'C' ]

So, anyway, I've figured it out:
const customJoi = Joi.extend(joi => ({
type: 'splitArray', // add type with custom name
base: joi.array(), // let's base it on Joi.array
messages: { // this is required to yell if we didn't get a string before parse
'split.base': '"value" must be a string',
},
coerce(value, helpers) { // this executes before 'validate' method is called, so we can convert string to an array
if (typeof(value) !== 'string') {
return { errors: helpers.error('split.base') };
}
const rule = helpers.schema.$_getRule('splitBy'); // this is needed to determent delimiter (default: /\s+/)
return { value: value.split(rule ? rule.args.delimiter : /\s+/) };
},
rules: {
splitBy: { // add new rule to set different delimiter from default
convert: true,
method(delimiter) {
return this.$_addRule({ name: 'splitBy', args: { delimiter } });
},
},
}
}));
...
// and voilà
customJoi.splitArray().items(customJoi.number()).validate('1 2\n3\t4')
// [ 1, 2, 3, 4 ]
customJoi.splitArray().splitBy('|').validate('A|B,C');
// [ 'A', 'B,C' ]
customJoi.splitArray().splitBy(',').validate('A|B,C');
// [ 'A|B', 'C' ]

Related

How can I return a dynamic data using methods in JavaScript?

I am having a variable that needs to return data according to the array of objects.
There are 3 dummies which are dynamic in such a way -->
Example:
dummy1_A, dummy1_B, dummy1_C, ...
dummy2_A, dummy2_B, dummy2_C, ...
dummy3_A, dummy3_B, dummy3_C, ...
I want to set 'fieldName' and 'text' while returning.
And this is my code. I have to use JSON.stringify to show the data I need
Also, I am using map method
let a1 = [
{
dummy1_A: 0.5526714707565221,
dummy2_A: 0.5526714707565223,
dummy3_A: 0.5526714707565224,
dummy1_B: 0.5028423429150607,
dummy2_B: 0.5028423429150605,
dummy3_B: 0.5028423429150604,
},
{
dummy1_A: 0.542947572819916,
dummy2_A: 0.4965857885945633,
dummy3_A: 0.4965857885945677,
dummy1_B: 0.4470431086251489,
dummy2_B: 0.3785646261205342,
dummy3_B: 0.3785646261205345,
},
];
let a2 = a1.map((x, i) => {
let seqStr = String(a1.entries); // I need some help here
return {
text: seqStr,
fieldName: seqStr,
};
});
// output
[
{ text: 'dummy1_A', fieldName: 'A' },
{ text: 'dummy2_A', fieldName: 'A' },
{ text: 'dummy1_B', fieldName: 'B' },
{ text: 'dummy2_B', fieldName: 'B' },
];
We can also use forEach but this also need more logic
a1.forEach(obj => {
const key = Object.keys(obj)[0];
const newKey = key[key.length - 1];
obj[newKey] = obj[key];
delete obj[key];
});
I have used console.log(JSON.stringify(a2)); Though I was using map but not got such result
let newHeaders1 = a1.map( i => {
return {
text: i,
fieldName: 'dummy1_'+i,
width: 110
};
});
Use this and create 2 more arrays with 2 and 3 respectively.
Then consolidated all the variable in an array using concat or spread operator

StartsWith() in an array

Basically I am using the values 'hhh' to retrieve my file. I have different field starting with 'hhhh' so I cannot base my if statement on this specific field. What I would like is to to do is map and retrieve the field with hhhh...something like this
values[startWith('hhhh')]
so then I will be able to check if the field starts with hhhh and then I will be able to differentiate the field depending on the type of each field(file, text ...)
Here is what I have as an example so you can understand. It seems that I cannot inject the startsWith() inside the array like this. Any clue on how to achieve this ?
const test =Object.entries(values['hhhhh']).map((entry, key) =>( {
test: entry[0],
test2:key
}))
You were almost there, you can use an "startWidth" method, but not directly, try combining with the array method filter, it's cleaner and readable.
const values = {
FLD_STR_101: {
test: 1
},
FLD_STR_102: {
test2: 2
},
INVALID_STR_103: {
test3: 3
}
};
const startWith = (str, prefix) => {
return str.slice(0, prefix.length) === prefix;
}
const test = Object.entries(values)
.filter(([key]) => startWith(key, 'FLD_STR_'))
.map((entry, key) =>( {
test: entry[0],
test2:key
}))
An idea can be
to filter the object key that match your expectation
loop with array.forEach method on these filtered keys
doing your map method on values[filteredKey]
const values = {
FLD_STR_101: {
test: 1,
type: 'type1'
},
FLD_STR_102: {
test2: 1,
type: 'type1'
},
FLD_STR_103_NO_TYPE: {
test2: 1
},
NOTFLD_STR_102: {
test3: 1
}
};
let test = [];
Object.keys(values)
.filter(key =>  key.startsWith('FLD_STR_') && values[key]['type'])
.forEach(filteredKey => {
test = [
...test,
...Object.entries(values[filteredKey]).map((entry, key) => ({
test: entry[0],
test2: key
}))]
});
console.log(test);

Reducing const asserted array of objects to create const asserted normalized map with reduce in Typescript

I'm trying to create a map normalized by object's key from an array of objects using reduce. The array is defined using a const assertion - as const:
const flavors = [
{ type: 'sweet' },
{ type: 'sour' },
] as const;
The desired normalized map looks like this:
const expectedFlavorsMap = {
'sweet': { type: 'sweet' },
'sour': { type: 'sour' }
} as const;
Now to create such map, I am using a reduce function so that keys are type and values are the objects itself from the flavors array.
const flavorsMap = flavors.reduce((acc, flavor) => {
acc[flavor.type] = flavor;
return acc;
}, {} as any); // any for now
Is it possible to obtain a const asserted object (map) of flavors after using reduce?
Here's what I tried, but I can see there is a problem with FlavorsMapType type.
const flavors = [
{ type: 'sweet' },
{ type: 'sour' },
] as const;
type FlavorsType = typeof flavors;
type FlavorsMapType = Record<FlavorsType[number]['type'], FlavorsType[number]>
const flavorsMap = flavors.reduce((acc, flavor) => {
acc[flavor.type] = flavor;
return acc;
}, {} as FlavorsMapType);
const expectedFlavorsMap = {
'sweet': { type: 'sweet' },
'sour': { type: 'sour' }
} as const;
const sweet = flavorsMap['sweet'];
// err, expected type to be the object {type: 'sweet'}
const sweet2: { readonly type: 'sweet' } = flavorsMap['sweet'];
Typescript playground
Thanks in advance!

JavaScript - Properly Extract Deep Object Properties and Construct New Object

Suppose the following array of objects is returned from an API:
const data = [
{ // first item
meta: {
stems: [
"serpentine",
"serpentinely"
]
},
hwi: {
hw: "sep*pen*tine",
prs: [
{
mw: "ˈsər-pən-ˌtēn",
sound: {
audio: "serpen02"
}
},
]
},
shortdef: [
"of or resembling a serpent (as in form or movement)",
"subtly wily or tempting",
"winding or turning one way and another"
]
},
{ // second item
meta: {
stems: [
"moribund",
"moribundities",
"moribundity"
]
},
hwi: {
hw: "mor*i*bund",
},
fl: "adjective"
}
]
I want to create a function that will generate a new array of objects. The objects in this new array will consist of data from the old objects, just rearranged. This is how I expect a new array to look, for example:
[
{
word: 'serpentine',
definitions: [
'of or resembling a serpent (as in form or movement)',
'subtly wily or tempting',
'winding or turning one way and another'
]
},
{
word: 'moribund',
definitions: [
'being in the state of dying : approaching death',
'being in a state of inactivity or obsolescence'
],
partOfSpeech: 'adjective'
}
]
I do this with the following function:
const buildNewData = arr => {
const newData = []
arr.forEach(item => {
newData.push({
...item.meta.stems[0] && { word: item.meta.stems[0]},
...item.shortdef && { definitions: item.shortdef },
...item.fl && { partOfSpeech: item.fl },
...item.hwi.prs[0].mw && { pronunciation: item.hwi.prs[0].mw}
})
})
return newData
}
buildNewData(data)
You may be curious as to why I use ...item.meta.stems[0] && { word: item.meta.stems[0]} in the creation of the new objects. This is to check if the property exists in the original object. If it doesn't exist, the expression will evaluate to false and therefore not be added to the new object. The first object in the original array does not have the fl property, so it evaluates to false when the new object is being constructed.
But this doesn't work when looking up a property that is an array. The code above fails with the error: TypeError: Cannot read property '0' of undefined. That's because the second item does not have a prs array under the hwi property, so the lookup fails.
Since I cannot control what data is returned from the API, how do I write a function that successfully creates a new array of objects in the format I've specified, without causing an error? I already have a solution to not add particular properties if they do not exist, but how do I take into account arrays?
More generally, I'm curious if there is a standardized way of extracting data from objects programmatically that prevents errors like this from occurring. Is there a better way to do this?
You need an additional guard so:
...item.hwi.prs[0].mw && { pronunciation: item.hwi.prs[0].mw}
becomes
...(Array.isArray(item.hwi.prs) && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
which can be shortened to:
...(item.hwi.prs && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
if you are confident that if item.hwi.prs exists its value will be an array that has a 0 value that can be spread.
const data = [
{ // first item
meta: {
stems: [
"serpentine",
"serpentinely"
]
},
hwi: {
hw: "sep*pen*tine",
prs: [
{
mw: "ˈsər-pən-ˌtēn",
sound: {
audio: "serpen02"
}
},
]
},
shortdef: [
"of or resembling a serpent (as in form or movement)",
"subtly wily or tempting",
"winding or turning one way and another"
]
},
{ // second item
meta: {
stems: [
"moribund",
"moribundities",
"moribundity"
]
},
hwi: {
hw: "mor*i*bund",
},
fl: "adjective"
}
];
const buildNewData = arr => {
const newData = []
arr.forEach(item => {
newData.push({
...item.meta.stems[0] && { word: item.meta.stems[0]},
...item.shortdef && { definitions: item.shortdef },
...item.fl && { partOfSpeech: item.fl },
...(Array.isArray(item.hwi.prs) && item.hwi.prs[0].mw) && { pronunciation: item.hwi.prs[0].mw}
})
})
return newData
}
let newData = buildNewData(data);
console.log(newData);
As you need to check existence of properties in an Object:
Use Optionnal chaining: https://javascript.info/optional-chaining
It returns a type undefined if the prop doesn't exist (but not string "undefined" ;) )
For desired order in new array, add numbers before the names of props.
let newData = [];
for (let i = 0; i < data.length; i++) {
newData[i] = {};
if (data[i]?.meta?.stems[i] != undefined)
newData[i].word = data[i].meta.stems[i];
if (data[i]?.shortdef != undefined) {
newData[i].definitions = data[i].shortdef.join(', ') + '.';
newData[i].definitions = newData[i].definitions.charAt(0).toUpperCase() + newData[i].definitions.substring(1); // Capitalize first letter
}
if (data[i]?.fl != undefined)
newData[i].partOfSpeech = data[i].fl;
}
console.log(...newData);

Extend Joi with a custom type for populated strings

I want to create a custom Joi type for populatedStrings by using .extend(..) to create a type based on joi.string() which:
Trims the string
Changes the value to undefined if the trimmed string === '' so the validated output won't contain the key at all
Overrides .required() so it acts on the trimmed string and creates errors using my own language. When .required() is set on my type it means that it requires a string which does not only contain white space or is empty
My attempt so far which is close:
const StandardJoi = require("joi");
const Joi = StandardJoi.extend(joi => ({
base: joi.string(),
name: "populatedString",
language: {
required: "needs to be a a string containing non whitespace characters"
},
pre(value, state, options) {
value = value.trim();
return value === "" ? undefined : value;
},
rules: [
{
name: "required",
validate(params, value, state, options) {
if (value === undefined) {
return this.createError(
"populatedString.required",
{ v: value },
state,
options
);
}
return value;
}
}
]
}));
Examples of it working
Joi.populatedString().validate(" x "); // $.value === 'x'
Joi.populatedString().validate(" "); // $.value === undefined
// $.error.details
//
// [ { message: '"value" needs to be a a string containing non whitespace characters',​​​​​
// ​​​​​ path: [],​​​​​
// ​​​​​ type: 'populatedString.required',​​​​​
// ​​​​​ context: { v: undefined, key: undefined, label: 'value' } } ]​​​​​
Joi.populatedString()
.required()
.validate(" ");
// $.value
//
// { inObj1: 'o' }
Joi.object()
.keys({
inObj1: Joi.populatedString()
})
.validate({ inObj1: " o " });
But it does not fail (as it should) for
// ​​​​​{ error: null, value: {}, then: [λ: then], catch: [λ: catch] }​​​​​
Joi.object()
.keys({
inObj2: Joi.populatedString(),
inObj3: Joi.populatedString().required()
})
.validate({ inObj2: " " });
Even though inObj3 is .required() and not supplied it doesn't fail.
I managed to solve it:
const BaseJoi = require("joi");
const Joi = BaseJoi.extend(joi => ({
base: joi.string(),
name: "populatedString",
language: {
required: "needs to be a a string containing non whitespace characters"
},
pre(value, state, options) {
value = value.trim();
return value === "" ? undefined : value;
},
rules: [
{
name: "required",
setup(params) {
return this.options({ presence: "required" });
},
validate(params, value, state, options) {
if (value === undefined) {
return this.createError(
"populatedString.required",
{ v: value },
state,
options
);
}
return value;
}
}
]
}));
The fix was to add setup and let it set the option presence = required if required() was called.

Categories

Resources