Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a plugin API to add methods to farm/model/client #73

Open
jgaehring opened this issue Oct 26, 2022 · 0 comments
Open

Add a plugin API to add methods to farm/model/client #73

jgaehring opened this issue Oct 26, 2022 · 0 comments
Milestone

Comments

@jgaehring
Copy link
Member

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.

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

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:

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

@jgaehring jgaehring added this to the 2.0.0 milestone Oct 26, 2022
@jgaehring jgaehring modified the milestones: 2.0.0, 2.0.0-beta.16 Jan 6, 2023
@jgaehring jgaehring modified the milestones: 2.0.0-beta.16, 2.0.0 Feb 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant