-
Notifications
You must be signed in to change notification settings - Fork 0
Add vitest support, enhance doctor command, implement formula/lookup RLS conditions #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| module.exports = { | ||
| preset: 'ts-jest', | ||
| testEnvironment: 'node', | ||
| roots: ['<rootDir>/__tests__'], | ||
| testMatch: ['**/*.test.ts'], | ||
| moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], | ||
| collectCoverageFrom: [ | ||
| 'src/**/*.ts', | ||
| '!src/**/*.d.ts', | ||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -153,35 +153,245 @@ export class QueryTrimmer { | |||||||||
| } | ||||||||||
|
|
||||||||||
| if (condition.type === 'formula') { | ||||||||||
| // Formula conditions can't be directly converted to filters | ||||||||||
| // They need to be evaluated in memory after fetching | ||||||||||
| // TODO: Consider implementing a formula-to-SQL compiler for common patterns | ||||||||||
| console.warn( | ||||||||||
| 'Formula conditions cannot be converted to query filters and will be evaluated in-memory. ' + | ||||||||||
| 'This may result in fetching more data than necessary. ' + | ||||||||||
| 'Consider using simple or complex conditions for better performance, ' + | ||||||||||
| 'or ensure formula conditions are combined with other filterable conditions.' | ||||||||||
| ); | ||||||||||
| // Formula-based conditions: Evaluate the formula and convert to filters | ||||||||||
| try { | ||||||||||
| const filter = this.formulaToFilter(condition.formula, user); | ||||||||||
| if (filter && Object.keys(filter).length > 0) { | ||||||||||
| return filter; | ||||||||||
| } | ||||||||||
| } catch (error: any) { | ||||||||||
| console.warn( | ||||||||||
| `Failed to convert formula condition to filter: ${error.message}. ` + | ||||||||||
| 'Formula will be evaluated in-memory, which may affect performance.' | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // If we can't convert to a filter, return empty object | ||||||||||
| // The record will need to be fetched and evaluated in-memory | ||||||||||
| return {}; | ||||||||||
|
Comment on lines
+156
to
171
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| if (condition.type === 'lookup') { | ||||||||||
| // Lookup conditions require join logic | ||||||||||
| // TODO: Implement support for lookup conditions in a future release | ||||||||||
| console.warn( | ||||||||||
| 'Lookup conditions are not yet supported in query trimming. ' + | ||||||||||
| 'This feature is planned for a future release. ' + | ||||||||||
| 'As a workaround, consider: ' + | ||||||||||
| '1. Using record-level rules instead of row-level security for lookup-based filtering ' + | ||||||||||
| '2. Denormalizing the lookup field into the main table ' + | ||||||||||
| '3. Implementing custom query logic in beforeQuery hooks' | ||||||||||
| ); | ||||||||||
| // Lookup conditions: Convert to subquery filter | ||||||||||
| try { | ||||||||||
| const filter = this.lookupToFilter(condition, user); | ||||||||||
| if (filter && Object.keys(filter).length > 0) { | ||||||||||
| return filter; | ||||||||||
| } | ||||||||||
| } catch (error: any) { | ||||||||||
| console.warn( | ||||||||||
| `Failed to convert lookup condition to filter: ${error.message}. ` + | ||||||||||
| 'Lookup will be evaluated in-memory, which may affect performance.' | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // If we can't convert to a filter, return empty object | ||||||||||
| return {}; | ||||||||||
|
Comment on lines
+188
to
189
|
||||||||||
| // If we can't convert to a filter, return empty object | |
| return {}; | |
| // If we can't convert to a filter safely, fail closed with a restrictive filter | |
| return { _id: null }; |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The operatorToMongoFilter method is called but uses MongoDB-specific syntax ($gt, $gte, $ne, etc.) which violates the architecture principle that QueryTrimmer should work at the AST level. The comment on line 18 states it "Works at the AST level to inject security filters before SQL generation" but the implementation leaks MongoDB-specific operators.
According to the ObjectQL architecture, packages/foundation must NEVER contain dialect-specific logic. Either refactor to use an abstract filter representation that drivers can interpret, or document this limitation.
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formula parser does not validate or sanitize the user-provided formula expression before evaluation. While it attempts to parse common patterns, there's no prevention of code injection through formula strings. For example, a malicious formula could potentially include method calls or property access that aren't intended to be supported.
Add input validation to ensure only safe operators and field references are allowed in formulas. Consider using an allowlist approach where only specific operators and syntax patterns are permitted.
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formula parser uses a simplistic regex-based approach that doesn't handle parentheses or operator precedence correctly. For example, the formula "status == 'active' && (role == 'admin' || role == 'manager')" would be incorrectly parsed because the splitOnOperator function splits on || first, which would break the parenthetical grouping. This could lead to incorrect security filters being applied, potentially allowing unauthorized access.
Consider implementing a proper expression parser or document the limitations of formula conditions to avoid security issues. At minimum, add validation to reject formulas containing parentheses if they cannot be safely parsed.
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The $lookup filter syntax used here is MongoDB-specific, but the QueryTrimmer is supposed to be database-agnostic and work at the AST level before SQL generation. According to the ObjectQL architecture principles, this should compile to abstract intent that can be interpreted by different drivers (SQL, MongoDB, etc.).
This implementation violates the "Compiler, Not ORM" principle by leaking database-specific syntax into the core security layer. The $lookup operator won't work with SQL drivers. Consider creating an abstract representation that can be translated by each driver, or document that lookup conditions only work with MongoDB.
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function name "jsOperatorToMongoOperator" is misleading. It claims to convert JavaScript operators to MongoDB operators, but the function actually converts to generic comparison operators ('=', '!=', etc.) that are then later converted to MongoDB syntax by operatorToMongoFilter.
Rename this to something more accurate like "normalizeComparisonOperator" or "jsOperatorToGenericOperator" to better reflect what it actually does.
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the ObjectQL architecture guideline (Step 3 of the Development Lifecycle), "All development MUST include documentation updates." This PR adds significant new functionality (formula and lookup RLS conditions) but does not include any updates to documentation or the @objectstack/spec repository.
The formula and lookup condition support should be documented in:
- The plugin-security documentation explaining the syntax and limitations
- The @objectstack/spec repository to define the protocol
- Usage examples showing how to use formula and lookup conditions in RLS configurations
Without documentation, users won't know about these features or how to use them safely.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -431,4 +431,104 @@ describe('CLI Commands', () => { | |
| expect(originalContent).toBeDefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('test command', () => { | ||
| beforeEach(() => { | ||
| // Create a test package.json for testing | ||
| const packageJson = { | ||
| name: 'test-project', | ||
| version: '1.0.0', | ||
| scripts: { | ||
| test: 'echo "test script"' | ||
| } | ||
| }; | ||
|
|
||
| fs.writeFileSync( | ||
| path.join(testDir, 'package.json'), | ||
| JSON.stringify(packageJson, null, 2), | ||
| 'utf-8' | ||
| ); | ||
| }); | ||
|
|
||
| it('should detect missing test runner', async () => { | ||
| const packageJson = { | ||
| name: 'test-project', | ||
| version: '1.0.0', | ||
| scripts: {} | ||
| }; | ||
|
|
||
| fs.writeFileSync( | ||
| path.join(testDir, 'package.json'), | ||
| JSON.stringify(packageJson, null, 2), | ||
| 'utf-8' | ||
| ); | ||
|
|
||
| // Mock console.log to capture output | ||
| const consoleSpy = jest.spyOn(console, 'log'); | ||
|
|
||
| const { test } = require('../src/commands/test'); | ||
| await test({ dir: testDir }); | ||
|
|
||
| // Should show no test configuration message | ||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('No test configuration found') | ||
| ); | ||
|
|
||
| consoleSpy.mockRestore(); | ||
| }); | ||
| }); | ||
|
Comment on lines
+435
to
+479
|
||
|
|
||
| describe('doctor command', () => { | ||
| it('should check Node.js version', async () => { | ||
| const consoleSpy = jest.spyOn(console, 'log'); | ||
|
|
||
| const { doctorCommand } = require('../src/commands/doctor'); | ||
| await doctorCommand(); | ||
|
|
||
| // Should print Node.js version | ||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('Node.js:') | ||
| ); | ||
|
|
||
| consoleSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should check for package.json', async () => { | ||
| const originalCwd = process.cwd(); | ||
|
|
||
| try { | ||
| // Change to test directory | ||
| process.chdir(testDir); | ||
|
|
||
| // Create a minimal package.json | ||
| const packageJson = { | ||
| name: 'test-project', | ||
| version: '1.0.0', | ||
| dependencies: { | ||
| '@objectql/core': '^4.0.0' | ||
| } | ||
| }; | ||
|
|
||
| fs.writeFileSync( | ||
| path.join(testDir, 'package.json'), | ||
| JSON.stringify(packageJson, null, 2), | ||
| 'utf-8' | ||
| ); | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'log'); | ||
|
|
||
| const { doctorCommand } = require('../src/commands/doctor'); | ||
| await doctorCommand(); | ||
|
|
||
| // Should detect package.json | ||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('package.json') | ||
| ); | ||
|
|
||
| consoleSpy.mockRestore(); | ||
| } finally { | ||
| process.chdir(originalCwd); | ||
| } | ||
| }); | ||
| }); | ||
|
Comment on lines
+481
to
+533
|
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The jest.config.js file is missing the moduleNameMapper and transform configuration that is used in other packages like core, driver-knex, and driver-mongo. This configuration is needed to enable tests to run without a build step by mapping workspace packages to their TypeScript source files. Without this, the tests will fail when run before building.
Add the moduleNameMapper to map '@objectql/*' to source directories and configure ts-jest with isolatedModules as shown in packages/foundation/core/jest.config.js.