UNPKG

redis-om

Version:

Object mapping, and more, for Redis and Node.js. Written in TypeScript.

1,692 lines (1,663 loc) 60 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // lib/index.ts var lib_exports = {}; __export(lib_exports, { AbstractSearch: () => AbstractSearch, ArrayHashInput: () => ArrayHashInput, Circle: () => Circle, Client: () => Client, EntityId: () => EntityId, EntityKeyName: () => EntityKeyName, Field: () => Field, FieldNotInSchema: () => FieldNotInSchema, InvalidHashInput: () => InvalidHashInput, InvalidHashValue: () => InvalidHashValue, InvalidInput: () => InvalidInput, InvalidJsonInput: () => InvalidJsonInput, InvalidJsonValue: () => InvalidJsonValue, InvalidSchema: () => InvalidSchema, InvalidValue: () => InvalidValue, NestedHashInput: () => NestedHashInput, NullJsonInput: () => NullJsonInput, NullJsonValue: () => NullJsonValue, PointOutOfRange: () => PointOutOfRange, RawSearch: () => RawSearch, RedisOmError: () => RedisOmError, Repository: () => Repository, Schema: () => Schema, Search: () => Search, SearchError: () => SearchError, SemanticSearchError: () => SemanticSearchError, Where: () => Where, WhereField: () => WhereField }); module.exports = __toCommonJS(lib_exports); // lib/client/client.ts var import_redis2 = require("redis"); // lib/entity/entity.ts var EntityId = Symbol("entityId"); var EntityKeyName = Symbol("entityKeyName"); // lib/indexer/index-builder.ts var import_redis = require("redis"); var entryBuilders = { HASH: addHashEntry, JSON: addJsonEntry }; function buildRediSearchSchema(schema) { const addEntry = entryBuilders[schema.dataStructure]; return schema.fields.reduce(addEntry, {}); } function addHashEntry(schema, field) { const hashField = field.hashField; switch (field.type) { case "boolean": schema[hashField] = buildHashBoolean(field); break; case "date": schema[hashField] = buildDateNumber(field); break; case "number": schema[hashField] = buildDateNumber(field); break; case "point": schema[hashField] = buildPoint(field); break; case "string[]": case "string": schema[hashField] = buildHashString(field); break; case "text": schema[hashField] = buildText(field); break; } return schema; } function addJsonEntry(schema, field) { const jsonPath = field.jsonPath; switch (field.type) { case "boolean": schema[jsonPath] = buildJsonBoolean(field); break; case "date": schema[jsonPath] = buildDateNumber(field); break; case "number": case "number[]": schema[jsonPath] = buildDateNumber(field); break; case "point": schema[jsonPath] = buildPoint(field); break; case "string": case "string[]": schema[jsonPath] = buildJsonString(field); break; case "text": schema[jsonPath] = buildText(field); break; } return schema; } function buildHashBoolean(field) { const schemaField = { type: import_redis.SchemaFieldTypes.TAG, AS: field.name }; addSortable(schemaField, field); addIndexed(schemaField, field); return schemaField; } function buildJsonBoolean(field) { if (field.sortable) console.warn(`You have marked a boolean field as sortable but RediSearch doesn't support the SORTABLE argument on a TAG for JSON. Ignored.`); const schemaField = { type: import_redis.SchemaFieldTypes.TAG, AS: field.name }; addIndexed(schemaField, field); return schemaField; } function buildDateNumber(field) { const schemaField = { type: import_redis.SchemaFieldTypes.NUMERIC, AS: field.name }; addSortable(schemaField, field); addIndexed(schemaField, field); return schemaField; } function buildPoint(field) { const schemaField = { type: import_redis.SchemaFieldTypes.GEO, AS: field.name }; addIndexed(schemaField, field); return schemaField; } function buildHashString(field) { const schemaField = { type: import_redis.SchemaFieldTypes.TAG, AS: field.name }; addCaseInsensitive(schemaField, field), addSeparable(schemaField, field), addSortable(schemaField, field); addIndexed(schemaField, field); return schemaField; } function buildJsonString(field) { if (field.sortable) console.warn(`You have marked a ${field.type} field as sortable but RediSearch doesn't support the SORTABLE argument on a TAG for JSON. Ignored.`); const schemaField = { type: import_redis.SchemaFieldTypes.TAG, AS: field.name }; addCaseInsensitive(schemaField, field), addSeparable(schemaField, field), addIndexed(schemaField, field); return schemaField; } function buildText(field) { const schemaField = { type: import_redis.SchemaFieldTypes.TEXT, AS: field.name }; addSortable(schemaField, field); addStemming(schemaField, field); addIndexed(schemaField, field); addPhonetic(schemaField, field); addWeight(schemaField, field); return schemaField; } function addCaseInsensitive(schemaField, field) { if (field.caseSensitive) schemaField.CASESENSITIVE = true; } function addIndexed(schemaField, field) { if (!field.indexed) schemaField.NOINDEX = true; } function addStemming(schemaField, field) { if (!field.stemming) schemaField.NOSTEM = true; } function addPhonetic(schemaField, field) { if (field.matcher) schemaField.PHONETIC = field.matcher; } function addSeparable(schemaField, field) { schemaField.SEPARATOR = field.separator; } function addSortable(schemaField, field) { if (field.normalized) { if (field.sortable) schemaField.SORTABLE = true; } else { schemaField.SORTABLE = "UNF"; } } function addWeight(schemaField, field) { if (field.weight) schemaField.WEIGHT = field.weight; } // lib/error/redis-om-error.ts var RedisOmError = class extends Error { }; // lib/error/invalid-input.ts var InvalidInput = class extends RedisOmError { }; var NullJsonInput = class extends InvalidInput { #field; constructor(field) { const message = `Null or undefined found in field '${field.name}' of type '${field.type}' in JSON at '${field.jsonPath}'.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } get jsonPath() { return this.#field.jsonPath; } }; var InvalidJsonInput = class extends InvalidInput { #field; constructor(field) { const message = `Unexpected value for field '${field.name}' of type '${field.type}' in JSON at '${field.jsonPath}'.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } get jsonPath() { return this.#field.jsonPath; } }; var InvalidHashInput = class extends InvalidInput { #field; constructor(field) { const message = `Unexpected value for field '${field.name}' of type '${field.type}' in Hash.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } }; var NestedHashInput = class extends InvalidInput { #property; constructor(property) { const message = `Unexpected object in Hash at property '${property}'. You can not store a nested object in a Redis Hash.`; super(message); this.#property = property; } get field() { return this.#property; } }; var ArrayHashInput = class extends InvalidInput { #property; constructor(property) { const message = `Unexpected array in Hash at property '${property}'. You can not store an array in a Redis Hash without defining it in the Schema.`; super(message); this.#property = property; } get field() { return this.#property; } }; // lib/error/invalid-schema.ts var InvalidSchema = class extends RedisOmError { }; // lib/error/invalid-value.ts var InvalidValue = class extends RedisOmError { }; var NullJsonValue = class extends InvalidValue { #field; constructor(field) { const message = `Null or undefined found in field '${field.name}' of type '${field.type}' from JSON path '${field.jsonPath}' in Redis.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } get jsonPath() { return this.#field.jsonPath; } }; var InvalidJsonValue = class extends InvalidValue { #field; constructor(field) { const message = `Unexpected value for field '${field.name}' of type '${field.type}' from JSON path '${field.jsonPath}' in Redis.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } get jsonPath() { return this.#field.jsonPath; } }; var InvalidHashValue = class extends InvalidValue { #field; constructor(field) { const message = `Unexpected value for field '${field.name}' of type '${field.type}' from Hash field '${field.hashField}' read from Redis.`; super(message); this.#field = field; } get fieldName() { return this.#field.name; } get fieldType() { return this.#field.type; } get hashField() { return this.#field.hashField; } }; // lib/error/point-out-of-range.ts var PointOutOfRange = class extends RedisOmError { #latitude; #longitude; constructor(point) { super("Points must be between \xB185.05112878 latitude and \xB1180 longitude."); this.#longitude = point.longitude; this.#latitude = point.latitude; } get point() { return { longitude: this.#longitude, latitude: this.#latitude }; } }; // lib/error/search-error.ts var SearchError = class extends RedisOmError { }; var SemanticSearchError = class extends SearchError { }; var FieldNotInSchema = class extends SearchError { #field; constructor(fieldName) { super(`The field '${fieldName}' is not part of the schema and thus cannot be used to search.`); this.#field = fieldName; } get field() { return this.#field; } }; // lib/transformer/transformer-common.ts var isNull = (value) => value === null; var isDefined = (value) => value !== void 0; var isUndefined = (value) => value === void 0; var isNullish = (value) => value === void 0 || value === null; var isNotNullish = (value) => value !== void 0 && value !== null; var isBoolean = (value) => typeof value === "boolean"; var isNumber = (value) => typeof value === "number"; var isString = (value) => typeof value === "string"; var isDate = (value) => value instanceof Date; var isDateString = (value) => isString(value) && !isNaN(new Date(value).getTime()); var isArray = (value) => Array.isArray(value); var isObject = (value) => value !== null && typeof value === "object" && !isArray(value) && !isDate(value); var isPoint = (value) => isObject(value) && Object.keys(value).length === 2 && typeof value.latitude === "number" && typeof value.longitude === "number"; var isNumberString = (value) => !isNaN(Number(value)); var isPointString = (value) => /^-?\d+(\.\d*)?,-?\d+(\.\d*)?$/.test(value); var isValidPoint = (value) => Math.abs(value.latitude) <= 85.05112878 && Math.abs(value.longitude) <= 180; var convertBooleanToString = (value) => value ? "1" : "0"; var convertNumberToString = (value) => value.toString(); var convertStringToNumber = (value) => Number.parseFloat(value); var convertDateToEpoch = (value) => value.getTime() / 1e3; var convertDateToString = (value) => convertDateToEpoch(value).toString(); var convertEpochDateToString = (value) => convertNumberToString(value); var convertIsoDateToEpoch = (value) => convertDateToEpoch(new Date(value)); var convertIsoDateToString = (value) => convertDateToString(new Date(value)); var convertEpochStringToDate = (value) => new Date(convertEpochToDate(convertStringToNumber(value))); var convertEpochToDate = (value) => new Date(value * 1e3); var convertPointToString = (value) => { if (isValidPoint(value)) return `${value.longitude},${value.latitude}`; throw new PointOutOfRange(value); }; var convertStringToPoint = (value) => { const [longitude, latitude] = value.split(",").map(convertStringToNumber); return { longitude, latitude }; }; // lib/transformer/from-hash-transformer.ts function fromRedisHash(schema, hashData) { const data = { ...hashData }; schema.fields.forEach((field) => { if (field.hashField) delete data[field.hashField]; const value = hashData[field.hashField]; if (isNotNullish(value)) { data[field.name] = convertKnownValueFromString(field, value); } else if (isNullish(value) && field.type === "string[]") { data[field.name] = []; } }); return data; } function convertKnownValueFromString(field, value) { switch (field.type) { case "boolean": if (value === "1") return true; if (value === "0") return false; throw new InvalidHashValue(field); case "number": if (isNumberString(value)) return convertStringToNumber(value); throw new InvalidHashValue(field); case "date": if (isNumberString(value)) return convertEpochStringToDate(value); throw new InvalidHashValue(field); case "point": if (isPointString(value)) return convertStringToPoint(value); throw new InvalidHashValue(field); case "string": case "text": return value; case "string[]": return convertStringToStringArray(value, field.separator); } } var convertStringToStringArray = (value, separator) => value.split(separator); // lib/transformer/from-json-transformer.ts var import_jsonpath_plus = require("jsonpath-plus"); var import_just_clone = __toESM(require("just-clone")); function fromRedisJson(schema, json) { const data = (0, import_just_clone.default)(json); convertFromRedisJsonKnown(schema, data); return data; } function convertFromRedisJsonKnown(schema, data) { schema.fields.forEach((field) => { const path = field.jsonPath; const results = (0, import_jsonpath_plus.JSONPath)({ resultType: "all", path, json: data }); if (field.isArray) { convertKnownResultsFromJson(field, results); return; } if (results.length === 1) { convertKnownResultFromJson(field, results[0]); return; } if (results.length > 1) throw new InvalidJsonValue(field); }); } function convertKnownResultFromJson(field, result) { const { value, parent, parentProperty } = result; parent[parentProperty] = convertKnownValueFromJson(field, value); } function convertKnownResultsFromJson(field, results) { results.forEach((result) => { const { value, parent, parentProperty } = result; parent[parentProperty] = convertKnownArrayValueFromJson(field, value); }); } function convertKnownValueFromJson(field, value) { if (isNull(value)) return value; switch (field.type) { case "boolean": if (isBoolean(value)) return value; throw new InvalidJsonValue(field); case "number": if (isNumber(value)) return value; throw new InvalidJsonValue(field); case "date": if (isNumber(value)) return convertEpochToDate(value); throw new InvalidJsonValue(field); case "point": if (isPointString(value)) return convertStringToPoint(value); throw new InvalidJsonValue(field); case "string": case "text": if (isBoolean(value)) return value.toString(); if (isNumber(value)) return value.toString(); if (isString(value)) return value; throw new InvalidJsonValue(field); } } function convertKnownArrayValueFromJson(field, value) { if (isNull(value)) throw new NullJsonValue(field); switch (field.type) { case "string[]": if (isBoolean(value)) return value.toString(); if (isNumber(value)) return value.toString(); if (isString(value)) return value; throw new InvalidJsonValue(field); case "number[]": if (isNumber(value)) return value; throw new InvalidJsonValue(field); } } // lib/transformer/to-hash-transformer.ts function toRedisHash(schema, data) { const hashData = {}; Object.entries(data).forEach(([key, value]) => { if (isNotNullish(value)) { const field = schema.fieldByName(key); const hashField = field ? field.hashField : key; if (field && field.type === "string[]" && isArray(value) && value.length === 0) { } else { hashData[hashField] = field ? convertKnownValueToString2(field, value) : convertUnknownValueToString(key, value); } } }); return hashData; } function convertKnownValueToString2(field, value) { switch (field.type) { case "boolean": if (isBoolean(value)) return convertBooleanToString(value); throw new InvalidHashInput(field); case "number": if (isNumber(value)) return convertNumberToString(value); throw new InvalidHashInput(field); case "date": if (isNumber(value)) return convertEpochDateToString(value); if (isDate(value)) return convertDateToString(value); if (isDateString(value)) return convertIsoDateToString(value); throw new InvalidHashInput(field); case "point": if (isPoint(value)) return convertPointToString(value); throw new InvalidHashInput(field); case "string": case "text": if (isBoolean(value)) return value.toString(); if (isNumber(value)) return value.toString(); if (isString(value)) return value; throw new InvalidHashInput(field); case "string[]": if (isArray(value)) return convertStringArrayToString(value, field.separator); throw new InvalidHashInput(field); default: throw new RedisOmError(`Expected a valid field type but received: ${field.type}`); } } function convertUnknownValueToString(key, value) { if (isBoolean(value)) return convertBooleanToString(value); if (isNumber(value)) return convertNumberToString(value); if (isDate(value)) return convertDateToString(value); if (isPoint(value)) return convertPointToString(value); if (isArray(value)) throw new ArrayHashInput(key); if (isObject(value)) throw new NestedHashInput(key); return value.toString(); } var convertStringArrayToString = (value, separator) => value.join(separator); // lib/transformer/to-json-transformer.ts var import_jsonpath_plus2 = require("jsonpath-plus"); var import_just_clone2 = __toESM(require("just-clone")); function toRedisJson(schema, data) { let json = (0, import_just_clone2.default)(data); convertToRedisJsonKnown(schema, json); return convertToRedisJsonUnknown(json); } function convertToRedisJsonKnown(schema, json) { schema.fields.forEach((field) => { const results = (0, import_jsonpath_plus2.JSONPath)({ resultType: "all", path: field.jsonPath, json }); if (field.isArray) { convertKnownResultsToJson(field, results); return; } if (results.length === 0) return; if (results.length === 1) { convertKnownResultToJson(field, results[0]); return; } throw new InvalidJsonInput(field); }); } function convertToRedisJsonUnknown(json) { Object.entries(json).forEach(([key, value]) => { if (isUndefined(value)) { delete json[key]; } else if (isObject(value)) { json[key] = convertToRedisJsonUnknown(value); } else { json[key] = convertUnknownValueToJson(value); } }); return json; } function convertKnownResultToJson(field, result) { const { value, parent, parentProperty } = result; if (isDefined(value)) parent[parentProperty] = convertKnownValueToJson(field, value); } function convertKnownResultsToJson(field, results) { results.forEach((result) => { const { value, parent, parentProperty } = result; if (isNull(value)) throw new NullJsonInput(field); if (isUndefined(value) && isArray(parent)) throw new NullJsonInput(field); if (isDefined(value)) parent[parentProperty] = convertKnownArrayValueToJson(field, value); }); } function convertKnownValueToJson(field, value) { if (isNull(value)) return value; switch (field.type) { case "boolean": if (isBoolean(value)) return value; throw new InvalidJsonInput(field); case "number": if (isNumber(value)) return value; throw new InvalidJsonInput(field); case "date": if (isNumber(value)) return value; if (isDate(value)) return convertDateToEpoch(value); if (isDateString(value)) return convertIsoDateToEpoch(value); throw new InvalidJsonInput(field); case "point": if (isPoint(value)) return convertPointToString(value); throw new InvalidJsonInput(field); case "string": case "text": if (isBoolean(value)) return value.toString(); if (isNumber(value)) return value.toString(); if (isString(value)) return value; throw new InvalidJsonInput(field); } } function convertKnownArrayValueToJson(field, value) { switch (field.type) { case "string[]": if (isBoolean(value)) return value.toString(); if (isNumber(value)) return value.toString(); if (isString(value)) return value; throw new InvalidJsonInput(field); case "number[]": if (isNumber(value)) return value; throw new InvalidJsonInput(field); } } function convertUnknownValueToJson(value) { if (isDate(value)) return convertDateToEpoch(value); return value; } // lib/search/results-converter.ts function extractCountFromSearchResults(results) { return results.total; } function extractKeyNamesFromSearchResults(results) { return results.documents.map((document) => document.id); } function extractEntityIdsFromSearchResults(schema, results) { const keyNames = extractKeyNamesFromSearchResults(results); const entityIds = keyNamesToEntityIds(schema.schemaName, keyNames); return entityIds; } function extractEntitiesFromSearchResults(schema, results) { if (schema.dataStructure === "HASH") { return results.documents.map((document) => hashDocumentToEntity(schema, document)); } else { return results.documents.map((document) => jsonDocumentToEntity(schema, document)); } } function hashDocumentToEntity(schema, document) { const keyName = document.id; const hashData = document.value; const entityData = fromRedisHash(schema, hashData); const entity = enrichEntityData(schema.schemaName, keyName, entityData); return entity; } function jsonDocumentToEntity(schema, document) { const keyName = document.id; const jsonData = document.value["$"] ?? false ? JSON.parse(document.value["$"]) : document.value; const entityData = fromRedisJson(schema, jsonData); const entity = enrichEntityData(schema.schemaName, keyName, entityData); return entity; } function enrichEntityData(keyPrefix, keyName, entityData) { const entityId = keyNameToEntityId(keyPrefix, keyName); const entity = { ...entityData, [EntityId]: entityId, [EntityKeyName]: keyName }; return entity; } function keyNamesToEntityIds(keyPrefix, keyNames) { return keyNames.map((keyName) => keyNameToEntityId(keyPrefix, keyName)); } function keyNameToEntityId(keyPrefix, keyName) { const escapedPrefix = keyPrefix.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&"); const regex = new RegExp(`^${escapedPrefix}:`); const entityId = keyName.replace(regex, ""); return entityId; } // lib/search/where.ts var Where = class { }; // lib/search/where-and.ts var WhereAnd = class extends Where { left; right; constructor(left, right) { super(); this.left = left; this.right = right; } toString() { return `( ${this.left.toString()} ${this.right.toString()} )`; } }; // lib/search/where-or.ts var WhereOr = class extends Where { left; right; constructor(left, right) { super(); this.left = left; this.right = right; } toString() { return `( ${this.left.toString()} | ${this.right.toString()} )`; } }; // lib/search/where-field.ts var WhereField = class { negated = false; search; field; constructor(search, field) { this.search = search; this.field = field; } get is() { return this; } get does() { return this; } get not() { this.negate(); return this; } negate() { this.negated = !this.negated; } buildQuery(valuePortion) { const negationPortion = this.negated ? "-" : ""; const fieldPortion = this.escapePunctuationAndSpaces(this.field.name); return `(${negationPortion}@${fieldPortion}:${valuePortion})`; } escapePunctuation(value) { const matchPunctuation = /[,.?<>{}[\]"':;!@#$%^&()\-+=~|/\\]/g; return value.replace(matchPunctuation, "\\$&"); } escapePunctuationAndSpaces(value) { const matchPunctuation = /[,.?<>{}[\]"':;!@#$%^&()\-+=~|/\\ ]/g; return value.replace(matchPunctuation, "\\$&"); } }; // lib/search/where-string-array.ts var WhereStringArray = class extends WhereField { value; contain(value) { this.value = [value]; return this.search; } contains(value) { return this.contain(value); } containsOneOf(...value) { this.value = value; return this.search; } containOneOf(...value) { return this.containsOneOf(...value); } toString() { const escapedValue = this.value.map((s) => this.escapePunctuationAndSpaces(s)).join("|"); return this.buildQuery(`{${escapedValue}}`); } }; // lib/search/where-boolean.ts var WhereBoolean = class extends WhereField { value; eq(value) { this.value = value; return this.search; } equal(value) { return this.eq(value); } equals(value) { return this.eq(value); } equalTo(value) { return this.eq(value); } true() { return this.eq(true); } false() { return this.eq(false); } }; var WhereHashBoolean = class extends WhereBoolean { toString() { return this.buildQuery(`{${this.value ? "1" : "0"}}`); } }; var WhereJsonBoolean = class extends WhereBoolean { toString() { return this.buildQuery(`{${this.value}}`); } }; // lib/search/where-number.ts var WhereNumber = class extends WhereField { lower = Number.NEGATIVE_INFINITY; upper = Number.POSITIVE_INFINITY; lowerExclusive = false; upperExclusive = false; eq(value) { this.lower = value; this.upper = value; return this.search; } gt(value) { this.lower = value; this.lowerExclusive = true; return this.search; } gte(value) { this.lower = value; return this.search; } lt(value) { this.upper = value; this.upperExclusive = true; return this.search; } lte(value) { this.upper = value; return this.search; } between(lower, upper) { this.lower = lower; this.upper = upper; return this.search; } equal(value) { return this.eq(value); } equals(value) { return this.eq(value); } equalTo(value) { return this.eq(value); } greaterThan(value) { return this.gt(value); } greaterThanOrEqualTo(value) { return this.gte(value); } lessThan(value) { return this.lt(value); } lessThanOrEqualTo(value) { return this.lte(value); } toString() { const lower = this.makeLowerString(); const upper = this.makeUpperString(); return this.buildQuery(`[${lower} ${upper}]`); } makeLowerString() { if (this.lower === Number.NEGATIVE_INFINITY) return "-inf"; if (this.lowerExclusive) return `(${this.lower}`; return this.lower.toString(); } makeUpperString() { if (this.upper === Number.POSITIVE_INFINITY) return "+inf"; if (this.upperExclusive) return `(${this.upper}`; return this.upper.toString(); } }; // lib/search/where-point.ts var Circle = class { longitudeOfOrigin = 0; latitudeOfOrigin = 0; size = 1; units = "m"; longitude(value) { this.longitudeOfOrigin = value; return this; } latitude(value) { this.latitudeOfOrigin = value; return this; } origin(pointOrLongitude, latitude) { if (typeof pointOrLongitude === "number" && latitude !== void 0) { this.longitudeOfOrigin = pointOrLongitude; this.latitudeOfOrigin = latitude; } else { const point = pointOrLongitude; this.longitudeOfOrigin = point.longitude; this.latitudeOfOrigin = point.latitude; } return this; } radius(size) { this.size = size; return this; } get m() { return this.meters; } get meter() { return this.meters; } get meters() { this.units = "m"; return this; } get km() { return this.kilometers; } get kilometer() { return this.kilometers; } get kilometers() { this.units = "km"; return this; } get ft() { return this.feet; } get foot() { return this.feet; } get feet() { this.units = "ft"; return this; } get mi() { return this.miles; } get mile() { return this.miles; } get miles() { this.units = "mi"; return this; } }; var WherePoint = class extends WhereField { circle = new Circle(); inRadius(circleFn) { return this.inCircle(circleFn); } inCircle(circleFn) { this.circle = circleFn(this.circle); return this.search; } toString() { const { longitudeOfOrigin, latitudeOfOrigin, size, units } = this.circle; return this.buildQuery(`[${longitudeOfOrigin} ${latitudeOfOrigin} ${size} ${units}]`); } }; // lib/search/where-string.ts var WhereString = class extends WhereField { value; eq(value) { this.value = value.toString(); return this.search; } equal(value) { return this.eq(value); } equals(value) { return this.eq(value); } equalTo(value) { return this.eq(value); } match(_) { return this.throwMatchExcpetion(); } matches(_) { return this.throwMatchExcpetion(); } matchExact(_) { return this.throwMatchExcpetion(); } matchExactly(_) { return this.throwMatchExcpetion(); } matchesExactly(_) { return this.throwMatchExcpetion(); } get exact() { return this.throwMatchExcpetionReturningThis(); } get exactly() { return this.throwMatchExcpetionReturningThis(); } toString() { const escapedValue = this.escapePunctuationAndSpaces(this.value); return this.buildQuery(`{${escapedValue}}`); } throwMatchExcpetion() { 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."); } throwMatchExcpetionReturningThis() { 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."); } }; // lib/search/where-text.ts var WhereText = class extends WhereField { value; exactValue = false; fuzzyMatching; levenshteinDistance; match(value, options = { fuzzyMatching: false, levenshteinDistance: 1 }) { this.value = value.toString(); this.fuzzyMatching = options.fuzzyMatching ?? false; this.levenshteinDistance = options.levenshteinDistance ?? 1; return this.search; } matchExact(value) { this.exact.value = value.toString(); return this.search; } matches(value, options = { fuzzyMatching: false, levenshteinDistance: 1 }) { return this.match(value, options); } matchExactly(value) { return this.matchExact(value); } matchesExactly(value) { return this.matchExact(value); } get exact() { this.exactValue = true; return this; } get exactly() { return this.exact; } eq(_) { return this.throwEqualsExcpetion(); } equal(_) { return this.throwEqualsExcpetion(); } equals(_) { return this.throwEqualsExcpetion(); } equalTo(_) { return this.throwEqualsExcpetion(); } toString() { const escapedValue = this.escapePunctuation(this.value); if (this.exactValue) { return this.buildQuery(`"${escapedValue}"`); } else if (this.fuzzyMatching) { return this.buildQuery(`${"%".repeat(this.levenshteinDistance)}${escapedValue}${"%".repeat(this.levenshteinDistance)}`); } else { return this.buildQuery(`'${escapedValue}'`); } } throwEqualsExcpetion() { 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."); } }; // lib/search/where-date.ts var WhereDate = class extends WhereField { lower = Number.NEGATIVE_INFINITY; upper = Number.POSITIVE_INFINITY; lowerExclusive = false; upperExclusive = false; eq(value) { const epoch = this.coerceDateToEpoch(value); this.lower = epoch; this.upper = epoch; return this.search; } gt(value) { const epoch = this.coerceDateToEpoch(value); this.lower = epoch; this.lowerExclusive = true; return this.search; } gte(value) { this.lower = this.coerceDateToEpoch(value); return this.search; } lt(value) { this.upper = this.coerceDateToEpoch(value); this.upperExclusive = true; return this.search; } lte(value) { this.upper = this.coerceDateToEpoch(value); return this.search; } between(lower, upper) { this.lower = this.coerceDateToEpoch(lower); this.upper = this.coerceDateToEpoch(upper); return this.search; } equal(value) { return this.eq(value); } equals(value) { return this.eq(value); } equalTo(value) { return this.eq(value); } greaterThan(value) { return this.gt(value); } greaterThanOrEqualTo(value) { return this.gte(value); } lessThan(value) { return this.lt(value); } lessThanOrEqualTo(value) { return this.lte(value); } on(value) { return this.eq(value); } after(value) { return this.gt(value); } before(value) { return this.lt(value); } onOrAfter(value) { return this.gte(value); } onOrBefore(value) { return this.lte(value); } toString() { const lower = this.makeLowerString(); const upper = this.makeUpperString(); return this.buildQuery(`[${lower} ${upper}]`); } makeLowerString() { if (this.lower === Number.NEGATIVE_INFINITY) return "-inf"; if (this.lowerExclusive) return `(${this.lower}`; return this.lower.toString(); } makeUpperString() { if (this.upper === Number.POSITIVE_INFINITY) return "+inf"; if (this.upperExclusive) return `(${this.upper}`; return this.upper.toString(); } coerceDateToEpoch(value) { if (value instanceof Date) return value.getTime() / 1e3; if (typeof value === "string") return new Date(value).getTime() / 1e3; return value; } }; // lib/search/search.ts var AbstractSearch = class { schema; client; sortOptions; constructor(schema, client) { this.schema = schema; this.client = client; } sortAscending(field) { return this.sortBy(field, "ASC"); } sortDesc(field) { return this.sortDescending(field); } sortDescending(field) { return this.sortBy(field, "DESC"); } sortAsc(field) { return this.sortAscending(field); } sortBy(fieldName, order = "ASC") { 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 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"]; 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); } 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.`); this.sortOptions = { BY: field.name, DIRECTION: order }; return this; } async min(field) { return await this.sortBy(field, "ASC").first(); } async minId(field) { return await this.sortBy(field, "ASC").firstId(); } async minKey(field) { return await this.sortBy(field, "ASC").firstKey(); } async max(field) { return await this.sortBy(field, "DESC").first(); } async maxId(field) { return await this.sortBy(field, "DESC").firstId(); } async maxKey(field) { return await this.sortBy(field, "DESC").firstKey(); } async count() { const searchResults = await this.callSearch(); return extractCountFromSearchResults(searchResults); } async page(offset, count) { const searchResults = await this.callSearch(offset, count); return extractEntitiesFromSearchResults(this.schema, searchResults); } async pageOfIds(offset, count) { const searchResults = await this.callSearch(offset, count, true); return extractEntityIdsFromSearchResults(this.schema, searchResults); } async pageOfKeys(offset, count) { const searchResults = await this.callSearch(offset, count, true); return extractKeyNamesFromSearchResults(searchResults); } async first() { const foundEntity = await this.page(0, 1); return foundEntity[0] ?? null; } async firstId() { const foundIds = await this.pageOfIds(0, 1); return foundIds[0] ?? null; } async firstKey() { const foundKeys = await this.pageOfKeys(0, 1); return foundKeys[0] ?? null; } async all(options = { pageSize: 10 }) { return this.allThings(this.page, options); } async allIds(options = { pageSize: 10 }) { return this.allThings(this.pageOfIds, options); } async allKeys(options = { pageSize: 10 }) { return this.allThings(this.pageOfKeys, options); } get return() { return this; } async returnMin(field) { return await this.min(field); } async returnMinId(field) { return await this.minId(field); } async returnMinKey(field) { return await this.minKey(field); } async returnMax(field) { return await this.max(field); } async returnMaxId(field) { return await this.maxId(field); } async returnMaxKey(field) { return await this.maxKey(field); } async returnCount() { return await this.count(); } async returnPage(offset, count) { return await this.page(offset, count); } async returnPageOfIds(offset, count) { return await this.pageOfIds(offset, count); } async returnPageOfKeys(offset, count) { return await this.pageOfKeys(offset, count); } async returnFirst() { return await this.first(); } async returnFirstId() { return await this.firstId(); } async returnFirstKey() { return await this.firstKey(); } async returnAll(options = { pageSize: 10 }) { return await this.all(options); } async returnAllIds(options = { pageSize: 10 }) { return await this.allIds(options); } async returnAllKeys(options = { pageSize: 10 }) { return await this.allKeys(options); } async allThings(pageFn, options = { pageSize: 10 }) { 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; } return things; } async callSearch(offset = 0, count = 0, keysOnly = false) { const dataStructure = this.schema.dataStructure; const indexName = this.schema.indexName; const query = this.query; const options = { LIMIT: { from: offset, size: count } }; if (this.sortOptions !== void 0) options.SORTBY = this.sortOptions; if (keysOnly) { options.RETURN = []; } else if (dataStructure === "JSON") { options.RETURN = "$"; } let searchResults; try { searchResults = await this.client.search(indexName, query, options); } catch (error) { const message = error.message; if (message.startsWith("Syntax error")) { throw new SearchError(`The query to RediSearch had a syntax error: "${message}". This 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; } return searchResults; } }; var RawSearch = class extends AbstractSearch { rawQuery; constructor(schema, client, query = "*") { super(schema, client); this.rawQuery = query; } get query() { return this.rawQuery; } }; var Search = class extends AbstractSearch { rootWhere; get query() { if (this.rootWhere === void 0) return "*"; return `${this.rootWhere.toString()}`; } where(fieldOrFn) { return this.anyWhere(WhereAnd, fieldOrFn); } and(fieldOrFn) { return this.anyWhere(WhereAnd, fieldOrFn); } or(fieldOrFn) { return this.anyWhere(WhereOr, fieldOrFn); } anyWhere(ctor, fieldOrFn) { if (typeof fieldOrFn === "string") { return this.anyWhereForField(ctor, fieldOrFn); } else { return this.anyWhereForFunction(ctor, fieldOrFn); } } anyWhereForField(ctor, field) { const where = this.createWhere(field); if (this.rootWhere === void 0) { this.rootWhere = where; } else { this.rootWhere = new ctor(this.rootWhere, where); } return where; } anyWhereForFunction(ctor, subSearchFn) { const search = new Search(this.schema, this.client); const subSearch = subSearchFn(search); if (subSearch.rootWhere === void 0) { throw new SearchError("Sub-search without a root where was somehow defined."); } else { if (this.rootWhere === void 0) { this.rootWhere = subSearch.rootWhere; } else { this.rootWhere = new ctor(this.rootWhere, subSearch.rootWhere); } } return this; } createWhere(fieldName) { const field = this.schema.fieldByName(fieldName); if (field === null) throw new FieldNotInSchema(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(`The field type of '${fieldDef.type}' is not a valid field type. Valid types include 'boolean', 'date', 'number', 'point', 'string', and 'string[]'.`); } }; // lib/repository/repository.ts var Repository = class { client; #schema; constructor(schema, clientOrConnection) { this.#schema = schema; if (clientOrConnection instanceof Client) { this.client = clientOrConnection; } else { this.client = new Client(); this.client.useNoClose(clientOrConnection); } } async createIndex() { const currentIndexHash = await this.client.get(this.#schema.indexHashName); const incomingIndexHash = this.#schema.indexHash; if (currentIndexHash !== incomingIndexHash) { await this.dropIndex(); const { indexName, indexHashName, dataStructure, schemaName: prefix, useStopWords, stopWords } = this.#schema; const schema = buildRediSearchSchema(this.#schema); const options = { ON: dataStructure, PREFIX: `${prefix}:` }; if (useStopWords === "OFF") { options.STOPWORDS = []; } else if (useStopWords === "CUSTOM") { options.STOPWORDS = stopWords; } await Promise.all([ this.client.createIndex(indexName, schema, options), this.client.set(indexHashName, incomingIndexHash) ]); } } async dropIndex() { try { await Promise.all([ this.client.unlink(this.#schema.indexHashName), this.client.dropIndex(this.#schema.indexName) ]); } catch (e) { if (e instanceof Error && (e.message === "Unknown Index name" || e.message === "Unknown index name")) { } else { throw e; } } } async save(entityOrId, maybeEntity) { let entity; let entityId; if (typeof entityOrId !== "string") { entity = entityOrId; entityId = entity[EntityId] ?? await this.#schema.generateId(); } else { entity = maybeEntity; entityId = entityOrId; } const keyName = `${this.#schema.schemaName}:${entityId}`; const clonedEntity = { ...entity, [EntityId]: entityId, [EntityKeyName]: keyName }; await this.writeEntity(clonedEntity); return clonedEntity; } async fetch(ids) { if (arguments.length > 1) return this.readEntities([...arguments]); if (Array.isArray(ids)) return this.readEntities(ids); const [entity] = await this.readEntities([ids]); return entity; } async remove(ids) { const keys = arguments.length > 1 ? this.makeKeys([...arguments]) : Array.isArray(ids) ? this.makeKeys(ids) : ids ? this.makeKeys([ids]) : []; if (keys.length === 0) return; await this.client.unlink(...keys); } async expire(idOrIds, ttlInSeconds) { const ids = typeof idOrIds === "string" ? [idOrIds] : idOrIds; await Promise.all( ids.map((id) => { const key = this.makeKey(id); return this.client.expire(key, ttlInSeconds); }) ); } async expireAt(idOrIds, expirationDate) { const ids = typeof idOrIds === "string" ? [idOrIds] : idOrIds; if (Date.now() >= expirationDate.getTime()) { throw new Error( `${expirationDate.toString()} is invalid. Expiration date must be in the future.` ); } await Promise.all( ids.map((id) => { const key = this.makeKey(id); return this.client.expireAt(key, expirationDate); }) ); } search() { return new Search(this.#schema, this.client); } searchRaw(query) { return new RawSearch(this.#schema, this.client, query); } async writeEntity(entity) { return this.#schema.dataStructure === "HASH" ? this.writeEntityToHash(entity) : this.writeEntityToJson(entity); } async readEntities(ids) { return this.#schema.dataStructure === "HASH" ? this.readEntitiesFromHash(ids) : this.readEntitiesFromJson(ids); } async writeEntityToHash(entity) { const keyName = entity[EntityKeyName]; const hashData = toRedisHash(this.#schema, entity); if (Object.keys(hashData).length === 0) { await this.client.unlink(keyName); } else { await this.client.hsetall(keyName, hashData); } } async readEntitiesFromHash(ids) { return Promise.all( ids.map(async (entityId) => { 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; }) ); } async writeEntityToJson(entity) { const keyName = entity[EntityKeyName]; const jsonData = toRedisJson(this.#schema, entity); await this.client.jsonset(keyName, jsonData); } async readEntitiesFromJson(ids) { return Promise.all( ids.map(async (entityId) => { 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; }) );