Repository navigation
Description
Something I've realized with the addition of the useSubrequests pattern I introduced with #68 is that it would actually be kind of nice to implement some kind of plugin architecture for more niche features like that, but also as a way to make even standard methods like .fetch() and .merge() a little more modular for core development.
I've noticed a bit of a pattern emerging in both the model and client methods where either the request or schemata instance must be injected as dependencies to return the final method, as below.
Lines 14 to 28 in 0d54425
| const sendEntity = (entityName, request) => (bundle, entity) => { | |
| const data = JSON.stringify({ data: entity }); | |
| const postURL = `/api/${entityName}/${bundle}`; | |
| const postOptions = { method: 'POST', data }; | |
| if (!entity.id) return request(postURL, postOptions); | |
| // We assume if an entity has an id it is a PATCH request, but that may not be | |
| // the case if it has a client-generated id. Such a PATCH request will result | |
| // in a 404 (NOT FOUND), since the endpoint includes the id. We handle this | |
| // error with a POST request, but otherwise return a rejected promise. | |
| const patchURL = `/api/${entityName}/${bundle}/${entity.id}`; | |
| const patchOptions = { method: 'PATCH', data }; | |
| const is404 = error => error.response && error.response.status === 404; | |
| return request(patchURL, patchOptions) | |
| .catch(e => (is404(e) ? request(postURL, postOptions) : Promise.reject(e))); | |
| }; |
Lines 20 to 60 in 0d54425
| const updateEntity = (schemata) => (entity, props) => { | |
| const { id } = entity; | |
| const { entity: entName, bundle, type } = parseTypeFromFields(entity); | |
| if (!validate(id)) { throw new Error(`Invalid ${entName} id: ${id}`); } | |
| const schema = schemata[entName] && schemata[entName][bundle]; | |
| if (!schema) { throw new Error(`Cannot find a schema for the ${entName} type: ${type}.`); } | |
| const now = new Date().toISOString(); | |
| const entityCopy = clone(entity); | |
| const propsCopy = clone(props); | |
| const { meta = {} } = entityCopy; | |
| let { changed = now } = meta; | |
| const { fieldChanges = {}, conflicts = [] } = meta; | |
| const updateFields = (fieldType) => { | |
| const fields = { ...entityCopy[fieldType] }; | |
| listProperties(schema, fieldType).forEach((name) => { | |
| if (name in propsCopy) { | |
| fields[name] = propsCopy[name]; | |
| fieldChanges[name] = now; | |
| changed = now; | |
| } | |
| }); | |
| return fields; | |
| }; | |
| const attributes = updateFields('attributes'); | |
| const relationships = updateFields('relationships'); | |
| return { | |
| id, | |
| type, | |
| attributes, | |
| relationships, | |
| meta: { | |
| ...meta, | |
| changed, | |
| fieldChanges, | |
| conflicts, | |
| }, | |
| }; | |
| }; |
That's OK, but I feel like it could be prudent to try to standardize on some like of .use() method that could be used internally to attach these methods, and then if it works well, eventually exposing it as a part of the API.
There may also be some lessons in the way we parameterized the auth mixin, and hopefully it would eliminate some of the yuckier patterns I've been falling into, like this entityMethods helper:
farmOS.js/src/client/adapter/index.js
Lines 112 to 140 in 0d54425
| ...entityMethods(({ nomenclature: { name, shortName } }) => ({ | |
| ...connection[shortName], | |
| /** @type {(options: import('../fetch.js').FetchOptions) => Promise} */ | |
| fetch: (options) => { | |
| const { filter, limit, sort } = options; | |
| const validTypes = Object.keys(model.schema.get(name)).map(b => `${name}--${b}`); | |
| const bundleRequests = splitFilterByType(filter, validTypes).map((f) => { | |
| const { type, ...tFilter } = f; | |
| /** @type {import('../fetch.js').FetchOptions} */ | |
| const fetchOptions = { ...tFilter, limit, sort }; | |
| const { bundle } = parseEntityType(type); | |
| if (name in fieldTransforms && bundle in fieldTransforms[name]) { | |
| fetchOptions.filterTransforms = fieldTransforms[name][bundle]; | |
| } | |
| const req = connection[shortName].fetch(bundle, fetchOptions); | |
| return chainRequests(req, limit); | |
| }); | |
| const concatBundle = (response, data) => { | |
| const bundle = response.flatMap(path(['data', 'data'])); | |
| return data.concat(bundle); | |
| }; | |
| return altogether(concatBundle, [], bundleRequests) | |
| .then(transformFetchResponse(name)); | |
| }, | |
| send: data => connection[shortName].send( | |
| parseEntityType(data.type).bundle, | |
| transformLocalEntity(data, fieldTransforms), | |
| ).then(transformSendResponse(name)), | |
| }), entities), |
None of this is necessarily required for the 2.0.0 general release, but it could be nice to establish this prior to that, so I'm including it in that milestone for consideration.
Activity