I am using Apollo Server to build a graphql based server that interfaces with a MongoDB instance. I want to make it so that if the client passes a value for a field that has a graphql type of ID, the value must be a valid mongo ObjectId (mongoose has a util to check this I think -- that's not the issue). That is, if the value isn't a valid ObjectId, I would expect to get a validation error at the graphql level (i.e. a 400). I don't want to get a 500 later in my server when I try to use that value as an ObjectId, and I don't want to have to check that the value is a valid ObjectId at multiple places within the server.
Basically I want to implement validation logic at the graphql level for the ID scalar type, if that makes sense.
You'd need to utilize a custom scalar instead of ID to get the sort of behavior you're looking for. You can write your own or use an existing one.
import { Kind, GraphQLError, GraphQLScalarType, ValueNode } from 'graphql';
const MONGODB_OBJECTID_REGEX = /*#__PURE__*/ new RegExp(/^[A-Fa-f0-9]{24}$/);
export const GraphQLObjectID = /*#__PURE__*/ new GraphQLScalarType({
name: 'ObjectID',
description:
'A field whose value conforms with the standard mongodb object ID as described here: https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId. Example: 5e5677d71bdc2ae76344968c',
serialize(value: string) {
if (!MONGODB_OBJECTID_REGEX.test(value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${value}`,
);
}
return value;
},
parseValue(value: string) {
if (!MONGODB_OBJECTID_REGEX.test(value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${value}`,
);
}
return value;
},
parseLiteral(ast: ValueNode) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as mongodb object id but got a: ${ast.kind}`,
);
}
if (!MONGODB_OBJECTID_REGEX.test(ast.value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${ast.value}`,
);
}
return ast.value;
},
});
Related
I have a Dexie.js database with the table "businessLayers" in my React application. I'd like to ensure de data types of the tuples inserted in that table. I thought the method Table.defineClass() would do that, but it does not. My db is the following:
import Dexie from 'dexie';
const db = new Dexie('MyDB');
db.version(1).stores({
businessLayers: '++id, layer'
});
const BusinessLayer = db.businessLayers.defineClass({
id: Number,
layer: String,
values: Object
});
export default db;
I'd like to make not possible to insert an invalid data type on each field. I haven't found any built-in method to do this. Do you know any? Thank you!
Table.defineClass() was an old feature in Dexie 1.x for code completion only - no enforcements. The method should have been deprecated. But the functionality you need can be implemented using a DBCore middleware or creating/updating hooks. DBCore middlware would be the most performant solution as it does not need to verify existing data.
Below is a dry coded full example. Please test and reply if it works. It should support String, Number, Boolean, Array, Object, Set, Map, ArrayBuffer, Uint8Array, etc... and even custom classes. If anyone wants to make a package of this code, please go ahead! I think it could be a nice addon to dexie:
import Dexie from 'dexie';
const db = new Dexie('MyDB');
db.version(1).stores({
businessLayers: '++id, layer'
});
// Use a DBCore middleware "enforceSchema" defined further down...
db.use(
enforceSchema({
businessLayers: {
id: Number,
layer: String,
values: Object
}
}
);
// This is the function that returns the middlware:
function enforceSchema(dbSchema) {
return {
stack: "dbcore",
name: "SchemaEnforcement",
create (downlevelDatabase) {
return {
...downlevelDatabase,
table (tableName) {
const downlevelTable = downlevelDatabase.table(tableName);
const tableSchema = dbSchema[tableName];
if (!tableSchema) return downlevelTable; // No schema for this table.
return {
...downlevelTable,
mutate: req => {
if (req.type === "add" || req.type === "put") {
for (obj of req.values) {
validateSchema(tableName, tableSchema, obj);
}
}
return downlevelTable.mutate(req);
}
}
}
};
}
};
}
function validateSchema(tableName, schema, obj) {
const invalidProp = Object.keys(schema).find(key =>
{
const value = obj[key];
const type = schema[key];
switch (type) {
// Handle numbers, strings and booleans specifically:
case Number: return typeof value !== "number";
case String: return typeof value !== "string";
case Boolean: return typeof value !== "boolean";
// All other types will be supported in the following
// single line:
default: return !(value instanceof type);
}
});
if (invalidProp) {
// Throw exception to abort the transaction and make the
// user get a rejected promise:
throw new TypeError(`Invalid type given for property ${invalidProp} in table ${tableName}. ${schema[invalidProp].name} expected.`);
}
}
I'm trying to figure out how to pass an array of objects into my GraphQL query, however i'm finding the documentation a little unclear on how to do so. I'm working with Apollo in the FE, Graphql-yoga in the BE and using Prisma as my database along with their API.
Here is my query with the array of objects hard coded:
const USERS = gql`
query USERS(
$userId: ID
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: [
{ itemId: 1 },
{ itemId: 2 }
]
}
}
) {
firstName
}
}
`;
The above query returns me what I want, where i'm a bit stuck is how to get this array:
[
{ itemId: 1 },
{ itemId: 2 }
]
passed in as a variable of the query. From what I could find online, I might need to create a GraphQLObjectType on the client side to be able to pass in an object definition. Here was my implementation of that:
import { GraphQLObjectType, GraphQLString } from 'graphql';
const ProductName = new GraphQLObjectType({
name: 'ProductName',
fields: () => ({
itemId: {
type: GraphQLString,
},
})
});
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: [ProductName]
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
The above returns me the following error:
Unknown type "ProductName"
Have I gone with the correct approach here for passing in arrays of objects, if so what's wrong with my implementation?
Types are created and used in creating your schema server-side. Once created, the schema cannot be modified at runtime -- it has whatever types and directives were specified when it was created. In other words, defining a new type on the client-side is meaningless -- it can't be used in any queries you send to the server since the server is not aware of the type.
If a variable (like $hasProducts) is passed to an argument (like hasProducts_some), that variable's type must match the type of the argument. This type could be a scalar (like String or Int) or it could be an input object type. What exact type that that is depends on the schema itself. To determine the type to use, you can open up your schema's documentation in GraphQL Playground (or GraphiQL) and search for the field in question (in this case, hasProducts_some).
Note that you can also just pass a single variable in for the whole where field.
Since the gql function expects a template literal, you should escape the product object like so:
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: [${ProductName}]
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
New to graphql. But was wondering if this can resolve it.
const USERS = gql`
query USERS(
$userId: ID,
$hasProducts: GraphQLList(ProductName)
) {
users(
where: {
id_not: $userId
hasProducts_some: {
OR: $hasProducts
}
}
) {
firstName
}
}
`;
Minor change, but am not privileged to comment . So posting it as answer.
I am looking for a way to modify the response object of a graphql query or mutation before it gets sent out.
Basically in addition the the data object, I want to have extra fields like code and message.
At the moment I am solving this by adding the fields directly into my GQL schemas take this type definition for example:
type Query {
myItems: myItemResponse
}
type myItemResponse {
myItem: Item
code: String!
success: Boolean!
message: String!
}
The response itself would be look like that:
{
data: {
myItems: {
myItem: [ ... fancy Items ... ],
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
}
}
I find that solution not nice because it overcomplicates things in my FrontEnd.
I would prefer a solution where message code and other Metadata are seperated from the actual data, so something like this:
{
data: {
myItems: [ ... fancy Items ... ],
},
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
With apollo-server I already tried the formatResponse object in the constructor:
const server = new ApolloServer({
...
formatResponse({ data }) {
return {
data,
test: 'Property to test if shown in the FrontEnd',
}
}
...
}
unfortunately that doesn't have the desired effect. Before I use express middlewares I want to ask if there is a possibility to do this via apollo-server out of the box or if I am maybe just missing something in the formatResponse function.
from graphql.org:
A response to a GraphQL operation must be a map.
If the operation encountered any errors, the response map must contain an entry with key errors. The value of this entry is described in the “Errors” section. If the operation completed without encountering any errors, this entry must not be present.
If the operation included execution, the response map must contain an entry with key data. The value of this entry is described in the “Data” section. If the operation failed before execution, due to a syntax error, missing information, or validation error, this entry must not be present.
The response map may also contain an entry with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementors to extend the protocol however they see fit, and hence there are no additional restrictions on its contents.
To ensure future changes to the protocol do not break existing servers and clients, the top level response map must not contain any entries other than the three described above.
After doing a lot of research I found out that the only allowed top level properties in a graphql responses are data, errors, extensions. Here you can find the regarding Issue in GitHub
GitHub Issue
for my purpose I will probably use the extensions field.
Example data modifier
This function will concat ":OK" suffix on each string in the output object
// Data/output modifier - concat ":OK" after each string
function outputModifier(input: any): any {
const inputType = typeof input;
if (inputType === 'string') {
return input + ':OK';
} else if (Array.isArray(input)) {
const inputLength = input.length;
for (let i = 0; i < inputLength; i += 1) {
input[i] = outputModifier(input[i]);
}
} else if (inputType === 'object') {
for (const key in input) {
if (input.hasOwnProperty(key)) {
input[key] = outputModifier(input[key]);
}
}
}
return input;
}
Solution 1 - Override GraphQL Resolvers
Long story short: you have 3 main types (Query, Mutation, and Subscription).
Each main type has fields with resolvers.
The resolvers are returning the output data.
So if you override the resolvers you will be able to modify the outputs.
Example transformer
import { GraphQLSchema } from 'graphql';
export const exampleTransformer = (schema: GraphQLSchema): GraphQLSchema => {
// Collect all main types & override the resolvers
[
schema?.getQueryType()?.getFields(),
schema?.getMutationType()?.getFields(),
schema?.getSubscriptionType()?.getFields()
].forEach(fields => {
// Resolvers override
Object.values(fields ?? {}).forEach(field => {
// Check is there any resolver at all
if (typeof field.resolve !== 'function') {
return;
}
// Save the original resolver
const originalResolve = field.resolve;
// Override the current resolver
field.resolve = async (source, inputData, context, info) => {
// Get the original output
const outputData: any = await originalResolve.apply(originalResolve.prototype, [source, inputData, context, info]);
// Modify and return the output
return outputModifier(outputData);
};
});
});
return schema;
};
How to use it:
// Attach it to the GraphQLSchema > https://graphql.org/graphql-js/type/
let schema = makeExecutableSchema({...});
schema = exampleTransformer(schema);
const server = new ApolloServer({schema});
server.listen(serverConfig.port);
This solution will work on any GraphQL-JS service (apollo, express-graphql, graphql-tools, etc.).
Keep in min with this solution you will be able to manipulate the inputData too.
Solution 2 - Modify the response
This solution is more elegant, but is implemented after the implementation of the directives and scalar types and can not manipulate the input data.
The specific for the output object is that the data is null-prototype object (no instance methods like .hasOwnProperty(), .toString(), ...) and the errors are locked objects (readonly).
In the example I'm unlocking the error object... be careful with this and do not change the structure of the objects.
Example transformer
import { Translator } from '#helpers/translations';
import type { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
import type { GraphQLFormattedError } from 'graphql';
export const exampleResponseFormatter = () => (response: GraphQLResponse, requestContext: GraphQLRequestContext) => {
// Parse locked error fields
response?.errors?.forEach(error => {
(error['message'] as GraphQLFormattedError['message']) = exampleTransformer(error['message']);
(error['extensions'] as GraphQLFormattedError['extensions']) = exampleTransformer(error['extensions']);
});
// Parse response data
response.data = exampleTransformer(response.data);
// Response
return response;
};
How to use it:
// Provide the schema to the ApolloServer constructor
const server = new ApolloServer({
schema,
formatResponse: exampleResponseFormatter()
});
Conclusion
I'm using both solutions in my projects. With the first you can control the input and the output based on specific access directives in the code or to validate the whole data flow (on any graphql type) .
And second to translate all the strings based on the context headers provided by the user without messing resolvers and the code with language variables.
Those examples are tested on TS 4+ and GraphQL 15 and 16
Is it possible to specify that a field in GraphQL should be a blackbox, similar to how Flow has an "any" type? I have a field in my schema that should be able to accept any arbitrary value, which could be a String, Boolean, Object, Array, etc.
I've come up with a middle-ground solution. Rather than trying to push this complexity onto GraphQL, I'm opting to just use the String type and JSON.stringifying my data before setting it on the field. So everything gets stringified, and later in my application when I need to consume this field, I JSON.parse the result to get back the desired object/array/boolean/ etc.
#mpen's answer is great, but I opted for a more compact solution:
const { GraphQLScalarType } = require('graphql')
const { Kind } = require('graphql/language')
const ObjectScalarType = new GraphQLScalarType({
name: 'Object',
description: 'Arbitrary object',
parseValue: (value) => {
return typeof value === 'object' ? value
: typeof value === 'string' ? JSON.parse(value)
: null
},
serialize: (value) => {
return typeof value === 'object' ? value
: typeof value === 'string' ? JSON.parse(value)
: null
},
parseLiteral: (ast) => {
switch (ast.kind) {
case Kind.STRING: return JSON.parse(ast.value)
case Kind.OBJECT: throw new Error(`Not sure what to do with OBJECT for ObjectScalarType`)
default: return null
}
}
})
Then my resolvers looks like:
{
Object: ObjectScalarType,
RootQuery: ...
RootMutation: ...
}
And my .gql looks like:
scalar Object
type Foo {
id: ID!
values: Object!
}
Yes. Just create a new GraphQLScalarType that allows anything.
Here's one I wrote that allows objects. You can extend it a bit to allow more root types.
import {GraphQLScalarType} from 'graphql';
import {Kind} from 'graphql/language';
import {log} from '../debug';
import Json5 from 'json5';
export default new GraphQLScalarType({
name: "Object",
description: "Represents an arbitrary object.",
parseValue: toObject,
serialize: toObject,
parseLiteral(ast) {
switch(ast.kind) {
case Kind.STRING:
return ast.value.charAt(0) === '{' ? Json5.parse(ast.value) : null;
case Kind.OBJECT:
return parseObject(ast);
}
return null;
}
});
function toObject(value) {
if(typeof value === 'object') {
return value;
}
if(typeof value === 'string' && value.charAt(0) === '{') {
return Json5.parse(value);
}
return null;
}
function parseObject(ast) {
const value = Object.create(null);
ast.fields.forEach((field) => {
value[field.name.value] = parseAst(field.value);
});
return value;
}
function parseAst(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT:
return parseObject(ast);
case Kind.LIST:
return ast.values.map(parseAst);
default:
return null;
}
}
For most use cases, you can use a JSON scalar type to achieve this sort of functionality. There's a number of existing libraries you can just import rather than writing your own scalar -- for example, graphql-type-json.
If you need a more fine-tuned approach, than you'll want to write your own scalar type. Here's a simple example that you can start with:
const { GraphQLScalarType, Kind } = require('graphql')
const Anything = new GraphQLScalarType({
name: 'Anything',
description: 'Any value.',
parseValue: (value) => value,
parseLiteral,
serialize: (value) => value,
})
function parseLiteral (ast) {
switch (ast.kind) {
case Kind.BOOLEAN:
case Kind.STRING:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return Number(ast.value)
case Kind.LIST:
return ast.values.map(parseLiteral)
case Kind.OBJECT:
return ast.fields.reduce((accumulator, field) => {
accumulator[field.name.value] = parseLiteral(field.value)
return accumulator
}, {})
case Kind.NULL:
return null
default:
throw new Error(`Unexpected kind in parseLiteral: ${ast.kind}`)
}
}
Note that scalars are used both as outputs (when returned in your response) and as inputs (when used as values for field arguments). The serialize method tells GraphQL how to serialize a value returned in a resolver into the data that's returned in the response. The parseLiteral method tells GraphQL what to do with a literal value that's passed to an argument (like "foo", or 4.2 or [12, 20]). The parseValue method tells GraphQL what to do with the value of a variable that's passed to an argument.
For parseValue and serialize we can just return the value we're given. Because parseLiteral is given an AST node object representing the literal value, we have to do a little bit of work to convert it into the appropriate format.
You can take the above scalar and customize it to your needs by adding validation logic as needed. In any of the three methods, you can throw an error to indicate an invalid value. For example, if we want to allow most values but don't want to serialize functions, we can do something like:
if (typeof value == 'function') {
throw new TypeError('Cannot serialize a function!')
}
return value
Using the above scalar in your schema is simple. If you're using vanilla GraphQL.js, then use it just like you would any of the other scalar types (GraphQLString, GraphQLInt, etc.) If you're using Apollo, you'll need to include the scalar in your resolver map as well as in your SDL:
const resolvers = {
...
// The property name here must match the name you specified in the constructor
Anything,
}
const typeDefs = `
# NOTE: The name here must match the name you specified in the constructor
scalar Anything
# the rest of your schema
`
Just send a stringified value via GraphQL and parse it on the other side, e.g. use this wrapper class.
export class Dynamic {
#Field(type => String)
private value: string;
getValue(): any {
return JSON.parse(this.value);
}
setValue(value: any) {
this.value = JSON.stringify(value);
}
}
For similar problem I've created schema like this:
"""`MetadataEntry` model"""
type MetadataEntry {
"""Key of the entry"""
key: String!
"""Value of the entry"""
value: String!
}
"""Object with metadata"""
type MyObjectWithMetadata {
"""
... rest of my object fields
"""
"""
Key-value entries that you can attach to an object. This can be useful for
storing additional information about the object in a structured format
"""
metadata: [MetadataEntry!]!
"""Returns value of `MetadataEntry` for given key if it exists"""
metadataValue(
"""`MetadataEntry` key"""
key: String!
): String
}
And my queries can look like this:
query {
listMyObjects {
# fetch meta values by key
meta1Value: metadataValue(key: "meta1")
meta2Value: metadataValue(key: "meta2")
# ... or list them all
metadata {
key
value
}
}
}
As background, I'm using Prisma (graphql), mysql2 (nodejs) and typescript.
I'm using an interactive command line script to connect to mysql2.
This is the warning I am seeing:
Ignoring invalid configuration option passed to Connection: type. This
is currently a warning, but in future versions of MySQL2, an error
will be thrown if you pass an invalid configuration options to a
Connection
This is how I instantiate mysql2, in my MysqlConnector class:
this.connectionPromise = await this.mysql.createConnection(this.connectionOptions)
"connectionOptions" is set in the class constructor:
constructor(connectionDetails: MysqlConnectionDetails) {
This is my type definition:
export interface MysqlConnectionDetails {
host: string
port: number
user: string
password: string
database?: string
}
This is the type definition of the object that I pass into my MysqlConnector class:
export interface DatabaseCredentials {
type: DatabaseType
host: string
port: number
user: string
password: string
database?: string
alreadyData?: boolean
schema?: string
ssl?: boolean
filter?: any
}
So, I am passing in an object that has additional parameters that mysql2 does not want/need. I am new to Typescript. I tried this, before passing the object in to the MysqlConnector class:
let forDeletion = ['type', 'alreadyData']
connector = new MysqlConnector(credentials.filter(item => !forDeletion.includes(item)))
But I get an error, saying "filter" is not a property on the type "DatabaseCredentials", and I concluded this is probably not the right way to do this.
I assumed there is a way in Typescript (through tsconfig) automatically to filter out properties that are not in the receiving type. "MysqlConnectionDetails" doesn't have a property "type", in its definition, so it gets it dynamically, when I pass in another type.
Not sure if there's a specific TypeScript way to deal with this, but you can use destructuring/spread for this:
const { type, ...validCredentials } = credentials;
This essentially picks type out of credentials, and validCredentials will contain the rest of the properties.
The reason .filter didn't work is because filter is a prototype method on arrays, not objects.
If you insist on using Array methods, .map() is probably most helpful:
let obj2big = { a: 1, b: 2, c: 3 }
// let's assume what you want from this is { a: 1, c: 3 }
let arr = [obj2big]
let result = arr.map(i=>({a:i.a, c: i.c}))
console.log(result[0])