Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
28017db
Revert to `${entity}--${bundle}` type format.
jgaehring Sep 1, 2022
9d8b58a
Fix type format in schema methods.
jgaehring Sep 3, 2022
f500e9d
Remove remaining uses of typeToBundle.
jgaehring Sep 3, 2022
d0ba0fc
Move type parsing functions to utils.
jgaehring Sep 3, 2022
614473e
Fix how fetchSchema handles its parameters.
jgaehring Oct 17, 2022
1f1fe0c
Fix type/bundle issues in tests.
jgaehring Oct 17, 2022
62daf3b
Update docs to reflect new type format.
jgaehring Oct 17, 2022
cc72f89
Export parseFilter & transform functions.
jgaehring May 24, 2022
304331b
Write a proof-of-concept test for subrequests.
jgaehring May 24, 2022
4505ce4
Send with subrequest.
jgaehring Jul 26, 2022
83f88f5
Refactor subrequests.
jgaehring Sep 1, 2022
ff11be6
Return parse & send methods from useSubrequests.
jgaehring Sep 3, 2022
838d7a0
Rename subrequests.js to subrequest.js.
jgaehring Sep 19, 2022
b231044
Add test for subrequest.js.
jgaehring Sep 19, 2022
3c486a6
Fix func sig in calls to transformLocalEntity.
jgaehring Sep 6, 2022
7f865a6
Use parseEntityType from utils.
jgaehring Sep 6, 2022
0d7db2f
Differentiate commands from actions.
jgaehring Sep 6, 2022
6429cab
Add $sort option to fetch params.
jgaehring Sep 6, 2022
61f7771
Use Array.prototype.filter in place of ramda.
jgaehring Sep 6, 2022
9528d94
Write $find subrequest command.
jgaehring Sep 7, 2022
ea13c2a
Override ESLint for recursion & console warnings.
jgaehring Sep 7, 2022
117ea90
Tidy up $find/$createIfNotFound command.
jgaehring Sep 8, 2022
041bf96
Remove BASE_URI from subrequests endpoint.
jgaehring Sep 8, 2022
085ba1d
Reformat $ROOT prefix.
jgaehring Sep 8, 2022
02d1b29
Transform all boolean filters to numeric values.
jgaehring Sep 15, 2022
3ecc076
Replace parseBundles with splitFilterByType.
jgaehring Sep 15, 2022
3121787
Move type parsing functions to separate file.
jgaehring Sep 19, 2022
1d3479a
Filter out field deps w/ missing data props.
jgaehring Sep 29, 2022
8577c72
Sort & resolve blueprints sequentially.
jgaehring Sep 29, 2022
c2fca79
Replace '/' with '::' as path separators.
jgaehring Sep 29, 2022
e71367c
Preserve all response data with subresponses.
jgaehring Sep 30, 2022
d4f6b91
Refactor & close over chainSubrequest helpers.
jgaehring Sep 30, 2022
c905687
Unnest body.data when mapping subrequest deps.
jgaehring Oct 11, 2022
6cf1bf1
Clarify some comments on subrequests.
jgaehring Oct 15, 2022
755e7f8
Fix missing body prop in createIfNotFound.
jgaehring Oct 15, 2022
a72e7ab
Store $find request ids before blueprint resolves.
jgaehring Oct 15, 2022
c00ac10
Fix missing relationship request id in waitFor.
jgaehring Oct 15, 2022
f99389a
Only resolve direct dependencies in subrequests.
jgaehring Oct 15, 2022
4bd4f24
Prune unmet deps from subrequest blueprints.
jgaehring Oct 15, 2022
427543b
Replace subrequest tests.
jgaehring Oct 17, 2022
e9052c4
Remove $ROOT subrequest unless data is provided.
jgaehring Oct 17, 2022
222c5c2
Add subrequests to docs.
jgaehring Oct 17, 2022
4dad548
Add a cleanup stage to subrequest tests.
jgaehring Oct 19, 2022
74dc944
Test subrequests against a fresh farmOS instance.
jgaehring Oct 19, 2022
19d28a8
Move subrequest cleanup to afterAll() hook.
jgaehring Oct 24, 2022
d276ee5
Fix sorting algorithm for subrequest dependencies.
jgaehring Oct 24, 2022
15ba551
Fix JSONPath for one-to-one relationships.
jgaehring Oct 25, 2022
3264356
Add target_revision_id to relationships request.
jgaehring Oct 25, 2022
af33982
Parse JSONPath token based on command data types.
jgaehring Oct 25, 2022
2011683
Skip subrequests tests for now.
jgaehring Oct 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"arrow-parens": "off",
"no-underscore-dangle": "off",
"import/extensions": "off",
"no-warning-comments": "warn"
"no-warning-comments": "warn",
"no-use-before-define": ["error", { "functions": false }],
"no-console": ["warn", { "allow": ["warn"] }]
},
"ignorePatterns": ["dist/**"],
"overrides": [
Expand Down
10 changes: 5 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ Create a farm model for generating and manipulating farmOS data structures.


* [model](#model) ⇒ [<code>FarmModel</code>](#FarmModel)
* [~getSchemata([entity], [type])](#model..getSchemata) ⇒ [<code>EntitySchemata</code>](#EntitySchemata) \| [<code>BundleSchemata</code>](#BundleSchemata) \| [<code>JsonSchemaDereferenced</code>](#JsonSchemaDereferenced)
* [~getSchemata([entity], [bundle])](#model..getSchemata) ⇒ [<code>EntitySchemata</code>](#EntitySchemata) \| [<code>BundleSchemata</code>](#BundleSchemata) \| [<code>JsonSchemaDereferenced</code>](#JsonSchemaDereferenced)
* [~setSchemata(...args)](#model..setSchemata)

<a name="model..getSchemata"></a>

### model~getSchemata([entity], [type]) ⇒ [<code>EntitySchemata</code>](#EntitySchemata) \| [<code>BundleSchemata</code>](#BundleSchemata) \| [<code>JsonSchemaDereferenced</code>](#JsonSchemaDereferenced)
### model~getSchemata([entity], [bundle]) ⇒ [<code>EntitySchemata</code>](#EntitySchemata) \| [<code>BundleSchemata</code>](#BundleSchemata) \| [<code>JsonSchemaDereferenced</code>](#JsonSchemaDereferenced)
Retrieve all schemata that have been previously set, or the schemata of a
particular entity, or one bundle's schema, if specified.

Expand All @@ -249,13 +249,13 @@ particular entity, or one bundle's schema, if specified.
| Param | Type | Description |
| --- | --- | --- |
| [entity] | <code>String</code> | The name of a farmOS entity (eg, 'asset', 'log', etc). |
| [type] | <code>String</code> | The entity's type (aka, bundle). |
| [bundle] | <code>String</code> | The entity's bundle (eg, 'activity' for type 'log--activity'). |

<a name="model..setSchemata"></a>

### model~setSchemata(...args)
Load all schemata, the schemata of a particular entity, or one bundle's
schema, if spcified.
schema, if specified.

**Kind**: inner method of [<code>model</code>](#model)
**Void**:
Expand Down Expand Up @@ -365,7 +365,7 @@ Create a farm client for interacting with farmOS servers.
| Name | Type | Description |
| --- | --- | --- |
| id | <code>String</code> | A v4 UUID as specified by RFC 4122. |
| type | <code>String</code> | Corresponding to the entity bundle (eg, 'activity'). |
| type | <code>String</code> | The combined form of entity & bundle (eg, 'log--activity'). |
| attributes | <code>Object</code> | Values directly attributable to this entity. |
| relationships | <code>Object.&lt;String, (EntityReference\|Array.&lt;EntityReference&gt;)&gt;</code> | References to other entities that define a one-to-one or one-to-many relationship. |
| meta | <code>Object</code> | Non-domain information associated with the creation, modification, storage and transmission of the entity. |
Expand Down
20 changes: 10 additions & 10 deletions docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ So for example, to request completed activity logs, you could use the following

```js
const filter = {
type: { $eq: 'activity' },
type: { $eq: 'log--activity' },
status: { $eq: 'done' },
};
const request = farm.log.fetch({ filter });
Expand All @@ -86,9 +86,9 @@ Generally speaking, it's a good idea to include the `type` in filter queries whe
const filter = {
type: {
$or: [
{ $eq: 'activity' },
{ $eq: 'harvest' },
{ $eq: 'input' },
{ $eq: 'log--activity' },
{ $eq: 'log--harvest' },
{ $eq: 'log--input' },
],
},
status: { $eq: 'done' },
Expand All @@ -100,9 +100,9 @@ This can be abbreviated somewhat by omitting the `$or` operator, and simply assi
```js
const filter = {
type: [
{ $eq: 'activity' },
{ $eq: 'harvest' },
{ $eq: 'input' },
{ $eq: 'log--activity' },
{ $eq: 'log--harvest' },
{ $eq: 'log--input' },
],
status: { $eq: 'done' },
};
Expand All @@ -112,7 +112,7 @@ And of course, a even more common shorthand is to omit the `$eq` operators, mere

```js
const filter = {
type: ['activity', 'harvest', 'input'],
type: ['log--activity', 'log--harvest', 'log--input'],
status: 'done',
};
```
Expand All @@ -122,7 +122,7 @@ As you can see, this works even for the elements of the array, which have been r
```js
const filter = {
$and: [
{ type: ['activity', 'harvest', 'input'] },
{ type: ['log--activity', 'log--harvest', 'log--input'] },
{ status: 'done' },
],
};
Expand Down Expand Up @@ -167,7 +167,7 @@ It's also important to note that this limit will apply separately for each entit
```js
const filter = {
$and: [
{ type: ['activity', 'harvest', 'input'] },
{ type: ['log--activity', 'log--harvest', 'log--input'] },
{ status: 'done' },
],
};
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ farm.schema.fetch()
### 5. CRUD operations with a farmOS log

```js
const initProps = { type: 'activity', name: 'did some stuff' };
const initProps = { type: 'log--activity', name: 'did some stuff' };
const activity = farm.log.create(initProps);
const { id } = activity; // `id` is a v4 UUID
farm.log.send(activity)
.then(() => farm.log.fetch({ filter: { type: 'activity', id } }))
.then(() => farm.log.fetch({ filter: { type: 'log--activity', id } }))
.then(({ data: [remoteActivity] }) => {
const updateProps = { name: 'did some more stuff' };
const updatedActivity = farm.log.update(activity, updateProps);
Expand Down
5 changes: 5 additions & 0 deletions docs/remotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ farm.remote.info().then((res) => { /** etc */ });
```

For farmOS based on Drupal 9 and JSON:API, this is essentially a shorthand for requesting the `/api` endpoint.

## Subrequests ⚠️ __WARNING: EXPERIMENTAL!__ ⚠️
To reduce the number of roundtrip requests, the [Drupal `subrequests` module](https://www.drupal.org/project/subrequests) is included in most farmOS instances. A special syntax is employed in farmOS.js, however, which allows for more concise and intuitive descriptions of the subrequest dependency graph.

Because this is still experimental, it should not be used in production, as there are known issues with its implementation. Until it is considered stable, refer to the [`test/subrequest.js`](https://github.com/farmOS/farmOS.js/blob/main/test/subrequest.js) for the most up-to-date examples on its usage.
14 changes: 7 additions & 7 deletions docs/schemata.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Schemata

## Schema publication enables interoperability
In order to generate and modify farmOS data structures, your farm instance must have the corresponding schema for each entity type (aka, the "bundle") that you wish to work with. For instance, if you wish to create an activity log and send it to a server, you will need the schema for logs with the type `'activity'`.
In order to generate and modify farmOS data structures, your farm instance must have the corresponding schema for each entity type (aka, the "bundle") that you wish to work with. For instance, if you wish to create an activity log and send it to a server, you will need the schema for logs with the type `'log--activity'`.

These schemata are formatted using the [JSON Schema](https://json-schema.org) specification.

Expand All @@ -17,7 +17,7 @@ farm.schema.set('log', 'activity', activityLogJSONSchema);
Once you have done so, you can create an activity log, which will be given a UUID and valid defaults:

```js
const activity = farm.log.create({ type: 'activity' });
const activity = farm.log.create({ type: 'log--activity' });
console.log(activity.timestamp) // => '2021-11-16T21:54:54.888Z'
```

Expand Down Expand Up @@ -50,22 +50,22 @@ One caveat to be aware of, however, is that calls to `farm.schema.set` will over
```js
const activityLogJSONSchema = { /** JSON Schema */ };
farm.schema.set({ log: { activity: activityLogJSONSchema } });
const harvest = farm.log.create({ type: 'harvest' }); // THROWS!
const equipment = farm.asset.create({ type: 'equipment' }); // THROWS!
const harvest = farm.log.create({ type: 'log--harvest' }); // THROWS!
const equipment = farm.asset.create({ type: 'asset--equipment' }); // THROWS!
```

Those calls to create a new harvest log and equipment asset will throw, because the schemata for them are no longer set. To avoid that scenario, it would have been better to call:

```js
farm.schema.set('log', 'activity', activityLogJSONSchema);
const harvest = farm.log.create({ type: 'harvest' }); // works
const equipment = farm.asset.create({ type: 'equipment' }); // works
const harvest = farm.log.create({ type: 'log--harvest' }); // works
const equipment = farm.asset.create({ type: 'log--equipment' }); // works
```

In this last example, only the activity log schema would have been overwritten, preserving the original schemata for harvest logs and equipment assets.

### Instantiate your farm with the `schemata` option
If schemata are available to your application when you instantiate `farmOS`, you can also provide a `schemata` option to the constructor. This option should have as its value an object, containing each entity as a key (eg, `'log'`), whose value is an object containing each entity type as a key (eg, `'activity'`), whose value is the corresponding schema for that entity type.
If schemata are available to your application when you instantiate `farmOS`, you can also provide a `schemata` option to the constructor. This option should have as its value an object, containing each entity as a key (eg, `'log'`), whose value is an object containing each entity type as a key (eg, `'log--activity'`), whose value is the corresponding schema for that entity type.

```js
const myFarm = farmOS({
Expand Down
79 changes: 18 additions & 61 deletions src/client/adapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import chain from 'ramda/src/chain.js';
import compose from 'ramda/src/compose.js';
import concat from 'ramda/src/concat.js';
import evolve from 'ramda/src/evolve.js';
import mapObjIndexed from 'ramda/src/mapObjIndexed.js';
import map from 'ramda/src/map.js';
import path from 'ramda/src/path.js';
import reduce from 'ramda/src/reduce.js';
import client from '../index.js';
import defaultEntities, { entityMethods } from '../../entities.js';
import { parseEntityType, splitFilterByType } from '../../types.js';
import {
generateFieldTransforms, transformD9Schema, transformLocalEntity,
transformFetchResponse, transformSendResponse,
Expand All @@ -22,44 +22,6 @@ import {

const DRUPAL_PAGE_LIMIT = 50;

export function parseBundles(filter, validTypes) {
const bundles = [];
// The filter must either be an object (logical $and) or an array (logical $or).
if (Array.isArray(filter) || Array.isArray(filter.$or)) {
(Array.isArray(filter) ? filter : filter.$or).forEach((f) => {
parseBundles(f).forEach(({ name, filter: bundleFilter }) => {
const i = bundles.findIndex(b => b.name === name);
if (i > -1) {
// Concat on an empty array to flatten either bundle or both.
bundles[i].filter = [].concat(bundles[i].filter, bundleFilter);
} else {
bundles.push({ name, filter: bundleFilter });
}
});
});
return bundles;
}
if (typeof filter !== 'object') return bundles;
const { type, ...rest } = typeof filter.$and === 'object' ? filter.$and : filter;
if (typeof type === 'string') {
if (!validTypes.includes(type)) return bundles;
bundles.push({ name: type, filter: rest });
}
if (Array.isArray(type)) {
type.forEach((t) => {
if (validTypes.includes(t)) {
bundles.push({ name: t, filter: rest });
}
});
}
if ([undefined, null].includes(type)) {
validTypes.forEach((t) => {
bundles.push({ name: t, filter: rest });
});
}
return bundles;
}

const aggregateBundles = reduce((aggregate, result) => {
const { reason, value, status } = result;
if (status === 'fulfilled') {
Expand Down Expand Up @@ -112,35 +74,30 @@ export default function adapter(model, opts) {
return {
...connection,
schema: {
fetch(entName, type) {
return connection.schema.fetch(entName, type)
fetch(entName, bundle) {
return connection.schema.fetch(entName, bundle)
.then((schemata) => {
if (!entName) {
return mapObjIndexed(
(entitySchemata, entityName) => map(
transformD9Schema(entityName),
entitySchemata,
),
schemata,
);
return map((s) => map(transformD9Schema, s), schemata);
}
if (!type) {
return map(transformD9Schema(entName), schemata);
if (!bundle) {
return map(transformD9Schema, schemata);
}
return transformD9Schema(entName)(schemata);
return transformD9Schema(schemata);
});
},
},
...entityMethods(({ nomenclature: { name, shortName } }) => ({
...connection[shortName],
fetch: ({ filter, limit }) => {
const validTypes = Object.keys(model.schema.get(name));
const bundles = parseBundles(filter, validTypes);
const bundleRequests = bundles.map(({ name: bundle, filter: bundleFilter }) => {
const fetchOptions = {
filter: bundleFilter,
limit,
};
/** @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];
}
Expand All @@ -155,8 +112,8 @@ export default function adapter(model, opts) {
.then(handleBundleResponse);
},
send: data => connection[shortName].send(
data.type,
transformLocalEntity(name, data, fieldTransforms),
parseEntityType(data.type).bundle,
transformLocalEntity(data, fieldTransforms),
).then(transformSendResponse(name)),
}), entities),
};
Expand Down
31 changes: 16 additions & 15 deletions src/client/adapter/transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import pick from 'ramda/src/pick.js';
import prop from 'ramda/src/prop.js';
import path from 'ramda/src/path.js';
import replace from 'ramda/src/replace.js';
import typeToBundle from '../type-to-bundle.js';
import { getPath } from '../../json-schema/index.js';
import { parseEntityType } from '../../types.js';
import { isObject } from '../../utils.js';

const dropMilliseconds = replace(/\.\d\d\d/, '');
Expand Down Expand Up @@ -118,18 +118,18 @@ const transformResourceSchema = (subschema) => {
return { type, title, items: resourceSchema };
};

export const transformD9Schema = entName => (d9Schema) => {
export const transformD9Schema = (d9Schema) => {
const {
$id, $schema, title, definitions: { type, attributes, relationships },
} = d9Schema;
const bundle = typeToBundle(entName, type.const);
const defaultTitle = `${bundle} ${entName}`;
const { entity, bundle } = parseEntityType(type.const);
const defaultTitle = `${bundle} ${entity}`;

const transformAttributeFields = evolve({
properties: mapObjIndexed((subschema, propName) => {
const hasOwnDefault = isObject(subschema) && 'default' in subschema;
if (hasOwnDefault) return subschema;
const standardDefault = path([entName, propName], attributeDefaults);
const standardDefault = path([entity, propName], attributeDefaults);
if (!standardDefault) return subschema;
return { ...subschema, default: standardDefault };
}),
Expand All @@ -143,7 +143,7 @@ export const transformD9Schema = entName => (d9Schema) => {
);
const transformRelationshipsSchema = compose(
transformRelationshipFields,
removeDrupalMetaSchemas(drupalMetaFields.relationships.concat(`${entName}_type`)),
removeDrupalMetaSchemas(drupalMetaFields.relationships.concat(`${entity}_type`)),
);

return {
Expand All @@ -153,7 +153,7 @@ export const transformD9Schema = entName => (d9Schema) => {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
type: { const: bundle },
type,
meta: { type: 'object' },
attributes: transformAttributesSchema(attributes),
relationships: transformRelationshipsSchema(relationships),
Expand All @@ -167,27 +167,28 @@ export const transformD9Schema = entName => (d9Schema) => {
* @typedef {Object<String, EntityTransforms>} FieldTransforms
*/
/**
* @param {String} ent Entity name
* @param {String} type Bundle name
* @param {String} type Entity type (eg, 'log--activity')
* @param {FieldTransforms} fns Collection of tranform functions
* @returns {Object<string, object>}
*/
const safeTransforms = (ent, type, fns = { [ent]: { [type]: {} } }) => fns[ent][type];
const safeTransforms = (type, fns = {}) => {
const { entity, bundle } = parseEntityType(type);
const transforms = fns[entity] && fns[entity][bundle];
return transforms || {};
};

/**
* For transforming local entities into acceptable format for D9 JSON:API farmOS.
* @param {String} entName Entity name
* @param {import('../../entities').Entity} data The actual data of such an entity
* @param {FieldTransforms} transforms Collection of transforms
* @returns {Object}
*/
export const transformLocalEntity = (entName, data, transforms) => compose(
export const transformLocalEntity = (data, transforms) => compose(
dissoc('meta'),
evolve({
type: t => `${entName}--${t}`,
attributes: {
timestamp: dropMilliseconds,
...safeTransforms(entName, data.type, transforms),
...safeTransforms(data.type, transforms),
},
relationships: map(r => ({ data: r })),
}),
Expand All @@ -211,7 +212,7 @@ const transformRemoteEntity = (entName, setLastSync = false) => ({
id, type, attributes = emptyAttrs, relationships = {},
}) => ({
id,
type: typeToBundle(entName, type),
type,
meta: {
created: safeIso(attributes.created),
changed: safeIso(attributes.changed),
Expand Down
Loading