AJV - Check property is a function - ajv

I'm using AJV to check a "settings" object. I want to add a new property onFeedbackChange that can be a function (not required).
const ajv = new Ajv({
allErrors: true,
});
ajv.addKeyword('function', {
valid: true,
validate: function (data) {
return typeof data === 'function';
}
});
const validate = ajv.compile(settingsSchema);
Schema:
feedback:
type: object
properties:
enabled:
type: boolean
saveFeedback: *endpoint
updateFeedback: *endpoint
onFeedbackChange: function
additionalProperties: false
required:
- enabled
- saveFeedback
- updateFeedback
But this fails with:
Error: schema is invalid: data.properties['modules'].properties['feedback'].properties['onFeedbackChange'] should be object,boolean
I wonder how to perform the validation, and why this isn't built-in.

We're using this to verify data that contains a React component:
The data we're validating:
const config = {
id: 'dailyGraph',
component: BarGraph, // <-- react component (function)
type: 'bar',
...
}
Our schema:
const barSchema = {
$schema: 'http://json-schema.org/draft-07/schema',
$id: 'dailyGraph',
type: 'object',
readOnly: true,
title: 'Schema for validating graph config',
properties: {
id: {
$id: '#/properties/id',
type: 'string'
},
component: {
$id: '#/properties/component',
instanceof: 'Function', // <-- ajv custom keyword
},
type: {
$id: '#/properties/type',
type: 'string',
enum: ['bar','pie'],
}
...
},
required: ['id', 'assays', 'graphType']
};
And the .addKeyword syntax as per here: https://github.com/epoberezkin/ajv/issues/147#issuecomment-199371370
const ajv = new Ajv();
const { id } = config;
const CLASSES = { Function: Function, ... };
ajv.addKeyword('instanceof', {
compile: schema => data => data instanceof CLASSES[schema]
});
ajv.validate(barSchema, config)
? res(true)
: console.error(`Graph config error for ${id}: ${ajv.errorsText()}`);
Passing component as a string (or anything but a function) throws: Graph config error for dailyGraph: data.component should pass "instanceof" keyword validation

Related

Mongoose: TypeError: Invalid value for schema path `guildId.type`, got value "undefined"

I got this error but I don't know what I have done wrong.
My code:
const mongoose = require('mongoose');
const GuildConfigSchema = new mongoose.Schema({
guildId: {
type: mongoose.SchemaType.String,
required: true,
unique: true,
},
prefix: {
type: mongoose.SchemaType.String,
required: true,
default: 'b!',
},
defaultRole: {
type: mongoose.SchemaType.String,
required: false,
},
memberLogChannel: {
type: mongoose.SchemaType.String,
required: false,
},
});
module.exports = mongoose.model('GuildConfig', GuildConfigSchema);
And my guild create event where I am setting the values of the database:
// https://discord.js.org/#/docs/main/stable/class/Client?scrollTo=e-guildCreate
const BaseEvent = require('../utils/structures/BaseEvent');
const GuildConfig = require('../database/schemas/GuildConfig');
module.exports = class GuildCreateEvent extends BaseEvent {
constructor() {
super('guildCreate');
}
async run(client, guild) {
try {
const guildConfig = await GuildConfig.create({
guildId: guild.id,
});
console.log('Successfully joined server!');
} catch(err) {
console.log(err);
}
}
}
My error:
TypeError: Invalid value for schema path `guildId.type`, got value "undefined"
Does anyone see what I've done wrong?
explicitly export GuildConfigSchema
module.exports.GuildConfigSchema = GuildConfigSchema;
and use destructuring where the schema is required.
const { GuildConfigSchema } = require("path to schema file");

Optional property based on value of enum

I'm trying to get some typings to work for a react useReducer.
Basically I have an action that has an optional property (data) based on the value of another property - so if STATUS is VIEW or EDIT, the action must have the data property. I almost have something working, but there's one case (see below) where this fails.
I guess one way of doing this is by explicitly setting STATUS.NEW to not require the extra property ({ type: 'SET_STATUS'; status: STATUS.NEW }), but I'm wondering if theres a better way. If in the future I added a bunch of different statuses then I'd have to specify each one to not require the data property.
Typescript Playground
enum STATUS {
NEW = 'new',
VIEW = 'view',
EDIT = 'edit'
}
/*
if status is 'view', or 'edit', action should also contain
a field called 'data'
*/
type Action =
| { type: 'SET_STATUS'; status: STATUS }
| { type: 'SET_STATUS'; status: STATUS.VIEW | STATUS.EDIT; data: string; }
// example actions
// CORRECT - is valid action
const a1: Action = { type: 'SET_STATUS', status: STATUS.NEW }
// CORRECT - is a valid action
const a2: Action = { type: 'SET_STATUS', status: STATUS.VIEW, data: 'foo' }
// FAILS - should throw an error because `data` property should be required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }
// CORRECT - should throw error because data is not required if status is new
const a4: Action = { type: 'SET_STATUS', status: STATUS.NEW, data: 'foo' }
And the second part of the question is how I'd incorporate this into a useCallback below. I would have thought that useCallback would be able to correctly infer the arguments into the appropriate action type.
/*
assume:
const [state, dispatch] = useReducer(stateReducer, initialState)
*/
const setStatus = useCallback(
(payload: Omit<Action, 'type'>) => dispatch({ type: 'SET_STATUS', ...payload }),
[],
)
/*
complains about:
Argument of type '{ status: STATUS.EDIT; data: string; }' is not assignable to parameter of type 'Pick<Action, "status">'.
Object literal may only specify known properties, and 'data' does not exist in type 'Pick<Action, "status">'
*/
setStatus({ status: STATUS.EDIT, data: 'foo' })
You can define a union of statues that require data, then exclude them in action representing all the others:
enum STATUS {
NEW = 'new',
VIEW = 'view',
EDIT = 'edit'
}
type WithDataStatuses = STATUS.VIEW | STATUS.EDIT;
type Action =
| { type: 'SET_STATUS'; status: Exclude<STATUS, WithDataStatuses> }
| {
type: 'SET_STATUS';
status: WithDataStatuses;
data: string;
}
// now CORRECT - data is required
const a3: Action = { type: 'SET_STATUS', status: STATUS.EDIT }
Answer for second part of question :-)
Assuming that you have defined Actions as suggested by #Aleksey L., useCallback can by typed as follows
// This is overloaded function which can take data or not depending of status
interface Callback {
(payload: { status: Exclude<STATUS, WithDataStatuses> }): void;
(payload: { status: WithDataStatuses; data: string; } ): void;
}
const [state, dispatch] = React.useReducer(stateReducer, {})
// Explicitly type useCallback with Callback interface
const setStatus = React.useCallback<Callback>(
(payload) => dispatch({ type: 'SET_STATUS', ...payload }),
[],
)
setStatus({ status: STATUS.EDIT, data: 'foo' })
setStatus({ status: STATUS.NEW })
The working demo

GraphQL: Adding subscription to schema

I am trying to setup subscriptions with GraphQL and Relay. I have the following mutation which adds a new team:
const createTeamMutation = mutationWithClientMutationId({
name: 'CreateTeam',
inputFields: {
ownerId: { type: new GraphQLNonNull(GraphQLInt) },
name: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
team: {
type: teamType,
resolve: (payload) => payload
}
},
mutateAndGetPayload: (team) => {
const { ownerId, name } = team;
// Using Sequalize ORM here..
return db.models.team.create(team)
.then.... etc
}
});
This works fine but I can't seem to figure out how to get my subscriptions working at least in GraphiQL. I have the following definition in my schema:
const GraphQLCreateTeamSubscription = subscriptionWithClientId({
name: 'CreateTeamSubscription',
outputFields: {
team: {
type: teamType,
resolve: (payload) => payload
}
},
subscribe: (input, context) => {
// What is meant to go here??
},
});
I am not sure how to build out the subscribe feature and can't seem to find enough documentation. When I run the following in GraphiQL,
subscription createFeedSubscription {
team {
id
ownerId
name
}
}
I get the following error:
Cannot query field team on type Subscription.
Thank you for all the help!

My query is failing in relay and I don't know why?

I have this simple query which works fine in my Graphql but I cannot pass data using relay to components and I don't know why :(
{
todolist { // todolist returns array of objects of todo
id
text
done
}
}
this is my code in an attempt to pass data in components using relay:
class TodoList extends React.Component {
render() {
return <ul>
{this.props.todos.todolist.map((todo) => {
<Todo todo={todo} />
})}
</ul>;
}
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on Query {
todolist {
id
text
done
}
}
`,
},
});
And lastly my schema
const Todo = new GraphQLObjectType({
name: 'Todo',
description: 'This contains list of todos which belong to its\' (Persons)users',
fields: () => {
return {
id: {
type: GraphQLInt,
resolve: (todo) => {
return todo.id;
}
},
text: {
type: GraphQLString,
resolve: (todo) => {
return todo.text;
}
},
done: {
type: GraphQLBoolean,
resolve: (todo) => {
return todo.done;
}
},
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
description: 'This is the root query',
fields: () => {
return {
todolist: {
type: new GraphQLList(Todo),
resolve: (root, args) => {
return Conn.models.todo.findAll({ where: args})
}
}
}
}
});
This code looks simple and I cannot see why this won't work and I have this error Uncaught TypeError: Cannot read property 'todolist' of undefined, but I configure todolist and I can query in my graphql, you can see the structure of the query is same, I don't know why this is not working?
todolist should be a connection type on Query. Also, your ids should be Relay global IDs. You will not have access to your objects' raw native id fields in Relay.
import {
connectionArgs,
connectionDefinitions,
globalIdField,
} from 'graphql-relay';
// I'm renaming Todo to TodoType
const TodoType = new GraphQLObjectType({
...,
fields: {
id: uidGlobalIdField('Todo'),
...
},
});
const {
connectionType: TodoConnection,
} = connectionDefinitions({ name: 'Todo', nodeType: TodoType });
// Also renaming Query to QueryType
const QueryType = new GraphQLObjectType({
...,
fields: {
id: globalIdField('Query', $queryId), // hard-code queryId if you only have one Query concept (Facebook thinks of this top level field as being a user, so the $queryId would be the user id in their world)
todos: { // Better than todoList; generally if it's plural in Relay it's assumed to be a connection or list
type: TodoConnection,
args: connectionArgs,
},
},
});
// Now, to be able to query off of QueryType
const viewerDefaultField = {
query: { // Normally this is called `viewer`, but `query` is ok (I think)
query: QueryType,
resolve: () => ({}),
description: 'The entry point into the graph',
}
};
export { viewerDefaultField };
The above is not fully complete (you'll likely also need to setup a node interface on one or more of your types, which will require node definitions), but it should answer your basic question and get you started.
It's a huge, huge pain to learn, but once you struggle through it it starts to make sense and you'll begin to love it over RESTful calls.

Relay/Graphql querying field "node" instead of "viewer" when using this.props.relay.setVariables

I have a table that is fetching 2 items on the initial page load. This correctly returns the 2 rows. When I check the Request Payload I see the following information:
{"query":"query CampaignQuery {
viewer {
id,
...F0
}
}
fragment F0 on User {
_campaigns3iwcB5:campaigns(first:2) {
edges {
node {
id,
account_id,
start_time
},
cursor
},
pageInfo {
hasNextPage,
hasPreviousPage
}
},
id
}","variables":{}}
I then have a button that triggers a function and sets a variable to return additional items via this.props.relay.setVariables.
I then get a 3 retries error and the following error:
[{message: "Cannot query field "node" on type "Query".", locations: [{line: 2, column: 3}]}]
when I check the Request Payload I notice that it is querying "node" instead of "viewer" like it did previously.
{"query":"query Listdata_ViewerRelayQL($id_0:ID!) {
node(id:$id_0) {
...F0
}
}
fragment F0 on User {
_campaignsgsWJ4:campaigns(after:\"CkkKFwoEbmFtZRIPGg1DYW1wYWlnbiBGb3VyEipqEXN+cmVhc29uaW5nLXVzLTAxchULEghDYW1wYWlnbhiAgICA3pCBCgwYACAA\",first:2) {
edges {
node {
id,
account_id,
start_time
},
cursor
},
pageInfo {
hasNextPage,
hasPreviousPage
}
},
id
}","variables":{"id_0":"VXNlcjo="}}
This is my schema.js file
/* #flow */
/* eslint-disable no-unused-consts, no-use-before-define */
import {
GraphQLBoolean,
GraphQLFloat,
GraphQLID,
GraphQLInt,
GraphQLList,
// GraphQLEnumType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString
} from 'graphql';
// const types = require('graphql').type;
// const GraphQLEnumType = types.GraphQLEnumType;
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions
} from 'graphql-relay';
import {
User,
Feature,
getUser,
getFeature,
getFeatures,
getEventStream,
Campaign,
getCampaigns,
resolveCampaigns,
campaignById,
} from './database';
// Import loader DataLoader
import Loader from './loader';
/**
* We get the node interface and field from the Relay library.
*
* The first method defines the way we resolve an ID to its object.
* The second defines the way we resolve an object to its GraphQL type.
*/
const { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
} else if (type === 'Feature') {
return getFeature(id);
} else if (type === 'Campaign') {
return campaignById(id);
} else {
return null;
}
},
(obj) => {
if (obj instanceof User) {
return userType;
} else if (obj instanceof Feature) {
return featureType;
} else if (obj instanceof Campaign) {
return campaignType;
} else {
return null;
}
}
);
/**
* Define your own types here
*/
const campaignType = new GraphQLObjectType({
name: 'Campaign',
description: 'A campaign',
fields: () => ({
id: globalIdField('Campaign'),
account_id: {
type: GraphQLString,
description: 'ID of the ad account that owns this campaign',
},
adlabels: {
type: GraphQLString,
description: 'Ad Labels associated with this campaign',
},
buying_type: {
type: GraphQLString,
description: 'Buying type, possible values are: AUCTION: default, RESERVED: for reach and frequency ads',
},
can_use_spend_cap: {
type: GraphQLBoolean,
description: 'Whether the campaign can set the spend cap',
},
configured_status: {
type: GraphQLString, // GraphQLEnumType,
description: '{ACTIVE, PAUSED, DELETED, ARCHIVED}. If this status is PAUSED, all its active ad sets and ads will be paused and have an effective status CAMPAIGN_PAUSED. Prefer using \'status\' instead of this.',
},
created_time: {
type: GraphQLID, // this should be a datetime
description: 'Created Time',
},
effective_status: {
type: GraphQLString, // GraphQLEnumType,
description: 'The effective status of this campaign. {ACTIVE, PAUSED, DELETED, PENDING_REVIEW, DISAPPROVED, PREAPPROVED, PENDING_BILLING_INFO, CAMPAIGN_PAUSED, ARCHIVED, ADSET_PAUSED}',
},
name: {
type: GraphQLString,
description: 'Campaign\'s name',
},
objective: {
type: GraphQLString,
description: 'Campaign\'s objective',
},
recommendations: {
type: GraphQLString, // GraphQLList,
description: 'If there are recommendations for this campaign, this field includes them. Otherwise, this field will be null.',
},
spend_cap: {
type: GraphQLFloat,
description: 'A spend cap for the campaign, such that it will not spend more than this cap. Expressed as integer value of the subunit in your currency.',
},
start_time: {
type: GraphQLID, // this should be a datetime
description: 'Start Time',
},
status: {
type: GraphQLString,
description: '{ACTIVE, PAUSED, DELETED, ARCHIVED} If this status is PAUSED, all its active ad sets and ads will be paused and have an effective status CAMPAIGN_PAUSED. The field returns the same value as \'configured_status\', and is the suggested one to use.',
},
stop_time: {
type: GraphQLID, // this should be a datetime
description: 'Stop Time',
},
updated_time: {
type: GraphQLID, // this should be a datetime
description: 'Updated Time',
},
}),
interfaces: [nodeInterface],
});
const userType = new GraphQLObjectType({
name: 'User',
description: 'A person who uses our app',
fields: () => ({
id: globalIdField('User'),
// advertising campaigns
campaigns: {
type: campaignConnection,
description: 'list of campaigns',
args: connectionArgs,
resolve: (viewer, args, source, info) => {
return resolveCampaigns(viewer, args, source, info);
},
},
features: {
type: featureConnection,
description: 'Features available to the logged in user',
args: connectionArgs,
resolve: (_, args) => connectionFromArray(getFeatures(), args)
},
username: {
type: GraphQLString,
description: 'Users\'s username'
},
website: {
type: GraphQLString,
description: 'User\'s website'
}
}),
interfaces: [nodeInterface]
});
const featureType = new GraphQLObjectType({
name: 'Feature',
description: 'Feature integrated in our starter kit',
fields: () => ({
id: globalIdField('Feature'),
name: {
type: GraphQLString,
description: 'Name of the feature'
},
description: {
type: GraphQLString,
description: 'Description of the feature'
},
url: {
type: GraphQLString,
description: 'Url of the feature'
}
}),
interfaces: [nodeInterface]
});
/**
* Define your own connection types here
*/
const {
connectionType: featureConnection
} = connectionDefinitions({
name: 'Feature',
nodeType: featureType
});
// Campaign list ConnectionType
const {
connectionType: campaignConnection,
} = connectionDefinitions({
name: 'Campaign',
nodeType: campaignType
});
/**
* This is the type that will be the root of our query,
* and the entry point into our schema.
*/
// Setup GraphQL RootQuery
const RootQuery = new GraphQLObjectType({
name: 'Query',
description: 'Realize Root Query',
fields: () => ({
viewer: {
type: userType,
resolve: () => '1'
},
})
});
/**
* This is the type that will be the root of our mutations,
* and the entry point into performing writes in our schema.
*/
const mutationType = new GraphQLObjectType({
name: 'Mutation',
fields: () => ({
// Add your own mutations here
})
});
/**
* Finally, we construct our schema (whose starting query type is the query
* type we defined above) and export it.
*/
export default new GraphQLSchema({
query: RootQuery
// Uncomment the following after adding some mutation fields:
// mutation: mutationType
});
I came across someone else having a similar issue and although they did not mention how they fixed it they did say this:
The problem was in my nodeDefinitions. I wasn't loading the user id correctly or identifying the node object. Once I got those working everything worked properly
Any help would be greatly appreciated.
TL;DR;
Your root query does not have a node field. That's why fetching more items fail. Add the node field like this:
const RootQuery = new GraphQLObjectType({
...
fields: () => ({
viewer: {
...
},
node: nodeField,
})
});
when I check the Request Payload I notice that it is querying "node" instead of "viewer" like it did previously.
The first time Relay fetches an object, it uses the regular query.
viewer {
id,
...F0
}
Now Relay knows the global ID of viewer. Later when more data of viewer need to be fetched, Relay uses node root field to query that object directly.
node(id:$id_0) {
...F0
}
See an excellent answer by steveluscher to how node definitions work in Relay.

Resources