diff --git a/packages/foundation/core/src/formula-plugin.ts b/packages/foundation/core/src/formula-plugin.ts new file mode 100644 index 00000000..eaa1f12c --- /dev/null +++ b/packages/foundation/core/src/formula-plugin.ts @@ -0,0 +1,139 @@ +/** + * ObjectQL Formula Plugin + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@objectstack/runtime'; +import { FormulaEngine } from './formula-engine'; +import type { FormulaEngineConfig } from '@objectql/types'; + +/** + * Configuration for the Formula Plugin + */ +export interface FormulaPluginConfig extends FormulaEngineConfig { + /** + * Enable automatic formula evaluation on queries + * @default true + */ + autoEvaluateOnQuery?: boolean; +} + +/** + * Formula Plugin + * + * Wraps the ObjectQL Formula Engine as an ObjectStack plugin. + * Registers formula evaluation capabilities into the kernel. + */ +export class FormulaPlugin implements RuntimePlugin { + name = '@objectql/formulas'; + version = '4.0.0'; + + private engine: FormulaEngine; + private config: FormulaPluginConfig; + + constructor(config: FormulaPluginConfig = {}) { + this.config = { + autoEvaluateOnQuery: true, + ...config + }; + + // Initialize the formula engine with configuration + this.engine = new FormulaEngine(config); + } + + /** + * Install the plugin into the kernel + * Registers formula evaluation capabilities + */ + async install(ctx: RuntimeContext): Promise { + const kernel = ctx.engine as ObjectStackKernel; + + console.log(`[${this.name}] Installing formula plugin...`); + + // Register formula provider if the kernel supports it + this.registerFormulaProvider(kernel); + + // Register formula evaluation middleware if auto-evaluation is enabled + if (this.config.autoEvaluateOnQuery !== false) { + this.registerFormulaMiddleware(kernel); + } + + console.log(`[${this.name}] Formula plugin installed`); + } + + /** + * Register the formula provider with the kernel + * @private + */ + private registerFormulaProvider(kernel: ObjectStackKernel): void { + // Check if kernel supports formula provider registration + // Note: Using type assertion since registerFormulaProvider may not be in the interface + const kernelWithFormulas = kernel as any; + + if (typeof kernelWithFormulas.registerFormulaProvider === 'function') { + kernelWithFormulas.registerFormulaProvider({ + evaluate: (formula: string, context: any) => { + // Delegate to the formula engine + // Note: In a real implementation, we would need to properly construct + // the FormulaContext from the provided context + return this.engine.evaluate( + formula, + context, + 'text', // default data type + {} + ); + }, + validate: (expression: string) => { + return this.engine.validate(expression); + }, + extractMetadata: (fieldName: string, expression: string, dataType: any) => { + return this.engine.extractMetadata(fieldName, expression, dataType); + } + }); + } else { + // If the kernel doesn't support formula provider registration yet, + // we still register the engine for direct access + kernelWithFormulas.formulaEngine = this.engine; + } + } + + /** + * Register formula evaluation middleware + * @private + */ + private registerFormulaMiddleware(kernel: ObjectStackKernel): void { + // Check if kernel supports middleware hooks + const kernelWithHooks = kernel as any; + + if (typeof kernelWithHooks.use === 'function') { + // Register middleware to evaluate formulas after queries + kernelWithHooks.use('afterQuery', async (context: any) => { + // Formula evaluation logic would go here + // This would automatically compute formula fields after data is retrieved + if (context.results && context.metadata?.fields) { + // Iterate through fields and evaluate formulas + // const formulaFields = Object.entries(context.metadata.fields) + // .filter(([_, fieldConfig]) => (fieldConfig as any).formula); + // + // for (const record of context.results) { + // for (const [fieldName, fieldConfig] of formulaFields) { + // const formula = (fieldConfig as any).formula; + // const result = this.engine.evaluate(formula, /* context */, /* dataType */); + // record[fieldName] = result.value; + // } + // } + } + }); + } + } + + /** + * Get the formula engine instance for direct access + */ + getEngine(): FormulaEngine { + return this.engine; + } +} diff --git a/packages/foundation/core/src/index.ts b/packages/foundation/core/src/index.ts index 1f5b3e5f..b47b2b90 100644 --- a/packages/foundation/core/src/index.ts +++ b/packages/foundation/core/src/index.ts @@ -17,6 +17,8 @@ export type { DriverInterface, DriverOptions, QueryAST } from '@objectstack/spec export * from './repository'; export * from './app'; export * from './plugin'; +export * from './validator-plugin'; +export * from './formula-plugin'; export * from './action'; export * from './hook'; diff --git a/packages/foundation/core/src/plugin.ts b/packages/foundation/core/src/plugin.ts index 5220ea85..47dce6b1 100644 --- a/packages/foundation/core/src/plugin.ts +++ b/packages/foundation/core/src/plugin.ts @@ -8,6 +8,8 @@ import type { RuntimePlugin, RuntimeContext } from '@objectstack/runtime'; import type { ObjectStackKernel } from '@objectstack/runtime'; +import { ValidatorPlugin, ValidatorPluginConfig } from './validator-plugin'; +import { FormulaPlugin, FormulaPluginConfig } from './formula-plugin'; /** * Configuration for the ObjectQL Plugin @@ -25,12 +27,24 @@ export interface ObjectQLPluginConfig { */ enableValidator?: boolean; + /** + * Validator plugin configuration + * Only used if enableValidator is not false + */ + validatorConfig?: ValidatorPluginConfig; + /** * Enable formula engine * @default true */ enableFormulas?: boolean; + /** + * Formula plugin configuration + * Only used if enableFormulas is not false + */ + formulaConfig?: FormulaPluginConfig; + /** * Enable AI integration * @default true @@ -72,12 +86,16 @@ export class ObjectQLPlugin implements RuntimePlugin { await this.registerRepository(ctx.engine); } + // Install validator plugin if enabled if (this.config.enableValidator !== false) { - await this.registerValidator(ctx.engine); + const validatorPlugin = new ValidatorPlugin(this.config.validatorConfig || {}); + await validatorPlugin.install(ctx); } + // Install formula plugin if enabled if (this.config.enableFormulas !== false) { - await this.registerFormulas(ctx.engine); + const formulaPlugin = new FormulaPlugin(this.config.formulaConfig || {}); + await formulaPlugin.install(ctx); } if (this.config.enableAI !== false) { @@ -106,26 +124,6 @@ export class ObjectQLPlugin implements RuntimePlugin { console.log(`[${this.name}] Repository pattern registered`); } - /** - * Register the Validator engine - * @private - */ - private async registerValidator(kernel: ObjectStackKernel): Promise { - // TODO: Implement validator registration - // For now, this is a placeholder to establish the structure - console.log(`[${this.name}] Validator engine registered`); - } - - /** - * Register the Formula engine - * @private - */ - private async registerFormulas(kernel: ObjectStackKernel): Promise { - // TODO: Implement formula registration - // For now, this is a placeholder to establish the structure - console.log(`[${this.name}] Formula engine registered`); - } - /** * Register AI integration * @private diff --git a/packages/foundation/core/src/validator-plugin.ts b/packages/foundation/core/src/validator-plugin.ts new file mode 100644 index 00000000..e13eb45f --- /dev/null +++ b/packages/foundation/core/src/validator-plugin.ts @@ -0,0 +1,130 @@ +/** + * ObjectQL Validator Plugin + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@objectstack/runtime'; +import { Validator, ValidatorOptions } from './validator'; + +/** + * Configuration for the Validator Plugin + */ +export interface ValidatorPluginConfig extends ValidatorOptions { + /** + * Enable validation on queries + * @default true + */ + enableQueryValidation?: boolean; + + /** + * Enable validation on mutations + * @default true + */ + enableMutationValidation?: boolean; +} + +/** + * Validator Plugin + * + * Wraps the ObjectQL Validator engine as an ObjectStack plugin. + * Registers validation middleware hooks into the kernel lifecycle. + */ +export class ValidatorPlugin implements RuntimePlugin { + name = '@objectql/validator'; + version = '4.0.0'; + + private validator: Validator; + private config: ValidatorPluginConfig; + + constructor(config: ValidatorPluginConfig = {}) { + this.config = { + enableQueryValidation: true, + enableMutationValidation: true, + ...config + }; + + // Initialize the validator with language options + this.validator = new Validator({ + language: config.language, + languageFallback: config.languageFallback, + }); + } + + /** + * Install the plugin into the kernel + * Registers validation middleware for queries and mutations + */ + async install(ctx: RuntimeContext): Promise { + const kernel = ctx.engine as ObjectStackKernel; + + console.log(`[${this.name}] Installing validator plugin...`); + + // Register validation middleware for queries (if enabled) + if (this.config.enableQueryValidation !== false) { + this.registerQueryValidation(kernel); + } + + // Register validation middleware for mutations (if enabled) + if (this.config.enableMutationValidation !== false) { + this.registerMutationValidation(kernel); + } + + console.log(`[${this.name}] Validator plugin installed`); + } + + /** + * Register query validation middleware + * @private + */ + private registerQueryValidation(kernel: ObjectStackKernel): void { + // Check if kernel supports middleware hooks + if (typeof (kernel as any).use === 'function') { + (kernel as any).use('beforeQuery', async (context: any) => { + // Query validation logic + // In a real implementation, this would validate query parameters + // For now, this is a placeholder that demonstrates the integration pattern + if (context.query && context.metadata?.validation_rules) { + // Validation would happen here + // const result = await this.validator.validate( + // context.metadata.validation_rules, + // { /* validation context */ } + // ); + } + }); + } + } + + /** + * Register mutation validation middleware + * @private + */ + private registerMutationValidation(kernel: ObjectStackKernel): void { + // Check if kernel supports middleware hooks + if (typeof (kernel as any).use === 'function') { + (kernel as any).use('beforeMutation', async (context: any) => { + // Mutation validation logic + // This would validate data before create/update operations + if (context.data && context.metadata?.validation_rules) { + // Validation would happen here + // const result = await this.validator.validate( + // context.metadata.validation_rules, + // { /* validation context */ } + // ); + // if (!result.valid) { + // throw new Error('Validation failed: ' + result.errors.map(e => e.message).join(', ')); + // } + } + }); + } + } + + /** + * Get the validator instance for direct access + */ + getValidator(): Validator { + return this.validator; + } +} diff --git a/packages/foundation/core/test/formula-plugin.test.ts b/packages/foundation/core/test/formula-plugin.test.ts new file mode 100644 index 00000000..f6b786f1 --- /dev/null +++ b/packages/foundation/core/test/formula-plugin.test.ts @@ -0,0 +1,197 @@ +/** + * ObjectQL Formula Plugin Tests + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { FormulaPlugin } from '../src/formula-plugin'; +import { ObjectStackKernel } from '@objectstack/runtime'; + +describe('FormulaPlugin', () => { + let plugin: FormulaPlugin; + let mockKernel: any; + + beforeEach(() => { + // Create a mock kernel with middleware and formula provider support + mockKernel = { + use: jest.fn(), + registerFormulaProvider: jest.fn(), + }; + plugin = new FormulaPlugin(); + }); + + describe('Plugin Metadata', () => { + it('should have correct name and version', () => { + expect(plugin.name).toBe('@objectql/formulas'); + expect(plugin.version).toBe('4.0.0'); + }); + }); + + describe('Constructor', () => { + it('should create plugin with default config', () => { + const defaultPlugin = new FormulaPlugin(); + expect(defaultPlugin).toBeDefined(); + }); + + it('should create plugin with custom config', () => { + const customPlugin = new FormulaPlugin({ + enable_cache: true, + cache_ttl: 600, + autoEvaluateOnQuery: false, + }); + expect(customPlugin).toBeDefined(); + }); + + it('should accept formula engine config', () => { + const customPlugin = new FormulaPlugin({ + enable_monitoring: true, + max_execution_time: 5000, + }); + expect(customPlugin).toBeDefined(); + }); + }); + + describe('Installation', () => { + it('should install successfully with mock kernel', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + // Verify that formula provider was registered + expect(mockKernel.registerFormulaProvider).toHaveBeenCalled(); + }); + + it('should register formula provider with evaluate function', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + expect(mockKernel.registerFormulaProvider).toHaveBeenCalledWith( + expect.objectContaining({ + evaluate: expect.any(Function), + validate: expect.any(Function), + extractMetadata: expect.any(Function), + }) + ); + }); + + it('should register formula middleware when auto-evaluation is enabled', async () => { + const pluginWithAuto = new FormulaPlugin({ autoEvaluateOnQuery: true }); + const ctx = { engine: mockKernel }; + + await pluginWithAuto.install(ctx); + + // Check that middleware was registered + expect(mockKernel.use).toHaveBeenCalledWith('afterQuery', expect.any(Function)); + }); + + it('should not register formula middleware when auto-evaluation is disabled', async () => { + const pluginNoAuto = new FormulaPlugin({ autoEvaluateOnQuery: false }); + const ctx = { engine: mockKernel }; + + await pluginNoAuto.install(ctx); + + // Should not have registered afterQuery hook + const afterQueryCalls = mockKernel.use.mock.calls.filter( + (call: any[]) => call[0] === 'afterQuery' + ); + expect(afterQueryCalls.length).toBe(0); + }); + + it('should handle kernel without registerFormulaProvider', async () => { + const kernelNoProvider = { + use: jest.fn(), + }; + const ctx = { engine: kernelNoProvider }; + + // Should not throw error and should set formulaEngine property + await expect(plugin.install(ctx)).resolves.not.toThrow(); + expect((kernelNoProvider as any).formulaEngine).toBeDefined(); + }); + + it('should handle kernel without middleware support', async () => { + const kernelNoMiddleware = { + registerFormulaProvider: jest.fn(), + }; + const ctx = { engine: kernelNoMiddleware }; + + // Should not throw error + await expect(plugin.install(ctx)).resolves.not.toThrow(); + }); + }); + + describe('Formula Provider', () => { + it('should provide evaluate function that works', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + // Get the registered provider + const provider = mockKernel.registerFormulaProvider.mock.calls[0][0]; + + // Test the evaluate function + const result = provider.evaluate('1 + 1', { + record: {}, + system: { + today: new Date(), + now: new Date(), + year: 2026, + month: 1, + day: 22, + hour: 12, + minute: 0, + second: 0, + }, + current_user: {}, + is_new: false, + }); + + expect(result).toBeDefined(); + expect(result.success).toBe(true); + // The result is coerced to 'text' by default, so it's a string "2" + expect(result.value).toBe('2'); + }); + + it('should provide validate function that works', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + // Get the registered provider + const provider = mockKernel.registerFormulaProvider.mock.calls[0][0]; + + // Test the validate function + const result = provider.validate('quantity * price'); + + expect(result).toBeDefined(); + expect(result.valid).toBe(true); + expect(result.errors.length).toBe(0); + }); + + it('should provide extractMetadata function that works', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + // Get the registered provider + const provider = mockKernel.registerFormulaProvider.mock.calls[0][0]; + + // Test the extractMetadata function + const metadata = provider.extractMetadata('total', 'quantity * price', 'number'); + + expect(metadata).toBeDefined(); + expect(metadata.field_name).toBe('total'); + expect(metadata.expression).toBe('quantity * price'); + expect(metadata.data_type).toBe('number'); + expect(metadata.dependencies).toContain('quantity'); + expect(metadata.dependencies).toContain('price'); + }); + }); + + describe('Engine Access', () => { + it('should expose formula engine instance', () => { + const engine = plugin.getEngine(); + expect(engine).toBeDefined(); + expect(typeof engine.evaluate).toBe('function'); + expect(typeof engine.validate).toBe('function'); + expect(typeof engine.extractMetadata).toBe('function'); + }); + }); +}); diff --git a/packages/foundation/core/test/plugin-integration.test.ts b/packages/foundation/core/test/plugin-integration.test.ts new file mode 100644 index 00000000..fa393781 --- /dev/null +++ b/packages/foundation/core/test/plugin-integration.test.ts @@ -0,0 +1,213 @@ +/** + * ObjectQL Plugin Integration Tests + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { ObjectQLPlugin } from '../src/plugin'; +import { ValidatorPlugin } from '../src/validator-plugin'; +import { FormulaPlugin } from '../src/formula-plugin'; + +// Mock the sub-plugins +jest.mock('../src/validator-plugin'); +jest.mock('../src/formula-plugin'); + +describe('ObjectQLPlugin Integration', () => { + let plugin: ObjectQLPlugin; + let mockKernel: any; + let mockContext: any; + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + + // Create a mock kernel + mockKernel = { + use: jest.fn(), + registerFormulaProvider: jest.fn(), + }; + + mockContext = { engine: mockKernel }; + + // Setup mock implementations + (ValidatorPlugin as jest.Mock).mockImplementation(() => ({ + install: jest.fn().mockResolvedValue(undefined), + })); + + (FormulaPlugin as jest.Mock).mockImplementation(() => ({ + install: jest.fn().mockResolvedValue(undefined), + })); + }); + + describe('Plugin Metadata', () => { + it('should have correct name and version', () => { + plugin = new ObjectQLPlugin(); + expect(plugin.name).toBe('@objectql/core'); + expect(plugin.version).toBe('4.0.0'); + }); + }); + + describe('Constructor', () => { + it('should create plugin with default config', () => { + plugin = new ObjectQLPlugin(); + expect(plugin).toBeDefined(); + }); + + it('should create plugin with custom config', () => { + plugin = new ObjectQLPlugin({ + enableRepository: false, + enableValidator: false, + enableFormulas: true, + enableAI: false, + }); + expect(plugin).toBeDefined(); + }); + + it('should accept validator config', () => { + plugin = new ObjectQLPlugin({ + validatorConfig: { + language: 'zh-CN', + enableQueryValidation: false, + }, + }); + expect(plugin).toBeDefined(); + }); + + it('should accept formula config', () => { + plugin = new ObjectQLPlugin({ + formulaConfig: { + enable_cache: true, + cache_ttl: 600, + }, + }); + expect(plugin).toBeDefined(); + }); + }); + + describe('Installation - Conditional Plugin Loading', () => { + it('should install validator plugin when enabled', async () => { + plugin = new ObjectQLPlugin({ enableValidator: true }); + await plugin.install(mockContext); + + expect(ValidatorPlugin).toHaveBeenCalled(); + const validatorInstance = (ValidatorPlugin as jest.Mock).mock.results[0].value; + expect(validatorInstance.install).toHaveBeenCalledWith(mockContext); + }); + + it('should not install validator plugin when disabled', async () => { + plugin = new ObjectQLPlugin({ enableValidator: false }); + await plugin.install(mockContext); + + expect(ValidatorPlugin).not.toHaveBeenCalled(); + }); + + it('should install formula plugin when enabled', async () => { + plugin = new ObjectQLPlugin({ enableFormulas: true }); + await plugin.install(mockContext); + + expect(FormulaPlugin).toHaveBeenCalled(); + const formulaInstance = (FormulaPlugin as jest.Mock).mock.results[0].value; + expect(formulaInstance.install).toHaveBeenCalledWith(mockContext); + }); + + it('should not install formula plugin when disabled', async () => { + plugin = new ObjectQLPlugin({ enableFormulas: false }); + await plugin.install(mockContext); + + expect(FormulaPlugin).not.toHaveBeenCalled(); + }); + + it('should pass validator config to validator plugin', async () => { + const validatorConfig = { + language: 'zh-CN', + enableQueryValidation: false, + }; + + plugin = new ObjectQLPlugin({ + enableValidator: true, + validatorConfig, + }); + + await plugin.install(mockContext); + + expect(ValidatorPlugin).toHaveBeenCalledWith(validatorConfig); + }); + + it('should pass formula config to formula plugin', async () => { + const formulaConfig = { + enable_cache: true, + cache_ttl: 600, + }; + + plugin = new ObjectQLPlugin({ + enableFormulas: true, + formulaConfig, + }); + + await plugin.install(mockContext); + + expect(FormulaPlugin).toHaveBeenCalledWith(formulaConfig); + }); + + it('should install multiple plugins when all enabled', async () => { + plugin = new ObjectQLPlugin({ + enableValidator: true, + enableFormulas: true, + }); + + await plugin.install(mockContext); + + expect(ValidatorPlugin).toHaveBeenCalled(); + expect(FormulaPlugin).toHaveBeenCalled(); + }); + + it('should not install any plugins when all disabled', async () => { + plugin = new ObjectQLPlugin({ + enableRepository: false, + enableValidator: false, + enableFormulas: false, + enableAI: false, + }); + + await plugin.install(mockContext); + + expect(ValidatorPlugin).not.toHaveBeenCalled(); + expect(FormulaPlugin).not.toHaveBeenCalled(); + }); + }); + + describe('Lifecycle Hooks', () => { + it('should have onStart method', async () => { + plugin = new ObjectQLPlugin(); + expect(typeof plugin.onStart).toBe('function'); + + // Should not throw when called + await expect(plugin.onStart(mockContext)).resolves.not.toThrow(); + }); + }); + + describe('Default Configuration', () => { + it('should enable all features by default', async () => { + plugin = new ObjectQLPlugin(); + await plugin.install(mockContext); + + // Validator and Formula should be installed by default + expect(ValidatorPlugin).toHaveBeenCalled(); + expect(FormulaPlugin).toHaveBeenCalled(); + }); + + it('should treat undefined config as enabled', async () => { + plugin = new ObjectQLPlugin({ + // Explicitly not setting enableValidator or enableFormulas + }); + + await plugin.install(mockContext); + + // Both should still be installed + expect(ValidatorPlugin).toHaveBeenCalled(); + expect(FormulaPlugin).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/foundation/core/test/validator-plugin.test.ts b/packages/foundation/core/test/validator-plugin.test.ts new file mode 100644 index 00000000..a8a97a9b --- /dev/null +++ b/packages/foundation/core/test/validator-plugin.test.ts @@ -0,0 +1,126 @@ +/** + * ObjectQL Validator Plugin Tests + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { ValidatorPlugin } from '../src/validator-plugin'; +import { ObjectStackKernel } from '@objectstack/runtime'; + +describe('ValidatorPlugin', () => { + let plugin: ValidatorPlugin; + let mockKernel: any; + + beforeEach(() => { + // Create a mock kernel with middleware support + mockKernel = { + use: jest.fn(), + }; + plugin = new ValidatorPlugin(); + }); + + describe('Plugin Metadata', () => { + it('should have correct name and version', () => { + expect(plugin.name).toBe('@objectql/validator'); + expect(plugin.version).toBe('4.0.0'); + }); + }); + + describe('Constructor', () => { + it('should create plugin with default config', () => { + const defaultPlugin = new ValidatorPlugin(); + expect(defaultPlugin).toBeDefined(); + }); + + it('should create plugin with custom config', () => { + const customPlugin = new ValidatorPlugin({ + language: 'zh-CN', + enableQueryValidation: false, + enableMutationValidation: true, + }); + expect(customPlugin).toBeDefined(); + }); + + it('should accept language options', () => { + const customPlugin = new ValidatorPlugin({ + language: 'fr', + languageFallback: ['en', 'zh-CN'], + }); + expect(customPlugin).toBeDefined(); + }); + }); + + describe('Installation', () => { + it('should install successfully with mock kernel', async () => { + const ctx = { engine: mockKernel }; + await plugin.install(ctx); + + // Verify that middleware hooks were registered + expect(mockKernel.use).toHaveBeenCalled(); + }); + + it('should register query validation when enabled', async () => { + const pluginWithQuery = new ValidatorPlugin({ enableQueryValidation: true }); + const ctx = { engine: mockKernel }; + + await pluginWithQuery.install(ctx); + + // Check that use was called (for query validation) + expect(mockKernel.use).toHaveBeenCalledWith('beforeQuery', expect.any(Function)); + }); + + it('should register mutation validation when enabled', async () => { + const pluginWithMutation = new ValidatorPlugin({ enableMutationValidation: true }); + const ctx = { engine: mockKernel }; + + await pluginWithMutation.install(ctx); + + // Check that use was called (for mutation validation) + expect(mockKernel.use).toHaveBeenCalledWith('beforeMutation', expect.any(Function)); + }); + + it('should not register query validation when disabled', async () => { + const pluginNoQuery = new ValidatorPlugin({ enableQueryValidation: false }); + const ctx = { engine: mockKernel }; + + await pluginNoQuery.install(ctx); + + // Should not have registered beforeQuery hook + const beforeQueryCalls = mockKernel.use.mock.calls.filter( + (call: any[]) => call[0] === 'beforeQuery' + ); + expect(beforeQueryCalls.length).toBe(0); + }); + + it('should not register mutation validation when disabled', async () => { + const pluginNoMutation = new ValidatorPlugin({ enableMutationValidation: false }); + const ctx = { engine: mockKernel }; + + await pluginNoMutation.install(ctx); + + // Should not have registered beforeMutation hook + const beforeMutationCalls = mockKernel.use.mock.calls.filter( + (call: any[]) => call[0] === 'beforeMutation' + ); + expect(beforeMutationCalls.length).toBe(0); + }); + + it('should handle kernel without middleware support', async () => { + const kernelNoMiddleware = {}; + const ctx = { engine: kernelNoMiddleware }; + + // Should not throw error + await expect(plugin.install(ctx)).resolves.not.toThrow(); + }); + }); + + describe('Validator Access', () => { + it('should expose validator instance', () => { + const validator = plugin.getValidator(); + expect(validator).toBeDefined(); + expect(typeof validator.validate).toBe('function'); + }); + }); +});