UNPKG

@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
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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