Skip to content

Instantly share code, notes, and snippets.

@leepfrog
Last active November 7, 2023 07:19
Show Gist options
  • Save leepfrog/ae977c81fcb705d8fd0bc104016c0272 to your computer and use it in GitHub Desktop.
Save leepfrog/ae977c81fcb705d8fd0bc104016c0272 to your computer and use it in GitHub Desktop.
JSON:API Handlers for msw & mswjs/data
import { http, HttpHandler, HttpResponse } from 'msw';
import db from './db';
import {
jsonApiIndexDocument,
jsonApiShowDocument,
jsonApiResource,
jsonApiResponseHeaders,
} from 'my-app/mocks/jsonapi';
import { getObjectsByKeypath } from 'my-app/mocks/utils';
import { pluralize } from 'ember-inflector';
import { Model, ModelKey } from 'my-app/mocks/index';
const handlers: HttpHandler[] = [
indexHandlerFor('book'),
indexHandlerFor('author'),
showHandlerFor('book'),
];
export function indexHandlerFor<T extends ModelKey>(key: T) {
const plural = pluralize(key);
return http.get(`/${plural}`, ({ request }) => {
const url = new URL(request.url);
const data = db[key].findMany({
orderBy: { createdAt: 'desc' },
}) as Model<T>[];
const body = jsonApiIndexDocument(data, url);
const include = url.searchParams.get('include');
if (include) {
body['included'] = data.flatMap((resource) => {
return getObjectsByKeypath(resource, include)
.flat()
.map(jsonApiResource);
});
}
return HttpResponse.json(body, {
status: 200,
headers: jsonApiResponseHeaders(),
});
});
}
export function showHandlerFor<T extends ModelKey>(key: T) {
const plural = pluralize(key);
return http.get(`/${plural}/:id`, ({ request, params }) => {
let { id } = params;
id = id as string;
const url = new URL(request.url);
const data = db[key].findFirst({
where: { id: { equals: id } },
}) as Model<T>;
if (!data) return new HttpResponse(null, { status: 404 });
const body = jsonApiShowDocument(data, url);
const include = url.searchParams.get('include');
if (include) {
body['included'] = getObjectsByKeypath(data, include)
.flat()
.map(jsonApiResource);
}
return HttpResponse.json(body, {
status: 200,
headers: jsonApiResponseHeaders(),
});
});
}
export default handlers;
import { models } from 'my-app/mocks/db';
import { Entity, ModelValueType } from '@mswjs/data/lib/glossary';
declare type ModelDictionary = typeof models;
declare type ModelKey = keyof typeof models;
declare type StringIndexed = { [key: string]: ModelValueType | E | E[] };
declare type E = Entity<ModelDictionary, ModelKey> & StringIndexed;
declare type Model<T extends ModelKey> = E<ModelDictionary, T>;
// Serialize json:api objects from provided mswjs/data object
import { ENTITY_TYPE } from '@mswjs/data/lib';
import { dasherize } from '@ember/-internals/string';
import { findGetters } from 'my-app/mocks/utils';
import { singularize } from 'ember-inflector';
import { E, Model, ModelKey } from 'my-app/mocks/index';
export function jsonApiResponseHeaders() {
return {
'content-type': 'application/vnd+api.json',
};
}
export function jsonApiIndexDocument(data: E[], url: URL): TopLevelDocument {
return {
data: data.map(jsonApiResource),
links: {
self: url.toString(),
},
};
}
export function jsonApiShowDocument(data: E, url: URL): TopLevelDocument {
return {
data: jsonApiResource(data),
links: {
self: url.toString(),
},
};
}
export function jsonApiResource<T extends ModelKey>(
record: Model<T>,
): Resource {
const { id } = record;
const type = dasherize(singularize(record[ENTITY_TYPE] as string));
const attributes = { ...record };
const relationships = findGetters(record).reduce(
(relationships: Relationships, relationshipKey) => {
const relationship = jsonApiRelationship(
record,
relationshipKey,
) as unknown as E;
if (!relationship) return relationships;
relationships[relationshipKey] = relationship as Partial<E>;
delete attributes[relationshipKey];
return relationships;
},
{},
);
delete attributes['id'];
return {
attributes,
id: id as string,
links: {
self: `http://localhost:4000/${type}/${id}`,
},
type,
relationships,
};
}
export function jsonApiRelationship<T extends ModelKey>(
data: Model<T>,
key: string,
): Relationship | undefined {
const related = data[key];
if (!related) return undefined;
if (Array.isArray(related))
return { data: related.map(jsonApiResourceLinkage) as ResourceLinkage };
return { data: jsonApiResourceLinkage(related) };
}
export function jsonApiResourceLinkage(data: E): ResourceLinkage {
const id = data['id'] as string;
const type = dasherize(singularize(String(data[ENTITY_TYPE])));
return {
id,
type,
};
}
import { E } from 'my-app/mocks/index';
import { ENTITY_TYPE } from '@mswjs/data/lib';
/**
Generate an array of objects obtained from the provided keypath.
Used for handling GET /?include=<keypath1,keypath2>
*/
export function getObjectsByKeypath(obj: E, keypath: string) {
const segments = keypath.split(',');
return segments.flatMap((keypath) =>
keypath.split('.').reduce<E[]>((models, key) => {
// First iteration of the reduce
if (models.length === 0) {
const include = obj[key] as E | E[];
if (isPrimitive(include)) return [];
if (Array.isArray(include)) return include;
return [include];
} else {
// Go through each previous model until the type isn't the same
// Assume the last object fetched is the type
let type;
for (let cursor = models.length - 1; cursor >= 0; cursor--) {
const model = models[cursor];
if (!model) break;
if (!type) type = model[ENTITY_TYPE];
if (model[ENTITY_TYPE] !== type) break;
let include = model[key] as E | E[];
if (isPrimitive(include)) break;
include = include as E | E[];
if (Array.isArray(include)) {
models = [...models, ...include];
} else {
models = [...models, include];
}
}
}
return models;
}, [] as E[]),
);
}
function isPrimitive(obj: unknown) {
return (
typeof obj === 'string' ||
typeof obj === 'number' ||
typeof obj === 'bigint' ||
typeof obj === 'boolean' ||
typeof obj === 'undefined' ||
typeof obj === 'symbol'
);
}
/**
find getter functions for the provided object
*/
export function findGetters(obj: E) {
const getters = [];
for (const prop of Object.keys(obj)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (descriptor && typeof descriptor.get === 'function') {
getters.push(prop);
}
}
return getters;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment