Last active
November 7, 2023 07:19
-
-
Save leepfrog/ae977c81fcb705d8fd0bc104016c0272 to your computer and use it in GitHub Desktop.
JSON:API Handlers for msw & mswjs/data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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