Skip to content

Implement freetext search in cht-datasource #9586

Open

Description

Spinning this off into its own ticket (from #9544) since it can stand alone.

What feature do you want to improve?
As a part of updating our freetext search implementation, we can take the opportunity to implement the search logic in cht-datasource. This will provide a more clean interface for cht-code code to perform searches of contacts and reports as well as resulting in the creation of new REST endpoints for the same searching.

Describe the improvement you'd like

shared-libs/cht-datasource

qualifier.ts

/**
* Combines multiple qualifiers into a single object.
 * @returns the combined qualifier
 * @throws Error if any of the qualifiers contain intersecting property names
*/
const and = <A, B, C, D>(qualA: A,  qualB: B,  qualC?: C,  qualD?: D): =>  A & B & (C | object) & (D | object);

/**
 * A qualifier that identifies entities based on a freetext search string.
 */
type FreetextQualifier = Readonly<{ freetext: string }>;

/**
 * Builds a qualifier for finding entities by the given freetext string.
 * @param freetext the text to search with
 * @returns the qualifier
 * @throws Error if the search string is not provided or is empty
 */
const byFreetext(freetext: string) => FreetextQualifier;

/**
* Returns `true` if the given qualifier is a {@link FreetextQualifier} otherwise `false`.
* @param qualifier the qualifier to check
* @returns `true` if the given type is a {@link FreetextQualifier}, otherwise `false`.
*/
const isFreetextQualifier = (qualifier: unknown) => qualifier is FreetextQualifier;

contact.ts

  // Move contents of libs/contact.ts into this file
namespace v1 {
  // Contact interface should go in the v1 namespace with the following documentation:
  /**
   * Immutable data about a contact.
   */

  /**
   * Immutable data about a contact, including the full records of the parent lineage.
   */
  interface ContactWithLineage extends Contact {
    readonly parent?: ContactWithLineage | NormalizedParent,
  }

  // New REST api: /api/v1/contact
  /**
   * Returns a function for retrieving a contact from the given data context.
   * @param context the current data context
   * @returns a function for retrieving a contact
   * @throws Error if a data context is not provided
   */
  /**
   * Returns a contact for the given qualifier.
   * @param qualifier identifier for the contact to retrieve
   * @returns the contact or `null` if no contact is found for the qualifier
   * @throws Error if the qualifier is invalid
   */
	const get = (context: DataContext) => (qualifier: UuidQualifier) => Promise<Nullable<Contact>>

  /**
   * Returns a function for retrieving a contact from the given data context with the contact's parent lineage.
   * @param context the current data context
   * @returns a function for retrieving a contact with the contact's parent lineage
   * @throws Error if a data context is not provided
   */
  /**
   * Returns a contact for the given qualifier with the contact's parent lineage.
   * @param qualifier identifier for the contact to retrieve
   * @returns the contact or `null` if no contact is found for the qualifier
   * @throws Error if the qualifier is invalid
   */
	const getWithLineage = (context: DataContext) => (qualifier: UuidQualifier) => Promise<Nullable<ContactWithLineage>>

	// New REST api: /api/v1/contact/id
  /**
   * Returns a function for retrieving a paged array of contact identifiers from the given data context.
   * @param context the current data context
   * @returns a function for retrieving a paged array of contact identifiers
   * @throws Error if a data context is not provided
   * @see {@link getIdsAll} which provides the same data, but without having to manually account for paging
   */
  /**
   * Returns an array of contact identifiers for the provided page specifications.
   * @param qualifier the limiter defining which identifiers to return
   * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be
   * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page.
   * @param limit the maximum number of identifiers to return. Default is 10000.
   * @returns a page of contact identifiers for the provided specification
   * @throws Error if no qualifier is provided or if the qualifier is invalid
   * @throws Error if the provided `limit` value is `<=0`
   * @throws Error if the provided cursor is not a valid page token or `null`
   */
    const getIdsPage = (context: DataContext) => (qualifier: ContactTypeQualifier | FreetextQualifier, cursor: Nullable<string>, limit: number) => Promise<Page<string>>

  /**
   * Returns a function for getting a generator that fetches contact identifiers from the given data context.
   * @param context the current data context
   * @returns a function for getting a generator that fetches contact identifiers
   * @throws Error if a data context is not provided
   */
  /**
   * Returns a generator for fetching all contact identifiers that match the given qualifier
   * @param qualifier the limiter defining which identifiers to return
   * @returns a generator for fetching all contact identifiers that match the given qualifier
   * @throws Error if no qualifier is provided or if the qualifier is invalid
   */
  const getIdsAll = (context: DataContext) => (qualifier: ContactTypeQualifier | FreetextQualifier) => AsyncGenerator<string, null>
}

report.ts

namespace v1 {
  /**
   * A report document.
   */
  interface Report extends Doc {
    readonly form: string;
    readonly reported_date: Date;
    readonly fields: DataObject;
  }

  // New REST api: /api/v1/report
  /**
   * Returns a function for retrieving a report from the given data context.
   * @param context the current data context
   * @returns a function for retrieving a report
   * @throws Error if a data context is not provided
   */
  /**
   * Returns a report for the given qualifier.
   * @param qualifier identifier for the report to retrieve
   * @returns the report or `null` if no report is found for the qualifier
   * @throws Error if the qualifier is invalid
   */
	const get = (context: DataContext) => (qualifier: UuidQualifier) => Promise<Nullable<Report>>

	// New REST api: /api/v1/report/id
  /**
   * Returns a function for retrieving a paged array of report identifiers from the given data context.
   * @param context the current data context
   * @returns a function for retrieving a paged array of report identifiers
   * @throws Error if a data context is not provided
   * @see {@link getIdsAll} which provides the same data, but without having to manually account for paging
   */
  /**
   * Returns an array of report identifiers for the provided page specifications.
   * @param qualifier the limiter defining which identifiers to return
   * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be
   * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page.
   * @param limit the maximum number of identifiers to return. Default is 10000.
   * @returns a page of report identifiers for the provided specification
   * @throws Error if no qualifier is provided or if the qualifier is invalid
   * @throws Error if the provided `limit` value is `<=0`
   * @throws Error if the provided cursor is not a valid page token or `null`
   */
    const getIdsPage = (context: DataContext) => (qualifier: FreetextQualifier, cursor: Nullable<string>, limit: number) => Promise<Page<string>>

  /**
   * Returns a function for getting a generator that fetches report identifiers from the given data context.
   * @param context the current data context
   * @returns a function for getting a generator that fetches report identifiers
   * @throws Error if a data context is not provided
   */
  /**
   * Returns a generator for fetching all report identifiers that match the given qualifier
   * @param qualifier the limiter defining which identifiers to return
   * @returns a generator for fetching all report identifiers that match the given qualifier
   * @throws Error if no qualifier is provided or if the qualifier is invalid
   */
  const getIdsAll = (context: DataContext) => (qualifier: FreetextQualifier) => AsyncGenerator<string, null>
}

person.ts/place.ts

Update the Person/PlaceWithLineage interfaces to use ContactWithLineage as the parent (instead of PlaceWithLineage). Pending #9584 it is probably not safe to assume that a contact parent will always be a place.

Need to do more research regarding the Place.contact value...

shared-libs/search

Update search logic to call through to the cht-datasource functions when doing freetext searches (or the basic contact_by_type search). For the multi-view searches, we will need to just stream in all pages.

Design details

As mentioned here (#9544 (comment)) the local adapter will need to account for Nouveau logic (though depending on the progress of that implementation, this PR might just port the same online/offline logic from shared-libs/search that was created by #9544).

In general we should strive to align to the patterns/code used for implementing the person/place-by-type flows in cht-datasource. All the paging logic should basically be the same.

Describe alternatives you've considered

Additional context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

Type

No type

Projects

  • Status

    This Week's commitments

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions