Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
0834ec5c91 | |||
6a2a708ea1 | |||
1d977986f1 | |||
e325b42906 | |||
1a359d355a | |||
b5a9449d5e | |||
558f83a3d9 | |||
76ae454221 | |||
90cfc4644d | |||
0be279e5f5 | |||
9755522bba | |||
de8736e99e | |||
c430627a21 | |||
0bfebaf5b9 | |||
4733982d03 | |||
368dc27607 | |||
938b25c925 | |||
ab251858ba | |||
24371ccf78 | |||
ed1eecbab8 | |||
0d2dcec3e2 | |||
9426a21a2a |
@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Run npm prepare
|
- name: Run npm prepare
|
||||||
run: npmci npm prepare
|
run: npmci npm prepare
|
||||||
|
@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,7 +3,6 @@
|
|||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
public/
|
public/
|
||||||
pages/
|
|
||||||
|
|
||||||
# installs
|
# installs
|
||||||
node_modules/
|
node_modules/
|
||||||
@ -17,4 +16,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
# custom
|
#------# custom
|
75
changelog.md
75
changelog.md
@ -1,5 +1,80 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-04-18 - 5.9.2 - fix(documentation)
|
||||||
|
Update search API documentation to replace deprecated searchWithLucene examples with the unified search(query) API and clarify its behavior.
|
||||||
|
|
||||||
|
- Replaced 'searchWithLucene' examples with 'search(query)' in the README.
|
||||||
|
- Updated explanation to detail field-specific exact match, partial word regex search, multi-word literal matching, and handling of empty queries.
|
||||||
|
- Clarified guidelines for creating MongoDB text indexes on searchable fields for optimized search performance.
|
||||||
|
|
||||||
|
## 2025-04-18 - 5.9.1 - fix(search)
|
||||||
|
Refactor search tests to use unified search API and update text index type casting
|
||||||
|
|
||||||
|
- Replaced all calls from searchWithLucene with search in test/search tests
|
||||||
|
- Updated text index specification in the collection class to use proper type casting
|
||||||
|
|
||||||
|
## 2025-04-18 - 5.9.0 - feat(collections/search)
|
||||||
|
Improve text index creation and search fallback mechanisms in collections and document search methods
|
||||||
|
|
||||||
|
- Auto-create a compound text index on all searchable fields in SmartdataCollection with a one-time flag to prevent duplicate index creation.
|
||||||
|
- Refine the search method in SmartDataDbDoc to support exact field matches and safe regex fallback for non-Lucene queries.
|
||||||
|
|
||||||
|
## 2025-04-17 - 5.8.4 - fix(core)
|
||||||
|
Update commit metadata with no functional code changes
|
||||||
|
|
||||||
|
- Commit info and documentation refreshed
|
||||||
|
- No code or test changes detected in the diff
|
||||||
|
|
||||||
|
## 2025-04-17 - 5.8.3 - fix(readme)
|
||||||
|
Improve readme documentation on data models and connection management
|
||||||
|
|
||||||
|
- Clarify that data models use @Collection, @unI, @svDb, @index, and @searchable decorators
|
||||||
|
- Document that ObjectId and Buffer fields are stored as BSON types natively without extra decorators
|
||||||
|
- Update connection management section to use 'db.close()' instead of 'db.disconnect()'
|
||||||
|
- Revise license section to reference the MIT License without including additional legal details
|
||||||
|
|
||||||
|
## 2025-04-14 - 5.8.2 - fix(classes.doc.ts)
|
||||||
|
Ensure collection initialization before creating a cursor in getCursorExtended
|
||||||
|
|
||||||
|
- Added 'await collection.init()' to guarantee that the MongoDB collection is initialized before using the cursor
|
||||||
|
- Prevents potential runtime errors when accessing collection.mongoDbCollection
|
||||||
|
|
||||||
|
## 2025-04-14 - 5.8.1 - fix(cursor, doc)
|
||||||
|
Add explicit return types and casts to SmartdataDbCursor methods and update getCursorExtended signature in SmartDataDbDoc.
|
||||||
|
|
||||||
|
- Specify Promise<T> as return type for next() in SmartdataDbCursor and cast return value to T.
|
||||||
|
- Specify Promise<T[]> as return type for toArray() in SmartdataDbCursor and cast return value to T[].
|
||||||
|
- Update getCursorExtended to return Promise<SmartdataDbCursor<T>> for clearer type safety.
|
||||||
|
|
||||||
|
## 2025-04-14 - 5.8.0 - feat(cursor)
|
||||||
|
Add toArray method to SmartdataDbCursor to convert raw MongoDB documents into initialized class instances
|
||||||
|
|
||||||
|
- Introduced asynchronous toArray method in SmartdataDbCursor which retrieves all documents from the MongoDB cursor
|
||||||
|
- Maps each native document to a SmartDataDbDoc instance using createInstanceFromMongoDbNativeDoc for consistent API usage
|
||||||
|
|
||||||
|
## 2025-04-14 - 5.7.0 - feat(SmartDataDbDoc)
|
||||||
|
Add extended cursor method getCursorExtended for flexible cursor modifications
|
||||||
|
|
||||||
|
- Introduces getCursorExtended in classes.doc.ts to allow modifier functions for MongoDB cursors
|
||||||
|
- Wraps the modified cursor with SmartdataDbCursor for improved API consistency
|
||||||
|
- Enhances querying capabilities by enabling customized cursor transformations
|
||||||
|
|
||||||
|
## 2025-04-07 - 5.6.0 - feat(indexing)
|
||||||
|
Add support for regular index creation in documents and collections
|
||||||
|
|
||||||
|
- Implement new index decorator in classes.doc.ts to mark properties with regular indexing options
|
||||||
|
- Update SmartdataCollection to create regular indexes if defined on a document during insert
|
||||||
|
- Enhance document structure to store and utilize regular index configurations
|
||||||
|
|
||||||
|
## 2025-04-06 - 5.5.1 - fix(ci & formatting)
|
||||||
|
Minor fixes: update CI workflow image and npmci package references, adjust package.json and readme URLs, and apply consistent code formatting.
|
||||||
|
|
||||||
|
- Update image and repo URL in Gitea workflows from GitLab to code.foss.global
|
||||||
|
- Replace '@shipzone/npmci' with '@ship.zone/npmci' throughout CI scripts
|
||||||
|
- Adjust homepage and bugs URL in package.json and readme
|
||||||
|
- Apply trailing commas and consistent formatting in TypeScript source files
|
||||||
|
- Minor update to .gitignore custom section label
|
||||||
|
|
||||||
## 2025-04-06 - 5.5.0 - feat(search)
|
## 2025-04-06 - 5.5.0 - feat(search)
|
||||||
Enhance search functionality with robust Lucene query transformation and reliable fallback mechanisms
|
Enhance search functionality with robust Lucene query transformation and reliable fallback mechanisms
|
||||||
|
|
||||||
|
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartdata",
|
"name": "@push.rocks/smartdata",
|
||||||
"version": "5.5.0",
|
"version": "5.9.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
|
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@ -18,9 +18,9 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://gitlab.com/pushrocks/smartdata/issues"
|
"url": "https://code.foss.global/push.rocks/smartdata/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://code.foss.global/push.rocks/smartdata",
|
"homepage": "https://code.foss.global/push.rocks/smartdata#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.0.14",
|
"@push.rocks/lik": "^6.0.14",
|
||||||
"@push.rocks/smartdelay": "^3.0.1",
|
"@push.rocks/smartdelay": "^3.0.1",
|
||||||
@ -68,5 +68,8 @@
|
|||||||
"custom data types",
|
"custom data types",
|
||||||
"ODM"
|
"ODM"
|
||||||
],
|
],
|
||||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
154
readme.md
154
readme.md
@ -18,7 +18,7 @@ A powerful TypeScript-first MongoDB wrapper that provides advanced features for
|
|||||||
- **Enhanced Cursors**: Chainable, type-safe cursor API with memory efficient data processing
|
- **Enhanced Cursors**: Chainable, type-safe cursor API with memory efficient data processing
|
||||||
- **Type Conversion**: Automatic handling of MongoDB types like ObjectId and Binary data
|
- **Type Conversion**: Automatic handling of MongoDB types like ObjectId and Binary data
|
||||||
- **Serialization Hooks**: Custom serialization and deserialization of document properties
|
- **Serialization Hooks**: Custom serialization and deserialization of document properties
|
||||||
- **Powerful Search Capabilities**: Lucene-like query syntax with field-specific search, advanced operators, and fallback mechanisms
|
- **Powerful Search Capabilities**: Unified `search(query)` API supporting field:value exact matches, multi-field regex searches, case-insensitive matching, and automatic escaping to prevent regex injection
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ A powerful TypeScript-first MongoDB wrapper that provides advanced features for
|
|||||||
- TypeScript >= 4.x (for development)
|
- TypeScript >= 4.x (for development)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
To install `@push.rocks/smartdata`, use npm:
|
To install `@push.rocks/smartdata`, use npm:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -40,9 +41,11 @@ pnpm add @push.rocks/smartdata
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
`@push.rocks/smartdata` enables efficient data handling and operation management with a focus on using MongoDB. It leverages TypeScript for strong typing and ESM syntax for modern JavaScript usage.
|
`@push.rocks/smartdata` enables efficient data handling and operation management with a focus on using MongoDB. It leverages TypeScript for strong typing and ESM syntax for modern JavaScript usage.
|
||||||
|
|
||||||
### Setting Up and Connecting to the Database
|
### Setting Up and Connecting to the Database
|
||||||
|
|
||||||
Before interacting with the database, you need to set up and establish a connection. The `SmartdataDb` class handles connection pooling and automatic reconnection.
|
Before interacting with the database, you need to set up and establish a connection. The `SmartdataDb` class handles connection pooling and automatic reconnection.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -62,37 +65,43 @@ await db.init();
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Defining Data Models
|
### Defining Data Models
|
||||||
Data models in `@push.rocks/smartdata` are classes that represent collections and documents in your MongoDB database. Use decorators such as `@Collection`, `@unI`, and `@svDb` to define your data models.
|
|
||||||
|
Data models in `@push.rocks/smartdata` are classes that represent collections and documents in your MongoDB database. Use decorators such as `@Collection`, `@unI`, `@svDb`, `@index`, and `@searchable` to define your data models. Fields of type `ObjectId` or `Buffer` decorated with `@svDb()` will be stored as BSON ObjectId and Binary, respectively; no separate `@oid()` or `@bin()` decorators are required.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartDataDbDoc, Collection, unI, svDb, oid, bin, index, searchable } from '@push.rocks/smartdata';
|
import {
|
||||||
|
SmartDataDbDoc,
|
||||||
|
Collection,
|
||||||
|
unI,
|
||||||
|
svDb,
|
||||||
|
index,
|
||||||
|
searchable,
|
||||||
|
} from '@push.rocks/smartdata';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
@Collection(() => db) // Associate this model with the database instance
|
@Collection(() => db) // Associate this model with the database instance
|
||||||
class User extends SmartDataDbDoc<User, User> {
|
class User extends SmartDataDbDoc<User, User> {
|
||||||
@unI()
|
@unI()
|
||||||
public id: string = 'unique-user-id'; // Mark 'id' as a unique index
|
public id: string = 'unique-user-id'; // Mark 'id' as a unique index
|
||||||
|
|
||||||
@svDb()
|
@svDb()
|
||||||
@searchable() // Mark 'username' as searchable
|
@searchable() // Mark 'username' as searchable
|
||||||
public username: string; // Mark 'username' to be saved in DB
|
public username: string; // Mark 'username' to be saved in DB
|
||||||
|
|
||||||
@svDb()
|
@svDb()
|
||||||
@searchable() // Mark 'email' as searchable
|
@searchable() // Mark 'email' as searchable
|
||||||
@index() // Create a regular index for this field
|
@index() // Create a regular index for this field
|
||||||
public email: string; // Mark 'email' to be saved in DB
|
public email: string; // Mark 'email' to be saved in DB
|
||||||
|
|
||||||
@svDb()
|
@svDb()
|
||||||
@oid() // Automatically handle as ObjectId type
|
public organizationId: ObjectId; // Stored as BSON ObjectId
|
||||||
public organizationId: ObjectId; // Will be automatically converted to/from ObjectId
|
|
||||||
|
|
||||||
@svDb()
|
@svDb()
|
||||||
@bin() // Automatically handle as Binary data
|
public profilePicture: Buffer; // Stored as BSON Binary
|
||||||
public profilePicture: Buffer; // Will be automatically converted to/from Binary
|
|
||||||
|
|
||||||
@svDb({
|
@svDb({
|
||||||
serialize: (data) => JSON.stringify(data), // Custom serialization
|
serialize: (data) => JSON.stringify(data), // Custom serialization
|
||||||
deserialize: (data) => JSON.parse(data) // Custom deserialization
|
deserialize: (data) => JSON.parse(data), // Custom deserialization
|
||||||
})
|
})
|
||||||
public preferences: Record<string, any>;
|
public preferences: Record<string, any>;
|
||||||
|
|
||||||
@ -105,15 +114,18 @@ class User extends SmartDataDbDoc<User, User> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### CRUD Operations
|
### CRUD Operations
|
||||||
|
|
||||||
`@push.rocks/smartdata` simplifies CRUD operations with intuitive methods on model instances.
|
`@push.rocks/smartdata` simplifies CRUD operations with intuitive methods on model instances.
|
||||||
|
|
||||||
#### Create
|
#### Create
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const newUser = new User('myUsername', 'myEmail@example.com');
|
const newUser = new User('myUsername', 'myEmail@example.com');
|
||||||
await newUser.save(); // Save the new user to the database
|
await newUser.save(); // Save the new user to the database
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Read
|
#### Read
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Fetch a single user by a unique attribute
|
// Fetch a single user by a unique attribute
|
||||||
const user = await User.getInstance({ username: 'myUsername' });
|
const user = await User.getInstance({ username: 'myUsername' });
|
||||||
@ -132,8 +144,8 @@ await cursor.forEach(async (user, index) => {
|
|||||||
|
|
||||||
// Chain cursor methods like in the MongoDB native driver
|
// Chain cursor methods like in the MongoDB native driver
|
||||||
const paginatedCursor = await User.getCursor({ active: true })
|
const paginatedCursor = await User.getCursor({ active: true })
|
||||||
.limit(10) // Limit results
|
.limit(10) // Limit results
|
||||||
.skip(20) // Skip first 20 results
|
.skip(20) // Skip first 20 results
|
||||||
.sort({ createdAt: -1 }); // Sort by creation date descending
|
.sort({ createdAt: -1 }); // Sort by creation date descending
|
||||||
|
|
||||||
// Convert cursor to array (when you know the result set is small)
|
// Convert cursor to array (when you know the result set is small)
|
||||||
@ -149,30 +161,34 @@ await cursor.close();
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Update
|
#### Update
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Assuming 'user' is an instance of User
|
// Assuming 'user' is an instance of User
|
||||||
user.email = 'newEmail@example.com';
|
user.email = 'newEmail@example.com';
|
||||||
await user.save(); // Update the user in the database
|
await user.save(); // Update the user in the database
|
||||||
|
|
||||||
// Upsert operations (insert if not exists, update if exists)
|
// Upsert operations (insert if not exists, update if exists)
|
||||||
const upsertedUser = await User.upsert(
|
const upsertedUser = await User.upsert(
|
||||||
{ id: 'user-123' }, // Query to find the user
|
{ id: 'user-123' }, // Query to find the user
|
||||||
{ // Fields to update or insert
|
{
|
||||||
|
// Fields to update or insert
|
||||||
username: 'newUsername',
|
username: 'newUsername',
|
||||||
email: 'newEmail@example.com'
|
email: 'newEmail@example.com',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Delete
|
#### Delete
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Assuming 'user' is an instance of User
|
// Assuming 'user' is an instance of User
|
||||||
await user.delete(); // Delete the user from the database
|
await user.delete(); // Delete the user from the database
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|
||||||
### Search Functionality
|
### Search Functionality
|
||||||
|
|
||||||
SmartData provides powerful search capabilities with a Lucene-like query syntax and robust fallback mechanisms:
|
SmartData provides powerful search capabilities with a Lucene-like query syntax and robust fallback mechanisms:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -202,44 +218,34 @@ class Product extends SmartDataDbDoc<Product, Product> {
|
|||||||
const searchableFields = getSearchableFields('Product'); // ['name', 'description', 'category']
|
const searchableFields = getSearchableFields('Product'); // ['name', 'description', 'category']
|
||||||
|
|
||||||
// Basic search across all searchable fields
|
// Basic search across all searchable fields
|
||||||
const iPhoneProducts = await Product.searchWithLucene('iPhone');
|
const iphoneProducts = await Product.search('iPhone');
|
||||||
|
|
||||||
// Field-specific search
|
// Field-specific exact match
|
||||||
const electronicsProducts = await Product.searchWithLucene('category:Electronics');
|
const electronicsProducts = await Product.search('category:Electronics');
|
||||||
|
|
||||||
// Search with wildcards
|
// Partial word search (regex across all fields)
|
||||||
const macProducts = await Product.searchWithLucene('Mac*');
|
const laptopResults = await Product.search('laptop');
|
||||||
|
|
||||||
// Search in specific fields with partial words
|
// Multi-word literal search
|
||||||
const laptopResults = await Product.searchWithLucene('description:laptop');
|
const paperwhite = await Product.search('Kindle Paperwhite');
|
||||||
|
|
||||||
// Search is case-insensitive
|
// Empty query returns all documents
|
||||||
const results1 = await Product.searchWithLucene('electronics');
|
const allProducts = await Product.search('');
|
||||||
const results2 = await Product.searchWithLucene('Electronics');
|
|
||||||
// results1 and results2 will contain the same documents
|
|
||||||
|
|
||||||
// Using boolean operators (requires text index in MongoDB)
|
|
||||||
const wirelessOrLaptop = await Product.searchWithLucene('wireless OR laptop');
|
|
||||||
|
|
||||||
// Negative searches
|
|
||||||
const electronicsNotSamsung = await Product.searchWithLucene('Electronics NOT Samsung');
|
|
||||||
|
|
||||||
// Phrase searches
|
|
||||||
const exactPhrase = await Product.searchWithLucene('"high-speed blender"');
|
|
||||||
|
|
||||||
// Grouping with parentheses
|
|
||||||
const complexQuery = await Product.searchWithLucene('(wireless OR bluetooth) AND Electronics');
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The search functionality includes:
|
The search functionality includes:
|
||||||
|
|
||||||
- `@searchable()` decorator for marking fields as searchable
|
- `@searchable()` decorator for marking fields as searchable
|
||||||
- `getSearchableFields()` to retrieve all searchable fields for a class
|
- `getSearchableFields()` to list searchable fields for a model
|
||||||
- `search()` method for basic search (requires MongoDB text index)
|
- `search(query: string)` method supporting:
|
||||||
- `searchWithLucene()` method with robust fallback mechanisms
|
- Field-specific exact matches (`field:value`)
|
||||||
- Support for field-specific searches, wildcards, and boolean operators
|
- Case-insensitive partial matches across all searchable fields
|
||||||
- Automatic fallback to regex-based search if MongoDB text search fails
|
- Multi-word literal matching
|
||||||
|
- Empty queries returning all documents
|
||||||
|
- Automatic escaping of special characters to prevent regex injection
|
||||||
|
|
||||||
### EasyStore
|
### EasyStore
|
||||||
|
|
||||||
EasyStore provides a simple key-value storage system with automatic persistence:
|
EasyStore provides a simple key-value storage system with automatic persistence:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -270,6 +276,7 @@ await store.deleteKey('apiKey');
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Distributed Coordination
|
### Distributed Coordination
|
||||||
|
|
||||||
Built-in support for distributed systems with leader election:
|
Built-in support for distributed systems with leader election:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -307,21 +314,25 @@ await coordinator.stop();
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Real-time Data Watching
|
### Real-time Data Watching
|
||||||
|
|
||||||
Watch for changes in your collections with RxJS integration using MongoDB Change Streams:
|
Watch for changes in your collections with RxJS integration using MongoDB Change Streams:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Create a watcher for a specific collection with a query filter
|
// Create a watcher for a specific collection with a query filter
|
||||||
const watcher = await User.watch({
|
const watcher = await User.watch(
|
||||||
active: true // Only watch for changes to active users
|
{
|
||||||
}, {
|
active: true, // Only watch for changes to active users
|
||||||
fullDocument: true, // Include the full document in change notifications
|
},
|
||||||
bufferTimeMs: 100 // Buffer changes for 100ms to reduce notification frequency
|
{
|
||||||
});
|
fullDocument: true, // Include the full document in change notifications
|
||||||
|
bufferTimeMs: 100, // Buffer changes for 100ms to reduce notification frequency
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Subscribe to changes using RxJS
|
// Subscribe to changes using RxJS
|
||||||
watcher.changeSubject.subscribe((change) => {
|
watcher.changeSubject.subscribe((change) => {
|
||||||
console.log('Change operation:', change.operationType); // 'insert', 'update', 'delete', etc.
|
console.log('Change operation:', change.operationType); // 'insert', 'update', 'delete', etc.
|
||||||
console.log('Document changed:', change.docInstance); // The full document instance
|
console.log('Document changed:', change.docInstance); // The full document instance
|
||||||
|
|
||||||
// Handle different types of changes
|
// Handle different types of changes
|
||||||
if (change.operationType === 'insert') {
|
if (change.operationType === 'insert') {
|
||||||
@ -343,6 +354,7 @@ await watcher.stop();
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Managed Collections
|
### Managed Collections
|
||||||
|
|
||||||
For more complex data models that require additional context:
|
For more complex data models that require additional context:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -365,6 +377,7 @@ class ManagedDoc extends SmartDataDbDoc<ManagedDoc, ManagedDoc> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Automatic Indexing
|
### Automatic Indexing
|
||||||
|
|
||||||
Define indexes directly in your model class:
|
Define indexes directly in your model class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -394,6 +407,7 @@ class Product extends SmartDataDbDoc<Product, Product> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Transaction Support
|
### Transaction Support
|
||||||
|
|
||||||
Use MongoDB transactions for atomic operations:
|
Use MongoDB transactions for atomic operations:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -414,6 +428,7 @@ try {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Deep Object Queries
|
### Deep Object Queries
|
||||||
|
|
||||||
SmartData provides fully type-safe deep property queries with the `DeepQuery` type:
|
SmartData provides fully type-safe deep property queries with the `DeepQuery` type:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -430,14 +445,14 @@ class UserProfile extends SmartDataDbDoc<UserProfile, UserProfile> {
|
|||||||
address: {
|
address: {
|
||||||
city: string;
|
city: string;
|
||||||
country: string;
|
country: string;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-safe string literals for dot notation
|
// Type-safe string literals for dot notation
|
||||||
const usersInUSA = await UserProfile.getInstances({
|
const usersInUSA = await UserProfile.getInstances({
|
||||||
'user.details.address.country': 'USA'
|
'user.details.address.country': 'USA',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fully typed deep queries with the DeepQuery type
|
// Fully typed deep queries with the DeepQuery type
|
||||||
@ -446,7 +461,7 @@ import { DeepQuery } from '@push.rocks/smartdata';
|
|||||||
const typedQuery: DeepQuery<UserProfile> = {
|
const typedQuery: DeepQuery<UserProfile> = {
|
||||||
id: 'profile-id',
|
id: 'profile-id',
|
||||||
'user.details.firstName': 'John',
|
'user.details.firstName': 'John',
|
||||||
'user.details.address.country': 'USA'
|
'user.details.address.country': 'USA',
|
||||||
};
|
};
|
||||||
|
|
||||||
// TypeScript will error if paths are incorrect
|
// TypeScript will error if paths are incorrect
|
||||||
@ -455,13 +470,14 @@ const results = await UserProfile.getInstances(typedQuery);
|
|||||||
// MongoDB query operators are supported
|
// MongoDB query operators are supported
|
||||||
const operatorQuery: DeepQuery<UserProfile> = {
|
const operatorQuery: DeepQuery<UserProfile> = {
|
||||||
'user.details.address.country': 'USA',
|
'user.details.address.country': 'USA',
|
||||||
'user.details.address.city': { $in: ['New York', 'Los Angeles'] }
|
'user.details.address.city': { $in: ['New York', 'Los Angeles'] },
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredResults = await UserProfile.getInstances(operatorQuery);
|
const filteredResults = await UserProfile.getInstances(operatorQuery);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Document Lifecycle Hooks
|
### Document Lifecycle Hooks
|
||||||
|
|
||||||
Implement custom logic at different stages of a document's lifecycle:
|
Implement custom logic at different stages of a document's lifecycle:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -507,33 +523,39 @@ class Order extends SmartDataDbDoc<Order, Order> {
|
|||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
### Connection Management
|
### Connection Management
|
||||||
|
|
||||||
- Always call `db.init()` before using any database features
|
- Always call `db.init()` before using any database features
|
||||||
- Use `db.disconnect()` when shutting down your application
|
- Use `db.close()` when shutting down your application
|
||||||
- Set appropriate connection pool sizes based on your application's needs
|
- Set appropriate connection pool sizes based on your application's needs
|
||||||
|
|
||||||
### Document Design
|
### Document Design
|
||||||
|
|
||||||
- Use appropriate decorators (`@svDb`, `@unI`, `@index`, `@searchable`) to optimize database operations
|
- Use appropriate decorators (`@svDb`, `@unI`, `@index`, `@searchable`) to optimize database operations
|
||||||
- Implement type-safe models by properly extending `SmartDataDbDoc`
|
- Implement type-safe models by properly extending `SmartDataDbDoc`
|
||||||
- Consider using interfaces to define document structures separately from implementation
|
- Consider using interfaces to define document structures separately from implementation
|
||||||
- Mark fields that need to be searched with the `@searchable()` decorator
|
- Mark fields that need to be searched with the `@searchable()` decorator
|
||||||
|
|
||||||
### Search Optimization
|
### Search Optimization
|
||||||
- Create MongoDB text indexes for collections that need advanced search operations
|
|
||||||
- Use `searchWithLucene()` for robust searches with fallback mechanisms
|
- (Optional) Create MongoDB text indexes on searchable fields to speed up full-text search
|
||||||
- Prefer field-specific searches when possible for better performance
|
- Use `search(query)` for all search operations (field:value, partial matches, multi-word)
|
||||||
- Use simple term queries instead of boolean operators if you don't have text indexes
|
- Prefer field-specific exact matches when possible for optimal performance
|
||||||
|
- Avoid unnecessary complexity in query strings to keep regex searches efficient
|
||||||
|
|
||||||
### Performance Optimization
|
### Performance Optimization
|
||||||
|
|
||||||
- Use cursors for large datasets instead of loading all documents into memory
|
- Use cursors for large datasets instead of loading all documents into memory
|
||||||
- Create appropriate indexes for frequent query patterns
|
- Create appropriate indexes for frequent query patterns
|
||||||
- Use projections to limit the fields returned when you don't need the entire document
|
- Use projections to limit the fields returned when you don't need the entire document
|
||||||
|
|
||||||
### Distributed Systems
|
### Distributed Systems
|
||||||
|
|
||||||
- Implement proper error handling for leader election events
|
- Implement proper error handling for leader election events
|
||||||
- Ensure all instances have synchronized clocks when using time-based coordination
|
- Ensure all instances have synchronized clocks when using time-based coordination
|
||||||
- Use the distributed coordinator's task management features for coordinated operations
|
- Use the distributed coordinator's task management features for coordinated operations
|
||||||
|
|
||||||
### Type Safety
|
### Type Safety
|
||||||
|
|
||||||
- Take advantage of the `DeepQuery<T>` type for fully type-safe queries
|
- Take advantage of the `DeepQuery<T>` type for fully type-safe queries
|
||||||
- Define proper types for your document models to enhance IDE auto-completion
|
- Define proper types for your document models to enhance IDE auto-completion
|
||||||
- Use generic type parameters to specify exact document types when working with collections
|
- Use generic type parameters to specify exact document types when working with collections
|
||||||
@ -552,7 +574,7 @@ Please make sure to update tests as appropriate and follow our coding standards.
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository is licensed under the MIT License. For details, see [MIT License](https://opensource.org/licenses/MIT).
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@ import * as smartmongo from '@push.rocks/smartmongo';
|
|||||||
import type * as taskbuffer from '@push.rocks/taskbuffer';
|
import type * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
import * as smartdata from '../ts/index.js';
|
import * as smartdata from '../ts/index.js';
|
||||||
import { SmartdataDistributedCoordinator, DistributedClass } from '../ts/classes.distributedcoordinator.js'; // path might need adjusting
|
import {
|
||||||
|
SmartdataDistributedCoordinator,
|
||||||
|
DistributedClass,
|
||||||
|
} from '../ts/classes.distributedcoordinator.js'; // path might need adjusting
|
||||||
const totalInstances = 10;
|
const totalInstances = 10;
|
||||||
|
|
||||||
// =======================================
|
// =======================================
|
||||||
@ -20,93 +23,100 @@ tap.test('should create a testinstance as database', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should instantiate DistributedClass', async (tools) => {
|
tap.test('should instantiate DistributedClass', async (tools) => {
|
||||||
const instance = new DistributedClass();
|
const instance = new DistributedClass();
|
||||||
expect(instance).toBeInstanceOf(DistributedClass);
|
expect(instance).toBeInstanceOf(DistributedClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('DistributedClass should update the time', async (tools) => {
|
tap.test('DistributedClass should update the time', async (tools) => {
|
||||||
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
await distributedCoordinator.start();
|
await distributedCoordinator.start();
|
||||||
const initialTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
const initialTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
||||||
await distributedCoordinator.sendHeartbeat();
|
await distributedCoordinator.sendHeartbeat();
|
||||||
const updatedTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
const updatedTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
||||||
expect(updatedTime).toBeGreaterThan(initialTime);
|
expect(updatedTime).toBeGreaterThan(initialTime);
|
||||||
await distributedCoordinator.stop();
|
await distributedCoordinator.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should instantiate SmartdataDistributedCoordinator', async (tools) => {
|
tap.test('should instantiate SmartdataDistributedCoordinator', async (tools) => {
|
||||||
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
await distributedCoordinator.start();
|
await distributedCoordinator.start();
|
||||||
expect(distributedCoordinator).toBeInstanceOf(SmartdataDistributedCoordinator);
|
expect(distributedCoordinator).toBeInstanceOf(SmartdataDistributedCoordinator);
|
||||||
await distributedCoordinator.stop();
|
await distributedCoordinator.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('SmartdataDistributedCoordinator should update leader status', async (tools) => {
|
tap.test('SmartdataDistributedCoordinator should update leader status', async (tools) => {
|
||||||
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
await distributedCoordinator.start();
|
await distributedCoordinator.start();
|
||||||
await distributedCoordinator.checkAndMaybeLead();
|
await distributedCoordinator.checkAndMaybeLead();
|
||||||
expect(distributedCoordinator.ownInstance.data.elected).toBeOneOf([true, false]);
|
expect(distributedCoordinator.ownInstance.data.elected).toBeOneOf([true, false]);
|
||||||
await distributedCoordinator.stop();
|
await distributedCoordinator.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('SmartdataDistributedCoordinator should handle distributed task requests', async (tools) => {
|
tap.test(
|
||||||
|
'SmartdataDistributedCoordinator should handle distributed task requests',
|
||||||
|
async (tools) => {
|
||||||
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
await distributedCoordinator.start();
|
await distributedCoordinator.start();
|
||||||
|
|
||||||
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
||||||
submitterId: "mockSubmitter12345", // Some unique mock submitter ID
|
submitterId: 'mockSubmitter12345', // Some unique mock submitter ID
|
||||||
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
||||||
taskName: "SampleTask",
|
taskName: 'SampleTask',
|
||||||
taskVersion: "1.0.0", // Assuming it's a version string
|
taskVersion: '1.0.0', // Assuming it's a version string
|
||||||
taskExecutionTime: Date.now(),
|
taskExecutionTime: Date.now(),
|
||||||
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
||||||
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
||||||
status: 'requesting'
|
status: 'requesting',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await distributedCoordinator.fireDistributedTaskRequest(mockTaskRequest);
|
const response = await distributedCoordinator.fireDistributedTaskRequest(mockTaskRequest);
|
||||||
console.log(response) // based on your expected structure for the response
|
console.log(response); // based on your expected structure for the response
|
||||||
await distributedCoordinator.stop();
|
await distributedCoordinator.stop();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('SmartdataDistributedCoordinator should update distributed task requests', async (tools) => {
|
tap.test(
|
||||||
|
'SmartdataDistributedCoordinator should update distributed task requests',
|
||||||
|
async (tools) => {
|
||||||
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
|
||||||
await distributedCoordinator.start();
|
await distributedCoordinator.start();
|
||||||
|
|
||||||
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
||||||
submitterId: "mockSubmitter12345", // Some unique mock submitter ID
|
submitterId: 'mockSubmitter12345', // Some unique mock submitter ID
|
||||||
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
||||||
taskName: "SampleTask",
|
taskName: 'SampleTask',
|
||||||
taskVersion: "1.0.0", // Assuming it's a version string
|
taskVersion: '1.0.0', // Assuming it's a version string
|
||||||
taskExecutionTime: Date.now(),
|
taskExecutionTime: Date.now(),
|
||||||
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
||||||
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
||||||
status: 'requesting'
|
status: 'requesting',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
await distributedCoordinator.updateDistributedTaskRequest(mockTaskRequest);
|
await distributedCoordinator.updateDistributedTaskRequest(mockTaskRequest);
|
||||||
// Here, we can potentially check if a DB entry got updated or some other side-effect of the update method.
|
// Here, we can potentially check if a DB entry got updated or some other side-effect of the update method.
|
||||||
await distributedCoordinator.stop();
|
await distributedCoordinator.stop();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('should elect only one leader amongst multiple instances', async (tools) => {
|
tap.test('should elect only one leader amongst multiple instances', async (tools) => {
|
||||||
const coordinators = Array.from({ length: totalInstances }).map(() => new SmartdataDistributedCoordinator(testDb));
|
const coordinators = Array.from({ length: totalInstances }).map(
|
||||||
await Promise.all(coordinators.map(coordinator => coordinator.start()));
|
() => new SmartdataDistributedCoordinator(testDb),
|
||||||
const leaders = coordinators.filter(coordinator => coordinator.ownInstance.data.elected);
|
);
|
||||||
for (const leader of leaders) {
|
await Promise.all(coordinators.map((coordinator) => coordinator.start()));
|
||||||
console.log(leader.ownInstance);
|
const leaders = coordinators.filter((coordinator) => coordinator.ownInstance.data.elected);
|
||||||
}
|
for (const leader of leaders) {
|
||||||
expect(leaders.length).toEqual(1);
|
console.log(leader.ownInstance);
|
||||||
|
}
|
||||||
|
expect(leaders.length).toEqual(1);
|
||||||
|
|
||||||
// stopping clears a coordinator from being elected.
|
// stopping clears a coordinator from being elected.
|
||||||
await Promise.all(coordinators.map(coordinator => coordinator.stop()));
|
await Promise.all(coordinators.map((coordinator) => coordinator.stop()));
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should clean up', async () => {
|
tap.test('should clean up', async () => {
|
||||||
await smartmongoInstance.stopAndDumpToDir(`.nogit/dbdump/test.distributedcoordinator.ts`);
|
await smartmongoInstance.stopAndDumpToDir(`.nogit/dbdump/test.distributedcoordinator.ts`);
|
||||||
setTimeout(() => process.exit(), 2000);
|
setTimeout(() => process.exit(), 2000);
|
||||||
})
|
});
|
||||||
|
|
||||||
tap.start({ throwOnError: true });
|
tap.start({ throwOnError: true });
|
||||||
|
@ -56,7 +56,7 @@ tap.test('should create test products with searchable fields', async () => {
|
|||||||
new Product('Kindle Paperwhite', 'E-reader with built-in light', 'Books', 129),
|
new Product('Kindle Paperwhite', 'E-reader with built-in light', 'Books', 129),
|
||||||
new Product('Harry Potter', 'Fantasy book series about wizards', 'Books', 49),
|
new Product('Harry Potter', 'Fantasy book series about wizards', 'Books', 49),
|
||||||
new Product('Coffee Maker', 'Automatic drip coffee machine', 'Kitchen', 89),
|
new Product('Coffee Maker', 'Automatic drip coffee machine', 'Kitchen', 89),
|
||||||
new Product('Blender', 'High-speed blender for smoothies', 'Kitchen', 129)
|
new Product('Blender', 'High-speed blender for smoothies', 'Kitchen', 129),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Save all products to the database
|
// Save all products to the database
|
||||||
@ -104,19 +104,21 @@ tap.test('should search products by basic search method', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should search products with searchWithLucene method', async () => {
|
tap.test('should search products with search method', async () => {
|
||||||
// Using the robust searchWithLucene method
|
// Using the robust searchWithLucene method
|
||||||
const wirelessResults = await Product.searchWithLucene('wireless');
|
const wirelessResults = await Product.search('wireless');
|
||||||
console.log(`Found ${wirelessResults.length} products matching 'wireless' using searchWithLucene`);
|
console.log(
|
||||||
|
`Found ${wirelessResults.length} products matching 'wireless' using search`,
|
||||||
|
);
|
||||||
|
|
||||||
expect(wirelessResults.length).toEqual(1);
|
expect(wirelessResults.length).toEqual(1);
|
||||||
expect(wirelessResults[0].name).toEqual('AirPods');
|
expect(wirelessResults[0].name).toEqual('AirPods');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should search products by category with searchWithLucene', async () => {
|
tap.test('should search products by category with search', async () => {
|
||||||
// Using field-specific search with searchWithLucene
|
// Using field-specific search with searchWithLucene
|
||||||
const kitchenResults = await Product.searchWithLucene('category:Kitchen');
|
const kitchenResults = await Product.search('category:Kitchen');
|
||||||
console.log(`Found ${kitchenResults.length} products in Kitchen category using searchWithLucene`);
|
console.log(`Found ${kitchenResults.length} products in Kitchen category using search`);
|
||||||
|
|
||||||
expect(kitchenResults.length).toEqual(2);
|
expect(kitchenResults.length).toEqual(2);
|
||||||
expect(kitchenResults[0].category).toEqual('Kitchen');
|
expect(kitchenResults[0].category).toEqual('Kitchen');
|
||||||
@ -125,7 +127,7 @@ tap.test('should search products by category with searchWithLucene', async () =>
|
|||||||
|
|
||||||
tap.test('should search products with partial word matches', async () => {
|
tap.test('should search products with partial word matches', async () => {
|
||||||
// Testing partial word matches
|
// Testing partial word matches
|
||||||
const proResults = await Product.searchWithLucene('Pro');
|
const proResults = await Product.search('Pro');
|
||||||
console.log(`Found ${proResults.length} products matching 'Pro'`);
|
console.log(`Found ${proResults.length} products matching 'Pro'`);
|
||||||
|
|
||||||
// Should match both "MacBook Pro" and "professionals" in description
|
// Should match both "MacBook Pro" and "professionals" in description
|
||||||
@ -134,7 +136,7 @@ tap.test('should search products with partial word matches', async () => {
|
|||||||
|
|
||||||
tap.test('should search across multiple searchable fields', async () => {
|
tap.test('should search across multiple searchable fields', async () => {
|
||||||
// Test searching across all searchable fields
|
// Test searching across all searchable fields
|
||||||
const bookResults = await Product.searchWithLucene('book');
|
const bookResults = await Product.search('book');
|
||||||
console.log(`Found ${bookResults.length} products matching 'book' across all fields`);
|
console.log(`Found ${bookResults.length} products matching 'book' across all fields`);
|
||||||
|
|
||||||
// Should match "MacBook" in name and "Books" in category
|
// Should match "MacBook" in name and "Books" in category
|
||||||
@ -143,8 +145,8 @@ tap.test('should search across multiple searchable fields', async () => {
|
|||||||
|
|
||||||
tap.test('should handle case insensitive searches', async () => {
|
tap.test('should handle case insensitive searches', async () => {
|
||||||
// Test case insensitivity
|
// Test case insensitivity
|
||||||
const electronicsResults = await Product.searchWithLucene('electronics');
|
const electronicsResults = await Product.search('electronics');
|
||||||
const ElectronicsResults = await Product.searchWithLucene('Electronics');
|
const ElectronicsResults = await Product.search('Electronics');
|
||||||
|
|
||||||
console.log(`Found ${electronicsResults.length} products matching lowercase 'electronics'`);
|
console.log(`Found ${electronicsResults.length} products matching lowercase 'electronics'`);
|
||||||
console.log(`Found ${ElectronicsResults.length} products matching capitalized 'Electronics'`);
|
console.log(`Found ${ElectronicsResults.length} products matching capitalized 'Electronics'`);
|
||||||
@ -164,14 +166,14 @@ tap.test('should demonstrate search fallback mechanisms', async () => {
|
|||||||
|
|
||||||
// Use a simpler term that should be found in descriptions
|
// Use a simpler term that should be found in descriptions
|
||||||
// Avoid using "OR" operator which requires a text index
|
// Avoid using "OR" operator which requires a text index
|
||||||
const results = await Product.searchWithLucene('high');
|
const results = await Product.search('high');
|
||||||
console.log(`Found ${results.length} products matching 'high'`);
|
console.log(`Found ${results.length} products matching 'high'`);
|
||||||
|
|
||||||
// "High-speed blender" contains "high"
|
// "High-speed blender" contains "high"
|
||||||
expect(results.length).toBeGreaterThan(0);
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Try another fallback example that won't need $text
|
// Try another fallback example that won't need $text
|
||||||
const powerfulResults = await Product.searchWithLucene('powerful');
|
const powerfulResults = await Product.search('powerful');
|
||||||
console.log(`Found ${powerfulResults.length} products matching 'powerful'`);
|
console.log(`Found ${powerfulResults.length} products matching 'powerful'`);
|
||||||
|
|
||||||
// "Powerful laptop for professionals" contains "powerful"
|
// "Powerful laptop for professionals" contains "powerful"
|
||||||
@ -190,6 +192,35 @@ tap.test('should explain the advantages of the integrated approach', async () =>
|
|||||||
expect(true).toEqual(true);
|
expect(true).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Additional robustness tests
|
||||||
|
tap.test('should search exact name using field:value', async () => {
|
||||||
|
const nameResults = await Product.search('name:AirPods');
|
||||||
|
expect(nameResults.length).toEqual(1);
|
||||||
|
expect(nameResults[0].name).toEqual('AirPods');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should throw when searching non-searchable field', async () => {
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await Product.search('price:129');
|
||||||
|
} catch (err) {
|
||||||
|
error = err as Error;
|
||||||
|
}
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toMatch(/not searchable/);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('empty query should return all products', async () => {
|
||||||
|
const allResults = await Product.search('');
|
||||||
|
expect(allResults.length).toEqual(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should search multi-word term across fields', async () => {
|
||||||
|
const termResults = await Product.search('iPhone 12');
|
||||||
|
expect(termResults.length).toEqual(1);
|
||||||
|
expect(termResults[0].name).toEqual('iPhone 12');
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('close database connection', async () => {
|
tap.test('close database connection', async () => {
|
||||||
await testDb.mongoDb.dropDatabase();
|
await testDb.mongoDb.dropDatabase();
|
||||||
await testDb.close();
|
await testDb.close();
|
||||||
|
@ -97,7 +97,7 @@ tap.test('should save the car to the db', async (toolsArg) => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Filled database with ${counter} of ${totalCars} Cars and memory usage ${
|
`Filled database with ${counter} of ${totalCars} Cars and memory usage ${
|
||||||
process.memoryUsage().rss / 1e6
|
process.memoryUsage().rss / 1e6
|
||||||
} MB`
|
} MB`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} while (counter < totalCars);
|
} while (counter < totalCars);
|
||||||
@ -116,7 +116,7 @@ tap.test('expect to get instance of Car with shallow match', async () => {
|
|||||||
console.log(
|
console.log(
|
||||||
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
|
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
|
||||||
Date.now() - timeStart
|
Date.now() - timeStart
|
||||||
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(myCars[0].deepData.sodeep).toEqual('yes');
|
expect(myCars[0].deepData.sodeep).toEqual('yes');
|
||||||
@ -139,7 +139,7 @@ tap.test('expect to get instance of Car with deep match', async () => {
|
|||||||
console.log(
|
console.log(
|
||||||
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
|
`performed ${counter} of ${totalQueryCycles} total query cycles: took ${
|
||||||
Date.now() - timeStart
|
Date.now() - timeStart
|
||||||
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(myCars2[0].deepData.sodeep).toEqual('yes');
|
expect(myCars2[0].deepData.sodeep).toEqual('yes');
|
||||||
@ -209,7 +209,7 @@ tap.test('should store a new Truck', async () => {
|
|||||||
tap.test('should return a count', async () => {
|
tap.test('should return a count', async () => {
|
||||||
const truckCount = await Truck.getCount();
|
const truckCount = await Truck.getCount();
|
||||||
expect(truckCount).toEqual(1);
|
expect(truckCount).toEqual(1);
|
||||||
})
|
});
|
||||||
|
|
||||||
tap.test('should use a cursor', async () => {
|
tap.test('should use a cursor', async () => {
|
||||||
const cursor = await Car.getCursor({});
|
const cursor = await Car.getCursor({});
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartdata',
|
name: '@push.rocks/smartdata',
|
||||||
version: '5.5.0',
|
version: '5.9.2',
|
||||||
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
|
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { SmartdataDb } from './classes.db.js';
|
import { SmartdataDb } from './classes.db.js';
|
||||||
import { SmartdataDbCursor } from './classes.cursor.js';
|
import { SmartdataDbCursor } from './classes.cursor.js';
|
||||||
import { SmartDataDbDoc } from './classes.doc.js';
|
import { SmartDataDbDoc, type IIndexOptions, getSearchableFields } from './classes.doc.js';
|
||||||
import { SmartdataDbWatcher } from './classes.watcher.js';
|
import { SmartdataDbWatcher } from './classes.watcher.js';
|
||||||
import { CollectionFactory } from './classes.collectionfactory.js';
|
import { CollectionFactory } from './classes.collectionfactory.js';
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export interface IManager {
|
|||||||
db: SmartdataDb;
|
db: SmartdataDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setDefaultManagerForDoc = <T>(managerArg: IManager, dbDocArg: T): T => {
|
export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T): T => {
|
||||||
(dbDocArg as any).prototype.defaultManager = managerArg;
|
(dbDocArg as any).prototype.defaultManager = managerArg;
|
||||||
return dbDocArg;
|
return dbDocArg;
|
||||||
};
|
};
|
||||||
@ -127,6 +127,9 @@ export class SmartdataCollection<T> {
|
|||||||
public collectionName: string;
|
public collectionName: string;
|
||||||
public smartdataDb: SmartdataDb;
|
public smartdataDb: SmartdataDb;
|
||||||
public uniqueIndexes: string[] = [];
|
public uniqueIndexes: string[] = [];
|
||||||
|
public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
|
||||||
|
// flag to ensure text index is created only once
|
||||||
|
private textIndexCreated: boolean = false;
|
||||||
|
|
||||||
constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
|
constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
|
||||||
// tell the collection where it belongs
|
// tell the collection where it belongs
|
||||||
@ -152,6 +155,16 @@ export class SmartdataCollection<T> {
|
|||||||
console.log(`Successfully initiated Collection ${this.collectionName}`);
|
console.log(`Successfully initiated Collection ${this.collectionName}`);
|
||||||
}
|
}
|
||||||
this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName);
|
this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName);
|
||||||
|
// Auto-create a compound text index on all searchable fields
|
||||||
|
const searchableFields = getSearchableFields(this.collectionName);
|
||||||
|
if (searchableFields.length > 0 && !this.textIndexCreated) {
|
||||||
|
// Build a compound text index spec
|
||||||
|
const indexSpec: Record<string, 'text'> = {};
|
||||||
|
searchableFields.forEach(f => { indexSpec[f] = 'text'; });
|
||||||
|
// Cast to any to satisfy TypeScript IndexSpecification typing
|
||||||
|
await this.mongoDbCollection.createIndex(indexSpec as any, { name: 'smartdata_text_index' });
|
||||||
|
this.textIndexCreated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +183,24 @@ export class SmartdataCollection<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates regular indexes for the collection
|
||||||
|
*/
|
||||||
|
public createRegularIndexes(indexesArg: Array<{field: string, options: IIndexOptions}> = []) {
|
||||||
|
for (const indexDef of indexesArg) {
|
||||||
|
// Check if we've already created this index
|
||||||
|
const indexKey = indexDef.field;
|
||||||
|
if (!this.regularIndexes.some(i => i.field === indexKey)) {
|
||||||
|
this.mongoDbCollection.createIndex(
|
||||||
|
{ [indexDef.field]: 1 }, // Simple single-field index
|
||||||
|
indexDef.options
|
||||||
|
);
|
||||||
|
// Track that we've created this index
|
||||||
|
this.regularIndexes.push(indexDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* adds a validation function that all newly inserted and updated objects have to pass
|
* adds a validation function that all newly inserted and updated objects have to pass
|
||||||
*/
|
*/
|
||||||
@ -190,7 +221,7 @@ export class SmartdataCollection<T> {
|
|||||||
|
|
||||||
public async getCursor(
|
public async getCursor(
|
||||||
filterObjectArg: any,
|
filterObjectArg: any,
|
||||||
dbDocArg: typeof SmartDataDbDoc
|
dbDocArg: typeof SmartDataDbDoc,
|
||||||
): Promise<SmartdataDbCursor<any>> {
|
): Promise<SmartdataDbCursor<any>> {
|
||||||
await this.init();
|
await this.init();
|
||||||
const cursor = this.mongoDbCollection.find(filterObjectArg);
|
const cursor = this.mongoDbCollection.find(filterObjectArg);
|
||||||
@ -213,7 +244,7 @@ export class SmartdataCollection<T> {
|
|||||||
*/
|
*/
|
||||||
public async watch(
|
public async watch(
|
||||||
filterObject: any,
|
filterObject: any,
|
||||||
smartdataDbDocArg: typeof SmartDataDbDoc
|
smartdataDbDocArg: typeof SmartDataDbDoc,
|
||||||
): Promise<SmartdataDbWatcher> {
|
): Promise<SmartdataDbWatcher> {
|
||||||
await this.init();
|
await this.init();
|
||||||
const changeStream = this.mongoDbCollection.watch(
|
const changeStream = this.mongoDbCollection.watch(
|
||||||
@ -224,7 +255,7 @@ export class SmartdataCollection<T> {
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
fullDocument: 'updateLookup',
|
fullDocument: 'updateLookup',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg);
|
const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg);
|
||||||
await smartdataWatcher.readyDeferred.promise;
|
await smartdataWatcher.readyDeferred.promise;
|
||||||
@ -238,6 +269,12 @@ export class SmartdataCollection<T> {
|
|||||||
await this.init();
|
await this.init();
|
||||||
await this.checkDoc(dbDocArg);
|
await this.checkDoc(dbDocArg);
|
||||||
this.markUniqueIndexes(dbDocArg.uniqueIndexes);
|
this.markUniqueIndexes(dbDocArg.uniqueIndexes);
|
||||||
|
|
||||||
|
// Create regular indexes if available
|
||||||
|
if (dbDocArg.regularIndexes && dbDocArg.regularIndexes.length > 0) {
|
||||||
|
this.createRegularIndexes(dbDocArg.regularIndexes);
|
||||||
|
}
|
||||||
|
|
||||||
const saveableObject = await dbDocArg.createSavableObject();
|
const saveableObject = await dbDocArg.createSavableObject();
|
||||||
const result = await this.mongoDbCollection.insertOne(saveableObject);
|
const result = await this.mongoDbCollection.insertOne(saveableObject);
|
||||||
return result;
|
return result;
|
||||||
@ -261,7 +298,7 @@ export class SmartdataCollection<T> {
|
|||||||
const result = await this.mongoDbCollection.updateOne(
|
const result = await this.mongoDbCollection.updateOne(
|
||||||
identifiableObject,
|
identifiableObject,
|
||||||
{ $set: updateableObject },
|
{ $set: updateableObject },
|
||||||
{ upsert: true }
|
{ upsert: true },
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,14 @@ export class SmartdataDbCursor<T = any> {
|
|||||||
this.smartdataDbDoc = dbDocArg;
|
this.smartdataDbDoc = dbDocArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async next(closeAtEnd = true) {
|
public async next(closeAtEnd = true): Promise<T> {
|
||||||
const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(
|
const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(
|
||||||
await this.mongodbCursor.next()
|
await this.mongodbCursor.next(),
|
||||||
);
|
);
|
||||||
if (!result && closeAtEnd) {
|
if (!result && closeAtEnd) {
|
||||||
await this.close();
|
await this.close();
|
||||||
}
|
}
|
||||||
return result;
|
return result as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async forEach(forEachFuncArg: (itemArg: T) => Promise<any>, closeCursorAtEnd = true) {
|
public async forEach(forEachFuncArg: (itemArg: T) => Promise<any>, closeCursorAtEnd = true) {
|
||||||
@ -40,6 +40,11 @@ export class SmartdataDbCursor<T = any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async toArray(): Promise<T[]> {
|
||||||
|
const result = await this.mongodbCursor.toArray();
|
||||||
|
return result.map((itemArg) => this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(itemArg)) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
public async close() {
|
public async close() {
|
||||||
await this.mongodbCursor.close();
|
await this.mongodbCursor.close();
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
|
|||||||
const eligibleLeader = leaders.find(
|
const eligibleLeader = leaders.find(
|
||||||
(leader) =>
|
(leader) =>
|
||||||
leader.data.lastUpdated >=
|
leader.data.lastUpdated >=
|
||||||
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 20 })
|
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 20 }),
|
||||||
);
|
);
|
||||||
return eligibleLeader;
|
return eligibleLeader;
|
||||||
});
|
});
|
||||||
@ -178,16 +178,14 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
|
|||||||
console.log('bidding code stored.');
|
console.log('bidding code stored.');
|
||||||
});
|
});
|
||||||
console.log(`bidding for leadership...`);
|
console.log(`bidding for leadership...`);
|
||||||
await plugins.smartdelay.delayFor(
|
await plugins.smartdelay.delayFor(plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 }));
|
||||||
plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 })
|
|
||||||
);
|
|
||||||
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
let biddingInstances = await DistributedClass.getInstances({});
|
let biddingInstances = await DistributedClass.getInstances({});
|
||||||
biddingInstances = biddingInstances.filter(
|
biddingInstances = biddingInstances.filter(
|
||||||
(instanceArg) =>
|
(instanceArg) =>
|
||||||
instanceArg.data.status === 'bidding' &&
|
instanceArg.data.status === 'bidding' &&
|
||||||
instanceArg.data.lastUpdated >=
|
instanceArg.data.lastUpdated >=
|
||||||
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 10 })
|
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 10 }),
|
||||||
);
|
);
|
||||||
console.log(`found ${biddingInstances.length} bidding instances...`);
|
console.log(`found ${biddingInstances.length} bidding instances...`);
|
||||||
this.ownInstance.data.elected = true;
|
this.ownInstance.data.elected = true;
|
||||||
@ -242,7 +240,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
|
|||||||
for (const instance of allInstances) {
|
for (const instance of allInstances) {
|
||||||
if (instance.data.status === 'stopped') {
|
if (instance.data.status === 'stopped') {
|
||||||
await instance.delete();
|
await instance.delete();
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
await plugins.smartdelay.delayFor(10000);
|
await plugins.smartdelay.delayFor(10000);
|
||||||
}
|
}
|
||||||
@ -250,7 +248,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
|
|||||||
|
|
||||||
// abstract implemented methods
|
// abstract implemented methods
|
||||||
public async fireDistributedTaskRequest(
|
public async fireDistributedTaskRequest(
|
||||||
taskRequestArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
|
taskRequestArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest,
|
||||||
): Promise<plugins.taskbuffer.distributedCoordination.IDistributedTaskRequestResult> {
|
): Promise<plugins.taskbuffer.distributedCoordination.IDistributedTaskRequestResult> {
|
||||||
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
if (!this.ownInstance) {
|
if (!this.ownInstance) {
|
||||||
@ -277,7 +275,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateDistributedTaskRequest(
|
public async updateDistributedTaskRequest(
|
||||||
infoBasisArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
|
infoBasisArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
const existingInfoBasis = this.ownInstance.data.taskRequests.find((infoBasisItem) => {
|
const existingInfoBasis = this.ownInstance.data.taskRequests.find((infoBasisItem) => {
|
||||||
|
@ -61,6 +61,10 @@ export function getSearchableFields(className: string): string[] {
|
|||||||
}
|
}
|
||||||
return Array.from(searchableFieldsMap.get(className));
|
return Array.from(searchableFieldsMap.get(className));
|
||||||
}
|
}
|
||||||
|
// Escape user input for safe use in MongoDB regular expressions
|
||||||
|
function escapeForRegex(input: string): string {
|
||||||
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* unique index - decorator to mark a unique index
|
* unique index - decorator to mark a unique index
|
||||||
@ -83,6 +87,46 @@ export function unI() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for MongoDB indexes
|
||||||
|
*/
|
||||||
|
export interface IIndexOptions {
|
||||||
|
background?: boolean;
|
||||||
|
unique?: boolean;
|
||||||
|
sparse?: boolean;
|
||||||
|
expireAfterSeconds?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* index - decorator to mark a field for regular indexing
|
||||||
|
*/
|
||||||
|
export function index(options?: IIndexOptions) {
|
||||||
|
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
|
||||||
|
console.log(`called index() on >${target.constructor.name}.${key}<`);
|
||||||
|
|
||||||
|
// Initialize regular indexes array if it doesn't exist
|
||||||
|
if (!target.regularIndexes) {
|
||||||
|
target.regularIndexes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this field to regularIndexes with its options
|
||||||
|
target.regularIndexes.push({
|
||||||
|
field: key,
|
||||||
|
options: options || {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also ensure it's marked as saveable
|
||||||
|
if (!target.saveableProperties) {
|
||||||
|
target.saveableProperties = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.saveableProperties.includes(key)) {
|
||||||
|
target.saveableProperties.push(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
|
export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
|
||||||
// Special case: detect MongoDB operators and pass them through directly
|
// Special case: detect MongoDB operators and pass them through directly
|
||||||
const topLevelOperators = ['$and', '$or', '$nor', '$not', '$text', '$where', '$regex'];
|
const topLevelOperators = ['$and', '$or', '$nor', '$not', '$text', '$where', '$regex'];
|
||||||
@ -135,7 +179,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
// STATIC
|
// STATIC
|
||||||
public static createInstanceFromMongoDbNativeDoc<T>(
|
public static createInstanceFromMongoDbNativeDoc<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
mongoDbNativeDocArg: any
|
mongoDbNativeDocArg: any,
|
||||||
): T {
|
): T {
|
||||||
const newInstance = new this();
|
const newInstance = new this();
|
||||||
(newInstance as any).creationStatus = 'db';
|
(newInstance as any).creationStatus = 'db';
|
||||||
@ -153,7 +197,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async getInstances<T>(
|
public static async getInstances<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg));
|
const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg));
|
||||||
const returnArray = [];
|
const returnArray = [];
|
||||||
@ -172,7 +216,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async getInstance<T>(
|
public static async getInstance<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg));
|
const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg));
|
||||||
if (foundDoc) {
|
if (foundDoc) {
|
||||||
@ -186,7 +230,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
/**
|
/**
|
||||||
* get a unique id prefixed with the class name
|
* get a unique id prefixed with the class name
|
||||||
*/
|
*/
|
||||||
public static async getNewId<T = any>(this: plugins.tsclass.typeFest.Class<T>, lengthArg: number = 20) {
|
public static async getNewId<T = any>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
lengthArg: number = 20,
|
||||||
|
) {
|
||||||
return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`;
|
return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,16 +243,30 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async getCursor<T>(
|
public static async getCursor<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
) {
|
) {
|
||||||
const collection: SmartdataCollection<T> = (this as any).collection;
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
const cursor: SmartdataDbCursor<T> = await collection.getCursor(
|
const cursor: SmartdataDbCursor<T> = await collection.getCursor(
|
||||||
convertFilterForMongoDb(filterArg),
|
convertFilterForMongoDb(filterArg),
|
||||||
this as any as typeof SmartDataDbDoc
|
this as any as typeof SmartDataDbDoc,
|
||||||
);
|
);
|
||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getCursorExtended<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
|
modifierFunction = (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => cursorArg,
|
||||||
|
): Promise<SmartdataDbCursor<T>> {
|
||||||
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
|
await collection.init();
|
||||||
|
let cursor: plugins.mongodb.FindCursor<any> = collection.mongoDbCollection.find(
|
||||||
|
convertFilterForMongoDb(filterArg),
|
||||||
|
);
|
||||||
|
cursor = modifierFunction(cursor);
|
||||||
|
return new SmartdataDbCursor<T>(cursor, this as any as typeof SmartDataDbDoc);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* watch the collection
|
* watch the collection
|
||||||
* @param this
|
* @param this
|
||||||
@ -214,12 +275,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async watch<T>(
|
public static async watch<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
) {
|
) {
|
||||||
const collection: SmartdataCollection<T> = (this as any).collection;
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
const watcher: SmartdataDbWatcher<T> = await collection.watch(
|
const watcher: SmartdataDbWatcher<T> = await collection.watch(
|
||||||
convertFilterForMongoDb(filterArg),
|
convertFilterForMongoDb(filterArg),
|
||||||
this as any
|
this as any,
|
||||||
);
|
);
|
||||||
return watcher;
|
return watcher;
|
||||||
}
|
}
|
||||||
@ -231,7 +292,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
public static async forEach<T>(
|
public static async forEach<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
forEachFunction: (itemArg: T) => Promise<any>
|
forEachFunction: (itemArg: T) => Promise<any>,
|
||||||
) {
|
) {
|
||||||
const cursor: SmartdataDbCursor<T> = await (this as any).getCursor(filterArg);
|
const cursor: SmartdataDbCursor<T> = await (this as any).getCursor(filterArg);
|
||||||
await cursor.forEach(forEachFunction);
|
await cursor.forEach(forEachFunction);
|
||||||
@ -242,7 +303,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async getCount<T>(
|
public static async getCount<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T> = ({} as any)
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T> = {} as any,
|
||||||
) {
|
) {
|
||||||
const collection: SmartdataCollection<T> = (this as any).collection;
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
return await collection.getCount(filterArg);
|
return await collection.getCount(filterArg);
|
||||||
@ -255,7 +316,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static createSearchFilter<T>(
|
public static createSearchFilter<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
luceneQuery: string
|
luceneQuery: string,
|
||||||
): any {
|
): any {
|
||||||
const className = (this as any).className || this.name;
|
const className = (this as any).className || this.name;
|
||||||
const searchableFields = getSearchableFields(className);
|
const searchableFields = getSearchableFields(className);
|
||||||
@ -269,53 +330,38 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search documents using Lucene query syntax
|
* Search documents by text or field:value syntax, with safe regex fallback
|
||||||
* @param luceneQuery Lucene query string
|
* @param query A search term or field:value expression
|
||||||
* @returns Array of matching documents
|
* @returns Array of matching documents
|
||||||
*/
|
*/
|
||||||
public static async search<T>(
|
public static async search<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
luceneQuery: string
|
query: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const filter = (this as any).createSearchFilter(luceneQuery);
|
const className = (this as any).className || this.name;
|
||||||
return await (this as any).getInstances(filter);
|
const searchableFields = getSearchableFields(className);
|
||||||
}
|
if (searchableFields.length === 0) {
|
||||||
|
throw new Error(`No searchable fields defined for class ${className}`);
|
||||||
/**
|
|
||||||
* Search documents using Lucene query syntax with robust error handling
|
|
||||||
* @param luceneQuery The Lucene query string to search with
|
|
||||||
* @returns Array of matching documents
|
|
||||||
*/
|
|
||||||
public static async searchWithLucene<T>(
|
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
|
||||||
luceneQuery: string
|
|
||||||
): Promise<T[]> {
|
|
||||||
try {
|
|
||||||
const className = (this as any).className || this.name;
|
|
||||||
const searchableFields = getSearchableFields(className);
|
|
||||||
|
|
||||||
if (searchableFields.length === 0) {
|
|
||||||
console.warn(`No searchable fields defined for class ${className}, falling back to simple search`);
|
|
||||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple term search optimization
|
|
||||||
if (!luceneQuery.includes(':') &&
|
|
||||||
!luceneQuery.includes(' AND ') &&
|
|
||||||
!luceneQuery.includes(' OR ') &&
|
|
||||||
!luceneQuery.includes(' NOT ')) {
|
|
||||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to use the Lucene-to-MongoDB conversion
|
|
||||||
const filter = (this as any).createSearchFilter(luceneQuery);
|
|
||||||
return await (this as any).getInstances(filter);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error in searchWithLucene: ${error.message}`);
|
|
||||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
|
||||||
}
|
}
|
||||||
|
// field:value exact match (case-sensitive for non-regex fields)
|
||||||
|
const fv = query.match(/^(\w+):(.+)$/);
|
||||||
|
if (fv) {
|
||||||
|
const field = fv[1];
|
||||||
|
const value = fv[2];
|
||||||
|
if (!searchableFields.includes(field)) {
|
||||||
|
throw new Error(`Field '${field}' is not searchable for class ${className}`);
|
||||||
|
}
|
||||||
|
return await (this as any).getInstances({ [field]: value });
|
||||||
|
}
|
||||||
|
// safe regex across all searchable fields (case-insensitive)
|
||||||
|
const escaped = escapeForRegex(query);
|
||||||
|
const orConditions = searchableFields.map((field) => ({
|
||||||
|
[field]: { $regex: escaped, $options: 'i' },
|
||||||
|
}));
|
||||||
|
return await (this as any).getInstances({ $or: orConditions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search by text across all searchable fields (fallback method)
|
* Search by text across all searchable fields (fallback method)
|
||||||
* @param searchText The text to search for in all searchable fields
|
* @param searchText The text to search for in all searchable fields
|
||||||
@ -323,7 +369,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
private static async searchByTextAcrossFields<T>(
|
private static async searchByTextAcrossFields<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
searchText: string
|
searchText: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
try {
|
try {
|
||||||
const className = (this as any).className || this.name;
|
const className = (this as any).className || this.name;
|
||||||
@ -332,8 +378,8 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
// Fallback to direct filter if we have searchable fields
|
// Fallback to direct filter if we have searchable fields
|
||||||
if (searchableFields.length > 0) {
|
if (searchableFields.length > 0) {
|
||||||
// Create a simple $or query with regex for each field
|
// Create a simple $or query with regex for each field
|
||||||
const orConditions = searchableFields.map(field => ({
|
const orConditions = searchableFields.map((field) => ({
|
||||||
[field]: { $regex: searchText, $options: 'i' }
|
[field]: { $regex: searchText, $options: 'i' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filter = { $or: orConditions };
|
const filter = { $or: orConditions };
|
||||||
@ -353,8 +399,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
return allDocs.filter((doc: any) => {
|
return allDocs.filter((doc: any) => {
|
||||||
for (const field of searchableFields) {
|
for (const field of searchableFields) {
|
||||||
const value = doc[field];
|
const value = doc[field];
|
||||||
if (value && typeof value === 'string' &&
|
if (value && typeof value === 'string' && value.toLowerCase().includes(lowerSearchText)) {
|
||||||
value.toLowerCase().includes(lowerSearchText)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,13 +422,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
* updated from db in any case where doc comes from db
|
* updated from db in any case where doc comes from db
|
||||||
*/
|
*/
|
||||||
@globalSvDb()
|
@globalSvDb()
|
||||||
_createdAt: string = (new Date()).toISOString();
|
_createdAt: string = new Date().toISOString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* will be updated everytime the doc is saved
|
* will be updated everytime the doc is saved
|
||||||
*/
|
*/
|
||||||
@globalSvDb()
|
@globalSvDb()
|
||||||
_updatedAt: string = (new Date()).toISOString();
|
_updatedAt: string = new Date().toISOString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an array of saveable properties of ALL doc
|
* an array of saveable properties of ALL doc
|
||||||
@ -395,6 +440,11 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public uniqueIndexes: string[];
|
public uniqueIndexes: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* regular indexes with their options
|
||||||
|
*/
|
||||||
|
public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an array of saveable properties of a specific doc
|
* an array of saveable properties of a specific doc
|
||||||
*/
|
*/
|
||||||
@ -424,7 +474,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
const self: any = this;
|
const self: any = this;
|
||||||
let dbResult: any;
|
let dbResult: any;
|
||||||
|
|
||||||
this._updatedAt = (new Date()).toISOString();
|
this._updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
switch (this.creationStatus) {
|
switch (this.creationStatus) {
|
||||||
case 'db':
|
case 'db':
|
||||||
@ -480,10 +530,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public async createSavableObject(): Promise<TImplements> {
|
public async createSavableObject(): Promise<TImplements> {
|
||||||
const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
|
const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
|
||||||
const saveableProperties = [
|
const saveableProperties = [...this.globalSaveableProperties, ...this.saveableProperties];
|
||||||
...this.globalSaveableProperties,
|
|
||||||
...this.saveableProperties
|
|
||||||
]
|
|
||||||
for (const propertyNameString of saveableProperties) {
|
for (const propertyNameString of saveableProperties) {
|
||||||
saveableObject[propertyNameString] = this[propertyNameString];
|
saveableObject[propertyNameString] = this[propertyNameString];
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export class EasyStore<T> {
|
|||||||
private async getEasyStore(): Promise<InstanceType<typeof this.easyStoreClass>> {
|
private async getEasyStore(): Promise<InstanceType<typeof this.easyStoreClass>> {
|
||||||
if (this.easyStorePromise) {
|
if (this.easyStorePromise) {
|
||||||
return this.easyStorePromise;
|
return this.easyStorePromise;
|
||||||
};
|
}
|
||||||
|
|
||||||
// first run from here
|
// first run from here
|
||||||
const deferred = plugins.smartpromise.defer<InstanceType<typeof this.easyStoreClass>>();
|
const deferred = plugins.smartpromise.defer<InstanceType<typeof this.easyStoreClass>>();
|
||||||
|
@ -4,7 +4,17 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type NodeType = 'TERM' | 'PHRASE' | 'FIELD' | 'AND' | 'OR' | 'NOT' | 'RANGE' | 'WILDCARD' | 'FUZZY' | 'GROUP';
|
type NodeType =
|
||||||
|
| 'TERM'
|
||||||
|
| 'PHRASE'
|
||||||
|
| 'FIELD'
|
||||||
|
| 'AND'
|
||||||
|
| 'OR'
|
||||||
|
| 'NOT'
|
||||||
|
| 'RANGE'
|
||||||
|
| 'WILDCARD'
|
||||||
|
| 'FUZZY'
|
||||||
|
| 'GROUP';
|
||||||
|
|
||||||
interface QueryNode {
|
interface QueryNode {
|
||||||
type: NodeType;
|
type: NodeType;
|
||||||
@ -59,7 +69,15 @@ interface GroupNode extends QueryNode {
|
|||||||
value: AnyQueryNode;
|
value: AnyQueryNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyQueryNode = TermNode | PhraseNode | FieldNode | BooleanNode | RangeNode | WildcardNode | FuzzyNode | GroupNode;
|
type AnyQueryNode =
|
||||||
|
| TermNode
|
||||||
|
| PhraseNode
|
||||||
|
| FieldNode
|
||||||
|
| BooleanNode
|
||||||
|
| RangeNode
|
||||||
|
| WildcardNode
|
||||||
|
| FuzzyNode
|
||||||
|
| GroupNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lucene query parser
|
* Lucene query parser
|
||||||
@ -137,8 +155,7 @@ export class LuceneParser {
|
|||||||
current += char;
|
current += char;
|
||||||
|
|
||||||
// Check if current is an operator
|
// Check if current is an operator
|
||||||
if (operators.test(current) &&
|
if (operators.test(current) && (i + 1 === input.length || /\s/.test(input[i + 1]))) {
|
||||||
(i + 1 === input.length || /\s/.test(input[i + 1]))) {
|
|
||||||
tokens.push(current);
|
tokens.push(current);
|
||||||
current = '';
|
current = '';
|
||||||
}
|
}
|
||||||
@ -164,7 +181,7 @@ export class LuceneParser {
|
|||||||
return {
|
return {
|
||||||
type: token as 'AND' | 'OR',
|
type: token as 'AND' | 'OR',
|
||||||
left,
|
left,
|
||||||
right
|
right,
|
||||||
};
|
};
|
||||||
} else if (token === 'NOT' || token === '-') {
|
} else if (token === 'NOT' || token === '-') {
|
||||||
this.pos++;
|
this.pos++;
|
||||||
@ -172,7 +189,7 @@ export class LuceneParser {
|
|||||||
return {
|
return {
|
||||||
type: 'NOT',
|
type: 'NOT',
|
||||||
left,
|
left,
|
||||||
right
|
right,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,7 +319,7 @@ export class LuceneParser {
|
|||||||
lower,
|
lower,
|
||||||
upper,
|
upper,
|
||||||
includeLower,
|
includeLower,
|
||||||
includeUpper
|
includeUpper,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,8 +369,8 @@ export class LuceneToMongoTransformer {
|
|||||||
// If specific fields are provided, search across those fields
|
// If specific fields are provided, search across those fields
|
||||||
if (searchFields && searchFields.length > 0) {
|
if (searchFields && searchFields.length > 0) {
|
||||||
// Create an $or query to search across multiple fields
|
// Create an $or query to search across multiple fields
|
||||||
const orConditions = searchFields.map(field => ({
|
const orConditions = searchFields.map((field) => ({
|
||||||
[field]: { $regex: node.value, $options: 'i' }
|
[field]: { $regex: node.value, $options: 'i' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { $or: orConditions };
|
return { $or: orConditions };
|
||||||
@ -370,8 +387,8 @@ export class LuceneToMongoTransformer {
|
|||||||
private transformPhrase(node: PhraseNode, searchFields?: string[]): any {
|
private transformPhrase(node: PhraseNode, searchFields?: string[]): any {
|
||||||
// If specific fields are provided, search phrase across those fields
|
// If specific fields are provided, search phrase across those fields
|
||||||
if (searchFields && searchFields.length > 0) {
|
if (searchFields && searchFields.length > 0) {
|
||||||
const orConditions = searchFields.map(field => ({
|
const orConditions = searchFields.map((field) => ({
|
||||||
[field]: { $regex: `${node.value.replace(/\s+/g, '\\s+')}`, $options: 'i' }
|
[field]: { $regex: `${node.value.replace(/\s+/g, '\\s+')}`, $options: 'i' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { $or: orConditions };
|
return { $or: orConditions };
|
||||||
@ -397,8 +414,8 @@ export class LuceneToMongoTransformer {
|
|||||||
return {
|
return {
|
||||||
[node.field]: {
|
[node.field]: {
|
||||||
$regex: this.luceneWildcardToRegex((node.value as WildcardNode).value),
|
$regex: this.luceneWildcardToRegex((node.value as WildcardNode).value),
|
||||||
$options: 'i'
|
$options: 'i',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,8 +424,8 @@ export class LuceneToMongoTransformer {
|
|||||||
return {
|
return {
|
||||||
[node.field]: {
|
[node.field]: {
|
||||||
$regex: this.createFuzzyRegex((node.value as FuzzyNode).value),
|
$regex: this.createFuzzyRegex((node.value as FuzzyNode).value),
|
||||||
$options: 'i'
|
$options: 'i',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,8 +439,8 @@ export class LuceneToMongoTransformer {
|
|||||||
return {
|
return {
|
||||||
[node.field]: {
|
[node.field]: {
|
||||||
$regex: `${(node.value as PhraseNode).value.replace(/\s+/g, '\\s+')}`,
|
$regex: `${(node.value as PhraseNode).value.replace(/\s+/g, '\\s+')}`,
|
||||||
$options: 'i'
|
$options: 'i',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,7 +526,7 @@ export class LuceneToMongoTransformer {
|
|||||||
for (const field in leftQuery) {
|
for (const field in leftQuery) {
|
||||||
if (field !== '$or' && field !== '$and') {
|
if (field !== '$or' && field !== '$and') {
|
||||||
notConditions.push({
|
notConditions.push({
|
||||||
[field]: { $not: { $regex: searchTerm, $options: 'i' } }
|
[field]: { $not: { $regex: searchTerm, $options: 'i' } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -517,15 +534,12 @@ export class LuceneToMongoTransformer {
|
|||||||
// If left query has $or or $and, we need to handle it differently
|
// If left query has $or or $and, we need to handle it differently
|
||||||
if (leftQuery.$or) {
|
if (leftQuery.$or) {
|
||||||
return {
|
return {
|
||||||
$and: [
|
$and: [leftQuery, { $nor: [{ $or: notConditions }] }],
|
||||||
leftQuery,
|
|
||||||
{ $nor: [{ $or: notConditions }] }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Simple case - just add $not to each field
|
// Simple case - just add $not to each field
|
||||||
return {
|
return {
|
||||||
$and: [leftQuery, { $and: notConditions }]
|
$and: [leftQuery, { $and: notConditions }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -576,8 +590,8 @@ export class LuceneToMongoTransformer {
|
|||||||
|
|
||||||
// If specific fields are provided, search wildcard across those fields
|
// If specific fields are provided, search wildcard across those fields
|
||||||
if (searchFields && searchFields.length > 0) {
|
if (searchFields && searchFields.length > 0) {
|
||||||
const orConditions = searchFields.map(field => ({
|
const orConditions = searchFields.map((field) => ({
|
||||||
[field]: { $regex: regex, $options: 'i' }
|
[field]: { $regex: regex, $options: 'i' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { $or: orConditions };
|
return { $or: orConditions };
|
||||||
@ -598,8 +612,8 @@ export class LuceneToMongoTransformer {
|
|||||||
|
|
||||||
// If specific fields are provided, search fuzzy term across those fields
|
// If specific fields are provided, search fuzzy term across those fields
|
||||||
if (searchFields && searchFields.length > 0) {
|
if (searchFields && searchFields.length > 0) {
|
||||||
const orConditions = searchFields.map(field => ({
|
const orConditions = searchFields.map((field) => ({
|
||||||
[field]: { $regex: regex, $options: 'i' }
|
[field]: { $regex: regex, $options: 'i' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { $or: orConditions };
|
return { $or: orConditions };
|
||||||
@ -691,21 +705,22 @@ export class SmartdataLuceneAdapter {
|
|||||||
convert(luceneQuery: string, searchFields?: string[]): any {
|
convert(luceneQuery: string, searchFields?: string[]): any {
|
||||||
try {
|
try {
|
||||||
// For simple single term queries, create a simpler query structure
|
// For simple single term queries, create a simpler query structure
|
||||||
if (!luceneQuery.includes(':') &&
|
if (
|
||||||
!luceneQuery.includes(' AND ') &&
|
!luceneQuery.includes(':') &&
|
||||||
!luceneQuery.includes(' OR ') &&
|
!luceneQuery.includes(' AND ') &&
|
||||||
!luceneQuery.includes(' NOT ') &&
|
!luceneQuery.includes(' OR ') &&
|
||||||
!luceneQuery.includes('(') &&
|
!luceneQuery.includes(' NOT ') &&
|
||||||
!luceneQuery.includes('[')) {
|
!luceneQuery.includes('(') &&
|
||||||
|
!luceneQuery.includes('[')
|
||||||
|
) {
|
||||||
// This is a simple term, use a more direct approach
|
// This is a simple term, use a more direct approach
|
||||||
const fieldsToSearch = searchFields || this.defaultSearchFields;
|
const fieldsToSearch = searchFields || this.defaultSearchFields;
|
||||||
|
|
||||||
if (fieldsToSearch && fieldsToSearch.length > 0) {
|
if (fieldsToSearch && fieldsToSearch.length > 0) {
|
||||||
return {
|
return {
|
||||||
$or: fieldsToSearch.map(field => ({
|
$or: fieldsToSearch.map((field) => ({
|
||||||
[field]: { $regex: luceneQuery, $options: 'i' }
|
[field]: { $regex: luceneQuery, $options: 'i' },
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -730,8 +745,12 @@ export class SmartdataLuceneAdapter {
|
|||||||
*/
|
*/
|
||||||
private transformWithFields(node: AnyQueryNode, searchFields: string[]): any {
|
private transformWithFields(node: AnyQueryNode, searchFields: string[]): any {
|
||||||
// Special case for term nodes without a specific field
|
// Special case for term nodes without a specific field
|
||||||
if (node.type === 'TERM' || node.type === 'PHRASE' ||
|
if (
|
||||||
node.type === 'WILDCARD' || node.type === 'FUZZY') {
|
node.type === 'TERM' ||
|
||||||
|
node.type === 'PHRASE' ||
|
||||||
|
node.type === 'WILDCARD' ||
|
||||||
|
node.type === 'FUZZY'
|
||||||
|
) {
|
||||||
return this.transformer.transform(node, searchFields);
|
return this.transformer.transform(node, searchFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export class SmartdataDbWatcher<T = any> {
|
|||||||
public changeSubject = new plugins.smartrx.rxjs.Subject<T>();
|
public changeSubject = new plugins.smartrx.rxjs.Subject<T>();
|
||||||
constructor(
|
constructor(
|
||||||
changeStreamArg: plugins.mongodb.ChangeStream<T>,
|
changeStreamArg: plugins.mongodb.ChangeStream<T>,
|
||||||
smartdataDbDocArg: typeof SmartDataDbDoc
|
smartdataDbDocArg: typeof SmartDataDbDoc,
|
||||||
) {
|
) {
|
||||||
this.changeStream = changeStreamArg;
|
this.changeStream = changeStreamArg;
|
||||||
this.changeStream.on('change', async (item: any) => {
|
this.changeStream.on('change', async (item: any) => {
|
||||||
@ -23,7 +23,7 @@ export class SmartdataDbWatcher<T = any> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.changeSubject.next(
|
this.changeSubject.next(
|
||||||
smartdataDbDocArg.createInstanceFromMongoDbNativeDoc(item.fullDocument) as any as T
|
smartdataDbDocArg.createInstanceFromMongoDbNativeDoc(item.fullDocument) as any as T,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
plugins.smartdelay.delayFor(0).then(() => {
|
plugins.smartdelay.delayFor(0).then(() => {
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user