diff --git a/lib/client/client.ts b/lib/client/client.ts index 3cdaf44..5b76440 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 68fef8f..a9d381d 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,13 +12,17 @@ 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 } /** 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 = { diff --git a/lib/repository/repository.ts b/lib/repository/repository.ts index 8f2d7cd..088e780 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 fabc270..01e1552 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 5ec8f07..9adf555 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 041d5fa..595d409 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 } from "../entity" +import {Entity, EntityKeys} from "../entity" -import { IdStrategy, DataStructure, StopWordOptions, SchemaOptions } from './options' +import {DataStructure, IdStrategy, SchemaOptions, StopWordOptions} from './options' -import { SchemaDefinition } from './definitions' -import { Field } from './field' -import { InvalidSchema } from '../error' +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 @@ -75,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 } @@ -110,7 +120,7 @@ export class Schema { */ async generateId(): Promise { const ulidStrategy = () => ulid() - return await (this.#options?.idStrategy ?? ulidStrategy)() + return await (this.#options?.idStrategy ?? ulidStrategy)(); } /** @@ -133,8 +143,9 @@ export class Schema { } #createFields() { - return Object.entries(this.#definition).forEach(([fieldName, fieldDef]) => { - const field = new Field(fieldName, fieldDef) + const entries = Object.entries(this.#definition) as [EntityKeys, FieldDefinition][]; + return entries.forEach(([fieldName, fieldDef]) => { + const field = new Field(String(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 e10c9ad..8de484e 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 8bb5bc8..8d37794 100644 --- a/lib/search/search.ts +++ b/lib/search/search.ts @@ -1,74 +1,80 @@ -import { SearchOptions } from "redis" - -import { Client } from "../client" -import { Entity } 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. * @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 * 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 + protected client: Client; /** @internal */ - protected sortOptions?: SortOptions + protected sortOptions?: SortOptions; /** @internal */ - constructor(schema: Schema, client: Client) { - this.schema = schema - this.client = client + constructor(schema: Schema, client: Client) { + this.schema = schema; + this.client = client; } /** @internal */ - abstract get query(): string + abstract get query(): string; /** * Applies an ascending sort to the query. * @param field The field to sort by. * @returns this */ - sortAscending(field: string): AbstractSearch { - return this.sortBy(field, 'ASC') + sortAscending(field: EntityKeys): AbstractSearch { + return this.sortBy(field, "ASC"); } /** * Alias for {@link Search.sortDescending}. */ - sortDesc(field: string): AbstractSearch { - return this.sortDescending(field) + sortDesc(field: EntityKeys): AbstractSearch { + return this.sortDescending(field); } /** @@ -76,55 +82,70 @@ export abstract class AbstractSearch { * @param field The field to sort by. * @returns this */ - sortDescending(field: string): AbstractSearch { - return this.sortBy(field, 'DESC') + sortDescending(field: EntityKeys): AbstractSearch { + return this.sortBy(field, "DESC"); } /** * Alias for {@link Search.sortAscending}. */ - sortAsc(field: string): AbstractSearch { - return this.sortAscending(field) + sortAsc(field: EntityKeys): AbstractSearch { + return this.sortAscending(field); } /** - * Applies sorting for the query. - * @param field 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 { - 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 '${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; } /** @@ -132,8 +153,8 @@ 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 { - return await this.sortBy(field, 'ASC').first() + async min(field: EntityKeys): Promise { + return await this.sortBy(field, "ASC").first(); } /** @@ -141,8 +162,8 @@ 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 { - return await this.sortBy(field, 'ASC').firstId() + async minId(field: EntityKeys): Promise { + return await this.sortBy(field, "ASC").firstId(); } /** @@ -150,8 +171,8 @@ 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 { - return await this.sortBy(field, 'ASC').firstKey() + async minKey(field: EntityKeys): Promise { + return await this.sortBy(field, "ASC").firstKey(); } /** @@ -159,8 +180,8 @@ 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 { - return await this.sortBy(field, 'DESC').first() + async max(field: EntityKeys): Promise { + return await this.sortBy(field, "DESC").first(); } /** @@ -168,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: string): Promise{ - return await this.sortBy(field, 'DESC').firstId() + async maxId(field: EntityKeys): Promise { + return await this.sortBy(field, "DESC").firstId(); } /** @@ -177,8 +198,8 @@ 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 { - return await this.sortBy(field, 'DESC').firstKey() + async maxKey(field: EntityKeys): Promise { + return await this.sortBy(field, "DESC").firstKey(); } /** @@ -186,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); } /** @@ -196,9 +217,9 @@ 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 { - const searchResults = await this.callSearch(offset, count) - return extractEntitiesFromSearchResults(this.schema, searchResults) + async page(offset: number, count: number): Promise { + const searchResults = await this.callSearch(offset, count); + return extractEntitiesFromSearchResults(this.schema, searchResults); } /** @@ -207,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); } /** @@ -219,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 + async first(): Promise { + 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. + * 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; } /** @@ -261,8 +282,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; } /** @@ -280,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; } /** @@ -298,172 +319,182 @@ 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 this.allThings(this.pageOfKeys, options); } /** * Returns the current instance. Syntactic sugar to make your code more fluent. * @returns this */ - get return(): AbstractSearch { - return this + get return(): AbstractSearch { + return this; } /** * Alias for {@link Search.min}. */ - async returnMin(field: string): Promise { - return await this.min(field) + async returnMin(field: EntityKeys): Promise { + return await this.min(field); } /** * Alias for {@link Search.minId}. */ - async returnMinId(field: string): Promise { - return await this.minId(field) + async returnMinId(field: EntityKeys): Promise { + return await this.minId(field); } /** * Alias for {@link Search.minKey}. */ - async returnMinKey(field: string): Promise { - return await this.minKey(field) + async returnMinKey(field: EntityKeys): Promise { + return await this.minKey(field); } /** * Alias for {@link Search.max}. */ - async returnMax(field: string): Promise { - return await this.max(field) + async returnMax(field: EntityKeys): Promise { + return await this.max(field); } /** * Alias for {@link Search.maxId}. */ - async returnMaxId(field: string): Promise { - return await this.maxId(field) + async returnMaxId(field: EntityKeys): Promise { + return await this.maxId(field); } /** * Alias for {@link Search.maxKey}. */ - async returnMaxKey(field: string): Promise { - return await this.maxKey(field) + async returnMaxKey(field: EntityKeys): Promise { + 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) + async returnPage(offset: number, count: number): Promise { + 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() + async returnFirst(): Promise { + 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: (offset: number, pageSide: number) => Promise, + options = { pageSize: 10 }, + ): 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) - if (foundThings.length < pageSize) break - offset += pageSize + const foundThings = await pageFn.call(this, offset, pageSize); + things.push(...(foundThings as unknown[])); + if (foundThings.length < pageSize) break; + offset += pageSize; } - return things + return things as R; } - private async callSearch(offset = 0, count = 0, keysOnly = false) { - - 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; } } @@ -473,34 +504,37 @@ 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< + 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()}`; } /** @@ -509,7 +543,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,9 +551,11 @@ 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 { - return this.anyWhere(WhereAnd, fieldOrFn) + where(subSearchFn: SubSearchFunction): Search; + where( + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + return this.anyWhere(WhereAnd, fieldOrFn); } /** @@ -527,16 +563,18 @@ 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 { - return this.anyWhere(WhereAnd, fieldOrFn) + and(subSearchFn: SubSearchFunction): Search; + and( + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + return this.anyWhere(WhereAnd, fieldOrFn); } /** @@ -544,71 +582,88 @@ 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 { - 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: string | SubSearchFunction): WhereField | Search { - if (typeof fieldOrFn === 'string') { - return this.anyWhereForField(ctor, fieldOrFn) + private anyWhere( + ctor: AndOrConstructor, + fieldOrFn: EntityKeys | SubSearchFunction, + ): WhereField | Search { + if (typeof fieldOrFn === "function") { + return this.anyWhereForFunction(ctor, fieldOrFn); } else { - return this.anyWhereForFunction(ctor, fieldOrFn) + return this.anyWhereForField(ctor, fieldOrFn); } } - private anyWhereForField(ctor: AndOrConstructor, field: string): 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: string): WhereField { - const field = this.schema.fieldByName(fieldName) + 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) - 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) + 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[]'.`) + 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[]'.`, + ); } } diff --git a/lib/search/where-boolean.ts b/lib/search/where-boolean.ts index ea58b80..7b6bd17 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 ad1b547..58c8c46 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 12b563c..6b67134 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 cabe670..c53d92b 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 76ba1ae..9dd8630 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 93102e3..9f21ce4 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 40cd1d5..1440718 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 423bc2e..7081925 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.") } }