From abac26ce17dc7a0d1fadffea5c39793f8984a515 Mon Sep 17 00:00:00 2001 From: PredictiveManish Date: Mon, 11 May 2026 11:04:25 +0530 Subject: [PATCH 1/4] examples n8n integration --- examples/cookbook/n8n-integration/README.md | 107 +++++++++ .../n8n-integration/moss-n8n-helper.ts | 209 ++++++++++++++++++ .../n8n-integration/n8n-moss-workflow.json | 152 +++++++++++++ 3 files changed, 468 insertions(+) create mode 100644 examples/cookbook/n8n-integration/README.md create mode 100644 examples/cookbook/n8n-integration/moss-n8n-helper.ts create mode 100644 examples/cookbook/n8n-integration/n8n-moss-workflow.json diff --git a/examples/cookbook/n8n-integration/README.md b/examples/cookbook/n8n-integration/README.md new file mode 100644 index 0000000..36b0ab2 --- /dev/null +++ b/examples/cookbook/n8n-integration/README.md @@ -0,0 +1,107 @@ +# Moss N8N Integration Cookbook + +This cookbook provides a TypeScript helper and example workflow for integrating Moss with n8n, the open-source workflow automation platform. + +## Overview + +The integration exposes Moss's core operations so any n8n workflow can incorporate real-time semantic search without writing SDK code from scratch: + +- **Moss: Create Index** - Bootstrap a new index from an upstream data source +- **Moss: Add Documents** - Upsert one or more documents (with optional metadata) into an index +- **Moss: Delete Documents** - Remove documents by ID +- **Moss: Query** - Run a semantic/hybrid search query against an existing index + +## Files + +1. `moss-n8n-helper.ts` - A TypeScript wrapper around the Moss SDK optimized for n8n usage +2. `n8n-moss-workflow.json` - An example n8n workflow demonstrating the four core operations + +## Usage + +### Option 1: Using the TypeScript Helper (Recommended) + +For the best developer experience, use the provided TypeScript helper in your n8n Function nodes: + +1. Copy `moss-n8n-helper.ts` to your n8n custom nodes directory or bundle it with your workflow +2. Import and use it in Function nodes: + +```typescript +// Import the helper (adjust path as needed) +import { MossN8NHelper } from './moss-n8n-helper'; + +// Initialize with your Moss credentials +const helper = new MossN8NHelper('your-project-id', 'your-project-key'); + +// Create an index +const createResult = await helper.createIndex('my-index', [ + { id: '1', text: 'Hello world', metadata: { source: 'example' } } +]); + +// Add documents +await helper.addDocs('my-index', [ + { id: '2', text: 'Another document', metadata: { source: 'example' } } +]); + +// Query the index +const results = await helper.query('my-index', 'hello', { topK: 5 }); + +// Delete documents +await helper.deleteDocs('my-index', ['1']); + +// Clean up +helper.dispose(); +``` + +### Option 2: Direct HTTP Requests + +If you prefer not to use the helper, you can call Moss's public REST endpoints directly using n8n's HTTP Request nodes. Refer to the [Moss API documentation](https://docs.moss.dev) for endpoint details. + +## Example Workflow + +The included `n8n-moss-workflow.json` demonstrates a complete workflow: + +1. **Start** - Manual trigger to begin the workflow +2. **Moss Config** - Function node to set up Moss credentials +3. **Create Moss Index** - Function node showing how to create an index +4. **Add Documents** - Function node showing how to add documents +5. **Query Moss Index** - Function node showing how to query the index +6. **Delete Documents** - Function node showing how to delete documents + +Note: The example workflow uses Function nodes to illustrate the logic. In a real implementation, you would replace the placeholder code with actual calls to the Moss N8N Helper. + +## Common Use Cases + +### Real-time Knowledge Base Sync +Pair n8n triggers (Google Drive, GitHub, Postgres, etc.) with the Moss "Add Documents" node to automatically keep a semantic index in sync with any upstream source. + +### RAG Pipelines Without Code +Feed data from various sources into a Moss index entirely within n8n workflows, then query it from AI agents or LLMs—no custom SDK code required. + +### AI Agent Workflows +Use the Moss query operation within AI agent chains to retrieve relevant context before generating responses. + +## Requirements + +- Node.js >= 16 +- n8n >= 0.200.0 +- Moss account with project ID and project key + +## Setup + +1. Obtain your Moss project ID and project key from [Moss Dashboard](https://app.moss.dev) +2. Install the Moss JavaScript SDK in your n8n environment (if using the helper approach): + ```bash + npm install @moss-dev/moss + ``` +3. Import the helper into your n8n Function nodes as shown above + +## Development + +To modify the helper: +1. Edit `moss-n8n-helper.ts` +2. Rebuild if necessary (though TypeScript works directly in n8n Function nodes) +3. Test with your n8n instance + +## License + +BSD-2-Clause - See [LICENSE](../LICENSE) for details. \ No newline at end of file diff --git a/examples/cookbook/n8n-integration/moss-n8n-helper.ts b/examples/cookbook/n8n-integration/moss-n8n-helper.ts new file mode 100644 index 0000000..d02a8c0 --- /dev/null +++ b/examples/cookbook/n8n-integration/moss-n8n-helper.ts @@ -0,0 +1,209 @@ +import { MossClient } from '@moss-dev/moss'; + +// Moss N8N Helper - Wrapper around Moss SDK for use in n8n workflows +// This helper provides easy-to-use functions for the four core Moss operations: +// - createIndex: Bootstrap a new index from documents +// - addDocs: Upsert documents into an index +// - deleteDocs: Remove documents by ID +// - query: Run semantic/hybrid search queries + +/** + * MossN8NHelper - Wrapper for Moss SDK optimized for n8n usage + */ +export class MossN8NHelper { + private client: MossClient; + + /** + * Initialize the Moss client + * @param projectId - Your Moss project identifier + * @param projectKey - Your Moss project authentication key + */ + constructor(projectId: string, projectKey: string) { + this.client = new MossClient(projectId, projectKey); + } + + /** + * Create a new index with documents + * @param indexName - Name of the index to create + * @param docs - Array of documents (each with id and text, optionally with metadata and embedding) + * @param options - Optional configuration (modelId, onProgress callback) + * @returns Promise resolving to the creation result + */ + async createIndex( + indexName: string, + docs: Array<{ + id: string; + text: string; + metadata?: Record; + embedding?: number[]; + }>, + options?: { + modelId?: string; + onProgress?: (status: string, progress: number) => void; + } + ): Promise<{ + jobId: string; + status: string; + progress: number; + }> { + // Convert to Moss DocumentInfo format + const mossDocs = docs.map(doc => ({ + id: doc.id, + text: doc.text, + ...(doc.metadata && { metadata: doc.metadata }), + ...(doc.embedding && { embedding: doc.embedding }) + })); + + const result = await this.client.createIndex(indexName, mossDocs, options); + return { + jobId: result.jobId, + status: result.status, + progress: result.progress + }; + } + + /** + * Add or update documents in an index + * @param indexName - Name of the target index + * @param docs - Array of documents to add/update + * @param options - Optional configuration (upsert, onProgress callback) + * @returns Promise resolving to the operation result + */ + async addDocs( + indexName: string, + docs: Array<{ + id: string; + text: string; + metadata?: Record; + embedding?: number[]; + }>, + options?: { + upsert?: boolean; + onProgress?: (status: string, progress: number) => void; + } + ): Promise<{ + jobId: string; + status: string; + progress: number; + }> { + // Convert to Moss DocumentInfo format + const mossDocs = docs.map(doc => ({ + id: doc.id, + text: doc.text, + ...(doc.metadata && { metadata: doc.metadata }), + ...(doc.embedding && { embedding: doc.embedding }) + })); + + const result = await this.client.addDocs(indexName, mossDocs, options); + return { + jobId: result.jobId, + status: result.status, + progress: result.progress + }; + } + + /** + * Delete documents from an index by their IDs + * @param indexName - Name of the target index + * @param docIds - Array of document IDs to delete + * @param options - Optional configuration (onProgress callback) + * @returns Promise resolving to the deletion result + */ + async deleteDocs( + indexName: string, + docIds: string[], + options?: { + onProgress?: (status: string, progress: number) => void; + } + ): Promise<{ + jobId: string; + status: string; + progress: number; + }> { + const result = await this.client.deleteDocs(indexName, docIds, options); + return { + jobId: result.jobId, + status: result.status, + progress: result.progress + }; + } + + /** + * Query an index for semantic/hybrid search + * @param indexName - Name of the index to query + * @param queryText - The search query text + * @param options - Optional query configuration (topK, etc.) + * @returns Promise resolving to search results + */ + async query( + indexName: string, + queryText: string, + options?: { + topK?: number; + } + ): Promise; + score: number; + }>> { + // Load index for fast local queries (recommended for n8n workflows) + await this.client.loadIndex(indexName); + + const results = await this.client.query(indexName, queryText, { + topK: options?.topK ?? 10 + }); + + // Convert to n8n-friendly format + return results.docs.map(doc => ({ + id: doc.id, + text: doc.text, + ...(doc.metadata && { metadata: doc.metadata }), + score: doc.score + })); + } + + /** + * Get the status of an async job + * @param jobId - The job ID from createIndex, addDocs, or deleteDocs + * @returns Promise resolving to job status + */ + async getJobStatus(jobId: string): Promise<{ + status: string; + progress: number; + }> { + const status = await this.client.getJobStatus(jobId); + return { + status: status.status, + progress: status.progress + }; + } + + /** + * Dispose of the client resources + */ + dispose(): void { + this.client.dispose(); + } +} + +// Example usage for n8n: +// const helper = new MossN8NHelper('your-project-id', 'your-project-key'); +// +// // Create index +// const createResult = await helper.createIndex('my-index', [ +// { id: '1', text: 'Hello world', metadata: { source: 'example' } } +// ]); +// +// // Add docs +// await helper.addDocs('my-index', [ +// { id: '2', text: 'Another document', metadata: { source: 'example' } } +// ]); +// +// // Query +// const results = await helper.query('my-index', 'hello'); +// +// // Delete docs +// await helper.deleteDocs('my-index', ['1']); +// +// helper.dispose(); \ No newline at end of file diff --git a/examples/cookbook/n8n-integration/n8n-moss-workflow.json b/examples/cookbook/n8n-integration/n8n-moss-workflow.json new file mode 100644 index 0000000..d13b73d --- /dev/null +++ b/examples/cookbook/n8n-integration/n8n-moss-workflow.json @@ -0,0 +1,152 @@ +{ + "name": "Moss Document Sync Workflow", + "nodes": [ + { + "parameters": { + "rules": { + "items": [ + { + "value": "manual", + "name": "trigger", + "operation": "equal" + } + ] + } + }, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 250, + 300 + ] + }, + { + "parameters": { + "functionCode": "// Initialize Moss N8N Helper\n// Replace with your actual project ID and key\nconst projectId = \"your-project-id\";\nconst projectKey = \"your-project-key\";\n\n// This would normally be done in a Function node\n// For demo purposes, we're just returning the config\nreturn [{\n projectId,\n projectKey\n}];" + }, + "name": "Moss Config", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 450, + 300 + ] + }, + { + "parameters": { + "functionCode": "// Example: Create a new index with sample documents\n// In a real workflow, this data would come from previous nodes\n\nconst { projectId, projectKey } = items[0].json;\n\n// This is where you'd use the Moss N8N Helper\n// Since we can't directly import TypeScript in n8n Function nodes,\n// we'll show the logic that would be implemented\n\nconst sampleDocs = [\n { id: 'doc1', text: 'Machine learning is a subset of AI', metadata: { category: 'tech' } },\n { id: 'doc2', text: 'Deep learning uses neural networks with multiple layers', metadata: { category: 'tech' } },\n { id: 'doc3', text: 'Natural language processing enables computers to understand text', metadata: { category: 'tech' } }\n];\n\n// In practice, you would:\n// 1. Create a helper instance: const helper = new MossN8NHelper(projectId, projectKey);\n// 2. Create index: const createResult = await helper.createIndex('knowledge-base', sampleDocs);\n// 3. Wait for completion using getJobStatus in a loop\n// 4. Return the job ID for tracking\n\nreturn [{\n indexName: 'knowledge-base',\n documentCount: sampleDocs.length,\n status: 'index_creation_started',\n note: 'In actual n8n workflow, use Function items to call Moss N8N Helper methods'\n}];" + }, + "name": "Create Moss Index", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 650, + 200 + ] + }, + { + "parameters": { + "functionCode": "// Example: Add documents to an existing index\n// This would typically follow a trigger from a data source (Notion, DB, etc.)\n\nconst newDocs = [\n { id: 'doc4', text: 'Computer vision enables machines to interpret visual information', metadata: { category: 'tech', source: 'webhook' } },\n { id: 'doc5', text: 'Reinforcement learning trains agents through reward signals', metadata: { category: 'tech', source: 'webhook' } }\n];\n\n// In actual implementation:\n// const helper = new MossN8NHelper($json[\"projectId\"], $json[\"projectKey\"]);\n// const addResult = await helper.addDocs('knowledge-base', newDocs, { upsert: true });\n\nreturn [{\n addedDocCount: newDocs.length,\n indexName: 'knowledge-base',\n status: 'documents_added',\n note: 'In actual n8n workflow, use Function items to call Moss N8N Helper methods'\n}];" + }, + "name": "Add Documents", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 650, + 400 + ] + }, + { + "parameters": { + "functionCode": "// Example: Query the index for relevant information\n// This could be used in an AI agent workflow\n\nconst queryText = \"What is deep learning?\";\n\n// In actual implementation:\n// const helper = new MossN8NHelper($json[\"projectId\"], $json[\"projectKey\"]);\n// await helper.loadIndex('knowledge-base'); // For fast local queries\n// const results = await helper.query('knowledge-base', queryText, { topK: 3 });\n\n// Return formatted results for n8n\nreturn [{\n query: queryText,\n results: [\n {\n id: \"doc2\",\n text: \"Deep learning uses neural networks with multiple layers\",\n metadata: { category: \"tech\" },\n score: 0.95\n },\n {\n id: \"doc3\",\n text: \"Natural language processing enables computers to understand text\",\n metadata: { category: \"tech\" },\n score: 0.78\n }\n ],\n resultCount: 2,\n status: 'query_completed'\n}];" + }, + "name": "Query Moss Index", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 850, + 300 + ] + }, + { + "parameters": { + "functionCode": "// Example: Delete documents from index\n// Useful for removing outdated information\n\nconst docsToDelete = ['doc1', 'doc3'];\n\n// In actual implementation:\n// const helper = new MossN8NHelper($json[\"projectId\"], $json[\"projectKey\"]);\n// const deleteResult = await helper.deleteDocs('knowledge-base', docsToDelete);\n\nreturn [{\n deletedDocIds: docsToDelete,\n indexName: 'knowledge-base',\n status: 'documents_deleted',\n note: 'In actual n8n workflow, use Function items to call Moss N8N Helper methods'\n}];" + }, + "name": "Delete Documents", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 1050, + 300 + ] + } + ], + "connections": { + "Start": { + "main": [ + [ + { + "node": "Moss Config", + "type": "main", + "index": 0 + } + ] + ] + }, + "Moss Config": { + "main": [ + [ + { + "node": "Create Moss Index", + "type": "main", + "index": 0 + }, + { + "node": "Add Documents", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Moss Index": { + "main": [ + [ + { + "node": "Query Moss Index", + "type": "main", + "index": 0 + } + ] + ] + }, + "Add Documents": { + "main": [ + [ + { + "node": "Query Moss Index", + "type": "main", + "index": 0 + } + ] + ] + }, + "Query Moss Index": { + "main": [ + [ + { + "node": "Delete Documents", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionTimeout": 5400 + }, + "id": "1" +} \ No newline at end of file From 6406900ff0eb014e4f976eea1bae5b72a89dc1c3 Mon Sep 17 00:00:00 2001 From: PredictiveManish Date: Mon, 11 May 2026 11:28:51 +0530 Subject: [PATCH 2/4] Added test and updated import design --- .../n8n-integration/moss-n8n-helper.ts | 102 +++++--- .../n8n-integration/test_moss_n8n_helper.ts | 244 ++++++++++++++++++ 2 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 examples/cookbook/n8n-integration/test_moss_n8n_helper.ts diff --git a/examples/cookbook/n8n-integration/moss-n8n-helper.ts b/examples/cookbook/n8n-integration/moss-n8n-helper.ts index d02a8c0..56b175f 100644 --- a/examples/cookbook/n8n-integration/moss-n8n-helper.ts +++ b/examples/cookbook/n8n-integration/moss-n8n-helper.ts @@ -1,4 +1,5 @@ import { MossClient } from '@moss-dev/moss'; +import type { MutationResult, JobStatusResponse } from '@moss-dev/moss-core'; // Moss N8N Helper - Wrapper around Moss SDK for use in n8n workflows // This helper provides easy-to-use functions for the four core Moss operations: @@ -27,7 +28,7 @@ export class MossN8NHelper { * @param indexName - Name of the index to create * @param docs - Array of documents (each with id and text, optionally with metadata and embedding) * @param options - Optional configuration (modelId, onProgress callback) - * @returns Promise resolving to the creation result + * @returns Promise resolving to the creation result with job info */ async createIndex( indexName: string, @@ -39,12 +40,12 @@ export class MossN8NHelper { }>, options?: { modelId?: string; - onProgress?: (status: string, progress: number) => void; + onProgress?: (jobProgress: { status: string; progress: number; currentPhase?: string }) => void; } ): Promise<{ jobId: string; - status: string; - progress: number; + indexName: string; + docCount: number; }> { // Convert to Moss DocumentInfo format const mossDocs = docs.map(doc => ({ @@ -54,11 +55,16 @@ export class MossN8NHelper { ...(doc.embedding && { embedding: doc.embedding }) })); + // Call the SDK method which returns MutationResult const result = await this.client.createIndex(indexName, mossDocs, options); + + // If there's a progress callback, we need to poll for status updates + // But for simplicity in this helper, we'll just return the basic result + // Users can call getJobStatus separately if they need progress tracking return { jobId: result.jobId, - status: result.status, - progress: result.progress + indexName: result.indexName, + docCount: result.docCount }; } @@ -67,7 +73,7 @@ export class MossN8NHelper { * @param indexName - Name of the target index * @param docs - Array of documents to add/update * @param options - Optional configuration (upsert, onProgress callback) - * @returns Promise resolving to the operation result + * @returns Promise resolving to the operation result with job info */ async addDocs( indexName: string, @@ -79,12 +85,12 @@ export class MossN8NHelper { }>, options?: { upsert?: boolean; - onProgress?: (status: string, progress: number) => void; + onProgress?: (jobProgress: { status: string; progress: number; currentPhase?: string }) => void; } ): Promise<{ jobId: string; - status: string; - progress: number; + indexName: string; + docCount: number; }> { // Convert to Moss DocumentInfo format const mossDocs = docs.map(doc => ({ @@ -94,11 +100,14 @@ export class MossN8NHelper { ...(doc.embedding && { embedding: doc.embedding }) })); + // Call the SDK method which returns MutationResult const result = await this.client.addDocs(indexName, mossDocs, options); + + // Return the basic result - users can call getJobStatus for progress return { jobId: result.jobId, - status: result.status, - progress: result.progress + indexName: result.indexName, + docCount: result.docCount }; } @@ -107,24 +116,27 @@ export class MossN8NHelper { * @param indexName - Name of the target index * @param docIds - Array of document IDs to delete * @param options - Optional configuration (onProgress callback) - * @returns Promise resolving to the deletion result + * @returns Promise resolving to the deletion result with job info */ async deleteDocs( indexName: string, docIds: string[], options?: { - onProgress?: (status: string, progress: number) => void; + onProgress?: (jobProgress: { status: string; progress: number; currentPhase?: string }) => void; } ): Promise<{ jobId: string; - status: string; - progress: number; + indexName: string; + docCount: number; }> { + // Call the SDK method which returns MutationResult const result = await this.client.deleteDocs(indexName, docIds, options); + + // Return the basic result - users can call getJobStatus for progress return { jobId: result.jobId, - status: result.status, - progress: result.progress + indexName: result.indexName, + docCount: result.docCount }; } @@ -147,9 +159,9 @@ export class MossN8NHelper { metadata?: Record; score: number; }>> { - // Load index for fast local queries (recommended for n8n workflows) - await this.client.loadIndex(indexName); - + // Note: Per Moss SDK docs, query() will use local index if loaded via loadIndex(), + // otherwise it falls back to cloud query. For best performance in n8n workflows, + // users should call loadIndex() once before doing multiple queries. const results = await this.client.query(indexName, queryText, { topK: options?.topK ?? 10 }); @@ -163,27 +175,50 @@ export class MossN8NHelper { })); } + /** + * Load an index into memory for fast local querying + * @param indexName - Name of the index to load + * @param options - Optional configuration including auto-refresh settings + * @returns Promise that resolves to index info + */ + async loadIndex( + indexName: string, + options?: { + autoRefresh?: boolean; + pollingIntervalInSeconds?: number; + } + ): Promise { + return this.client.loadIndex(indexName, options); + } + /** * Get the status of an async job * @param jobId - The job ID from createIndex, addDocs, or deleteDocs - * @returns Promise resolving to job status + * @returns Promise resolving to job status with progress details */ async getJobStatus(jobId: string): Promise<{ status: string; progress: number; + currentPhase?: string; + error?: string; }> { const status = await this.client.getJobStatus(jobId); return { status: status.status, - progress: status.progress + progress: status.progress, + currentPhase: status.currentPhase?.toString(), + error: status.error }; } /** - * Dispose of the client resources + * Close the client and release resources + * Note: MossClient doesn't have a dispose() method, but we can clean up references */ - dispose(): void { - this.client.dispose(); + close(): void { + // In JavaScript, we just nullify the reference to allow garbage collection + // The actual cleanup happens internally in the MossClient + (this as any).client = null; } } @@ -195,15 +230,22 @@ export class MossN8NHelper { // { id: '1', text: 'Hello world', metadata: { source: 'example' } } // ]); // +// // Check creation status (optional) +// const createStatus = await helper.getJobStatus(createResult.jobId); +// // // Add docs -// await helper.addDocs('my-index', [ +// const addResult = await helper.addDocs('my-index', [ // { id: '2', text: 'Another document', metadata: { source: 'example' } } // ]); // +// // Load index for fast local queries (recommended before querying) +// await helper.loadIndex('my-index'); +// // // Query -// const results = await helper.query('my-index', 'hello'); +// const results = await helper.query('my-index', 'hello', { topK: 5 }); // // // Delete docs -// await helper.deleteDocs('my-index', ['1']); +// const deleteResult = await helper.deleteDocs('my-index', ['1']); // -// helper.dispose(); \ No newline at end of file +// // Clean up +// helper.close(); \ No newline at end of file diff --git a/examples/cookbook/n8n-integration/test_moss_n8n_helper.ts b/examples/cookbook/n8n-integration/test_moss_n8n_helper.ts new file mode 100644 index 0000000..c8751da --- /dev/null +++ b/examples/cookbook/n8n-integration/test_moss_n8n_helper.ts @@ -0,0 +1,244 @@ +import { MossN8NHelper } from './moss-n8n-helper'; + +// Mock MossClient for testing +const mockCreateIndex = jest.fn(); +const mockAddDocs = jest.fn(); +const mockDeleteDocs = jest.fn(); +const mockQuery = jest.fn(); +const mockLoadIndex = jest.fn(); +const mockGetJobStatus = jest.fn(); +const mockClose = jest.fn(); + +// Mock the MossClient class +jest.mock('@moss-dev/moss', () => { + return { + MossClient: jest.fn().mockImplementation(() => ({ + createIndex: mockCreateIndex, + addDocs: mockAddDocs, + deleteDocs: mockDeleteDocs, + query: mockQuery, + loadIndex: mockLoadIndex, + getJobStatus: mockGetJobStatus, + close: mockClose + })) + }; +}); + +describe('MossN8NHelper', () => { + let helper: MossN8NHelper; + const projectId = 'test-project-id'; + const projectKey = 'test-project-key'; + + beforeEach(() => { + helper = new MossN8NHelper(projectId, projectKey); + jest.clearAllMocks(); + }); + + describe('createIndex', () => { + it('should call MossClient.createIndex with correct parameters', async () => { + const mockResult = { + jobId: 'job-123', + indexName: 'test-index', + docCount: 2 + }; + mockCreateIndex.mockResolvedValue(mockResult); + + const docs = [ + { id: '1', text: 'Hello world', metadata: { source: 'test' } }, + { id: '2', text: 'Another doc', metadata: { source: 'test' } } + ]; + + const result = await helper.createIndex('test-index', docs); + + expect(mockCreateIndex).toHaveBeenCalledWith( + 'test-index', + expect.arrayContaining([ + expect.objectContaining({ id: '1', text: 'Hello world' }), + expect.objectContaining({ id: '2', text: 'Another doc' }) + ]), + undefined + ); + expect(result).toEqual(mockResult); + }); + + it('should handle options correctly', async () => { + const mockResult = { + jobId: 'job-123', + indexName: 'test-index', + docCount: 1 + }; + mockCreateIndex.mockResolvedValue(mockResult); + + const docs = [{ id: '1', text: 'Test doc' }]; + const options = { modelId: 'custom-model' }; + + await helper.createIndex('test-index', docs, options); + + expect(mockCreateIndex).toHaveBeenCalledWith( + 'test-index', + expect.any(Array), + options + ); + }); + }); + + describe('addDocs', () => { + it('should call MossClient.addDocs with correct parameters', async () => { + const mockResult = { + jobId: 'job-456', + indexName: 'test-index', + docCount: 5 + }; + mockAddDocs.mockResolvedValue(mockResult); + + const docs = [ + { id: '3', text: 'New doc', metadata: { source: 'api' } } + ]; + + const result = await helper.addDocs('test-index', docs); + + expect(mockAddDocs).toHaveBeenCalledWith( + 'test-index', + expect.arrayContaining([ + expect.objectContaining({ id: '3', text: 'New doc' }) + ]), + undefined + ); + expect(result).toEqual(mockResult); + }); + + it('should handle upsert option', async () => { + const mockResult = { + jobId: 'job-456', + indexName: 'test-index', + docCount: 3 + }; + mockAddDocs.mockResolvedValue(mockResult); + + const docs = [{ id: '1', text: 'Updated doc' }]; + const options = { upsert: true }; + + await helper.addDocs('test-index', docs, options); + + expect(mockAddDocs).toHaveBeenCalledWith( + 'test-index', + expect.any(Array), + options + ); + }); + }); + + describe('deleteDocs', () => { + it('should call MossClient.deleteDocs with correct parameters', async () => { + const mockResult = { + jobId: 'job-789', + indexName: 'test-index', + docCount: 0 + }; + mockDeleteDocs.mockResolvedValue(mockResult); + + const docIds = ['1', '2']; + const result = await helper.deleteDocs('test-index', docIds); + + expect(mockDeleteDocs).toHaveBeenCalledWith('test-index', ['1', '2'], undefined); + expect(result).toEqual(mockResult); + }); + }); + + describe('query', () => { + it('should call MossClient.query and return formatted results', async () => { + const mockSearchResult = { + docs: [ + { + id: 'doc1', + text: 'Test document', + metadata: { category: 'test' }, + score: 0.95 + } + ], + query: 'test query', + timeTakenInMs: 5 + }; + mockQuery.mockResolvedValue(mockSearchResult); + + const results = await helper.query('test-index', 'test query', { topK: 5 }); + + expect(mockQuery).toHaveBeenCalledWith('test-index', 'test query', { topK: 5 }); + expect(results).toEqual([ + { + id: 'doc1', + text: 'Test document', + metadata: { category: 'test' }, + score: 0.95 + } + ]); + }); + + it('should use default topK when not provided', async () => { + const mockSearchResult = { + docs: [], + query: 'test', + timeTakenInMs: 2 + }; + mockQuery.mockResolvedValue(mockSearchResult); + + await helper.query('test-index', 'test'); + + expect(mockQuery).toHaveBeenCalledWith('test-index', 'test', { topK: 10 }); // default value + }); + }); + + describe('loadIndex', () => { + it('should call MossClient.loadIndex', async () => { + const mockResult = 'test-index'; + mockLoadIndex.mockResolvedValue(mockResult); + + const result = await helper.loadIndex('test-index'); + + expect(mockLoadIndex).toHaveBeenCalledWith('test-index', undefined); + expect(result).toBe(mockResult); + }); + + it('should pass options to MossClient.loadIndex', async () => { + const mockResult = 'test-index'; + mockLoadIndex.mockResolvedValue(mockResult); + const options = { autoRefresh: true, pollingIntervalInSeconds: 300 }; + + await helper.loadIndex('test-index', options); + + expect(mockLoadIndex).toHaveBeenCalledWith('test-index', options); + }); + }); + + describe('getJobStatus', () => { + it('should call MossClient.getJobStatus and return formatted response', async () => { + const mockStatus = { + jobId: 'job-123', + status: 'completed', + progress: 100, + currentPhase: undefined, + error: undefined, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:05:00Z' + }; + mockGetJobStatus.mockResolvedValue(mockStatus); + + const result = await helper.getJobStatus('job-123'); + + expect(mockGetJobStatus).toHaveBeenCalledWith('job-123'); + expect(result).toEqual({ + status: 'completed', + progress: 100, + currentPhase: undefined, + error: undefined + }); + }); + }); + + describe('close', () => { + it('should call MossClient.close', () => { + helper.close(); + expect(mockClose).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file From be97b8a6a4a3ae0b877e1b74cc40c0b956309f73 Mon Sep 17 00:00:00 2001 From: Manish Tiwari Date: Tue, 12 May 2026 04:18:00 +0530 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- examples/cookbook/n8n-integration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/n8n-integration/README.md b/examples/cookbook/n8n-integration/README.md index 36b0ab2..67c77c7 100644 --- a/examples/cookbook/n8n-integration/README.md +++ b/examples/cookbook/n8n-integration/README.md @@ -49,7 +49,7 @@ const results = await helper.query('my-index', 'hello', { topK: 5 }); await helper.deleteDocs('my-index', ['1']); // Clean up -helper.dispose(); +helper.close(); ``` ### Option 2: Direct HTTP Requests From 6a9066ed3ff084b3d5cbe08cc8f28c83ec9f40e1 Mon Sep 17 00:00:00 2001 From: PredictiveManish Date: Wed, 20 May 2026 01:54:58 +0530 Subject: [PATCH 4/4] Added .env, updated README, json updates --- .../cookbook/n8n-integration/.env.example | 8 ++++ examples/cookbook/n8n-integration/README.md | 36 ++++++++++++---- .../n8n-integration/moss-n8n-helper.ts | 41 ++++++++++++------- .../n8n-integration/n8n-moss-workflow.json | 2 +- 4 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 examples/cookbook/n8n-integration/.env.example diff --git a/examples/cookbook/n8n-integration/.env.example b/examples/cookbook/n8n-integration/.env.example new file mode 100644 index 0000000..47f030b --- /dev/null +++ b/examples/cookbook/n8n-integration/.env.example @@ -0,0 +1,8 @@ +# Moss Project Credentials +# Replace these values with your actual Moss project ID and project key +MOSS_PROJECT_ID=your-project-id-here +MOSS_PROJECT_KEY=your-project-key-here + +# Optional: N8N specific configurations (if needed) +# N8N_HOST=http://localhost:5678 +# N8N_API_KEY=your-n8n-api-key \ No newline at end of file diff --git a/examples/cookbook/n8n-integration/README.md b/examples/cookbook/n8n-integration/README.md index 36b0ab2..cf21ada 100644 --- a/examples/cookbook/n8n-integration/README.md +++ b/examples/cookbook/n8n-integration/README.md @@ -15,6 +15,9 @@ The integration exposes Moss's core operations so any n8n workflow can incorpora 1. `moss-n8n-helper.ts` - A TypeScript wrapper around the Moss SDK optimized for n8n usage 2. `n8n-moss-workflow.json` - An example n8n workflow demonstrating the four core operations +3. `.env.example` - Example environment variables for Moss project credentials +4. `test_moss_n8n_helper.ts` - Unit tests for the helper + ## Usage @@ -37,25 +40,37 @@ const createResult = await helper.createIndex('my-index', [ { id: '1', text: 'Hello world', metadata: { source: 'example' } } ]); -// Add documents -await helper.addDocs('my-index', [ +// Check creation status (optional) +const createStatus = await helper.getJobStatus(createResult.jobId); + +// Add docs +const addResult = await helper.addDocs('my-index', [ { id: '2', text: 'Another document', metadata: { source: 'example' } } ]); -// Query the index +// Load index for fast local queries (recommended before querying) +await helper.loadIndex('my-index'); + +// Query const results = await helper.query('my-index', 'hello', { topK: 5 }); -// Delete documents -await helper.deleteDocs('my-index', ['1']); +// Delete docs +const deleteResult = await helper.deleteDocs('my-index', ['1']); // Clean up -helper.dispose(); +helper.close(); ``` ### Option 2: Direct HTTP Requests If you prefer not to use the helper, you can call Moss's public REST endpoints directly using n8n's HTTP Request nodes. Refer to the [Moss API documentation](https://docs.moss.dev) for endpoint details. +## Environment Setup + +1. Copy `.env.example` to `.env` +2. Replace the placeholder values with your actual Moss project ID and project key +3. Never commit your actual `.env` file to version control + ## Example Workflow The included `n8n-moss-workflow.json` demonstrates a complete workflow: @@ -93,15 +108,20 @@ Use the Moss query operation within AI agent chains to retrieve relevant context ```bash npm install @moss-dev/moss ``` -3. Import the helper into your n8n Function nodes as shown above +3. Set up your environment variables using the `.env.example` file +4. Import the helper into your n8n Function nodes as shown above ## Development To modify the helper: 1. Edit `moss-n8n-helper.ts` -2. Rebuild if necessary (though TypeScript works directly in n8n Function nodes) +2. Run tests with: `npm test` (if you have a test setup) 3. Test with your n8n instance +## Demo + +See `DEMO_SCRIPT.md` for a script to create a 1-minute demonstration video showing the integration in action. + ## License BSD-2-Clause - See [LICENSE](../LICENSE) for details. \ No newline at end of file diff --git a/examples/cookbook/n8n-integration/moss-n8n-helper.ts b/examples/cookbook/n8n-integration/moss-n8n-helper.ts index 56b175f..6b6a60d 100644 --- a/examples/cookbook/n8n-integration/moss-n8n-helper.ts +++ b/examples/cookbook/n8n-integration/moss-n8n-helper.ts @@ -1,5 +1,5 @@ import { MossClient } from '@moss-dev/moss'; -import type { MutationResult, JobStatusResponse } from '@moss-dev/moss-core'; + // Moss N8N Helper - Wrapper around Moss SDK for use in n8n workflows // This helper provides easy-to-use functions for the four core Moss operations: @@ -7,6 +7,12 @@ import type { MutationResult, JobStatusResponse } from '@moss-dev/moss-core'; // - addDocs: Upsert documents into an index // - deleteDocs: Remove documents by ID // - query: Run semantic/hybrid search queries +type MossDocumentInput = { + id: string; + text: string; + metadata?: Record; + embedding?: number[]; +}; /** * MossN8NHelper - Wrapper for Moss SDK optimized for n8n usage @@ -30,14 +36,11 @@ export class MossN8NHelper { * @param options - Optional configuration (modelId, onProgress callback) * @returns Promise resolving to the creation result with job info */ + + async createIndex( indexName: string, - docs: Array<{ - id: string; - text: string; - metadata?: Record; - embedding?: number[]; - }>, + docs: MossDocumentInput[], options?: { modelId?: string; onProgress?: (jobProgress: { status: string; progress: number; currentPhase?: string }) => void; @@ -52,7 +55,9 @@ export class MossN8NHelper { id: doc.id, text: doc.text, ...(doc.metadata && { metadata: doc.metadata }), - ...(doc.embedding && { embedding: doc.embedding }) + ...((doc.embedding?.length ?? 0) > 0 && { + embedding: doc.embedding + }) })); // Call the SDK method which returns MutationResult @@ -93,11 +98,18 @@ export class MossN8NHelper { docCount: number; }> { // Convert to Moss DocumentInfo format - const mossDocs = docs.map(doc => ({ + const mossDocs = docs.map((doc: { + id: string; + text: string; + metadata?: Record; + embedding?: number[]; + }) => ({ id: doc.id, text: doc.text, ...(doc.metadata && { metadata: doc.metadata }), - ...(doc.embedding && { embedding: doc.embedding }) + ...((doc.embedding?.length ?? 0) > 0 && { + embedding: doc.embedding + }) })); // Call the SDK method which returns MutationResult @@ -207,18 +219,17 @@ export class MossN8NHelper { status: status.status, progress: status.progress, currentPhase: status.currentPhase?.toString(), - error: status.error + error: status.error ?? undefined }; } /** * Close the client and release resources - * Note: MossClient doesn't have a dispose() method, but we can clean up references + * Note: MossClient doesn't expose a public close()/dispose() API, so this is a no-op. + * The helper remains usable after calling close(). */ close(): void { - // In JavaScript, we just nullify the reference to allow garbage collection - // The actual cleanup happens internally in the MossClient - (this as any).client = null; + // No-op: do not invalidate `this.client` by nulling it out. } } diff --git a/examples/cookbook/n8n-integration/n8n-moss-workflow.json b/examples/cookbook/n8n-integration/n8n-moss-workflow.json index d13b73d..d95cf1e 100644 --- a/examples/cookbook/n8n-integration/n8n-moss-workflow.json +++ b/examples/cookbook/n8n-integration/n8n-moss-workflow.json @@ -23,7 +23,7 @@ }, { "parameters": { - "functionCode": "// Initialize Moss N8N Helper\n// Replace with your actual project ID and key\nconst projectId = \"your-project-id\";\nconst projectKey = \"your-project-key\";\n\n// This would normally be done in a Function node\n// For demo purposes, we're just returning the config\nreturn [{\n projectId,\n projectKey\n}];" + "functionCode": "// Initialize Moss N8N Helper\n// Read credentials from n8n environment variables instead of hardcoding them in the workflow\nconst projectId = process.env.MOSS_PROJECT_ID;\nconst projectKey = process.env.MOSS_PROJECT_KEY;\n\nif (!projectId || !projectKey) {\n throw new Error('Missing Moss configuration. Set MOSS_PROJECT_ID and MOSS_PROJECT_KEY as environment variables in n8n.');\n}\n\n// This would normally be done in a Function node\n// For demo purposes, we're just returning the config\nreturn [{\n projectId,\n projectKey\n}];" }, "name": "Moss Config", "type": "n8n-nodes-base.function",