From de475c71dd93eff1c1f3741c57dbf38b16df0c4a Mon Sep 17 00:00:00 2001 From: Dennis Lysenko Date: Mon, 10 Jun 2024 18:45:52 -0400 Subject: [PATCH 1/5] Set default value for Entity type in various classes Entity type T in AbstractSearch, RawSearch, Search, WhereField, SchemaDefinition, Schema, and Repository classes now defaults to Record. This change provides flexibility as it allows the underlying data structure to be a general object where keys are string types and values can be of any type. --- lib/client/client.ts | 4 +- lib/entity/entity.ts | 8 ++- lib/repository/repository.ts | 68 ++++++++++---------- lib/schema/definitions.ts | 4 +- lib/schema/field.ts | 2 +- lib/schema/schema.ts | 33 +++++++--- lib/search/results-converter.ts | 33 ++++------ lib/search/search.ts | 106 +++++++++++++++---------------- lib/search/where-boolean.ts | 19 +++--- lib/search/where-date.ts | 39 ++++++------ lib/search/where-field.ts | 77 +++++++++++----------- lib/search/where-number.ts | 29 +++++---- lib/search/where-point.ts | 8 +-- lib/search/where-string-array.ts | 11 ++-- lib/search/where-string.ts | 25 ++++---- lib/search/where-text.ts | 23 +++---- 16 files changed, 254 insertions(+), 235 deletions(-) diff --git a/lib/client/client.ts b/lib/client/client.ts index 3cdaf443..5b764409 100644 --- a/lib/client/client.ts +++ b/lib/client/client.ts @@ -1,7 +1,7 @@ import { createClient, createCluster, RediSearchSchema, SearchOptions } from 'redis' import { Repository } from '../repository' -import { Schema } from '../schema' +import {InferSchema, Schema} from '../schema' import { RedisOmError } from '../error' /** A conventional Redis connection. */ @@ -116,7 +116,7 @@ export class Client { * @param schema The schema. * @returns A repository for the provided schema. */ - fetchRepository(schema: Schema): Repository { + fetchRepository>(schema: T): Repository> { this.#validateRedisOpen() return new Repository(schema, this) } diff --git a/lib/entity/entity.ts b/lib/entity/entity.ts index 68fef8fb..f742b6e5 100644 --- a/lib/entity/entity.ts +++ b/lib/entity/entity.ts @@ -4,9 +4,7 @@ export const EntityId = Symbol('entityId') /** The Symbol used to access the keyname of an {@link Entity}. */ export const EntityKeyName = Symbol('entityKeyName') -/** Defines the objects returned from calls to {@link Repository | repositories }. */ -export type Entity = EntityData & { - +export type EntityInternal = { /** The unique ID of the {@link Entity}. Access using the {@link EntityId} Symbol. */ [EntityId]?: string @@ -14,6 +12,10 @@ export type Entity = EntityData & { [EntityKeyName]?: string } +/** Defines the objects returned from calls to {@link Repository | repositories }. */ +export type Entity = EntityData & EntityInternal +export type EntityKeys = Exclude + /** The free-form data associated with an {@link Entity}. */ export type EntityData = { [key: string]: EntityDataValue | EntityData | Array diff --git a/lib/repository/repository.ts b/lib/repository/repository.ts index 8f2d7cd9..cc60d14e 100644 --- a/lib/repository/repository.ts +++ b/lib/repository/repository.ts @@ -1,9 +1,9 @@ -import { Client, CreateOptions, RedisConnection, RedisHashData, RedisJsonData } from '../client' -import { Entity, EntityId, EntityKeyName } from '../entity' -import { buildRediSearchSchema } from '../indexer' -import { Schema } from '../schema' -import { Search, RawSearch } from '../search' -import { fromRedisHash, fromRedisJson, toRedisHash, toRedisJson } from '../transformer' +import {Client, CreateOptions, RedisConnection, RedisHashData, RedisJsonData} from '../client' +import {Entity, EntityId, EntityKeyName} from '../entity' +import {buildRediSearchSchema} from '../indexer' +import {Schema} from '../schema' +import {RawSearch, Search} from '../search' +import {fromRedisHash, fromRedisJson, toRedisHash, toRedisJson} from '../transformer' /** * A repository is the main interaction point for reading, writing, and @@ -41,19 +41,19 @@ import { fromRedisHash, fromRedisJson, toRedisHash, toRedisJson } from '../trans * .and('aBoolean').is.false().returnAll() * ``` */ -export class Repository { +export class Repository> { // NOTE: Not using "#" private as the spec needs to check calls on this class. Will be resolved when Client class is removed. - private client: Client - #schema: Schema + private readonly client: Client + readonly #schema: Schema /** * Creates a new {@link Repository}. * * @param schema The schema defining that data in the repository. - * @param client A client to talk to Redis. + * @param clientOrConnection A client to talk to Redis. */ - constructor(schema: Schema, clientOrConnection: Client | RedisConnection) { + constructor(schema: Schema, clientOrConnection: Client | RedisConnection) { this.#schema = schema if (clientOrConnection instanceof Client) { this.client = clientOrConnection @@ -131,7 +131,7 @@ export class Repository { * @param entity The Entity to save. * @returns A copy of the provided Entity with EntityId and EntityKeyName properties added. */ - async save(entity: Entity): Promise + async save(entity: T): Promise /** * Insert or update the {@link Entity} to Redis using the provided entityId. @@ -140,10 +140,10 @@ export class Repository { * @param entity The Entity to save. * @returns A copy of the provided Entity with EntityId and EntityKeyName properties added. */ - async save(id: string, entity: Entity): Promise + async save(id: string, entity: T): Promise - async save(entityOrId: Entity | string, maybeEntity?: Entity): Promise { - let entity: Entity | undefined + async save(entityOrId: T | string, maybeEntity?: T): Promise { + let entity: T | undefined let entityId: string | undefined if (typeof entityOrId !== 'string') { @@ -155,7 +155,7 @@ export class Repository { } const keyName = `${this.#schema.schemaName}:${entityId}` - const clonedEntity = { ...entity, [EntityId]: entityId, [EntityKeyName]: keyName } + const clonedEntity = { ...entity, [EntityId]: entityId, [EntityKeyName]: keyName } as T await this.writeEntity(clonedEntity) return clonedEntity @@ -168,7 +168,7 @@ export class Repository { * @param id The ID of the {@link Entity} you seek. * @returns The matching Entity. */ - async fetch(id: string): Promise + async fetch(id: string): Promise /** * Read and return the {@link Entity | Entities} from Redis with the given IDs. If @@ -177,7 +177,7 @@ export class Repository { * @param ids The IDs of the {@link Entity | Entities} you seek. * @returns The matching Entities. */ - async fetch(...ids: string[]): Promise + async fetch(...ids: string[]): Promise /** * Read and return the {@link Entity | Entities} from Redis with the given IDs. If @@ -186,9 +186,9 @@ export class Repository { * @param ids The IDs of the {@link Entity | Entities} you seek. * @returns The matching Entities. */ - async fetch(ids: string[]): Promise + async fetch(ids: string[]): Promise - async fetch(ids: string | string[]): Promise { + async fetch(ids: string | string[]): Promise { if (arguments.length > 1) return this.readEntities([...arguments]) if (Array.isArray(ids)) return this.readEntities(ids) @@ -246,6 +246,7 @@ export class Repository { * ids. If a particular {@link Entity} is not found, does nothing. * * @param ids The IDs of the {@link Entity | Entities} you wish to delete. + * @param ttlInSeconds The time to live in seconds. */ async expire(ids: string[], ttlInSeconds: number): Promise @@ -298,7 +299,7 @@ export class Repository { * * @returns A {@link Search} object. */ - search(): Search { + search(): Search { return new Search(this.#schema, this.client) } @@ -313,20 +314,19 @@ export class Repository { * @query The raw RediSearch query you want to rune. * @returns A {@link RawSearch} object. */ - searchRaw(query: string): RawSearch { + searchRaw(query: string): RawSearch { return new RawSearch(this.#schema, this.client, query) } - private async writeEntity(entity: Entity): Promise { - return this.#schema.dataStructure === 'HASH' ? this.writeEntityToHash(entity) : this.writeEntityToJson(entity) + private async writeEntity(entity: T): Promise { + return this.#schema.dataStructure === 'HASH' ? this.#writeEntityToHash(entity) : this.writeEntityToJson(entity) } - private async readEntities(ids: string[]): Promise { + private async readEntities(ids: string[]): Promise { return this.#schema.dataStructure === 'HASH' ? this.readEntitiesFromHash(ids) : this.readEntitiesFromJson(ids) } - // TODO: make this actually private... like with # - private async writeEntityToHash(entity: Entity): Promise { + async #writeEntityToHash(entity: Entity): Promise { const keyName = entity[EntityKeyName]! const hashData: RedisHashData = toRedisHash(this.#schema, entity) if (Object.keys(hashData).length === 0) { @@ -336,14 +336,13 @@ export class Repository { } } - private async readEntitiesFromHash(ids: string[]): Promise { + private async readEntitiesFromHash(ids: string[]): Promise { return Promise.all( - ids.map(async (entityId) => { + ids.map(async (entityId): Promise => { const keyName = this.makeKey(entityId) const hashData = await this.client.hgetall(keyName) const entityData = fromRedisHash(this.#schema, hashData) - const entity = { ...entityData, [EntityId]: entityId, [EntityKeyName]: keyName } - return entity + return {...entityData, [EntityId]: entityId, [EntityKeyName]: keyName} as T })) } @@ -353,14 +352,13 @@ export class Repository { await this.client.jsonset(keyName, jsonData) } - private async readEntitiesFromJson(ids: string[]): Promise { + private async readEntitiesFromJson(ids: string[]): Promise { return Promise.all( - ids.map(async (entityId) => { + ids.map(async (entityId): Promise => { const keyName = this.makeKey(entityId) const jsonData = await this.client.jsonget(keyName) ?? {} const entityData = fromRedisJson(this.#schema, jsonData) - const entity = {...entityData, [EntityId]: entityId, [EntityKeyName]: keyName } - return entity + return {...entityData, [EntityId]: entityId, [EntityKeyName]: keyName} as T })) } diff --git a/lib/schema/definitions.ts b/lib/schema/definitions.ts index fabc2701..01e1552c 100644 --- a/lib/schema/definitions.ts +++ b/lib/schema/definitions.ts @@ -1,3 +1,5 @@ +import {Entity, EntityKeys} from "$lib/entity"; + /** Valid field types for a {@link FieldDefinition}. */ export type FieldType = 'boolean' | 'date' | 'number' | 'number[]' | 'point' | 'string' | 'string[]' | 'text' @@ -120,4 +122,4 @@ export type FieldDefinition = TextFieldDefinition /** Group of {@link FieldDefinition}s that define the schema for an {@link Entity}. */ -export type SchemaDefinition = Record +export type SchemaDefinition> = Record, FieldDefinition> diff --git a/lib/schema/field.ts b/lib/schema/field.ts index 5ec8f075..9adf5553 100644 --- a/lib/schema/field.ts +++ b/lib/schema/field.ts @@ -5,7 +5,7 @@ import { AllFieldDefinition, FieldDefinition, FieldType } from './definitions' */ export class Field { - #name: string + readonly #name: string #definition: AllFieldDefinition /** diff --git a/lib/schema/schema.ts b/lib/schema/schema.ts index 041d5fac..39b860d3 100644 --- a/lib/schema/schema.ts +++ b/lib/schema/schema.ts @@ -1,11 +1,11 @@ import { createHash } from 'crypto' import { ulid } from 'ulid' -import { Entity } from "../entity" +import { Entity, EntityKeys } from "../entity" import { IdStrategy, DataStructure, StopWordOptions, SchemaOptions } from './options' -import { SchemaDefinition } from './definitions' +import { FieldDefinition, SchemaDefinition } from './definitions' import { Field } from './field' import { InvalidSchema } from '../error' @@ -16,7 +16,17 @@ import { InvalidSchema } from '../error' * a {@link SchemaDefinition}, and optionally {@link SchemaOptions}: * * ```typescript - * const schema = new Schema('foo', { + * interface Foo extends Entity { + * aString: string, + * aNumber: number, + * aBoolean: boolean, + * someText: string, + * aPoint: Point, + * aDate: Date, + * someStrings: string[], + * } + * + * const schema = new Schema('foo', { * aString: { type: 'string' }, * aNumber: { type: 'number' }, * aBoolean: { type: 'boolean' }, @@ -32,11 +42,11 @@ import { InvalidSchema } from '../error' * A Schema is primarily used by a {@link Repository} which requires a Schema in * its constructor. */ -export class Schema { +export class Schema> { - #schemaName: string - #fieldsByName: Record = {} - #definition: SchemaDefinition + readonly #schemaName: string + #fieldsByName = {} as Record, Field>; + readonly #definition: SchemaDefinition #options?: SchemaOptions /** @@ -46,7 +56,7 @@ export class Schema { * @param schemaDef Defines all of the fields for the Schema and how they are mapped to Redis. * @param options Additional options for this Schema. */ - constructor(schemaName: string, schemaDef: SchemaDefinition, options?: SchemaOptions) { + constructor(schemaName: string, schemaDef: SchemaDefinition, options?: SchemaOptions) { this.#schemaName = schemaName this.#definition = schemaDef this.#options = options @@ -110,7 +120,7 @@ export class Schema { */ async generateId(): Promise { const ulidStrategy = () => ulid() - return await (this.#options?.idStrategy ?? ulidStrategy)() + return (this.#options?.idStrategy ?? ulidStrategy)(); } /** @@ -133,7 +143,8 @@ export class Schema { } #createFields() { - return Object.entries(this.#definition).forEach(([fieldName, fieldDef]) => { + const entries = Object.entries(this.#definition) as [EntityKeys, FieldDefinition][]; + return entries.forEach(([fieldName, fieldDef]) => { const field = new Field(fieldName, fieldDef) this.#validateField(field) this.#fieldsByName[fieldName] = field @@ -166,3 +177,5 @@ export class Schema { throw new InvalidSchema(`The field '${field.name}' is configured with a type of '${field.type}'. This type is only valid with a data structure of 'JSON'.`) } } + +export type InferSchema = T extends Schema ? R : never; \ No newline at end of file diff --git a/lib/search/results-converter.ts b/lib/search/results-converter.ts index e10c9ada..8de484e0 100644 --- a/lib/search/results-converter.ts +++ b/lib/search/results-converter.ts @@ -1,7 +1,7 @@ -import { RedisHashData, RedisJsonData, SearchDocument, SearchResults } from "../client" -import { Entity, EntityData, EntityId, EntityKeyName } from "../entity" -import { Schema } from "../schema" -import { fromRedisHash, fromRedisJson } from "../transformer" +import {RedisHashData, RedisJsonData, SearchDocument, SearchResults} from "../client" +import {Entity, EntityData, EntityId, EntityKeyName} from "../entity" +import {Schema} from "../schema" +import {fromRedisHash, fromRedisJson} from "../transformer" export function extractCountFromSearchResults(results: SearchResults): number { return results.total @@ -11,13 +11,12 @@ export function extractKeyNamesFromSearchResults(results: SearchResults): string return results.documents.map(document => document.id) } -export function extractEntityIdsFromSearchResults(schema: Schema, results: SearchResults): string[] { +export function extractEntityIdsFromSearchResults(schema: Schema, results: SearchResults): string[] { const keyNames = extractKeyNamesFromSearchResults(results) - const entityIds = keyNamesToEntityIds(schema.schemaName, keyNames) - return entityIds + return keyNamesToEntityIds(schema.schemaName, keyNames) } -export function extractEntitiesFromSearchResults(schema: Schema, results: SearchResults): Entity[] { +export function extractEntitiesFromSearchResults(schema: Schema, results: SearchResults): T[] { if (schema.dataStructure === 'HASH') { return results.documents.map(document => hashDocumentToEntity(schema, document)) } else { @@ -25,28 +24,25 @@ export function extractEntitiesFromSearchResults(schema: Schema, results: Search } } -function hashDocumentToEntity(schema: Schema, document: SearchDocument): Entity { +function hashDocumentToEntity(schema: Schema, document: SearchDocument): T { const keyName: string = document.id const hashData: RedisHashData = document.value const entityData = fromRedisHash(schema, hashData) - const entity = enrichEntityData(schema.schemaName, keyName, entityData) - return entity + return enrichEntityData(schema.schemaName, keyName, entityData) } -function jsonDocumentToEntity(schema: Schema, document: SearchDocument): Entity { +function jsonDocumentToEntity(schema: Schema, document: SearchDocument): T { const keyName: string = document.id const jsonData: RedisJsonData = document.value['$'] ?? false ? JSON.parse(document.value['$']) : document.value const entityData = fromRedisJson(schema, jsonData) - const entity = enrichEntityData(schema.schemaName, keyName, entityData) - return entity + return enrichEntityData(schema.schemaName, keyName, entityData) } -function enrichEntityData(keyPrefix: string, keyName: string, entityData: EntityData) { +function enrichEntityData(keyPrefix: string, keyName: string, entityData: EntityData): T { const entityId = keyNameToEntityId(keyPrefix, keyName) - const entity = { ...entityData, [EntityId]: entityId, [EntityKeyName]: keyName} - return entity + return {...entityData, [EntityId]: entityId, [EntityKeyName]: keyName} as T } function keyNamesToEntityIds(keyPrefix: string, keyNames: string[]): string[] { @@ -56,6 +52,5 @@ function keyNamesToEntityIds(keyPrefix: string, keyNames: string[]): string[] { function keyNameToEntityId(keyPrefix: string, keyName: string): string { const escapedPrefix = keyPrefix.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') const regex = new RegExp(`^${escapedPrefix}:`) - const entityId = keyName.replace(regex, "") - return entityId + return keyName.replace(regex, "") } diff --git a/lib/search/search.ts b/lib/search/search.ts index 8bb5bc83..9960027e 100644 --- a/lib/search/search.ts +++ b/lib/search/search.ts @@ -1,7 +1,7 @@ import { SearchOptions } from "redis" -import { Client } from "../client" -import { Entity } from '../entity' +import { Client, SearchResults } from "../client" +import { Entity, EntityKeys } from '../entity' import { Schema } from "../schema" import { Where } from './where' @@ -23,7 +23,7 @@ import { WhereDate } from "./where-date" * A function that takes a {@link Search} and returns a {@link Search}. Used in nested queries. * @template TEntity The type of {@link Entity} being sought. */ -export type SubSearchFunction = (search: Search) => Search +export type SubSearchFunction = (search: Search) => Search type AndOrConstructor = new (left: Where, right: Where) => Where @@ -35,10 +35,10 @@ type SortOptions = { BY: string, DIRECTION: 'ASC' | 'DESC' } * contains methods to return search results. * @template TEntity The type of {@link Entity} being sought. */ -export abstract class AbstractSearch { +export abstract class AbstractSearch> { /** @internal */ - protected schema: Schema + protected schema: Schema /** @internal */ protected client: Client @@ -47,7 +47,7 @@ export abstract class AbstractSearch { protected sortOptions?: SortOptions /** @internal */ - constructor(schema: Schema, client: Client) { + constructor(schema: Schema, client: Client) { this.schema = schema this.client = client } @@ -60,14 +60,14 @@ export abstract class AbstractSearch { * @param field The field to sort by. * @returns this */ - sortAscending(field: string): AbstractSearch { + sortAscending(field: EntityKeys): AbstractSearch { return this.sortBy(field, 'ASC') } /** * Alias for {@link Search.sortDescending}. */ - sortDesc(field: string): AbstractSearch { + sortDesc(field: EntityKeys): AbstractSearch { return this.sortDescending(field) } @@ -76,24 +76,24 @@ export abstract class AbstractSearch { * @param field The field to sort by. * @returns this */ - sortDescending(field: string): AbstractSearch { + sortDescending(field: EntityKeys): AbstractSearch { return this.sortBy(field, 'DESC') } /** * Alias for {@link Search.sortAscending}. */ - sortAsc(field: string): AbstractSearch { + sortAsc(field: EntityKeys): AbstractSearch { return this.sortAscending(field) } /** * Applies sorting for the query. - * @param field The field to sort by. + * @param fieldName The field to sort by. * @param order The order of returned {@link Entity | Entities} Defaults to `ASC` (ascending) if not specified * @returns this */ - sortBy(fieldName: string, order: 'ASC' | 'DESC' = 'ASC'): AbstractSearch { + sortBy(fieldName: EntityKeys, order: 'ASC' | 'DESC' = 'ASC'): AbstractSearch { const field = this.schema.fieldByName(fieldName) const dataStructure = this.schema.dataStructure @@ -132,7 +132,7 @@ export abstract class AbstractSearch { * @param field The field with the minimal value. * @returns The {@link Entity} with the minimal value */ - async min(field: string): Promise { + async min(field: EntityKeys): Promise { return await this.sortBy(field, 'ASC').first() } @@ -141,7 +141,7 @@ export abstract class AbstractSearch { * @param field The field with the minimal value. * @returns The entity ID with the minimal value */ - async minId(field: string): Promise { + async minId(field: EntityKeys): Promise { return await this.sortBy(field, 'ASC').firstId() } @@ -150,7 +150,7 @@ export abstract class AbstractSearch { * @param field The field with the minimal value. * @returns The key name with the minimal value */ - async minKey(field: string): Promise { + async minKey(field: EntityKeys): Promise { return await this.sortBy(field, 'ASC').firstKey() } @@ -159,7 +159,7 @@ export abstract class AbstractSearch { * @param field The field with the maximal value. * @returns The entity ID {@link Entity} with the maximal value */ - async max(field: string): Promise { + async max(field: EntityKeys): Promise { return await this.sortBy(field, 'DESC').first() } @@ -168,7 +168,7 @@ export abstract class AbstractSearch { * @param field The field with the maximal value. * @returns The entity ID with the maximal value */ - async maxId(field: string): Promise{ + async maxId(field: EntityKeys): Promise{ return await this.sortBy(field, 'DESC').firstId() } @@ -177,7 +177,7 @@ export abstract class AbstractSearch { * @param field The field with the maximal value. * @returns The key name with the maximal value */ - async maxKey(field: string): Promise { + async maxKey(field: EntityKeys): Promise { return await this.sortBy(field, 'DESC').firstKey() } @@ -196,7 +196,7 @@ export abstract class AbstractSearch { * @param count The number of {@link Entity | Entities} to return. * @returns An array of {@link Entity | Entities} matching the query. */ - async page(offset: number, count: number): Promise { + async page(offset: number, count: number): Promise { const searchResults = await this.callSearch(offset, count) return extractEntitiesFromSearchResults(this.schema, searchResults) } @@ -226,7 +226,7 @@ export abstract class AbstractSearch { /** * Returns the first {@link Entity} that matches this query. */ - async first(): Promise { + async first(): Promise { const foundEntity = await this.page(0, 1) return foundEntity[0] ?? null } @@ -240,7 +240,7 @@ export abstract class AbstractSearch { } /** - * Returns the first key name that matches this query. + * Returns the first key name that matches this query. */ async firstKey(): Promise { const foundKeys = await this.pageOfKeys(0, 1) @@ -261,8 +261,8 @@ export abstract class AbstractSearch { * @param options.pageSize Number of {@link Entity | Entities} returned per batch. * @returns An array of {@link Entity | Entities} matching the query. */ - async all(options = { pageSize: 10 }): Promise { - return this.allThings(this.page, options) as Promise + async all(options = { pageSize: 10 }): Promise { + return this.allThings(this.page, options) as Promise } /** @@ -298,56 +298,56 @@ export abstract class AbstractSearch { * @returns An array of key names matching the query. */ async allKeys(options = { pageSize: 10 }): Promise { - return this.allThings(this.pageOfKeys, options) as Promise + return await this.allThings(this.pageOfKeys, options) as string[] } /** * Returns the current instance. Syntactic sugar to make your code more fluent. * @returns this */ - get return(): AbstractSearch { + get return(): AbstractSearch { return this } /** * Alias for {@link Search.min}. */ - async returnMin(field: string): Promise { + async returnMin(field: EntityKeys): Promise { return await this.min(field) } /** * Alias for {@link Search.minId}. */ - async returnMinId(field: string): Promise { + async returnMinId(field: EntityKeys): Promise { return await this.minId(field) } /** * Alias for {@link Search.minKey}. */ - async returnMinKey(field: string): Promise { + async returnMinKey(field: EntityKeys): Promise { return await this.minKey(field) } /** * Alias for {@link Search.max}. */ - async returnMax(field: string): Promise { + async returnMax(field: EntityKeys): Promise { return await this.max(field) } /** * Alias for {@link Search.maxId}. */ - async returnMaxId(field: string): Promise { + async returnMaxId(field: EntityKeys): Promise { return await this.maxId(field) } /** * Alias for {@link Search.maxKey}. */ - async returnMaxKey(field: string): Promise { + async returnMaxKey(field: EntityKeys): Promise { return await this.maxKey(field) } @@ -361,7 +361,7 @@ export abstract class AbstractSearch { /** * Alias for {@link Search.page}. */ - async returnPage(offset: number, count: number): Promise { + async returnPage(offset: number, count: number): Promise { return await this.page(offset, count) } @@ -382,7 +382,7 @@ export abstract class AbstractSearch { /** * Alias for {@link Search.first}. */ - async returnFirst(): Promise { + async returnFirst(): Promise { return await this.first() } @@ -403,7 +403,7 @@ export abstract class AbstractSearch { /** * Alias for {@link Search.all}. */ - async returnAll(options = { pageSize: 10 }): Promise { + async returnAll(options = { pageSize: 10 }): Promise { return await this.all(options) } @@ -421,7 +421,7 @@ export abstract class AbstractSearch { return await this.allKeys(options) } - private async allThings(pageFn: Function, options = { pageSize: 10 }): Promise { + private async allThings(pageFn: Function, options = { pageSize: 10 }): Promise { const things = [] let offset = 0 const pageSize = options.pageSize @@ -436,7 +436,7 @@ export abstract class AbstractSearch { return things } - private async callSearch(offset = 0, count = 0, keysOnly = false) { + private async callSearch(offset = 0, count = 0, keysOnly = false): Promise { const dataStructure = this.schema.dataStructure const indexName = this.schema.indexName @@ -473,11 +473,11 @@ export abstract class AbstractSearch { * installed. * @template TEntity The type of {@link Entity} being sought. */ -export class RawSearch extends AbstractSearch { - private rawQuery: string +export class RawSearch> extends AbstractSearch { + private readonly rawQuery: string /** @internal */ - constructor(schema: Schema, client: Client, query: string = '*') { + constructor(schema: Schema, client: Client, query: string = '*') { super(schema, client) this.rawQuery = query } @@ -494,7 +494,7 @@ export class RawSearch extends AbstractSearch { * Requires that RediSearch (and optionally RedisJSON) be installed. * @template TEntity The type of {@link Entity} being sought. */ -export class Search extends AbstractSearch { +export class Search> extends AbstractSearch { private rootWhere?: Where /** @internal */ @@ -509,7 +509,7 @@ export class Search extends AbstractSearch { * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - where(field: string): WhereField + where(field: EntityKeys): WhereField /** * Sets up a nested search. If there are multiple calls to {@link Search.where}, @@ -517,8 +517,8 @@ export class Search extends AbstractSearch { * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - where(subSearchFn: SubSearchFunction): Search - where(fieldOrFn: string | SubSearchFunction): WhereField | Search { + where(subSearchFn: SubSearchFunction): Search + where(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { return this.anyWhere(WhereAnd, fieldOrFn) } @@ -527,15 +527,15 @@ export class Search extends AbstractSearch { * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - and(field: string): WhereField + and(field: EntityKeys): WhereField /** * Sets up a nested search as a logical AND. * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - and(subSearchFn: SubSearchFunction): Search - and(fieldOrFn: string | SubSearchFunction): WhereField | Search { + and(subSearchFn: SubSearchFunction): Search + and(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { return this.anyWhere(WhereAnd, fieldOrFn) } @@ -544,19 +544,19 @@ export class Search extends AbstractSearch { * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - or(field: string): WhereField + or(field: EntityKeys): WhereField /** * Sets up a nested search as a logical OR. * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - or(subSearchFn: SubSearchFunction): Search - or(fieldOrFn: string | SubSearchFunction): WhereField | Search { + or(subSearchFn: SubSearchFunction): Search + or(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { return this.anyWhere(WhereOr, fieldOrFn) } - private anyWhere(ctor: AndOrConstructor, fieldOrFn: string | SubSearchFunction): WhereField | Search { + private anyWhere(ctor: AndOrConstructor, fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { if (typeof fieldOrFn === 'string') { return this.anyWhereForField(ctor, fieldOrFn) } else { @@ -564,7 +564,7 @@ export class Search extends AbstractSearch { } } - private anyWhereForField(ctor: AndOrConstructor, field: string): WhereField { + private anyWhereForField(ctor: AndOrConstructor, field: EntityKeys): WhereField { const where = this.createWhere(field) if (this.rootWhere === undefined) { @@ -576,7 +576,7 @@ export class Search extends AbstractSearch { return where } - private anyWhereForFunction(ctor: AndOrConstructor, subSearchFn: SubSearchFunction): Search { + private anyWhereForFunction(ctor: AndOrConstructor, subSearchFn: SubSearchFunction): Search { const search = new Search(this.schema, this.client) const subSearch = subSearchFn(search) @@ -593,7 +593,7 @@ export class Search extends AbstractSearch { return this } - private createWhere(fieldName: string): WhereField { + private createWhere(fieldName: EntityKeys): WhereField { const field = this.schema.fieldByName(fieldName) if (field === null) throw new FieldNotInSchema(fieldName) diff --git a/lib/search/where-boolean.ts b/lib/search/where-boolean.ts index ea58b809..7b6bd179 100644 --- a/lib/search/where-boolean.ts +++ b/lib/search/where-boolean.ts @@ -1,31 +1,32 @@ import { Search } from "./search" import { WhereField } from "./where-field" +import {Entity} from "$lib/entity"; -export abstract class WhereBoolean extends WhereField { +export abstract class WhereBoolean extends WhereField { protected value!: boolean - eq(value: boolean): Search { + eq(value: boolean): Search { this.value = value return this.search } - equal(value: boolean): Search { return this.eq(value) } - equals(value: boolean): Search { return this.eq(value) } - equalTo(value: boolean): Search { return this.eq(value) } + equal(value: boolean): Search { return this.eq(value) } + equals(value: boolean): Search { return this.eq(value) } + equalTo(value: boolean): Search { return this.eq(value) } - true(): Search { return this.eq(true) } - false(): Search { return this.eq(false) } + true(): Search { return this.eq(true) } + false(): Search { return this.eq(false) } abstract toString(): string } -export class WhereHashBoolean extends WhereBoolean { +export class WhereHashBoolean extends WhereBoolean { toString(): string { return this.buildQuery(`{${this.value ? '1' : '0'}}`) } } -export class WhereJsonBoolean extends WhereBoolean { +export class WhereJsonBoolean extends WhereBoolean { toString(): string { return this.buildQuery(`{${this.value}}`) } diff --git a/lib/search/where-date.ts b/lib/search/where-date.ts index ad1b547e..58c8c46f 100644 --- a/lib/search/where-date.ts +++ b/lib/search/where-date.ts @@ -1,62 +1,63 @@ import { Search } from "./search" import { WhereField } from "./where-field" +import {Entity} from "$lib/entity"; -export class WhereDate extends WhereField { +export class WhereDate extends WhereField { private lower: number = Number.NEGATIVE_INFINITY private upper: number = Number.POSITIVE_INFINITY private lowerExclusive: boolean = false private upperExclusive: boolean = false - eq(value: Date | number | string): Search { + eq(value: Date | number | string): Search { const epoch = this.coerceDateToEpoch(value) this.lower = epoch this.upper = epoch return this.search } - gt(value: Date | number | string): Search { + gt(value: Date | number | string): Search { const epoch = this.coerceDateToEpoch(value) this.lower = epoch this.lowerExclusive = true return this.search } - gte(value: Date | number | string): Search { + gte(value: Date | number | string): Search { this.lower = this.coerceDateToEpoch(value) return this.search } - lt(value: Date | number | string): Search { + lt(value: Date | number | string): Search { this.upper = this.coerceDateToEpoch(value) this.upperExclusive = true return this.search } - lte(value: Date | number | string): Search { + lte(value: Date | number | string): Search { this.upper = this.coerceDateToEpoch(value) return this.search } - between(lower: Date | number | string, upper: Date | number | string): Search { + between(lower: Date | number | string, upper: Date | number | string): Search { this.lower = this.coerceDateToEpoch(lower) this.upper = this.coerceDateToEpoch(upper) return this.search } - equal(value: Date | number | string): Search { return this.eq(value) } - equals(value: Date | number | string): Search { return this.eq(value) } - equalTo(value: Date | number | string): Search { return this.eq(value) } + equal(value: Date | number | string): Search { return this.eq(value) } + equals(value: Date | number | string): Search { return this.eq(value) } + equalTo(value: Date | number | string): Search { return this.eq(value) } - greaterThan(value: Date | number | string): Search { return this.gt(value) } - greaterThanOrEqualTo(value: Date | number | string): Search { return this.gte(value) } - lessThan(value: Date | number | string): Search { return this.lt(value) } - lessThanOrEqualTo(value: Date | number | string): Search { return this.lte(value) } + greaterThan(value: Date | number | string): Search { return this.gt(value) } + greaterThanOrEqualTo(value: Date | number | string): Search { return this.gte(value) } + lessThan(value: Date | number | string): Search { return this.lt(value) } + lessThanOrEqualTo(value: Date | number | string): Search { return this.lte(value) } - on(value: Date | number | string): Search { return this.eq(value) } - after(value: Date | number | string): Search { return this.gt(value) } - before(value: Date | number | string): Search { return this.lt(value) } - onOrAfter(value: Date | number | string): Search { return this.gte(value) } - onOrBefore(value: Date | number | string): Search { return this.lte(value) } + on(value: Date | number | string): Search { return this.eq(value) } + after(value: Date | number | string): Search { return this.gt(value) } + before(value: Date | number | string): Search { return this.lt(value) } + onOrAfter(value: Date | number | string): Search { return this.gte(value) } + onOrBefore(value: Date | number | string): Search { return this.lte(value) } toString(): string { const lower = this.makeLowerString() diff --git a/lib/search/where-field.ts b/lib/search/where-field.ts index 12b563c5..6b671340 100644 --- a/lib/search/where-field.ts +++ b/lib/search/where-field.ts @@ -2,12 +2,13 @@ import { Field } from "../schema" import { Search } from "./search" import { Where } from "./where" import { CircleFunction } from "./where-point" +import { Entity } from "$lib/entity"; /** * Interface with all the methods from all the concrete * classes under {@link WhereField}. */ -export interface WhereField extends Where { +export interface WhereField> extends Where { /** * Adds an equals comparison to the query. @@ -17,7 +18,7 @@ export interface WhereField extends Where { * @param value The value to be compared * @returns The {@link Search} that was called to create this {@link WhereField}. */ - eq(value: string | number | boolean | Date): Search + eq(value: string | number | boolean | Date): Search /** * Adds an equals comparison to the query. @@ -27,7 +28,7 @@ export interface WhereField extends Where { * @param value The value to be compared * @returns The {@link Search} that was called to create this {@link WhereField}. */ - equal(value: string | number | boolean | Date): Search + equal(value: string | number | boolean | Date): Search /** * Adds an equals comparison to the query. @@ -37,7 +38,7 @@ export interface WhereField extends Where { * @param value The value to be compared * @returns The {@link Search} that was called to create this {@link WhereField}. */ - equals(value: string | number | boolean | Date): Search + equals(value: string | number | boolean | Date): Search /** * Adds an equals comparison to the query. @@ -47,128 +48,130 @@ export interface WhereField extends Where { * @param value The value to be compared * @returns The {@link Search} that was called to create this {@link WhereField}. */ - equalTo(value: string | number | boolean | Date): Search + equalTo(value: string | number | boolean | Date): Search /** * Adds a full-text search comparison to the query. * @param value The word or phrase sought. + * @param options * @param options.fuzzyMatching Whether to use fuzzy matching to find the sought word or phrase. Defaults to `false`. * @param options.levenshteinDistance The levenshtein distance to use for fuzzy matching. Supported values are `1`, `2`, and `3`. Defaults to `1`. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - match(value: string | number | boolean, options?: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 }): Search + match(value: string | number | boolean, options?: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 }): Search /** * Adds a full-text search comparison to the query. * @param value The word or phrase sought. + * @param options * @param options.fuzzyMatching Whether to use fuzzy matching to find the sought word or phrase. Defaults to `false`. * @param options.levenshteinDistance The levenshtein distance to use for fuzzy matching. Supported values are `1`, `2`, and `3`. Defaults to `1`. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - matches(value: string | number | boolean, options?: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 }): Search + matches(value: string | number | boolean, options?: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 }): Search /** * Adds a full-text search comparison to the query that matches an exact word or phrase. * @param value The word or phrase sought. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - matchExact(value: string | number | boolean): Search + matchExact(value: string | number | boolean): Search /** * Adds a full-text search comparison to the query that matches an exact word or phrase. * @param value The word or phrase sought. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - matchExactly(value: string | number | boolean): Search + matchExactly(value: string | number | boolean): Search /** * Adds a full-text search comparison to the query that matches an exact word or phrase. * @param value The word or phrase sought. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - matchesExactly(value: string | number | boolean): Search + matchesExactly(value: string | number | boolean): Search /** * Makes a call to {@link WhereField.match} a {@link WhereField.matchExact} call. Calling * this multiple times will have no effect. * @returns this. */ - readonly exact: WhereField + readonly exact: WhereField /** * Makes a call to {@link WhereField.match} a {@link WhereField.matchExact} call. Calling * this multiple times will have no effect. * @returns this. */ - readonly exactly: WhereField + readonly exactly: WhereField /** * Adds a boolean match with a value of `true` to the query. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - true(): Search + true(): Search /** * Adds a boolean match with a value of `false` to the query. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - false(): Search + false(): Search /** * Adds a greater than comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - gt(value: string | number | Date): Search + gt(value: string | number | Date): Search /** * Adds a greater than comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - greaterThan(value: string | number | Date): Search + greaterThan(value: string | number | Date): Search /** * Adds a greater than or equal to comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - gte(value: string | number | Date): Search + gte(value: string | number | Date): Search /** * Adds a greater than or equal to comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - greaterThanOrEqualTo(value: string | number | Date): Search + greaterThanOrEqualTo(value: string | number | Date): Search /** * Adds a less than comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - lt(value: string | number | Date): Search + lt(value: string | number | Date): Search /** * Adds a less than comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - lessThan(value: string | number | Date): Search + lessThan(value: string | number | Date): Search /** * Adds a less than or equal to comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - lte(value: string | number | Date): Search + lte(value: string | number | Date): Search /** * Adds a less than or equal to comparison against a field to the search query. * @param value The value to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - lessThanOrEqualTo(value: string | number | Date): Search + lessThanOrEqualTo(value: string | number | Date): Search /** * Adds an inclusive range comparison against a field to the search query. @@ -176,21 +179,21 @@ export interface WhereField extends Where { * @param upper The upper bound of the range. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - between(lower: string | number | Date, upper: string | number | Date): Search + between(lower: string | number | Date, upper: string | number | Date): Search /** * Adds a whole-string match for a value within a string array to the search query. * @param value The string to be matched. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - contain(value: string): Search + contain(value: string): Search /** * Adds a whole-string match for a value within a string array to the search query. * @param value The string to be matched. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - contains(value: string): Search + contains(value: string): Search /** * Adds a whole-string match against a string array to the query. If any of the provided @@ -198,7 +201,7 @@ export interface WhereField extends Where { * @param value An array of strings that you want to match one of. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - containOneOf(...value: Array): Search + containOneOf(...value: Array): Search /** * Adds a whole-string match against a string array to the query. If any of the provided @@ -206,56 +209,56 @@ export interface WhereField extends Where { * @param value An array of strings that you want to match one of. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - containsOneOf(...value: Array): Search + containsOneOf(...value: Array): Search /** * Adds a search for points that fall within a defined circle. * @param circleFn A function that returns a {@link Circle} instance defining the search area. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - inCircle(circleFn: CircleFunction): Search + inCircle(circleFn: CircleFunction): Search /** * Adds a search for points that fall within a defined radius. * @param circleFn A function that returns a {@link Circle} instance defining the search area. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - inRadius(circleFn: CircleFunction): Search + inRadius(circleFn: CircleFunction): Search /** * Add a search for an exact UTC datetime to the query. * @param value The datetime to match. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - on(value: string | number | Date): Search + on(value: string | number | Date): Search /** * Add a search that matches all datetimes *before* the provided UTC datetime to the query. * @param value The datetime to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - before(value: string | number | Date): Search + before(value: string | number | Date): Search /** * Add a search that matches all datetimes *after* the provided UTC datetime to the query. * @param value The datetime to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - after(value: string | number | Date): Search + after(value: string | number | Date): Search /** * Add a search that matches all datetimes *on or before* the provided UTC datetime to the query. * @param value The datetime to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - onOrBefore(value: string | number | Date): Search + onOrBefore(value: string | number | Date): Search /** * Add a search that matches all datetimes *on or after* the provided UTC datetime to the query. * @param value The datetime to compare against. * @returns The {@link Search} that was called to create this {@link WhereField}. */ - onOrAfter(value: string | number | Date): Search + onOrAfter(value: string | number | Date): Search } /** @@ -263,17 +266,17 @@ export interface WhereField extends Where { * with extend. When you call {@link Search.where}, a * subclass of this is returned. */ -export abstract class WhereField { +export abstract class WhereField { private negated: boolean = false /** @internal */ - protected search: Search + protected search: Search /** @internal */ protected field: Field /** @internal */ - constructor(search: Search, field: Field) { + constructor(search: Search, field: Field) { this.search = search this.field = field } diff --git a/lib/search/where-number.ts b/lib/search/where-number.ts index cabe6707..c53d92bf 100644 --- a/lib/search/where-number.ts +++ b/lib/search/where-number.ts @@ -1,54 +1,55 @@ import { Search } from "./search" import { WhereField } from "./where-field" +import {Entity} from "$lib/entity"; -export class WhereNumber extends WhereField { +export class WhereNumber extends WhereField { private lower: number = Number.NEGATIVE_INFINITY private upper: number = Number.POSITIVE_INFINITY private lowerExclusive: boolean = false private upperExclusive: boolean = false - eq(value: number): Search { + eq(value: number): Search { this.lower = value this.upper = value return this.search } - gt(value: number): Search { + gt(value: number): Search { this.lower = value this.lowerExclusive = true return this.search } - gte(value: number): Search { + gte(value: number): Search { this.lower = value return this.search } - lt(value: number): Search { + lt(value: number): Search { this.upper = value this.upperExclusive = true return this.search } - lte(value: number): Search { + lte(value: number): Search { this.upper = value return this.search } - between(lower: number, upper: number): Search { + between(lower: number, upper: number): Search { this.lower = lower this.upper = upper return this.search } - equal(value: number): Search { return this.eq(value) } - equals(value: number): Search { return this.eq(value) } - equalTo(value: number): Search { return this.eq(value) } + equal(value: number): Search { return this.eq(value) } + equals(value: number): Search { return this.eq(value) } + equalTo(value: number): Search { return this.eq(value) } - greaterThan(value: number): Search { return this.gt(value) } - greaterThanOrEqualTo(value: number): Search { return this.gte(value) } - lessThan(value: number): Search { return this.lt(value) } - lessThanOrEqualTo(value: number): Search { return this.lte(value) } + greaterThan(value: number): Search { return this.gt(value) } + greaterThanOrEqualTo(value: number): Search { return this.gte(value) } + lessThan(value: number): Search { return this.lt(value) } + lessThanOrEqualTo(value: number): Search { return this.lte(value) } toString(): string { const lower = this.makeLowerString() diff --git a/lib/search/where-point.ts b/lib/search/where-point.ts index 76ba1ae8..9dd8630f 100644 --- a/lib/search/where-point.ts +++ b/lib/search/where-point.ts @@ -1,4 +1,4 @@ -import { Point } from "../entity" +import {Entity, Point} from "../entity" import { Search } from "./search" import { WhereField } from "./where-field" @@ -173,14 +173,14 @@ export class Circle { } } -export class WherePoint extends WhereField { +export class WherePoint extends WhereField { private circle: Circle = new Circle() - inRadius(circleFn: CircleFunction): Search { + inRadius(circleFn: CircleFunction): Search { return this.inCircle(circleFn) } - inCircle(circleFn: CircleFunction): Search { + inCircle(circleFn: CircleFunction): Search { this.circle = circleFn(this.circle) return this.search } diff --git a/lib/search/where-string-array.ts b/lib/search/where-string-array.ts index 93102e32..9f21ce48 100644 --- a/lib/search/where-string-array.ts +++ b/lib/search/where-string-array.ts @@ -1,22 +1,23 @@ import { Search } from "./search" import { WhereField } from "./where-field" +import {Entity} from "$lib/entity"; -export class WhereStringArray extends WhereField { +export class WhereStringArray extends WhereField { private value!: Array - contain(value: string): Search { + contain(value: string): Search { this.value = [value] return this.search } - contains(value: string): Search { return this.contain(value) } + contains(value: string): Search { return this.contain(value) } - containsOneOf(...value: Array): Search { + containsOneOf(...value: Array): Search { this.value = value return this.search } - containOneOf(...value: Array): Search { return this.containsOneOf(...value) } + containOneOf(...value: Array): Search { return this.containsOneOf(...value) } toString(): string { const escapedValue = this.value.map(s => this.escapePunctuationAndSpaces(s)).join('|') diff --git a/lib/search/where-string.ts b/lib/search/where-string.ts index 40cd1d5c..1440718c 100644 --- a/lib/search/where-string.ts +++ b/lib/search/where-string.ts @@ -1,24 +1,25 @@ import { Search } from "./search" import { WhereField } from "./where-field" import { SemanticSearchError } from "../error" +import {Entity} from "$lib/entity"; -export class WhereString extends WhereField { +export class WhereString extends WhereField { private value!: string - eq(value: string | number | boolean): Search { + eq(value: string | number | boolean): Search { this.value = value.toString() return this.search } - equal(value: string | number | boolean): Search { return this.eq(value) } - equals(value: string | number | boolean): Search { return this.eq(value) } - equalTo(value: string | number | boolean): Search { return this.eq(value) } + equal(value: string | number | boolean): Search { return this.eq(value) } + equals(value: string | number | boolean): Search { return this.eq(value) } + equalTo(value: string | number | boolean): Search { return this.eq(value) } - match(_: string | number | boolean): Search { return this.throwMatchExcpetion() } - matches(_: string | number | boolean): Search { return this.throwMatchExcpetion() } - matchExact(_: string | number | boolean): Search { return this.throwMatchExcpetion() } - matchExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } - matchesExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } + match(_: string | number | boolean): Search { return this.throwMatchExcpetion() } + matches(_: string | number | boolean): Search { return this.throwMatchExcpetion() } + matchExact(_: string | number | boolean): Search { return this.throwMatchExcpetion() } + matchExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } + matchesExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } get exact() { return this.throwMatchExcpetionReturningThis() } get exactly() { return this.throwMatchExcpetionReturningThis() } @@ -28,11 +29,11 @@ export class WhereString extends WhereField { return this.buildQuery(`{${escapedValue}}`) } - private throwMatchExcpetion(): Search { + private throwMatchExcpetion(): Search { throw new SemanticSearchError("Cannot perform full-text search operations like .match on field of type 'string'. If full-text search is needed on this field, change the type to 'text' in the Schema.") } - private throwMatchExcpetionReturningThis(): WhereString { + private throwMatchExcpetionReturningThis(): WhereString { throw new SemanticSearchError("Cannot perform full-text search operations like .match on field of type 'string'. If full-text search is needed on this field, change the type to 'text' in the Schema.") } } diff --git a/lib/search/where-text.ts b/lib/search/where-text.ts index 423bc2e9..7081925b 100644 --- a/lib/search/where-text.ts +++ b/lib/search/where-text.ts @@ -2,8 +2,9 @@ import { Search } from "./search" import { WhereField } from "./where-field" import { SemanticSearchError } from "../error" +import {Entity} from "$lib/entity"; -export class WhereText extends WhereField { +export class WhereText extends WhereField { private value!: string private exactValue = false private fuzzyMatching!: boolean @@ -15,14 +16,14 @@ export class WhereText extends WhereField { fuzzyMatching: false, levenshteinDistance: 1, } - ): Search { + ): Search { this.value = value.toString() this.fuzzyMatching = options.fuzzyMatching ?? false this.levenshteinDistance = options.levenshteinDistance ?? 1 return this.search } - matchExact(value: string | number | boolean): Search { + matchExact(value: string | number | boolean): Search { this.exact.value = value.toString() return this.search } @@ -33,9 +34,9 @@ export class WhereText extends WhereField { fuzzyMatching: false, levenshteinDistance: 1, } - ): Search { return this.match(value, options) } - matchExactly(value: string | number | boolean): Search { return this.matchExact(value) } - matchesExactly(value: string | number | boolean): Search { return this.matchExact(value) } + ): Search { return this.match(value, options) } + matchExactly(value: string | number | boolean): Search { return this.matchExact(value) } + matchesExactly(value: string | number | boolean): Search { return this.matchExact(value) } get exact() { this.exactValue = true @@ -46,10 +47,10 @@ export class WhereText extends WhereField { return this.exact } - eq(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } - equal(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } - equals(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } - equalTo(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } + eq(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } + equal(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } + equals(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } + equalTo(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } toString(): string { const escapedValue = this.escapePunctuation(this.value) @@ -63,7 +64,7 @@ export class WhereText extends WhereField { } } - private throwEqualsExcpetion(): Search { + private throwEqualsExcpetion(): Search { throw new SemanticSearchError("Cannot call .equals on a field of type 'text', either use .match to perform full-text search or change the type to 'string' in the Schema.") } } From 4972f71b3b327e35abff1e5d7a2df3970487b685 Mon Sep 17 00:00:00 2001 From: Dennis Lysenko Date: Mon, 10 Jun 2024 19:14:16 -0400 Subject: [PATCH 2/5] Refactor Entity typings in entity.ts Changed the EntityKeys type to exclude keys from EntityInternal instead of just symbol and number. Also, fixed a formatting issue with the EntityDataValue type definition. --- lib/entity/entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/entity/entity.ts b/lib/entity/entity.ts index f742b6e5..a9d381d9 100644 --- a/lib/entity/entity.ts +++ b/lib/entity/entity.ts @@ -14,7 +14,7 @@ export type EntityInternal = { /** Defines the objects returned from calls to {@link Repository | repositories }. */ export type Entity = EntityData & EntityInternal -export type EntityKeys = Exclude +export type EntityKeys = Exclude; /** The free-form data associated with an {@link Entity}. */ export type EntityData = { @@ -22,7 +22,7 @@ export type EntityData = { } /** Valid types for values in an {@link Entity}. */ -export type EntityDataValue = string | number | boolean | Date | Point | null | undefined | Array +export type EntityDataValue = string | number | boolean | Date | Point | null | undefined | Array /** Defines a point on the globe using longitude and latitude. */ export type Point = { From c593dbe7b8707e2e44828948180ea47cb2226a67 Mon Sep 17 00:00:00 2001 From: Dennis Lysenko Date: Mon, 10 Jun 2024 19:54:49 -0400 Subject: [PATCH 3/5] Refactor type checks and field name casting Changed conditions in 'anyWhere' method to check for function types instead of string. Also updated instances of field identifier use to ensure type safety by casting identifier to string. This improves error messaging and overall type consistency across --- lib/schema/schema.ts | 18 +++++++-------- lib/search/search.ts | 55 ++++++++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/lib/schema/schema.ts b/lib/schema/schema.ts index 39b860d3..f5e8e027 100644 --- a/lib/schema/schema.ts +++ b/lib/schema/schema.ts @@ -1,13 +1,13 @@ -import { createHash } from 'crypto' -import { ulid } from 'ulid' +import {createHash} from 'crypto' +import {ulid} from 'ulid' -import { Entity, EntityKeys } from "../entity" +import {Entity, EntityKeys} from "../entity" -import { IdStrategy, DataStructure, StopWordOptions, SchemaOptions } from './options' +import {DataStructure, IdStrategy, SchemaOptions, StopWordOptions} from './options' -import { FieldDefinition, SchemaDefinition } from './definitions' -import { Field } from './field' -import { InvalidSchema } from '../error' +import {FieldDefinition, SchemaDefinition} from './definitions' +import {Field} from './field' +import {InvalidSchema} from '../error' /** @@ -85,7 +85,7 @@ export class Schema> { * @param name The name of the {@link Field} in this Schema. * @returns The {@link Field}, or null of not found. */ - fieldByName(name: string): Field | null { + fieldByName(name: EntityKeys): Field | null { return this.#fieldsByName[name] ?? null } @@ -145,7 +145,7 @@ export class Schema> { #createFields() { const entries = Object.entries(this.#definition) as [EntityKeys, FieldDefinition][]; return entries.forEach(([fieldName, fieldDef]) => { - const field = new Field(fieldName, fieldDef) + const field = new Field(String(fieldName), fieldDef) this.#validateField(field) this.#fieldsByName[fieldName] = field }) diff --git a/lib/search/search.ts b/lib/search/search.ts index 9960027e..fa55eeda 100644 --- a/lib/search/search.ts +++ b/lib/search/search.ts @@ -1,23 +1,28 @@ -import { SearchOptions } from "redis" - -import { Client, SearchResults } from "../client" -import { Entity, EntityKeys } from '../entity' -import { Schema } from "../schema" - -import { Where } from './where' -import { WhereAnd } from './where-and' -import { WhereOr } from './where-or' -import { WhereField } from './where-field' -import { WhereStringArray } from './where-string-array' -import { WhereHashBoolean, WhereJsonBoolean } from './where-boolean' -import { WhereNumber } from './where-number' -import { WherePoint } from './where-point' -import { WhereString } from './where-string' -import { WhereText } from './where-text' - -import { extractCountFromSearchResults, extractEntitiesFromSearchResults, extractEntityIdsFromSearchResults, extractKeyNamesFromSearchResults } from "./results-converter" -import { FieldNotInSchema, RedisOmError, SearchError } from "../error" -import { WhereDate } from "./where-date" +import {SearchOptions} from "redis" + +import {Client, SearchResults} from "../client" +import {Entity, EntityKeys} from '../entity' +import {Schema} from "../schema" + +import {Where} from './where' +import {WhereAnd} from './where-and' +import {WhereOr} from './where-or' +import {WhereField} from './where-field' +import {WhereStringArray} from './where-string-array' +import {WhereHashBoolean, WhereJsonBoolean} from './where-boolean' +import {WhereNumber} from './where-number' +import {WherePoint} from './where-point' +import {WhereString} from './where-string' +import {WhereText} from './where-text' + +import { + extractCountFromSearchResults, + extractEntitiesFromSearchResults, + extractEntityIdsFromSearchResults, + extractKeyNamesFromSearchResults +} from "./results-converter" +import {FieldNotInSchema, RedisOmError, SearchError} from "../error" +import {WhereDate} from "./where-date" /** * A function that takes a {@link Search} and returns a {@link Search}. Used in nested queries. @@ -98,7 +103,7 @@ export abstract class AbstractSearch> { const dataStructure = this.schema.dataStructure if (!field) { - const message = `'sortBy' was called on field '${fieldName}' which is not defined in the Schema.` + const message = `'sortBy' was called on field '${String(fieldName)}' which is not defined in the Schema.` console.error(message) throw new RedisOmError(message) } @@ -557,10 +562,10 @@ export class Search> extends AbstractSear } private anyWhere(ctor: AndOrConstructor, fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { - if (typeof fieldOrFn === 'string') { - return this.anyWhereForField(ctor, fieldOrFn) - } else { + if (typeof fieldOrFn === 'function') { return this.anyWhereForFunction(ctor, fieldOrFn) + } else { + return this.anyWhereForField(ctor, fieldOrFn) } } @@ -596,7 +601,7 @@ export class Search> extends AbstractSear private createWhere(fieldName: EntityKeys): WhereField { const field = this.schema.fieldByName(fieldName) - if (field === null) throw new FieldNotInSchema(fieldName) + if (field === null) throw new FieldNotInSchema(String(fieldName)) if (field.type === 'boolean' && this.schema.dataStructure === 'HASH') return new WhereHashBoolean(this, field) if (field.type === 'boolean' && this.schema.dataStructure === 'JSON') return new WhereJsonBoolean(this, field) From e37ef9b2dc3e94c0b5edc330c0229829fdb54e74 Mon Sep 17 00:00:00 2001 From: Dennis Lysenko Date: Wed, 10 Jul 2024 11:28:06 -0400 Subject: [PATCH 4/5] formatting --- lib/repository/repository.ts | 2 +- lib/schema/schema.ts | 2 +- lib/search/search.ts | 418 +++++++++++++++++++---------------- 3 files changed, 235 insertions(+), 187 deletions(-) diff --git a/lib/repository/repository.ts b/lib/repository/repository.ts index cc60d14e..088e780e 100644 --- a/lib/repository/repository.ts +++ b/lib/repository/repository.ts @@ -142,7 +142,7 @@ export class Repository> { */ async save(id: string, entity: T): Promise - async save(entityOrId: T | string, maybeEntity?: T): Promise { + async save(entityOrId: T | string, maybeEntity?: T): Promise { let entity: T | undefined let entityId: string | undefined diff --git a/lib/schema/schema.ts b/lib/schema/schema.ts index f5e8e027..595d4090 100644 --- a/lib/schema/schema.ts +++ b/lib/schema/schema.ts @@ -120,7 +120,7 @@ export class Schema> { */ async generateId(): Promise { const ulidStrategy = () => ulid() - return (this.#options?.idStrategy ?? ulidStrategy)(); + return await (this.#options?.idStrategy ?? ulidStrategy)(); } /** diff --git a/lib/search/search.ts b/lib/search/search.ts index fa55eeda..826b1d15 100644 --- a/lib/search/search.ts +++ b/lib/search/search.ts @@ -1,39 +1,41 @@ -import {SearchOptions} from "redis" - -import {Client, SearchResults} from "../client" -import {Entity, EntityKeys} from '../entity' -import {Schema} from "../schema" - -import {Where} from './where' -import {WhereAnd} from './where-and' -import {WhereOr} from './where-or' -import {WhereField} from './where-field' -import {WhereStringArray} from './where-string-array' -import {WhereHashBoolean, WhereJsonBoolean} from './where-boolean' -import {WhereNumber} from './where-number' -import {WherePoint} from './where-point' -import {WhereString} from './where-string' -import {WhereText} from './where-text' +import { SearchOptions } from "redis"; + +import { Client, SearchResults } from "../client"; +import { Entity, EntityKeys } from "../entity"; +import { Schema } from "../schema"; + +import { Where } from "./where"; +import { WhereAnd } from "./where-and"; +import { WhereOr } from "./where-or"; +import { WhereField } from "./where-field"; +import { WhereStringArray } from "./where-string-array"; +import { WhereHashBoolean, WhereJsonBoolean } from "./where-boolean"; +import { WhereNumber } from "./where-number"; +import { WherePoint } from "./where-point"; +import { WhereString } from "./where-string"; +import { WhereText } from "./where-text"; import { extractCountFromSearchResults, extractEntitiesFromSearchResults, extractEntityIdsFromSearchResults, - extractKeyNamesFromSearchResults -} from "./results-converter" -import {FieldNotInSchema, RedisOmError, SearchError} from "../error" -import {WhereDate} from "./where-date" + extractKeyNamesFromSearchResults, +} from "./results-converter"; +import { FieldNotInSchema, RedisOmError, SearchError } from "../error"; +import { WhereDate } from "./where-date"; /** * A function that takes a {@link Search} and returns a {@link Search}. Used in nested queries. * @template TEntity The type of {@link Entity} being sought. */ -export type SubSearchFunction = (search: Search) => Search +export type SubSearchFunction = ( + search: Search, +) => Search; -type AndOrConstructor = new (left: Where, right: Where) => Where +type AndOrConstructor = new (left: Where, right: Where) => Where; // This is a simplified redefintion of the SortByProperty type that is not exported by Node Redis -type SortOptions = { BY: string, DIRECTION: 'ASC' | 'DESC' } +type SortOptions = { BY: string; DIRECTION: "ASC" | "DESC" }; /** * Abstract base class for {@link Search} and {@link RawSearch} that @@ -41,24 +43,23 @@ type SortOptions = { BY: string, DIRECTION: 'ASC' | 'DESC' } * @template TEntity The type of {@link Entity} being sought. */ export abstract class AbstractSearch> { - /** @internal */ - protected schema: Schema + protected schema: Schema; /** @internal */ - protected client: Client + protected client: Client; /** @internal */ - protected sortOptions?: SortOptions + protected sortOptions?: SortOptions; /** @internal */ constructor(schema: Schema, client: Client) { - this.schema = schema - this.client = client + this.schema = schema; + this.client = client; } /** @internal */ - abstract get query(): string + abstract get query(): string; /** * Applies an ascending sort to the query. @@ -66,14 +67,14 @@ export abstract class AbstractSearch> { * @returns this */ sortAscending(field: EntityKeys): AbstractSearch { - return this.sortBy(field, 'ASC') + return this.sortBy(field, "ASC"); } /** * Alias for {@link Search.sortDescending}. */ sortDesc(field: EntityKeys): AbstractSearch { - return this.sortDescending(field) + return this.sortDescending(field); } /** @@ -82,54 +83,69 @@ export abstract class AbstractSearch> { * @returns this */ sortDescending(field: EntityKeys): AbstractSearch { - return this.sortBy(field, 'DESC') + return this.sortBy(field, "DESC"); } /** * Alias for {@link Search.sortAscending}. */ sortAsc(field: EntityKeys): AbstractSearch { - return this.sortAscending(field) + return this.sortAscending(field); } /** - * Applies sorting for the query. - * @param fieldName The field to sort by. - * @param order The order of returned {@link Entity | Entities} Defaults to `ASC` (ascending) if not specified - * @returns this - */ - sortBy(fieldName: EntityKeys, order: 'ASC' | 'DESC' = 'ASC'): AbstractSearch { - const field = this.schema.fieldByName(fieldName) - const dataStructure = this.schema.dataStructure + * Applies sorting for the query. + * @param fieldName The field to sort by. + * @param order The order of returned {@link Entity | Entities} Defaults to `ASC` (ascending) if not specified + * @returns this + */ + sortBy( + fieldName: EntityKeys, + order: "ASC" | "DESC" = "ASC", + ): AbstractSearch { + const field = this.schema.fieldByName(fieldName); + const dataStructure = this.schema.dataStructure; if (!field) { - const message = `'sortBy' was called on field '${String(fieldName)}' which is not defined in the Schema.` - console.error(message) - throw new RedisOmError(message) + const message = `'sortBy' was called on field '${String(fieldName)}' which is not defined in the Schema.`; + console.error(message); + throw new RedisOmError(message); } - const type = field.type - const markedSortable = field.sortable + const type = field.type; + const markedSortable = field.sortable; - const UNSORTABLE = ['point', 'string[]'] - const JSON_SORTABLE = ['number', 'text', 'date'] - const HASH_SORTABLE = ['string', 'boolean', 'number', 'text', 'date'] + const UNSORTABLE = ["point", "string[]"]; + const JSON_SORTABLE = ["number", "text", "date"]; + const HASH_SORTABLE = ["string", "boolean", "number", "text", "date"]; if (UNSORTABLE.includes(type)) { - const message = `'sortBy' was called on '${field.type}' field '${field.name}' which cannot be sorted.` - console.error(message) - throw new RedisOmError(message) + const message = `'sortBy' was called on '${field.type}' field '${field.name}' which cannot be sorted.`; + console.error(message); + throw new RedisOmError(message); } - if (dataStructure === 'JSON' && JSON_SORTABLE.includes(type) && !markedSortable) - console.warn(`'sortBy' was called on field '${field.name}' which is not marked as sortable in the Schema. This may result is slower searches. If possible, mark the field as sortable in the Schema.`) + if ( + dataStructure === "JSON" && + JSON_SORTABLE.includes(type) && + !markedSortable + ) + console.warn( + `'sortBy' was called on field '${field.name}' which is not marked as sortable in the Schema. This may result is slower searches. If possible, mark the field as sortable in the Schema.`, + ); - if (dataStructure === 'HASH' && HASH_SORTABLE.includes(type) && !markedSortable) - console.warn(`'sortBy' was called on field '${field.name}' which is not marked as sortable in the Schema. This may result is slower searches. If possible, mark the field as sortable in the Schema.`) + if ( + dataStructure === "HASH" && + HASH_SORTABLE.includes(type) && + !markedSortable + ) + console.warn( + `'sortBy' was called on field '${field.name}' which is not marked as sortable in the Schema. This may result is slower searches. If possible, mark the field as sortable in the Schema.`, + ); - this.sortOptions = { BY: field.name, DIRECTION: order } + this.sortOptions = { BY: field.name, DIRECTION: order }; - return this + return this; } /** @@ -138,7 +154,7 @@ export abstract class AbstractSearch> { * @returns The {@link Entity} with the minimal value */ async min(field: EntityKeys): Promise { - return await this.sortBy(field, 'ASC').first() + return await this.sortBy(field, "ASC").first(); } /** @@ -147,7 +163,7 @@ export abstract class AbstractSearch> { * @returns The entity ID with the minimal value */ async minId(field: EntityKeys): Promise { - return await this.sortBy(field, 'ASC').firstId() + return await this.sortBy(field, "ASC").firstId(); } /** @@ -156,7 +172,7 @@ export abstract class AbstractSearch> { * @returns The key name with the minimal value */ async minKey(field: EntityKeys): Promise { - return await this.sortBy(field, 'ASC').firstKey() + return await this.sortBy(field, "ASC").firstKey(); } /** @@ -165,7 +181,7 @@ export abstract class AbstractSearch> { * @returns The entity ID {@link Entity} with the maximal value */ async max(field: EntityKeys): Promise { - return await this.sortBy(field, 'DESC').first() + return await this.sortBy(field, "DESC").first(); } /** @@ -173,8 +189,8 @@ export abstract class AbstractSearch> { * @param field The field with the maximal value. * @returns The entity ID with the maximal value */ - async maxId(field: EntityKeys): Promise{ - return await this.sortBy(field, 'DESC').firstId() + async maxId(field: EntityKeys): Promise { + return await this.sortBy(field, "DESC").firstId(); } /** @@ -183,7 +199,7 @@ export abstract class AbstractSearch> { * @returns The key name with the maximal value */ async maxKey(field: EntityKeys): Promise { - return await this.sortBy(field, 'DESC').firstKey() + return await this.sortBy(field, "DESC").firstKey(); } /** @@ -191,8 +207,8 @@ export abstract class AbstractSearch> { * @returns */ async count(): Promise { - const searchResults = await this.callSearch() - return extractCountFromSearchResults(searchResults) + const searchResults = await this.callSearch(); + return extractCountFromSearchResults(searchResults); } /** @@ -202,8 +218,8 @@ export abstract class AbstractSearch> { * @returns An array of {@link Entity | Entities} matching the query. */ async page(offset: number, count: number): Promise { - const searchResults = await this.callSearch(offset, count) - return extractEntitiesFromSearchResults(this.schema, searchResults) + const searchResults = await this.callSearch(offset, count); + return extractEntitiesFromSearchResults(this.schema, searchResults); } /** @@ -212,9 +228,9 @@ export abstract class AbstractSearch> { * @param count The number of entity IDs to return. * @returns An array of strings matching the query. */ - async pageOfIds(offset: number, count: number): Promise { - const searchResults = await this.callSearch(offset, count, true) - return extractEntityIdsFromSearchResults(this.schema, searchResults) + async pageOfIds(offset: number, count: number): Promise { + const searchResults = await this.callSearch(offset, count, true); + return extractEntityIdsFromSearchResults(this.schema, searchResults); } /** @@ -224,32 +240,32 @@ export abstract class AbstractSearch> { * @returns An array of strings matching the query. */ async pageOfKeys(offset: number, count: number): Promise { - const searchResults = await this.callSearch(offset, count, true) - return extractKeyNamesFromSearchResults(searchResults) + const searchResults = await this.callSearch(offset, count, true); + return extractKeyNamesFromSearchResults(searchResults); } /** * Returns the first {@link Entity} that matches this query. */ async first(): Promise { - const foundEntity = await this.page(0, 1) - return foundEntity[0] ?? null + const foundEntity = await this.page(0, 1); + return foundEntity[0] ?? null; } /** * Returns the first entity ID that matches this query. */ - async firstId(): Promise { - const foundIds = await this.pageOfIds(0, 1) - return foundIds[0] ?? null + async firstId(): Promise { + const foundIds = await this.pageOfIds(0, 1); + return foundIds[0] ?? null; } /** * Returns the first key name that matches this query. */ - async firstKey(): Promise { - const foundKeys = await this.pageOfKeys(0, 1) - return foundKeys[0] ?? null + async firstKey(): Promise { + const foundKeys = await this.pageOfKeys(0, 1); + return foundKeys[0] ?? null; } /** @@ -267,7 +283,7 @@ export abstract class AbstractSearch> { * @returns An array of {@link Entity | Entities} matching the query. */ async all(options = { pageSize: 10 }): Promise { - return this.allThings(this.page, options) as Promise + return this.allThings(this.page, options) as Promise; } /** @@ -285,7 +301,7 @@ export abstract class AbstractSearch> { * @returns An array of entity IDs matching the query. */ async allIds(options = { pageSize: 10 }): Promise { - return this.allThings(this.pageOfIds, options) as Promise + return this.allThings(this.pageOfIds, options) as Promise; } /** @@ -303,7 +319,7 @@ export abstract class AbstractSearch> { * @returns An array of key names matching the query. */ async allKeys(options = { pageSize: 10 }): Promise { - return await this.allThings(this.pageOfKeys, options) as string[] + return (await this.allThings(this.pageOfKeys, options)) as string[]; } /** @@ -311,164 +327,172 @@ export abstract class AbstractSearch> { * @returns this */ get return(): AbstractSearch { - return this + return this; } /** * Alias for {@link Search.min}. */ async returnMin(field: EntityKeys): Promise { - return await this.min(field) + return await this.min(field); } /** * Alias for {@link Search.minId}. */ async returnMinId(field: EntityKeys): Promise { - return await this.minId(field) + return await this.minId(field); } /** * Alias for {@link Search.minKey}. */ async returnMinKey(field: EntityKeys): Promise { - return await this.minKey(field) + return await this.minKey(field); } /** * Alias for {@link Search.max}. */ async returnMax(field: EntityKeys): Promise { - return await this.max(field) + return await this.max(field); } /** * Alias for {@link Search.maxId}. */ async returnMaxId(field: EntityKeys): Promise { - return await this.maxId(field) + return await this.maxId(field); } /** * Alias for {@link Search.maxKey}. */ async returnMaxKey(field: EntityKeys): Promise { - return await this.maxKey(field) + return await this.maxKey(field); } /** * Alias for {@link Search.count}. */ async returnCount(): Promise { - return await this.count() + return await this.count(); } /** * Alias for {@link Search.page}. */ async returnPage(offset: number, count: number): Promise { - return await this.page(offset, count) + return await this.page(offset, count); } /** * Alias for {@link Search.pageOfIds}. */ async returnPageOfIds(offset: number, count: number): Promise { - return await this.pageOfIds(offset, count) + return await this.pageOfIds(offset, count); } /** * Alias for {@link Search.pageOfKeys}. */ async returnPageOfKeys(offset: number, count: number): Promise { - return await this.pageOfKeys(offset, count) + return await this.pageOfKeys(offset, count); } /** * Alias for {@link Search.first}. */ async returnFirst(): Promise { - return await this.first() + return await this.first(); } /** * Alias for {@link Search.firstId}. */ async returnFirstId(): Promise { - return await this.firstId() + return await this.firstId(); } /** * Alias for {@link Search.firstKey}. */ async returnFirstKey(): Promise { - return await this.firstKey() + return await this.firstKey(); } /** * Alias for {@link Search.all}. */ - async returnAll(options = { pageSize: 10 }): Promise { - return await this.all(options) + async returnAll(options = { pageSize: 10 }): Promise { + return await this.all(options); } /** * Alias for {@link Search.allIds}. */ async returnAllIds(options = { pageSize: 10 }): Promise { - return await this.allIds(options) + return await this.allIds(options); } /** * Alias for {@link Search.allKeys}. */ async returnAllKeys(options = { pageSize: 10 }): Promise { - return await this.allKeys(options) + return await this.allKeys(options); } - private async allThings(pageFn: Function, options = { pageSize: 10 }): Promise { - const things = [] - let offset = 0 - const pageSize = options.pageSize + private async allThings( + pageFn: Function, + options = { pageSize: 10 }, + ): Promise { + const things = []; + let offset = 0; + const pageSize = options.pageSize; while (true) { - const foundThings = await pageFn.call(this, offset, pageSize) - things.push(...foundThings) - if (foundThings.length < pageSize) break - offset += pageSize + const foundThings = await pageFn.call(this, offset, pageSize); + things.push(...foundThings); + if (foundThings.length < pageSize) break; + offset += pageSize; } - return things + return things; } - private async callSearch(offset = 0, count = 0, keysOnly = false): Promise { - - const dataStructure = this.schema.dataStructure - const indexName = this.schema.indexName - const query = this.query + private async callSearch( + offset = 0, + count = 0, + keysOnly = false, + ): Promise { + const dataStructure = this.schema.dataStructure; + const indexName = this.schema.indexName; + const query = this.query; const options: SearchOptions = { - LIMIT: { from: offset, size: count } - } + LIMIT: { from: offset, size: count }, + }; - if (this.sortOptions !== undefined) options.SORTBY = this.sortOptions + if (this.sortOptions !== undefined) options.SORTBY = this.sortOptions; if (keysOnly) { - options.RETURN = [] - } else if (dataStructure === 'JSON') { - options.RETURN = '$' + options.RETURN = []; + } else if (dataStructure === "JSON") { + options.RETURN = "$"; } - let searchResults + let searchResults; try { - searchResults = await this.client.search(indexName, query, options) + searchResults = await this.client.search(indexName, query, options); } catch (error) { - const message = (error as Error).message + const message = (error as Error).message; if (message.startsWith("Syntax error")) { - throw new SearchError(`The query to RediSearch had a syntax error: "${message}".\nThis is often the result of using a stop word in the query. Either change the query to not use a stop word or change the stop words in the schema definition. You can check the RediSearch source for the default stop words at: https://github.com/RediSearch/RediSearch/blob/master/src/stopwords.h.`) + throw new SearchError( + `The query to RediSearch had a syntax error: "${message}".\nThis is often the result of using a stop word in the query. Either change the query to not use a stop word or change the stop words in the schema definition. You can check the RediSearch source for the default stop words at: https://github.com/RediSearch/RediSearch/blob/master/src/stopwords.h.`, + ); } - throw error + throw error; } - return searchResults + return searchResults; } } @@ -478,34 +502,37 @@ export abstract class AbstractSearch> { * installed. * @template TEntity The type of {@link Entity} being sought. */ -export class RawSearch> extends AbstractSearch { - private readonly rawQuery: string +export class RawSearch< + T extends Entity = Record, +> extends AbstractSearch { + private readonly rawQuery: string; /** @internal */ - constructor(schema: Schema, client: Client, query: string = '*') { - super(schema, client) - this.rawQuery = query + constructor(schema: Schema, client: Client, query: string = "*") { + super(schema, client); + this.rawQuery = query; } /** @internal */ get query(): string { - return this.rawQuery + return this.rawQuery; } } - /** * Entry point to fluent search. This is the default Redis OM experience. * Requires that RediSearch (and optionally RedisJSON) be installed. * @template TEntity The type of {@link Entity} being sought. */ -export class Search> extends AbstractSearch { - private rootWhere?: Where +export class Search< + T extends Entity = Record, +> extends AbstractSearch { + private rootWhere?: Where; /** @internal */ get query(): string { - if (this.rootWhere === undefined) return '*' - return `${this.rootWhere.toString()}` + if (this.rootWhere === undefined) return "*"; + return `${this.rootWhere.toString()}`; } /** @@ -514,7 +541,7 @@ export class Search> extends AbstractSear * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - where(field: EntityKeys): WhereField + where(field: EntityKeys): WhereField; /** * Sets up a nested search. If there are multiple calls to {@link Search.where}, @@ -522,9 +549,11 @@ export class Search> extends AbstractSear * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - where(subSearchFn: SubSearchFunction): Search - where(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { - return this.anyWhere(WhereAnd, fieldOrFn) + where(subSearchFn: SubSearchFunction): Search; + where( + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + return this.anyWhere(WhereAnd, fieldOrFn); } /** @@ -532,16 +561,18 @@ export class Search> extends AbstractSear * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - and(field: EntityKeys): WhereField + and(field: EntityKeys): WhereField; /** * Sets up a nested search as a logical AND. * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - and(subSearchFn: SubSearchFunction): Search - and(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { - return this.anyWhere(WhereAnd, fieldOrFn) + and(subSearchFn: SubSearchFunction): Search; + and( + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + return this.anyWhere(WhereAnd, fieldOrFn); } /** @@ -549,71 +580,88 @@ export class Search> extends AbstractSear * @param field The field to filter on. * @returns A subclass of {@link WhereField} matching the type of the field. */ - or(field: EntityKeys): WhereField + or(field: EntityKeys): WhereField; /** * Sets up a nested search as a logical OR. * @param subSearchFn A function that takes a {@link Search} and returns another {@link Search}. * @returns `this`. */ - or(subSearchFn: SubSearchFunction): Search - or(fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { - return this.anyWhere(WhereOr, fieldOrFn) + or(subSearchFn: SubSearchFunction): Search; + or( + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + return this.anyWhere(WhereOr, fieldOrFn); } - private anyWhere(ctor: AndOrConstructor, fieldOrFn: EntityKeys | SubSearchFunction): WhereField | Search { - if (typeof fieldOrFn === 'function') { - return this.anyWhereForFunction(ctor, fieldOrFn) + private anyWhere( + ctor: AndOrConstructor, + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + if (typeof fieldOrFn === "function") { + return this.anyWhereForFunction(ctor, fieldOrFn); } else { - return this.anyWhereForField(ctor, fieldOrFn) + return this.anyWhereForField(ctor, fieldOrFn); } } - private anyWhereForField(ctor: AndOrConstructor, field: EntityKeys): WhereField { - const where = this.createWhere(field) + private anyWhereForField( + ctor: AndOrConstructor, + field: EntityKeys, + ): WhereField { + const where = this.createWhere(field); if (this.rootWhere === undefined) { - this.rootWhere = where + this.rootWhere = where; } else { - this.rootWhere = new ctor(this.rootWhere, where) + this.rootWhere = new ctor(this.rootWhere, where); } - return where + return where; } - private anyWhereForFunction(ctor: AndOrConstructor, subSearchFn: SubSearchFunction): Search { - const search = new Search(this.schema, this.client) - const subSearch = subSearchFn(search) + private anyWhereForFunction( + ctor: AndOrConstructor, + subSearchFn: SubSearchFunction, + ): Search { + const search = new Search(this.schema, this.client); + const subSearch = subSearchFn(search); if (subSearch.rootWhere === undefined) { - throw new SearchError("Sub-search without a root where was somehow defined.") + throw new SearchError( + "Sub-search without a root where was somehow defined.", + ); } else { if (this.rootWhere === undefined) { - this.rootWhere = subSearch.rootWhere + this.rootWhere = subSearch.rootWhere; } else { - this.rootWhere = new ctor(this.rootWhere, subSearch.rootWhere) + this.rootWhere = new ctor(this.rootWhere, subSearch.rootWhere); } } - return this + return this; } private createWhere(fieldName: EntityKeys): WhereField { - const field = this.schema.fieldByName(fieldName) - - if (field === null) throw new FieldNotInSchema(String(fieldName)) - - if (field.type === 'boolean' && this.schema.dataStructure === 'HASH') return new WhereHashBoolean(this, field) - if (field.type === 'boolean' && this.schema.dataStructure === 'JSON') return new WhereJsonBoolean(this, field) - if (field.type === 'date') return new WhereDate(this, field) - if (field.type === 'number') return new WhereNumber(this, field) - if (field.type === 'number[]') return new WhereNumber(this, field) - if (field.type === 'point') return new WherePoint(this, field) - if (field.type === 'text') return new WhereText(this, field) - if (field.type === 'string') return new WhereString(this, field) - if (field.type === 'string[]') return new WhereStringArray(this, field) - - // @ts-ignore: This is a trap for JavaScript - throw new RedisOmError(`The field type of '${fieldDef.type}' is not a valid field type. Valid types include 'boolean', 'date', 'number', 'point', 'string', and 'string[]'.`) + const field = this.schema.fieldByName(fieldName); + + if (field === null) throw new FieldNotInSchema(String(fieldName)); + + if (field.type === "boolean" && this.schema.dataStructure === "HASH") + return new WhereHashBoolean(this, field); + if (field.type === "boolean" && this.schema.dataStructure === "JSON") + return new WhereJsonBoolean(this, field); + if (field.type === "date") return new WhereDate(this, field); + if (field.type === "number") return new WhereNumber(this, field); + if (field.type === "number[]") return new WhereNumber(this, field); + if (field.type === "point") return new WherePoint(this, field); + if (field.type === "text") return new WhereText(this, field); + if (field.type === "string") return new WhereString(this, field); + if (field.type === "string[]") return new WhereStringArray(this, field); + + throw new RedisOmError( + // @ts-ignore: This is a trap for JavaScript + `The field type of '${fieldDef.type}' is not a valid field type. Valid types include 'boolean', 'date', 'number', 'point', 'string', and 'string[]'.`, + ); } } From 94f6b8fa4ad3098ea598323dfe32146cfd496e7f Mon Sep 17 00:00:00 2001 From: Dennis Lysenko Date: Wed, 10 Jul 2024 11:52:28 -0400 Subject: [PATCH 5/5] fixed `allThings` typing --- lib/search/search.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/search/search.ts b/lib/search/search.ts index 826b1d15..8d377944 100644 --- a/lib/search/search.ts +++ b/lib/search/search.ts @@ -319,7 +319,7 @@ export abstract class AbstractSearch> { * @returns An array of key names matching the query. */ async allKeys(options = { pageSize: 10 }): Promise { - return (await this.allThings(this.pageOfKeys, options)) as string[]; + return this.allThings(this.pageOfKeys, options); } /** @@ -442,22 +442,24 @@ export abstract class AbstractSearch> { return await this.allKeys(options); } - private async allThings( - pageFn: Function, + private async allThings( + pageFn: (offset: number, pageSide: number) => Promise, options = { pageSize: 10 }, - ): Promise { - const things = []; + ): Promise { + // TypeScript is just being mean in this function. The internal logic will be fine in runtime, + // However, it is important during future changes to double check your work. + let things: unknown[] = []; let offset = 0; const pageSize = options.pageSize; while (true) { const foundThings = await pageFn.call(this, offset, pageSize); - things.push(...foundThings); + things.push(...(foundThings as unknown[])); if (foundThings.length < pageSize) break; offset += pageSize; } - return things; + return things as R; } private async callSearch(