diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a07a8f..8c024e39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,16 @@ jobs: build: runs-on: ubuntu-latest timeout-minutes: 15 + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: node-version: [18.x, 20.x] diff --git a/README.md b/README.md index 9c3a61d6..7fff2b05 100644 --- a/README.md +++ b/README.md @@ -112,22 +112,39 @@ main(); ObjectQL isolates the "What" (Query) from the "How" (Execution). -### SQL Driver (`@objectql/driver-sql`) +### Official Drivers + +#### SQL Driver (`@objectql/driver-sql`) * Supports PostgreSQL, MySQL, SQLite, SQL Server. * **Smart Hybrid Mode:** Automatically maps defined fields to SQL columns and undefined fields to a `_jsonb` column. This gives you the speed of SQL with the flexibility of NoSQL. -### Mongo Driver (`@objectql/driver-mongo`) +#### Mongo Driver (`@objectql/driver-mongo`) * Native translation to Aggregation Pipelines. * High performance for massive datasets. -### SDK / Remote Driver (`@objectql/sdk`) +#### SDK / Remote Driver (`@objectql/sdk`) * **The Magic:** You can run ObjectQL in the **Browser**. * Instead of connecting to a DB, it connects to an ObjectQL Server API. * The API usage remains exactly the same (`repo.find(...)`), but it runs over HTTP. +### Extensibility + +ObjectQL's driver architecture supports custom database implementations. Potential databases include: + +* **Key-Value Stores:** Redis, Memcached, etcd +* **Search Engines:** Elasticsearch, OpenSearch, Algolia +* **Graph Databases:** Neo4j, ArangoDB +* **Time-Series:** InfluxDB, TimescaleDB +* **Cloud Databases:** DynamoDB, Firestore, Cosmos DB + +**Want to add support for your database?** Check out: +* [Driver Extensibility Guide](./docs/guide/drivers/extensibility.md) - Lists potential databases and their implementation complexity +* [Implementing Custom Drivers](./docs/guide/drivers/implementing-custom-driver.md) - Step-by-step guide with code examples +* [Redis Driver Example](./packages/drivers/redis/) - Reference implementation + --- ## 🛠️ Validation & Logic diff --git a/docs/guide/drivers/extensibility.md b/docs/guide/drivers/extensibility.md new file mode 100644 index 00000000..0be72d14 --- /dev/null +++ b/docs/guide/drivers/extensibility.md @@ -0,0 +1,178 @@ +# Driver Extensibility Guide + +ObjectQL is designed to support multiple database backends through its **Driver** abstraction layer. This guide explains how to extend ObjectQL with additional database types. + +## Current Official Drivers + +ObjectQL currently provides official support for: + +| Driver | Package | Databases Supported | Status | +|--------|---------|---------------------|--------| +| **SQL Driver** | `@objectql/driver-sql` | PostgreSQL, MySQL, SQLite, SQL Server | ✅ Production Ready | +| **MongoDB Driver** | `@objectql/driver-mongo` | MongoDB 4.0+ | ✅ Production Ready | +| **SDK/Remote Driver** | `@objectql/sdk` | HTTP-based ObjectQL servers | ✅ Production Ready | + +## Potential Database Types for Extension + +ObjectQL's Driver interface can theoretically support any database system. Here are common database types that could be implemented: + +### Key-Value Stores + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **Redis** | Caching, real-time data, pub/sub | 🟢 Low - Simple key-value operations | +| **Memcached** | Distributed caching | 🟢 Low - Basic get/set operations | +| **etcd** | Configuration management, service discovery | 🟡 Medium - Hierarchical keys | + +### Document Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **CouchDB** | Multi-master replication, offline-first | 🟡 Medium - Similar to MongoDB | +| **Firebase/Firestore** | Real-time sync, mobile apps | 🟡 Medium - Cloud-native features | +| **RavenDB** | .NET integration, ACID transactions | 🟡 Medium - Advanced indexing | + +### Wide Column Stores + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **Cassandra** | High availability, time-series data | 🔴 High - Distributed architecture | +| **HBase** | Hadoop ecosystem, big data | 🔴 High - Complex data model | +| **DynamoDB** | AWS-native, serverless | 🟡 Medium - Single-table design patterns | + +### Search Engines + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **Elasticsearch** | Full-text search, analytics | 🟡 Medium - Query DSL mapping | +| **OpenSearch** | Fork of Elasticsearch, AWS managed | 🟡 Medium - Similar to Elasticsearch | +| **Algolia** | Hosted search, real-time indexing | 🟢 Low - REST API based | +| **Meilisearch** | Typo-tolerant search | 🟢 Low - Simple REST API | + +### Graph Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **Neo4j** | Social networks, recommendation engines | 🔴 High - Cypher query language | +| **ArangoDB** | Multi-model (graph + document) | 🟡 Medium - AQL query language | +| **OrientDB** | Multi-model graph database | 🟡 Medium - SQL-like syntax | + +### Time-Series Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **InfluxDB** | Metrics, IoT, monitoring | 🟡 Medium - Time-based queries | +| **TimescaleDB** | PostgreSQL extension for time-series | 🟢 Low - SQL compatible | +| **Prometheus** | Monitoring and alerting | 🟡 Medium - PromQL query language | + +### NewSQL Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **CockroachDB** | Distributed SQL, PostgreSQL compatible | 🟢 Low - PostgreSQL protocol | +| **TiDB** | MySQL compatible, horizontal scaling | 🟢 Low - MySQL protocol | +| **YugabyteDB** | PostgreSQL compatible, cloud-native | 🟢 Low - PostgreSQL protocol | + +### Cloud-Native Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **AWS DynamoDB** | Serverless, auto-scaling | 🟡 Medium - NoSQL patterns | +| **Google Cloud Firestore** | Real-time sync, mobile | 🟡 Medium - Document-based | +| **Azure Cosmos DB** | Multi-model, global distribution | 🟡 Medium - Multiple APIs | +| **Supabase** | PostgreSQL-as-a-service | 🟢 Low - PostgreSQL protocol | +| **PlanetScale** | MySQL-compatible, serverless | 🟢 Low - MySQL protocol | + +### Columnar Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **ClickHouse** | OLAP, analytics, data warehousing | 🔴 High - Column-oriented queries | +| **Apache Druid** | Real-time analytics | 🔴 High - Complex aggregations | + +### Embedded Databases + +| Database | Use Case | Implementation Complexity | +|----------|----------|--------------------------| +| **LevelDB** | Embedded key-value store | 🟢 Low - Simple operations | +| **RocksDB** | High-performance embedded DB | 🟢 Low - LevelDB-compatible | +| **LMDB** | Memory-mapped key-value store | 🟢 Low - Fast read operations | + +## Implementation Complexity Guide + +- 🟢 **Low Complexity** (1-2 weeks): Database has SQL compatibility or simple REST API, straightforward query mapping +- 🟡 **Medium Complexity** (3-6 weeks): Custom query language, moderate feature mapping required +- 🔴 **High Complexity** (2-3 months): Distributed systems, complex data models, significant architectural differences + +## Choosing a Database to Implement + +When deciding which database to add support for, consider: + +### 1. **Use Case Alignment** +- Does the database solve a specific problem for ObjectQL users? +- Does it complement existing drivers? + +### 2. **Community Demand** +- Is there active interest in this database? +- Are there existing GitHub issues requesting it? + +### 3. **Technical Feasibility** +- How well does the database's data model map to ObjectQL's abstraction? +- Does it support required operations (CRUD, filters, sorting)? + +### 4. **Maintenance Burden** +- Is the database actively maintained? +- Does it have a stable API? +- Is there good documentation? + +### 5. **Ecosystem Maturity** +- Are there quality Node.js/TypeScript clients? +- Is the client library actively maintained? + +## Recommended First Extensions + +Based on community needs and implementation complexity, we recommend prioritizing: + +1. **Redis Driver** - High demand, simple implementation, excellent for caching +2. **Elasticsearch Driver** - Popular for search features, clear use case +3. **DynamoDB Driver** - AWS ecosystem, serverless applications +4. **ClickHouse Driver** - Analytics and reporting use cases + +## Getting Started + +To implement a custom driver: + +1. Review the [Driver Implementation Guide](./implementing-custom-driver.md) +2. Study existing driver implementations: + - [`@objectql/driver-sql`](../../../packages/drivers/sql/src/index.ts) - SQL databases + - [`@objectql/driver-mongo`](../../../packages/drivers/mongo/src/index.ts) - MongoDB +3. Review the [Driver Interface](../../../packages/foundation/types/src/driver.ts) +4. Follow the [Driver Testing Guide](./testing-drivers.md) (coming soon) + +## Community Drivers + +We encourage the community to create and maintain third-party drivers for additional databases. If you've implemented a driver, please: + +1. Follow the ObjectQL driver conventions +2. Include comprehensive tests +3. Document configuration and usage +4. Submit a PR to add your driver to this list + +### Publishing Community Drivers + +Name your package following the convention: `@your-org/objectql-driver-` + +Example: `@acme/objectql-driver-redis` + +## Need Help? + +- 📖 Read the [Driver Implementation Guide](./implementing-custom-driver.md) +- 💬 Join the [ObjectQL Discord](https://discord.gg/objectql) (if available) +- 🐛 [Open a GitHub Issue](https://github.com/objectstack-ai/objectql/issues) +- 📧 Contact the maintainers + +## Related Documentation + +- [SQL Driver Documentation](./sql.md) +- [MongoDB Driver Documentation](./mongo.md) +- [Driver Interface Reference](../../../packages/foundation/types/src/driver.ts) diff --git a/docs/guide/drivers/implementing-custom-driver.md b/docs/guide/drivers/implementing-custom-driver.md new file mode 100644 index 00000000..212d0135 --- /dev/null +++ b/docs/guide/drivers/implementing-custom-driver.md @@ -0,0 +1,552 @@ +# Implementing a Custom Driver + +This guide walks you through implementing a custom database driver for ObjectQL. By the end, you'll understand how to create a driver that seamlessly integrates with ObjectQL's universal data protocol. + +## Table of Contents + +1. [Understanding the Driver Interface](#understanding-the-driver-interface) +2. [Implementation Steps](#implementation-steps) +3. [Best Practices](#best-practices) +4. [Testing Your Driver](#testing-your-driver) +5. [Example: Redis Driver](#example-redis-driver) + +## Understanding the Driver Interface + +All ObjectQL drivers implement the `Driver` interface defined in `@objectql/types/src/driver.ts`: + +```typescript +export interface Driver { + // Basic CRUD Operations + find(objectName: string, query: any, options?: any): Promise; + findOne(objectName: string, id: string | number, query?: any, options?: any): Promise; + create(objectName: string, data: any, options?: any): Promise; + update(objectName: string, id: string | number, data: any, options?: any): Promise; + delete(objectName: string, id: string | number, options?: any): Promise; + count(objectName: string, filters: any, options?: any): Promise; + + // Schema / Lifecycle (Optional) + init?(objects: any[]): Promise; + introspectSchema?(): Promise; + + // Advanced Operations (Optional) + aggregate?(objectName: string, query: any, options?: any): Promise; + distinct?(objectName: string, field: string, filters?: any, options?: any): Promise; + + // Bulk / Atomic Operations (Optional) + createMany?(objectName: string, data: any[], options?: any): Promise; + updateMany?(objectName: string, filters: any, data: any, options?: any): Promise; + deleteMany?(objectName: string, filters: any, options?: any): Promise; + findOneAndUpdate?(objectName: string, filters: any, update: any, options?: any): Promise; + + // Transaction Support (Optional) + beginTransaction?(): Promise; + commitTransaction?(trx: any): Promise; + rollbackTransaction?(trx: any): Promise; + + // Connection Management (Optional) + disconnect?(): Promise; +} +``` + +### Required Methods + +These methods **must** be implemented: + +- `find()` - Query multiple records +- `findOne()` - Get a single record by ID +- `create()` - Create a new record +- `update()` - Update an existing record +- `delete()` - Delete a record +- `count()` - Count records matching filters + +### Optional Methods + +These methods enhance functionality but are not required: + +- `init()` - Initialize database schema +- `introspectSchema()` - Read existing database schema +- `aggregate()` - Perform aggregation operations +- `distinct()` - Get distinct values +- `createMany()`, `updateMany()`, `deleteMany()` - Bulk operations +- `beginTransaction()`, `commitTransaction()`, `rollbackTransaction()` - Transaction support +- `disconnect()` - Clean up resources + +## Implementation Steps + +### Step 1: Set Up the Package + +Create a new package in `packages/drivers//`: + +```bash +mkdir -p packages/drivers/redis +cd packages/drivers/redis +``` + +Create `package.json`: + +```json +{ + "name": "@objectql/driver-redis", + "version": "1.0.0", + "description": "Redis driver for ObjectQL", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "dependencies": { + "@objectql/types": "workspace:*", + "redis": "^4.6.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "jest": "^29.0.0", + "@types/jest": "^29.0.0" + } +} +``` + +Create `tsconfig.json`: + +```json +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} +``` + +### Step 2: Create the Driver Class + +Create `src/index.ts`: + +```typescript +import { Driver } from '@objectql/types'; +import { createClient, RedisClientType } from 'redis'; + +export class RedisDriver implements Driver { + private client: RedisClientType; + private config: any; + private connected: Promise; + + constructor(config: { url: string }) { + this.config = config; + this.client = createClient({ url: config.url }); + this.connected = this.connect(); + } + + private async connect(): Promise { + await this.client.connect(); + } + + // Implement required methods... +} +``` + +### Step 3: Implement Core CRUD Methods + +#### `find()` + +The `find()` method must handle: +- **Filters**: Array of filter conditions +- **Sorting**: Sort order specifications +- **Pagination**: `skip` and `limit` +- **Field Projection**: `fields` array + +```typescript +async find(objectName: string, query: any, options?: any): Promise { + await this.connected; + + // Get all keys for this object type + const pattern = `${objectName}:*`; + const keys = await this.client.keys(pattern); + + // Retrieve all documents + let results: any[] = []; + for (const key of keys) { + const data = await this.client.get(key); + if (data) { + const doc = JSON.parse(data); + results.push(doc); + } + } + + // Apply filters + if (query.filters) { + results = this.applyFilters(results, query.filters); + } + + // Apply sorting + if (query.sort) { + results = this.applySort(results, query.sort); + } + + // Apply pagination + if (query.skip) { + results = results.slice(query.skip); + } + if (query.limit) { + results = results.slice(0, query.limit); + } + + // Apply field projection + if (query.fields) { + results = results.map(doc => this.projectFields(doc, query.fields)); + } + + return results; +} +``` + +#### `findOne()` + +```typescript +async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise { + await this.connected; + + const key = `${objectName}:${id}`; + const data = await this.client.get(key); + + if (!data) { + return null; + } + + return JSON.parse(data); +} +``` + +#### `create()` + +```typescript +async create(objectName: string, data: any, options?: any): Promise { + await this.connected; + + // Generate ID if not provided + const id = data.id || this.generateId(); + const doc = { + ...data, + id, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + const key = `${objectName}:${id}`; + await this.client.set(key, JSON.stringify(doc)); + + return doc; +} +``` + +#### `update()` + +```typescript +async update(objectName: string, id: string | number, data: any, options?: any): Promise { + await this.connected; + + const key = `${objectName}:${id}`; + const existing = await this.client.get(key); + + if (!existing) { + throw new Error(`Record not found: ${objectName}:${id}`); + } + + const doc = { + ...JSON.parse(existing), + ...data, + id, + updated_at: new Date().toISOString() + }; + + await this.client.set(key, JSON.stringify(doc)); + + return doc; +} +``` + +#### `delete()` + +```typescript +async delete(objectName: string, id: string | number, options?: any): Promise { + await this.connected; + + const key = `${objectName}:${id}`; + const result = await this.client.del(key); + + return result > 0; +} +``` + +#### `count()` + +```typescript +async count(objectName: string, filters: any, options?: any): Promise { + await this.connected; + + const pattern = `${objectName}:*`; + const keys = await this.client.keys(pattern); + + if (!filters) { + return keys.length; + } + + // Count only records matching filters + let count = 0; + for (const key of keys) { + const data = await this.client.get(key); + if (data) { + const doc = JSON.parse(data); + if (this.matchesFilters(doc, filters)) { + count++; + } + } + } + + return count; +} +``` + +### Step 4: Implement Filter Logic + +ObjectQL uses a universal filter format: + +```typescript +// Example filters: +[ + ['name', '=', 'John'], // Simple equality + 'or', // Logical operator + ['age', '>', 25], // Comparison + 'and', + ['status', 'in', ['active', 'pending']] // IN operator +] +``` + +Implement filter matching: + +```typescript +private applyFilters(records: any[], filters: any[]): any[] { + return records.filter(record => this.matchesFilters(record, filters)); +} + +private matchesFilters(record: any, filters: any[]): boolean { + if (!filters || filters.length === 0) { + return true; + } + + let result = true; + let nextJoin = 'and'; + + for (const item of filters) { + if (typeof item === 'string') { + // Logical operator + nextJoin = item.toLowerCase(); + continue; + } + + if (Array.isArray(item)) { + const [field, operator, value] = item; + const matches = this.evaluateCondition(record[field], operator, value); + + if (nextJoin === 'and') { + result = result && matches; + } else { + result = result || matches; + } + + nextJoin = 'and'; // Reset to default + } + } + + return result; +} + +private evaluateCondition(fieldValue: any, operator: string, compareValue: any): boolean { + switch (operator) { + case '=': + return fieldValue === compareValue; + case '!=': + return fieldValue !== compareValue; + case '>': + return fieldValue > compareValue; + case '>=': + return fieldValue >= compareValue; + case '<': + return fieldValue < compareValue; + case '<=': + return fieldValue <= compareValue; + case 'in': + return Array.isArray(compareValue) && compareValue.includes(fieldValue); + case 'nin': + return Array.isArray(compareValue) && !compareValue.includes(fieldValue); + case 'contains': + return String(fieldValue).includes(String(compareValue)); + default: + return false; + } +} +``` + +### Step 5: Implement Sorting + +```typescript +private applySort(records: any[], sort: any[]): any[] { + const sorted = [...records]; + + // Apply sorts in reverse order for correct precedence + for (let i = sort.length - 1; i >= 0; i--) { + const [field, direction] = Array.isArray(sort[i]) + ? sort[i] + : [sort[i].field, sort[i].order || 'asc']; + + sorted.sort((a, b) => { + const aVal = a[field]; + const bVal = b[field]; + + if (aVal < bVal) return direction === 'asc' ? -1 : 1; + if (aVal > bVal) return direction === 'asc' ? 1 : -1; + return 0; + }); + } + + return sorted; +} +``` + +### Step 6: Add Cleanup + +```typescript +async disconnect(): Promise { + await this.client.quit(); +} +``` + +### Step 7: Add Helper Methods + +```typescript +private generateId(): string { + // Simple UUID v4 generation + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +private projectFields(doc: any, fields: string[]): any { + const result: any = {}; + for (const field of fields) { + if (doc[field] !== undefined) { + result[field] = doc[field]; + } + } + return result; +} +``` + +## Best Practices + +### 1. **Consistent ID Handling** + +- Use `id` as the primary key field name (not `_id`) +- Support both string and number IDs +- Auto-generate IDs when not provided + +### 2. **Error Handling** + +```typescript +import { ObjectQLError } from '@objectql/types'; + +async create(objectName: string, data: any): Promise { + try { + // Implementation + } catch (error) { + throw new ObjectQLError({ + code: 'CREATE_FAILED', + message: `Failed to create ${objectName}: ${error.message}`, + details: { objectName, error } + }); + } +} +``` + +### 3. **TypeScript Strict Mode** + +- Enable `strict: true` in `tsconfig.json` +- Avoid using `any` where possible +- Define proper types for configuration + +### 4. **Async/Await Consistency** + +- All driver methods should be `async` +- Always await database operations +- Handle connection pooling properly + +### 5. **Performance Optimization** + +- Use connection pooling +- Batch operations when possible +- Implement proper indexing strategies + +### 6. **Testing** + +Create comprehensive tests in `test/index.test.ts`: + +```typescript +import { RedisDriver } from '../src'; + +describe('RedisDriver', () => { + let driver: RedisDriver; + + beforeAll(async () => { + driver = new RedisDriver({ url: 'redis://localhost:6379' }); + }); + + afterAll(async () => { + await driver.disconnect(); + }); + + describe('CRUD Operations', () => { + it('should create a record', async () => { + const result = await driver.create('users', { + name: 'Alice', + email: 'alice@example.com' + }); + + expect(result).toHaveProperty('id'); + expect(result.name).toBe('Alice'); + }); + + // Add more tests... + }); +}); +``` + +## Example: Redis Driver + +See the complete Redis driver implementation in [`packages/drivers/redis/`](../../../packages/drivers/redis/) (if available). + +## Publishing Your Driver + +1. **Test Thoroughly**: Ensure all required methods work correctly +2. **Document**: Create clear README with usage examples +3. **Version**: Follow semantic versioning (SemVer) +4. **Publish**: Publish to npm with appropriate tags + +```bash +npm publish --access public +``` + +## Getting Help + +- Review existing drivers: [SQL](../../../packages/drivers/sql/), [MongoDB](../../../packages/drivers/mongo/) +- Check the [Driver Interface](../../../packages/foundation/types/src/driver.ts) +- Open an issue on [GitHub](https://github.com/objectstack-ai/objectql/issues) + +## Next Steps + +- Read the [Driver Extensibility Guide](./extensibility.md) +- Study the [SQL Driver Implementation](../../../packages/drivers/sql/src/index.ts) +- Join the ObjectQL community for support diff --git a/docs/guide/drivers/index.md b/docs/guide/drivers/index.md index 645e8475..3c0c430a 100644 --- a/docs/guide/drivers/index.md +++ b/docs/guide/drivers/index.md @@ -11,6 +11,13 @@ We currently support the following official drivers: * **[SQL Driver](./sql)**: Supports PostgreSQL, MySQL, SQLite, MSSQL, etc. * **[MongoDB Driver](./mongo)**: Supports MongoDB. +## Extensibility + +ObjectQL is designed to support additional database types through custom drivers. + +* **[Driver Extensibility Guide](./extensibility)**: Learn about potential database types that can be supported and how to choose the right one for your needs. +* **[Implementing a Custom Driver](./implementing-custom-driver)**: Step-by-step guide to building your own database driver for ObjectQL. + ## Unified ID Field ObjectQL provides a **consistent API** across all database drivers by standardizing on the `id` field name for primary keys: diff --git a/packages/drivers/TEST_COVERAGE.md b/packages/drivers/TEST_COVERAGE.md index 87baf665..6eaf8ef4 100644 --- a/packages/drivers/TEST_COVERAGE.md +++ b/packages/drivers/TEST_COVERAGE.md @@ -6,7 +6,9 @@ This document describes the comprehensive test coverage for ObjectQL database dr The test suite ensures all database drivers implement the `Driver` interface correctly and handle edge cases properly. -## SQL Driver (SqlDriver) +## Official Production Drivers + +### SQL Driver (SqlDriver) Location: `packages/drivers/sql/test/` @@ -132,7 +134,63 @@ Location: `packages/drivers/mongo/test/` ### Total MongoDB Driver Tests: 42 tests -## Running Tests +--- + +## Example Driver Implementations + +### Redis Driver (RedisDriver) - Example/Template + +Location: `packages/drivers/redis/test/` + +**Status:** ⚠️ **Example Implementation Only** - Not for production use + +The Redis driver is provided as a **reference implementation** to demonstrate how to create custom ObjectQL drivers. It shows the complete pattern but has performance limitations. + +#### Test Files + +1. **index.test.ts** - Basic functionality (20+ tests) + - Connection management + - CRUD operations (Create, Read, Update, Delete) + - Query filtering (equality, comparison, OR logic, contains) + - Query options (sorting, pagination, field projection) + - Count operations + +#### Purpose + +- Educational reference for driver developers +- Template for creating new database drivers +- Demonstrates ObjectQL driver interface implementation +- Shows filter, sort, and pagination logic + +#### Known Limitations + +- Uses Redis KEYS command (scans all keys - inefficient for production) +- All filtering and sorting done in-memory +- Not suitable for large datasets (> 10K records) +- No transaction support +- No aggregation support + +#### Production Recommendations + +For production Redis support, enhance with: +- RedisJSON module for native JSON queries +- RedisSearch for indexed queries +- Secondary indexes using Redis Sets +- Cursor-based pagination +- Connection pooling + +### Total Redis Driver Tests: ~20 tests (Example only) + +--- + +## Test Coverage Summary + +| Driver | Files | Tests | Status | +|--------|-------|-------|--------| +| SQL (SqlDriver) | 3 | 54 | ✅ All Passing (Production) | +| MongoDB (MongoDriver) | 2 | 42 | ✅ All Passing (Production, 39 skip without MongoDB) | +| Redis (RedisDriver) | 1 | ~20 | ⚠️ Example/Template Only | +| **Production Total** | **5** | **96** | **✅** | ### Run all driver tests ```bash diff --git a/packages/drivers/redis/CHANGELOG.md b/packages/drivers/redis/CHANGELOG.md new file mode 100644 index 00000000..f02c865b --- /dev/null +++ b/packages/drivers/redis/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog - @objectql/driver-redis + +All notable changes to the Redis driver will be documented in this file. + +## [0.1.0] - 2026-01-15 + +### Added +- Initial example implementation of Redis driver for ObjectQL +- Basic CRUD operations (Create, Read, Update, Delete) +- Query filtering support (in-memory) +- Sorting support (in-memory) +- Pagination (skip/limit) +- Count operations +- Comprehensive test suite +- Documentation and usage examples + +### Notes +- This is an **example/template implementation** for educational purposes +- Not recommended for production use with large datasets due to full key scanning +- Serves as a reference for creating custom ObjectQL drivers + +### Known Limitations +- Uses KEYS command which scans all keys (inefficient for large datasets) +- All filtering and sorting done in-memory +- No native aggregation support +- No transaction support +- No schema introspection + +### Recommendations for Production +- Implement RedisJSON module integration +- Add RedisSearch for indexed queries +- Create secondary indexes using Redis Sets +- Implement cursor-based pagination +- Add connection pooling and retry logic diff --git a/packages/drivers/redis/README.md b/packages/drivers/redis/README.md new file mode 100644 index 00000000..67fa8935 --- /dev/null +++ b/packages/drivers/redis/README.md @@ -0,0 +1,253 @@ +# Redis Driver for ObjectQL (Example Implementation) + +> ⚠️ **Note**: This is an **example/template implementation** to demonstrate how to create custom ObjectQL drivers. It is not production-ready and serves as a reference for driver development. + +## Overview + +The Redis Driver is a reference implementation showing how to adapt a key-value store (Redis) to work with ObjectQL's universal data protocol. While Redis is primarily designed for caching and simple key-value operations, this driver demonstrates how to map ObjectQL's rich query interface to a simpler database model. + +## Features + +- ✅ Basic CRUD operations (Create, Read, Update, Delete) +- ✅ Query filtering (in-memory) +- ✅ Sorting (in-memory) +- ✅ Pagination (skip/limit) +- ✅ Count operations +- ⚠️ Limited performance for complex queries (scans all keys) +- ❌ No native aggregation support +- ❌ No transaction support +- ❌ No schema introspection + +## Use Cases + +This driver is suitable for: + +- **Caching Layer**: Store frequently accessed data +- **Session Storage**: User sessions and temporary data +- **Simple Key-Value Storage**: When you don't need complex queries +- **Development/Testing**: Quick prototyping without a full database + +## Installation + +```bash +npm install @objectql/driver-redis redis +``` + +## Configuration + +```typescript +import { ObjectQL } from '@objectql/core'; +import { RedisDriver } from '@objectql/driver-redis'; + +const driver = new RedisDriver({ + url: 'redis://localhost:6379', + // Optional: Redis client options + options: { + password: 'your-password', + database: 0 + } +}); + +const app = new ObjectQL({ + driver: driver +}); + +await app.init(); +``` + +## Basic Usage + +```typescript +// Create a record +const user = await app.create('users', { + name: 'Alice', + email: 'alice@example.com', + role: 'admin' +}); +console.log(user.id); // Auto-generated ID + +// Find records +const users = await app.find('users', { + filters: [['role', '=', 'admin']], + sort: [['name', 'asc']], + limit: 10 +}); + +// Update a record +await app.update('users', user.id, { + email: 'alice.new@example.com' +}); + +// Delete a record +await app.delete('users', user.id); +``` + +## Performance Considerations + +### ⚠️ Important Limitations + +This Redis driver uses **full key scanning** for queries, which means: + +1. **Find Operations**: Scans ALL keys matching `objectName:*` pattern +2. **Filters**: Applied in-memory after loading all records +3. **Count**: Loads all records to count matches + +### Performance Impact + +- **Small Datasets** (< 1000 records): ✅ Acceptable +- **Medium Datasets** (1K-10K records): ⚠️ Slow for complex queries +- **Large Datasets** (> 10K records): ❌ Not recommended + +### Optimization Strategies + +For production use, consider: + +1. **Redis Modules**: Use RedisJSON or RedisSearch for better query support +2. **Indexing**: Implement secondary indexes using Redis Sets +3. **Hybrid Approach**: Use Redis for caching, another driver for queries +4. **Sharding**: Distribute data across multiple Redis instances + +## How Data is Stored + +Records are stored as JSON strings with keys following the pattern: + +``` +objectName:id +``` + +Example: +``` +users:user-123 → {"id":"user-123","name":"Alice","email":"alice@example.com","created_at":"2026-01-15T00:00:00.000Z"} +``` + +## API Reference + +### Constructor + +```typescript +new RedisDriver(config: RedisDriverConfig) +``` + +**Config Options:** +- `url` (string, required): Redis connection URL +- `options` (object, optional): Additional Redis client options + +### Methods + +All standard Driver interface methods are implemented: + +- `find(objectName, query, options)` - Query multiple records +- `findOne(objectName, id, query, options)` - Get single record by ID +- `create(objectName, data, options)` - Create new record +- `update(objectName, id, data, options)` - Update existing record +- `delete(objectName, id, options)` - Delete record +- `count(objectName, filters, options)` - Count matching records +- `disconnect()` - Close Redis connection + +## Example: Using as Cache Layer + +Redis works great as a caching layer in front of another driver: + +```typescript +import { SqlDriver } from '@objectql/driver-sql'; +import { RedisDriver } from '@objectql/driver-redis'; + +// Primary database +const sqlDriver = new SqlDriver({ + client: 'pg', + connection: process.env.DATABASE_URL +}); + +// Cache layer +const redisDriver = new RedisDriver({ + url: process.env.REDIS_URL +}); + +// Use SQL for writes, Redis for cached reads +const app = new ObjectQL({ + datasources: { + default: sqlDriver, + cache: redisDriver + } +}); +``` + +## Development + +### Building + +```bash +pnpm build +``` + +### Testing + +```bash +# Start Redis +docker run -d -p 6379:6379 redis:latest + +# Run tests +pnpm test +``` + +### Project Structure + +``` +packages/drivers/redis/ +├── src/ +│ └── index.ts # Main driver implementation +├── test/ +│ └── index.test.ts # Unit tests +├── package.json +├── tsconfig.json +├── jest.config.js +└── README.md +``` + +## Extending This Driver + +This is an example implementation. To make it production-ready: + +1. **Add Redis Modules Support** + - RedisJSON for native JSON queries + - RedisSearch for full-text search + +2. **Implement Secondary Indexes** + - Use Redis Sets for indexed fields + - Maintain index consistency + +3. **Add Transaction Support** + - Use Redis MULTI/EXEC for atomic operations + +4. **Optimize Queries** + - Avoid scanning all keys + - Implement cursor-based pagination + +5. **Add Connection Pooling** + - Handle connection failures + - Implement retry logic + +## Related Documentation + +- [Driver Extensibility Guide](../../../docs/guide/drivers/extensibility.md) +- [Implementing Custom Drivers](../../../docs/guide/drivers/implementing-custom-driver.md) +- [Driver Interface Reference](../../foundation/types/src/driver.ts) + +## License + +MIT - Same as ObjectQL + +## Contributing + +This is an example driver for educational purposes. For production Redis support: + +1. Fork this implementation +2. Add production features (see "Extending This Driver") +3. Publish as a community driver +4. Share with the ObjectQL community + +## Questions? + +- 📖 Read the [Custom Driver Guide](../../../docs/guide/drivers/implementing-custom-driver.md) +- 💬 Ask in ObjectQL Discussions +- 🐛 [Report Issues](https://github.com/objectstack-ai/objectql/issues) diff --git a/packages/drivers/redis/jest.config.js b/packages/drivers/redis/jest.config.js new file mode 100644 index 00000000..8f1c3c1c --- /dev/null +++ b/packages/drivers/redis/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/test/**/*.test.ts'], + collectCoverageFrom: ['src/**/*.ts'], + moduleNameMapper: { + '^@objectql/types$': '/../../foundation/types/src', + } +}; diff --git a/packages/drivers/redis/package.json b/packages/drivers/redis/package.json new file mode 100644 index 00000000..ef3d0da4 --- /dev/null +++ b/packages/drivers/redis/package.json @@ -0,0 +1,35 @@ +{ + "name": "@objectql/driver-redis", + "version": "0.1.0", + "description": "Redis driver for ObjectQL - Example implementation for key-value storage", + "keywords": [ + "objectql", + "driver", + "redis", + "key-value", + "cache", + "database", + "adapter" + ], + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "dependencies": { + "@objectql/types": "workspace:*", + "redis": "^4.6.0" + }, + "devDependencies": { + "@types/jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/objectql.git", + "directory": "packages/drivers/redis" + } +} diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts new file mode 100644 index 00000000..4ddbc061 --- /dev/null +++ b/packages/drivers/redis/src/index.ts @@ -0,0 +1,422 @@ +/** + * Redis Driver for ObjectQL (Example Implementation) + * + * This is a reference implementation demonstrating how to create a custom ObjectQL driver. + * It adapts Redis (a key-value store) to work with ObjectQL's universal data protocol. + * + * ⚠️ WARNING: This is an educational example, not production-ready. + * It uses full key scanning which is inefficient for large datasets. + * + * For production use, consider: + * - RedisJSON module for native JSON queries + * - RedisSearch for indexed queries + * - Secondary indexes using Redis Sets + * + * Note: This example implements only the core required methods from the Driver interface. + * Optional methods like introspectSchema(), aggregate(), transactions, etc. are not implemented. + */ + +import { Driver } from '@objectql/types'; +import { createClient, RedisClientType } from 'redis'; + +/** + * Configuration options for the Redis driver. + */ +export interface RedisDriverConfig { + /** Redis connection URL (e.g., 'redis://localhost:6379') */ + url: string; + /** Additional Redis client options */ + options?: any; +} + +/** + * Redis Driver Implementation + * + * Stores ObjectQL documents as JSON strings in Redis with keys formatted as: + * `objectName:id` + * + * Example: `users:user-123` → `{"id":"user-123","name":"Alice",...}` + */ +export class RedisDriver implements Driver { + private client: RedisClientType; + private config: RedisDriverConfig; + private connected: Promise; + + constructor(config: RedisDriverConfig) { + this.config = config; + this.client = createClient({ + url: config.url, + ...config.options + }) as RedisClientType; + + // Handle connection errors + this.client.on('error', (err: Error) => { + console.error('[RedisDriver] Connection error:', err); + }); + + this.connected = this.connect(); + } + + private async connect(): Promise { + await this.client.connect(); + } + + /** + * Find multiple records matching the query criteria. + * + * ⚠️ WARNING: This implementation scans ALL keys for the object type. + * Performance degrades with large datasets. + */ + async find(objectName: string, query: any = {}, options?: any): Promise { + await this.connected; + + // Get all keys for this object type + const pattern = `${objectName}:*`; + const keys = await this.client.keys(pattern); + + // Retrieve all documents + let results: any[] = []; + for (const key of keys) { + const data = await this.client.get(key); + if (data) { + try { + const doc = JSON.parse(data); + results.push(doc); + } catch (error) { + console.warn(`[RedisDriver] Failed to parse document at key ${key}:`, error); + } + } + } + + // Apply filters (in-memory) + if (query.filters) { + results = this.applyFilters(results, query.filters); + } + + // Apply sorting (in-memory) + if (query.sort && Array.isArray(query.sort)) { + results = this.applySort(results, query.sort); + } + + // Apply pagination + if (query.skip) { + results = results.slice(query.skip); + } + if (query.limit) { + results = results.slice(0, query.limit); + } + + // Apply field projection + if (query.fields && Array.isArray(query.fields)) { + results = results.map(doc => this.projectFields(doc, query.fields)); + } + + return results; + } + + /** + * Find a single record by ID. + */ + async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise { + await this.connected; + + // If ID is provided, fetch directly + if (id) { + const key = `${objectName}:${id}`; + const data = await this.client.get(key); + + if (!data) { + return null; + } + + try { + return JSON.parse(data); + } catch (error) { + console.warn(`[RedisDriver] Failed to parse document at key ${key}:`, error); + return null; + } + } + + // If query is provided, use find and return first result + if (query) { + const results = await this.find(objectName, { ...query, limit: 1 }, options); + return results[0] || null; + } + + return null; + } + + /** + * Create a new record. + */ + async create(objectName: string, data: any, options?: any): Promise { + await this.connected; + + // Generate ID if not provided + const id = data.id || this.generateId(); + const now = new Date().toISOString(); + + const doc = { + ...data, + id, + created_at: data.created_at || now, + updated_at: data.updated_at || now + }; + + const key = `${objectName}:${id}`; + await this.client.set(key, JSON.stringify(doc)); + + return doc; + } + + /** + * Update an existing record. + */ + async update(objectName: string, id: string | number, data: any, options?: any): Promise { + await this.connected; + + const key = `${objectName}:${id}`; + const existing = await this.client.get(key); + + if (!existing) { + throw new Error(`Record not found: ${objectName}:${id}`); + } + + const existingDoc = JSON.parse(existing); + const doc = { + ...existingDoc, + ...data, + id, // Preserve ID + created_at: existingDoc.created_at, // Preserve created_at + updated_at: new Date().toISOString() + }; + + await this.client.set(key, JSON.stringify(doc)); + + return doc; + } + + /** + * Delete a record. + */ + async delete(objectName: string, id: string | number, options?: any): Promise { + await this.connected; + + const key = `${objectName}:${id}`; + const result = await this.client.del(key); + + return result > 0; + } + + /** + * Count records matching filters. + * + * ⚠️ WARNING: Loads all records to count matches. + */ + async count(objectName: string, filters: any, options?: any): Promise { + await this.connected; + + const pattern = `${objectName}:*`; + const keys = await this.client.keys(pattern); + + // If no filters, return total count + if (!filters || (Array.isArray(filters) && filters.length === 0)) { + return keys.length; + } + + // Extract actual filters from query object if needed + let actualFilters = filters; + if (filters && !Array.isArray(filters) && filters.filters) { + actualFilters = filters.filters; + } + + // Count only records matching filters + let count = 0; + for (const key of keys) { + const data = await this.client.get(key); + if (data) { + try { + const doc = JSON.parse(data); + if (this.matchesFilters(doc, actualFilters)) { + count++; + } + } catch (error) { + console.warn(`[RedisDriver] Failed to parse document at key ${key}:`, error); + } + } + } + + return count; + } + + /** + * Close the Redis connection. + */ + async disconnect(): Promise { + await this.client.quit(); + } + + // ========== Helper Methods ========== + + /** + * Apply filters to an array of records (in-memory filtering). + * + * Supports ObjectQL filter format: + * [ + * ['field', 'operator', value], + * 'or', + * ['field2', 'operator', value2] + * ] + */ + private applyFilters(records: any[], filters: any[]): any[] { + if (!filters || filters.length === 0) { + return records; + } + + return records.filter(record => this.matchesFilters(record, filters)); + } + + /** + * Check if a single record matches the filter conditions. + */ + private matchesFilters(record: any, filters: any[]): boolean { + if (!filters || filters.length === 0) { + return true; + } + + let conditions: boolean[] = []; + let operators: string[] = []; + + for (const item of filters) { + if (typeof item === 'string') { + // Logical operator (and/or) + operators.push(item.toLowerCase()); + } else if (Array.isArray(item)) { + const [field, operator, value] = item; + + // Handle nested filter groups + if (typeof field !== 'string') { + // Nested group - recursively evaluate + conditions.push(this.matchesFilters(record, item)); + } else { + // Single condition + const matches = this.evaluateCondition(record[field], operator, value); + conditions.push(matches); + } + } + } + + // Combine conditions with operators + if (conditions.length === 0) { + return true; + } + + let result = conditions[0]; + for (let i = 0; i < operators.length; i++) { + const op = operators[i]; + const nextCondition = conditions[i + 1]; + + if (op === 'or') { + result = result || nextCondition; + } else { // 'and' or default + result = result && nextCondition; + } + } + + return result; + } + + /** + * Evaluate a single filter condition. + */ + private evaluateCondition(fieldValue: any, operator: string, compareValue: any): boolean { + switch (operator) { + case '=': + return fieldValue === compareValue; + case '!=': + return fieldValue !== compareValue; + case '>': + return fieldValue > compareValue; + case '>=': + return fieldValue >= compareValue; + case '<': + return fieldValue < compareValue; + case '<=': + return fieldValue <= compareValue; + case 'in': + return Array.isArray(compareValue) && compareValue.includes(fieldValue); + case 'nin': + return Array.isArray(compareValue) && !compareValue.includes(fieldValue); + case 'contains': + return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase()); + default: + console.warn(`[RedisDriver] Unsupported operator: ${operator}`); + return false; + } + } + + /** + * Apply sorting to an array of records (in-memory sorting). + */ + private applySort(records: any[], sort: any[]): any[] { + const sorted = [...records]; + + // Apply sorts in reverse order for correct precedence + for (let i = sort.length - 1; i >= 0; i--) { + const sortItem = sort[i]; + + let field: string; + let direction: string; + + if (Array.isArray(sortItem)) { + [field, direction] = sortItem; + } else if (typeof sortItem === 'object') { + field = sortItem.field; + direction = sortItem.order || sortItem.direction || sortItem.dir || 'asc'; + } else { + continue; + } + + sorted.sort((a, b) => { + const aVal = a[field]; + const bVal = b[field]; + + // Handle null/undefined + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + + // Compare values + if (aVal < bVal) return direction === 'asc' ? -1 : 1; + if (aVal > bVal) return direction === 'asc' ? 1 : -1; + return 0; + }); + } + + return sorted; + } + + /** + * Project specific fields from a document. + */ + private projectFields(doc: any, fields: string[]): any { + const result: any = {}; + for (const field of fields) { + if (doc[field] !== undefined) { + result[field] = doc[field]; + } + } + return result; + } + + /** + * Generate a unique ID (simple UUID v4 implementation). + */ + private generateId(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/packages/drivers/redis/test/index.test.ts b/packages/drivers/redis/test/index.test.ts new file mode 100644 index 00000000..6f858970 --- /dev/null +++ b/packages/drivers/redis/test/index.test.ts @@ -0,0 +1,317 @@ +/** + * Redis Driver Tests + * + * These tests demonstrate the expected behavior of the Redis driver. + * They require a running Redis instance to execute. + */ + +import { RedisDriver } from '../src'; + +describe('RedisDriver', () => { + let driver: RedisDriver; + const TEST_OBJECT = 'test_users'; + + beforeAll(async () => { + let d: RedisDriver | undefined; + // Suppress console.error solely for the connection probe to avoid noise + // when Redis is intentionaly missing (e.g. in some local envs) + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Skip tests if Redis is not available + try { + d = new RedisDriver({ + url: process.env.REDIS_URL || 'redis://127.0.0.1:6379' + }); + + // Verify connection by attempting a simple operation + await d.count('_test_connection', []); + driver = d; + } catch (error) { + console.warn('Redis not available, skipping tests'); + if (d) { + try { + await d.disconnect(); + } catch (e) { + // Ignore disconnect error + } + } + } finally { + // Restore console.error + consoleErrorSpy.mockRestore(); + } + }); + + afterAll(async () => { + if (driver) { + // Clean up test data + try { + const results = await driver.find(TEST_OBJECT, {}); + for (const record of results) { + await driver.delete(TEST_OBJECT, record.id); + } + } catch (error) { + // Ignore cleanup errors + } + + await driver.disconnect(); + } + }); + + afterEach(async () => { + if (driver) { + // Clean up after each test + try { + const results = await driver.find(TEST_OBJECT, {}); + for (const record of results) { + await driver.delete(TEST_OBJECT, record.id); + } + } catch (error) { + // Ignore cleanup errors + } + } + }); + + describe('Connection', () => { + it('should connect to Redis', () => { + expect(driver).toBeDefined(); + }); + }); + + describe('CRUD Operations', () => { + it('should create a record', async () => { + if (!driver) { + console.log('Skipping test: Redis not available'); + return; + } + + const result = await driver.create(TEST_OBJECT, { + name: 'Alice', + email: 'alice@example.com', + role: 'admin' + }); + + expect(result).toHaveProperty('id'); + expect(result.name).toBe('Alice'); + expect(result.email).toBe('alice@example.com'); + expect(result).toHaveProperty('created_at'); + expect(result).toHaveProperty('updated_at'); + }); + + it('should create a record with custom ID', async () => { + if (!driver) return; + + const result = await driver.create(TEST_OBJECT, { + id: 'custom-123', + name: 'Bob' + }); + + expect(result.id).toBe('custom-123'); + expect(result.name).toBe('Bob'); + }); + + it('should find all records', async () => { + if (!driver) return; + + await driver.create(TEST_OBJECT, { name: 'Alice' }); + await driver.create(TEST_OBJECT, { name: 'Bob' }); + + const results = await driver.find(TEST_OBJECT, {}); + + expect(results).toHaveLength(2); + }); + + it('should findOne by ID', async () => { + if (!driver) return; + + const created = await driver.create(TEST_OBJECT, { name: 'Alice' }); + const found = await driver.findOne(TEST_OBJECT, created.id); + + expect(found).toBeDefined(); + expect(found.id).toBe(created.id); + expect(found.name).toBe('Alice'); + }); + + it('should return null for non-existent ID', async () => { + if (!driver) return; + + const found = await driver.findOne(TEST_OBJECT, 'non-existent-id'); + + expect(found).toBeNull(); + }); + + it('should update a record', async () => { + if (!driver) return; + + const created = await driver.create(TEST_OBJECT, { + name: 'Alice', + email: 'alice@example.com' + }); + + // Wait a bit to ensure updated_at is different + await new Promise(resolve => setTimeout(resolve, 10)); + + const updated = await driver.update(TEST_OBJECT, created.id, { + email: 'alice.new@example.com' + }); + + expect(updated.id).toBe(created.id); + expect(updated.name).toBe('Alice'); + expect(updated.email).toBe('alice.new@example.com'); + expect(updated.created_at).toBe(created.created_at); + expect(updated.updated_at).not.toBe(created.updated_at); + }); + + it('should delete a record', async () => { + if (!driver) return; + + const created = await driver.create(TEST_OBJECT, { name: 'Alice' }); + const deleted = await driver.delete(TEST_OBJECT, created.id); + + expect(deleted).toBe(true); + + const found = await driver.findOne(TEST_OBJECT, created.id); + expect(found).toBeNull(); + }); + + it('should count records', async () => { + if (!driver) return; + + await driver.create(TEST_OBJECT, { name: 'Alice' }); + await driver.create(TEST_OBJECT, { name: 'Bob' }); + + const count = await driver.count(TEST_OBJECT, []); + + expect(count).toBe(2); + }); + }); + + describe('Query Filtering', () => { + beforeEach(async () => { + if (!driver) return; + + await driver.create(TEST_OBJECT, { name: 'Alice', age: 30, role: 'admin' }); + await driver.create(TEST_OBJECT, { name: 'Bob', age: 25, role: 'user' }); + await driver.create(TEST_OBJECT, { name: 'Charlie', age: 35, role: 'user' }); + }); + + it('should filter by equality', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + filters: [['role', '=', 'user']] + }); + + expect(results).toHaveLength(2); + expect(results.every((r: any) => r.role === 'user')).toBe(true); + }); + + it('should filter by greater than', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + filters: [['age', '>', 25]] + }); + + expect(results).toHaveLength(2); + }); + + it('should filter with OR operator', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + filters: [ + ['name', '=', 'Alice'], + 'or', + ['name', '=', 'Bob'] + ] + }); + + expect(results).toHaveLength(2); + }); + + it('should filter with contains', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + filters: [['name', 'contains', 'li']] + }); + + expect(results).toHaveLength(2); // Alice and Charlie + }); + + it('should count with filters', async () => { + if (!driver) return; + + const count = await driver.count(TEST_OBJECT, [['role', '=', 'user']]); + + expect(count).toBe(2); + }); + }); + + describe('Query Options', () => { + beforeEach(async () => { + if (!driver) return; + + await driver.create(TEST_OBJECT, { name: 'Alice', age: 30 }); + await driver.create(TEST_OBJECT, { name: 'Bob', age: 25 }); + await driver.create(TEST_OBJECT, { name: 'Charlie', age: 35 }); + }); + + it('should sort ascending', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + sort: [['age', 'asc']] + }); + + expect(results[0].name).toBe('Bob'); + expect(results[1].name).toBe('Alice'); + expect(results[2].name).toBe('Charlie'); + }); + + it('should sort descending', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + sort: [['age', 'desc']] + }); + + expect(results[0].name).toBe('Charlie'); + expect(results[1].name).toBe('Alice'); + expect(results[2].name).toBe('Bob'); + }); + + it('should limit results', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + limit: 2 + }); + + expect(results).toHaveLength(2); + }); + + it('should skip results', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + sort: [['age', 'asc']], + skip: 1 + }); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('Alice'); + }); + + it('should project fields', async () => { + if (!driver) return; + + const results = await driver.find(TEST_OBJECT, { + fields: ['name'] + }); + + expect(results[0]).toHaveProperty('name'); + expect(results[0]).not.toHaveProperty('age'); + }); + }); +}); diff --git a/packages/drivers/redis/tsconfig.json b/packages/drivers/redis/tsconfig.json new file mode 100644 index 00000000..f6004589 --- /dev/null +++ b/packages/drivers/redis/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7643ad87..780056fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,6 +227,25 @@ importers: specifier: ^11.0.1 version: 11.0.1(socks@2.8.7) + packages/drivers/redis: + dependencies: + '@objectql/types': + specifier: workspace:* + version: link:../../foundation/types + redis: + specifier: ^4.6.0 + version: 4.7.1 + devDependencies: + '@types/jest': + specifier: ^29.0.0 + version: 29.5.14 + jest: + specifier: ^29.0.0 + version: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/drivers/sdk: dependencies: '@objectql/types': @@ -1260,10 +1279,23 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/console@30.2.0': resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/core@30.2.0': resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1277,18 +1309,34 @@ packages: resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/environment@30.2.0': resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@30.2.0': resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect@30.2.0': resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/fake-timers@30.2.0': resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1297,6 +1345,10 @@ packages: resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/globals@30.2.0': resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1305,6 +1357,15 @@ packages: resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/reporters@30.2.0': resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1314,6 +1375,10 @@ packages: node-notifier: optional: true + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1322,22 +1387,42 @@ packages: resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@30.0.1': resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-result@30.2.0': resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-sequencer@30.2.0': resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/transform@30.2.0': resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@30.2.0': resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1408,6 +1493,35 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@remix-run/router@1.23.2': resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} @@ -1564,12 +1678,18 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.34.47': resolution: {integrity: sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} @@ -1625,6 +1745,9 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1640,6 +1763,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} @@ -2082,16 +2208,30 @@ packages: react-native-b4a: optional: true + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-0 + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + babel-plugin-istanbul@7.0.1: resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} engines: {node: '>=12'} + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@30.2.0: resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2101,6 +2241,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + babel-preset-jest@30.2.0: resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2267,6 +2413,9 @@ packages: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -2285,6 +2434,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -2391,6 +2544,11 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -2500,6 +2658,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -2640,10 +2802,18 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect@30.2.0: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2781,6 +2951,10 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3033,6 +3207,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} @@ -3041,6 +3219,10 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} @@ -3056,14 +3238,32 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-changed-files@30.2.0: resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-circus@30.2.0: resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest-cli@30.2.0: resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3074,6 +3274,18 @@ packages: node-notifier: optional: true + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + jest-config@30.2.0: resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3089,38 +3301,78 @@ packages: ts-node: optional: true + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.2.0: resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@30.2.0: resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-each@30.2.0: resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-node@30.2.0: resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@30.2.0: resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-leak-detector@30.2.0: resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.2.0: resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@30.2.0: resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-mock@30.2.0: resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3134,46 +3386,96 @@ packages: jest-resolve: optional: true + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-regex-util@30.0.1: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve-dependencies@30.2.0: resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve@30.2.0: resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runner@30.2.0: resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runtime@30.2.0: resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-snapshot@30.2.0: resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@30.2.0: resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.2.0: resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-watcher@30.2.0: resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@30.2.0: resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest@30.2.0: resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3228,6 +3530,10 @@ packages: resolution: {integrity: sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==} engines: {node: '>v0.4.10'} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + knex@3.1.0: resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==} engines: {node: '>=16'} @@ -3882,6 +4188,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.2.0: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3901,6 +4211,10 @@ packages: promised-io@0.3.6: resolution: {integrity: sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3918,6 +4232,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -3994,6 +4311,9 @@ packages: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + redis@4.7.1: + resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4018,6 +4338,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -4130,6 +4454,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4642,6 +4969,10 @@ packages: engines: {node: '>=0.1.97'} deprecated: wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years. + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5406,6 +5737,15 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + '@jest/console@30.2.0': dependencies: '@jest/types': 30.2.0 @@ -5415,6 +5755,41 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@30.2.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 @@ -5453,6 +5828,13 @@ snapshots: '@jest/diff-sequences@30.0.1': {} + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + jest-mock: 29.7.0 + '@jest/environment@30.2.0': dependencies: '@jest/fake-timers': 30.2.0 @@ -5460,10 +5842,21 @@ snapshots: '@types/node': 20.19.28 jest-mock: 30.2.0 + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + '@jest/expect-utils@30.2.0': dependencies: '@jest/get-type': 30.1.0 + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + '@jest/expect@30.2.0': dependencies: expect: 30.2.0 @@ -5471,6 +5864,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.19.28 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + '@jest/fake-timers@30.2.0': dependencies: '@jest/types': 30.2.0 @@ -5482,6 +5884,15 @@ snapshots: '@jest/get-type@30.1.0': {} + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + '@jest/globals@30.2.0': dependencies: '@jest/environment': 30.2.0 @@ -5496,6 +5907,35 @@ snapshots: '@types/node': 20.19.28 jest-regex-util: 30.0.1 + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 20.19.28 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/reporters@30.2.0': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -5524,6 +5964,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + '@jest/schemas@30.0.5': dependencies: '@sinclair/typebox': 0.34.47 @@ -5535,12 +5979,25 @@ snapshots: graceful-fs: 4.2.11 natural-compare: 1.4.0 + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + '@jest/source-map@30.0.1': dependencies: '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + '@jest/test-result@30.2.0': dependencies: '@jest/console': 30.2.0 @@ -5548,6 +6005,13 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + '@jest/test-sequencer@30.2.0': dependencies: '@jest/test-result': 30.2.0 @@ -5555,6 +6019,26 @@ snapshots: jest-haste-map: 30.2.0 slash: 3.0.0 + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.5 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + '@jest/transform@30.2.0': dependencies: '@babel/core': 7.28.5 @@ -5575,6 +6059,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.19.28 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jest/types@30.2.0': dependencies: '@jest/pattern': 30.0.1 @@ -5671,6 +6164,32 @@ snapshots: '@pkgr/core@0.2.9': {} + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + '@remix-run/router@1.23.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -5790,12 +6309,18 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.34.47': {} '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@13.0.5': dependencies: '@sinonjs/commons': 3.0.1 @@ -5869,6 +6394,10 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 20.19.28 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -5885,6 +6414,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + '@types/jest@30.0.0': dependencies: expect: 30.2.0 @@ -6310,6 +6844,19 @@ snapshots: b4a@1.7.3: {} + babel-jest@29.7.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.5) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@30.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -6323,6 +6870,16 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + babel-plugin-istanbul@7.0.1: dependencies: '@babel/helper-plugin-utils': 7.27.1 @@ -6333,6 +6890,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + babel-plugin-jest-hoist@30.2.0: dependencies: '@types/babel__core': 7.20.5 @@ -6356,6 +6920,12 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + babel-preset-jest@29.6.3(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + babel-preset-jest@30.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -6543,6 +7113,8 @@ snapshots: ci-info@4.3.1: {} + cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} class-variance-authority@0.7.1: @@ -6560,6 +7132,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -6642,6 +7216,21 @@ snapshots: dependencies: is-what: 5.5.0 + create-jest@29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cross-inspect@1.0.1: @@ -6710,6 +7299,8 @@ snapshots: didyoumean@1.2.2: {} + diff-sequences@29.6.3: {} + diff@4.0.2: {} dir-glob@3.0.1: @@ -6876,8 +7467,18 @@ snapshots: exit-x@0.2.2: {} + exit@0.1.2: {} + expand-template@2.0.3: {} + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + expect@30.2.0: dependencies: '@jest/expect-utils': 30.2.0 @@ -7061,6 +7662,8 @@ snapshots: wide-align: 1.1.5 optional: true + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -7320,6 +7923,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.28.5 @@ -7336,6 +7949,14 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -7359,12 +7980,44 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + jest-changed-files@30.2.0: dependencies: execa: 5.1.1 jest-util: 30.2.0 p-limit: 3.1.0 + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-circus@30.2.0: dependencies: '@jest/environment': 30.2.0 @@ -7391,6 +8044,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@30.2.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) @@ -7410,6 +8082,37 @@ snapshots: - supports-color - ts-node + jest-config@29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.28 + ts-node: 10.9.2(@types/node@20.19.28)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@30.2.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -7443,6 +8146,13 @@ snapshots: - babel-plugin-macros - supports-color + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-diff@30.2.0: dependencies: '@jest/diff-sequences': 30.0.1 @@ -7450,10 +8160,22 @@ snapshots: chalk: 4.1.2 pretty-format: 30.2.0 + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + jest-docblock@30.2.0: dependencies: detect-newline: 3.1.0 + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + jest-each@30.2.0: dependencies: '@jest/get-type': 30.1.0 @@ -7462,6 +8184,15 @@ snapshots: jest-util: 30.2.0 pretty-format: 30.2.0 + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jest-environment-node@30.2.0: dependencies: '@jest/environment': 30.2.0 @@ -7472,6 +8203,24 @@ snapshots: jest-util: 30.2.0 jest-validate: 30.2.0 + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.19.28 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + jest-haste-map@30.2.0: dependencies: '@jest/types': 30.2.0 @@ -7487,11 +8236,23 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-leak-detector@30.2.0: dependencies: '@jest/get-type': 30.1.0 pretty-format: 30.2.0 + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-matcher-utils@30.2.0: dependencies: '@jest/get-type': 30.1.0 @@ -7499,6 +8260,18 @@ snapshots: jest-diff: 30.2.0 pretty-format: 30.2.0 + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-message-util@30.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -7511,18 +8284,37 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + jest-util: 29.7.0 + jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 '@types/node': 20.19.28 jest-util: 30.2.0 + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): optionalDependencies: jest-resolve: 30.2.0 + jest-regex-util@29.6.3: {} + jest-regex-util@30.0.1: {} + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + jest-resolve-dependencies@30.2.0: dependencies: jest-regex-util: 30.0.1 @@ -7530,6 +8322,18 @@ snapshots: transitivePeerDependencies: - supports-color + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + slash: 3.0.0 + jest-resolve@30.2.0: dependencies: chalk: 4.1.2 @@ -7541,6 +8345,32 @@ snapshots: slash: 3.0.0 unrs-resolver: 1.11.1 + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + jest-runner@30.2.0: dependencies: '@jest/console': 30.2.0 @@ -7568,6 +8398,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + jest-runtime@30.2.0: dependencies: '@jest/environment': 30.2.0 @@ -7595,6 +8452,31 @@ snapshots: transitivePeerDependencies: - supports-color + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/types': 7.28.5 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + jest-snapshot@30.2.0: dependencies: '@babel/core': 7.28.5 @@ -7621,6 +8503,15 @@ snapshots: transitivePeerDependencies: - supports-color + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 @@ -7630,6 +8521,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 4.0.3 + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + jest-validate@30.2.0: dependencies: '@jest/get-type': 30.1.0 @@ -7639,6 +8539,17 @@ snapshots: leven: 3.1.0 pretty-format: 30.2.0 + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.28 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + jest-watcher@30.2.0: dependencies: '@jest/test-result': 30.2.0 @@ -7650,6 +8561,13 @@ snapshots: jest-util: 30.2.0 string-length: 4.0.2 + jest-worker@29.7.0: + dependencies: + '@types/node': 20.19.28 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jest-worker@30.2.0: dependencies: '@types/node': 20.19.28 @@ -7658,6 +8576,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@30.2.0(@types/node@20.19.28)(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@20.19.28)(typescript@5.9.3)) @@ -7705,6 +8635,8 @@ snapshots: promised-io: 0.3.6 walker: 1.0.8 + kleur@3.0.3: {} + knex@3.1.0(sqlite3@5.1.7): dependencies: colorette: 2.0.19 @@ -8300,6 +9232,12 @@ snapshots: prettier@3.7.4: {} + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-format@30.2.0: dependencies: '@jest/schemas': 30.0.5 @@ -8317,6 +9255,11 @@ snapshots: promised-io@0.3.6: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -8337,6 +9280,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + pure-rand@7.0.1: {} qs@6.14.1: @@ -8416,6 +9361,15 @@ snapshots: dependencies: resolve: 1.22.11 + redis@4.7.1: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -8436,6 +9390,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -8598,6 +9554,8 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + sisteransi@1.0.5: {} + slash@3.0.0: {} smart-buffer@4.2.0: {} @@ -9213,6 +10171,11 @@ snapshots: wrench@1.3.9: {} + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4