Apollo GraphQL Server - Access query params from cache plugin - node.js

I have an Apollo GraphQL server using the apollo-server-plugin-response-cache plugin and I need to determine whether or not I'm going to write to the cache based on incoming parameters. I have the plugin set up and I'm using the shouldWriteToCache hook. I can print out the GraphQLRequestContext object that gets passed into the hook, and I can see the full request source, but request.variables is empty. Other than parsing the query itself, how can I access the actual params for the resolver in this hook? (In the example below, I need the value of param2.)
Apollo Server:
new ApolloServer({
introspection: true,
playground: true,
subscriptions: false,
typeDefs,
resolvers,
cacheControl: {
defaultMaxAge: 60
},
plugins: [
apolloServerPluginResponseCache({
cache, // This is a "apollo-server-cache-redis" instance
shouldWriteToCache: (requestContext) => {
// I get a lot of info here, including the source query, but not the
// parsed out query variables
console.log(requestContext.request);
// What I want to do here is:
return !context.request.variables.param2
// but `variables` is empty, and I can't see that value parsed anywhere else
}
})
]
})
Here is my resolver:
export async function exapi(variables, context) {
// in here I use context.param1 and context.param2
// ...
}
I have also tried:
export async function exapi(variables, { param1, param2 }) {
// ...
}
Here is what I get logged out from the code above:
{
query: '{\n' +
' exapi(param1: "value1", param2: true) {\n' +
' records\n' +
' }\n' +
'}\n',
operationName: null,
variables: {}, // <-- this is empty?! How can I get param2's value??
extensions: undefined,
http: Request {
size: 0,
timeout: 0,
follow: 20,
compress: true,
counter: 0,
agent: undefined,
[Symbol(Body internals)]: { body: null, disturbed: false, error: null },
[Symbol(Request internals)]: {
method: 'POST',
redirect: 'follow',
headers: [Headers],
parsedURL: [Url],
signal: null
}
}
}

If you didn't provide variables for GraphQL query, you could get the arguments from the GraphQL query string via ArgumentNode of AST
If you provide variables for GraphQL query, you will get them from requestContext.request.variables.
E.g.
server.js:
import apolloServerPluginResponseCache from 'apollo-server-plugin-response-cache';
import { ApolloServer, gql } from 'apollo-server';
import { RedisCache } from 'apollo-server-cache-redis';
const typeDefs = gql`
type Query {
exapi(param1: String, param2: Boolean): String
}
`;
const resolvers = {
Query: {
exapi: (_, { param1, param2 }) => 'teresa teng',
},
};
const cache = new RedisCache({ host: 'localhost', port: 6379 });
const server = new ApolloServer({
introspection: true,
playground: true,
subscriptions: false,
typeDefs,
resolvers,
cacheControl: {
defaultMaxAge: 60,
},
plugins: [
apolloServerPluginResponseCache({
cache,
shouldWriteToCache: (requestContext) => {
console.log(requestContext.document.definitions[0].selectionSet.selections[0].arguments);
return true;
},
}),
],
});
server.listen().then(({ url }) => console.log(`🚀 Server ready at ${url}`));
GraphQL query:
query{
exapi(param1: "value1", param2: true)
}
Server logs print param1 and param2 arguments:
🚀 Server ready at http://localhost:4000/
[]
[ { kind: 'Argument',
name: { kind: 'Name', value: 'param1', loc: [Object] },
value:
{ kind: 'StringValue',
value: 'value1',
block: false,
loc: [Object] },
loc: { start: 15, end: 31 } },
{ kind: 'Argument',
name: { kind: 'Name', value: 'param2', loc: [Object] },
value: { kind: 'BooleanValue', value: true, loc: [Object] },
loc: { start: 33, end: 45 } } ]

Related

Can a push notification be send from the server without a client call?

I have a javascript file on my NodeJS server that runs at 00:00:00 and updates some fields in the database, if that happens I want to send out a push notification to some users. I've set this up in my Javascript file:
https://dev.to/devsmranjan/web-push-notification-with-web-push-angular-node-js-36de
const subscription = {
endpoint: '',
expirationTime: null,
keys: {
auth: '',
p256dh: '',
},
};
const payload = {
notification: {
title: 'Title',
body: 'This is my body',
icon: 'assets/icons/icon-384x384.png',
actions: [
{action: 'bar', title: 'Focus last'},
{action: 'baz', title: 'Navigate last'},
],
data: {
onActionClick: {
default: {operation: 'openWindow'},
bar: {
operation: 'focusLastFocusedOrOpen',
url: '/signin',
},
baz: {
operation: 'navigateLastFocusedOrOpen',
url: '/signin',
},
},
},
},
};
const options = {
vapidDetails: {
subject: 'mailto:example_email#example.com',
publicKey: process.env.REACT_APP_PUBLIC_VAPID_KEY,
privateKey: process.env.REACT_APP_PRIVATE_VAPID_KEY,
},
TTL: 60,
};
webpush.sendNotification(subscription, JSON.stringify(payload), options)
.then((_) => {
console.log(subscription);
console.log('SENT!!!');
console.log(_);
})
.catch((_) => {
console.log(subscription);
console.log(_);
});
But when I run the file I get the message:
{ endpoint: '', expirationTime: null, keys: { auth: '', p256dh: '' } } Error: You must pass in a subscription with at least an endpoint.
Which makes sense since the server has no idea about service workers etc. Any suggestions on how to proceed?

Why am I getting this error when I run my Discord Bot? - code: 50035

When I run my Discord bot, which is made through the Discord.js and Node.js framework, I receive the following error.
node_modules\#discordjs\rest\dist\lib\handlers\SequentialHandler.cjs:293
throw new DiscordAPIError.DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^
DiscordAPIError[50035]: Invalid Form Body
options[0].type[NUMBER_TYPE_COERCE]: Value "string" is not int.
options[1].type[NUMBER_TYPE_COERCE]: Value "string" is not int.
at SequentialHandler.runRequest (D:\Programming\GitHub\void-bot\node_modules\#discordjs\rest\dist\lib\handlers\SequentialHandler.cjs:293:15)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async SequentialHandler.queueRequest (D:\Programming\GitHub\void-bot\node_modules\#discordjs\rest\dist\lib\handlers\SequentialHandler.cjs:99:14)
at async REST.request (D:\Programming\GitHub\void-bot\node_modules\#discordjs\rest\dist\lib\REST.cjs:52:22)
at async GuildApplicationCommandManager.create (D:\Programming\GitHub\void-bot\node_modules\discord.js\src\managers\ApplicationCommandManager.js:144:18) {
rawError: {
code: 50035,
errors: {
options: {
'0': { type: { _errors: [Array] } },
'1': { type: { _errors: [Array] } }
}
},
message: 'Invalid Form Body'
},
code: 50035,
status: 400,
method: 'POST',
url: 'https://discord.com/api/v10/applications/1016623217399570512/guilds/915541815225118740/commands',
requestBody: {
files: undefined,
json: {
name: 'poll',
name_localizations: undefined,
description: 'Sets up a poll.',
description_localizations: undefined,
type: undefined,
options: [
{
type: 'string',
name: 'title',
name_localizations: undefined,
name_localized: undefined,
description: 'The title of the poll.',
description_localizations: undefined,
description_localized: undefined,
required: true,
autocomplete: undefined,
choices: undefined,
options: undefined,
channel_types: undefined,
min_value: undefined,
max_value: undefined,
min_length: undefined,
max_length: undefined
},
{
type: 'string',
name: 'content',
name_localizations: undefined,
name_localized: undefined,
description: 'The content of the poll.',
description_localizations: undefined,
description_localized: undefined,
required: true,
autocomplete: undefined,
choices: undefined,
options: undefined,
channel_types: undefined,
min_value: undefined,
max_value: undefined,
min_length: undefined,
max_length: undefined
}
],
default_member_permissions: undefined,
dm_permission: undefined
}
}
}
I am unsure why I am receiving this for the code below:
require("dotenv").config();
const { GatewayIntentBits } = require("discord.js");
const Discord = require("discord.js");
const PREFIX = "!";
const client = new Discord.Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildBans,
GatewayIntentBits.GuildEmojisAndStickers,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildWebhooks,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessageTyping,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessageTyping,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildScheduledEvents
]
});
client.on("ready", () => {
console.log(`Logged in as ${client.user.tag}!`);
const guildID = "915541815225118740";
const guild = client.guilds.cache.get(guildID);
let commands;
if (guild) {
commands = guild.commands;
} else {
commands = client.application?.commands;
}
commands?.create({
name: "poll",
description: "Sets up a poll.",
options: [
{
name: "title",
description: "The title of the poll.",
required: true,
type: "string"
},
{
name: "content",
description: "The content of the poll.",
required: true,
type: "string"
}
]
});
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand()) {
return;
}
const { commandName, options } = interaction;
if (commandName === "poll") {
const embed = new Discord.EmbedBuilder()
.setColor(0x0099FF)
.setTitle(options.get("title"))
.addFields(
{
value: options.get("content"),
inline: false
},
{
value: ":thumbsup: - Yes",
inline: true
},
{
value: ":thumbsdown: - No",
inline: true
}
)
}
interaction.reply(embed);
});
client.login(process.env.TOKEN);
I hope someone can help me out with this. It would be greatly appreciated as I have been stuck on this issue for a couple of days now.
Thanks,
nozzy
You've specified the option type using a string literal. It should be a value of the ApplicationCommandOptionType enum. So instead you should be using:
{
name: "...",
description: "...",
required: ...
type: ApplicationCommandOptionType.STRING
}

Can't connect to Atlas cluster from my lambda using mongoose

I am trying to connect to a cluster using the last example from mongoose site
Here are my files using node14 and typescript
src/index.ts
import { APIGatewayProxyHandler } from "aws-lambda"
export { list as productsList } from "./products"
export const list: APIGatewayProxyHandler = (event, context, callback) => {
callback(null, {
statusCode: 200,
body: `Hello from ${process.env.AWS_SAM_BRANCH}`,
})
}
src/utils.ts
import mongoose from "mongoose"
import { APIGatewayProxyResult, Callback } from "aws-lambda"
let mongoConnection: Promise<typeof mongoose> | null = null
export const connectMongoose = async () => {
if (mongoConnection == null) {
const mongoURI = `mongodb+srv://USER:PASS#cluster0.ohjoj.mongodb.net/myFirstDB?retryWrites=true&w=majority`
mongoConnection = mongoose
.connect(mongoURI, { serverSelectionTimeoutMS: 3000 })
.then((mongooseReply) => {
console.log({ mongooseReply })
return mongoose
})
.catch((mongooseError) => {
console.log({ mongooseError })
return mongoose
})
await mongoConnection
}
return mongoConnection
}
export const errorHandler = (error: unknown, callback: Callback<APIGatewayProxyResult>) => {
console.error("catchedError", error)
if (error instanceof Error) {
callback(null, {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: error.message }),
})
} else {
callback(null, {
statusCode: 500,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: "Internal server error" }),
})
}
}
src/products/index.ts
import { APIGatewayProxyHandler } from "aws-lambda"
import Model from "./model"
import { connectMongoose, errorHandler } from "../utils"
export const list: APIGatewayProxyHandler = (event, context, callback) => {
try {
connectMongoose()
Model.find({}, (error: unknown, reply: unknown) => {
if (error) throw error
callback(null, {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reply),
})
})
} catch (error) {
errorHandler(error, callback)
}
}
src/products/model.ts
import mongoose from "mongoose"
const model = mongoose.model(
"Product",
new mongoose.Schema(
{
title: {
type: String,
required: true,
maxLength: 256,
},
description: {
type: String,
required: true,
maxLength: 2048,
},
count: {
type: Number,
required: true,
min: 0,
max: 1000 * 1000,
},
},
{
timestamps: true,
versionKey: false,
}
)
)
export default model
Here is the code in a repo is includes commands used to deploy with AWS SAM
https://github.com/LuisEnMarroquin/aws-sam-mongo
There are 2 routes in my app
https://2us58gl430.execute-api.us-east-2.amazonaws.com/
This works and returns Hello from test with status 200
https://2us58gl430.execute-api.us-east-2.amazonaws.com/products
This doesn't work and returns {"message":"Internal Server Error"} with status 500
Here are the CloudWatch logs exported as CSV
timestamp,message
1647203544609,"START RequestId: 83fd3fc8-1134-4ff4-a5f7-7e83a65159ce Version: $LATEST
"
1647203545742,"2022-03-13T20:32:25.685Z 83fd3fc8-1134-4ff4-a5f7-7e83a65159ce INFO {
mongooseReply: <ref *1> Mongoose {
connections: [ [NativeConnection] ],
models: { Product: Model { Product } },
events: EventEmitter {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
[Symbol(kCapture)]: false
},
options: {
pluralization: true,
autoIndex: true,
autoCreate: true,
[Symbol(mongoose:default)]: true
},
_pluralize: [Function: pluralize],
Schema: [Function: Schema] {
reserved: [Object: null prototype],
Types: [Object],
ObjectId: [Function]
},
model: [Function (anonymous)],
plugins: [ [Array], [Array], [Array], [Array], [Array], [Array] ],
default: [Circular *1],
mongoose: [Circular *1]
}
}
"
1647203549616,"END RequestId: 83fd3fc8-1134-4ff4-a5f7-7e83a65159ce
"
1647203549616,"REPORT RequestId: 83fd3fc8-1134-4ff4-a5f7-7e83a65159ce Duration: 5005.75 ms Billed Duration: 5000 ms Memory Size: 128 MB Max Memory Used: 76 MB Init Duration: 366.30 ms
"
1647203549616,"2022-03-13T20:32:29.616Z 83fd3fc8-1134-4ff4-a5f7-7e83a65159ce Task timed out after 5.01 seconds
"
As explained in this GitHub issue
A few suggestions:
You should either choose between a full callback approach and a full promise approach
Don't mix async / await with .then syntax when you can avoid it
import mongoose from "mongoose"
import { APIGatewayProxyHandler } from "aws-lambda"
let mongoConnection: Promise<typeof mongoose> | null = null
const connectMongoose = async () => {
if (mongoConnection == null) {
const mongoURI = `mongodb+srv://YOUR_CLUSTER_URL`
mongoConnection = mongoose
.connect(mongoURI, { serverSelectionTimeoutMS: 3000 })
.then((mongooseReply) => {
console.log({ mongooseReply })
return mongoose
})
.catch((mongooseError) => {
console.log({ mongooseError })
return mongoose
})
await mongoConnection
}
return mongoConnection
}
const Model = mongoose.model(
"Product",
new mongoose.Schema(
{
title: String,
description: String,
},
{
timestamps: true,
versionKey: false,
}
)
)
export const myRoute: APIGatewayProxyHandler = async (event, context) => {
try {
await connectMongoose();
const reply = await Model.find({}).exec();
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reply),
};
} catch (error) {
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: "Server error",
};
}
}

how to test Router.push options with Jest / React

I'm new to testing with routing and was wondering how I would go about testing this
const handleFilter = (items: ParsedUrlQueryInput) => {
push({ pathname: '/games', query: items }, undefined, { scroll: false });
return;
};
<S.ShowMoreButton role="button" onClick={handleShowMore}
I made some mocks and the following test worked without the options, but when I add options, it's impossible to test
mock
const useRouter = jest.spyOn(require('next/router'), 'useRouter');
const push = jest.fn();
useRouter.mockImplementation(() => ({
push,
query: '',
asPath: '',
route: '/',
}));
test
expect(push).toHaveBeenCalledWith({
pathname: '/games',
query: { platforms: ['windows', 'linux'], sort_by: 'low-to-high' },
});
error in test
Expected: {"pathname": "/games", "query": {"platforms": ["windows", "linux"], "sort_by": "low-to-high"}}
Received
1
Object {
"pathname": "/games",
- "query": Object {
- "platforms": Array [
- "windows",
- "linux",
- ],
- "sort_by": "low-to-high",
- },
+ "query": Object {},
},
+ undefined,
+ {"scroll": false}

Sequelize and response request GraphQL

I try to have a response on my request GraphQL.
I tried many things but currently I have always the Sequence response, and no the Buckets response (belongs To relation).
I have 2 tables :
Sequence [id | is_active]
Bucket [id | fk_language_id | fk_sequence_id | is_active]
model/sequence.js
'use strict';
module.exports = (sequelize, DataTypes) => {
// define sequence
const Sequence = sequelize.define('sequence', {
is_active: {type: DataTypes.BOOLEAN}
});
Sequence.associate = function (models) {
models.Sequence.hasMany(models.Bucket, {
foreignKey: 'fk_sequence_id'
});
return Sequence;
};
model/bucket.js
'use strict';
module.exports = (sequelize, DataTypes) => {
const Bucket = sequelize.define('bucket', {
code : {type: DataTypes.STRING},
is_active: {type: DataTypes.BOOLEAN}
});
Bucket.associate = function (models) {
models.Bucket.belongsTo(models.Language, {
foreignKey: 'fk_language_id',
});
models.Bucket.belongsTo(models.Sequence, {
foreignKey: 'fk_sequence_id',
});
};
return Bucket;
};
schema.js
# Sequence
type Sequence {
id: Int!,
code: String,
buckets: [Bucket],
is_active: Boolean
}
# Bucket
type Bucket {
id: Int!,
code: String
blocks: [Block]
is_active: Boolean
}
# SequenceInput
input SequenceInput {
buckets: [BucketInput],
is_active: Boolean
}
# BucketInput
input BucketInput {
code: String,
fk_language_id: Int,
fk_sequence_id: Int,
is_active: Boolean
}
type Query {
sequences: [Sequence]
sequence(id: Int): Sequence
buckets: [Bucket]
bucket(id: Int): Bucket
}
type Mutation {
createSequence(input: SequenceInput): Sequence,
}
Request GraphQL
mutation {
createSequence(input: {
is_active: false,
buckets: [
{fk_language_id: 2, code: "Test"}
]
}) {
is_active,
buckets {
id,
code
}
}
}
But I have this result, the Buckets doesn't load :
{
"data": {
"createSequence": {
"is_active": false,
"buckets": []
}
}
}
my mutation :
...
Sequence : {
buckets(sequence) {
return models.Bucket.findAll({
where: {id: sequence.id}
});
},
...
},
...
Mutation : {
createSequence(_, {input}) {
let sequenceId = 0;
// Create Sequence
return models.Sequence.create(input)
.then((sequence) => {
sequenceId = sequence.id;
console.log('sequence created');
// Create Bucket
// Foreach on buckets
return Promise.map(input.buckets, function (bucket) {
bucket.fk_sequence_id = sequenceId;
console.log('bucket created');
return models.Bucket.create(bucket);
})
})
.then(() => {
console.log('load created', sequenceId);
return models.Sequence.findOne({
where : {id: sequenceId},
include: [
{
model: models.Bucket,
where: { fk_sequence_id: sequenceId }
}
]
}).then((response) => {
console.log(response);
return response;
})
});
},
}
The final console.log show many informations...
sequence {
dataValues:
{ id: 416,
is_active: false,
created_at: 2019-03-29T20:33:56.196Z,
updated_at: 2019-03-29T20:33:56.196Z,
buckets: [ [Object] ] },
_previousDataValues:
{ id: 416,
is_active: false,
created_at: 2019-03-29T20:33:56.196Z,
updated_at: 2019-03-29T20:33:56.196Z,
buckets: [ [Object] ] },
_changed: {},
_modelOptions:
{ timestamps: true,
validate: {},
freezeTableName: true,
underscored: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: { id: 416 },
schema: null,
schemaDelimiter: '',
defaultScope: {},
scopes: {},
indexes: [],
name: { plural: 'sequences', singular: 'sequence' },
omitNull: false,
createdAt: 'created_at',
updatedAt: 'updated_at',
sequelize:
Sequelize {
options: [Object],
config: [Object],
dialect: [Object],
queryInterface: [Object],
models: [Object],
modelManager: [Object],
connectionManager: [Object],
importCache: [Object],
test: [Object] },
hooks: {} },
_options:
{ isNewRecord: false,
_schema: null,
_schemaDelimiter: '',
include: [ [Object] ],
includeNames: [ 'buckets' ],
includeMap: { buckets: [Object] },
includeValidated: true,
attributes: [ 'id', 'is_active', 'created_at', 'updated_at' ],
raw: true },
isNewRecord: false,
buckets:
[ bucket {
dataValues: [Object],
_previousDataValues: [Object],
_changed: {},
_modelOptions: [Object],
_options: [Object],
isNewRecord: false } ] }
Your mutation resolver returns a Promise, which resolves into a Model instance. The promise in question is returned on this line:
return models.Sequence.create(input)
.
As such, the server will wait until that promise is resolved before passing the value forward. Other actions were also waiting on that promise, but they were not the promises returned, so they will not be waited for.
All you have to do is wait for all of your operations to finish before resolving your promise.
createSequence: async (parent, { input }) => {
const sequence = await models.Sequence.create({
is_active: input.is_active
})
if (!input.buckets) return sequence
// You may have to modify your Sequence.buckets resolver to avoid fetching buckets again.
sequence.buckets = await Promise.all(input.buckets.map(bucket => {
// You can avoid these if checks by implementing stricter input types.
// e.g. buckets: [BucketInput!]!
if (!bucket) return null
return models.Bucket.create({
...bucket,
fk_sequence_id: sequence.id
})
}))
return sequence
}
Also, make sure your Sequence.buckets resolver isn't overwriting buckets with faulty data. The resolver you've provided will try to match bucket primary keys with a sequence primary key instead of matching the correct foreign keys with a primary key.
Here's a resolver that will work:
buckets: (parent) => (
parent.buckets // This line may conflict with some of your code and cause problems.
|| models.Bucket.findAll({
where: {fk_sequence_id: parent.id}
})
)

Resources