How to use the post_poll method in Zapier CLI - zapier-cli

According to the docs, I should use a post_poll function to add the missing id field in the response.
How do I add the post_poll function ?
Here's my error:
Results must be an array, got: object,
({"totalevents":83,"events":[{"eventid":10266033,"c)
- Got a result missing the "id" property (83)
Tried following this but it is not clear to me, I'm very new to Zapier-CLI
Update - adding code
This is the function that returns the data:
const listEvents = (z, bundle) => {
console.log('listing events.. ');
let client_id = bundle.inputData.client_id;
const requestOpts = {
url: `https://wccqa.on24.com/wcc/api/v2/client/${client_id}/event`
};
return z.request(requestOpts)
.then((response) => {
return z.JSON.parse(response.content);
});
};
The sample response is the following, with the distiction that I added the id param manually to avoid errors when zapier test|push:
{
"id": 9964513,
"eventid": 9964513,
"archivestart": "2017-09-21T10:30:00-07:00",
"archiveend": "2018-09-21T10:30:00-07:00",
"description": "Zapier Event Test",
"iseliteexpired": "N",
"displaytimezonecd": "America/Bogota",
"eventtype": "Live Webcam ",
"regrequired": true,
"clientid": 22921,
"liveend": "2017-09-21T10:00:00-07:00",
"createtimestamp": "2017-09-21T09:47:44-07:00",
"audienceurl": "https://localhost.on24.com/wcc/r/9964513/C49755A02229BD48E6010848D7C81EF8",
"lastmodified": "2017-09-21T09:47:44-07:00",
"livestart": "2017-09-21T08:45:00-07:00",
"goodafter": "2017-09-21T09:00:00-07:00",
"regnotificationrequired": true,
"isactive": true,
"localelanguagecd": "en"
}
The ACTUAL response from the endpoint the following which is used in the app created in the Web Builder App instead of CLI and works fine:
{
"events": [
{
"eventid": 9964513,
"archivestart": "2017-09-21T10:30:00-07:00",
"archiveend": "2018-09-21T10:30:00-07:00",
"description": "Zapier Event Test",
"iseliteexpired": "N",
"displaytimezonecd": "America/Bogota",
"eventtype": "Live Webcam ",
"regrequired": true,
"clientid": 22921,
"liveend": "2017-09-21T10:00:00-07:00",
"createtimestamp": "2017-09-21T09:47:44-07:00",
"audienceurl": "https://localhost.on24.com/wcc/r/9964513/C49755A02229BD48E6010848D7C81EF8",
"lastmodified": "2017-09-21T09:47:44-07:00",
"livestart": "2017-09-21T08:45:00-07:00",
"goodafter": "2017-09-21T09:00:00-07:00",
"regnotificationrequired": true,
"isactive": true,
"localelanguagecd": "en"
}
],
"totalevents": 1
}
I was thinking something along the line of the following, but how do I register this ?
const postPoll = (event,z,bundle) => {
if(event.key === 'events'){
var results = z.JSON.parse(bundle.request.data).results;
var events = results.events.map(function(event){
event.id = event.eventid;
return event;
});
bundle.request.data = events;
}
};
module.exports = postPoll;

Nice, so you're almost there! CLI apps don't have pre_ and post_ poll methods. Instead, you put any manipulation after the response comes in.
const listEvents = (z, bundle) => {
console.log('listing events.. ');
let client_id = bundle.inputData.client_id;
const requestOpts = {
url: `https://wccqa.on24.com/wcc/api/v2/client/${client_id}/event`
};
return z.request(requestOpts)
.then((response) => {
return z.JSON.parse(response.content);
})
.then(data => {
const events = data.events; // array of events
return events.map(function(e){ // returns array of objects with `id` defined
e.id = e.event_id
return e
})
})
};

Related

Slack Bolt App: Options body view state is not updated like actions body view state

I am trying to implement dependent external selects inside a modal but I am having problems passing the state of the first dropdown to the second. I can see the state I need inside the app.action listener but I am not getting the same state inside the app.options listener.
body.view.state inside app.action("case_types"). I specifically need the case_create_case_type_block state.
"state": {
"values": {
"case_create_user_select_block": {
"case_create_selected_user": {
"type": "users_select",
"selected_user": "U01R3AE65GE"
}
},
"case_create_case_type_block": {
"case_types": {
"type": "external_select",
"selected_option": {
"text": { "type": "plain_text", "text": "Incident", "emoji": true },
"value": "Incident"
}
}
},
"case_create_case_subtype_block": {
"case_subtypes": { "type": "external_select", "selected_option": null }
},
"case_create_case_owner_block": {
"case_owners": { "type": "external_select", "selected_option": null }
},
"case_create_subject_block": {
"case_create_case_subject": {
"type": "plain_text_input",
"value": null
}
},
"case_create_description_block": {
"case_create_case_description": {
"type": "plain_text_input",
"value": null
}
}
}
},
body.view.state inside app.options("case_subtypes")
"state": {
"values": {
"case_create_user_select_block": {
"case_create_selected_user": {
"type": "users_select",
"selected_user": "U01R3AE65GE"
}
}
}
},
I did also try to update the view myself hoping it would update the state variables inside app.action({ action_id: "case_types" })
//need to update view with new values
try {
// Call views.update with the built-in client
const result = await client.views.update({
// Pass the view_id
view_id: body.view.id,
// Pass the current hash to avoid race conditions
hash: body.view.hash,
});
console.log("Case Type View Update result:");
console.log(JSON.stringify(result));
//await ack();
} catch (error) {
console.error(error);
//await ack();
}
I ended up posting this on the github issues page for slack bolt. This was a bug that will fixed in a future release. Below is the workaround using private metadata to hold the state to check for future dependent dropdowns.
// Action handler for case type
app.action('case_type_action_id', async ({ ack, body, client }) => {
try {
// Create a copy of the modal view template and update the private metadata
// with the selected case type from the first external select
const viewTemplate = JSON.parse(JSON.stringify(modalViewTemplate))
viewTemplate.private_metadata = JSON.stringify({
case_type: body.view.state.values['case_type_block_id']['case_type_action_id'].selected_option.value,
});
// Call views.update with the built-in client
const result = await client.views.update({
// Pass the view_id
view_id: body.view.id,
// Pass the current hash to avoid race conditions
hash: body.view.hash,
// Pass the updated view
view: viewTemplate,
});
console.log(JSON.stringify(result, 0, 2));
} catch (error) {
console.error(error);
}
await ack();
});
// Options handler for case subtype
app.options('case_subtype_action_id', async ({ body, options, ack }) => {
try {
// Get the private metadata that stores the selected case type
const privateMetadata = JSON.parse(body.view.private_metadata);
// Continue to render the case subtype options based on the case type
// ...
} catch (error) {
console.error(error);
}
});
See the full explaination here: https://github.com/slackapi/bolt-js/issues/1146

Remove null results from a array that can contain nullable values in GraphQL

I have a query in my app that works but response is little ugly, there is probably two ways to solve this:
Write resolver differently
Clean response from null values
Here is resolver:
t.list.field('manyDevices', {
type: 'Device',
description: 'Get list of devices belonging to user',
args: {
input: nonNull(deviceIdentifierInput.asArg()),
},
resolve: async (_, { input: { id } }, { prisma }) => {
return await prisma.device.findMany({ where: { userId: id } });
},
});
This resolver looks for all devices with provided id. Id can be mine and also can be from a some other user. Devices can be public and private, and I don't want to receive private devices except if they are mine.
const isDevicePublic = rule({ cache: 'strict' })(
async ({ isPublic }: Device) => {
if (!isPublic) {
return permissionErrors.noPermission;
}
return true;
},
);
const isDeviceOwner = rule({ cache: 'strict' })(
async ({ userId }: Device, _, { user }: Context) => {
assertValue(user, permissionErrors.noAuthentication);
if (userId !== user.id) {
return permissionErrors.noPermission;
}
return true;
},
);
These are rules that I place on my schema with graphql-shield library and it works. There is just one problem, if a user have a private device it will be listed in response array as null and graphql-shield will throw error, so response can look like this:
{
"errors": [
{
"message": "You have no permission to access this resource",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": [
"manyDevices",
0,
"name"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: You have no permission to access this resource",
" at Rule.resolve (/workspace/node_modules/graphql-shield/dist/rules.js:33:24)",
" at runMicrotasks (<anonymous>)",
" at processTicksAndRejections (internal/process/task_queues.js:93:5)",
" at async Promise.all (index 0)"
]
}
}
}
],
"data": {
"manyDevices": [
null,
{
"name": "device-01"
}
]
}
}
So there is one fetched device and other that is private that throws this error, can I somehow remove null and error response or should I filter them out in resolver?

sendResponse in Port.postmessage()

I have the following code
browser.runtime.onConnect.addListener(function (externalPort) {
externalPort.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(42);
}
});
However, it seems that listeners for Port.onMessage do not get called with a sendResponse as listeners for browser.runtime.onMessage.
Any idea how to send responses for messages to ports?
Port-based messaging doesn't use sendResponse. Simply post another message to the port.
Here's a very simplified example of a port-based messaging system. It doesn't transfer errors or exceptions, doesn't have a timeout. The idea is to pass an id, save the callback for the id in a map, and use the same id in the response to call that saved callback.
Unlike browser.runtime.sendMessage that creates a new port each time (a relatively expensive operation in case you send a lot of messages), we reuse the same port.
sender:
const port = browser.runtime.connect({name: 'foo'});
const portMap = new Map();
let portMessageId = 0;
port.onMessage.addListener(msg => {
const {id, data} = msg;
const resolve = portMap.get(id);
portMap.delete(id);
resolve(data);
});
function send(data) {
return new Promise(resolve => {
const id = ++portMessageId;
portMap.set(id, resolve);
port.postMessage({id, data});
});
}
usage:
(async () => {
const response = await send({foo: 'whatever'});
console.log(response);
})();
receiver:
/** #param {chrome.runtime.Port} port */
browser.runtime.onConnect.addListener(port => {
if (port.name === 'foo') {
port.onMessage.addListener(msg => {
const {id, data} = msg;
port.postMessage({id, data: processMessage(data)});
});
}
});
The Port.postMessage() is a push-only messaging method, so you need to use regular runtime.sendMessage() method in parallel. Here is an example:
manifest.json:
{
"name": "panel example",
"version": "1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_title": "panel",
"default_popup": "panel.html"
},
"permissions": [
"tabs"
]
}
background.js:
browser.runtime.onConnect.addListener(port => {
let tabId;
const listenerForPort = (message, sender) => {
if (message &&
typeof message == 'object' &&
message.portName == port.name) {
switch (message.type) {
case 'get-tabId':
return Promise.resolve(tabId);
}
}
};
browser.runtime.onMessage.addListener(listenerForPort);
port.onMessage.addListener(message => {
if (message &&
typeof message == 'object' &&
message.tabId)
tabId = message.tabId;
});
port.onDisconnect.addListener(port => {
browser.runtime.onMessage.removeListener(listenerForPort);
if (tabId)
browser.tabs.remove(tabId);
});
});
panel.html:
<!DOCTYPE html>
<script type="application/javascript" src="panel.js"></script>
<button id="button">Click Me</button>
panel.js:
browser.windows.getCurrent({ populate: true }).then(win => {
const portName = `port for window ${win.id}`;
const activeTab = win.tabs.find(tab => tab.active);
const port = browser.runtime.connect({
name: portName
});
port.postMessage({ tabId: activeTab.id });
const button = document.getElementById('button');
button.addEventListener('click', async event => {
const tabIdFromBackground = await browser.runtime.sendMessage({
type: 'get-tabId',
portName
});
button.textContent = tabIdFromBackground;
});
});
In this example, there is a listener corresponding to a connection and it is designed to respond only to messages sent with the corresponding port name.

Why does `chrome.notifications.update` only update once on Windows 10?

I'm developing a chrome extension that initiates a 3 seconds countdown using notifications. It works by creating an initial notification "recording in 3 seconds" and then updating that notification every second:
background-script.js
const waitForMs = (ms) => new Promise(r => setTimeout(r, ms))
const displayNotification = (message, updateNotificationId = undefined, expireInMs = undefined) => new Promise(
(resolve) => {
const notificationOptions = {
type: 'basic',
iconUrl: 'icon.png', // required
title: message,
message,
// Avoids sounds and vibrations
silent: true,
// Required to have notification update in real-time
requireInteraction: true
}
// Resolves with given notification id, possibly after clearing after timeout
const resolveWithId = async (id) => {
if (typeof expireInMs === 'number') {
await waitForMs(expireInMs)
chrome.notifications.clear(id)
}
resolve(id)
}
// Update if notification id is defined
// Otherwise create a new notification
if (typeof updateNotificationId === 'string') {
chrome.notifications.update(
updateNotificationId,
notificationOptions,
(updated) => {
console.log(`updated? ${updated}`)
resolveWithId(updateNotificationId)
}
)
}
else {
chrome.notifications.create(
notificationOptions,
(id) => resolveWithId(id)
)
}
}
)
chrome.browserAction.onClicked.addListener(() => {
chrome.notifications.getPermissionLevel(async (permissionLevel) => {
console.log(`permissionLevel: ${permissionLevel}`)
if (permissionLevel === 'granted') {
const notificationId = await displayNotification('Recording in 3...')
await waitForMs(1000)
await displayNotification('Recording in 2...', notificationId)
await waitForMs(1000)
await displayNotification('Recording in 1...', notificationId)
await waitForMs(1000)
await displayNotification('Now recording!', notificationId, 3000)
}
})
})
manifest.json:
{
"name": "NotificationExample",
"short_name": "NotificationExample",
"version": "1.0.0",
"manifest_version": 2,
"description": "",
"background": {
"scripts": [
"background-script.js"
],
"persistent": false
},
"browser_action": {
"default_title": "Notification Countdown Example"
},
"permissions": [
"notifications"
]
}
However, as you can see in this screen recording, on Windows 10 the notification is only updated successfully once (from 3 seconds to 2 seconds), and does not update the 3rd or 4th time. On MacOS it does work. Why does this not work on Windows 10 and how could this be resolved?

useState depending on other state

I have this useSiren hook that should update its state with the incoming json argument but it doesnt.
On the first call the json is an empty object, because the fetch effect has not been run yet.
On the second call its also an empty object (triggered by loading getting set to true in App)
And on the third call its filled with valid data. However, the valid data is not applied. The state keeps its initial value.
I guess somehow setSiren must be called to update it, since initial state can only be set once. But how would I do that? Who should call `setSiren?
import { h, render } from 'https://unpkg.com/preact#latest?module';
import { useEffect, useState, useCallback } from 'https://unpkg.com/preact#latest/hooks/dist/hooks.module.js?module';
import htm from "https://unpkg.com/htm#latest/dist/htm.module.js?module";
const html = htm.bind(h);
function useFetch({
method = "GET",
autoFetch = true,
href,
body
}) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState()
const [response, setResponse] = useState()
const [isCancelled, cancel] = useState()
const [json, setJson] = useState({})
const sendRequest = async payload => {
try {
setLoading(true)
setError(undefined)
const response = await fetch(href.replace("http://", "https://"), {
method
})
const json = await response.json()
if (!isCancelled) {
setJson(json)
setResponse(response)
}
return json
} catch (err) {
if (!isCancelled) {
setError(err)
}
throw err
} finally {
setLoading(false)
}
}
if (autoFetch) {
useEffect(() => {
sendRequest(body)
return () => cancel(true)
}, [])
}
return [{
loading,
response,
error,
json
},
sendRequest
]
}
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}
function Action(props) {
const [{ loading, error, json }, sendRequest] = useFetch({ autoFetch: false, href: props.href, method: props.method })
const requestAndUpdate = () => {
sendRequest().then(props.onRefresh)
}
return (
html`
<button disabled=${loading} onClick=${requestAndUpdate}>
${props.title}
</button>
`
)
}
function App() {
const [{ loading, json }, sendRequest] = useFetch({ href: "https://restlr.io/toggle/0" })
const [{ state, actions }, setSiren] = useSiren(json)
return (
html`<div>
<div>State: ${loading ? "Loading..." : (state.properties && state.properties.value)}</div>
${actions.map(action => html`<${Action} href=${action.href} title=${action.title || action.name} method=${action.method} onRefresh=${setSiren}/>`)}
<button disabled=${loading} onClick=${sendRequest}>
REFRESH
</button>
</div>
`
);
}
render(html`<${App}/>`, document.body)
Maybe what you want to do is to update the siren state when the json param changes? You can use a useEffect to automatically update it.
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
useEffect(() => { // here
setSiren(json)
}, [json])
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}
The pattern mentioned by #awmleer is packaged in use-selector:
import { useSelectorValue } from 'use-selector';
const { entities=[], actions=[], title} = json;
const siren = useSelectorValue(() => ({
state: entities.find(entity => entity.class === 'state') || {},
actions,
title
}), [json]);
Disclosure I'm author and maintainer of use-selector

Resources