Skip to content

Commit

Permalink
feat(hub-common): add support for searching for Events3 events from h… (
Browse files Browse the repository at this point in the history
  • Loading branch information
rweber-esri authored May 2, 2024
1 parent 5d3da09 commit 4afe9a4
Show file tree
Hide file tree
Showing 27 changed files with 1,123 additions and 14 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ export const getHubRelativeUrl = (
"project",
"initiative",
"discussion",
"event",
];
// default to the catchall content route
let path = "/content";
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/content/get-family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export function getFamily(type: string) {
case "discussion":
family = "discussion";
break;
case "event":
family = "event";
break;
case "hub initiative":
family = "initiative";
break;
Expand Down
4 changes: 0 additions & 4 deletions packages/common/src/events/HubEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ export class HubEvent
partialEvent: Partial<IHubEvent>,
context: IArcGISContext
): IHubEvent {
// TODO: Figure out how to approach slugs for Events
// TODO: remove orgUrlKey if either:
// 1. back-end generates the slug at time of create/update
// 2. slug is derived on client from title & ID appears, e.g. `my-event-clu34rsub00003b6thiioms4a`
// ensure we have the orgUrlKey
if (!partialEvent.orgUrlKey) {
partialEvent.orgUrlKey = context.portal.urlKey;
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/events/_internal/PropertyMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
IOnlineMeeting,
} from "../api/orval/api/orval-events";
import { HubEventAttendanceType, HubEventOnlineCapacityType } from "../types";
import { computeLinks } from "./computeLinks";
import { getEventSlug } from "./getEventSlug";

/**
* @private
Expand Down Expand Up @@ -92,6 +94,8 @@ export class EventPropertyMapper extends PropertyMapper<
obj.createdDateSource = "createdAt";
obj.updatedDate = new Date(store.updatedAt);
obj.updatedDateSource = "updatedAt";
obj.links = computeLinks(store as IEvent);
obj.slug = getEventSlug(store as IEvent);

return obj;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/common/src/events/_internal/computeLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IHubEntityLinks } from "../../core/types";
import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl";
import { getHubRelativeUrl } from "../../content/_internal/internalContentUtils";
import { IEvent } from "../api/orval/api/orval-events";
import { getEventSlug } from "./getEventSlug";

/**
* Compute the links that get appended to a Hub Event
* search result and entity
*
* @param item
* @param requestOptions
*/
export function computeLinks(event: IEvent): IHubEntityLinks {
const siteRelative = getHubRelativeUrl("event", getEventSlug(event));
return {
self: siteRelative,
siteRelative,
workspaceRelative: getRelativeWorkspaceUrl("Event", event.id),
// TODO
// thumbnail: "",
};
}
18 changes: 18 additions & 0 deletions packages/common/src/events/_internal/getEventSlug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { slugify } from "../../utils/slugify";
import { IEvent } from "../api/orval/api/orval-events";

/**
* Builds a slug for the given IEvent record.
* @param event An IEvent record
* @returns the slug for the given IEvent record
*/
export function getEventSlug(event: IEvent): string {
return (
[slugify(event.title), event.id]
.join("-")
// remove double hyphens
.split("-")
.filter(Boolean)
.join("-")
);
}
2 changes: 0 additions & 2 deletions packages/common/src/events/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export async function createHubEvent(
// so set endDate to startDate
event.endDate = event.startDate;

// TODO: how to handle slugs
// TODO: how to handle events being discussable vs non-discussable

const mapper = new EventPropertyMapper(getPropertyMap());
Expand Down Expand Up @@ -79,7 +78,6 @@ export async function updateHubEvent(
): Promise<IHubEvent> {
const eventUpdates = { ...buildDefaultEventEntity(), ...event };

// TODO: how to handle slugs
// TODO: how to handle events being discussable vs non-discussable

const mapper = new EventPropertyMapper(getPropertyMap());
Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/events/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export function fetchEvent(
eventId: string,
requestOptions: IHubRequestOptions
): Promise<IHubEvent> {
const spl = eventId.split("-");
const id = spl[spl.length - 1];
return getEvent({
eventId,
eventId: id,
...requestOptions,
})
.then((event) => convertClientEventToHubEvent(event, requestOptions))
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/search/_internal/commonHelpers/getApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expandApi } from "../../utils";
import { shouldUseOgcApi } from "./shouldUseOgcApi";
import { getOgcApiDefinition } from "./getOgcApiDefinition";
import { shouldUseDiscussionsApi } from "./shouldUseDiscussionsApi";
import { shouldUseEventsApi } from "./shouldUseEventsApi";
import { getDiscussionsApiDefinition } from "./getDiscussionsApiDefinition";

/**
Expand Down Expand Up @@ -32,6 +33,11 @@ export function getApi(
result = expandApi(api);
} else if (shouldUseDiscussionsApi(targetEntity, options)) {
result = getDiscussionsApiDefinition();
} else if (shouldUseEventsApi(targetEntity, options)) {
// Currently, url is null because this is handled internally by the
// events request method called by getEvents, which relies on
// the URL defined in the request options.hubApiUrl
result = { type: "arcgis-hub", url: null };
} else if (shouldUseOgcApi(targetEntity, options)) {
result = getOgcApiDefinition(targetEntity, options);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { EntityType } from "../../types/IHubCatalog";
import { IHubSearchOptions } from "../../types/IHubSearchOptions";

/**
* @private
* Determines if the Events API can be targeted with the given
* search parameters
* @param targetEntity
* @param options
* @returns boolean
*/
export function shouldUseEventsApi(
targetEntity: EntityType,
options: IHubSearchOptions
): boolean {
const {
requestOptions: { isPortal },
} = options;
return targetEntity === "event" && !isPortal;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getUser } from "@esri/arcgis-rest-portal";
import { IHubSearchOptions } from "../../types/IHubSearchOptions";
import { IHubSearchResult } from "../../types/IHubSearchResult";
import { IEvent } from "../../../events/api/orval/api/orval-events";
import { AccessLevel } from "../../../core/types/types";
import { HubFamily } from "../../../types";
import { computeLinks } from "../../../events/_internal/computeLinks";

/**
* Resolves an IHubSearchResult for the given IEvent record
* @param event An IEvent record
* @param options An IHubSearchOptions object
* @returns a IHubSearchResult for the given IEvent record
*/
export async function eventToSearchResult(
event: IEvent,
options: IHubSearchOptions
): Promise<IHubSearchResult> {
const ownerUser = await getUser({
username: event.creator.username,
...options.requestOptions,
});
const result = {
access: event.access.toLowerCase() as AccessLevel,
id: event.id,
type: "Event",
name: event.title,
owner: event.creator.username,
ownerUser,
summary: event.summary || event.description,
createdDate: new Date(event.createdAt),
createdDateSource: "event.createdAt",
updatedDate: new Date(event.updatedAt),
updatedDateSource: "event.updatedAt",
family: "event" as HubFamily,
links: computeLinks(event),
tags: event.tags,
categories: event.categories,
rawResult: event,
};
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { IFilter, IPredicate } from "../../types/IHubCatalog";
import {
EventStatus,
GetEventsParams,
} from "../../../events/api/orval/api/orval-events";
import { unique } from "../../../util";

const getPredicateValuesByKey = (
filters: IFilter[],
predicateKey: string
): any[] => {
const toPredicateValuesByKey = (a1: any[], filter: IFilter): any[] =>
filter.predicates.reduce<any[]>(
(a2, predicate) =>
Object.entries(predicate).reduce(
(a3, [key, val]) => (key === predicateKey ? [...a3, val] : a3),
a2
),
a1
);
return filters.reduce(toPredicateValuesByKey, []);
};

const getOptionalPredicateStringsByKey = (
filters: IFilter[],
predicateKey: string
): string => {
const predicateValues = getPredicateValuesByKey(filters, predicateKey);
const str = predicateValues.filter(unique).join(",");
if (str) {
return str;
}
};

/**
* Builds a Partial<GetEventsParams> given an Array of IFilter objects
* @param filters An Array of IFilter
* @returns a Partial<GetEventsParams> for the given Array of IFilter objects
*/
export function processFilters(filters: IFilter[]): Partial<GetEventsParams> {
const processedFilters: Partial<GetEventsParams> = {};
const access = getOptionalPredicateStringsByKey(filters, "access");
if (access?.length) {
// TODO: remove ts-ignore once GetEventsParams supports filtering by access
// @ts-ignore
processedFilters.access = access;
}
const term = getPredicateValuesByKey(filters, "term");
if (term.length) {
processedFilters.title = term[0];
}
const categories = getOptionalPredicateStringsByKey(filters, "categories");
if (categories?.length) {
processedFilters.categories = categories;
}
const tags = getOptionalPredicateStringsByKey(filters, "tags");
if (tags?.length) {
processedFilters.tags = tags;
}
const attendanceType = getOptionalPredicateStringsByKey(
filters,
"attendanceType"
);
if (attendanceType?.length) {
processedFilters.attendanceTypes = attendanceType;
}
const status = getOptionalPredicateStringsByKey(filters, "status");
processedFilters.status = status?.length
? status
: [EventStatus.PLANNED, EventStatus.CANCELED]
.map((val) => val.toLowerCase())
.join(",");
const startDateRange = getPredicateValuesByKey(filters, "startDateRange");
if (startDateRange.length) {
processedFilters.startDateTimeBefore = new Date(
startDateRange[0].to
).toISOString();
processedFilters.startDateTimeAfter = new Date(
startDateRange[0].from
).toISOString();
}
return processedFilters;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IHubSearchOptions } from "../../types/IHubSearchOptions";
import {
EventSort,
GetEventsParams,
SortOrder,
} from "../../../events/api/orval/api/orval-events";

/**
* Builds a Partial<GetEventsParams> for the given IHubSearchOptions
* @param options An IHubSearchOptions object
* @returns a Partial<GetEventsParams> for the given IHubSearchOptions
*/
export function processOptions(
options: IHubSearchOptions
): Partial<GetEventsParams> {
const processedOptions: Partial<GetEventsParams> = {};
if (options.num > 0) {
processedOptions.num = options.num.toString();
}
if (options.start > 1) {
processedOptions.start = options.start.toString();
}
if (options.sortField === "modified") {
processedOptions.sortBy = EventSort.updatedAt;
} else if (options.sortField === "created") {
processedOptions.sortBy = EventSort.createdAt;
} else if (options.sortField === "title") {
processedOptions.sortBy = EventSort.title;
}
processedOptions.sortOrder =
options.sortOrder === "desc" ? SortOrder.desc : SortOrder.asc;
return processedOptions;
}
50 changes: 50 additions & 0 deletions packages/common/src/search/_internal/hubSearchEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { IQuery } from "../types/IHubCatalog";
import { IHubSearchOptions } from "../types/IHubSearchOptions";
import { IHubSearchResponse } from "../types/IHubSearchResponse";
import { IHubSearchResult } from "../types/IHubSearchResult";
import { getEvents } from "../../events/api/events";
import { GetEventsParams } from "../../events/api/orval/api/orval-events";
import { eventToSearchResult } from "./hubEventsHelpers/eventToSearchResult";
import { processOptions } from "./hubEventsHelpers/processOptions";
import { processFilters } from "./hubEventsHelpers/processFilters";

/**
* Searches for events against the Events 3 API using the given `query` and `options`
* @param query An IQuery object
* @param options An IHubSearchOptions object
* @returns a promise that resolves a <IHubSearchResponse<IHubSearchResult> object
*/
export async function hubSearchEvents(
query: IQuery,
options: IHubSearchOptions
): Promise<IHubSearchResponse<IHubSearchResult>> {
const processedFilters = processFilters(query.filters);
const processedOptions = processOptions(options);
const data: GetEventsParams = {
...processedFilters,
...processedOptions,
include: "creator,registrations",
};
const { items, nextStart, total } = await getEvents({
...options.requestOptions,
data,
});
const results = await Promise.all(
items.map((event) => eventToSearchResult(event, options))
);
const hasNext = nextStart > -1;
return {
total,
results,
hasNext,
next: () => {
if (!hasNext) {
throw new Error("No more hub events for the given query and options");
}
return hubSearchEvents(query, {
...options,
start: nextStart,
});
},
};
}
1 change: 1 addition & 0 deletions packages/common/src/search/_internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./hubSearchItems";
export * from "./portalSearchGroups";
export * from "./portalSearchUsers";
export * from "./hubSearchChannels";
export * from "./hubSearchEvents";
Loading

0 comments on commit 4afe9a4

Please sign in to comment.