Joi object validation: How to validate values with unknown key names? - validation

I have an object with key names I cannot possibly know - they are created by user. However I do know what values they (keys) are going to store, and they (values) are going to be ISO strings. How do I validate those values? And, optionally, how do I validate uknown object's keys, i.e.:
key: Joi.string().min(2).max(25)
What I have already tried was based on Joi API docs :
Another benefits of using Joi.object([schema]) instead of a plain JS object is >that you can set any options on the object like allowing unknown keys, e.g:
const schema = Joi.object({
arg: Joi.string().valid('firstname', 'lastname', 'title', 'company', 'jobtitle'),
value: Joi.string(),
}).pattern(/firstname|lastname/, Joi.string().min(2));
What I understood from the example is that arg key represents Joi.object()'s key, and value represents it's value.
My example:
campaign: Joi.object({
arg: Joi.string().valid( 'unknown' ),
value: Joi.date().iso(),
}).pattern( /unknown/, Joi.string().min(2).max(25) )
My input;
campaign: { g_ad_adwords: "2017-01-19T11:33:26.205Z" }
My error:
"campaign" fails because ["g_ad_adwords" is not allowed]

Try this. It'll basically accept any key within an object campaign and the value must validate against Joi.date().iso()
campaign: Joi.object().pattern(/^/, Joi.date().iso())
This however will match any key. You can restrict this by padding out the regex a little. e.g. only word characters between 2 and 25 chars
campaign: Joi.object().pattern(/\w{2,25}/, Joi.date().iso())
UPDATE
Regarding the example in the Joi docs, I haven't tested it but here's my interpretation. I can understand that it's not the most straightforward example they could have given...
const schema = Joi.object({
arg: Joi.string().valid('firstname', 'lastname', 'title', 'company', 'jobtitle'),
value: Joi.string(),
}).pattern(/firstname|lastname/, Joi.string().min(2));
The objects to validate must contain the two attributes arg and valuewhere arg's value can be one of 'firstname', 'lastname', 'title', 'company', 'jobtitle' and value is just a string.
{
arg: 'firstname',
value: 'john'
}
{
arg: 'lastname',
value: 'smith'
}
{
arg: 'jobtitle',
value: 'brewer'
}
However it will also allow the object to have the attributes firstname and lastname where both of their values is a string with more than two characters. So the above examples could be condensed into a single valid object.
{
firstname: 'john',
lastname: 'smith',
arg: 'jobtitle',
value: 'brewer'
}

Related

keystonejs form a multi-column unique constraint

How to form a unique constraint with multiple fields in keystonejs?
const Redemption = list({
access: allowAll,
fields: {
program: relationship({ ref: 'Program', many: false }),
type: text({ label: 'Type', validation: { isRequired: true }, isIndexed: 'unique' }),
name: text({ label: 'name', validation: { isRequired: true }, isIndexed: 'unique' }),
},
//TODO: validation to check that program, type, name form a unique constraint
})
The best way I can think to do this currently is by adding another field to the list and concatenating your other values into it using a hook. This lets you enforces uniqueness across these three values (combine) at the DB-level.
The list config (and hook) might look like this:
const Redemption = list({
access: allowAll,
fields: {
program: relationship({ ref: 'Program', many: false }),
type: text({ validation: { isRequired: true } }),
name: text({ validation: { isRequired: true } }),
compoundKey: text({
isIndexed: 'unique',
ui: {
createView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'read' },
listView: { fieldMode: 'hidden' },
},
graphql: { omit: ['create', 'update'] },
}),
},
hooks: {
resolveInput: async ({ item, resolvedData }) => {
const program = resolvedData.program?.connect.id || ( item ? item?.programId : 'none');
const type = resolvedData.type || item?.type;
const name = resolvedData.name || item?.name;
resolvedData.compoundKey = `${program}-${type}-${name}`;
return resolvedData;
},
}
});
Few things to note here:
I've removed the isIndexed: 'unique' config for the main three fields. If I understand the problem you're trying to solve correctly, you actually don't want these values (on their own) to be distinct.
I've also remove the label config from your example. The label defaults to the field key so, in your example, that config is redundant.
As you can see, I've added the compoundKey field to store our composite values:
The ui settings make the field appear as uneditable in the UI
The graphql settings block updates on the API too (you could do the same thing with access control but I think just omitting the field is a bit cleaner)
And of course the unique index, which will be enforced by the DB
I've used a resolveInput hook as it lets you modify data before it's saved. To account for both create and update operations we need to consult both the resolvedData and item arguments - resolvedData gives us new/updated values (but undefined for any fields not being updated) and item give us the existing values in the DB. By combining values from both we can build the correct compound key each time and add it to the returned object.
And it works! When creating a redemption we'll be prompted for the 3 main fields (the compound key is hidden):
And the compound key is correctly set from the values entered:
Editing any of the values also updates the compound key:
Note that the compound key field is read-only for clarity.
And if we check the resultant DB structure, we can see our unique constraint being enforced:
CREATE TABLE "Redemption" (
id text PRIMARY KEY,
program text REFERENCES "Program"(id) ON DELETE SET NULL ON UPDATE CASCADE,
type text NOT NULL DEFAULT ''::text,
name text NOT NULL DEFAULT ''::text,
"compoundKey" text NOT NULL DEFAULT ''::text
);
CREATE UNIQUE INDEX "Redemption_pkey" ON "Redemption"(id text_ops);
CREATE INDEX "Redemption_program_idx" ON "Redemption"(program text_ops);
CREATE UNIQUE INDEX "Redemption_compoundKey_key" ON "Redemption"("compoundKey" text_ops);
Attempting to violate the constraint will produce an error:
If you wanted to customise this behaviour you could implement a validateInput hook and return a custom ValidationFailureError message.

In GraphQL schema, how can a field be GraphQLString as well as GraphQLInt?

Here's my code from the express application for GraphQL schema:-
let data = new GraphQLObjectType({
name:"Data",
fields: {
id: {type: GraphQLID},
value: {type: GraphQLString} // TODO: allow for string as well as int
}
});
How can I make 'value' field accept a string value as well as int value so that it can be stored using the correct type?
According to your comment saying that you use NoSQL database which can place both strings and ints in the same field, you should go with GraphQLString. When creating new instance of above object, you can create a resolve method for value field, which would check if passed value is string or int (of course it would always be string because of GraphQLString type, however it can be a string like "123" which can be parsed to int) - according to this you can perform some parsing before saving in the database.
On the other hand, when you will retrieve the data from database, it will always occur as a string in the graphql representation - if this is not a case I think that this could be a simple solution.
However, if you are not satisfied with this proposition, I am afraid that you can't trick GraphQL as you want to. Every field can obtain only single type definition.
EDIT:
This solution is not valid for the question. It works only for object types and not scalars
You should look at GraphQLUnionType: http://graphql.org/graphql-js/type/#graphqluniontype
I'm not used to define types like this, but I expect it to be something like the below:
var ValueType = new GraphQLUnionType({
name: 'Value',
types: [ GraphQLString, GraphQLInt ],
resolveType(value) {
if (value instanceof string) {
return GraphQLString;
}
if (value instanceof number) {
return GraphQLInt;
}
}
});
let data = new GraphQLObjectType({
name:"Data",
fields: {
id: {type: GraphQLID},
value: {type: ValueType}
}
});

Meteor SimpleSchema says random stuff is valid

I'm using aldeed:collection2 and aldeed:simple-schema packages. I want to validate a doc against the schema. My schema contains e.g. a string field with allowedValues array and an array of nested object, described with sub-schema. Like this:
...
type: {
type: String,
allowedValues: [ 'A', 'B', 'C' ],
defaultValue: 'A',
index: 1,
},
nestedStuff: {
type: [ new SimpleSchema(nestedStuffSchema.schema(Meteor, SimpleSchema)) ],
defaultValue: [],
},
...
I have a 'bad' doc which has e.g. "D" in type field and invalid nested array items.
At client I'm trying to:
Contacts.simpleSchema().namedContext().validate(badDoc);
and it returns true. SimpleSchema says the doc is valid even though its fields do not abide to schema.
Validating 'bad' type field individually also returns true.
What am I doing wrong? Why could SimpleSchema assume random stuff to be valid?
if you want to validate an array of strings you need to keep String inside [ ].See the below code it may help
type: {
type: [String],
allowedValues: [ 'A', 'B', 'C' ],
defaultValue: ['A'],
index: 1,
},
nestedStuff: {
type: [ new SimpleSchema(nestedStuffSchema.schema(Meteor,SimpleSchema)) ],
defaultValue: [],
},
Thanks

How to use the names in a GraphQLEnumType as the defaultValue of a GraphQL query argument?

When defining a query in a schema, how do I refer to a value of an GraphQLEnumType declared previously, to use it as the default value of an argument?
Let's say I've defined following ObservationPeriod GraphQLEnumType:
observationPeriodEnum = new GraphQLEnumType {
name: "ObservationPeriod"
description: "One of the performance metrics observation periods"
values:
Daily:
value: '1D'
description: "Daily"
[…]
}
and use it as the type of query argument period:
queryRootType = new GraphQLObjectType {
name: "QueryRoot"
description: "Query entry points to the DWH."
fields:
performance:
type: performanceType
description: "Given a portfolio EID, an observation period (defaults to YTD)
and as-of date, as well as the source performance engine,
return the matching performance metrics."
args:
period:
type: observationPeriodEnum
defaultValue: observationPeriodEnum.Daily ← how to achieve this?
[…]
}
Currently I'm using the actual '1D' string value as the default value; this works:
period:
type: observationPeriodEnum
defaultValue: '1D'
But is there a way I could use the Daily symbolic name instead? I couldn't find a way to use the names within the schema itself. Is there something I overlooked?
I'm asking, because I was expecting an enum type to behave as a set of constants also, and to be able to use them like this in the schema definition:
period:
type: observationPeriodEnum
defaultValue: observationPeriodEnum.Daily
Naïve workaround:
##
# Given a GraphQLEnumType instance, this macro function injects the names
# of its enum values as keys the instance itself and returns the modified
# GraphQLEnumType instance.
#
modifiedWithNameKeys = (enumType) ->
for ev in enumType.getValues()
unless enumType[ ev.name]?
enumType[ ev.name] = ev.value
else
console.warn "SCHEMA> Enum name #{ev.name} conflicts with key of same
name on GraphQLEnumType object; it won't be injected for value lookup"
enumType
observationPeriodEnum = modifiedWithNameKeys new GraphQLEnumType {
name: "description: "Daily""
values:
[…]
which allows to use it as desired in schema definition:
period:
type: observationPeriodEnum
defaultValue: observationPeriodEnum.Daily
Of course, this modifier fullfils its promise, only as long as the enum names do not interfere with GraphQLEnumType existing method and variable names (which are currently: name, description, _values, _enumConfig, _valueLookup, _nameLookup, getValues, serialize, parseValue, _getValueLookup, _getNameLookup and toString — see definition of GraphQLEnumType class around line 687 in https://github.com/graphql/graphql-js/blob/master/src/type/definition.js#L687)
I just ran into this. My enum:
const contributorArgs = Object.assign(
{},
connectionArgs, {
sort: {
type: new GraphQLEnumType({
name: 'ContributorSort',
values: {
top: { value: 0 },
},
})
},
}
);
In my queries, I was doing:
... on Topic {
id
contributors(first: 10, sort: 'top') {
...
}
}
Turns out you just don't quote the value (which after thinking about it makes sense; it's a value in the enum type, not an actual value:
... on Topic {
id
contributors(first: 10, sort: top) {
...
}
}
It's possible to declare enum values as default inputs via the schema definition language, but it looks like you are only using the JS library APIs. You might be able to get to a solution by taking a look at the ASTs for the working example and comparing that with the AST from what your JS code is producing.
Sorry not a solution, but hope that helps!
I found a pull request adding a method .getValue() to enum types, which returns name and value. In your case this call:
observationPeriodEnum.getValue('Daily');
would return:
{
name: 'Daily',
value: '1D'
}

Exception when updating bigquery schema

I tried insert to bigquery with schema:
require 'gcloud'
gc = Gcloud.new 'PROJECT_ID'
bq = gc.bigquery
ds = bq.dataset 'MY_DATASET'
t = ds.create_table 'MY_TABLE'
t.schema = { fields: [ { name: 'Name', type: 'STRING' } ] }
t.insert [{'name' => 'test1'}]
As expected, my terminal console showed the output error:
[{"reason"=>"invalid", "location"=>"name",
"debugInfo"=>"generic::not_found: no such field.", "message"=>"no such
field."}]
When I tried to update schema to insert the key Name:
t.schema = {
fields: [
{ name: 'Name', type: 'STRING' },
{ name: 'name', type: 'STRING' }
]
}
Displayed the exception:
Gcloud::Bigquery::ApiError: Field name already exists in schema
Any suggestion how can I solve this? This is a BigQuery bug?
When inserting data (via streaming, at least, which t.insert appears to use), field names are case-sensitive. So if you updated to 'Name', it should work with your schema.
t.insert [{'Name' => 'test1'}]
However, within queries, field names are case-insensitive, so it's invalid for a table to have field names that differ only by case: they'd be indistinguishable. This leads to your second error with "Field name already exists in schema".
This is admittedly pretty confusing. I'll look into whether we can make case-sensitivity more predictable for users.

Resources