ALB Trigger Lamda function missing permission CDK - aws-lambda

Currently i've got a problem to invoke a Lamda function from an ALB as a trigger function. I am getting the error massage , that
elasticloadbalancing principal does not have permission to
invoke arn:aws:lambda:us-east-2:ACN:function
API: elasticloadbalancingv2:RegisterTargets elasticloadbalancing principal
does not have permission to invoke arn:aws:lambda:us-east-...function:Ddns
from target group arn:aws:elasticloadbalancing:us-east-2:...targetgroup/DdnsL
export class DdnsLamdaApiGateWayCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = Vpc.fromLookup(this, 'global-vpc', {
vpcId: 'vpc-a0b8bec8',
});
const sg = ec2.SecurityGroup.fromSecurityGroupId(this, 'SG', 'sg-0740900526b94fd8f')
const fn = new lambda.Function(this, "API", {
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X,
role: Role.fromRoleArn(this, 'lambda-role', 'arn:aws:iam::.....:role/service-role/LamdaR'),
code: Code.fromInline("test"),
});
fn.addToRolePolicy( new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"lambda:InvokeFunction"
],
resources: [
"*"
]
}));
const lb = new elbv2.ApplicationLoadBalancer(this, "LoadBalancer", {
vpc,
internetFacing: false,
securityGroup: sg
});
const listener = lb.addListener("Listener", {
port: 80,
});
listener.addTargets('Targets', {
targets: [new LambdaALBTarget(fn)]
});
}
}
class LambdaALBTarget implements elbv2.IApplicationLoadBalancerTarget {
private fn: lambda.IFunction;
constructor(fn: lambda.IFunction) {
this.fn = fn;
}
attachToApplicationTargetGroup(
targetGroup: elbv2.ApplicationTargetGroup
): elbv2.LoadBalancerTargetProps {
return {
targetType: "lambda" as elbv2.TargetType,
targetJson: {
id: this.fn.functionArn
}
};
}
}
I am assuming -> that I missing this particular Permission:
LambdaFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt LambdaTargetFunction.Arn
Principal: elasticloadbalancing.amazonaws.com
SourceArn: !Ref TargetGroup
But I cannot figure out know how to include this Permision in the given source-code. Does anyone had the same issue and know how to solve it?

I've found a workaround where the permission is set automatically when deploying the stack instead of creating the class LambdaALBTarget and then call the method attachToApplicationTargetGroup, just add the (new LambdaTarget) to the listener -> attachToApplicationTargetGroup&attachToNetworkTargetGroup get automatically called when you add the target to a load balancer
listener.addTargets('Targets', {
targets: [new LambdaTarget(fn)]
});
...
here the section of created invoke function permission (template.json)
"APIInvokeServicePrincipalelasticloadbalancingamazonawscom68C82386": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"API62EA1CFF",
"Arn"
]
},
"Principal": "elasticloadbalancing.amazonaws.com"
},
"Metadata": {
"aws:cdk:path": "DdnsLamdaApiGateWayCdkStack/API/InvokeServicePrincipal(elasticloadbalancing.amazonaws.com)"
}
Here is the finished Source-Code
export class DdnsLamdaApiGateWayCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = Vpc.fromLookup(this, 'global-vpc', {
vpcId: '....',
});
const code = fs.readFileSync('./code.js','utf8');
const dnsRegistrationRole = new Role(this, 'DnsRegRole', {
roleName: 'Lamda-DnsRegRole',
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCFullAccess'),
ManagedPolicy.fromAwsManagedPolicyName('AmazonRoute53AutoNamingRegistrantAccess'),
ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole '),
],
inlinePolicies: {
Route53ListHostedZone: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ['route53:ListResourceRecordSets'],
resources: ['arn:aws:route53:::hostedzone/*'],
}),
],
}),
},
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
});
const dnsRegistrationLambda = new lambda.Function(this, "API", {
handler: 'index.handler',
runtime: Runtime.NODEJS_12_X,
role: dnsRegistrationRole,
code: Code.fromInline(code),
memorySize: 256,
});
const loadBalancerSecurityGroup = new ec2.SecurityGroup(this, "loadBalancer-security-group", {
vpc: vpc,
allowAllOutbound: true,
description: 'loadBalancerSecurityGroup'
});
loadBalancerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(),ec2.Port.tcp(80),"HTTP");
loadBalancerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(),ec2.Port.tcp(443),"HTTPS")
const lb = new elbv2.ApplicationLoadBalancer(this, "LoadBalancer", {
vpc,
internetFacing: true,
securityGroup: loadBalancerSecurityGroup
});
const listener = lb.addListener("Listener", {
port: 80,
});
listener.addTargets('Targets', {
targets: [new LambdaTarget(dnsRegistrationLambda)]
});
}
}
basically i've build a serverless Dynamic DNS System with ALB&Lamda

Related

how to dynamically assume a role to access DynamoDB from a Lambda using appConfig?

I have two AWS stacks :
one has a dynamoDB table and "exports" (to appConfig) the tableArn, tableName and tableRoleArn (which ideally should allow access to the table).
import { App, Stack, StackProps } from '#aws-cdk/core';
import * as dynamodb from '#aws-cdk/aws-dynamodb';
import * as cdk from '#aws-cdk/core';
import * as appconfig from '#aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ServicePrincipal } from '#aws-cdk/aws-iam';
export class ExportingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, id, {
billingMode: dynamodb.BillingMode.PROVISIONED,
readCapacity: 1,
writeCapacity: 1,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
pointInTimeRecovery: true
});
const tablePolicy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [table.tableArn],
actions: ['*']
});
const role = new Role(this, 'tableRoleArn', {
assumedBy: new ServicePrincipal('lambda.amazonaws.com')
});
role.addToPolicy(
tablePolicy
);
const app = '***';
const environment = '***';
const profile = '***';
const strategy = 'v';
const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'ConfigurationName', {
applicationId: app,
configurationProfileId: profile,
contentType: 'application/json',
content: JSON.stringify({
tableArn: table.tableArn,
tableName: table.tableName,
tableRoleArn: role.roleArn
}),
description: 'table config'
});
const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
applicationId: app,
configurationProfileId: profile,
environmentId: environment,
configurationVersion: newConfig.ref,
deploymentStrategyId: strategy
});
}
}
The second has a function which I would like to be able to use the appConfig configuration to dynamically access the table.
import { App, CfnOutput, Stack, StackProps } from '#aws-cdk/core';
import { LayerVersion, Runtime } from '#aws-cdk/aws-lambda';
import { NodejsFunction } from '#aws-cdk/aws-lambda-nodejs';
import { Effect, PolicyStatement } from '#aws-cdk/aws-iam';
export class ConsumingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const fn = new NodejsFunction(this, 'foo', {
runtime: Runtime.NODEJS_12_X,
handler: 'foo',
entry: `stack/foo.ts`
});
fn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
resources: ['*'],
actions: [
'ssm:*',
'appconfig:*',
'sts:*',
]
})
);
new CfnOutput(this, 'functionArn', { value: fn.functionArn});
// https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html
// https://github.com/aws-samples/aws-appconfig-codepipeline-cdk/blob/main/infrastructure/src/main/kotlin/com/app/config/ServerlessAppStack.kt
const appConfigLayer = LayerVersion.fromLayerVersionArn(
this,
'appconfigLayer',
'arn:aws:lambda:eu-west-2:282860088358:layer:AWS-AppConfig-Extension:47'
);
fn.addLayers(appConfigLayer);
}
}
and handler
import type { Context } from 'aws-lambda';
import fetch from 'node-fetch';
import { DynamoDB, STS } from 'aws-sdk';
import { Agent } from 'https';
export const foo = async (event: any, lambdaContext: Context): Promise<void> => {
const application = '*****';
const environment = '*****';
const configuration = '*****';
const response = await fetch(
`http://localhost:2772/applications/${application}/environments/${environment}/configurations/${configuration}`
);
const configurationData = await response.json();
console.log(configurationData);
const credentials = await assumeRole(configurationData.tableRoleArn);
const db = new DynamoDB({
credentials: {
sessionToken: credentials.sessionToken,
secretAccessKey: credentials.secretAccessKey,
accessKeyId: credentials.accessKeyId
},
apiVersion: '2012-08-10',
region: '*****',
httpOptions: {
agent: new Agent({ keepAlive: true }),
connectTimeout: 1000,
timeout: 5000
},
signatureVersion: 'v4',
maxRetries: 3
});
const item = await db
.getItem({ TableName: configurationData.tableName, Key: { id: { S: 'coolPeople' }, createdAt: { N: '0' } } }, (e) => {
console.log('e', e);
})
.promise();
console.log('item:', item?.Item?.value?.L);
};
/**
* Assume Role for cross account operations
*/
export const assumeRole = async (tableRoleArn: string): Promise<any> => {
let params = {
RoleArn: tableRoleArn,
RoleSessionName: 'RoleSessionName12345'
};
console.info('Assuming Role with params:', params);
let sts = new STS();
return new Promise((resolve, reject) => {
sts.assumeRole(params, (error, data) => {
if (error) {
console.log(`Could not assume role, error : ${JSON.stringify(error)}`);
reject({
statusCode: 400,
message: error['message']
});
} else {
console.log(`Successfully Assumed Role details data=${JSON.stringify(data)}`);
resolve({
statusCode: 200,
body: data
});
}
});
});
};
The issue is that I get this error when trying to assumeRole within the lambda.
Could not assume role, error : {"message":"User: arn:aws:sts::****:assumed-role/ConsumingStack-fooServiceRole****-***/ConsumingStack-foo****-*** is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::****:role/ExportingStack-tableRoleArn****-***","code":"AccessDenied","time":"2022-02-21T16:06:44.474Z","requestId":"****-***-****-****","statusCode":403,"retryable":false,"retryDelay":26.827985116659757}
So is it possible for a Lambda to dynamically assume a role to access a table from a different stack?
I've got it working by changing the trust relationship of the table role to be arn:aws:iam::${Stack.of(this).account}:root
import { App, Stack, StackProps } from '#aws-cdk/core';
import * as dynamodb from '#aws-cdk/aws-dynamodb';
import * as cdk from '#aws-cdk/core';
import * as appconfig from '#aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ArnPrincipal } from '#aws-cdk/aws-iam';
export class ExportingStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, id, {
billingMode: dynamodb.BillingMode.PROVISIONED,
readCapacity: 1,
writeCapacity: 1,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
pointInTimeRecovery: true
});
const tablePolicy = new PolicyStatement({
effect: Effect.ALLOW,
resources: [table.tableArn],
actions: ['*']
});
const role = new Role(this, 'tableRoleArn', {
assumedBy: new ArnPrincipal(`arn:aws:iam::${Stack.of(this).account}:root`)
});
role.addToPolicy(tablePolicy);
const app = '***';
const environment = '***';
const profile = '****';
const strategy = '****';
const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'myConfiguration', {
applicationId: app,
configurationProfileId: profile,
contentType: 'application/json',
content: JSON.stringify({
tableArn: table.tableArn,
tableName: table.tableName,
tableRoleArn: role.roleArn
}),
description: 'table config'
});
const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
applicationId: app,
configurationProfileId: profile,
environmentId: environment,
configurationVersion: newConfig.ref,
deploymentStrategyId: strategy
});
}
}

How to enable graphql subscription in loopback 4 with openapi-to-graphql

as per the title, I am having problem trying to enable graphql subscription in my loopback 4 application.
Here is my code that I've done so far.
index.ts
export async function main(options: ApplicationConfig = {}) {
const app = new BackendLb4Application(options)
await app.boot()
await app.start()
const url = app.restServer.url;
const oas: Oas3 = <Oas3><unknown>await app.restServer.getApiSpec()
const {schema} = await createGraphQLSchema(oas, {
operationIdFieldNames: true,
baseUrl: url,
createSubscriptionsFromCallbacks: true,
})
const handler = graphqlHTTP( (request:any, response:any, graphQLParams: any) => ({
schema,
pretty: true,
graphiql: true
}))
app.mountExpressRouter(graphqlPath, handler);
const pubsub = new PubSub()
const ws = createServer(app);
ws.listen(PORT, () => {
new SubscriptionServer(
{
execute,
subscribe,
schema,
onConnect: (params: any, socket: any, ctx: any) => {
console.log(params, 'here on onconnect')
// Add pubsub to context to be used by GraphQL subscribe field
return { pubsub }
}
},
{
server: ws,
path: '/subscriptions'
}
)
})
return app
}
Here is my schema
type Subscription {
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlApiNotification(secondInputInput: SecondInputInput): String
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlOnNotificationUpdate(firstInputInput: FirstInputInput): String
}
Here is an example of my controller
#patch('/notification-update', {
operationId: 'notificationUpdate',
description: '**GraphQL notificationUpdate**',
callbacks:[ {
onNotificationUpdate: {
//'{$request.query.callbackUrl}/onNotificationUpdate': {
post: {
requestBody: {
operationId: 'notificationUpdateCallback',
description: 'rasjad',
content: {
'application/json': {
schema: {
title: "firstInput",
type: 'object',
properties: {
userData: {
type: "string"
}
}
}
}
}
},
responses: {
'200': {
description: 'response to subscription',
}
}
}
},
// }
}],
responses: {
'200': {
description: 'Notification PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Notification, {partial: true}),
},
},
})
notification: Notification,
#param.where(Notification) where?: Where<Notification>,
): Promise<Count> {
return this.notificationRepository.update(notification, where);
}
Ive defined the callbacks object in my controller which will then create a subscription in my schema. Tested it out on graphiql but did not work.
I am not sure where to go from here. Do I need a custom resolver or something? Not sure.
Appreciate it if anyone could help on this.
Just in case someone else is looking to do the same thing.
I switched out graphqlHTTP with Apollo Server to create my graphql server.
So my final index.ts looks like this.
export async function main(options: ApplicationConfig = {}) {
const lb4Application = new BackendLb4Application(options)
await lb4Application.boot()
await lb4Application.migrateSchema()
await lb4Application.start()
const url = lb4Application.restServer.url;
const graphqlPath = '/graphql'
// Get the OpenApiSpec
const oas: Oas3 = <Oas3><unknown>await lb4Application.restServer.getApiSpec()
// Create GraphQl Schema from OpenApiSpec
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: true,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL'
},
createSubscriptionsFromCallbacks: true,
customResolvers: {
"lb4-title": {
"your-path":{
patch: (obj, args, context, info) => {
const num = Math.floor(Math.random() * 10);
pubsub.publish("something", { yourMethodName: {count: num} }).catch((err: any) => {
console.log(err)
})
return {count: 1}
}
}
}
},
customSubscriptionResolvers: {
"lb4-title" : {
"yourMethodName": {
post: {
subscribe: () => pubsub.asyncIterator("something"),
resolve: (obj: any, args: any, context, info) => {
console.log(obj, 'obj')
}
}
}
}
}
})
const app = express();
const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServers.close();
}
};
}
}],
})
const subscriptionServers = SubscriptionServer.create(
{
// This is the `schema` we just created.
schema,
// These are imported from `graphql`.
execute,
subscribe,
},
{
server: lb4Application.restServer.httpServer?.server,
path: server.graphqlPath,
//path: server.graphqlPath,
}
);
await server.start();
server.applyMiddleware({ app, path: "/" });
lb4Application.mountExpressRouter('/graphql', app);
return lb4Application
}
Also you will need to define the callbacks object in your controller like so.
#patch('/something-update', {
operationId: 'somethingUpdate',
description: '**GraphQL somethingUpdate**',
callbacks:[
{
yourMethodName: {
post: {
responses: {
'200': {
description: 'response to subscription',
content: {'application/json': {schema: CountSchema}},
}
}
}
},
}
],
responses: {
'200': {
description: 'Something PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Something, {partial: true}),
},
},
})
something: Something,
#param.where(Something) where?: Where<Something>,
): Promise<Count> {
return this.somethingRepository.updateAll(something, where);
}
And that is it. You can test it out from the GraphQL Playground and play around with the subscriptions.
For the time being, I am fine with defining customResolvers and customSubscriptionResolvers but I'm pretty sure I can automate this two objects from the controllers.
Cheers!

seneca - communication between two microservices

I'm new in Seneca. I have been trying to make two microservices to communicate each other but I keep failing and get this errors:
Error: Response Error: 404 Not Found
at module.exports.internals.Utils.internals.Utils.handle_response (c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\seneca-transport\lib\transport-utils.js:71:11)
at c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\seneca-transport\lib\http.js:154:25
at read (c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\wreck\lib\index.js:590:24)
at finish (c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\wreck\lib\index.js:398:20)
at wrapped (c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\hoek\lib\index.js:879:20)
at module.exports.internals.Recorder.onReaderFinish (c:\Users\Actiview\Desktop\microservices\orderManager\node_modules\wreck\lib\index.js:449:16)
at Object.onceWrapper (events.js:313:30)
at emitNone (events.js:111:20)
at module.exports.internals.Recorder.emit (events.js:208:7)
at finishMaybe (_stream_writable.js:614:14)
=== SENECA FATAL ERROR === MESSAGE: ::: seneca: Action failed: Response Error: 404 Not Found. CODE: ::: act_execute INSTANCE :::
Seneca/pcbyi7v5c76v/1534346071465/6536/3.7.0/- DETAILS ::: {
message: 'Response Error: 404 Not Found',
pattern: '',
fn: { [Function: transport_client] id: 'host:127.0.0.2,pg:,port:8080' },
callback:
{ [Function: bound action_reply]
seneca:
Seneca {
'private$':
{ act:
{ parent:
{ start: 1534346071559,
end: 1534346071561, and more...
this is my code:
orderIndex.ts
{
const orderPlugin = require('./orderManagerPlugin');
const express = require('express');
const SenecaWeb = require('seneca-web');
const seneca = require("seneca")();
let bodyParser = require('body-parser');
var Routes = [{
prefix: '/orders',
pin: 'area:order,action:*',
map: {
fetch: { GET: true },
create: { GET: false, POST: true },
delete: { GET: false, DELETE: true },
}
}]
var config = {
routes: Routes,
adapter: require('seneca-web-adapter-express'),
context: express().use(bodyParser.urlencoded({ 'extended': 'true' })).use(bodyParser.json()),
options: {parseBody: false}
}
seneca.use(SenecaWeb,config);
seneca.use( orderPlugin );
seneca.ready(function (err) {
const app = seneca.export('web/context')();
app.listen({ host: "127.0.0.4", port: 8081 });
});
}
orderPlugin.ts
{
var plugin = function orderPlugin(options) {
var seneca = this;
var senecaEmailer;
seneca.add({ area: "order", action: "fetch" }, function (args,
done) {
var orders = this.make("orders");
orders.list$({ id: args.id }, done);
});
seneca.add({ area: "order", action: "delete" }, function (args,
done) {
var orders = this.make("orders");
orders.remove$({ id: args.id }, function (err) {
done(err, null);
});
});
seneca.add({ area: "order", action: "create" }, function (args,
done) {
console.log('create order');
senecaEmailer.act( 'role:web', {area: 'email', action:'send'} , done);
});
this.add( { init: "orderPlugin" }, function (args, done) {
senecaEmailer = require("seneca")().client({ host: "127.0.0.2", port: 8080 });
done();
});
}
module.exports = plugin;
}
emailIndex.ts
{
const mailPlugin = require('./emailingPlugin');
const express = require('express');
const SenecaWeb = require('seneca-web');
const seneca = require("seneca")();
let bodyParser = require('body-parser');
var Routes = [{
prefix: '/emails',
pin: 'area:email, action:*',
map: {
send: { GET: true },
}
}]
var config = {
routes: Routes,
adapter: require('seneca-web-adapter-express'),
context: express().use(bodyParser.urlencoded({ 'extended': 'true' })).use(bodyParser.json()),
options: {parseBody: false}
}
seneca.use(SenecaWeb,config);
seneca.use( mailPlugin );
seneca.ready(function (err) {
const app = seneca.export('web/context')();
app.listen({ host: "127.0.0.2", port: 8080 } );
});
}
emailPlugin.ts
{
import {EmailService} from './emailService';
var plugin = function emailPlugin(options) {
var seneca = this;
let mailer :EmailService ;
seneca.add({area: "email", action: "send"}, function(args, done) {
mailer.sendMail('guzon56#gmail.com', done);
});
this.add( { init: "emailPlugin" }, function (args, done) {
console.log('before init');
mailer = require('./emailService')();
console.log('after init');
done();
});
};
module.exports = plugin;
}
please help me.
Tnx.
Seneca is explained by Richard Rodger in this post. The chapter "Service Discovery" talks about meshing the microservices in a network.
For my applications I use the seneca-mesh plugin. This plugin README says:
To join the network, all a service has to do is contact one other
service already in the network. The network then shares information
about which services respond to which patterns. There is no need to
configure the location of individual services anywhere.
Reading Richard's post and the plugin documentation could be a good starting point for your project. Hope it helps!

graphql-subscriptions withFilter returns undefined; subscriptions without variables work ok

I'm trying to get my head around graphql-subscriptions and withFilter. Subscriptions without variables work as intended, but if I try to use withFilter, I only get 'Subscription field must return Async Iterable. Received: undefined' error when I try to run the subscription.
Am I doing something wrong with setting up withFilter, are the some incompatibilities with packages I'm using or am I completely missing something obvious here? All queries and mutations work properly, so the basic set up should be fine.
My set up is similar to this (all code snippets are in https://gist.github.com/aqmattil/41e10e7c9f30b8ea964cecdc61c58f20
Package.json
// package.json
"dependencies": {
"apollo-server-express": "^2.0.0-beta.2",
"body-parser": "^1.18.3",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-subscriptions": "^0.5.8",
"subscriptions-transport-ws": "^0.9.11"
}
Mutations
// mutations.js
const mutation = new GraphQLObjectType({
name: 'mutation',
fields: {
addSite: {
type: SiteType,
description: "Create a new Site",
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
location: { type: GraphQLString },
company: { type: GraphQLString }
},
async resolve(parentValue, { name, location, company }) {
const site = await new Site({ name, location, company }).save()
const siteid = site._id;
console.log("addSite resolve", siteid, name, location, company );
pubsub.publish('siteAdded', { 'siteAdded': site } );
return site;
}
}
}
});
module.exports = mutation;
Subscriptions
// subscriptions.js
const graphql = require('graphql');
const {
GraphQLObjectType,
GraphQLString
} = graphql;
const { withFilter } = require('graphql-subscriptions');
const SiteType = require('./site_type');
const pubsub = require('./pubsub_helper');
const Subscriptions = new GraphQLObjectType({
name: 'subscription',
fields: () => ({
/*
// this code works, commented out to test withfilter
siteAdded: {
type: SiteType,
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
return pubsub.asyncIterator('siteAdded');
}
},
*/
// test withFilter
siteAdded: {
type: SiteType,
args: {
name: { type: GraphQLString }
},
resolve(payload) {
return payload.siteAdded;
},
subscribe() {
// this returns undefined
withFilter(
() => {
console.log("in subscribe withfilter");
return pubsub.asyncIterator('siteAdded');
}
),
(payload, variables) => {
console.log("payload, variables", payload, variables);
return true;
}
}
}
})
});
module.exports = Subscriptions;
I'm using graphiql to run the queries,
// this is used to add a site
mutation {
addSite(name:"test name", location: "somewhere") {
id
}
}
// simple subscription - this works as inteded, and new sites are shown
subscription {
siteAdded {
name
location
company {
id
}
}
}
// using query variables --> returns "Subscription
// field must return Async Iterable. Received: undefined"
subscription {
siteAdded(name: "test name") {
name
location
company {
id
}
}
}

Issue in querying graphQL relay calls (.then is not a function)

I have a graphql server running which I am using for query one of the object that gets instantiated when server starts (kind of in-memory db). Here company object is created every time schema is loaded or say server is started which I am using in QueryType object to resolve.
Here is the graphQL Schema
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLList
} = require('graphql');
const {
connectionDefinitions,
connectionArgs,
connectionFromArray,
connectionFromPromisedArray
} = require('graphql-relay');
//**************************** In-Memory Data ********************************//
var company = {
id:'123456',
customFieldDefinitions:[
{
name: 'cfm1',
id: '123'
},
{
name: 'cfm2',
id: '1234'
}
]
};
//**************************** In-Memory Code Ends *********************************//
const CustomFieldDefinitionType = new GraphQLObjectType({
name: 'Common_CustomFieldDefinitionsConnection',
fields: {
id: {
type: GraphQLString,
resolve: (cfd) => cfd.id
},
name: {
type: GraphQLString,
resolve: (cfd) => cfd.name
}
}
});
const { connectionType: CustomFieldDefinitionConnection } =
connectionDefinitions({
name: 'Common_CustomFieldDefinition',
nodeType: CustomFieldDefinitionType
});
const CompanyType = new GraphQLObjectType({
name: 'Company',
fields: {
id: {
type: GraphQLString,
resolve: (obj) => obj.id
},
customFieldDefinitions: {
type: CustomFieldDefinitionConnection,
description: 'A list of Custom Fields',
args: connectionArgs,
resolve: (obj, args) => connectionFromPromisedArray(obj.customFieldDefinitions, args)
}
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
company: {
args: {
id: { type: GraphQLString },
},
type: CompanyType,
resolve: (_, args) => company
}
}
});
const mySchema = new GraphQLSchema({
query: QueryType
});
module.exports = mySchema;
Now when I try to query as below on graphiQL editor
query{
company{
customFieldDefinitions {
edges {
node {
id
}
}
}
}
}
I get error as below.
{
"data": {
"company": {
"customFieldDefinitions": null
}
},
"errors": [
{
"message": "dataPromise.then is not a function",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": [
"company",
"customFieldDefinitions"
]
}
]
}
How can I identify the problem?
It looks like the error is with this line:
resolve: (obj, args) => connectionFromPromisedArray(obj.customFieldDefinitions, args)
The problem is that the connectionFromPromisedArray function imported from graphql-relay expects a promise that returns an array and obj.customFieldDefinitions is a normal array. Just to trace it back, obj is the parent element of that resolver which in this case is what you return from the company resolver on the Query type.
To fix it, either change connectionFromPromisedArray to connectionFromArray or change your company object to this:
var company = {
id:'123456',
customFieldDefinitions: Promise.resolve([
{
name: 'cfm1',
id: '123'
},
{
name: 'cfm2',
id: '1234'
}
])
};

Resources