Skip to content

Commit

Permalink
feat: add/update associations business logic (#1370)
Browse files Browse the repository at this point in the history
This PR exposes the following set of top-level association utils: requestAssociation, acceptAssociation, breakAssociation, getAssociatedEntitiesQuery, getPendingEntitiesQuery, getRequestingEntitiesQuery, getAvailableToRequestEntitiesQuery, getWellKnownAssociationsCatalog, getAssociationStats
  • Loading branch information
juliannemarik authored Jan 9, 2024
1 parent 1931b30 commit 75abd91
Show file tree
Hide file tree
Showing 76 changed files with 2,687 additions and 336 deletions.
131 changes: 0 additions & 131 deletions packages/common/e2e/associations.e2e.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/common/src/associations/addAssociation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { IWithAssociations } from "../core/traits/IWithAssociations";
import { IAssociationInfo } from "./types";

/**
* ** DEPRECATED: please use requestAssociation instead.
* This will be removed in the next breaking version **
*
* Add an association to an entity
* Persisted into the entity's `.typeKeywords` array
* @param info
Expand Down
70 changes: 70 additions & 0 deletions packages/common/src/associations/breakAssociation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { unshareItemWithGroup } from "@esri/arcgis-rest-portal";
import { IArcGISContext } from "../ArcGISContext";
import { getProp } from "../objects";
import { fetchHubEntity } from "../core";
import { HubEntity, HubEntityType } from "../core/types";
import { getTypeFromEntity } from "../core/getTypeFromEntity";
import { updateHubEntity } from "../core/updateHubEntity";
import { getAssociationHierarchy } from "./internal/getAssociationHierarchy";
import { isAssociationSupported } from "./internal/isAssociationSupported";
import { removeAssociationKeyword } from "./internal/removeAssociationKeyword";

/**
* When an entity decides it wants to "disconnect" itself
* from an existing association, half of the association
* "connection" is broken.
*
* from the parent's perspective: the parent removes
* the child from its association group
*
* From the child's perspective: the child removes
* the parent reference (ref|<parentType>|<parentID>)
* from its typeKeywords
*
* @param entity - entity initiating the disconnection
* @param type - type of the entity the initiating entity wants to disconnect from
* @param id - id of the entity the initiating entity wants to disconnect from
* @param context - contextual portal and auth information
*/
export const breakAssociation = async (
entity: HubEntity,
associationType: HubEntityType,
id: string,
context: IArcGISContext
): Promise<void> => {
const entityType = getTypeFromEntity(entity);
const isSupported = isAssociationSupported(entityType, associationType);

if (!isSupported) {
throw new Error(
`breakAssociation: Association between ${entityType} and ${associationType} is not supported.`
);
}

const associationHierarchy = getAssociationHierarchy(entityType);
const isParent = associationHierarchy.children.includes(associationType);

if (isParent) {
const associationGroupId = getProp(entity, "associations.groupId");
const { owner } = await fetchHubEntity(associationType, id, context);
try {
await unshareItemWithGroup({
id,
groupId: associationGroupId,
authentication: context.session,
owner,
});
} catch (error) {
throw new Error(
`breakAssociation: there was an error unsharing ${id} from ${associationGroupId}: ${error}`
);
}
} else {
entity.typeKeywords = removeAssociationKeyword(
entity.typeKeywords,
associationType,
id
);
await updateHubEntity(entityType, entity, context);
}
};
51 changes: 51 additions & 0 deletions packages/common/src/associations/getAssociatedEntitiesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getTypeFromEntity } from "../core/getTypeFromEntity";
import { IQuery } from "../search/types";
import { HubEntity, HubEntityType } from "../core/types";
import { getAssociationHierarchy } from "./internal/getAssociationHierarchy";
import { isAssociationSupported } from "./internal/isAssociationSupported";
import { getIncludesAndReferencesQuery } from "./internal/getIncludesAndReferencesQuery";
import { IArcGISContext } from "../ArcGISContext";

/**
* Associated entities are those which have mutually
* "agreed" to be connected with one another. They
* require a two-way "connection" between parent/child:
*
* parent: "includes" the child in its association query
* child: "references" the parent via a typeKeyword of
* the form ref|<parentType>|<parentID>
*
* The following returns a query to view an entity's
* associations with another entity type
*
* @param entity - Hub entity
* @param associationType - entity type to query for
* @param context - contextual auth and portal information
* @returns {IQuery}
*/
export const getAssociatedEntitiesQuery = async (
entity: HubEntity,
associationType: HubEntityType,
context: IArcGISContext
): Promise<IQuery> => {
const entityType = getTypeFromEntity(entity);
const isSupported = isAssociationSupported(entityType, associationType);

if (!isSupported) {
throw new Error(
`getAssociatedEntitiesQuery: Association between ${entityType} and ${associationType} is not supported.`
);
}

const associationHierarchy = getAssociationHierarchy(entityType);
const isParent = associationHierarchy.children.includes(associationType);

const query = await getIncludesAndReferencesQuery(
entity,
associationType,
isParent,
context
);

return query;
};
76 changes: 76 additions & 0 deletions packages/common/src/associations/getAssociationStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { IArcGISContext } from "../ArcGISContext";
import { getTypeFromEntity } from "../core/getTypeFromEntity";
import { HubEntity, HubEntityType } from "../core/types";
import { IQuery } from "../search/types";
import { hubSearch } from "../search/hubSearch";
import { getAssociatedEntitiesQuery } from "./getAssociatedEntitiesQuery";
import { getPendingEntitiesQuery } from "./getPendingEntitiesQuery";
import { getRequestingEntitiesQuery } from "./getRequestingEntitiesQuery";
import { getAssociationHierarchy } from "./internal/getAssociationHierarchy";
import { isAssociationSupported } from "./internal/isAssociationSupported";
import { IAssociationStats } from "./types";

/**
* get an entity's association stats - # of associated, pending,
* requesting, included/referenced entities
*
* @param entity - Hub entity
* @param associationType - entity type to query for
* @param context - contextual auth and portal information
* @returns
*/
export const getAssociationStats = async (
entity: HubEntity,
associationType: HubEntityType,
context: IArcGISContext
): Promise<IAssociationStats> => {
let stats: IAssociationStats;
const entityType = getTypeFromEntity(entity);
const isSupported = isAssociationSupported(entityType, associationType);

if (!isSupported) {
throw new Error(
`getAssociationStats: Association between ${entityType} and ${associationType} is not supported.`
);
}

const associationHierarchy = getAssociationHierarchy(entityType);
const isParent = associationHierarchy.children.includes(associationType);

stats = {
associated: 0,
pending: 0,
requesting: 0,
...(isParent ? { included: 0 } : { referenced: 0 }),
};

try {
const queries = await Promise.all([
getAssociatedEntitiesQuery(entity, associationType, context),
getPendingEntitiesQuery(entity, associationType, context),
getRequestingEntitiesQuery(entity, associationType, context),
]);

const [{ total: associated }, { total: pending }, { total: requesting }] =
await Promise.all(
queries.map((query: IQuery) => {
return hubSearch(query, {
requestOptions: context.hubRequestOptions,
});
})
);

stats = {
associated,
pending,
requesting,
...(isParent
? { included: associated + pending }
: { referenced: associated + pending }),
};
} catch (error) {
return stats;
}

return stats;
};
Loading

0 comments on commit 75abd91

Please sign in to comment.