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

feat(hub-common): add support for searching for Events3 events from h… #1476

Merged
merged 16 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
rweber-esri marked this conversation as resolved.
Show resolved Hide resolved

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";