From f1231cfb927efeb655fee71a9cca8ba6d11dfc65 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 07:32:00 +0000
Subject: [PATCH 1/6] Initial plan
From a3cbe9469438b0403f7547a7e0c31b6bada74e29 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 07:43:30 +0000
Subject: [PATCH 2/6] Create @objectql/plugin-server package with Hono adapter
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
packages/plugins/server/CHANGELOG.md | 27 +
packages/plugins/server/README.md | 126 ++++
packages/plugins/server/jest.config.js | 7 +
packages/plugins/server/package.json | 46 ++
.../plugins/server/src/adapters/graphql.ts | 559 ++++++++++++++++++
packages/plugins/server/src/adapters/hono.ts | 258 ++++++++
packages/plugins/server/src/adapters/node.ts | 293 +++++++++
packages/plugins/server/src/adapters/rest.ts | 338 +++++++++++
packages/plugins/server/src/file-handler.ts | 422 +++++++++++++
packages/plugins/server/src/index.ts | 25 +
packages/plugins/server/src/metadata.ts | 244 ++++++++
packages/plugins/server/src/openapi.ts | 207 +++++++
packages/plugins/server/src/plugin.ts | 239 ++++++++
packages/plugins/server/src/server.ts | 274 +++++++++
packages/plugins/server/src/storage.ts | 179 ++++++
packages/plugins/server/src/templates.ts | 57 ++
packages/plugins/server/src/types.ts | 181 ++++++
packages/plugins/server/src/utils.ts | 29 +
packages/plugins/server/tsconfig.json | 9 +
packages/runtime/server/src/index.ts | 13 +
pnpm-lock.yaml | 37 ++
pnpm-workspace.yaml | 1 +
22 files changed, 3571 insertions(+)
create mode 100644 packages/plugins/server/CHANGELOG.md
create mode 100644 packages/plugins/server/README.md
create mode 100644 packages/plugins/server/jest.config.js
create mode 100644 packages/plugins/server/package.json
create mode 100644 packages/plugins/server/src/adapters/graphql.ts
create mode 100644 packages/plugins/server/src/adapters/hono.ts
create mode 100644 packages/plugins/server/src/adapters/node.ts
create mode 100644 packages/plugins/server/src/adapters/rest.ts
create mode 100644 packages/plugins/server/src/file-handler.ts
create mode 100644 packages/plugins/server/src/index.ts
create mode 100644 packages/plugins/server/src/metadata.ts
create mode 100644 packages/plugins/server/src/openapi.ts
create mode 100644 packages/plugins/server/src/plugin.ts
create mode 100644 packages/plugins/server/src/server.ts
create mode 100644 packages/plugins/server/src/storage.ts
create mode 100644 packages/plugins/server/src/templates.ts
create mode 100644 packages/plugins/server/src/types.ts
create mode 100644 packages/plugins/server/src/utils.ts
create mode 100644 packages/plugins/server/tsconfig.json
diff --git a/packages/plugins/server/CHANGELOG.md b/packages/plugins/server/CHANGELOG.md
new file mode 100644
index 00000000..dc137c54
--- /dev/null
+++ b/packages/plugins/server/CHANGELOG.md
@@ -0,0 +1,27 @@
+# @objectql/plugin-server
+
+## 3.0.1
+
+### Added
+
+- Initial release as an ObjectQL plugin
+- Support for JSON-RPC, REST, GraphQL, and Metadata APIs
+- Hono framework adapter
+- Express/Node.js adapter
+- Plugin-based architecture
+- Configurable routes and middleware
+- File upload/download support
+- Auto-start capability
+- Backward compatibility with @objectql/server
+
+### Changed
+
+- Refactored server functionality into plugin architecture
+- Improved modularity and extensibility
+- Enhanced framework integration support
+
+### Documentation
+
+- Added comprehensive README with examples
+- Documented all configuration options
+- Included usage examples for Hono, Express, and standalone usage
diff --git a/packages/plugins/server/README.md b/packages/plugins/server/README.md
new file mode 100644
index 00000000..35487fc3
--- /dev/null
+++ b/packages/plugins/server/README.md
@@ -0,0 +1,126 @@
+# @objectql/plugin-server
+
+HTTP server plugin for ObjectQL. Provides Express, Hono, and custom HTTP server support with JSON-RPC, REST, GraphQL, and Metadata APIs.
+
+## Installation
+
+```bash
+npm install @objectql/plugin-server
+```
+
+## Usage
+
+### As a Plugin
+
+```typescript
+import { ObjectQL } from '@objectql/core';
+import { ServerPlugin } from '@objectql/plugin-server';
+
+const app = new ObjectQL({
+ datasources: { /* ... */ },
+ plugins: [
+ new ServerPlugin({
+ port: 3000,
+ autoStart: true,
+ enableREST: true,
+ enableRPC: true,
+ enableMetadata: true
+ })
+ ]
+});
+
+await app.init();
+```
+
+### With Hono Framework
+
+```typescript
+import { Hono } from 'hono';
+import { ObjectQL } from '@objectql/core';
+import { createHonoAdapter } from '@objectql/plugin-server/adapters/hono';
+
+const app = new ObjectQL({ /* ... */ });
+await app.init();
+
+const server = new Hono();
+const objectqlHandler = createHonoAdapter(app);
+
+server.all('/api/*', objectqlHandler);
+
+export default server;
+```
+
+### With Express
+
+```typescript
+import express from 'express';
+import { ObjectQL } from '@objectql/core';
+import { createNodeHandler } from '@objectql/plugin-server';
+
+const app = new ObjectQL({ /* ... */ });
+await app.init();
+
+const server = express();
+const objectqlHandler = createNodeHandler(app);
+
+server.all('/api/*', objectqlHandler);
+
+server.listen(3000);
+```
+
+## Features
+
+### JSON-RPC API
+- Protocol-first approach
+- Supports all ObjectQL operations
+- Type-safe requests and responses
+
+### REST API
+- Standard HTTP methods (GET, POST, PUT, DELETE)
+- RESTful resource endpoints
+- Query parameter support
+
+### GraphQL API
+- Auto-generated schema from ObjectQL metadata
+- Support for queries and mutations
+- Introspection support
+
+### Metadata API
+- Explore object schemas
+- Discover available operations
+- Runtime schema inspection
+
+### File Upload/Download
+- Single and batch file uploads
+- Secure file storage
+- File download support
+
+## Configuration Options
+
+```typescript
+interface ServerPluginOptions {
+ port?: number; // Default: 3000
+ host?: string; // Default: 'localhost'
+ routes?: ApiRouteConfig; // Custom route configuration
+ fileStorage?: IFileStorage; // Custom file storage
+ enableGraphQL?: boolean; // Default: false
+ enableREST?: boolean; // Default: true
+ enableMetadata?: boolean; // Default: true
+ enableRPC?: boolean; // Default: true
+ autoStart?: boolean; // Default: false
+ middleware?: Function[]; // Custom middleware
+}
+```
+
+## API Routes
+
+Default routes (customizable):
+- JSON-RPC: `/api/objectql`
+- REST: `/api/data`
+- GraphQL: `/api/graphql`
+- Metadata: `/api/metadata`
+- Files: `/api/files`
+
+## License
+
+MIT
diff --git a/packages/plugins/server/jest.config.js b/packages/plugins/server/jest.config.js
new file mode 100644
index 00000000..52184bc1
--- /dev/null
+++ b/packages/plugins/server/jest.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ roots: ['/test'],
+ testMatch: ['**/*.test.ts'],
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+};
diff --git a/packages/plugins/server/package.json b/packages/plugins/server/package.json
new file mode 100644
index 00000000..8aa20317
--- /dev/null
+++ b/packages/plugins/server/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@objectql/plugin-server",
+ "version": "3.0.1",
+ "description": "HTTP server plugin for ObjectQL - Provides Express, Hono and REST API support",
+ "keywords": [
+ "objectql",
+ "plugin",
+ "server",
+ "http",
+ "api",
+ "rest",
+ "graphql",
+ "express",
+ "hono",
+ "adapter",
+ "backend"
+ ],
+ "license": "MIT",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@objectql/core": "workspace:*",
+ "@objectql/types": "workspace:*",
+ "graphql": "^16.8.1",
+ "@graphql-tools/schema": "^10.0.2",
+ "js-yaml": "^4.1.1"
+ },
+ "peerDependencies": {
+ "hono": "^4.11.0"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^20.10.0",
+ "hono": "^4.11.0",
+ "typescript": "^5.3.0"
+ }
+}
diff --git a/packages/plugins/server/src/adapters/graphql.ts b/packages/plugins/server/src/adapters/graphql.ts
new file mode 100644
index 00000000..160c2b41
--- /dev/null
+++ b/packages/plugins/server/src/adapters/graphql.ts
@@ -0,0 +1,559 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ObjectConfig, FieldConfig } from '@objectql/types';
+import { ObjectQLServer } from '../server';
+import { ErrorCode } from '../types';
+import { IncomingMessage, ServerResponse } from 'http';
+import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLInputObjectType, GraphQLFieldConfigMap, GraphQLOutputType, GraphQLInputType } from 'graphql';
+
+const APOLLO_SANDBOX_HTML = `
+
+
+
+
+
+
+
+`;
+
+/**
+ * Normalize ObjectQL response to use 'id' instead of '_id'
+ */
+function normalizeId(data: unknown): unknown {
+ if (!data) return data;
+
+ if (Array.isArray(data)) {
+ return data.map(item => normalizeId(item));
+ }
+
+ if (typeof data === 'object') {
+ const normalized = { ...data as Record };
+
+ // Map _id to id if present
+ if ('_id' in normalized) {
+ normalized.id = normalized._id;
+ delete normalized._id;
+ }
+
+ // Remove '@type' field as it's not needed in GraphQL
+ delete normalized['@type'];
+
+ return normalized;
+ }
+
+ return data;
+}
+
+/**
+ * Map ObjectQL field types to GraphQL types
+ */
+function mapFieldTypeToGraphQL(field: FieldConfig, isInput: boolean = false): GraphQLOutputType | GraphQLInputType {
+ const type = field.type;
+
+ switch (type) {
+ case 'text':
+ case 'textarea':
+ case 'markdown':
+ case 'html':
+ case 'email':
+ case 'url':
+ case 'phone':
+ case 'password':
+ return GraphQLString;
+ case 'number':
+ case 'currency':
+ case 'percent':
+ return GraphQLFloat;
+ case 'autonumber':
+ return GraphQLInt;
+ case 'boolean':
+ return GraphQLBoolean;
+ case 'date':
+ case 'datetime':
+ case 'time':
+ return GraphQLString; // ISO 8601 string format
+ case 'select':
+ // For select fields, we could create an enum type, but for simplicity use String
+ return GraphQLString;
+ case 'lookup':
+ case 'master_detail':
+ // For relationships, return ID reference
+ return GraphQLString;
+ case 'file':
+ case 'image':
+ // File fields return metadata object (simplified as String for now)
+ return GraphQLString;
+ case 'object':
+ case 'formula':
+ case 'summary':
+ case 'location':
+ case 'vector':
+ case 'grid':
+ // Return as JSON string
+ return GraphQLString;
+ default:
+ return GraphQLString;
+ }
+}
+
+/**
+ * Sanitize field/object names to be valid GraphQL identifiers
+ * GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/
+ */
+function sanitizeGraphQLName(name: string): string {
+ // Replace invalid characters with underscores
+ let sanitized = name.replace(/[^_a-zA-Z0-9]/g, '_');
+
+ // Ensure it starts with a letter or underscore
+ if (!/^[_a-zA-Z]/.test(sanitized)) {
+ sanitized = '_' + sanitized;
+ }
+
+ return sanitized;
+}
+
+/**
+ * Generate GraphQL schema from ObjectQL metadata
+ */
+export function generateGraphQLSchema(app: IObjectQL): GraphQLSchema {
+ const objects = app.metadata.list('object');
+
+ // Validate that there are objects to generate schema from
+ if (!objects || objects.length === 0) {
+ // Create a minimal schema with a dummy query to avoid GraphQL error
+ return new GraphQLSchema({
+ query: new GraphQLObjectType({
+ name: 'Query',
+ fields: {
+ _schema: {
+ type: GraphQLString,
+ description: 'Schema introspection placeholder',
+ resolve: () => 'No objects registered in ObjectQL metadata'
+ }
+ }
+ })
+ });
+ }
+
+ const typeMap: Record = {};
+ const inputTypeMap: Record = {};
+ const deleteResultTypeMap: Record = {};
+
+ // Create a shared ObjectQL server instance to reuse across resolvers
+ // This is safe because ObjectQLServer is stateless - it only holds a reference to the app
+ // and creates fresh contexts for each request via handle()
+ const server = new ObjectQLServer(app);
+
+ // First pass: Create all object types
+ for (const config of objects) {
+ const objectName = config.name;
+
+ // Skip if no name or fields defined
+ if (!objectName || !config.fields || Object.keys(config.fields).length === 0) {
+ continue;
+ }
+
+ const sanitizedTypeName = sanitizeGraphQLName(objectName.charAt(0).toUpperCase() + objectName.slice(1));
+
+ // Create output type
+ const fields: GraphQLFieldConfigMap = {
+ id: { type: new GraphQLNonNull(GraphQLString) }
+ };
+
+ for (const [fieldName, fieldConfig] of Object.entries(config.fields)) {
+ const sanitizedFieldName = sanitizeGraphQLName(fieldName);
+ const gqlType = mapFieldTypeToGraphQL(fieldConfig, false) as GraphQLOutputType;
+ fields[sanitizedFieldName] = {
+ type: fieldConfig.required ? new GraphQLNonNull(gqlType) : gqlType,
+ description: fieldConfig.label || fieldName
+ };
+ }
+
+ typeMap[objectName] = new GraphQLObjectType({
+ name: sanitizedTypeName,
+ description: config.label || objectName,
+ fields
+ });
+
+ // Create input type for mutations
+ const inputFields: Record = {};
+
+ for (const [fieldName, fieldConfig] of Object.entries(config.fields)) {
+ const sanitizedFieldName = sanitizeGraphQLName(fieldName);
+ const gqlType = mapFieldTypeToGraphQL(fieldConfig, true) as GraphQLInputType;
+ inputFields[sanitizedFieldName] = {
+ type: gqlType,
+ description: fieldConfig.label || fieldName
+ };
+ }
+
+ inputTypeMap[objectName] = new GraphQLInputObjectType({
+ name: sanitizedTypeName + 'Input',
+ description: `Input type for ${config.label || objectName}`,
+ fields: inputFields
+ });
+
+ // Create delete result type (shared across all delete mutations for this object)
+ deleteResultTypeMap[objectName] = new GraphQLObjectType({
+ name: 'Delete' + sanitizedTypeName + 'Result',
+ fields: {
+ id: { type: new GraphQLNonNull(GraphQLString) },
+ deleted: { type: new GraphQLNonNull(GraphQLBoolean) }
+ }
+ });
+ }
+
+ // Build query root
+ const queryFields: GraphQLFieldConfigMap = {};
+
+ for (const config of objects) {
+ const objectName = config.name;
+
+ if (!objectName || !typeMap[objectName]) continue;
+
+ // Query single record by ID
+ queryFields[objectName] = {
+ type: typeMap[objectName],
+ args: {
+ id: { type: new GraphQLNonNull(GraphQLString) }
+ },
+ resolve: async (_, args) => {
+ const result = await server.handle({
+ op: 'findOne',
+ object: objectName,
+ args: args.id
+ });
+
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+
+ return normalizeId(result);
+ }
+ };
+
+ // Query list of records
+ // Using 'List' suffix to avoid naming conflicts and handle irregular plurals
+ queryFields[objectName + 'List'] = {
+ type: new GraphQLList(typeMap[objectName]),
+ args: {
+ limit: { type: GraphQLInt },
+ skip: { type: GraphQLInt },
+ filters: { type: GraphQLString }, // JSON string
+ fields: { type: new GraphQLList(GraphQLString) },
+ sort: { type: GraphQLString } // JSON string
+ },
+ resolve: async (_, args) => {
+ const queryArgs: any = {};
+ if (args.limit) queryArgs.limit = args.limit;
+ if (args.skip) queryArgs.skip = args.skip;
+ if (args.fields) queryArgs.fields = args.fields;
+ if (args.filters) {
+ try {
+ queryArgs.filters = JSON.parse(args.filters);
+ } catch (e) {
+ throw new Error('Invalid filters JSON');
+ }
+ }
+ if (args.sort) {
+ try {
+ queryArgs.sort = JSON.parse(args.sort);
+ } catch (e) {
+ throw new Error('Invalid sort JSON');
+ }
+ }
+
+ const result = await server.handle({
+ op: 'find',
+ object: objectName,
+ args: queryArgs
+ });
+
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+
+ return normalizeId(result.items || []);
+ }
+ };
+ }
+
+ const queryType = new GraphQLObjectType({
+ name: 'Query',
+ fields: queryFields
+ });
+
+ // Build mutation root
+ const mutationFields: GraphQLFieldConfigMap = {};
+
+ for (const config of objects) {
+ const objectName = config.name;
+
+ if (!objectName || !typeMap[objectName] || !inputTypeMap[objectName]) continue;
+
+ const capitalizedName = sanitizeGraphQLName(objectName.charAt(0).toUpperCase() + objectName.slice(1));
+
+ // Create mutation
+ mutationFields['create' + capitalizedName] = {
+ type: typeMap[objectName],
+ args: {
+ input: { type: new GraphQLNonNull(inputTypeMap[objectName]) }
+ },
+ resolve: async (_, args) => {
+ const result = await server.handle({
+ op: 'create',
+ object: objectName,
+ args: args.input
+ });
+
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+
+ return normalizeId(result);
+ }
+ };
+
+ // Update mutation
+ mutationFields['update' + capitalizedName] = {
+ type: typeMap[objectName],
+ args: {
+ id: { type: new GraphQLNonNull(GraphQLString) },
+ input: { type: new GraphQLNonNull(inputTypeMap[objectName]) }
+ },
+ resolve: async (_, args) => {
+ const result = await server.handle({
+ op: 'update',
+ object: objectName,
+ args: {
+ id: args.id,
+ data: args.input
+ }
+ });
+
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+
+ return normalizeId(result);
+ }
+ };
+
+ // Delete mutation - use shared delete result type
+ mutationFields['delete' + capitalizedName] = {
+ type: deleteResultTypeMap[objectName],
+ args: {
+ id: { type: new GraphQLNonNull(GraphQLString) }
+ },
+ resolve: async (_, args) => {
+ const result = await server.handle({
+ op: 'delete',
+ object: objectName,
+ args: { id: args.id }
+ });
+
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+
+ return result;
+ }
+ };
+ }
+
+ const mutationType = new GraphQLObjectType({
+ name: 'Mutation',
+ fields: mutationFields
+ });
+
+ return new GraphQLSchema({
+ query: queryType,
+ mutation: mutationType
+ });
+}
+
+/**
+ * Parse GraphQL request body
+ */
+function readBody(req: IncomingMessage): Promise {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.on('data', chunk => body += chunk.toString());
+ req.on('end', () => {
+ if (!body) return resolve({});
+ try {
+ resolve(JSON.parse(body));
+ } catch (e) {
+ reject(new Error('Invalid JSON'));
+ }
+ });
+ req.on('error', reject);
+ });
+}
+
+/**
+ * Send JSON response
+ */
+function sendJSON(res: ServerResponse, statusCode: number, data: any) {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = statusCode;
+ res.end(JSON.stringify(data));
+}
+
+/**
+ * Creates a GraphQL HTTP request handler for ObjectQL
+ *
+ * Endpoints:
+ * - POST /api/graphql - GraphQL queries and mutations
+ * - GET /api/graphql - GraphQL queries via URL parameters
+ */
+export function createGraphQLHandler(app: IObjectQL) {
+ // Generate schema once - Note: Schema is static after handler creation.
+ // If metadata changes at runtime, create a new handler or regenerate the schema.
+ const schema = generateGraphQLSchema(app);
+
+ return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
+ try {
+ // CORS headers
+ const requestOrigin = req.headers.origin;
+ const configuredOrigin = process.env.OBJECTQL_CORS_ORIGIN;
+ const isProduction = process.env.NODE_ENV === 'production';
+
+ if (!isProduction) {
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin || '*');
+ } else if (configuredOrigin && (!requestOrigin || requestOrigin === configuredOrigin)) {
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin);
+ }
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ if (req.method === 'OPTIONS') {
+ res.statusCode = 200;
+ res.end();
+ return;
+ }
+
+ // HTML Playground Support (Apollo Sandbox)
+ // If it's a browser GET request without query params, show the playground
+ const acceptHeader = req.headers.accept || '';
+ const urlObj = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
+ const hasQueryParams = urlObj.searchParams.has('query');
+
+ if (req.method === 'GET' && acceptHeader.includes('text/html') && !hasQueryParams) {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(APOLLO_SANDBOX_HTML);
+ return;
+ }
+
+ const url = req.url || '';
+ const method = req.method || 'POST';
+
+ if (method !== 'GET' && method !== 'POST') {
+ sendJSON(res, 405, {
+ errors: [{
+ message: 'Method not allowed. Use GET or POST.'
+ }]
+ });
+ return;
+ }
+
+ let query: string = '';
+ let variables: any = null;
+ let operationName: string | null = null;
+
+ if (method === 'GET') {
+ // Parse query string for GET requests
+ const urlObj = new URL(url, `http://${req.headers.host || 'localhost'}`);
+ query = urlObj.searchParams.get('query') || '';
+ const varsParam = urlObj.searchParams.get('variables');
+ if (varsParam) {
+ try {
+ variables = JSON.parse(varsParam);
+ } catch (e) {
+ sendJSON(res, 400, {
+ errors: [{
+ message: 'Invalid variables JSON'
+ }]
+ });
+ return;
+ }
+ }
+ operationName = urlObj.searchParams.get('operationName');
+ } else {
+ // Parse body for POST requests
+ const body = req.body || await readBody(req);
+ query = body.query || '';
+ variables = body.variables || null;
+ operationName = body.operationName || null;
+ }
+
+ if (!query) {
+ sendJSON(res, 400, {
+ errors: [{
+ message: 'Must provide query string'
+ }]
+ });
+ return;
+ }
+
+ // Execute GraphQL query
+ const result = await graphql({
+ schema,
+ source: query,
+ variableValues: variables,
+ operationName,
+ contextValue: { app }
+ });
+
+ sendJSON(res, 200, result);
+
+ } catch (e: any) {
+ console.error('[GraphQL Handler] Error:', e);
+
+ const errorResponse: {
+ errors: Array<{
+ message: string;
+ extensions: {
+ code: ErrorCode;
+ debug?: {
+ message?: string;
+ stack?: string;
+ };
+ };
+ }>;
+ } = {
+ errors: [{
+ message: 'Internal server error',
+ extensions: {
+ code: ErrorCode.INTERNAL_ERROR
+ }
+ }]
+ };
+
+ // In non-production environments, include additional error details to aid debugging
+ if (typeof process !== 'undefined' &&
+ process.env &&
+ process.env.NODE_ENV !== 'production') {
+ const firstError = errorResponse.errors[0];
+ firstError.extensions.debug = {
+ message: e && typeof e.message === 'string' ? e.message : undefined,
+ stack: e && typeof e.stack === 'string' ? e.stack : undefined
+ };
+ }
+
+ sendJSON(res, 500, errorResponse);
+ }
+ };
+}
diff --git a/packages/plugins/server/src/adapters/hono.ts b/packages/plugins/server/src/adapters/hono.ts
new file mode 100644
index 00000000..db6852c8
--- /dev/null
+++ b/packages/plugins/server/src/adapters/hono.ts
@@ -0,0 +1,258 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
+import { ObjectQLServer } from '../server';
+import { ObjectQLRequest, ErrorCode } from '../types';
+
+/**
+ * Options for createHonoAdapter
+ */
+export interface HonoAdapterOptions {
+ /** Custom API route configuration */
+ routes?: ApiRouteConfig;
+}
+
+/**
+ * Creates a Hono-compatible middleware for ObjectQL
+ *
+ * This adapter integrates ObjectQL with the Hono web framework.
+ *
+ * @example
+ * ```typescript
+ * import { Hono } from 'hono';
+ * import { ObjectQL } from '@objectql/core';
+ * import { createHonoAdapter } from '@objectql/plugin-server/hono';
+ *
+ * const app = new ObjectQL({ ... });
+ * await app.init();
+ *
+ * const server = new Hono();
+ * const objectqlMiddleware = createHonoAdapter(app);
+ *
+ * server.all('/api/*', objectqlMiddleware);
+ * ```
+ */
+export function createHonoAdapter(app: IObjectQL, options?: HonoAdapterOptions) {
+ const server = new ObjectQLServer(app);
+ const routes = resolveApiRoutes(options?.routes);
+
+ // Return Hono-compatible handler
+ return async (c: any) => {
+ try {
+ const req = c.req;
+ const path = req.path;
+ const method = req.method;
+
+ // Handle JSON-RPC endpoint
+ if (path === routes.rpc || path.startsWith(routes.rpc + '/')) {
+ if (method === 'POST') {
+ const body = await req.json();
+ const qlReq: ObjectQLRequest = {
+ op: body.op,
+ object: body.object,
+ args: body.args,
+ user: body.user,
+ ai_context: body.ai_context
+ };
+
+ const result = await server.handle(qlReq);
+
+ // Determine HTTP status code based on error
+ let statusCode = 200;
+ if (result.error) {
+ statusCode = getStatusCodeFromError(result.error.code as ErrorCode);
+ }
+
+ return c.json(result, statusCode);
+ }
+ return c.json({ error: { code: ErrorCode.INVALID_REQUEST, message: 'Method not allowed' } }, 405);
+ }
+
+ // Handle REST API endpoint
+ if (path.startsWith(routes.data + '/')) {
+ const pathParts = path.replace(routes.data + '/', '').split('/');
+ const objectName = pathParts[0];
+ const id = pathParts[1];
+
+ let qlReq: ObjectQLRequest;
+
+ switch (method) {
+ case 'GET':
+ if (id) {
+ // GET /api/data/:object/:id - findOne
+ qlReq = {
+ op: 'findOne',
+ object: objectName,
+ args: id
+ };
+ } else {
+ // GET /api/data/:object - find with query params
+ const query = req.query();
+ const args: any = {};
+ if (query.filter) args.filters = JSON.parse(query.filter);
+ if (query.fields) args.fields = query.fields.split(',');
+ if (query.limit || query.top) args.limit = parseInt(query.limit || query.top);
+ if (query.skip || query.offset) args.skip = parseInt(query.skip || query.offset);
+
+ qlReq = {
+ op: 'find',
+ object: objectName,
+ args
+ };
+ }
+ break;
+
+ case 'POST':
+ const createBody = await req.json();
+ if (Array.isArray(createBody)) {
+ // Bulk create
+ qlReq = {
+ op: 'createMany',
+ object: objectName,
+ args: createBody
+ };
+ } else {
+ // Single create
+ qlReq = {
+ op: 'create',
+ object: objectName,
+ args: createBody
+ };
+ }
+ break;
+
+ case 'PUT':
+ case 'PATCH':
+ if (!id) {
+ return c.json({
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'ID is required for update'
+ }
+ }, 400);
+ }
+ const updateBody = await req.json();
+ qlReq = {
+ op: 'update',
+ object: objectName,
+ args: { id, data: updateBody }
+ };
+ break;
+
+ case 'DELETE':
+ if (!id) {
+ return c.json({
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'ID is required for delete'
+ }
+ }, 400);
+ }
+ qlReq = {
+ op: 'delete',
+ object: objectName,
+ args: { id }
+ };
+ break;
+
+ default:
+ return c.json({
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'Method not allowed'
+ }
+ }, 405);
+ }
+
+ const result = await server.handle(qlReq);
+ let statusCode = 200;
+ if (result.error) {
+ statusCode = getStatusCodeFromError(result.error.code as ErrorCode);
+ } else if (method === 'POST') {
+ statusCode = 201;
+ }
+
+ return c.json(result, statusCode);
+ }
+
+ // Handle Metadata endpoint
+ if (path.startsWith(routes.metadata + '/') || path === routes.metadata) {
+ const resource = path.replace(routes.metadata, '').replace(/^\//, '');
+
+ if (!resource || resource === 'object') {
+ // List all objects
+ const objects = app.metadata.list('object');
+ return c.json({ objects });
+ }
+
+ if (resource.startsWith('object/')) {
+ // Get specific object
+ const objectName = resource.replace('object/', '');
+ const obj = app.getObject(objectName);
+ if (!obj) {
+ return c.json({
+ error: {
+ code: ErrorCode.NOT_FOUND,
+ message: `Object '${objectName}' not found`
+ }
+ }, 404);
+ }
+ return c.json({ object: obj });
+ }
+
+ return c.json({
+ error: {
+ code: ErrorCode.NOT_FOUND,
+ message: 'Metadata resource not found'
+ }
+ }, 404);
+ }
+
+ // Default 404
+ return c.json({
+ error: {
+ code: ErrorCode.NOT_FOUND,
+ message: 'Endpoint not found'
+ }
+ }, 404);
+
+ } catch (e: any) {
+ console.error('[Hono Adapter] Error:', e);
+ return c.json({
+ error: {
+ code: ErrorCode.INTERNAL_ERROR,
+ message: 'Internal server error'
+ }
+ }, 500);
+ }
+ };
+}
+
+/**
+ * Map ObjectQL error codes to HTTP status codes
+ */
+function getStatusCodeFromError(code: ErrorCode): number {
+ switch (code) {
+ case ErrorCode.INVALID_REQUEST:
+ case ErrorCode.VALIDATION_ERROR:
+ return 400;
+ case ErrorCode.UNAUTHORIZED:
+ return 401;
+ case ErrorCode.FORBIDDEN:
+ return 403;
+ case ErrorCode.NOT_FOUND:
+ return 404;
+ case ErrorCode.CONFLICT:
+ return 409;
+ case ErrorCode.RATE_LIMIT_EXCEEDED:
+ return 429;
+ default:
+ return 500;
+ }
+}
diff --git a/packages/plugins/server/src/adapters/node.ts b/packages/plugins/server/src/adapters/node.ts
new file mode 100644
index 00000000..3542d580
--- /dev/null
+++ b/packages/plugins/server/src/adapters/node.ts
@@ -0,0 +1,293 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
+import { ObjectQLServer } from '../server';
+import { ObjectQLRequest, ErrorCode, IFileStorage } from '../types';
+import { IncomingMessage, ServerResponse } from 'http';
+import { generateOpenAPI } from '../openapi';
+import { createFileUploadHandler, createBatchFileUploadHandler, createFileDownloadHandler } from '../file-handler';
+import { LocalFileStorage } from '../storage';
+import { escapeRegexPath } from '../utils';
+import { getWelcomePageHtml } from '../templates';
+
+/**
+ * Options for createNodeHandler
+ */
+export interface NodeHandlerOptions {
+ /** File storage provider (defaults to LocalFileStorage) */
+ fileStorage?: IFileStorage;
+ /** Custom API route configuration */
+ routes?: ApiRouteConfig;
+}
+
+/**
+ * Creates a standard Node.js HTTP request handler.
+ */
+export function createNodeHandler(app: IObjectQL, options?: NodeHandlerOptions) {
+ const server = new ObjectQLServer(app);
+ const routes = resolveApiRoutes(options?.routes);
+
+ // Initialize file storage
+ const defaultBaseUrl = process.env.OBJECTQL_BASE_URL || `http://localhost:3000${routes.files}`;
+ const fileStorage = options?.fileStorage || new LocalFileStorage({
+ baseDir: process.env.OBJECTQL_UPLOAD_DIR || './uploads',
+ baseUrl: defaultBaseUrl
+ });
+
+ // Create file handlers
+ const uploadHandler = createFileUploadHandler(fileStorage, app);
+ const batchUploadHandler = createBatchFileUploadHandler(fileStorage, app);
+ const downloadHandler = createFileDownloadHandler(fileStorage);
+
+
+ return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
+ // CORS Headers
+ const origin = req.headers.origin;
+ if (origin) {
+ res.setHeader('Access-Control-Allow-Origin', origin);
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
+ } else {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ }
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
+
+ // Handle preflight requests
+ if (req.method === 'OPTIONS') {
+ res.statusCode = 204;
+ res.end();
+ return;
+ }
+
+ // Handle OpenAPI spec request
+ if (req.method === 'GET' && req.url?.endsWith('/openapi.json')) {
+ const spec = generateOpenAPI(app);
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = 200;
+ res.end(JSON.stringify(spec));
+ return;
+ }
+
+ const handleRequest = async (json: any) => {
+ try {
+ // Determine Operation based on JSON or previously derived info
+ const qlReq: ObjectQLRequest = {
+ op: json.op,
+ object: json.object,
+ args: json.args,
+ user: json.user,
+ ai_context: json.ai_context
+ };
+
+ const result = await server.handle(qlReq);
+
+ // Determine HTTP status code based on error
+ let statusCode = 200;
+ if (result.error) {
+ switch (result.error.code) {
+ case ErrorCode.INVALID_REQUEST:
+ case ErrorCode.VALIDATION_ERROR:
+ statusCode = 400;
+ break;
+ case ErrorCode.UNAUTHORIZED:
+ statusCode = 401;
+ break;
+ case ErrorCode.FORBIDDEN:
+ statusCode = 403;
+ break;
+ case ErrorCode.NOT_FOUND:
+ statusCode = 404;
+ break;
+ case ErrorCode.CONFLICT:
+ statusCode = 409;
+ break;
+ case ErrorCode.RATE_LIMIT_EXCEEDED:
+ statusCode = 429;
+ break;
+ default:
+ statusCode = 500;
+ }
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = statusCode;
+ res.end(JSON.stringify(result));
+ } catch (e) {
+ console.error(e);
+ res.statusCode = 500;
+ res.end(JSON.stringify({
+ error: {
+ code: ErrorCode.INTERNAL_ERROR,
+ message: 'Internal Server Error'
+ }
+ }));
+ }
+ };
+
+ // Parse URL
+ const urlObj = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
+ const pathName = urlObj.pathname;
+ const method = req.method;
+
+ // 1. JSON-RPC: POST {rpcPath}
+ if (pathName === routes.rpc && method === 'POST') {
+ await processBody(req, async (json) => {
+ await handleRequest(json);
+ }, res);
+ return;
+ }
+
+ // 2. REST API: {dataPath}/:object and {dataPath}/:object/:id
+ // Regex to match {dataPath}/objectName(/id)?
+ const escapedDataPath = escapeRegexPath(routes.data);
+ const restMatch = pathName.match(new RegExp(`^${escapedDataPath}/([^/]+)(?:/(.+))?$`));
+
+ if (restMatch) {
+ const objectName = restMatch[1];
+ const id = restMatch[2];
+ const query = Object.fromEntries(urlObj.searchParams.entries());
+
+ if (method === 'GET') {
+ // GET {dataPath}/:object/:id -> findOne
+ if (id) {
+ await handleRequest({
+ op: 'findOne',
+ object: objectName,
+ args: id
+ });
+ }
+ // GET {dataPath}/:object -> find (List)
+ else {
+ // Parse standard params
+ const args: any = {};
+ if (query.fields) args.fields = (query.fields as string).split(',');
+ if (query.top) args.limit = parseInt(query.top as string);
+ if (query.skip) args.skip = parseInt(query.skip as string);
+ if (query.filter) {
+ try {
+ args.filters = JSON.parse(query.filter as string);
+ } catch (e) {
+ // ignore invalid filter json
+ }
+ }
+ await handleRequest({ op: 'find', object: objectName, args });
+ }
+ return;
+ }
+
+ if (method === 'POST' && !id) {
+ // POST {dataPath}/:object -> create
+ await processBody(req, async (body) => {
+ await handleRequest({
+ op: 'create',
+ object: objectName,
+ args: body.data || body // Support enclosed in data or flat
+ });
+ }, res);
+ return;
+ }
+
+ if (method === 'PATCH' && id) {
+ // PATCH {dataPath}/:object/:id -> update
+ await processBody(req, async (body) => {
+ await handleRequest({
+ op: 'update',
+ object: objectName,
+ args: {
+ id: id,
+ data: body.data || body
+ }
+ });
+ }, res);
+ return;
+ }
+
+ if (method === 'DELETE' && id) {
+ // DELETE {dataPath}/:object/:id -> delete
+ await handleRequest({
+ op: 'delete',
+ object: objectName,
+ args: { id: id }
+ });
+ return;
+ }
+ }
+
+ // File Upload Endpoints
+ // POST {filesPath}/upload - Single file upload
+ if (pathName === `${routes.files}/upload` && method === 'POST') {
+ await uploadHandler(req, res);
+ return;
+ }
+
+ // POST {filesPath}/upload/batch - Batch file upload
+ if (pathName === `${routes.files}/upload/batch` && method === 'POST') {
+ await batchUploadHandler(req, res);
+ return;
+ }
+
+ // GET {filesPath}/:fileId - Download file
+ const escapedFilesPath = escapeRegexPath(routes.files);
+ const fileMatch = pathName.match(new RegExp(`^${escapedFilesPath}/([^/]+)$`));
+ if (fileMatch && method === 'GET') {
+ const fileId = fileMatch[1];
+ await downloadHandler(req, res, fileId);
+ return;
+ }
+
+ // Fallback or 404
+ if (req.method === 'POST') {
+ // Fallback for root POSTs if people forget {rpcPath} but send to /api something
+ await processBody(req, handleRequest, res);
+ return;
+ }
+
+ // Special case for root: since we accept POST / (RPC), correct response for GET / is 405
+ if (pathName === '/') {
+ if (method === 'GET') {
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.statusCode = 200;
+ res.end(getWelcomePageHtml(routes));
+ return;
+ }
+
+ res.setHeader('Allow', 'POST');
+ res.statusCode = 405;
+ res.end(JSON.stringify({ error: { code: ErrorCode.INVALID_REQUEST, message: 'Method Not Allowed. Use POST for JSON-RPC.' } }));
+ return;
+ }
+
+ res.statusCode = 404;
+ res.end(JSON.stringify({ error: { code: ErrorCode.NOT_FOUND, message: 'Not Found' } }));
+ };
+}
+
+// Helper to process body
+async function processBody(req: IncomingMessage & { body?: any }, callback: (json: any) => Promise, res: ServerResponse) {
+ if (req.body && typeof req.body === 'object') {
+ return callback(req.body);
+ }
+
+ let body = '';
+ req.on('data', chunk => body += chunk);
+ req.on('end', async () => {
+ try {
+ const json = body ? JSON.parse(body) : {};
+ await callback(json);
+ } catch (e) {
+ res.statusCode = 400;
+ res.end(JSON.stringify({
+ error: {
+ code: 'INVALID_JSON',
+ message: 'Invalid JSON body'
+ }
+ }));
+ }
+ });
+}
diff --git a/packages/plugins/server/src/adapters/rest.ts b/packages/plugins/server/src/adapters/rest.ts
new file mode 100644
index 00000000..d0396320
--- /dev/null
+++ b/packages/plugins/server/src/adapters/rest.ts
@@ -0,0 +1,338 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
+import { ObjectQLServer } from '../server';
+import { ObjectQLRequest, ErrorCode } from '../types';
+import { IncomingMessage, ServerResponse } from 'http';
+import { escapeRegexPath } from '../utils';
+
+/**
+ * Parse query string parameters
+ */
+function parseQueryParams(url: string): Record {
+ const params: Record = {};
+ const queryIndex = url.indexOf('?');
+ if (queryIndex === -1) return params;
+
+ const queryString = url.substring(queryIndex + 1);
+ const pairs = queryString.split('&');
+
+ for (const pair of pairs) {
+ const [key, value] = pair.split('=');
+ if (!key) continue;
+
+ const decodedKey = decodeURIComponent(key);
+ const decodedValue = decodeURIComponent(value || '');
+
+ // Try to parse JSON values
+ try {
+ params[decodedKey] = JSON.parse(decodedValue);
+ } catch {
+ params[decodedKey] = decodedValue;
+ }
+ }
+
+ return params;
+}
+
+/**
+ * Read request body as JSON
+ */
+function readBody(req: IncomingMessage): Promise {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.on('data', chunk => body += chunk.toString());
+ req.on('end', () => {
+ if (!body) return resolve({});
+ try {
+ resolve(JSON.parse(body));
+ } catch (e) {
+ reject(new Error('Invalid JSON'));
+ }
+ });
+ req.on('error', reject);
+ });
+}
+
+/**
+ * Send JSON response
+ */
+function sendJSON(res: ServerResponse, statusCode: number, data: any) {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = statusCode;
+ res.end(JSON.stringify(data));
+}
+
+/**
+ * Options for createRESTHandler
+ */
+export interface RESTHandlerOptions {
+ /** Custom API route configuration */
+ routes?: ApiRouteConfig;
+}
+
+/**
+ * Creates a REST-style HTTP request handler for ObjectQL
+ *
+ * Default Endpoints (configurable via routes option):
+ * - GET {dataPath}/:object - List records
+ * - GET {dataPath}/:object/:id - Get single record
+ * - POST {dataPath}/:object - Create record (or create many if array)
+ * - POST {dataPath}/:object/bulk-update - Update many records
+ * - POST {dataPath}/:object/bulk-delete - Delete many records
+ * - PUT {dataPath}/:object/:id - Update record
+ * - DELETE {dataPath}/:object/:id - Delete record
+ *
+ * @param app - ObjectQL application instance
+ * @param options - Optional configuration including custom routes
+ */
+export function createRESTHandler(app: IObjectQL, options?: RESTHandlerOptions) {
+ const server = new ObjectQLServer(app);
+ const routes = resolveApiRoutes(options?.routes);
+ const dataPath = routes.data;
+
+ return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
+ try {
+ // CORS headers
+ const requestOrigin = req.headers.origin;
+ const configuredOrigin = process.env.OBJECTQL_CORS_ORIGIN;
+ const isProduction = process.env.NODE_ENV === 'production';
+
+ // In development, allow all origins by default (or use configured override).
+ // In production, require an explicit OBJECTQL_CORS_ORIGIN to be set.
+ if (!isProduction) {
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin || '*');
+ } else if (configuredOrigin && (!requestOrigin || requestOrigin === configuredOrigin)) {
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin);
+ }
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ if (req.method === 'OPTIONS') {
+ res.statusCode = 200;
+ res.end();
+ return;
+ }
+
+ const url = req.url || '';
+ const method = req.method || 'GET';
+
+ // Parse URL: {dataPath}/:object or {dataPath}/:object/:id or {dataPath}/:object/bulk-*
+ const escapedPath = escapeRegexPath(dataPath);
+ const match = url.match(new RegExp(`^${escapedPath}/([^/\\?]+)(?:/([^/\\?]+))?(\\?.*)?$`));
+
+ if (!match) {
+ sendJSON(res, 404, {
+ error: {
+ code: ErrorCode.NOT_FOUND,
+ message: 'Invalid REST API endpoint'
+ }
+ });
+ return;
+ }
+
+ const [, objectName, id, queryString] = match;
+ const queryParams = queryString ? parseQueryParams(queryString) : {};
+
+ let qlRequest: ObjectQLRequest;
+
+ switch (method) {
+ case 'GET':
+ if (id) {
+ // GET /api/data/:object/:id - Get single record
+ qlRequest = {
+ op: 'findOne',
+ object: objectName,
+ args: id
+ };
+ } else {
+ // GET {dataPath}/:object - List records
+ const args: any = {};
+
+ // Parse query parameters
+ if (queryParams.filter) {
+ args.filters = queryParams.filter;
+ }
+ if (queryParams.fields) {
+ args.fields = queryParams.fields;
+ }
+ if (queryParams.sort) {
+ args.sort = Array.isArray(queryParams.sort)
+ ? queryParams.sort
+ : [[queryParams.sort, 'asc']];
+ }
+ if (queryParams.top || queryParams.limit) {
+ args.limit = queryParams.top || queryParams.limit;
+ }
+ if (queryParams.skip || queryParams.offset) {
+ args.skip = queryParams.skip || queryParams.offset;
+ }
+ if (queryParams.expand) {
+ args.expand = queryParams.expand;
+ }
+
+ qlRequest = {
+ op: 'find',
+ object: objectName,
+ args
+ };
+ }
+ break;
+
+ case 'POST':
+ const createBody = req.body || await readBody(req);
+
+ // Check for bulk operations
+ if (id === 'bulk-update') {
+ // POST {dataPath}/:object/bulk-update - Update many records
+ qlRequest = {
+ op: 'updateMany',
+ object: objectName,
+ args: {
+ filters: createBody.filters,
+ data: createBody.data
+ }
+ };
+ } else if (id === 'bulk-delete') {
+ // POST {dataPath}/:object/bulk-delete - Delete many records
+ qlRequest = {
+ op: 'deleteMany',
+ object: objectName,
+ args: {
+ filters: createBody.filters || {}
+ }
+ };
+ } else if (Array.isArray(createBody)) {
+ // POST {dataPath}/:object with array - Create many records
+ qlRequest = {
+ op: 'createMany',
+ object: objectName,
+ args: createBody
+ };
+ } else {
+ // POST {dataPath}/:object - Create single record
+ qlRequest = {
+ op: 'create',
+ object: objectName,
+ args: createBody
+ };
+ }
+ break;
+
+ case 'PUT':
+ case 'PATCH':
+ // PUT {dataPath}/:object/:id - Update record
+ if (!id) {
+ sendJSON(res, 400, {
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'ID is required for update operation'
+ }
+ });
+ return;
+ }
+
+ const updateBody = req.body || await readBody(req);
+ qlRequest = {
+ op: 'update',
+ object: objectName,
+ args: {
+ id,
+ data: updateBody
+ }
+ };
+ break;
+
+ case 'DELETE':
+ // DELETE {dataPath}/:object/:id - Delete record
+ if (!id) {
+ sendJSON(res, 400, {
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'ID is required for delete operation'
+ }
+ });
+ return;
+ }
+
+ qlRequest = {
+ op: 'delete',
+ object: objectName,
+ args: { id }
+ };
+ break;
+
+ default:
+ sendJSON(res, 405, {
+ error: {
+ code: ErrorCode.INVALID_REQUEST,
+ message: 'Method not allowed'
+ }
+ });
+ return;
+ }
+
+ // Execute the request
+ const result = await server.handle(qlRequest);
+
+ if (!result) {
+ sendJSON(res, 404, {
+ error: {
+ code: ErrorCode.NOT_FOUND,
+ message: 'Resource not found'
+ }
+ });
+ return;
+ }
+
+ // Determine HTTP status code
+ let statusCode = 200;
+ if (result.error) {
+ switch (result.error.code) {
+ case ErrorCode.INVALID_REQUEST:
+ case ErrorCode.VALIDATION_ERROR:
+ statusCode = 400;
+ break;
+ case ErrorCode.UNAUTHORIZED:
+ statusCode = 401;
+ break;
+ case ErrorCode.FORBIDDEN:
+ statusCode = 403;
+ break;
+ case ErrorCode.NOT_FOUND:
+ statusCode = 404;
+ break;
+ case ErrorCode.CONFLICT:
+ statusCode = 409;
+ break;
+ case ErrorCode.RATE_LIMIT_EXCEEDED:
+ statusCode = 429;
+ break;
+ default:
+ statusCode = 500;
+ }
+ } else if (method === 'POST' && qlRequest.op === 'create') {
+ statusCode = 201; // Created - only for single create
+ } else if (method === 'POST' && qlRequest.op === 'createMany') {
+ statusCode = 201; // Created - for bulk create
+ }
+
+ sendJSON(res, statusCode, result);
+
+ } catch (e: any) {
+ console.error('[REST Handler] Error:', e);
+ sendJSON(res, 500, {
+ error: {
+ code: ErrorCode.INTERNAL_ERROR,
+ message: 'Internal server error'
+ }
+ });
+ }
+ };
+}
diff --git a/packages/plugins/server/src/file-handler.ts b/packages/plugins/server/src/file-handler.ts
new file mode 100644
index 00000000..03846347
--- /dev/null
+++ b/packages/plugins/server/src/file-handler.ts
@@ -0,0 +1,422 @@
+/**
+ * ObjectQL
+ * 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 { IncomingMessage, ServerResponse } from 'http';
+import { IFileStorage, AttachmentData, ErrorCode } from './types';
+import { IObjectQL, FieldConfig } from '@objectql/types';
+
+/**
+ * Parse multipart/form-data request
+ */
+export function parseMultipart(
+ req: IncomingMessage,
+ boundary: string
+): Promise<{ fields: Record; files: Array<{ fieldname: string; filename: string; mimeType: string; buffer: Buffer }> }> {
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+
+ req.on('data', (chunk) => chunks.push(chunk));
+ req.on('error', reject);
+ req.on('end', () => {
+ try {
+ const buffer = Buffer.concat(chunks);
+ const result = parseMultipartBuffer(buffer, boundary);
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+}
+
+function parseMultipartBuffer(
+ buffer: Buffer,
+ boundary: string
+): { fields: Record; files: Array<{ fieldname: string; filename: string; mimeType: string; buffer: Buffer }> } {
+ const fields: Record = {};
+ const files: Array<{ fieldname: string; filename: string; mimeType: string; buffer: Buffer }> = [];
+
+ const delimiter = Buffer.from(`--${boundary}`);
+ const parts = splitBuffer(buffer, delimiter);
+
+ for (const part of parts) {
+ if (part.length === 0 || part.toString().trim() === '--') {
+ continue;
+ }
+
+ // Find header/body separator (double CRLF)
+ const headerEnd = findSequence(part, Buffer.from('\r\n\r\n'));
+ if (headerEnd === -1) continue;
+
+ const headerSection = part.slice(0, headerEnd).toString();
+ const bodySection = part.slice(headerEnd + 4);
+
+ // Parse Content-Disposition header
+ const dispositionMatch = headerSection.match(/Content-Disposition: form-data; name="([^"]+)"(?:; filename="([^"]+)")?/i);
+ if (!dispositionMatch) continue;
+
+ const fieldname = dispositionMatch[1];
+ const filename = dispositionMatch[2];
+
+ if (filename) {
+ // This is a file upload
+ const contentTypeMatch = headerSection.match(/Content-Type: (.+)/i);
+ const mimeType = contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream';
+
+ // Remove trailing CRLF from body
+ let fileBuffer = bodySection;
+ if (fileBuffer.length >= 2 && fileBuffer[fileBuffer.length - 2] === 0x0d && fileBuffer[fileBuffer.length - 1] === 0x0a) {
+ fileBuffer = fileBuffer.slice(0, -2);
+ }
+
+ files.push({ fieldname, filename, mimeType, buffer: fileBuffer });
+ } else {
+ // This is a regular form field
+ let value = bodySection.toString('utf-8');
+ if (value.endsWith('\r\n')) {
+ value = value.slice(0, -2);
+ }
+ fields[fieldname] = value;
+ }
+ }
+
+ return { fields, files };
+}
+
+function splitBuffer(buffer: Buffer, delimiter: Buffer): Buffer[] {
+ const parts: Buffer[] = [];
+ let start = 0;
+ let pos = 0;
+
+ while (pos <= buffer.length - delimiter.length) {
+ let match = true;
+ for (let i = 0; i < delimiter.length; i++) {
+ if (buffer[pos + i] !== delimiter[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ if (match) {
+ if (pos > start) {
+ parts.push(buffer.slice(start, pos));
+ }
+ pos += delimiter.length;
+ start = pos;
+ } else {
+ pos++;
+ }
+ }
+
+ if (start < buffer.length) {
+ parts.push(buffer.slice(start));
+ }
+
+ return parts;
+}
+
+function findSequence(buffer: Buffer, sequence: Buffer): number {
+ for (let i = 0; i <= buffer.length - sequence.length; i++) {
+ let match = true;
+ for (let j = 0; j < sequence.length; j++) {
+ if (buffer[i + j] !== sequence[j]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) return i;
+ }
+ return -1;
+}
+
+/**
+ * Validate uploaded file against field configuration
+ */
+export function validateFile(
+ file: { filename: string; mimeType: string; buffer: Buffer },
+ fieldConfig?: FieldConfig,
+ objectName?: string,
+ fieldName?: string
+): { valid: boolean; error?: { code: string; message: string; details?: any } } {
+ // If no field config provided, allow the upload
+ if (!fieldConfig) {
+ return { valid: true };
+ }
+
+ const fileSize = file.buffer.length;
+ const fileName = file.filename;
+ const mimeType = file.mimeType;
+
+ // Validate file size
+ if (fieldConfig.max_size && fileSize > fieldConfig.max_size) {
+ return {
+ valid: false,
+ error: {
+ code: 'FILE_TOO_LARGE',
+ message: `File size (${fileSize} bytes) exceeds maximum allowed size (${fieldConfig.max_size} bytes)`,
+ details: {
+ file: fileName,
+ size: fileSize,
+ max_size: fieldConfig.max_size
+ }
+ }
+ };
+ }
+
+ if (fieldConfig.min_size && fileSize < fieldConfig.min_size) {
+ return {
+ valid: false,
+ error: {
+ code: 'FILE_TOO_SMALL',
+ message: `File size (${fileSize} bytes) is below minimum required size (${fieldConfig.min_size} bytes)`,
+ details: {
+ file: fileName,
+ size: fileSize,
+ min_size: fieldConfig.min_size
+ }
+ }
+ };
+ }
+
+ // Validate file type/extension
+ if (fieldConfig.accept && Array.isArray(fieldConfig.accept) && fieldConfig.accept.length > 0) {
+ const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
+ const acceptedExtensions = fieldConfig.accept.map(ext => ext.toLowerCase());
+
+ if (!acceptedExtensions.includes(fileExt)) {
+ return {
+ valid: false,
+ error: {
+ code: 'FILE_TYPE_NOT_ALLOWED',
+ message: `File type '${fileExt}' is not allowed. Allowed types: ${acceptedExtensions.join(', ')}`,
+ details: {
+ file: fileName,
+ extension: fileExt,
+ allowed: acceptedExtensions
+ }
+ }
+ };
+ }
+ }
+
+ return { valid: true };
+}
+
+/**
+ * Send error response
+ */
+export function sendError(res: ServerResponse, statusCode: number, code: string, message: string, details?: any) {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = statusCode;
+ res.end(JSON.stringify({
+ error: {
+ code,
+ message,
+ details
+ }
+ }));
+}
+
+/**
+ * Send success response
+ */
+export function sendSuccess(res: ServerResponse, data: any) {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = 200;
+ res.end(JSON.stringify({ data }));
+}
+
+/**
+ * Extract user ID from authorization header
+ * @internal This is a placeholder implementation. In production, integrate with actual auth middleware.
+ */
+function extractUserId(authHeader: string | undefined): string | undefined {
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return undefined;
+ }
+
+ // TODO: In production, decode JWT or validate token properly
+ // This is a placeholder implementation
+ console.warn('[Security] File upload authentication is using placeholder implementation. Integrate with actual auth system.');
+ return 'user_from_token';
+}
+
+/**
+ * Create file upload handler
+ */
+export function createFileUploadHandler(storage: IFileStorage, app: IObjectQL) {
+ return async (req: IncomingMessage, res: ServerResponse) => {
+ try {
+ // Check content type
+ const contentType = req.headers['content-type'];
+ if (!contentType || !contentType.startsWith('multipart/form-data')) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'Content-Type must be multipart/form-data');
+ return;
+ }
+
+ // Extract boundary
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
+ if (!boundaryMatch) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'Missing boundary in Content-Type');
+ return;
+ }
+
+ const boundary = boundaryMatch[1];
+
+ // Parse multipart data
+ const { fields, files } = await parseMultipart(req, boundary);
+
+ if (files.length === 0) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'No file provided');
+ return;
+ }
+
+ // Get field configuration if object and field are specified
+ let fieldConfig: FieldConfig | undefined;
+ if (fields.object && fields.field) {
+ const objectConfig = (app as any).getObject(fields.object);
+ if (objectConfig && objectConfig.fields) {
+ fieldConfig = objectConfig.fields[fields.field];
+ }
+ }
+
+ // Single file upload
+ const file = files[0];
+
+ // Validate file
+ const validation = validateFile(file, fieldConfig, fields.object, fields.field);
+ if (!validation.valid) {
+ sendError(res, 400, validation.error!.code, validation.error!.message, validation.error!.details);
+ return;
+ }
+
+ // Extract user ID from authorization header
+ const userId = extractUserId(req.headers.authorization);
+
+ // Save file
+ const attachmentData = await storage.save(
+ file.buffer,
+ file.filename,
+ file.mimeType,
+ {
+ folder: fields.folder,
+ object: fields.object,
+ field: fields.field,
+ userId
+ }
+ );
+
+ sendSuccess(res, attachmentData);
+ } catch (error) {
+ console.error('File upload error:', error);
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, 'File upload failed');
+ }
+ };
+}
+
+/**
+ * Create batch file upload handler
+ */
+export function createBatchFileUploadHandler(storage: IFileStorage, app: IObjectQL) {
+ return async (req: IncomingMessage, res: ServerResponse) => {
+ try {
+ // Check content type
+ const contentType = req.headers['content-type'];
+ if (!contentType || !contentType.startsWith('multipart/form-data')) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'Content-Type must be multipart/form-data');
+ return;
+ }
+
+ // Extract boundary
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
+ if (!boundaryMatch) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'Missing boundary in Content-Type');
+ return;
+ }
+
+ const boundary = boundaryMatch[1];
+
+ // Parse multipart data
+ const { fields, files } = await parseMultipart(req, boundary);
+
+ if (files.length === 0) {
+ sendError(res, 400, ErrorCode.INVALID_REQUEST, 'No files provided');
+ return;
+ }
+
+ // Get field configuration if object and field are specified
+ let fieldConfig: FieldConfig | undefined;
+ if (fields.object && fields.field) {
+ const objectConfig = (app as any).getObject(fields.object);
+ if (objectConfig && objectConfig.fields) {
+ fieldConfig = objectConfig.fields[fields.field];
+ }
+ }
+
+ // Extract user ID from authorization header
+ const userId = extractUserId(req.headers.authorization);
+
+ // Upload all files
+ const uploadedFiles: AttachmentData[] = [];
+
+ for (const file of files) {
+ // Validate each file
+ const validation = validateFile(file, fieldConfig, fields.object, fields.field);
+ if (!validation.valid) {
+ sendError(res, 400, validation.error!.code, validation.error!.message, validation.error!.details);
+ return;
+ }
+
+ // Save file
+ const attachmentData = await storage.save(
+ file.buffer,
+ file.filename,
+ file.mimeType,
+ {
+ folder: fields.folder,
+ object: fields.object,
+ field: fields.field,
+ userId
+ }
+ );
+
+ uploadedFiles.push(attachmentData);
+ }
+
+ sendSuccess(res, uploadedFiles);
+ } catch (error) {
+ console.error('Batch file upload error:', error);
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, 'Batch file upload failed');
+ }
+ };
+}
+
+/**
+ * Create file download handler
+ */
+export function createFileDownloadHandler(storage: IFileStorage) {
+ return async (req: IncomingMessage, res: ServerResponse, fileId: string) => {
+ try {
+ const file = await storage.get(fileId);
+
+ if (!file) {
+ sendError(res, 404, ErrorCode.NOT_FOUND, 'File not found');
+ return;
+ }
+
+ // Set appropriate headers
+ res.setHeader('Content-Type', 'application/octet-stream');
+ res.setHeader('Content-Length', file.length);
+ res.statusCode = 200;
+ res.end(file);
+ } catch (error) {
+ console.error('File download error:', error);
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, 'File download failed');
+ }
+ };
+}
diff --git a/packages/plugins/server/src/index.ts b/packages/plugins/server/src/index.ts
new file mode 100644
index 00000000..5649a368
--- /dev/null
+++ b/packages/plugins/server/src/index.ts
@@ -0,0 +1,25 @@
+/**
+ * ObjectQL
+ * 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.
+ */
+
+// Re-export plugin
+export * from './plugin';
+
+// Re-export all core server functionality
+export * from './types';
+export * from './utils';
+export * from './openapi';
+export * from './server';
+export * from './metadata';
+export * from './storage';
+export * from './file-handler';
+
+// Re-export adapters
+export * from './adapters/node';
+export * from './adapters/rest';
+export * from './adapters/graphql';
+export * from './adapters/hono';
diff --git a/packages/plugins/server/src/metadata.ts b/packages/plugins/server/src/metadata.ts
new file mode 100644
index 00000000..ae655f6e
--- /dev/null
+++ b/packages/plugins/server/src/metadata.ts
@@ -0,0 +1,244 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
+import { IncomingMessage, ServerResponse } from 'http';
+import { ErrorCode } from './types';
+import { escapeRegexPath } from './utils';
+
+function readBody(req: IncomingMessage): Promise {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.on('data', chunk => body += chunk.toString());
+ req.on('end', () => {
+ if (!body) return resolve({});
+ try {
+ resolve(JSON.parse(body));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ req.on('error', reject);
+ });
+}
+
+/**
+ * Options for createMetadataHandler
+ */
+export interface MetadataHandlerOptions {
+ /** Custom API route configuration */
+ routes?: ApiRouteConfig;
+}
+
+/**
+ * Creates a handler for metadata endpoints.
+ * These endpoints expose information about registered objects and other metadata.
+ *
+ * @param app - ObjectQL application instance
+ * @param options - Optional configuration including custom routes
+ */
+export function createMetadataHandler(app: IObjectQL, options?: MetadataHandlerOptions) {
+ const routes = resolveApiRoutes(options?.routes);
+ const metadataPath = routes.metadata;
+ return async (req: IncomingMessage, res: ServerResponse) => {
+ // Parse the URL
+ const url = req.url || '';
+ const method = req.method;
+
+ // CORS headers for development
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+
+ if (req.method === 'OPTIONS') {
+ res.statusCode = 200;
+ res.end();
+ return;
+ }
+
+ try {
+ // Helper to send JSON
+ const sendJson = (data: any, status = 200) => {
+ res.setHeader('Content-Type', 'application/json');
+ res.statusCode = status;
+ res.end(JSON.stringify(data));
+ };
+
+ const sendError = (code: ErrorCode, message: string, status = 400) => {
+ sendJson({ error: { code, message } }, status);
+ };
+
+ // ---------------------------------------------------------
+ // 1. List Entries (GET {metadataPath}/:type)
+ // ---------------------------------------------------------
+
+ // Generic List: {metadataPath}/:type
+ // Also handles legacy {metadataPath} (defaults to objects)
+ const escapedPath = escapeRegexPath(metadataPath);
+ const listMatch = url.match(new RegExp(`^${escapedPath}/([^/]+)$`));
+ const isRootMetadata = url === metadataPath;
+
+ if (method === 'GET' && (listMatch || isRootMetadata)) {
+ let type = isRootMetadata ? 'object' : listMatch![1];
+ if (type === 'objects') type = 'object'; // Alias behavior
+
+ if (type === 'object') {
+ const configs = app.getConfigs();
+ const objects = Object.values(configs).map(obj => ({
+ name: obj.name,
+ label: obj.label || obj.name,
+ icon: obj.icon,
+ description: obj.description,
+ fields: obj.fields || {}
+ }));
+ // Return standardized format with items
+ return sendJson({ items: objects });
+ }
+
+ const entries = app.metadata.list(type);
+ // Return standardized list format
+ return sendJson({
+ items: entries
+ });
+ }
+
+ // ---------------------------------------------------------
+ // 2. Get Single Entry (GET {metadataPath}/:type/:id)
+ // ---------------------------------------------------------
+
+ const detailMatch = url.match(new RegExp(`^${escapedPath}/([^/]+)/([^/\\?]+)$`));
+
+ if (method === 'GET' && detailMatch) {
+ let [, type, id] = detailMatch;
+ if (type === 'objects') type = 'object';
+
+ // Handle Object Special Logic (Field Formatting)
+ if (type === 'object') {
+ const metadata = app.getObject(id);
+ if (!metadata) {
+ return sendError(ErrorCode.NOT_FOUND, `Object '${id}' not found`, 404);
+ }
+
+ // Convert fields to map with name populated
+ const fields: Record = {};
+ if (metadata.fields) {
+ Object.entries(metadata.fields).forEach(([key, field]) => {
+ fields[key] = {
+ ...field,
+ name: field.name || key
+ };
+ });
+ }
+
+ return sendJson({
+ ...metadata,
+ fields
+ });
+ } else {
+ // Generic Metadata (View, Form, etc.)
+ const content = app.metadata.get(type, id);
+ if (!content) {
+ return sendError(ErrorCode.NOT_FOUND, `${type} '${id}' not found`, 404);
+ }
+ return sendJson(content);
+ }
+ }
+
+ // ---------------------------------------------------------
+ // 3. Update Entry (POST/PUT {metadataPath}/:type/:id)
+ // ---------------------------------------------------------
+ if ((method === 'POST' || method === 'PUT') && detailMatch) {
+ let [, type, id] = detailMatch;
+ if (type === 'objects') type = 'object';
+
+ const body = await readBody(req);
+ try {
+ // await app.updateMetadata(type, id, body);
+ // return sendJson({ success: true });
+ return sendError(ErrorCode.INTERNAL_ERROR, 'Metadata updates via API are temporarily disabled in this architectural version.', 501);
+ } catch (e: any) {
+ const isUserError = e.message.startsWith('Cannot update') || e.message.includes('not found');
+ return sendError(
+ isUserError ? ErrorCode.INVALID_REQUEST : ErrorCode.INTERNAL_ERROR,
+ e.message,
+ isUserError ? 400 : 500
+ );
+ }
+ }
+
+ // ---------------------------------------------------------
+ // 4. Object Sub-resources (Fields, Actions)
+ // ---------------------------------------------------------
+
+ // GET {metadataPath}/object/:name/fields/:field
+ // Legacy path support.
+ const fieldMatch = url.match(new RegExp(`^${escapedPath}/(?:objects|object)/([^/]+)/fields/([^/\\?]+)$`));
+ if (method === 'GET' && fieldMatch) {
+ const [, objectName, fieldName] = fieldMatch;
+ const metadata = app.getObject(objectName);
+
+ if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
+
+ const field = metadata.fields?.[fieldName];
+ if (!field) return sendError(ErrorCode.NOT_FOUND, `Field '${fieldName}' not found`, 404);
+
+ return sendJson({
+ name: field.name || fieldName,
+ type: field.type,
+ label: field.label,
+ required: field.required,
+ unique: field.unique,
+ defaultValue: field.defaultValue,
+ options: field.options,
+ min: field.min,
+ max: field.max,
+ minLength: field.minLength,
+ maxLength: field.maxLength,
+ validation: field.validation
+ });
+ }
+
+ // GET {metadataPath}/object/:name/actions
+ const actionsMatch = url.match(new RegExp(`^${escapedPath}/(?:objects|object)/([^/]+)/actions$`));
+ if (method === 'GET' && actionsMatch) {
+ const [, objectName] = actionsMatch;
+ const metadata = app.getObject(objectName);
+
+ if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
+
+ const actions = metadata.actions || {};
+ const formattedActions = Object.entries(actions).map(([key, action]) => {
+ const actionConfig = action as any;
+ const hasFields = !!actionConfig.fields && Object.keys(actionConfig.fields).length > 0;
+ return {
+ name: key,
+ type: actionConfig.type || (hasFields ? 'record' : 'global'),
+ label: actionConfig.label || key,
+ params: actionConfig.params || {},
+ description: actionConfig.description
+ };
+ });
+
+ return sendJson({ items: formattedActions });
+ }
+
+ // Not found
+ sendError(ErrorCode.NOT_FOUND, 'Not Found', 404);
+
+ } catch (e: any) {
+ console.error('[Metadata Handler] Error:', e);
+ res.statusCode = 500;
+ res.end(JSON.stringify({
+ error: {
+ code: ErrorCode.INTERNAL_ERROR,
+ message: 'Internal Server Error'
+ }
+ }));
+ }
+ };
+}
diff --git a/packages/plugins/server/src/openapi.ts b/packages/plugins/server/src/openapi.ts
new file mode 100644
index 00000000..e122295d
--- /dev/null
+++ b/packages/plugins/server/src/openapi.ts
@@ -0,0 +1,207 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ObjectConfig, FieldConfig, ApiRouteConfig, resolveApiRoutes } from '@objectql/types';
+
+interface OpenAPISchema {
+ openapi: string;
+ info: {
+ title: string;
+ version: string;
+ };
+ paths: Record;
+ components: {
+ schemas: Record;
+ };
+}
+
+export function generateOpenAPI(app: IObjectQL, routeConfig?: ApiRouteConfig): OpenAPISchema {
+ const registry = (app as any).metadata; // Direct access or via interface
+ const objects = registry.list('object') as ObjectConfig[];
+ const routes = resolveApiRoutes(routeConfig);
+
+ const paths: Record = {};
+ const schemas: Record = {};
+
+
+ // 1. JSON-RPC Endpoint
+ paths[routes.rpc] = {
+ post: {
+ summary: 'JSON-RPC Entry Point',
+ description: 'Execute any ObjectQL operation via a JSON body.',
+ tags: ['System'],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ op: { type: 'string', enum: ['find', 'findOne', 'create', 'update', 'delete', 'count', 'action'] },
+ object: { type: 'string' },
+ args: { type: 'object' }
+ },
+ required: ['op', 'object']
+ }
+ }
+ }
+ },
+ responses: {
+ 200: {
+ description: 'Operation Result',
+ content: {
+ 'application/json': {
+ schema: { type: 'object' } // Dynamic result
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // 2. Generate Schemas
+ for (const obj of objects) {
+ const schemaName = obj.name;
+ const properties: Record = {};
+
+ for (const [fieldName, field] of Object.entries(obj.fields)) {
+ properties[fieldName] = mapFieldTypeToOpenAPI(field);
+ }
+
+ schemas[schemaName] = {
+ type: 'object',
+ properties
+ };
+ }
+
+ // 3. REST API Paths
+ for (const obj of objects) {
+ const name = obj.name;
+ const basePath = `${routes.data}/${name}`; // Standard REST Path
+
+ // GET {dataPath}/:name (List)
+ paths[basePath] = {
+ get: {
+ summary: `List ${name}`,
+ tags: [name],
+ parameters: [
+ { name: 'filter', in: 'query', schema: { type: 'string' }, description: 'JSON filter args' },
+ { name: 'fields', in: 'query', schema: { type: 'string' }, description: 'Comma-separated fields to return' },
+ { name: 'top', in: 'query', schema: { type: 'integer' }, description: 'Limit' },
+ { name: 'skip', in: 'query', schema: { type: 'integer' }, description: 'Offset' }
+ ],
+ responses: {
+ 200: {
+ description: `List of ${name}`,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ data: { type: 'array', items: { $ref: `#/components/schemas/${name}` } }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ post: {
+ summary: `Create ${name}`,
+ tags: [name],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ data: { $ref: `#/components/schemas/${name}` }
+ }
+ }
+ }
+ }
+ },
+ responses: {
+ 200: { description: 'Created' }
+ }
+ }
+ };
+
+ // /api/data/:name/:id
+ paths[`${basePath}/{id}`] = {
+ get: {
+ summary: `Get ${name}`,
+ tags: [name],
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
+ responses: {
+ 200: {
+ description: 'Item',
+ content: {
+ 'application/json': {
+ schema: { $ref: `#/components/schemas/${name}` }
+ }
+ }
+ }
+ }
+ },
+ patch: {
+ summary: `Update ${name}`,
+ tags: [name],
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object' }
+ }
+ }
+ }
+ }
+ },
+ responses: {
+ 200: { description: 'Updated' }
+ }
+ },
+ delete: {
+ summary: `Delete ${name}`,
+ tags: [name],
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
+ responses: {
+ 200: { description: 'Deleted' }
+ }
+ }
+ };
+ }
+
+ return {
+ openapi: '3.0.0',
+ info: {
+ title: 'ObjectQL API',
+ version: '1.0.0'
+ },
+ paths,
+ components: {
+ schemas
+ }
+ };
+}
+
+function mapFieldTypeToOpenAPI(field: FieldConfig | string): any {
+ const type = typeof field === 'string' ? field : field.type;
+
+ switch (type) {
+ case 'string': return { type: 'string' };
+ case 'integer': return { type: 'integer' };
+ case 'float': return { type: 'number' };
+ case 'boolean': return { type: 'boolean' };
+ case 'date': return { type: 'string', format: 'date-time' };
+ case 'json': return { type: 'object' };
+ default: return { type: 'string' }; // Fallback or relationship ID
+ }
+}
diff --git a/packages/plugins/server/src/plugin.ts b/packages/plugins/server/src/plugin.ts
new file mode 100644
index 00000000..a2550eb3
--- /dev/null
+++ b/packages/plugins/server/src/plugin.ts
@@ -0,0 +1,239 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ObjectQLPlugin, ApiRouteConfig } from '@objectql/types';
+import { IncomingMessage, ServerResponse, createServer, Server } from 'http';
+import { createNodeHandler, NodeHandlerOptions } from './adapters/node';
+import { createRESTHandler, RESTHandlerOptions } from './adapters/rest';
+import { createGraphQLHandler } from './adapters/graphql';
+import { createMetadataHandler } from './metadata';
+
+export interface ServerPluginOptions {
+ /**
+ * Port number to listen on
+ * @default 3000
+ */
+ port?: number;
+
+ /**
+ * Host address to bind to
+ * @default 'localhost'
+ */
+ host?: string;
+
+ /**
+ * Custom API route configuration
+ */
+ routes?: ApiRouteConfig;
+
+ /**
+ * File storage configuration
+ */
+ fileStorage?: NodeHandlerOptions['fileStorage'];
+
+ /**
+ * Enable GraphQL endpoint
+ * @default false
+ */
+ enableGraphQL?: boolean;
+
+ /**
+ * Enable REST endpoint
+ * @default true
+ */
+ enableREST?: boolean;
+
+ /**
+ * Enable metadata endpoint
+ * @default true
+ */
+ enableMetadata?: boolean;
+
+ /**
+ * Enable JSON-RPC endpoint
+ * @default true
+ */
+ enableRPC?: boolean;
+
+ /**
+ * Automatically start server on setup
+ * @default false
+ */
+ autoStart?: boolean;
+
+ /**
+ * Custom request handler middleware
+ */
+ middleware?: ((req: IncomingMessage, res: ServerResponse, next: () => void) => void)[];
+}
+
+/**
+ * Server Plugin for ObjectQL
+ * Provides HTTP server capabilities with support for JSON-RPC, REST, GraphQL and Metadata APIs
+ */
+export class ServerPlugin implements ObjectQLPlugin {
+ name = 'objectql-server';
+ private server?: Server;
+ private options: ServerPluginOptions;
+
+ constructor(options: ServerPluginOptions = {}) {
+ this.options = {
+ port: options.port || parseInt(process.env.PORT || '3000'),
+ host: options.host || process.env.HOST || 'localhost',
+ routes: options.routes || {},
+ fileStorage: options.fileStorage,
+ enableGraphQL: options.enableGraphQL ?? false,
+ enableREST: options.enableREST ?? true,
+ enableMetadata: options.enableMetadata ?? true,
+ enableRPC: options.enableRPC ?? true,
+ autoStart: options.autoStart ?? false,
+ middleware: options.middleware || []
+ };
+ }
+
+ async setup(app: IObjectQL): Promise {
+ console.log('[ServerPlugin] Setting up HTTP server...');
+
+ // Create handlers based on enabled features
+ const nodeHandler = this.options.enableRPC
+ ? createNodeHandler(app, {
+ routes: this.options.routes,
+ fileStorage: this.options.fileStorage
+ })
+ : undefined;
+
+ const restHandler = this.options.enableREST
+ ? createRESTHandler(app, { routes: this.options.routes })
+ : undefined;
+
+ const graphqlHandler = this.options.enableGraphQL
+ ? createGraphQLHandler(app)
+ : undefined;
+
+ const metadataHandler = this.options.enableMetadata
+ ? createMetadataHandler(app)
+ : undefined;
+
+ // Create HTTP server
+ this.server = createServer((req, res) => {
+ // Apply middleware
+ let middlewareIndex = 0;
+ const middleware = this.options.middleware || [];
+ const next = () => {
+ if (middlewareIndex < middleware.length) {
+ const fn = middleware[middlewareIndex++];
+ fn(req, res, next);
+ } else {
+ // Route to appropriate handler
+ this.routeRequest(req, res, {
+ nodeHandler,
+ restHandler,
+ graphqlHandler,
+ metadataHandler
+ });
+ }
+ };
+ next();
+ });
+
+ // Auto-start if configured
+ if (this.options.autoStart) {
+ await this.start();
+ }
+
+ console.log('[ServerPlugin] Server setup complete');
+ }
+
+ /**
+ * Route incoming requests to the appropriate handler
+ */
+ private routeRequest(
+ req: IncomingMessage,
+ res: ServerResponse,
+ handlers: {
+ nodeHandler?: (req: IncomingMessage, res: ServerResponse) => Promise;
+ restHandler?: (req: IncomingMessage, res: ServerResponse) => Promise;
+ graphqlHandler?: (req: IncomingMessage, res: ServerResponse) => Promise;
+ metadataHandler?: (req: IncomingMessage, res: ServerResponse) => Promise;
+ }
+ ) {
+ const url = req.url || '/';
+ const resolvedRoutes = this.options.routes || {};
+
+ // Determine which handler to use based on URL path
+ // Note: GraphQL not in default routes, would need custom configuration
+ if (handlers.restHandler && url.startsWith(resolvedRoutes.data || '/api/data')) {
+ handlers.restHandler(req, res);
+ } else if (handlers.metadataHandler && url.startsWith(resolvedRoutes.metadata || '/api/metadata')) {
+ handlers.metadataHandler(req, res);
+ } else if (handlers.nodeHandler) {
+ handlers.nodeHandler(req, res);
+ } else {
+ res.statusCode = 404;
+ res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'Endpoint not found' } }));
+ }
+ }
+
+ /**
+ * Start the HTTP server
+ */
+ async start(): Promise {
+ if (!this.server) {
+ throw new Error('Server not initialized. Call setup() first.');
+ }
+
+ return new Promise((resolve, reject) => {
+ this.server!.listen(this.options.port, this.options.host, () => {
+ const routes = this.options.routes || {};
+ console.log(`\n🚀 ObjectQL Server running on http://${this.options.host}:${this.options.port}`);
+ console.log(`\n🔌 APIs:`);
+ if (this.options.enableRPC) {
+ console.log(` - JSON-RPC: http://${this.options.host}:${this.options.port}${routes.rpc || '/api/objectql'}`);
+ }
+ if (this.options.enableREST) {
+ console.log(` - REST: http://${this.options.host}:${this.options.port}${routes.data || '/api/data'}`);
+ }
+ if (this.options.enableGraphQL) {
+ console.log(` - GraphQL: http://${this.options.host}:${this.options.port}/api/graphql`);
+ }
+ if (this.options.enableMetadata) {
+ console.log(` - Metadata: http://${this.options.host}:${this.options.port}${routes.metadata || '/api/metadata'}`);
+ }
+ resolve();
+ });
+
+ this.server!.on('error', reject);
+ });
+ }
+
+ /**
+ * Stop the HTTP server
+ */
+ async stop(): Promise {
+ if (!this.server) {
+ return;
+ }
+
+ return new Promise((resolve, reject) => {
+ this.server!.close((err) => {
+ if (err) reject(err);
+ else {
+ console.log('[ServerPlugin] Server stopped');
+ resolve();
+ }
+ });
+ });
+ }
+
+ /**
+ * Get the underlying Node.js HTTP server instance
+ */
+ getServer(): Server | undefined {
+ return this.server;
+ }
+}
diff --git a/packages/plugins/server/src/server.ts b/packages/plugins/server/src/server.ts
new file mode 100644
index 00000000..732672df
--- /dev/null
+++ b/packages/plugins/server/src/server.ts
@@ -0,0 +1,274 @@
+/**
+ * ObjectQL
+ * 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 { IObjectQL, ObjectQLContext } from '@objectql/types';
+import { ObjectQLRequest, ObjectQLResponse, ErrorCode } from './types';
+
+export class ObjectQLServer {
+ constructor(private app: IObjectQL) {}
+
+ /**
+ * The core handler that processes a JSON request object and returns a result.
+ * This is framework-agnostic.
+ */
+ async handle(req: ObjectQLRequest): Promise {
+ try {
+ // Log AI context if provided
+ if (req.ai_context) {
+ console.log('[ObjectQL AI Context]', {
+ object: req.object,
+ op: req.op,
+ intent: req.ai_context.intent,
+ natural_language: req.ai_context.natural_language,
+ use_case: req.ai_context.use_case
+ });
+ }
+
+ // 1. Build Context
+ // TODO: integrate with real session/auth
+ const contextOptions = {
+ userId: req.user?.id,
+ roles: req.user?.roles || [],
+ // TODO: spaceId
+ };
+
+ // Note: Currently IObjectQL.createContext implies we have access to it.
+ // But IObjectQL interface in @objectql/types usually doesn't expose createContext (it's on the class).
+ // We need to cast or fix the interface. Assuming 'app' behaves like ObjectQL class.
+ const app = this.app as any;
+ if (typeof app.createContext !== 'function') {
+ return this.errorResponse(
+ ErrorCode.INTERNAL_ERROR,
+ "The provided ObjectQL instance does not support createContext."
+ );
+ }
+
+ const ctx: ObjectQLContext = app.createContext(contextOptions);
+
+ // Validate object exists
+ const objectConfig = app.getObject(req.object);
+ if (!objectConfig) {
+ return this.errorResponse(
+ ErrorCode.NOT_FOUND,
+ `Object '${req.object}' not found`
+ );
+ }
+
+ const repo = ctx.object(req.object);
+
+ let result: any;
+
+ switch (req.op) {
+ case 'find':
+ result = await repo.find(req.args);
+ // For find operations, return items array with pagination metadata
+ return this.buildListResponse(result, req.args, repo);
+ case 'findOne':
+ // Support both string ID and query object
+ result = await repo.findOne(req.args);
+ if (result) {
+ return { ...result, '@type': req.object };
+ }
+ return result;
+ case 'create':
+ result = await repo.create(req.args);
+ if (result) {
+ return { ...result, '@type': req.object };
+ }
+ return result;
+ case 'update':
+ result = await repo.update(req.args.id, req.args.data);
+ if (result) {
+ return { ...result, '@type': req.object };
+ }
+ return result;
+ case 'delete':
+ result = await repo.delete(req.args.id);
+ if (!result) {
+ return this.errorResponse(
+ ErrorCode.NOT_FOUND,
+ `Record with id '${req.args.id}' not found for delete`
+ );
+ }
+ // Return standardized delete response with object type
+ return {
+ id: req.args.id,
+ deleted: true,
+ '@type': req.object
+ };
+ case 'count':
+ result = await repo.count(req.args);
+ return { count: result, '@type': req.object };
+ case 'action':
+ // Map generic args to ActionContext
+ result = await app.executeAction(req.object, req.args.action, {
+ ...ctx, // Pass context (user, etc.)
+ id: req.args.id,
+ input: req.args.input || req.args.params // Support both for convenience
+ });
+ if (result && typeof result === 'object') {
+ return { ...result, '@type': req.object };
+ }
+ return result;
+ case 'createMany':
+ // Bulk create operation
+ if (!Array.isArray(req.args)) {
+ return this.errorResponse(
+ ErrorCode.INVALID_REQUEST,
+ 'createMany expects args to be an array of records'
+ );
+ }
+ result = await repo.createMany(req.args);
+ return {
+ items: result,
+ count: Array.isArray(result) ? result.length : 0,
+ '@type': req.object
+ };
+ case 'updateMany':
+ // Bulk update operation
+ // args should be { filters, data }
+ if (!req.args || typeof req.args !== 'object' || !req.args.data) {
+ return this.errorResponse(
+ ErrorCode.INVALID_REQUEST,
+ 'updateMany expects args to be an object with { filters, data }'
+ );
+ }
+ result = await repo.updateMany(req.args.filters || {}, req.args.data);
+ return {
+ count: result,
+ '@type': req.object
+ };
+ case 'deleteMany':
+ // Bulk delete operation
+ // args should be { filters }
+ if (!req.args || typeof req.args !== 'object') {
+ return this.errorResponse(
+ ErrorCode.INVALID_REQUEST,
+ 'deleteMany expects args to be an object with { filters }'
+ );
+ }
+ result = await repo.deleteMany(req.args.filters || {});
+ return {
+ count: result,
+ '@type': req.object
+ };
+ default:
+ return this.errorResponse(
+ ErrorCode.INVALID_REQUEST,
+ `Unknown operation: ${req.op}`
+ );
+ }
+
+ } catch (e: any) {
+ return this.handleError(e);
+ }
+ }
+
+ /**
+ * Build a standardized list response with pagination metadata
+ */
+ private async buildListResponse(items: any[], args: any, repo: any): Promise {
+ const response: ObjectQLResponse = {
+ items
+ };
+
+ // Calculate pagination metadata if limit/skip are present
+ if (args && (args.limit || args.skip)) {
+ const skip = args.skip || 0;
+ const limit = args.limit || items.length;
+
+ // Get total count - use the same arguments as the query to ensure consistency
+ const total = await repo.count(args || {});
+
+ const size = limit;
+ const page = limit > 0 ? Math.floor(skip / limit) + 1 : 1;
+ const pages = limit > 0 ? Math.ceil(total / limit) : 1;
+ const has_next = skip + items.length < total;
+
+ response.meta = {
+ total,
+ page,
+ size,
+ pages,
+ has_next
+ };
+ }
+
+ return response;
+ }
+
+ /**
+ * Handle errors and convert them to appropriate error responses
+ */
+ private handleError(error: any): ObjectQLResponse {
+ console.error('[ObjectQL Server] Error:', error);
+
+ // Handle validation errors
+ if (error.name === 'ValidationError' || error.code === 'VALIDATION_ERROR') {
+ return this.errorResponse(
+ ErrorCode.VALIDATION_ERROR,
+ 'Validation failed',
+ { fields: error.fields || error.details }
+ );
+ }
+
+ // Handle permission errors
+ if (error.name === 'PermissionError' || error.code === 'FORBIDDEN') {
+ return this.errorResponse(
+ ErrorCode.FORBIDDEN,
+ error.message || 'You do not have permission to access this resource',
+ error.details
+ );
+ }
+
+ // Handle not found errors
+ if (error.name === 'NotFoundError' || error.code === 'NOT_FOUND') {
+ return this.errorResponse(
+ ErrorCode.NOT_FOUND,
+ error.message || 'Resource not found'
+ );
+ }
+
+ // Handle conflict errors (e.g., unique constraint violations)
+ if (error.name === 'ConflictError' || error.code === 'CONFLICT') {
+ return this.errorResponse(
+ ErrorCode.CONFLICT,
+ error.message || 'Resource conflict',
+ error.details
+ );
+ }
+
+ // Handle database errors
+ if (error.name === 'DatabaseError' || error.code?.startsWith('DB_')) {
+ return this.errorResponse(
+ ErrorCode.DATABASE_ERROR,
+ 'Database operation failed',
+ { originalError: error.message }
+ );
+ }
+
+ // Default to internal error
+ return this.errorResponse(
+ ErrorCode.INTERNAL_ERROR,
+ error.message || 'An error occurred'
+ );
+ }
+
+ /**
+ * Create a standardized error response
+ */
+ private errorResponse(code: ErrorCode, message: string, details?: any): ObjectQLResponse {
+ return {
+ error: {
+ code,
+ message,
+ details
+ }
+ };
+ }
+}
diff --git a/packages/plugins/server/src/storage.ts b/packages/plugins/server/src/storage.ts
new file mode 100644
index 00000000..5f5d2bef
--- /dev/null
+++ b/packages/plugins/server/src/storage.ts
@@ -0,0 +1,179 @@
+/**
+ * ObjectQL
+ * 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 { IFileStorage, AttachmentData, FileStorageOptions } from './types';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as crypto from 'crypto';
+
+/**
+ * Local filesystem storage implementation for file attachments
+ */
+export class LocalFileStorage implements IFileStorage {
+ private baseDir: string;
+ private baseUrl: string;
+
+ constructor(options: { baseDir: string; baseUrl: string }) {
+ this.baseDir = options.baseDir;
+ this.baseUrl = options.baseUrl;
+
+ // Ensure base directory exists
+ if (!fs.existsSync(this.baseDir)) {
+ fs.mkdirSync(this.baseDir, { recursive: true });
+ }
+ }
+
+ async save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise {
+ // Generate unique ID for the file
+ const id = crypto.randomBytes(16).toString('hex');
+ const ext = path.extname(filename);
+ const basename = path.basename(filename, ext);
+ const storedFilename = `${id}${ext}`;
+
+ // Determine storage path
+ let folder = options?.folder || 'uploads';
+ if (options?.object) {
+ folder = path.join(folder, options.object);
+ }
+
+ const folderPath = path.join(this.baseDir, folder);
+ if (!fs.existsSync(folderPath)) {
+ fs.mkdirSync(folderPath, { recursive: true });
+ }
+
+ const filePath = path.join(folderPath, storedFilename);
+
+ // Write file to disk (async for better performance)
+ await fs.promises.writeFile(filePath, file);
+
+ // Generate public URL
+ const url = this.getPublicUrl(path.join(folder, storedFilename));
+
+ const attachmentData: AttachmentData = {
+ id,
+ name: storedFilename,
+ url,
+ size: file.length,
+ type: mimeType,
+ original_name: filename,
+ uploaded_at: new Date().toISOString(),
+ uploaded_by: options?.userId
+ };
+
+ return attachmentData;
+ }
+
+ async get(fileId: string): Promise {
+ try {
+ // Search for file in the upload directory
+ const found = this.findFile(this.baseDir, fileId);
+ if (!found) {
+ return null;
+ }
+ // Use async read for better performance
+ return await fs.promises.readFile(found);
+ } catch (error) {
+ console.error('Error reading file:', error);
+ return null;
+ }
+ }
+
+ async delete(fileId: string): Promise {
+ try {
+ const found = this.findFile(this.baseDir, fileId);
+ if (!found) {
+ return false;
+ }
+ // Use async unlink for better performance
+ await fs.promises.unlink(found);
+ return true;
+ } catch (error) {
+ console.error('Error deleting file:', error);
+ return false;
+ }
+ }
+
+ getPublicUrl(filePath: string): string {
+ // Normalize path separators for URLs
+ const normalizedPath = filePath.replace(/\\/g, '/');
+ return `${this.baseUrl}/${normalizedPath}`;
+ }
+
+ /**
+ * Recursively search for a file by ID
+ */
+ private findFile(dir: string, fileId: string): string | null {
+ const files = fs.readdirSync(dir);
+
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ const stat = fs.statSync(filePath);
+
+ if (stat.isDirectory()) {
+ const found = this.findFile(filePath, fileId);
+ if (found) {
+ return found;
+ }
+ } else if (file.startsWith(fileId)) {
+ return filePath;
+ }
+ }
+
+ return null;
+ }
+}
+
+/**
+ * Memory storage implementation for testing
+ */
+export class MemoryFileStorage implements IFileStorage {
+ private files = new Map();
+ private baseUrl: string;
+
+ constructor(options: { baseUrl: string }) {
+ this.baseUrl = options.baseUrl;
+ }
+
+ async save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise {
+ const id = crypto.randomBytes(16).toString('hex');
+ const ext = path.extname(filename);
+ const storedFilename = `${id}${ext}`;
+
+ const attachmentData: AttachmentData = {
+ id,
+ name: storedFilename,
+ url: this.getPublicUrl(storedFilename),
+ size: file.length,
+ type: mimeType,
+ original_name: filename,
+ uploaded_at: new Date().toISOString(),
+ uploaded_by: options?.userId
+ };
+
+ this.files.set(id, { buffer: file, metadata: attachmentData });
+
+ return attachmentData;
+ }
+
+ async get(fileId: string): Promise {
+ const entry = this.files.get(fileId);
+ return entry ? entry.buffer : null;
+ }
+
+ async delete(fileId: string): Promise {
+ return this.files.delete(fileId);
+ }
+
+ getPublicUrl(filePath: string): string {
+ return `${this.baseUrl}/${filePath}`;
+ }
+
+ clear(): void {
+ this.files.clear();
+ }
+}
diff --git a/packages/plugins/server/src/templates.ts b/packages/plugins/server/src/templates.ts
new file mode 100644
index 00000000..647d8b99
--- /dev/null
+++ b/packages/plugins/server/src/templates.ts
@@ -0,0 +1,57 @@
+/**
+ * ObjectQL
+ * 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.
+ */
+
+export function getWelcomePageHtml(routes: { rpc: string; data: string; }) {
+ return `
+
+
+ ObjectQL Server
+
+
+
+
+ Running
+
+ ObjectQL Server
+ The server is operational and ready to accept requests.
+
+
+
API Endpoints
+
+ - JSON-RPC:
POST ${routes.rpc}
+ - REST API:
GET ${routes.data}/:object
+ - OpenAPI Spec: /openapi.json
+
+
+
+
+
+
+ Powered by ObjectQL
+
+
+`;
+}
diff --git a/packages/plugins/server/src/types.ts b/packages/plugins/server/src/types.ts
new file mode 100644
index 00000000..49be18b3
--- /dev/null
+++ b/packages/plugins/server/src/types.ts
@@ -0,0 +1,181 @@
+/**
+ * ObjectQL
+ * 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.
+ */
+
+// src/types.ts
+
+/**
+ * Standardized error codes for ObjectQL API
+ */
+export enum ErrorCode {
+ INVALID_REQUEST = 'INVALID_REQUEST',
+ VALIDATION_ERROR = 'VALIDATION_ERROR',
+ UNAUTHORIZED = 'UNAUTHORIZED',
+ FORBIDDEN = 'FORBIDDEN',
+ NOT_FOUND = 'NOT_FOUND',
+ CONFLICT = 'CONFLICT',
+ INTERNAL_ERROR = 'INTERNAL_ERROR',
+ DATABASE_ERROR = 'DATABASE_ERROR',
+ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED'
+}
+
+/**
+ * AI context for better logging, debugging, and AI processing
+ */
+export interface AIContext {
+ intent?: string;
+ natural_language?: string;
+ use_case?: string;
+ [key: string]: unknown;
+}
+
+/**
+ * ObjectQL JSON-RPC style request
+ */
+export interface ObjectQLRequest {
+ // Identity provided by the framework adapter (e.g. from session)
+ user?: {
+ id: string;
+ roles: string[];
+ [key: string]: any;
+ };
+
+ // The actual operation
+ op: 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'count' | 'action' | 'createMany' | 'updateMany' | 'deleteMany';
+ object: string;
+
+ // Arguments
+ args: any;
+
+ // Optional AI context for explainability
+ ai_context?: AIContext;
+}
+
+/**
+ * Error details structure
+ */
+export interface ErrorDetails {
+ field?: string;
+ reason?: string;
+ fields?: Record;
+ required_permission?: string;
+ user_roles?: string[];
+ retry_after?: number;
+ [key: string]: unknown;
+}
+
+/**
+ * Pagination metadata
+ */
+export interface PaginationMeta {
+ total: number; // Total number of records
+ page?: number; // Current page number (1-indexed, e.g. page 1 corresponds to skip=0)
+ size?: number; // Number of items per page
+ pages?: number; // Total number of pages
+ has_next?: boolean; // Whether there is a next page
+}
+
+/**
+ * ObjectQL API response
+ */
+export interface ObjectQLResponse {
+ // For list operations (find)
+ items?: any[];
+
+ // Pagination metadata (for list operations)
+ meta?: PaginationMeta;
+
+ // Error information
+ error?: {
+ code: ErrorCode | string;
+ message: string;
+ details?: ErrorDetails | any; // Allow flexible details structure
+ };
+
+ // For single item operations, the response is the object itself with '@type' field
+ // This allows any additional fields from the actual data object
+ [key: string]: any;
+}
+
+/**
+ * Attachment/File metadata structure
+ */
+export interface AttachmentData {
+ /** Unique identifier for this file */
+ id: string;
+ /** File name (e.g., "invoice.pdf") */
+ name: string;
+ /** Publicly accessible URL to the file */
+ url: string;
+ /** File size in bytes */
+ size: number;
+ /** MIME type (e.g., "application/pdf", "image/jpeg") */
+ type: string;
+ /** Original filename as uploaded by user */
+ original_name?: string;
+ /** Upload timestamp (ISO 8601) */
+ uploaded_at?: string;
+ /** User ID who uploaded the file */
+ uploaded_by?: string;
+}
+
+/**
+ * Image-specific attachment data with metadata
+ */
+export interface ImageAttachmentData extends AttachmentData {
+ /** Image width in pixels */
+ width?: number;
+ /** Image height in pixels */
+ height?: number;
+ /** Thumbnail URL (if generated) */
+ thumbnail_url?: string;
+ /** Alternative sizes/versions */
+ variants?: {
+ small?: string;
+ medium?: string;
+ large?: string;
+ };
+}
+
+/**
+ * File storage provider interface
+ */
+export interface IFileStorage {
+ /**
+ * Save a file and return its metadata
+ */
+ save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise;
+
+ /**
+ * Retrieve a file by its ID or path
+ */
+ get(fileId: string): Promise;
+
+ /**
+ * Delete a file
+ */
+ delete(fileId: string): Promise;
+
+ /**
+ * Generate a public URL for a file
+ */
+ getPublicUrl(fileId: string): string;
+}
+
+/**
+ * Options for file storage operations
+ */
+export interface FileStorageOptions {
+ /** Logical folder/path for organization */
+ folder?: string;
+ /** Object name (for context/validation) */
+ object?: string;
+ /** Field name (for validation against field config) */
+ field?: string;
+ /** User ID who uploaded the file */
+ userId?: string;
+}
diff --git a/packages/plugins/server/src/utils.ts b/packages/plugins/server/src/utils.ts
new file mode 100644
index 00000000..88163a35
--- /dev/null
+++ b/packages/plugins/server/src/utils.ts
@@ -0,0 +1,29 @@
+/**
+ * ObjectQL
+ * 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.
+ */
+
+/**
+ * Utility functions for server operations
+ */
+
+/**
+ * Escapes special regex characters in a path string for use in RegExp
+ * @param path - The path string to escape
+ * @returns Escaped path string safe for use in RegExp
+ */
+export function escapeRegexPath(path: string): string {
+ return path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/**
+ * Normalizes a path to ensure it starts with a forward slash
+ * @param path - The path string to normalize
+ * @returns Normalized path string starting with '/'
+ */
+export function normalizePath(path: string): string {
+ return path.startsWith('/') ? path : `/${path}`;
+}
diff --git a/packages/plugins/server/tsconfig.json b/packages/plugins/server/tsconfig.json
new file mode 100644
index 00000000..f6004589
--- /dev/null
+++ b/packages/plugins/server/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/packages/runtime/server/src/index.ts b/packages/runtime/server/src/index.ts
index b91676ba..c26c5b12 100644
--- a/packages/runtime/server/src/index.ts
+++ b/packages/runtime/server/src/index.ts
@@ -6,6 +6,19 @@
* LICENSE file in the root directory of this source tree.
*/
+/**
+ * @deprecated This package is deprecated. Use @objectql/plugin-server instead.
+ *
+ * @example
+ * ```typescript
+ * // Old way (still supported for backward compatibility)
+ * import { createNodeHandler } from '@objectql/server';
+ *
+ * // New way (recommended)
+ * import { ServerPlugin } from '@objectql/plugin-server';
+ * ```
+ */
+
export * from './types';
export * from './utils';
export * from './openapi';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9fe393ec..870bb40b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -513,6 +513,37 @@ importers:
specifier: ^2.4.0
version: 2.4.0
+ packages/plugins/server:
+ dependencies:
+ '@graphql-tools/schema':
+ specifier: ^10.0.2
+ version: 10.0.31(graphql@16.12.0)
+ '@objectql/core':
+ specifier: workspace:*
+ version: link:../../foundation/core
+ '@objectql/types':
+ specifier: workspace:*
+ version: link:../../foundation/types
+ graphql:
+ specifier: ^16.8.1
+ version: 16.12.0
+ js-yaml:
+ specifier: ^4.1.1
+ version: 4.1.1
+ devDependencies:
+ '@types/js-yaml':
+ specifier: ^4.0.9
+ version: 4.0.9
+ '@types/node':
+ specifier: ^20.10.0
+ version: 20.19.29
+ hono:
+ specifier: ^4.11.0
+ version: 4.11.4
+ typescript:
+ specifier: ^5.3.0
+ version: 5.9.3
+
packages/runtime/server:
dependencies:
'@graphql-tools/schema':
@@ -4857,6 +4888,10 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+ hono@4.11.4:
+ resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==}
+ engines: {node: '>=16.9.0'}
+
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -13241,6 +13276,8 @@ snapshots:
property-information: 7.1.0
space-separated-tokens: 2.0.2
+ hono@4.11.4: {}
+
hookable@5.5.3: {}
hosted-git-info@2.8.9: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 1bc9005c..68a4b0f6 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,6 +2,7 @@ packages:
- packages/foundation/*
- packages/drivers/*
- packages/runtime/*
+ - packages/plugins/*
- packages/tools/*
- examples/quickstart/*
- examples/integrations/*
From eb2758f249c23fbc2078be3cbdddf02891d29a93 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 07:46:32 +0000
Subject: [PATCH 3/6] Add Hono server example demonstrating plugin usage
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
examples/integrations/hono-server/README.md | 82 +++++++++
.../integrations/hono-server/package.json | 42 +++++
.../integrations/hono-server/src/index.ts | 174 ++++++++++++++++++
.../hono-server/src/task.object.yml | 24 +++
.../hono-server/src/user.object.yml | 17 ++
.../integrations/hono-server/tsconfig.json | 9 +
pnpm-lock.yaml | 83 +++++++--
7 files changed, 420 insertions(+), 11 deletions(-)
create mode 100644 examples/integrations/hono-server/README.md
create mode 100644 examples/integrations/hono-server/package.json
create mode 100644 examples/integrations/hono-server/src/index.ts
create mode 100644 examples/integrations/hono-server/src/task.object.yml
create mode 100644 examples/integrations/hono-server/src/user.object.yml
create mode 100644 examples/integrations/hono-server/tsconfig.json
diff --git a/examples/integrations/hono-server/README.md b/examples/integrations/hono-server/README.md
new file mode 100644
index 00000000..17e3ab9d
--- /dev/null
+++ b/examples/integrations/hono-server/README.md
@@ -0,0 +1,82 @@
+# ObjectQL Hono Server Example
+
+This example demonstrates how to integrate ObjectQL with the [Hono](https://hono.dev/) web framework.
+
+## Features
+
+- ⚡ Fast and lightweight Hono framework
+- 🔌 ObjectQL plugin-based architecture
+- 📡 JSON-RPC, REST, and Metadata APIs
+- 🌐 CORS support
+- 💾 SQLite in-memory database
+
+## Quick Start
+
+```bash
+# Install dependencies
+pnpm install
+
+# Start the server
+pnpm dev
+```
+
+The server will start on http://localhost:3005
+
+## API Endpoints
+
+### JSON-RPC
+```bash
+curl -X POST http://localhost:3005/api/objectql \
+ -H "Content-Type: application/json" \
+ -d '{"op": "find", "object": "User", "args": {}}'
+```
+
+### REST API
+```bash
+# List all users
+curl http://localhost:3005/api/data/User
+
+# Get a specific user
+curl http://localhost:3005/api/data/User/1
+
+# Create a user
+curl -X POST http://localhost:3005/api/data/User \
+ -H "Content-Type: application/json" \
+ -d '{"name": "John", "email": "john@example.com", "age": 30, "status": "active"}'
+```
+
+### Metadata API
+```bash
+# List all objects
+curl http://localhost:3005/api/metadata/object
+
+# Get User object schema
+curl http://localhost:3005/api/metadata/object/User
+```
+
+## Why Hono?
+
+Hono is a modern, ultra-lightweight web framework that works on any JavaScript runtime (Node.js, Cloudflare Workers, Deno, Bun). It's perfect for:
+
+- Edge computing deployments
+- Serverless functions
+- High-performance APIs
+- TypeScript-first development
+
+## Architecture
+
+This example uses the `@objectql/plugin-server` package which provides a clean adapter for Hono:
+
+```typescript
+import { createHonoAdapter } from '@objectql/plugin-server';
+
+const server = new Hono();
+const objectqlHandler = createHonoAdapter(app);
+server.all('/api/*', objectqlHandler);
+```
+
+## Learn More
+
+- [Hono Documentation](https://hono.dev/)
+- [ObjectQL Documentation](https://objectql.org)
+- [@objectql/plugin-server](../../packages/plugins/server)
diff --git a/examples/integrations/hono-server/package.json b/examples/integrations/hono-server/package.json
new file mode 100644
index 00000000..fd6f79a4
--- /dev/null
+++ b/examples/integrations/hono-server/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@objectql/example-hono-server",
+ "version": "3.0.1",
+ "description": "Hono Server Integration Example for ObjectQL",
+ "private": true,
+ "keywords": [
+ "objectql",
+ "module",
+ "hono",
+ "api",
+ "rest",
+ "server",
+ "interface"
+ ],
+ "license": "MIT",
+ "author": "ObjectQL Contributors",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/objectql/objectql.git",
+ "directory": "examples/integrations/hono-server"
+ },
+ "scripts": {
+ "build": "tsc && cp src/*.yml dist/ || true",
+ "start": "node dist/index.js",
+ "dev": "tsx src/index.ts"
+ },
+ "dependencies": {
+ "@objectql/core": "workspace:*",
+ "@objectql/plugin-server": "workspace:*",
+ "@objectql/types": "workspace:*",
+ "@objectql/driver-sql": "workspace:*",
+ "@objectql/platform-node": "workspace:*",
+ "hono": "^4.11.0",
+ "@hono/node-server": "^1.19.0",
+ "sqlite3": "^5.1.7"
+ },
+ "devDependencies": {
+ "@types/node": "^20.10.0",
+ "typescript": "^5.0.0",
+ "tsx": "^4.7.0"
+ }
+}
diff --git a/examples/integrations/hono-server/src/index.ts b/examples/integrations/hono-server/src/index.ts
new file mode 100644
index 00000000..d8bc5436
--- /dev/null
+++ b/examples/integrations/hono-server/src/index.ts
@@ -0,0 +1,174 @@
+/**
+ * ObjectQL
+ * 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 { Hono } from 'hono';
+import { serve } from '@hono/node-server';
+import { ObjectQL } from '@objectql/core';
+import { SqlDriver } from '@objectql/driver-sql';
+import { ObjectLoader } from '@objectql/platform-node';
+import { createHonoAdapter } from '@objectql/plugin-server';
+import * as path from 'path';
+
+async function main() {
+ // 1. Init ObjectQL
+ const app = new ObjectQL({
+ datasources: {
+ default: new SqlDriver({
+ client: 'sqlite3',
+ connection: {
+ filename: ':memory:'
+ },
+ useNullAsDefault: true
+ })
+ }
+ });
+
+ // 2. Load Schema
+ const rootDir = path.resolve(__dirname, '..');
+ const loader = new ObjectLoader(app.metadata);
+ loader.load(rootDir);
+
+ // 3. Init
+ await app.init();
+
+ // 4. Create Hono server with ObjectQL adapter
+ const server = new Hono();
+ const port = 3005;
+
+ // Add CORS middleware
+ server.use('*', async (c, next) => {
+ c.header('Access-Control-Allow-Origin', '*');
+ c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ if (c.req.method === 'OPTIONS') {
+ return c.text('', 200);
+ }
+
+ await next();
+ });
+
+ // Mount ObjectQL handler
+ const objectqlHandler = createHonoAdapter(app);
+ server.all('/api/*', objectqlHandler);
+
+ // Welcome page
+ server.get('/', (c) => {
+ return c.html(`
+
+
+
+ ObjectQL Hono Server
+
+
+
+ 🚀 ObjectQL Hono Server
+ Welcome to the ObjectQL Hono integration example!
+
+ Available APIs
+
+ JSON-RPC: POST /api/objectql
+ Example: {"op": "find", "object": "User", "args": {}}
+
+
+ REST: GET /api/data/:object
+ Example: GET /api/data/User
+
+
+ Metadata: GET /api/metadata/object
+ Get schema information
+
+
+ Test Commands
+ curl -X POST http://localhost:${port}/api/objectql \\
+ -H "Content-Type: application/json" \\
+ -d '{"op": "find", "object": "User", "args": {}}'
+
+curl http://localhost:${port}/api/data/User
+
+curl http://localhost:${port}/api/metadata/object
+
+
+ `);
+ });
+
+ // Create some sample data
+ const ctx = app.createContext({ isSystem: true });
+ await ctx.object('User').create({
+ name: 'Alice',
+ email: 'alice@example.com',
+ age: 28,
+ status: 'active'
+ });
+ await ctx.object('User').create({
+ name: 'Bob',
+ email: 'bob@example.com',
+ age: 35,
+ status: 'active'
+ });
+ await ctx.object('User').create({
+ name: 'Charlie',
+ email: 'charlie@example.com',
+ age: 42,
+ status: 'inactive'
+ });
+
+ await ctx.object('Task').create({
+ title: 'Complete project',
+ description: 'Finish the ObjectQL console',
+ status: 'in-progress',
+ priority: 'high'
+ });
+ await ctx.object('Task').create({
+ title: 'Write documentation',
+ description: 'Document the new console feature',
+ status: 'pending',
+ priority: 'medium'
+ });
+ await ctx.object('Task').create({
+ title: 'Code review',
+ description: 'Review pull requests',
+ status: 'pending',
+ priority: 'low'
+ });
+
+ // Start Hono server
+ console.log(`\n🚀 ObjectQL Hono Server running on http://localhost:${port}`);
+ console.log(`\n🔌 APIs:`);
+ console.log(` - JSON-RPC: http://localhost:${port}/api/objectql`);
+ console.log(` - REST: http://localhost:${port}/api/data`);
+ console.log(` - Metadata: http://localhost:${port}/api/metadata`);
+ console.log(` - Web UI: http://localhost:${port}/`);
+
+ serve({
+ fetch: server.fetch,
+ port
+ });
+}
+
+main().catch(console.error);
diff --git a/examples/integrations/hono-server/src/task.object.yml b/examples/integrations/hono-server/src/task.object.yml
new file mode 100644
index 00000000..a4a8b9ee
--- /dev/null
+++ b/examples/integrations/hono-server/src/task.object.yml
@@ -0,0 +1,24 @@
+label: Tasks
+fields:
+ title:
+ type: string
+ label: Title
+ required: true
+ description:
+ type: text
+ label: Description
+ status:
+ type: string
+ label: Status
+ defaultValue: pending
+ priority:
+ type: string
+ label: Priority
+ defaultValue: medium
+ due_date:
+ type: date
+ label: Due Date
+ completed:
+ type: boolean
+ label: Completed
+ defaultValue: false
diff --git a/examples/integrations/hono-server/src/user.object.yml b/examples/integrations/hono-server/src/user.object.yml
new file mode 100644
index 00000000..77f7af15
--- /dev/null
+++ b/examples/integrations/hono-server/src/user.object.yml
@@ -0,0 +1,17 @@
+label: Users
+fields:
+ name:
+ type: string
+ label: Full Name
+ required: true
+ email:
+ type: string
+ label: Email Address
+ required: true
+ status:
+ type: string
+ label: Status
+ defaultValue: active
+ age:
+ type: number
+ label: Age
diff --git a/examples/integrations/hono-server/tsconfig.json b/examples/integrations/hono-server/tsconfig.json
new file mode 100644
index 00000000..b54386af
--- /dev/null
+++ b/examples/integrations/hono-server/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 870bb40b..7f4e565a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,7 +65,7 @@ importers:
version: 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
vite:
specifier: ^7.3.1
- version: 7.3.1(@types/node@20.19.29)(jiti@1.21.7)
+ version: 7.3.1(@types/node@20.19.29)(jiti@1.21.7)(tsx@4.21.0)
vitepress:
specifier: ^1.6.4
version: 1.6.4(@algolia/client-search@5.46.2)(@types/node@20.19.29)(@types/react@18.3.27)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)
@@ -80,7 +80,7 @@ importers:
version: 10.0.0(fumadocs-core@13.4.10(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
fumadocs-ui:
specifier: ^13.0.0
- version: 13.4.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19)
+ version: 13.4.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0))
katex:
specifier: ^0.16.27
version: 0.16.27
@@ -135,7 +135,7 @@ importers:
version: 8.5.6
tailwindcss:
specifier: ^3.4.0
- version: 3.4.19
+ version: 3.4.19(tsx@4.21.0)
typescript:
specifier: ^5.3.0
version: 5.9.3
@@ -243,6 +243,43 @@ importers:
specifier: ^5.0.0
version: 5.9.3
+ examples/integrations/hono-server:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^1.19.0
+ version: 1.19.9(hono@4.11.4)
+ '@objectql/core':
+ specifier: workspace:*
+ version: link:../../../packages/foundation/core
+ '@objectql/driver-sql':
+ specifier: workspace:*
+ version: link:../../../packages/drivers/sql
+ '@objectql/platform-node':
+ specifier: workspace:*
+ version: link:../../../packages/foundation/platform-node
+ '@objectql/plugin-server':
+ specifier: workspace:*
+ version: link:../../../packages/plugins/server
+ '@objectql/types':
+ specifier: workspace:*
+ version: link:../../../packages/foundation/types
+ hono:
+ specifier: ^4.11.0
+ version: 4.11.4
+ sqlite3:
+ specifier: ^5.1.7
+ version: 5.1.7
+ devDependencies:
+ '@types/node':
+ specifier: ^20.10.0
+ version: 20.19.29
+ tsx:
+ specifier: ^4.7.0
+ version: 4.21.0
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
examples/quickstart/hello-world:
dependencies:
'@objectql/core':
@@ -1616,6 +1653,12 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+ '@hono/node-server@1.19.9':
+ resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -7676,6 +7719,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsx@4.21.0:
+ resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -9140,6 +9188,10 @@ snapshots:
dependencies:
graphql: 16.12.0
+ '@hono/node-server@1.19.9(hono@4.11.4)':
+ dependencies:
+ hono: 4.11.4
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -10333,10 +10385,10 @@ snapshots:
'@swc/counter': 0.1.3
tslib: 2.8.1
- '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)':
+ '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0))':
dependencies:
postcss-selector-parser: 6.0.10
- tailwindcss: 3.4.19
+ tailwindcss: 3.4.19(tsx@4.21.0)
'@textlint/ast-node-types@15.5.0': {}
@@ -12928,7 +12980,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- fumadocs-ui@13.4.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19):
+ fumadocs-ui@13.4.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(tsx@4.21.0)):
dependencies:
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -12937,7 +12989,7 @@ snapshots:
'@radix-ui/react-popover': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@tailwindcss/typography': 0.5.19(tailwindcss@3.4.19)
+ '@tailwindcss/typography': 0.5.19(tailwindcss@3.4.19(tsx@4.21.0))
class-variance-authority: 0.7.1
cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
fumadocs-core: 13.4.10(@types/react@18.3.27)(next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -15775,12 +15827,13 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.6
- postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6):
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.6
+ tsx: 4.21.0
postcss-nested@6.2.0(postcss@8.5.6):
dependencies:
@@ -16831,7 +16884,7 @@ snapshots:
tailwind-merge@2.6.0: {}
- tailwindcss@3.4.19:
+ tailwindcss@3.4.19(tsx@4.21.0):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -16850,7 +16903,7 @@ snapshots:
postcss: 8.5.6
postcss-import: 15.1.0(postcss@8.5.6)
postcss-js: 4.1.0(postcss@8.5.6)
- postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)
postcss-nested: 6.2.0(postcss@8.5.6)
postcss-selector-parser: 6.1.2
resolve: 1.22.11
@@ -17039,6 +17092,13 @@ snapshots:
tslib@2.8.1: {}
+ tsx@4.21.0:
+ dependencies:
+ esbuild: 0.27.2
+ get-tsconfig: 4.13.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
@@ -17325,7 +17385,7 @@ snapshots:
'@types/node': 20.19.29
fsevents: 2.3.3
- vite@7.3.1(@types/node@20.19.29)(jiti@1.21.7):
+ vite@7.3.1(@types/node@20.19.29)(jiti@1.21.7)(tsx@4.21.0):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -17337,6 +17397,7 @@ snapshots:
'@types/node': 20.19.29
fsevents: 2.3.3
jiti: 1.21.7
+ tsx: 4.21.0
vitepress@1.6.4(@algolia/client-search@5.46.2)(@types/node@20.19.29)(@types/react@18.3.27)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3):
dependencies:
From 9cafa9f1e206ff065b73af847e385738adddc779 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 07:49:45 +0000
Subject: [PATCH 4/6] Fix object names and test Hono server functionality
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
examples/integrations/hono-server/README.md | 12 +++++------
.../integrations/hono-server/src/index.ts | 20 +++++++++----------
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/examples/integrations/hono-server/README.md b/examples/integrations/hono-server/README.md
index 17e3ab9d..da6eba07 100644
--- a/examples/integrations/hono-server/README.md
+++ b/examples/integrations/hono-server/README.md
@@ -28,19 +28,19 @@ The server will start on http://localhost:3005
```bash
curl -X POST http://localhost:3005/api/objectql \
-H "Content-Type: application/json" \
- -d '{"op": "find", "object": "User", "args": {}}'
+ -d '{"op": "find", "object": "user", "args": {}}'
```
### REST API
```bash
# List all users
-curl http://localhost:3005/api/data/User
+curl http://localhost:3005/api/data/user
# Get a specific user
-curl http://localhost:3005/api/data/User/1
+curl http://localhost:3005/api/data/user/1
# Create a user
-curl -X POST http://localhost:3005/api/data/User \
+curl -X POST http://localhost:3005/api/data/user \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "john@example.com", "age": 30, "status": "active"}'
```
@@ -50,8 +50,8 @@ curl -X POST http://localhost:3005/api/data/User \
# List all objects
curl http://localhost:3005/api/metadata/object
-# Get User object schema
-curl http://localhost:3005/api/metadata/object/User
+# Get user object schema
+curl http://localhost:3005/api/metadata/object/user
```
## Why Hono?
diff --git a/examples/integrations/hono-server/src/index.ts b/examples/integrations/hono-server/src/index.ts
index d8bc5436..71f7a528 100644
--- a/examples/integrations/hono-server/src/index.ts
+++ b/examples/integrations/hono-server/src/index.ts
@@ -93,11 +93,11 @@ async function main() {
Available APIs
JSON-RPC: POST /api/objectql
- Example: {"op": "find", "object": "User", "args": {}}
+ Example: {"op": "find", "object": "user", "args": {}}
REST: GET /api/data/:object
- Example: GET /api/data/User
+ Example: GET /api/data/user
Metadata: GET /api/metadata/object
@@ -107,9 +107,9 @@ async function main() {
Test Commands
curl -X POST http://localhost:${port}/api/objectql \\
-H "Content-Type: application/json" \\
- -d '{"op": "find", "object": "User", "args": {}}'
+ -d '{"op": "find", "object": "user", "args": {}}'
-curl http://localhost:${port}/api/data/User
+curl http://localhost:${port}/api/data/user
curl http://localhost:${port}/api/metadata/object
-
-
-
-
-