/** * ObjectQL 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 } from '@objectql/types'; import { ConsoleLogger, ObjectQLError } from '@objectql/types'; import type { Logger } from '@objectql/types'; import { ValidatorPlugin, ValidatorPluginConfig } from '@objectql/plugin-validator'; import { FormulaPlugin, FormulaPluginConfig } from '@objectql/plugin-formula'; import { QueryPlugin } from '@objectql/plugin-query'; import type { Driver } from '@objectql/types'; import { SchemaRegistry } from '@objectstack/objectql'; /** * Configuration for the ObjectQL Plugin */ export interface ObjectQLPluginConfig { /** * Enable repository pattern for data access * @default true */ enableRepository?: boolean; /** * Enable validation engine * @default true */ 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 query service and analyzer * @default true */ enableQueryService?: boolean; /** * Datasources for query service * Required if enableQueryService is true */ datasources?: Record; } /** * ObjectQL Plugin * * Thin orchestrator that composes ObjectQL's extension plugins * (QueryPlugin, ValidatorPlugin, FormulaPlugin, AI) on top of the microkernel. * * Delegates query execution to @objectql/plugin-query and provides * repository-pattern CRUD bridging to the kernel. */ export class ObjectQLPlugin implements RuntimePlugin { name = '@objectql/core'; version = '4.2.0'; private logger: Logger; constructor(private config: ObjectQLPluginConfig = {}, _ql?: any) { // Set defaults this.config = { enableRepository: true, enableValidator: true, enableFormulas: true, enableQueryService: true, ...config }; this.logger = new ConsoleLogger({ name: '@objectql/core/plugin', level: 'info' }); } /** * Install the plugin into the kernel * This is called during kernel initialization */ async install(ctx: RuntimeContext): Promise { this.logger.info('Installing plugin...'); const kernel = ctx.engine as any; // Get datasources - either from config or from kernel drivers let datasources = this.config.datasources; if (!datasources) { // Try to get drivers from kernel (micro-kernel pattern) const drivers = kernel.getAllDrivers?.(); if (drivers && drivers.length > 0) { datasources = {}; drivers.forEach((driver: any, index: number) => { // Use driver name if available, otherwise use 'default' for first driver const driverName = driver.name || (index === 0 ? 'default' : `driver_${index + 1}`); datasources![driverName] = driver; }); this.logger.info('Using drivers from kernel', { drivers: Object.keys(datasources), }); } else { this.logger.warn('No datasources configured and no drivers found in kernel. Repository and QueryService will not be available.'); } } // Delegate query service registration to QueryPlugin if (this.config.enableQueryService !== false && datasources) { const queryPlugin = new QueryPlugin({ datasources }); await queryPlugin.install(ctx); this.logger.info('QueryPlugin installed (QueryService + QueryAnalyzer)'); } // Register components based on configuration if (this.config.enableRepository !== false && datasources) { await this.registerRepository(kernel, datasources); } // Install validator plugin if enabled if (this.config.enableValidator !== false) { const validatorPlugin = new ValidatorPlugin(this.config.validatorConfig || {}); await validatorPlugin.install?.(ctx); } // Install formula plugin if enabled if (this.config.enableFormulas !== false) { const formulaPlugin = new FormulaPlugin(this.config.formulaConfig || {}); await formulaPlugin.install?.(ctx); } // Register system service aliases if (typeof (ctx as any).registerService === 'function') { const registerService = (ctx as any).registerService.bind(ctx); // 1. Metadata service if (kernel.metadata) { registerService('metadata', kernel.metadata); this.logger.debug('Registered metadata service alias'); } // 2. Data service (prefer QueryService, fallback to kernel) const dataService = kernel.queryService || kernel; registerService('data', dataService); this.logger.debug('Registered data service alias'); // 3. Analytics service (via QueryService) if (kernel.queryService) { registerService('analytics', kernel.queryService); this.logger.debug('Registered analytics service alias'); } } this.logger.info('Plugin installed successfully'); } /** * Called when the kernel starts * This is the initialization phase */ async onStart(_ctx: RuntimeContext): Promise { this.logger.debug('Starting plugin...'); // Additional startup logic can be added here } // --- Adapter for @objectstack/core compatibility --- // --------------------------------------------------- /** * Register the Repository pattern * @private */ private async registerRepository(kernel: any, datasources: Record): Promise { // Helper function to get the driver for an object const getDriver = (objectName: string): Driver => { const objectConfig = (kernel as any).metadata.get('object', objectName); const datasourceName = objectConfig?.datasource || 'default'; const driver = datasources[datasourceName]; if (!driver) { throw new ObjectQLError({ code: 'NOT_FOUND', message: `Datasource '${datasourceName}' not found for object '${objectName}'` }); } return driver; }; // Override kernel CRUD methods to use drivers kernel.create = async (objectName: string, data: any): Promise => { const driver = getDriver(objectName); return await driver.create(objectName, data, {}); }; kernel.update = async (objectName: string, id: string, data: any): Promise => { const driver = getDriver(objectName); return await driver.update(objectName, id, data, {}); }; kernel.delete = async (objectName: string, id: string): Promise => { const driver = getDriver(objectName); const result = await driver.delete(objectName, id, {}); return !!result; }; kernel.find = async (objectName: string, query: any): Promise<{ value: any[]; count: number }> => { // Use QueryService if available for advanced query processing (AST, optimizations) if ((kernel as any).queryService) { const result = await (kernel as any).queryService.find(objectName, query); return { value: result.value, count: result.count !== undefined ? result.count : result.value.length }; } // Fallback to direct driver call const driver = getDriver(objectName); const value = await driver.find(objectName, query); const count = value.length; return { value, count }; }; kernel.get = async (objectName: string, id: string): Promise => { // Use QueryService if available if ((kernel as any).queryService) { const result = await (kernel as any).queryService.findOne(objectName, id); return result.value; } const driver = getDriver(objectName); return await driver.findOne(objectName, id); }; kernel.count = async (objectName: string, filters?: any): Promise => { // Use QueryService if available if ((kernel as any).queryService) { const result = await (kernel as any).queryService.count(objectName, filters); return result.value; } const driver = getDriver(objectName); return await driver.count(objectName, filters || {}, {}); }; this.logger.info('Repository pattern registered'); } // --- Adapter for @objectstack/core compatibility --- init = async (pluginCtx: any): Promise => { // The @objectstack/core kernel passes a PluginContext (with registerService, getKernel, etc.) // We extract the actual kernel and build a RuntimeContext wrapper that proxies registerService. const actualKernel = typeof pluginCtx.getKernel === 'function' ? pluginCtx.getKernel() : pluginCtx; // Create metadata facade on the kernel if not already present. // The raw ObjectKernel from @objectstack/core has no .metadata property; // it's normally created by the ObjectQL wrapper (app.ts). // When running via `objectstack serve`, we must create it here. if (!actualKernel.metadata) { const unwrapContent = (item: any) => (item && item.content) ? item.content : item; actualKernel.metadata = { register: (type: string, item: any) => SchemaRegistry.registerItem(type, item, item.id ? 'id' : 'name'), get: (type: string, name: string) => { const item = SchemaRegistry.getItem(type, name) as any; return unwrapContent(item); }, getEntry: (type: string, name: string) => SchemaRegistry.getItem(type, name), list: (type: string) => { const items = SchemaRegistry.listItems(type); return items.map(unwrapContent); }, unregister: (type: string, name: string) => { if (typeof SchemaRegistry.unregisterItem === 'function') { SchemaRegistry.unregisterItem(type, name); } }, unregisterPackage: (packageName: string) => { if (typeof (SchemaRegistry as any).unregisterObjectsByPackage === 'function') { (SchemaRegistry as any).unregisterObjectsByPackage(packageName); } }, }; this.logger.info('Created metadata facade on kernel'); } const ctx: any = { engine: actualKernel, getKernel: () => actualKernel, // Proxy registerService from PluginContext so install() can register metadata/data/analytics registerService: typeof pluginCtx.registerService === 'function' ? pluginCtx.registerService.bind(pluginCtx) : undefined, }; // Register 'objectql' service for AppPlugin compatibility if (typeof pluginCtx.registerService === 'function') { pluginCtx.registerService('objectql', this); this.logger.debug('Registered objectql service'); } return this.install(ctx); } start = async (pluginCtx: any): Promise => { const actualKernel = typeof pluginCtx.getKernel === 'function' ? pluginCtx.getKernel() : pluginCtx; const ctx: any = { engine: actualKernel, getKernel: () => actualKernel, }; return this.onStart(ctx); } }