New to Apollo, so I decided to take the most simple example I found and try to work it in a slightly different way. My code can be found here.
The problem I am having is that the Subscription doesn't seem to get called when I call the Mutation createTask(). The Mutation and Subscription are defined in schema.graphql as:
type Mutation {
createTask(
text: String!
): Task
}
type Subscription {
taskCreated: Task
}
And in resolvers.js as:
Mutation: {
createTask(_, { text }) {
const task = { id: nextTaskId(), text, isComplete: false };
tasks.push(task);
pubsub.publish('taskCreated', task);
return task;
},
},
Subscription: {
taskCreated(task) {
console.log(`Subscript called for new task ID ${task.id}`);
return task;
},
},
What I am expecting to happen is that I would get a console.log in the server every time I run the following in the client:
mutation Mutation($text: String!) {
createTask(text:$text) {
id
text
isComplete
}
}
But nothing happens. What am I missing?
The subscription resolver function is called when there is actually a subscription to the GraphQL Subscription.
As you did not add a client which uses subscriptions-transport-ws and the SubscriptionClient for subscribing to your websocket and the subscription it will not work.
What you could do is add the subscription Channel to the setupFunctions of the SubscriptionManager and therein you get the value that the pubsub.publish function delivers.
Could look like this:
...
const WS_PORT = 8080;
const websocketServer = createServer((request, response) => {
response.writeHead(404);
response.end();
});
websocketServer.listen(WS_PORT, () => console.log( // eslint-disable-line no-console
`Websocket Server is now running on http://localhost:${WS_PORT}`
));
const subscriptionManager = new SubscriptionManager({
schema: executableSchema,
pubsub: pubsub,
setupFunctions: testRunChanged: (options, args) => {
return {
taskCreated: {
filter: (task) => {
console.log(task); // sould be log when the pubsub is called
return true;
}
},
};
},
,
});
subscriptionServer = new SubscriptionServer({
subscriptionManager: subscriptionManager
}, {
server: websocketServer,
path: '/',
});
...
Related
Is it possible to test graphql subscriptions using k6 framework?
I tried to do it, but did not have much success. Also tried to do it with k6 websockets, but did not help.
Thanks
Grapqhql Subscription is based on Websockets so this is theoretically possible to implement using k6 WebSocket.
You can also refer to the documentation for subscriptions here.
You can also use the playground and Networks tab in developer tools to figure out the messages/requests that are sent to the server.
Here is how I was able to achieve it:
import ws from "k6/ws";
export default function(){
const url = "ws://localhost:4000/graphql" // replace with your url
const token = null; // replace with your auth token
const operation = `
subscription PostFeed {
postCreated {
author
comment
}
}` // replace with your subscription
const headers = {
"Sec-WebSocket-Protocol": "graphql-ws",
};
if (token != null) Object.assign(headers,{ Authorization: `Bearer ${token}`});
ws.connect(
url,
{
headers,
},
(socket) => {
socket.on("message", (msg) => {
const message = JSON.parse(msg);
if (message.type == "connection_ack")
console.log("Connection Established with WebSocket");
if (message.type == "data") console.log(`Message Received: ${message}`)
});
socket.on("open", () => {
socket.send(
JSON.stringify({
type: "connection_init",
payload: headers,
})
);
socket.send(
JSON.stringify({
type: "start",
payload: {
query: operation,
},
})
);
});
}
);
}
Hope this helps! 🍻
Debugging update:
So, we went a bit further in debugging this and it seems like 'client:root' cannot access the connection at all by itself.
To debug the complete store, we added this line in the updater function after exporting the store variable from the relay/environment.
console.log(relayEnvStore.getSource().toJSON())
If I use .get() with the specific string client:root:__ItemList_items_connection, I can access the records I have been looking for but it's definitely not pretty.
const testStore = store.get('client:root:__ItemList_items_connection')
console.log(testStore.getLinkedRecords('edges'))
Original:
I'm using Relay Modern and trying to update the cache after the updateItem mutation is completed with the updater. The call to ConnectionHandler.getConnection('client:root', 'ItemList_items') returns undefined.
I'm not sure if it's because I'm trying to use 'client:root' as my parent record or if there's a problem with my code. Has anyone found themselves with a similar issue?
Here's the paginationContainer:
const ItemListPaginationContainer = createPaginationContainer(
ItemList,
{
node: graphql`
fragment ItemList_node on Query
#argumentDefinitions(count: { type: "Int", defaultValue: 3 }, cursor: { type: "String" }) {
items(first: $count, after: $cursor) #connection(key: "ItemList_items") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: props => props.node && props.node.items,
getVariables(props, { count, cursor }) {
return {
count,
cursor
}
},
query: graphql`
query ItemListQuery($count: Int!, $cursor: String) {
...ItemList_node #arguments(count: $count, cursor: $cursor)
}
`
}
)
Here's the mutation:
const mutation = graphql`
mutation UpdateItemMutation($id: ID!, $name: String) {
updateItem(id: $id, name: $name) {
id
name
}
}
`
Here's the updater:
updater: (store) => {
const root = store.getRoot()
const conn = ConnectionHandler.getConnection(
root, // parent record
'ItemList_items' // connection key
)
console.log(conn)
},
Turns out that I was setting my environment incorrectly. The store would reset itself every time I would make a query or a mutation, hence why I couldn't access any of the connections. I initially had the following:
export default server => {
return new Environment({
network: network(server),
store: new Store(new RecordSource())
})
}
All connections are accessible with this change:
const storeObject = new Store(new RecordSource())
export default server => {
return new Environment({
network: network(server),
store: storeObject
})
}
I'm currently running some tests with postman where I get a schema and try to validate my results against it.
I know the schema is not consistent with the response I'm getting but I wanted to know how is it possible to expand the results to give a bit more information.
so for example if I have a request like this:
GET /OBJ/{ID}
it just fails with the feedback:
Schema is valid:
expected false to be true
I was hoping to manage to get a bit more feedback in my newman report
this is an example of my test:
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// only preform tests if response is successful
if (pm.response.code === 200) {
var jsonData = pm.response.json();
pm.test("Data element contains an id", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.id).eql(pm.environment.get("obj_id"));
});
pm.test('Schema is valid', function() {
pm.expect(tv4.validate(jsonData, pm.globals.get("objSchema"))).to.be.true;
});
}
and this is how I run my tests:
const newman = require('newman');
newman.run({
insecure: true,
collection: require('../resources/API.postman_collection.json'),
environment: require('../resources/API.postman_environment.json'),
reporters: 'htmlextra',
reporter: {
htmlextra: {
export: './build/newman_report.html',
logs: true,
showOnlyFails: false,
darkTheme: false
}
}
}, function (err) {
if (err) {
throw err;
}
console.log('collection run complete!');
});
is there a way I can get more information about the validation failure?
I tried a few quick google search but have not come up to nothing that seemed meaningful
it's not exactly what I wanted but I managed to fix it with something like this:
// pre-check
var schemaUrl = pm.environment.get("ocSpecHost") + "type.schema";
pm.sendRequest(schemaUrl, function (err, response) {
pm.globals.set("rspSchema", response.json());
});
// test
var basicCheck = () => {
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response time is less than 200ms", function () {
pm.expect(pm.response.responseTime).to.be.below(200);
});
};
// create an error to get the output from the item validation
var outputItemError = (err) => {
pm.test(`${err.schemaPath} ${err.dataPath}: ${err.message}`, function () {
pm.expect(true).to.be.false; // just output the error
});
};
var itemCheck = (item, allErrors) => {
pm.test("Element contains an id", function () {
pm.expect(item.id).not.eql(undefined);
});
var Ajv = require('ajv');
ajv = new Ajv({
allErrors: allErrors,
logger: console
});
var valid = ajv.validate(pm.globals.get("rspSchema"), item);
if (valid) {
pm.test("Item is valid against schema", function () {
pm.expect(valid).to.be.true; // just to output that schema was validated
});
} else {
ajv.errors.forEach(err => outputItemError(err));
}
};
// check for individual response
var individualCheck = (allErrors) => {
// need to use eval to run this section
basicCheck();
// only preform tests if response is successful
if (pm.response.code === 200) {
var jsonData = pm.response.json();
pm.test("ID is expected ID", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.id).eql(pm.environment.get("nextItemId"));
});
itemCheck(jsonData, allErrors);
}
}
individualCheck(true);
just create a function to do an item test where I do a stupid assert.false to output each individual error in the schema path
For some time I have been trying to find a solution to my problem, however, nothing has worked so far. I'm working on Ionic 4 application with Angular 8 and Ngrx. I created #Effect that calling a service which calling http service and then I need to dispatch two actions. One of them have a payload also.
Everything working fine in development (browsers). I've tried on Chrome, Firefox, Safari. Problem is appearing when I'm trying on the iPhone. On the iPhone payload sending to action is empty object {} instead of object with proper fields.
I've tried to build in non-production mode, disabling aot, build-optimizer, optimization.
Store init:
StoreModule.forFeature('rental', reducer),
EffectsModule.forFeature([RentalServiceEffect]),
Store:
export interface Contract {
address: string;
identity: string;
endRentSignature?: string;
}
export interface RentalStoreState {
status: RentStatus;
contract?: Contract;
metadata?: RentalMetadata;
summary?: RentalSummary;
carState?: CarState;
}
export const initialState: RentalStoreState = {
status: RentStatus.NOT_STARTED,
contract: {
address: null,
identity: null,
endRentSignature: null,
},
};
Action:
export const rentVerified = createAction(
'[RENTAL] RENT_VERIFIED',
(payload: Contract) => ({ payload })
);
Reducer:
const rentalReducer = createReducer(
initialState,
on(RentActions.rentVerified, (state, { payload }) => ({
...state,
contract: payload,
status: RentStatus.RENT_VERIFIED
})));
export function reducer(state: RentalStoreState | undefined, action: Action) {
return rentalReducer(state, action);
}
Method from a service:
public startRentalProcedure(
vehicle: Vehicle,
loading: any
): Observable<IRentalStartResponse> {
loading.present();
return new Observable(observe => {
const id = '';
const key = this.walletService.getActiveAccountId();
this.fleetNodeSrv
.startRent(id, key, vehicle.id)
.subscribe(
res => {
loading.dismiss();
observe.next(res);
observe.complete();
},
err => {
loading.dismiss();
observe.error(err);
observe.complete();
}
);
});
}
Problematic effect:
#Effect()
public startRentalProcedure$ = this.actions$.pipe(
ofType(RentalActions.startRentVerifying),
switchMap(action => {
return this.rentalSrv
.startRentalProcedure(action.vehicle, action.loading)
.pipe(
mergeMap(response => {
return [
RentalActions.rentVerified({
address: response.address,
identity: response.identity
}),
MainActions.rentalProcedureStarted()
];
}),
catchError(err => {
this.showConfirmationError(err);
return of({ type: '[RENTAL] START_RENTAL_FAILED' });
})
);
})
);
In manifest.json, I have following model definition:
{
"sap.ui5": {
"models": {
"SalesInvoices": {
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultCountMode": "Request"
},
"dataSource": "ZAM_SALES_STATISTICS_CDS",
"preload": true
}
}
}
}
As you can see, SalesInvoices is connected to the OData service.
Now on the onInit function in the controller, I am trying to get Metadata from OData as following:
{ // Controller
onInit: function() {
const oPayerModel = this.getView().getModel("SalesInvoices");
console.log(oPayerModel.getMetadata());
setTimeout(() => {
const oPayerModel = this.getView().getModel("SalesInvoices");
console.log(oPayerModel.getMetadata());
}, 600);
},
// ...
}
As you can see, I have to delay to get the OData instance.
setTimeout is not recommended to use in SAPUI5, how can I do it better?
You can avoid setTimeout, as mentioned in this answer, by using the v2.ODataModel API metadataLoaded instead which returns a promise. The promise is fulfilled once the service metadata is loaded successfully.
onInit: async function() {
const oPayerModel = this.getOwnerComponent().getModel("SalesInvoices");
try {
await oPayerModel.metadataLoaded(true);
const oServiceMetadata = oPayerModel.getServiceMetadata(); // NOT .getMetadata()
// ...
} catch (oError) {/* ... */}
},
About the model being undefined in onInit, here are answers with better explanations:
Nabi's answer
My other answer
I think you are running into the issue I reported some time ago: Component + default OData model: this.getView().getModel() returns undefined in onInit() of controllers:
don't use this.getView().getModel() directly in onInit()
instead use this.getOwnerComponent().getModel() in onInit()
anywhere else in the controller you can use this.getView().getModel()
In your case you should be fine changing the suggestion of #boghyon slightly:
onInit: function() {
const oPayerModel = this.getOwnerComponent().getModel("SalesInvoices");
oPayerModel.metadataLoaded().then(this.onMetadataLoaded.bind(this, oPayerModel));
},
onMetadataLoaded: function(myODataModel) {
const metadata = myODataModel.getServiceMetadata(); // NOT .getMetadata()
// ...
},
This way you can get rid of setTimeout(...).