GraphQL Validation Error on mutation - javascript

I am trying to set up a mutation with modified code from the Relay Todo example.
When I try to compile I get the following error:
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Cannot query field "addCampaign" on type "Mutation".
Source:
>
> mutation AddCampaignMutation {addCampaign}
> ^^^
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Unknown type "AddCampaignPayload". Did you mean "AddTodoPayload" or "RenameTodoPayload"?
Source:
>
> fragment AddCampaignMutationRelayQL on AddCampaignPayload #relay(pattern: true) {
> ^^
I have duplicated the Todo code so I don't know why the Todo mutation is working correctly but my new Campaign test isn't.
This is my database.js file, I have removed the Todo related items to make the document easier to read:
export class Campaign {}
export class User {}
// Mock authenticated ID
const VIEWER_ID = 'me';
// Mock user data
const viewer = new User();
viewer.id = VIEWER_ID;
const usersById = {
[VIEWER_ID]: viewer,
};
// Mock campaign data
const campaignsById = {};
const campaignIdsByUser = {
[VIEWER_ID]: [],
};
let nextCampaignId = 0;
addCampaign('Campaign1');
addCampaign('Campaign2');
addCampaign('Campaign3');
addCampaign('Campaign4');
export function addCampaign(text) {
const campaign = new Campaign();
//campaign.complete = !!complete;
campaign.id = `${nextCampaignId++}`;
campaign.text = text;
campaignsById[campaign.id] = campaign;
campaignIdsByUser[VIEWER_ID].push(campaign.id);
return campaign.id;
}
export function getCampaign(id) {
return campaignsById[id];
}
export function getCampaigns(status = 'any') {
const campaigns = campaignIdsByUser[VIEWER_ID].map(id => campaignsById[id]);
if (status === 'any') {
return campaigns;
}
return campaigns.filter(campaign => campaign.complete === (status === 'completed'));
}
This is my schema.js file, again I have removed the Todo related items to make the document easier to read:
import {
GraphQLBoolean,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId,
} from 'graphql-relay';
import {
Campaign,
addCampaign,
getCampaign,
getCampaigns,
User,
getViewer,
} from './database';
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
} else if (type === 'Campaign') {
return getCampaign(id);
}
return null;
},
(obj) => {
if (obj instanceof User) {
return GraphQLUser;
} else if (obj instanceof Campaign) {
return GraphQLCampaign;
}
return null;
}
);
/**
* Define your own connection types here
*/
const GraphQLAddCampaignMutation = mutationWithClientMutationId({
name: 'AddCampaign',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
campaignEdge: {
type: GraphQLCampaignEdge,
resolve: ({localCampaignId}) => {
const campaign = getCampaign(localCampaignId);
return {
cursor: cursorForObjectInConnection(getCampaigns(), campaign),
node: campaign,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localCampaignId = addCampaign(text);
return {localCampaignId};
},
});
const GraphQLCampaign = new GraphQLObjectType({
name: 'Campaign',
description: 'Campaign integrated in our starter kit',
fields: () => ({
id: globalIdField('Campaign'),
text: {
type: GraphQLString,
description: 'Name of the campaign',
resolve: (obj) => obj.text,
}
}),
interfaces: [nodeInterface]
});
const {
connectionType: CampaignsConnection,
edgeType: GraphQLCampaignEdge,
} = connectionDefinitions({
name: 'Campaign',
nodeType: GraphQLCampaign,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
campaigns: {
type: CampaignsConnection,
args: {
...connectionArgs,
},
resolve: (obj, {...args}) =>
connectionFromArray(getCampaigns(), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
This is my AddCampaignMutation.js file:
import Relay from 'react-relay';
export default class AddCampaignMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
console.log('getMutation');
return Relay.QL`mutation{addCampaign}`;
}
getFatQuery() {
console.log('getFatQuery');
return Relay.QL`
fragment on AddCampaignPayload #relay(pattern: true) {
campaignEdge,
viewer {
campaigns,
},
}
`;
}
getConfigs() {
console.log('getConfigs');
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'campaigns',
edgeName: 'campaignEdge',
rangeBehaviors: ({orderby}) => {
if (orderby === 'newest') {
return 'prepend';
} else {
return 'append';
}
},
//rangeBehaviors: ({status}) => {
// if (status === 'completed') {
// return 'ignore';
// } else {
// return 'append';
// }
//},
}];
}
getVariables() {
console.log('getVariables');
return {
text: this.props.text,
};
}
getOptimisticResponse() {
console.log('getOptimisticResponse');
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
campaignEdge: {
node: {
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
And finally this is the app file that contains my text input and the call to AddCampaignMutation:
import AddTodoMutation from '../mutations/AddTodoMutation';
import AddCampaignMutation from '../mutations/AddCampaignMutation';
import TodoListFooter from './TodoListFooter';
import TodoTextInput from './TodoTextInput';
import React from 'react';
import Relay from 'react-relay';
class TodoApp extends React.Component {
_handleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddTodoMutation({text, viewer: this.props.viewer})
);
};
_campaignHandleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddCampaignMutation({text, viewer: this.props.viewer})
);
};
render() {
const hasTodos = this.props.viewer.totalCount > 0;
return (
<div>
<section className="todoapp">
<header className="header">
<TodoTextInput
autoFocus={true}
className="new-campaign"
onSave={this._campaignHandleTextInputSave}
placeholder="Campaign name"
/>
<h1>
todos
</h1>
<TodoTextInput
autoFocus={true}
className="new-todo"
onSave={this._handleTextInputSave}
placeholder="What needs to be done?"
/>
</header>
{this.props.children}
{hasTodos &&
<TodoListFooter
todos={this.props.viewer.todos}
viewer={this.props.viewer}
/>
}
</section>
</div>
);
}
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on User {
totalCount,
${AddTodoMutation.getFragment('viewer')},
${AddCampaignMutation.getFragment('viewer')},
${TodoListFooter.getFragment('viewer')},
}
`,
},
});

Well I feel a bit silly but I found out what the problem was. I didn't realise that my schema.json file was not updating!
If anyone has a similar problem make sure that the schema.json file is up to date by running the following command to rebuild it:
npm run-script update-schema

Related

Wordpress Gutenberg subscribe not getting updated meta field value

I've been trying to resolve this issue where using the Gutenberg meta box updates, doesn't fetch the new updated meta value.
Meta registration:
add_action('init', function() {
register_meta('post', 'open_unit', array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function() {
return current_user_can('edit_posts');
}
));
register_meta('post', 'open_active', array(
'type' => 'boolean',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function() {
return current_user_can('edit_posts');
}
));
register_meta('post', 'open_abstract', array(
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => function($text) {
return sanitize_text_field($text);
},
'auth_callback' => function() {
return current_user_can('edit_posts');
}
));
});
Enqueue assets:
add_action('enqueue_block_editor_assets', function() {
$screen = get_current_screen();
if ($screen->post_type === 'page') return;
wp_enqueue_script(
'open-panel',
plugin_dir_url( __FILE__ ) . 'js/admin.js',
array('wp-i18n', 'wp-blocks', 'wp-edit-post', 'wp-element', 'wp-editor', 'wp-components', 'wp-data', 'wp-plugins', 'wp-edit-post', 'wp-api-fetch'),
filemtime(dirname( __FILE__ ) . '/js/admin.js')
);
});
Javascript:
const el = element.createElement;
const { Fragment } = element;
const { registerPlugin } = plugins;
const { PluginDocumentSettingPanel } = editPost;
const { TextareaControl, ToggleControl, Text } = components;
const { withSelect, withDispatch, subscribe, registerStore } = data;
const ActiveCheckboxControl = compose.compose(
withDispatch(function(dispatch, props) {
return {
setMetaValue: function(metaValue) {
dispatch('core/editor').editPost(
//{ meta: { [props.metaKey]: (openValidate && metaValue) } }
{ meta: { [props.metaKey]: metaValue } }
);
}
}
}),
withSelect(function(select, props) {
return {
metaValue: select('core/editor').getEditedPostAttribute('meta')[props.metaKey],
}
}))(function(props) {
return el(ToggleControl, {
label: props.title,
checked: props.metaValue,
onChange: function(content) {
props.setMetaValue(content);
},
});
}
);
const AbstractTextControl = compose.compose(
withDispatch(function(dispatch, props) {
return {
setMetaValue: function(metaValue) {
dispatch('core/editor').editPost(
{ meta: { [props.metaKey]: metaValue } }
);
}
}
}),
withSelect(function(select, props) {
return {
metaValue: select('core/editor').getEditedPostAttribute('meta')[props.metaKey],
}
}))(function(props) {
return el(TextareaControl, {
label: props.title,
value: props.metaValue,
onChange: function(content) {
props.setMetaValue(content);
}
});
}
);
registerPlugin('open', {
render: function() {
return el(Fragment, {},
el(PluginDocumentSettingPanel,
{
name: 'open',
title: 'Open'
},
// Active
el(ActiveCheckboxControl,
{
metaKey: 'open_active',
title : 'Show'
}
),
// Abstract
el(AbstractTextControl,
{
metaKey: 'open_abstract',
title : 'Abstract'
}
)
)
);
}
});
...
let isSavingChecked = true;
let editor = data.select('core/editor');
const getOpenUnit = () => editor.getEditedPostAttribute('meta') ? editor.getEditedPostAttribute('meta').open_unit : null;
const getOpenActive = () => editor.getEditedPostAttribute('meta') ? editor.getEditedPostAttribute('meta').open_active : false;
const getOpenAbstract = () => editor.getEditedPostAttribute('meta') ? editor.getEditedPostAttribute('meta').open_abstract : null;
let openUnit = getOpenUnit();
let openActive = getOpenActive();
let openAbstract = getOpenAbstract();
console.log(openUnit);
const unsubscribe = subscribe( _.debounce( () => {
const isSavingPost = editor.isSavingPost();
const newOpenUnit = getOpenUnit();
const newOpenActive = getOpenActive();
const newOpenAbstract = getOpenActive();
if (isSavingPost) {
isSavingChecked = false;
} else {
if(!isSavingChecked) {
let post = editor.getCurrentPost();
let data = {
active: openActive ? 'active':'paused',
abstract: post.meta.open_abstract,
wp_id: post.id,
wp_title: post.title,
wp_url: post.link,
wp_image: post.featured_media
}
let openValidation = openValidate(data);
if (openValidation.valid) {
if(openActive !== newOpenActive || openAbstract !== newOpenAbstract || openUnit !== newOpenUnit) {
openRemote(data);
} else {
console.log(newOpenUnit); //This field is not returning the updated meta field from Wordpress
openRemoteUpdate(data);
}
} else {
wp.data.dispatch('core/notices').removeNotice('OPEN_NOTICE');
wp.data.dispatch('core/notices').createNotice(
'warning',
openValidation.messages.join(', '),
{ id: 'OPEN_NOTICE', isDismissible: true }
);
}
isSavingChecked = true;
openActive = newOpenActive;
openAbstract = newOpenAbstract;
openUnit = newOpenUnit;
}
}
}));
I am basically trying to fetch the updated meta field:
const getOpenUnit = () => editor.getEditedPostAttribute('meta') ? editor.getEditedPostAttribute('meta').open_unit : null;
But it's currently looking like this in the console where it's null(console.log(openUnit)) or empty(console.log(newOpenUnit)) https://share.getcloudapp.com/E0uYKWGv Lines 195 & 224
Any help or advice would be appreciated!
The easiest way I have found to get and set meta is using useEntityProp() in a function component. It is a lot easier to reason about than using withSelect and withDispatch.
import { __ } from '#wordpress/i18n';
import { useSelect } from '#wordpress/data';
import { useEntityProp } from '#wordpress/core-data';
import { TextControl } from '#wordpress/components';
export default function MetaComponent(props) {
const postType = useSelect((select) => {
return select('core/editor').getCurrentPostType();
});
const [meta, setMeta] = useEntityProp('postType', postType, 'meta');
return (
<TextControl
label={ __('Meta', 'pb') }
value={ meta.open_abstract ? meta.open_abstract : '' }
onChange={ (value) => setMeta({open_abstract: value}) }
/>
);
}

Object is possibly 'undefined' in Vuex mutation with TypeScript

I'm learning Vue.js with Vuex and TypeScript, in the application i'm building i came across the error "Object is possibly 'undefined'" in the Vuex Store.
The error happens in the "newCard" mutation in this line:
state.board.lists.find(list => list.id === idList).cards.unshift(card)
The complete store code:
const state: BoardState = {
board: {
id: "",
name: "",
idProject: "",
closed: false,
lists: []
}
};
const getters: GetterTree<BoardState, {}> = {
getBoard: state => state.board
};
const mutations: MutationTree<BoardState> = {
setBoard: (state, board) => (state.board = board),
newList: (state, list) => state.board.lists.unshift(list),
newCard: (state, { card, idList }) =>
state.board.lists.find(list => list.id === idList).cards.unshift(card)
};
const actions: ActionTree<BoardState, {}> = {
async fetchBoard({ commit }) {
const response = await axios.post("https://app.fakejson.com/q", json);
commit("setBoard", response.data);
},
async addList({ commit }, list) {
const response = await axios.post("https://app.fakejson.com/q", {
token: "oplF0L7vj1Ial4cRqtx9DA",
data: list
});
commit("newList", response.data);
},
async addCard({ commit }, { card, idList }) {
const response = await axios.post("https://app.fakejson.com/q", {
token: "oplF0L7vj1Ial4cRqtx9DA",
data: card
});
commit("newCard", response.data, idList);
}
};
TypeScript types:
// Store
export interface BoardState {
board: Board;
}
// Models
export interface Board {
id: String;
name: String;
idProject: String;
closed: Boolean;
lists: List[];
}
export interface List {
id: String;
name: String;
idBoard: String;
closed: Boolean;
cards: Card[];
}
export interface Card {
id: String;
name: String;
idList: String;
closed: Boolean;
}
The response data of the board state is like this:
{
"id":"1",
"name":"Tasks",
"idProject":"1",
"closed":false,
"lists":[
{
"id":"1",
"name":"To Do",
"idBoard":"1",
"closed":false,
"cards":[
{
"id":"1",
"idList":"1",
"name":"Card 1",
"closed":false
},
{
"id":"2",
"idList":"1",
"name":"Card 2",
"closed":false
}
]
}
]
}
state.board.lists.find(list => list.id === idList).cards.unshift(card)
The specific list may not be found. So you won't be able to pick cards from it.
const list = state.board.lists.find(list => list.id === idList)
if (!list) {
// do something
return
}
// list found so return
return list.cards.unshift(card)

Yields "TypeError: Cannot read property 'xxxx' of undefined" after running jest with Vue

I'm trying to make a test using jest with Vue.
the details below.
Problem:
Can't mount using shallowMount option.
Situation:
Run the test after mounting the component using shallowMount option that provides in Vue-test-utils.
Throw error "Cannot read property 'XXXX' of undefined
This is my test code.
import myComponent from '#/~';
import Vuex from 'vuex';
import Vuelidate from 'vuelidate';
import { shallowMount, createLocalVue } from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(Vuelidate);
describe('myComponent~', () => {
let store;
beforeEach(() => {
store = new Vuex.Store({
modules: {
user: {
namespaced: true,
getters: {
profile: () => {
const profile = { name: 'blahblah' };
return profile;
},
},
},
},
});
});
describe('profile.name is "blahblah"', () => {
it('return something~', () => {
const wrapper = shallowMount(myComponent, {
localVue,
store,
mocks: {
$api: {
options: {
testMethod() {
return new Promise((resolve, reject) => {
resolve('test');
});
},
},
},
$i18n: {
t() {
return {
EN: 'EN',
KO: 'KO',
JP: 'JA',
SC: 'zh-CN',
TC: 'tw-CN',
};
},
},
},
});
expect(wrapper.find('.profile').text()).toBe('blahblah');
});
I think the problem is that property isn't set as a specified value or an empty value like an array or object.
But I don't know how I set properly the properties in my logic.
For example,
when the error yields "Cannot read property 'images' of undefined",
I add to a wrapper in the relevant method like this.
exampleMethod() {
this.something = this.something.map(item => {
if (item.detailContent.images) { // <-- the added wrapper is here
~~~logic~~~~
}
})
}
But the undefined properties are so many, I also think this way is not proper.
How I do solve this problem?
added
These are details about the above example method:
exampleMethod() {
this.something = this.something.map(item => {
let passValidation = false;
let failValidation = false;
if (item.detailContent.images) {
if (this.detail.showLanguages.includes(item.code)) {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: true,
}
});
}
checkValidationPass = true;
} else {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: false,
}
});
}
checkValidationPass = false;
}
return {
...item,
required: passValidation,
warning: failValidation,
}
}
});
if (this.configId === 'OPTION2') {
this.checkOption2Validation();
} else if (this.configId === 'OPTION3') {
this.checkOption3Validation();
} else {
this.checkOption1Validation();
}
},
And this is 'this.something':
data() {
return {
something: []
}
}
The detailContent is set here.
setMethod() {
this.something = [
...this.otherthings,
];
this.something = this.something.map(item => {
let details1 = {};
if (this.configId === 'OPTION2') {
details1 = {
images: [
{ deviceType: 'PC', titleList: [null, null], imageType: 'IMAGE' },
{ deviceType: 'MOBILE', titleList: [null, null, null] }
]
};
} else if (this.configId === 'OPTION3') {
details1 = {
images: [
{ deviceType: 'PC' },
{ deviceType: 'MOBILE' }
],
links: { linkType: 'EMPTY' },
};
}
let details2 = {
mainTitle: {
content: null,
}
}
let checkValidation = false;
this.detail.detailLanguages.forEach(element => {
if (element.language === item.code) {
details1 = { ...element };
if (!!element.mainTitle) {
details2 = { ...element };
} else {
details2 = {
...details2,
...element
};
}
if (this.configId !== 'OPTION1') {
details1.images = details1.images.map(image => {
return {
...image,
required: true,
}
});
}
checkValidation = true;
}
});
return {
...item,
detailContent: this.configId !== 'OPTION1' ? details1 : details2,
required: false,
warning: false,
}
});
},

Tiptap extension for images and editable captions

enter image description here
I want to implement image and editable caption with tiptap extension
https://discuss.prosemirror.net/t/figure-and-editable-caption/462
There was a very good example with ProseMirror, but is it difficult to achieve with tiptap?
Please tell me what code you should write if possible.
I attach the code that I wrote below.
The image and caption have been successfully added, but the caption cannot be edited yet.
// ./CustomImage.ts
// #ts-ignore
import { Node, Plugin } from 'tiptap'
// #ts-ignore
import { nodeInputRule } from 'tiptap-commands'
const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
export default class CustomImage extends Node {
get name () {
return 'customImage'
}
get schema () {
return {
attrs: {
src: {
default: null
},
alt: {
default: null
},
title: {
default: null
},
caption: {
default: null
}
},
group: 'block',
selectable: false,
draggable: true,
parseDOM: [
{
tag: 'figure'
},
[
{
tag: 'img[src]',
getAttrs: (dom: any) => ({
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt')
})
},
{
tag: 'figcaption'
}
]
],
toDOM: (node: any) => [
'figure',
[
'img',
{
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
],
[
'figcaption',
{
contenteditable: 'true'
},
node.attrs.caption
]
]
}
}
commands ({ type }: any) {
return (attrs: any) => (state: any, dispatch: any) => {
const { selection } = state
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
inputRules ({ type }: any) {
return [
nodeInputRule(IMAGE_INPUT_REGEX, type, (match: any) => {
const [, alt, src, title] = match
return {
src,
alt,
title
}
})
]
}
get plugins () {
return [
new Plugin({
props: {
handleDOMEvents: {
drop (view: any, event: any) {
const hasFiles = event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length
if (!hasFiles) {
return
}
const images = Array
.from(event.dataTransfer.files)
.filter((file: any) => (/image/i).test(file.type))
if (images.length === 0) {
return
}
event.preventDefault()
const { schema } = view.state
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
images.forEach((image: any) => {
const reader = new FileReader()
reader.onload = (readerEvent: any) => {
const node = schema.nodes.image.create({
src: readerEvent.target.result
})
const transaction = view.state.tr.insert(coordinates.pos, node)
view.dispatch(transaction)
}
reader.readAsDataURL(image)
})
}
}
}
})
]
}
}
You're missing a "hole" in the figure caption to make it editable. See https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM. I implemented one of the solutions mentioned in the ProseMirror thread mentioned; however, the cursor can still enter text in between image and figurecaption. I would recommend using the last example with 3 schemas instead.
get schema() {
return {
content: "inline*",
attrs: {src: {default: ""}, title: {default: ""}},
group: 'block',
draggable: true,
isolating: true,
parseDOM: [{
tag: "figure",
contentElement: "figcaption",
getAttrs(dom) {
let img = dom.querySelector("img")
console.log(img, img.getAttribute('src'), img.getAttribute('alt'));
return {src: img.getAttribute('src'), title: img.getAttribute('alt')}
}
}],
toDOM: node => ["figure", ["img", node.attrs], ["figurecaption", 0]],
}
}

ReactiveAggregate() + collection.update() -> Error: Expected to find a document to change

The reactive aggregation is published to the client initially without errors. The error seems to be triggered, when the Meteor.user collection is being updated by a Meteor.call() from the client:
updateProductFavorites = (product_key, action) => {
const { ranking } = this.props
const { product_keys } = ranking[0]
Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else
this.getProductsByKeys(product_keys)
})
}
I have subscribed to both the Meteor.user() and the reactive aggregation:
export default withTracker(() => {
const handle = Meteor.subscribe("products.RankingList")
return {
ranking: AggregatedProductRanking.find({}).fetch(),
user: Meteor.user(),
isLoading: !handle.ready() || !Meteor.user()
}
})(ProductRankingList)
I have declared and imported the clientCollection on both sides, as also suggested in this answer. This is the relevant code on the server side:
const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
// aggregation stages can be seen in the code snippet below
], { clientCollection: "aggregatedProductRanking"})
Meteor.methods({
'Accounts.updateProductFavorites': function(product_key, action) {
allowOrDeny(this.userId)
action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
return Meteor.users.update({_id: this.userId}, action)
}
})
Meteor.publish('products.RankingList', function() {
const callback = () => this.stop()
allowOrDenySubscription(this.userId, callback)
return getProductRankingList(this)
})
What baffles me, is that the update called by Meteor.call('Accounts.updateProductFavorites') is still executed reliably, even with this error being thrown.
So the change to the logged in Meteor.user() flows back to the client and the component rerenders. Only the subscription of the ReactiveAggregate seems to stop working. The following error is thrown and I have to reload the browser to see the changes in the aggregation result. (full stack trace at the bottom)
Uncaught Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
...
// In a certain case the error message is a bit different:
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
...
I am guessing that this update() is called by the ReactiveAggregate() in order to populate the clientCollection. But what am I doing wrong?
For a more complete code sample:
Server side
import { Meteor } from 'meteor/meteor'
import { ReactiveAggregate } from 'meteor/jcbernack:reactive-aggregate';
// Collections
import { AggregatedProductRanking } from '../imports/collections'
const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
{
$match: { productFavorites: {$ne: [] }}
},{
$project: {
_id: 0,
productFavorites: { $concatArrays: "$productFavorites" },
}
},{
$unwind: "$productFavorites"
},{
$facet: {
rankingList: [
{
$group: {
_id: "$productFavorites",
count: { $sum: 1 }
}
},{
$sort: { "count": -1 }
}
],
product_keys: [
{
$group: {
_id: 0,
product_keys: { $addToSet: "$productFavorites" }
}
}
]
}
},{
$unwind: "$product_keys"
},{
$project: {
_id: 0,
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
Meteor.methods({
'Accounts.updateProductFavorites': function(product_key, action) {
allowOrDeny(this.userId)
action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
return Meteor.users.update({_id: this.userId}, action)
},
'Products.getByProductKey': function(productFavorites) {
allowOrDeny(this.userId)
if (productFavorites == undefined)
productFavorites = Meteor.users.findOne({_id: this.userId}, {fields: {productFavorites: 1}}).productFavorites
if (productFavorites.length > 0) {
return Products.find(
{ product_key: {$in: productFavorites }, price_100_g_ml: {$ne: null} },
{ sort: {product_name: 1} }).fetch()
} else
return []
},
})
function allowOrDenySubscription(userId, callback) {
if (!userId) {
callback()
return;
}
}
Meteor.publish(null, function() {
if (!this.userId)
return false
return Meteor.users.find({_id: this.userId}, { fields: {
firstName: 1, lastName: 1,
zip: 1, city: 1, street: 1, houseNumber: 1,
phone: 1, iban: 1, bic: 1,
memberId: 1, membershipFee: 1,
productFavorites: 1
}})
}, { is_auto: true })
Meteor.publish('products.RankingList', function() {
const callback = () => this.stop()
allowOrDenySubscription(this.userId, callback)
return getProductRankingList(this)
})
Client side
import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
// ... more imports
// Collections
import { AggregatedProductRanking } from '../../../imports/collections';
class ProductRankingList extends Component {
constructor() {
super()
this.state = {
products: [],
productDetails: true,
singleProductDetails: 0,
}
}
getProductsByKeys = (product_keys) => {
Meteor.call('Products.getByProductKey', product_keys, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else {
this.setState({products: response})
console.log(response)
}
})
}
updateProductFavorites = (product_key, action) => {
const { ranking } = this.props
const { product_keys } = ranking[0]
console.log(product_keys)
Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else
this.getProductsByKeys(product_keys)
})
}
toggleProductFavorite = (product_key) => {
const { productFavorites } = this.props.user
if (productFavorites.includes(product_key))
this.updateProductFavorites(product_key, 'remove')
else
this.updateProductFavorites(product_key, 'add')
}
mapProductFavorites = () => {
const { products, productDetails, singleProductDetails } = this.state
const { productFavorites } = this.props.user
const { ranking } = this.props
const { rankingList } = ranking[0]
if (products.length == 0)
return <div className="alert alert-primary col-12">No one has favorited any products at the moment, it seems.</div>
products.map((product, i) => {
const { order_number, supplierId } = product
product["count"] = rankingList.find(product => product._id == `${supplierId}_${order_number}`).count
})
products.sort((a, b) => b.count - a.count)
return (
products.map((product, i) => {
if (product.price_100_g_ml) {
var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
}
const { product_name, units, trading_unit, certificate, origin, order_number, supplierId, count } = product
const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'
return (
<div className="col-lg-6" key={i}>
<div key={i} className="product-card">
<div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
{product_name}
{/* <span className="fa-layers fa-fw heart-with-count">
<FontAwesomeIcon icon="heart"/>
<div className="fa-layers-text">{count}</div>
</span> */}
</div>
{productDetails || singleProductDetails == order_number ?
<>
<div className="card-body">
{euro ?
<>
<div className="product-actions">
<button className={`btn btn-light btn-lg product-${isFavorite}`}
onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
<FontAwesomeIcon icon="heart"/>
<span className="ml-2">{count}</span>
</button>
</div>
<div className="price-100-g-ml">
<small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
<big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
</div>
</> : null}
</div>
<div className="card-footer">
<div className="row">
<div className="col-4">{trading_unit}</div>
<div className="col-4 text-center">{certificate}</div>
<div className="col-4 text-right">{origin}</div>
</div>
</div>
</> : null }
</div>
</div>)
})
)
}
componentDidUpdate(prevProps, prevState) {
const { isLoading, ranking } = this.props
if (ranking.length < 1)
return null
if (!isLoading && (prevProps.ranking != ranking)) {
this.getProductsByKeys(ranking[0].product_keys)
}
}
render() {
const { isLoading, ranking } = this.props
if (isLoading || ranking.length < 1)
return null
return(
<div className="row mt-3">
{this.mapProductFavorites()}
</div>
)
}
}
export default withTracker(() => {
const handle = Meteor.subscribe("products.RankingList")
return {
ranking: AggregatedProductRanking.find({}).fetch(),
user: Meteor.user(),
isLoading: !handle.ready() || !Meteor.user()
}
})(ProductRankingList)
Full Stack Trace
// When loading component through history.push()
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
at livedata_connection.js:1192
at Array.forEach (<anonymous>)
at livedata_connection.js:1191
at Array.forEach (<anonymous>)
at Connection._performWrites (livedata_connection.js:1187)
at Connection._flushBufferedWrites (livedata_connection.js:1167)
at meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1234
// After second call of Meteor.call('Accounts.updateProductFavorites')
Uncaught Error: Expected to find a document to change
at Object.update (collection.js:207)
at Object.store.<computed> [as update] (livedata_connection.js:310)
at livedata_connection.js:1192
at Array.forEach (<anonymous>)
at livedata_connection.js:1191
at Array.forEach (<anonymous>)
at Connection._performWrites (livedata_connection.js:1187)
at Connection._flushBufferedWrites (livedata_connection.js:1167)
at Connection._livedata_data (livedata_connection.js:1133)
at Connection.onMessage (livedata_connection.js:1663)
It turned out, that the solution was pretty simple: The reason for the above mentioned errors was a missing _id field in the aggregation result:
...
},{
$project: {
_id: 0, // This will give the error.
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
The docs for ReactiveAggregate() (meteor-reactive-aggregate) state that the _id field can be omitted, as it will be created automatically by ReactiveAggregate(). But even after removing the _id: 0, it didn't work.
What does work is this:
...
},{
$project: {
_id: "whatTheFuckIsGoingOnHere", // Calm down, bro!
rankingList: 1,
product_keys: "$product_keys.product_keys"
}
}
], { clientCollection: "aggregatedProductRanking"})
Wonderfully reactive aggregations, just for a little pain in the a**.
I made a bug report in the github repo

Categories

Resources