@push.rocks/smartdata
Version:
An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.
900 lines • 78.8 kB
JavaScript
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
import * as plugins from './plugins.js';
import { SmartdataDb } from './classes.db.js';
import { logger } from './logging.js';
import { SmartdataDbCursor } from './classes.cursor.js';
import { SmartdataCollection } from './classes.collection.js';
import { SmartdataDbWatcher } from './classes.watcher.js';
import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
/**
* Detects plain object literals only. Anything carrying a custom prototype
* (Date, ObjectId, Binary, Buffer, RegExp, Decimal128, class instances, ...) is
* deliberately excluded so that BSON types are never rebuilt or otherwise
* altered while stripping undefined values.
*/
const isPlainObject = (valueArg) => {
if (typeof valueArg !== 'object' || valueArg === null) {
return false;
}
const prototype = Object.getPrototypeOf(valueArg);
return prototype === Object.prototype || prototype === null;
};
/**
* Recursively drops object properties whose value is `undefined` so MongoDB
* stores them as absent rather than as BSON null.
*
* Notes on the deliberate boundaries of this traversal:
* - Array positions are preserved: an `undefined` element stays in place and is
* serialized as null, exactly as `JSON.stringify` does. Removing it would
* shift every following index.
* - Only plain objects are recursed into, so BSON types keep their identity.
* - On a circular structure the offending value is returned untouched, letting
* BSON raise its usual "Cannot convert circular structure to BSON" error
* instead of overflowing the stack here.
*/
const stripUndefinedValues = (valueArg, seenArg) => {
if (typeof valueArg !== 'object' || valueArg === null) {
return valueArg;
}
const isArray = Array.isArray(valueArg);
if (!isArray && !isPlainObject(valueArg)) {
return valueArg;
}
const seen = seenArg ?? new WeakSet();
if (seen.has(valueArg)) {
return valueArg;
}
seen.add(valueArg);
if (isArray) {
return valueArg.map((entryArg) => stripUndefinedValues(entryArg, seen));
}
const strippedObject = {};
for (const keyArg of Object.keys(valueArg)) {
const entry = valueArg[keyArg];
if (entry === undefined) {
continue;
}
strippedObject[keyArg] = stripUndefinedValues(entry, seen);
}
return strippedObject;
};
export function globalSvDb() {
return (value, context) => {
if (context.kind !== 'field') {
throw new Error('globalSvDb can only decorate fields');
}
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata;
if (!metadata.globalSaveableProperties) {
metadata.globalSaveableProperties = [];
}
metadata.globalSaveableProperties.push(String(context.name));
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function () {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
}
});
};
}
/**
* saveable - saveable decorator to be used on class properties
*/
export function svDb(options) {
return (value, context) => {
if (context.kind !== 'field') {
throw new Error('svDb can only decorate fields');
}
const propName = String(context.name);
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata;
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
metadata.saveableProperties.push(propName);
// Store options in metadata
if (options) {
if (!metadata._svDbOptions) {
metadata._svDbOptions = {};
}
metadata._svDbOptions[propName] = options;
}
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function () {
const proto = this.constructor.prototype;
const ctor = this.constructor;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.saveableProperties = [...metadata.saveableProperties];
}
// Initialize svDbOptions from metadata
if (metadata && metadata._svDbOptions && !ctor._svDbOptions) {
ctor._svDbOptions = { ...metadata._svDbOptions };
}
});
};
}
/**
* searchable - marks a property as searchable with Lucene query syntax
*/
export function searchable() {
return (value, context) => {
if (context.kind !== 'field') {
throw new Error('searchable can only decorate fields');
}
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata;
if (!metadata.searchableFields) {
metadata.searchableFields = [];
}
metadata.searchableFields.push(propName);
// Use addInitializer to set up constructor property once
context.addInitializer(function () {
const ctor = this.constructor;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.searchableFields && !Array.isArray(ctor.searchableFields)) {
// Initialize from metadata (runs once per class)
ctor.searchableFields = [...metadata.searchableFields];
}
});
};
}
// Escape user input for safe use in MongoDB regular expressions
function escapeForRegex(input) {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* unique index - decorator to mark a unique index
*/
export function unI() {
return (value, context) => {
if (context.kind !== 'field') {
throw new Error('unI can only decorate fields');
}
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata;
if (!metadata.uniqueIndexes) {
metadata.uniqueIndexes = [];
}
metadata.uniqueIndexes.push(propName);
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function () {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}
/**
* index - decorator to mark a field for regular indexing
*/
export function index(options) {
return (value, context) => {
if (context.kind !== 'field') {
throw new Error('index can only decorate fields');
}
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata;
if (!metadata.regularIndexes) {
metadata.regularIndexes = [];
}
metadata.regularIndexes.push({
field: propName,
options: options || {}
});
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function () {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}
export const convertFilterForMongoDb = (filterArg) => {
// SECURITY: Block $where to prevent server-side JS execution
if (filterArg.$where !== undefined) {
throw new Error('$where operator is not allowed for security reasons');
}
// Handle logical operators recursively
const logicalOperators = ['$and', '$or', '$nor', '$not'];
const processedFilter = {};
for (const key of Object.keys(filterArg)) {
if (logicalOperators.includes(key)) {
if (key === '$not') {
processedFilter[key] = convertFilterForMongoDb(filterArg[key]);
}
else if (Array.isArray(filterArg[key])) {
processedFilter[key] = filterArg[key].map((subFilter) => convertFilterForMongoDb(subFilter));
}
}
}
// If only logical operators, return them
const hasOnlyLogicalOperators = Object.keys(filterArg).every(key => logicalOperators.includes(key));
if (hasOnlyLogicalOperators) {
return processedFilter;
}
// Original conversion logic for non-MongoDB query objects
const convertedFilter = {};
// Helper to merge operator objects
const mergeIntoConverted = (path, value) => {
const existing = convertedFilter[path];
if (!existing) {
convertedFilter[path] = value;
}
else if (typeof existing === 'object' && !Array.isArray(existing) &&
typeof value === 'object' && !Array.isArray(value) &&
(Object.keys(existing).some(k => k.startsWith('$')) || Object.keys(value).some(k => k.startsWith('$')))) {
// Both have operators, merge them
convertedFilter[path] = { ...existing, ...value };
}
else {
// Otherwise later wins
convertedFilter[path] = value;
}
};
const convertFilterArgument = (keyPathArg2, filterArg2) => {
if (Array.isArray(filterArg2)) {
// Arrays are typically used as values for operators like $in or as direct equality matches
mergeIntoConverted(keyPathArg2, filterArg2);
return;
}
else if (typeof filterArg2 === 'object' && filterArg2 !== null) {
// Check if this is an object with MongoDB operators
const keys = Object.keys(filterArg2);
const hasOperators = keys.some(key => key.startsWith('$'));
if (hasOperators) {
// This object contains MongoDB operators
// Validate and pass through allowed operators
const allowedOperators = [
// Comparison operators
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte',
// Array operators
'$in', '$nin', '$all', '$elemMatch', '$size',
// Element operators
'$exists', '$type',
// Evaluation operators (safe ones only)
'$regex', '$options', '$text', '$mod',
// Logical operators (nested)
'$and', '$or', '$nor', '$not'
];
// Check for dangerous operators
if (keys.includes('$where')) {
throw new Error('$where operator is not allowed for security reasons');
}
// Validate all operators are in the allowed list
const invalidOperators = keys.filter(key => key.startsWith('$') && !allowedOperators.includes(key));
if (invalidOperators.length > 0) {
console.warn(`Warning: Unknown MongoDB operators detected: ${invalidOperators.join(', ')}`);
}
// For array operators, ensure the values are appropriate
if (filterArg2.$in && !Array.isArray(filterArg2.$in)) {
throw new Error('$in operator requires an array value');
}
if (filterArg2.$nin && !Array.isArray(filterArg2.$nin)) {
throw new Error('$nin operator requires an array value');
}
if (filterArg2.$all && !Array.isArray(filterArg2.$all)) {
throw new Error('$all operator requires an array value');
}
if (filterArg2.$size && typeof filterArg2.$size !== 'number') {
throw new Error('$size operator requires a numeric value');
}
// Use merge helper to handle duplicate paths
mergeIntoConverted(keyPathArg2, filterArg2);
return;
}
// No operators, check for dots in keys
for (const key of keys) {
if (key.includes('.')) {
throw new Error('keys cannot contain dots');
}
}
// Recursively process nested objects
for (const key of keys) {
convertFilterArgument(`${keyPathArg2}.${key}`, filterArg2[key]);
}
}
else {
// Primitive values
mergeIntoConverted(keyPathArg2, filterArg2);
}
};
for (const key of Object.keys(filterArg)) {
// Skip logical operators, they were already processed
if (!logicalOperators.includes(key)) {
convertFilterArgument(key, filterArg[key]);
}
}
// Add back processed logical operators
Object.assign(convertedFilter, processedFilter);
return convertedFilter;
};
let SmartDataDbDoc = (() => {
let __createdAt_decorators;
let __createdAt_initializers = [];
let __createdAt_extraInitializers = [];
let __updatedAt_decorators;
let __updatedAt_initializers = [];
let __updatedAt_extraInitializers = [];
return class SmartDataDbDoc {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__createdAt_decorators = [globalSvDb()];
__updatedAt_decorators = [globalSvDb()];
__esDecorate(null, null, __createdAt_decorators, { kind: "field", name: "_createdAt", static: false, private: false, access: { has: obj => "_createdAt" in obj, get: obj => obj._createdAt, set: (obj, value) => { obj._createdAt = value; } }, metadata: _metadata }, __createdAt_initializers, __createdAt_extraInitializers);
__esDecorate(null, null, __updatedAt_decorators, { kind: "field", name: "_updatedAt", static: false, private: false, access: { has: obj => "_updatedAt" in obj, get: obj => obj._updatedAt, set: (obj, value) => { obj._updatedAt = value; } }, metadata: _metadata }, __updatedAt_initializers, __updatedAt_extraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
/**
* the collection object an Doc belongs to
*/
static collection;
static defaultManager;
static manager;
/**
* Helper to get collection with fallback to static for Deno compatibility
*/
getCollectionSafe() {
return this.collection || this.constructor.collection;
}
// STATIC
static createInstanceFromMongoDbNativeDoc(mongoDbNativeDocArg) {
const newInstance = new this();
newInstance.creationStatus = 'db';
for (const key of Object.keys(mongoDbNativeDocArg)) {
const rawValue = mongoDbNativeDocArg[key];
const optionsMap = this._svDbOptions || {};
const opts = optionsMap[key];
newInstance[key] = opts && typeof opts.deserialize === 'function'
? opts.deserialize(rawValue)
: rawValue;
}
return newInstance;
}
/**
* gets all instances as array
* @param this
* @param filterArg - Type-safe MongoDB filter with nested object support and operators
* @returns
*/
static async getInstances(filterArg, opts) {
// Pass session through to findAll for transactional queries
const foundDocs = await this.collection.findAll(convertFilterForMongoDb(filterArg), { session: opts?.session });
const returnArray = [];
for (const foundDoc of foundDocs) {
const newInstance = this.createInstanceFromMongoDbNativeDoc(foundDoc);
returnArray.push(newInstance);
}
return returnArray;
}
/**
* Cursor-paged query with stable seek pagination — the org-standard way to
* list unbounded collections. Orders by a comparable (ideally indexed)
* sort field with a unique tiebreaker, returns one page plus the cursor
* for the next, and never materializes the full result set.
*/
static async getPagedInstances(optionsArg) {
const collection = this.collection;
await collection.init();
const sortDirection = optionsArg.sortDirection === 'asc' ? 1 : -1;
const uniqueField = optionsArg.uniqueField || 'id';
const requestedLimit = Number.isSafeInteger(optionsArg.limit) && optionsArg.limit > 0
? optionsArg.limit
: 100;
const limit = Math.min(requestedLimit, 1000);
const baseSelector = convertFilterForMongoDb(optionsArg.filter || {});
let selector = baseSelector;
if (optionsArg.cursor) {
const seekOperator = sortDirection === 1 ? '$gt' : '$lt';
const seekSelector = {
$or: [
{ [optionsArg.sortField]: { [seekOperator]: optionsArg.cursor.sortValue } },
{
[optionsArg.sortField]: optionsArg.cursor.sortValue,
[uniqueField]: { [seekOperator]: optionsArg.cursor.uniqueValue },
},
],
};
selector = Object.keys(baseSelector).length > 0
? { $and: [baseSelector, seekSelector] }
: seekSelector;
}
const rawCursor = collection.mongoDbCollection
.find(selector, { session: optionsArg.session })
.sort({ [optionsArg.sortField]: sortDirection, [uniqueField]: sortDirection })
.limit(limit + 1);
try {
const rawDocuments = await rawCursor.toArray();
const hasMore = rawDocuments.length > limit;
const pageRows = rawDocuments.slice(0, limit);
const documents = pageRows.map((rawDocument) => this.createInstanceFromMongoDbNativeDoc(rawDocument));
const lastRow = pageRows[pageRows.length - 1];
const total = optionsArg.withTotal
? await collection.mongoDbCollection.countDocuments(baseSelector, {
session: optionsArg.session,
})
: undefined;
return {
documents,
...(hasMore && lastRow
? {
nextCursor: {
sortValue: lastRow[optionsArg.sortField],
uniqueValue: lastRow[uniqueField],
},
}
: {}),
...(total !== undefined ? { total } : {}),
};
}
finally {
await rawCursor.close();
}
}
/**
* gets the first matching instance
* @param this
* @param filterArg
* @returns
*/
static async getInstance(filterArg, opts) {
// Retrieve one document, with optional session for transactions
const foundDoc = await this.collection.findOne(convertFilterForMongoDb(filterArg), { session: opts?.session });
if (foundDoc) {
const newInstance = this.createInstanceFromMongoDbNativeDoc(foundDoc);
return newInstance;
}
else {
return null;
}
}
/**
* get a unique id prefixed with the class name
*/
static async getNewId(lengthArg = 20) {
return `${this.className}:${plugins.smartunique.shortId(lengthArg)}`;
}
/**
* Get a cursor for streaming results, with optional session and native cursor modifiers.
* @param filterArg Partial filter to apply
* @param opts Optional session and modifier for the raw MongoDB cursor
*/
static async getCursor(filterArg, opts) {
const collection = this.collection;
const { session, modifier } = opts || {};
await collection.init();
let rawCursor = collection.mongoDbCollection.find(convertFilterForMongoDb(filterArg), { session });
if (modifier) {
rawCursor = modifier(rawCursor);
}
return new SmartdataDbCursor(rawCursor, this);
}
/**
* watch the collection
* @param this
* @param filterArg
* @param forEachFunction
*/
/**
* Watch the collection for changes, with optional buffering and change stream options.
* @param filterArg MongoDB filter to select which changes to observe
* @param opts optional ChangeStreamOptions plus bufferTimeMs
*/
static async watch(filterArg, opts) {
const collection = this.collection;
const watcher = await collection.watch(convertFilterForMongoDb(filterArg), opts || {}, this);
return watcher;
}
/**
* run a function for all instances
* @returns
*/
static async forEach(filterArg, forEachFunction) {
const cursor = await this.getCursor(filterArg);
await cursor.forEach(forEachFunction);
}
/**
* returns a count of the documents in the collection
*/
static async getCount(filterArg = {}) {
const collection = this.collection;
return await collection.getCount(filterArg);
}
/**
* Runs an integrity check on this collection.
* Returns a summary with estimated vs actual counts and any duplicate unique fields.
*/
static async checkCollectionIntegrity() {
const collection = this.collection;
return await collection.checkCollectionIntegrity();
}
/**
* Create a MongoDB filter from a Lucene query string
* @param luceneQuery Lucene query string
* @returns MongoDB query object
*/
static createSearchFilter(luceneQuery) {
const searchableFields = this.getSearchableFields();
if (searchableFields.length === 0) {
throw new Error(`No searchable fields defined for class ${this.name}`);
}
const adapter = new SmartdataLuceneAdapter(searchableFields);
return adapter.convert(luceneQuery);
}
/**
* List all searchable fields defined on this class
*/
static getSearchableFields() {
const ctor = this;
return Array.isArray(ctor.searchableFields) ? ctor.searchableFields : [];
}
/**
* Execute a query with optional hard filter and post-fetch validation
*/
static async execQuery(baseFilter, opts) {
let mongoFilter = baseFilter || {};
if (opts?.filter) {
mongoFilter = { $and: [mongoFilter, opts.filter] };
}
// Fetch with optional session for transactions
// Fetch within optional session
let docs = await this.getInstances(mongoFilter, { session: opts?.session });
if (opts?.validate) {
const out = [];
for (const d of docs) {
if (await opts.validate(d))
out.push(d);
}
docs = out;
}
return docs;
}
/**
* Search documents by text or field:value syntax, with safe regex fallback
* Supports additional filtering and post-fetch validation via opts
* @param query A search term or field:value expression
* @param opts Optional filter and validate hooks
* @returns Array of matching documents
*/
static async search(query, opts) {
const searchableFields = this.getSearchableFields();
if (searchableFields.length === 0) {
throw new Error(`No searchable fields defined for class ${this.name}`);
}
// empty query -> return all
const q = query.trim();
if (!q) {
// empty query: fetch all, apply opts
return await this.execQuery({}, opts);
}
// simple exact field:value (no spaces, no wildcards, no quotes)
// simple exact field:value (no spaces, wildcards, quotes)
const simpleExact = q.match(/^(\w+):([^"'\*\?\s]+)$/);
if (simpleExact) {
const field = simpleExact[1];
const value = simpleExact[2];
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
// simple field:value search
return await this.execQuery({ [field]: value }, opts);
}
// quoted phrase across all searchable fields: exact match of phrase
const quoted = q.match(/^"(.+)"$|^'(.+)'$/);
if (quoted) {
const phrase = quoted[1] || quoted[2] || '';
const parts = phrase.split(/\s+/).map((t) => escapeForRegex(t));
const pattern = parts.join('\\s+');
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
return await this.execQuery({ $or: orConds }, opts);
}
// wildcard field:value (supports * and ?) -> direct regex on that field
const wildcardField = q.match(/^(\w+):(.+[*?].*)$/);
if (wildcardField) {
const field = wildcardField[1];
// Support quoted wildcard patterns: strip surrounding quotes
let pattern = wildcardField[2];
if ((pattern.startsWith('"') && pattern.endsWith('"')) ||
(pattern.startsWith("'") && pattern.endsWith("'"))) {
pattern = pattern.slice(1, -1);
}
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
// escape regex special chars except * and ?, then convert wildcards
const escaped = pattern.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
return await this.execQuery({ [field]: { $regex: regexPattern, $options: 'i' } }, opts);
}
// wildcard plain term across all fields (supports * and ?)
if (!q.includes(':') && (q.includes('*') || q.includes('?'))) {
// build wildcard regex pattern: escape all except * and ? then convert
const escaped = q.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
return await this.execQuery({ $or: orConds }, opts);
}
// implicit AND for multiple tokens: free terms, quoted phrases, and field:values
{
// Split query into tokens, preserving quoted substrings
const rawTokens = q.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
// Only apply when more than one token and no boolean operators or grouping
if (rawTokens.length > 1 &&
!/(\bAND\b|\bOR\b|\bNOT\b|\(|\))/i.test(q) &&
!/\[|\]/.test(q)) {
const andConds = [];
for (let token of rawTokens) {
// field:value token
const fv = token.match(/^(\w+):(.+)$/);
if (fv) {
const field = fv[1];
let value = fv[2];
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
// Strip surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
// Wildcard search?
if (value.includes('*') || value.includes('?')) {
const escaped = value.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
andConds.push({ [field]: { $regex: pattern, $options: 'i' } });
}
else {
andConds.push({ [field]: value });
}
}
else if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'"))) {
// Quoted free phrase across all fields
const phrase = token.slice(1, -1);
const parts = phrase.split(/\s+/).map((t) => escapeForRegex(t));
const pattern = parts.join('\\s+');
andConds.push({ $or: searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } })) });
}
else {
// Free term across all fields
const esc = escapeForRegex(token);
andConds.push({ $or: searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })) });
}
}
return await this.execQuery({ $and: andConds }, opts);
}
}
// detect advanced Lucene syntax: field:value, wildcards, boolean, grouping
const luceneSyntax = /(\w+:[^\s]+)|\*|\?|\bAND\b|\bOR\b|\bNOT\b|\(|\)/;
if (luceneSyntax.test(q)) {
const filter = this.createSearchFilter(q);
return await this.execQuery(filter, opts);
}
// multi-term unquoted -> AND of regex across fields for each term
const terms = q.split(/\s+/);
if (terms.length > 1) {
const andConds = terms.map((term) => {
const esc = escapeForRegex(term);
const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
return { $or: ors };
});
return await this.execQuery({ $and: andConds }, opts);
}
// single term -> regex across all searchable fields
const esc = escapeForRegex(q);
const orConds = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
return await this.execQuery({ $or: orConds }, opts);
}
// INSTANCE
// INSTANCE
/**
* how the Doc in memory was created, may prove useful later.
*/
creationStatus = 'new';
/**
* updated from db in any case where doc comes from db
*/
_createdAt = __runInitializers(this, __createdAt_initializers, new Date().toISOString());
/**
* will be updated everytime the doc is saved
*/
_updatedAt = (__runInitializers(this, __createdAt_extraInitializers), __runInitializers(this, __updatedAt_initializers, new Date().toISOString()));
/**
* name
*/
name = __runInitializers(this, __updatedAt_extraInitializers);
/**
* primary id in the database
*/
dbDocUniqueId;
/**
* class constructor
*/
constructor() { }
/**
* saves this instance (optionally within a transaction)
*/
async save(opts) {
// allow hook before saving
if (typeof this.beforeSave === 'function') {
await this.beforeSave();
}
// tslint:disable-next-line: no-this-assignment
const self = this;
let dbResult;
// update timestamp
this._updatedAt = new Date().toISOString();
// perform insert or update
switch (this.creationStatus) {
case 'db':
dbResult = await this.getCollectionSafe().update(self, { session: opts?.session });
break;
case 'new':
dbResult = await this.getCollectionSafe().insert(self, { session: opts?.session });
this.creationStatus = 'db';
break;
default:
logger.log('error', 'neither new nor in db?');
}
// allow hook after saving
if (typeof this.afterSave === 'function') {
await this.afterSave();
}
return dbResult;
}
/**
* deletes a document from the database (optionally within a transaction)
*/
async delete(opts) {
// allow hook before deleting
if (typeof this.beforeDelete === 'function') {
await this.beforeDelete();
}
// perform deletion
const result = await this.getCollectionSafe().delete(this, { session: opts?.session });
// allow hook after delete
if (typeof this.afterDelete === 'function') {
await this.afterDelete();
}
return result;
}
/**
* also store any referenced objects to DB
* better for data consistency
*/
saveDeep(savedMapArg) {
if (!savedMapArg) {
savedMapArg = new plugins.lik.ObjectMap();
}
savedMapArg.add(this);
this.save();
for (const propertyKey of Object.keys(this)) {
const property = this[propertyKey];
if (property instanceof SmartDataDbDoc && !savedMapArg.checkForObject(property)) {
property.saveDeep(savedMapArg);
}
}
}
/**
* updates an object from db
*/
async updateFromDb() {
const mongoDbNativeDoc = await this.getCollectionSafe().findOne(await this.createIdentifiableObject());
if (!mongoDbNativeDoc) {
return false; // Document not found in database
}
for (const key of Object.keys(mongoDbNativeDoc)) {
const rawValue = mongoDbNativeDoc[key];
const optionsMap = this.constructor._svDbOptions || {};
const opts = optionsMap[key];
this[key] = opts && typeof opts.deserialize === 'function'
? opts.deserialize(rawValue)
: rawValue;
}
return true;
}
/**
* creates a saveable object so the instance can be persisted as json in the database
*
* Properties whose value is `undefined` are omitted entirely rather than
* written as BSON null, so "absent" stays representable. Explicit `null` is
* untouched and remains storable wherever the field type allows it.
*/
async createSavableObject() {
const saveableObject = {}; // is not exposed to outside, so any is ok here
const globalProps = this.globalSaveableProperties || [];
const specificProps = this.saveableProperties || [];
const saveableProperties = [...globalProps, ...specificProps];
// apply custom serialization if configured
const optionsMap = this.constructor._svDbOptions || {};
for (const propertyNameString of saveableProperties) {
const rawValue = this[propertyNameString];
const opts = optionsMap[propertyNameString];
const serializedValue = opts && typeof opts.serialize === 'function'
? opts.serialize(rawValue)
: rawValue;
if (serializedValue === undefined) {
// an undefined property means "absent" - skip it so the driver cannot
// turn it into BSON null during serialization
continue;
}
saveableObject[propertyNameString] = stripUndefinedValues(serializedValue);
}
return saveableObject;
}
/**
* creates an identifiable object for operations that require filtering
*/
async createIdentifiableObject() {
const identifiableObject = {}; // is not exposed to outside, so any is ok here
for (const propertyNameString of this.uniqueIndexes) {
identifiableObject[propertyNameString] = this[propertyNameString];
}
return identifiableObject;
}
};
})();
export { SmartDataDbDoc };
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kb2MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLmRvYy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzlDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDdEMsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDeEQsT0FBTyxFQUFpQixtQkFBbUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzdFLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzFELE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBaUNyRTs7Ozs7R0FLRztBQUNILE1BQU0sYUFBYSxHQUFHLENBQUMsUUFBaUIsRUFBdUMsRUFBRTtJQUMvRSxJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEQsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBQ0QsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUNsRCxPQUFPLFNBQVMsS0FBSyxNQUFNLENBQUMsU0FBUyxJQUFJLFNBQVMsS0FBSyxJQUFJLENBQUM7QUFDOUQsQ0FBQyxDQUFDO0FBRUY7Ozs7Ozs7Ozs7OztHQVlHO0FBQ0gsTUFBTSxvQkFBb0IsR0FBRyxDQUFJLFFBQVcsRUFBRSxPQUF5QixFQUFLLEVBQUU7SUFDNUUsSUFBSSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksUUFBUSxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ3RELE9BQU8sUUFBUSxDQUFDO0lBQ2xCLENBQUM7SUFDRCxN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