Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
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
|
23
changelog.md
23
changelog.md
@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.7.0",
|
||||||
"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": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
90
readme.md
90
readme.md
@ -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,25 +65,35 @@ 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`, and `@svDb` to define your data models.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartDataDbDoc, Collection, unI, svDb, oid, bin, index, searchable } from '@push.rocks/smartdata';
|
import {
|
||||||
|
SmartDataDbDoc,
|
||||||
|
Collection,
|
||||||
|
unI,
|
||||||
|
svDb,
|
||||||
|
oid,
|
||||||
|
bin,
|
||||||
|
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
|
@oid() // Automatically handle as ObjectId type
|
||||||
@ -92,7 +105,7 @@ class User extends SmartDataDbDoc<User, User> {
|
|||||||
|
|
||||||
@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 +118,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 +148,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 +165,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
|
||||||
@ -232,6 +252,7 @@ const complexQuery = await Product.searchWithLucene('(wireless OR bluetooth) AND
|
|||||||
```
|
```
|
||||||
|
|
||||||
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 retrieve all searchable fields for a class
|
||||||
- `search()` method for basic search (requires MongoDB text index)
|
- `search()` method for basic search (requires MongoDB text index)
|
||||||
@ -240,6 +261,7 @@ The search functionality includes:
|
|||||||
- Automatic fallback to regex-based search if MongoDB text search fails
|
- Automatic fallback to regex-based search if MongoDB text search fails
|
||||||
|
|
||||||
### 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 +292,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 +330,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 +370,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 +393,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 +423,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 +444,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 +461,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 +477,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 +486,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 +539,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.disconnect()` 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
|
- Create MongoDB text indexes for collections that need advanced search operations
|
||||||
- Use `searchWithLucene()` for robust searches with fallback mechanisms
|
- Use `searchWithLucene()` for robust searches with fallback mechanisms
|
||||||
- Prefer field-specific searches when possible for better performance
|
- Prefer field-specific searches when possible for better performance
|
||||||
- Use simple term queries instead of boolean operators if you don't have text indexes
|
- Use simple term queries instead of boolean operators if you don't have text indexes
|
||||||
|
|
||||||
### 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
|
||||||
|
@ -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
|
||||||
@ -107,7 +107,9 @@ 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 searchWithLucene method', async () => {
|
||||||
// Using the robust searchWithLucene method
|
// Using the robust searchWithLucene method
|
||||||
const wirelessResults = await Product.searchWithLucene('wireless');
|
const wirelessResults = await Product.searchWithLucene('wireless');
|
||||||
console.log(`Found ${wirelessResults.length} products matching 'wireless' using searchWithLucene`);
|
console.log(
|
||||||
|
`Found ${wirelessResults.length} products matching 'wireless' using searchWithLucene`,
|
||||||
|
);
|
||||||
|
|
||||||
expect(wirelessResults.length).toEqual(1);
|
expect(wirelessResults.length).toEqual(1);
|
||||||
expect(wirelessResults[0].name).toEqual('AirPods');
|
expect(wirelessResults[0].name).toEqual('AirPods');
|
||||||
|
@ -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.7.0',
|
||||||
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 } 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,7 @@ 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}> = [];
|
||||||
|
|
||||||
constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
|
constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
|
||||||
// tell the collection where it belongs
|
// tell the collection where it belongs
|
||||||
@ -170,6 +171,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 +209,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 +232,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 +243,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 +257,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 +286,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;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class SmartdataDbCursor<T = any> {
|
|||||||
|
|
||||||
public async next(closeAtEnd = true) {
|
public async next(closeAtEnd = true) {
|
||||||
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();
|
||||||
|
@ -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) => {
|
||||||
|
@ -83,6 +83,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 +175,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 +193,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 +212,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 +226,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 +239,29 @@ 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,
|
||||||
|
) {
|
||||||
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
|
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 +270,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 +287,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 +298,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 +311,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);
|
||||||
@ -275,7 +331,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async search<T>(
|
public static async search<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
luceneQuery: string
|
luceneQuery: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const filter = (this as any).createSearchFilter(luceneQuery);
|
const filter = (this as any).createSearchFilter(luceneQuery);
|
||||||
return await (this as any).getInstances(filter);
|
return await (this as any).getInstances(filter);
|
||||||
@ -288,22 +344,26 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|||||||
*/
|
*/
|
||||||
public static async searchWithLucene<T>(
|
public static async searchWithLucene<T>(
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
luceneQuery: string
|
luceneQuery: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
try {
|
try {
|
||||||
const className = (this as any).className || this.name;
|
const className = (this as any).className || this.name;
|
||||||
const searchableFields = getSearchableFields(className);
|
const searchableFields = getSearchableFields(className);
|
||||||
|
|
||||||
if (searchableFields.length === 0) {
|
if (searchableFields.length === 0) {
|
||||||
console.warn(`No searchable fields defined for class ${className}, falling back to simple search`);
|
console.warn(
|
||||||
|
`No searchable fields defined for class ${className}, falling back to simple search`,
|
||||||
|
);
|
||||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
return (this as any).searchByTextAcrossFields(luceneQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple term search optimization
|
// Simple term search optimization
|
||||||
if (!luceneQuery.includes(':') &&
|
if (
|
||||||
!luceneQuery.includes(' AND ') &&
|
!luceneQuery.includes(':') &&
|
||||||
!luceneQuery.includes(' OR ') &&
|
!luceneQuery.includes(' AND ') &&
|
||||||
!luceneQuery.includes(' NOT ')) {
|
!luceneQuery.includes(' OR ') &&
|
||||||
|
!luceneQuery.includes(' NOT ')
|
||||||
|
) {
|
||||||
return (this as any).searchByTextAcrossFields(luceneQuery);
|
return (this as any).searchByTextAcrossFields(luceneQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +383,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 +392,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 +413,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 +436,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 +454,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 +488,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 +544,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"
|
||||||
|
Reference in New Issue
Block a user