# Queries Queries are the core data request interface for any Meetup Web Platform app. They are primarily generated by application Routes to indicate what data each one needs from the API when it is active. However, they can be used for any request to the API, and are therefore used for POST, PATCH, PUT and DELETE requests as well. At a high level, you can think of a Query as a JSON-encoded API request, including all request parameters, the endpoint URI, and any metadata properties like custom headers ## Spec ### Query object A Query is just a plain object with the following shape: A Query is just a plain object with the following shape: ```js { ref: string, endpoint: string, list?: { dynamicRef: string, merge?: { sort: (Object, Object) => number, idTest: (Object, Object) => boolean, } }, params?: object, type?: string, // DEPRECATED meta?: { flags?: string[], method?: string variants: { [string]: string | number | string[] | number[], // e.g. { experiment1: chapterId } }, metaRequestHeaders?: string[], }, } ``` **Example** ```js { ref: 'foobar', endpoint: 'foo/bar', params: { memberId: 1234, }, type: 'foo', // generally not needed meta: { flags: ['thisflag', 'thatflag'], method: 'get', // generally not needed variants: { 'my-member-experiment': '1234', // memberId - MUST BE STRING (e.g. `memberId.toString()`) 'my-group-experiment': '5678', // chapterId }, metaRequestHeaders: ['unread-messages'], }, } ``` #### `ref` A unique string reference to the query. The `ref` is used to uniquely identify the query _and_ uniquely assign the resulting API data to Redux state at `state.api[ref]`. #### `endpoint` The endpoint can either be a URL pathname that assumes the REST API domain api.meetup.com, e.g. `members/123456` will call https://api.meetup.com/members/123456, or a fully-qualified URL that specifies an alternative domain, e.g. `https://example.com/list` will be used as-is. `endpoint: 'members/123456'` and `endpoint: 'https://api.meetup.com/members/123456'` are functionally equivalent _Note_: this URL should not include any parameter placeholders like `/:urlname` - the values should be filled in as needed. #### `params` The parameters that should be passed to the API either in the querystring (for GET requests) or the request body (for POST requests). #### `type` A one-word description of the 'data type' expected to be returned by the API for this query. This information is used on the server to further process the data for certain data types like `'group'` objects, which will receive special duotone photo URLs in addition to the standard photo URLs provided by the API. The `type` should be the same regardless of whether the returned data is an array or a singleton. **Type examples** - `group` - `member` - `event` - `comment` - feature-specific objects like `home`, `conversations` #### `meta` ##### `flags` An array of feature flag (Runtime Flag) names that should be returned alongside the main request. #### `metaRequestHeaders` The `metaRequestHeaders` property is in reference to `X-Meta-Request-Headers` in the [meetup api](https://www.meetup.com/meetup_api/#meta-headers). The response for each header passed in will be in `REF.meta`, converted from `snake-case` to `camelCase`. ##### `method` You can force the query to be sent with a particular HTTP method by specifying it here as a string: `get`, `post`, `delete`, `patch` or `put`. Note that you should never try to make a request that contains multiple queries with different `method`s. Alternatively, you can use method-specific action creators to automatically assingn the method and make the request for individual requests. ##### `variants` [DEPRECATED - call variants endpoint directly] You can request variant names for particular experiments with particular contexts by populating `meta.variants` with an object containing keys that are experiment names and values that are string IDs (member ID or chapter ID depending on the experiment). ### Query Response A query response is also a plain object ```js { ref: string, value: any, type?: string, meta?: { flags?: string[], variants?: { [string]: { // experiment [string]: string // context: variant }, }, }, } ``` #### `meta` #### `variants` [DEPRECATED - call variants endpoint directly] If a query contains a `meta.variants` request, the query response _might not contain a corresponding `variants` response_ if the variants service fails - you must test for the existence of `meta.variants` before reading from it. ## Usage In general, queries start as the payload of an `API_REQ` action, which will generate responses that are applied to Redux state. ### Action creation You should always use the action creators in `apiActionCreators` to dispatch API requests. Each action creator takes a single query or an array of queries as its first argument, and an optional `meta` argument #### `get` ```js import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; const getQuery = { endpoint: 'ny-tech/members', ref: 'newMember', }; const getAction = api.get(getQuery); ``` #### `post`/`patch`/`put` ```js import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; const postQuery = { endpoint: 'ny-tech/members', ref: 'newMember', params: { name, bio }, }; const postAction = api.post(postQuery); const patchAction = api.patch(postQuery); const putAction = api.put(postQuery); ``` #### `delete` ```js import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; const deleteQuery = { endpoint: 'ny-tech/members/123456', ref: 'deletedMember', params: { id }, }; const deleteAction = api.del(deleteQuery); // note `api.del` not `api.delete` because `delete` is a keywork ``` ### Query dispatch Use Redux's `store.dispatch` or `bindActionCreators` to dispatch the query actions. #### `Promise` interface for dispatched queries An API request action object always has a type of `API_REQ`, and it will always have a `meta` property that contains a `request` property corresponding to the current status of the request. The Promise will resolve on a successful API request, and `reject` on a 400/500 error from the fetch call. This property can be accessed by the `dispatch` _caller_ by consuming the return value of the `dispatch()` call, which is the dispatched action itself. The caller can then attach response handlers in `action.meta.request.then()` Promise callbacks. **Example using `mapDispatchToProps`** ```js // Example.jsx import { SubmissionError } from 'redux-form'; import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; function mapDispatchToProps(dispatch) { return bindActionCreators({ post: api.post }, dispatch); } class Example extends React.Component { onSubmit() { const formQuery = { ... }; const apiRequest = this.props.post(formQuery); // this triggers the POST, returns dispatched action object const { request } = apiRequest.meta; request.then(response => { const formResponse = response; // validate the form, throw `SubmissionError` as needed }) } } ``` ### Sync middleware When query actions are dispatched, the sync middleware will generate a corresponding `fetch` to the API proxy endpoint of the app server, e.g. ``` GET /mu_api?queries=[query, ...] ``` **API response array returned as JSON from app server** The app server API proxy endpoint will respond with an array of Query responses. ```js [ { ref: string, value: {}, error?: {}, type?: string, meta?: {}, // data returned from API separate from `value` }, // ... ] ``` The fetchQueries function will then filter these Query Response objects into separate `successes` and `errors` arrays, where each array element is an object containing the original `query` object and its corresponding `response`: ```js { successes: [{ query, response }, ...], errors: [{ query, response }, ...], } ``` The sync middleware will read these two arrays and generate a separate `API_RESP_SUCCESS` or `API_RESP_ERROR` action for each response object. #### Request failure - `API_RESP_FAIL` If the `fetch` fails entirely, no responses will be delivered to the application. Instead, an `API_RESP_FAIL` action will be dispatched with an `Error` payload. #### Request complete - `API_RESP_COMPLETE` After all Query Responses have been dispatched, the sync middleware will dispatch a final `API_RESP_COMPLETE` action. ### Platform `api` reducer - `state.api` When `API_REQ` is first dispatched, the platform `api` reducer will add each Query's `ref` to an `inFlight` array. This property can be inspected to determine whether a particular ref is 'in-flight', e.g. ```js mapStateToProps(state) { return { isLoading: state.api.inFlight.includes(myRef), }; } ``` When the API responds, the platform reducer will read the `API_RESP_SUCCESS` and `API_RESP_ERROR` values into `state.api[ref]` for each response and clear the corresponding `ref` from `state.api.inFlight`. **Redux state after being processed by `API_RESP_SUCCESS` action** ```js { [ref]: { ref, type?, value: {}, meta?: {} }, // ... } ``` **Redux state after being processed by `API_RESP_ERROR` action** ```js { [ref]: { ref, type?, error: error, meta?: {} }, // ... } ``` If `API_RESP_FAIL` is dispatched, `state.api` will not receive new data, but will instead populate `state.fail` with an `Error` object describing the the failed call. **Redux state after being processed by `API_RESP_FAIL` action** ```js { fail: Error, // ... all other data is stale but technically still valid } ``` ### Route query functions ```js // see description for docs about object argument ({ params, isExact, url, path, location }, state) => Query; ``` One of the primary uses for queries is to load route-specific data from the API. The application will automatically generate these queries by calling particular 'query creator' functions that are assigned to application routes, producing a fully-qualified 'query' object from the routing state input. Route query creator functions have two requirements: 1. They are assigned as _props_ of React Router `route`s .The `query` prop can be either a single query creator function or an array of functions 2. They are pure functions that take two arguments: - object constructed from the [`match` object from React Router](https://reacttraining.com/react-router/web/api/match), which includes `params` extracted from the URL) with an additional `location` property corresponding to the current [React Router `location`](https://reacttraining.com/react-router/web/api/location). - current Redux `state`, _including feature flag values_ More info in the [mwp-router docs](../mwp-router/README.md). #### Example query function ```js export const GROUP_REF = 'group'; function groupQuery({ params, location }) { const { urlname } = params; return { ref: GROUP_REF, type: 'group', endpoint: `/${urlname}`, params: { fields: ['event_sample'], }, }; } // applied to a route: const groupRoute = { path: '/:urlname', query: groupQuery, // or [groupQuery, ...] component: GroupContainer, }; ``` ## React recipes ### GET (lazy loading) On page load, the application will automatically collect any query objects generated by the query functions you assign to your routes. However, if you need to make another GET request, you can manually dispatch an API request using the `get` action creator from [`apiActionCreators`](../src/actions/apiActionCreators.js). ```js // Example.jsx import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; function mapDispatchToProps(dispatch) { return bindActionCreators({ get: api.get }, dispatch); } class Example extends React.Component { componentDidMount() { const lazyQuery = { endpoint: `${this.props.match.params.urlname}/more/stuff`, ref: 'moreStuff', params: { foo: 'bar' }, }; this.props.get(lazyQuery); } } ``` ### POST ```js // Example.jsx import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; const NEW_STUFF_REF = 'newStuff'; function mapStateToProps(state) { // when the POST returns, the response will be accessible in Redux state, // populated by an `API_RESP_SUCCESS` or `API_RESP_ERROR` action return { NEW_STUFF_REF: state.api[NEW_STUFF_REF], }; } function mapDispatchToProps(dispatch) { return bindActionCreators({ post: api.post }, dispatch); } class Example extends React.Component { componentDidUpdate(prevProps) { if (prevProps[NEW_STUFF_REF] !== this.props[NEW_STUFF_REF]) { // the POST returned _something_ - maybe an error // you probably want to call `this.setState` or something here } } onSubmit(e) { e.preventDefault(); // prevent full-page submit const postQuery = { endpoint: `${this.props.match.params.urlname}/new/stuff`, ref: NEW_STUFF_REF, params: this.state.formValues, // this would be set by controlled inputs in the form }; this.props.post(postQuery); } render() { return
...
; } } ``` #### Uploading files API POST/PATCH/PUT endpoints that support file uploads have one additional constraint because the file data cannot be easily JSON-serialized like `params` in other query objects. The standard way of encoding form data that includes file uploads is to assemble the entire form contents into a [`FormData` instance](https://developer.mozilla.org/en/docs/Web/API/FormData), which allows the file contents to be passed around the application as a `Blob` that can be encoded for transmission. To form a Query with form data, simply pass a `FormData` instance as the `params` property ```js // Example.jsx import * as api from 'meetup-web-platform/lib/actions/apiActionCreators'; const NEW_FILE_STUFF = 'newFileStuff'; function mapStateToProps(state) { // when the POST returns, the response will be accessible in Redux state, // populated by an `API_RESP_SUCCESS` or `API_RESP_ERROR` action return { NEW_FILE_STUFF: state.api[NEW_FILE_STUFF], }; } function mapDispatchToProps(dispatch) { return bindActionCreators({ post: api.post }, dispatch); } class Example extends React.Component { componentDidUpdate(prevProps) { if (prevProps[NEW_FILE_STUFF] !== this.props[NEW_FILE_STUFF]) { // the POST returned _something_ - maybe an error // you probably want to call `this.setState` or something here } } onSubmit(e) { e.preventDefault(); // prevent full-page submit const postQuery = { endpoint: `${this.props.match.params.urlname}/new/stuff`, ref: NEW_FILE_STUFF, params: new FormData(this.form), // one stop form encoding - forces 'multipart/form-data' content type }; this.props.post(postQuery); } render() { return (
(this.form = el)}> ...
); } } ``` ### PATCH In practice, a PATCH request is just a POST by another name - use the `post` action creator from `apiActionCreators`. ### PUT The difference between PUT and POST is that PUT is idempotent: calling it once or several times successively has the same effect. ### DELETE In practice, a DELETE request is just a GET by another name - use the `del` action creator from `apiActionCreators`. The response will generally be a '204 - No Content', so you'll have to read the 'updated' value in Redux state a little more carefully. ## Calling the variants service The variants service provides its own public API endpoint returning JSON. See [the variants service README](https://github.com/meetup/variant/blob/master/README.md) and [the OpenAPI spec](https://github.com/meetup/variant/blob/master/src/main/openapi/meetup-scala-server/variant.yaml). To use it in production, set the `endpoint` to `https://variant.data.meetuphq.io/variant/v2/${experimentId}/${entityId}` where `expirementId` is the name of your experiment in the variants service, and `entityId` is a _member ID_, _chapter ID_, or group `urlname`, depending on the type of experiment. _In dev_, use the `variantNoEnrollment` version of the endpoint (documented in the OpenAPI spec) in order to prevent the experiment data from showing up in site analytics (Looker), i.e. `https://variant.data.meetuphq.io/variantNoEnrollment/v2/${experimentId}/${entityId}` ```js import { getProperty } from '@meetup/api-state-selectors'; const MY_VARIANT_REF = 'my-cool-experiment-variant'; const variantEndpoint = process.env.NODE_ENV === 'production' ? 'variant' : 'variantNoEnrollment'; const myVariantQuery = { endpoint: `https://variant.data.meetuphq.io/${variantEndpoint}/v2/my-cool-experiment/${member.id}`, ref: MY_VARIANT_REF, }; const myVariantSelector = state => { const response = state.api[MY_VARIANT_REF] || {}; // get the assigned variant as a string, default to '' return getProperty(response, 'variant', ''); }; ```