Skip to content

Commit 471effc

Browse files
Copilothotlong
andcommitted
feat: Create @objectql/driver-utils shared utilities package
- Created comprehensive utility package for driver development - QueryAST normalization and parsing utilities - FilterCondition evaluation and conversion - Error handling with standard error codes - ID generation (nanoid, UUID, sequential, timestamp) - Timestamp management utilities - Transaction state management utilities - Comprehensive documentation and usage examples Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 9133ef8 commit 471effc

11 files changed

Lines changed: 1228 additions & 0 deletions

File tree

packages/drivers/utils/README.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# @objectql/driver-utils
2+
3+
Shared utilities for ObjectQL drivers to reduce code duplication and standardize driver implementations.
4+
5+
## Overview
6+
7+
This package provides common functionality used across all ObjectQL drivers:
8+
9+
- **QueryAST normalization** - Convert between legacy and modern query formats
10+
- **FilterCondition evaluation** - MongoDB-style query evaluation and conversion
11+
- **Error handling** - Standard error codes and error creation utilities
12+
- **ID generation** - Multiple ID generation strategies (nanoid, UUID, sequential, timestamp)
13+
- **Timestamp management** - Automatic timestamp handling for create/update operations
14+
- **Transaction utilities** - Transaction state management and helpers
15+
16+
## Installation
17+
18+
```bash
19+
pnpm add @objectql/driver-utils
20+
```
21+
22+
## Usage
23+
24+
### QueryAST Normalization
25+
26+
```typescript
27+
import { normalizeQuery, normalizeOrderBy, applySorting, applyPagination } from '@objectql/driver-utils';
28+
29+
// Normalize query from various formats
30+
const normalized = normalizeQuery({
31+
filters: { role: 'admin' }, // Legacy format
32+
sort: [['age', 'asc']],
33+
skip: 10,
34+
limit: 20
35+
});
36+
// Returns: { where: {...}, orderBy: [...], offset: 10, limit: 20 }
37+
38+
// Apply sorting to records
39+
const sorted = applySorting(records, [
40+
{ field: 'age', order: 'asc' },
41+
{ field: 'name', order: 'desc' }
42+
]);
43+
44+
// Apply pagination
45+
const paginated = applyPagination(records, 10, 20); // offset: 10, limit: 20
46+
```
47+
48+
### FilterCondition Evaluation
49+
50+
```typescript
51+
import { evaluateFilter, filterRecords, isFilterCondition } from '@objectql/driver-utils';
52+
53+
// Evaluate a single record against a condition
54+
const matches = evaluateFilter(record, {
55+
age: { $gt: 18 },
56+
role: 'user'
57+
});
58+
59+
// Filter an array of records
60+
const filtered = filterRecords(records, {
61+
$or: [
62+
{ status: 'active' },
63+
{ priority: { $gte: 5 } }
64+
]
65+
});
66+
67+
// Check if value is a FilterCondition
68+
if (isFilterCondition(query.where)) {
69+
// Handle MongoDB-style query
70+
}
71+
```
72+
73+
### Error Handling
74+
75+
```typescript
76+
import {
77+
createRecordNotFoundError,
78+
createDuplicateRecordError,
79+
createValidationError,
80+
wrapError,
81+
DriverError
82+
} from '@objectql/driver-utils';
83+
84+
// Throw standard errors
85+
throw createRecordNotFoundError('users', userId);
86+
throw createDuplicateRecordError('users', userId);
87+
88+
// Wrap native errors
89+
try {
90+
// ... database operation
91+
} catch (error) {
92+
throw wrapError(error as Error, {
93+
operation: 'create',
94+
objectName: 'users'
95+
});
96+
}
97+
```
98+
99+
### ID Generation
100+
101+
```typescript
102+
import { IDGenerator, generateNanoId, generateUUID, generateTimestampId } from '@objectql/driver-utils';
103+
104+
// Using ID Generator
105+
const idGen = new IDGenerator();
106+
const id1 = idGen.generateSequential('users'); // "1"
107+
const id2 = idGen.generateSequential('users', 'usr_'); // "usr_2"
108+
const id3 = idGen.generateRandom(16); // Random nanoid
109+
110+
// Direct functions
111+
const nanoId = generateNanoId(16);
112+
const uuid = generateUUID();
113+
const timestampId = generateTimestampId('doc');
114+
```
115+
116+
### Timestamp Utilities
117+
118+
```typescript
119+
import { addCreateTimestamps, addUpdateTimestamps, getCurrentTimestamp } from '@objectql/driver-utils';
120+
121+
// Add timestamps for create
122+
const newRecord = addCreateTimestamps({ name: 'Alice' });
123+
// Returns: { name: 'Alice', created_at: '2026-02-02T...', updated_at: '2026-02-02T...' }
124+
125+
// Add timestamps for update
126+
const updated = addUpdateTimestamps(
127+
{ email: 'new@example.com' },
128+
existingRecord.created_at
129+
);
130+
// Returns: { email: '...', created_at: '<preserved>', updated_at: '2026-02-02T...' }
131+
```
132+
133+
### Transaction Utilities
134+
135+
```typescript
136+
import {
137+
createTransaction,
138+
generateTransactionId,
139+
isTransactionActive,
140+
markCommitted,
141+
TransactionState
142+
} from '@objectql/driver-utils';
143+
144+
// Create transaction
145+
const tx = createTransaction();
146+
console.log(tx.id); // "tx_1234567890_abc123"
147+
console.log(tx.state); // TransactionState.ACTIVE
148+
149+
// Check state
150+
if (isTransactionActive(tx)) {
151+
// Execute operations
152+
}
153+
154+
// Update state
155+
markCommitted(tx);
156+
console.log(tx.state); // TransactionState.COMMITTED
157+
```
158+
159+
## API Reference
160+
161+
See individual module documentation for detailed API information:
162+
163+
- [query-ast.ts](./src/query-ast.ts) - Query normalization and parsing
164+
- [filter-condition.ts](./src/filter-condition.ts) - Filter evaluation and conversion
165+
- [error-handler.ts](./src/error-handler.ts) - Error handling utilities
166+
- [id-generator.ts](./src/id-generator.ts) - ID generation strategies
167+
- [timestamp-utils.ts](./src/timestamp-utils.ts) - Timestamp management
168+
- [transaction-utils.ts](./src/transaction-utils.ts) - Transaction helpers
169+
170+
## Benefits
171+
172+
Using `@objectql/driver-utils` in your driver implementation provides:
173+
174+
1. **Reduced Code Duplication** - Common patterns extracted and tested
175+
2. **Consistency** - All drivers behave the same way for core operations
176+
3. **Maintainability** - Bug fixes and improvements benefit all drivers
177+
4. **Type Safety** - Full TypeScript support with proper type definitions
178+
5. **Testing** - Shared utilities are thoroughly tested
179+
180+
## License
181+
182+
MIT
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@objectql/driver-utils",
3+
"version": "4.0.3",
4+
"description": "Shared utilities for ObjectQL drivers - QueryAST parsing, FilterCondition evaluation, error handling",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"scripts": {
8+
"build": "tsc",
9+
"test": "jest"
10+
},
11+
"keywords": [
12+
"objectql",
13+
"driver",
14+
"utils",
15+
"queryast",
16+
"filter"
17+
],
18+
"license": "MIT",
19+
"dependencies": {
20+
"@objectql/types": "workspace:*",
21+
"@objectstack/spec": "^0.8.2"
22+
},
23+
"devDependencies": {
24+
"@types/jest": "^29.0.0",
25+
"jest": "^30.0.0",
26+
"typescript": "^5.3.0"
27+
}
28+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Error Handling Utilities for Drivers
3+
*
4+
* Provides standard error handling and error creation
5+
*/
6+
7+
/**
8+
* Base driver error class
9+
*/
10+
export class DriverError extends Error {
11+
public readonly code: string;
12+
public readonly details?: any;
13+
14+
constructor(params: { code: string; message: string; details?: any }) {
15+
super(params.message);
16+
this.code = params.code;
17+
this.details = params.details;
18+
this.name = 'DriverError';
19+
20+
// Maintains proper stack trace for where our error was thrown (only available on V8)
21+
if (Error.captureStackTrace) {
22+
Error.captureStackTrace(this, DriverError);
23+
}
24+
}
25+
}
26+
27+
/**
28+
* Standard error codes for drivers
29+
*/
30+
export const DriverErrorCodes = {
31+
RECORD_NOT_FOUND: 'RECORD_NOT_FOUND',
32+
DUPLICATE_RECORD: 'DUPLICATE_RECORD',
33+
VALIDATION_ERROR: 'VALIDATION_ERROR',
34+
CONNECTION_ERROR: 'CONNECTION_ERROR',
35+
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
36+
QUERY_ERROR: 'QUERY_ERROR',
37+
INVALID_QUERY: 'INVALID_QUERY',
38+
PERMISSION_DENIED: 'PERMISSION_DENIED',
39+
DRIVER_ERROR: 'DRIVER_ERROR'
40+
} as const;
41+
42+
export type DriverErrorCode = typeof DriverErrorCodes[keyof typeof DriverErrorCodes];
43+
44+
/**
45+
* Create a standard error for record not found
46+
*
47+
* @param objectName - Name of the object
48+
* @param id - ID of the record
49+
* @returns DriverError instance
50+
*/
51+
export function createRecordNotFoundError(objectName: string, id: string | number): DriverError {
52+
return new DriverError({
53+
code: DriverErrorCodes.RECORD_NOT_FOUND,
54+
message: `Record with id '${id}' not found in '${objectName}'`,
55+
details: { objectName, id }
56+
});
57+
}
58+
59+
/**
60+
* Create a standard error for duplicate record
61+
*
62+
* @param objectName - Name of the object
63+
* @param id - ID of the record
64+
* @returns DriverError instance
65+
*/
66+
export function createDuplicateRecordError(objectName: string, id: string | number): DriverError {
67+
return new DriverError({
68+
code: DriverErrorCodes.DUPLICATE_RECORD,
69+
message: `Record with id '${id}' already exists in '${objectName}'`,
70+
details: { objectName, id }
71+
});
72+
}
73+
74+
/**
75+
* Create a standard error for validation failure
76+
*
77+
* @param message - Error message
78+
* @param details - Additional details
79+
* @returns DriverError instance
80+
*/
81+
export function createValidationError(message: string, details?: any): DriverError {
82+
return new DriverError({
83+
code: DriverErrorCodes.VALIDATION_ERROR,
84+
message,
85+
details
86+
});
87+
}
88+
89+
/**
90+
* Create a standard error for connection issues
91+
*
92+
* @param message - Error message
93+
* @param details - Additional details
94+
* @returns DriverError instance
95+
*/
96+
export function createConnectionError(message: string, details?: any): DriverError {
97+
return new DriverError({
98+
code: DriverErrorCodes.CONNECTION_ERROR,
99+
message,
100+
details
101+
});
102+
}
103+
104+
/**
105+
* Create a standard error for transaction issues
106+
*
107+
* @param message - Error message
108+
* @param details - Additional details
109+
* @returns DriverError instance
110+
*/
111+
export function createTransactionError(message: string, details?: any): DriverError {
112+
return new DriverError({
113+
code: DriverErrorCodes.TRANSACTION_ERROR,
114+
message,
115+
details
116+
});
117+
}
118+
119+
/**
120+
* Create a standard error for query issues
121+
*
122+
* @param message - Error message
123+
* @param details - Additional details
124+
* @returns DriverError instance
125+
*/
126+
export function createQueryError(message: string, details?: any): DriverError {
127+
return new DriverError({
128+
code: DriverErrorCodes.QUERY_ERROR,
129+
message,
130+
details
131+
});
132+
}
133+
134+
/**
135+
* Wrap a native error into a DriverError
136+
*
137+
* @param error - Native error to wrap
138+
* @param context - Additional context information
139+
* @returns DriverError instance
140+
*/
141+
export function wrapError(error: Error, context?: { operation?: string; objectName?: string; details?: any }): DriverError {
142+
if (error instanceof DriverError) {
143+
return error;
144+
}
145+
146+
return new DriverError({
147+
code: DriverErrorCodes.DRIVER_ERROR,
148+
message: error.message,
149+
details: {
150+
originalError: error.name,
151+
stack: error.stack,
152+
...context
153+
}
154+
});
155+
}
156+
157+
/**
158+
* Safe error handler that ensures errors are properly formatted
159+
*
160+
* @param fn - Function to wrap
161+
* @returns Wrapped function that catches and formats errors
162+
*/
163+
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(
164+
fn: T,
165+
context?: { operation?: string; objectName?: string }
166+
): T {
167+
return (async (...args: any[]) => {
168+
try {
169+
return await fn(...args);
170+
} catch (error) {
171+
throw wrapError(error as Error, context);
172+
}
173+
}) as T;
174+
}

0 commit comments

Comments
 (0)