Skip to content
This repository has been archived by the owner on Jun 19, 2023. It is now read-only.

Latest commit

 

History

History
601 lines (471 loc) · 16.7 KB

File metadata and controls

601 lines (471 loc) · 16.7 KB

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:

{
  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

{
  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. 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 methods.

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

{
  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

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

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

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

// 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.

[
  {
    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:

{
  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.

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

{
  [ref]: {
    ref,
    type?,
    value: {},
    meta?: {}
  },
  // ...
}

Redux state after being processed by API_RESP_ERROR action

{
  [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

{
  fail: Error,
  // ...  all other data is stale but technically still valid
}

Route query functions

// 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 routes .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:

More info in the mwp-router docs.

Example query function

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, ...<other queries>]
	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.

// 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

// 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 <form onSubmit={this.onSubmit}>...</form>;
	}
}

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, 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

// 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 (
			<form onSubmit={this.onSubmit} ref={el => (this.form = el)}>
				...
			</form>
		);
	}
}

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 and the OpenAPI spec.

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}

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', '');
};