diff --git a/common/utils/config_field_types.py b/common/utils/config_field_types.py index 5fee4d9..7fd8143 100644 --- a/common/utils/config_field_types.py +++ b/common/utils/config_field_types.py @@ -660,3 +660,48 @@ def json_to_pattern_list(json_content: str) -> list: return patterns except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON format: {e}") + + +def normalize_json_field(value: str | dict | None, field_name: str = "JSON field") -> dict: + """ + Normalize a JSON field value to ensure it's a valid dictionary. + + Args: + value: The input value which can be: + - None (will return empty dict) + - String (will be parsed as JSON) + - Dict (will be validated and returned) + field_name: Name of the field for error messages + + Returns: + dict: The normalized JSON data as a Python dictionary + + Raises: + ValueError: If the input string is not valid JSON or the input dict contains invalid types + """ + # Handle None case + if value is None: + return {} + + # Handle dictionary case + if isinstance(value, dict): + try: + # Validate all values are JSON serializable + import json + json.dumps(value) + return value + except TypeError as e: + raise ValueError(f"{field_name} contains invalid types: {str(e)}") + + # Handle string case + if isinstance(value, str): + if not value.strip(): + return {} + + try: + import json + return json.loads(value) + except json.JSONDecodeError as e: + raise ValueError(f"{field_name} contains invalid JSON: {str(e)}") + + raise ValueError(f"{field_name} must be a string, dictionary, or None (got {type(value)})") diff --git a/common/utils/document_utils.py b/common/utils/document_utils.py index c97b552..51a663c 100644 --- a/common/utils/document_utils.py +++ b/common/utils/document_utils.py @@ -11,6 +11,8 @@ from flask_security import current_user import requests from urllib.parse import urlparse, unquote, urlunparse import os + +from .config_field_types import normalize_json_field from .eveai_exceptions import (EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType, EveAIInvalidCatalog, EveAIInvalidDocument, EveAIInvalidDocumentVersion, EveAIException) from ..models.user import Tenant @@ -88,10 +90,10 @@ def create_version_for_document(document, tenant_id, url, sub_file_type, langua new_doc_vers.user_context = user_context if user_metadata != '' and user_metadata is not None: - new_doc_vers.user_metadata = user_metadata + new_doc_vers.user_metadata = normalize_json_field(user_metadata, "user_metadata") if catalog_properties != '' and catalog_properties is not None: - new_doc_vers.catalog_properties = catalog_properties + new_doc_vers.catalog_properties = normalize_json_field(catalog_properties, "catalog_properties") if sub_file_type != '': new_doc_vers.sub_file_type = sub_file_type @@ -262,7 +264,8 @@ def edit_document_version(tenant_id, version_id, user_context, catalog_propertie if not doc_vers: raise EveAIInvalidDocumentVersion(tenant_id, version_id) doc_vers.user_context = user_context - doc_vers.catalog_properties = catalog_properties + doc_vers.catalog_properties = normalize_json_field(catalog_properties, "catalog_properties") + update_logging_information(doc_vers, dt.now(tz.utc)) try: @@ -319,6 +322,56 @@ def refresh_document_with_info(doc_id, tenant_id, api_input): return new_doc_vers, task.id +def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: bytes, api_input: dict) -> tuple: + """ + Refresh document with new content + + Args: + doc_id: Document ID + tenant_id: Tenant ID + file_content: New file content + api_input: Additional document information + + Returns: + Tuple of (new_version, task_id) + """ + doc = Document.query.get(doc_id) + if not doc: + raise EveAIInvalidDocument(tenant_id, doc_id) + + old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first() + + # Create new version with same file type as original + extension = old_doc_vers.file_type + + new_doc_vers = create_version_for_document( + doc, tenant_id, + '', # No URL for content-based updates + old_doc_vers.sub_file_type, + api_input.get('language', old_doc_vers.language), + api_input.get('user_context', old_doc_vers.user_context), + api_input.get('user_metadata', old_doc_vers.user_metadata), + api_input.get('catalog_properties', old_doc_vers.catalog_properties), + ) + + try: + db.session.add(new_doc_vers) + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + return None, str(e) + + # Upload new content + upload_file_for_version(new_doc_vers, file_content, extension, tenant_id) + + # Start embedding task + task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id], queue='embeddings') + current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} ' + f'with task id: {task.id}.') + + return new_doc_vers, task.id + + # Update the existing refresh_document function to use the new refresh_document_with_info def refresh_document(doc_id, tenant_id): current_app.logger.info(f'Refreshing document {doc_id}') @@ -388,6 +441,10 @@ def lookup_document(tenant_id: int, lookup_criteria: dict, metadata_type: str) - for key, value in lookup_criteria.items(): query = query.filter(metadata_field[key].astext == str(value)) + # Log the final SQL query + current_app.logger.debug( + f"Final SQL query: {query.statement.compile(compile_kwargs={'literal_binds': True})}") + # Get first result result = query.first() @@ -411,55 +468,3 @@ def lookup_document(tenant_id: int, lookup_criteria: dict, metadata_type: str) - "Error during document lookup", status_code=500 ) - - -# Add to common/utils/document_utils.py - -def refresh_document_with_content(doc_id: int, tenant_id: int, file_content: bytes, api_input: dict) -> tuple: - """ - Refresh document with new content - - Args: - doc_id: Document ID - tenant_id: Tenant ID - file_content: New file content - api_input: Additional document information - - Returns: - Tuple of (new_version, task_id) - """ - doc = Document.query.get(doc_id) - if not doc: - raise EveAIInvalidDocument(tenant_id, doc_id) - - old_doc_vers = DocumentVersion.query.filter_by(doc_id=doc_id).order_by(desc(DocumentVersion.id)).first() - - # Create new version with same file type as original - extension = old_doc_vers.file_type - - new_doc_vers = create_version_for_document( - doc, tenant_id, - '', # No URL for content-based updates - old_doc_vers.sub_file_type, - api_input.get('language', old_doc_vers.language), - api_input.get('user_context', old_doc_vers.user_context), - api_input.get('user_metadata', old_doc_vers.user_metadata), - api_input.get('catalog_properties', old_doc_vers.catalog_properties), - ) - - try: - db.session.add(new_doc_vers) - db.session.commit() - except SQLAlchemyError as e: - db.session.rollback() - return None, str(e) - - # Upload new content - upload_file_for_version(new_doc_vers, file_content, extension, tenant_id) - - # Start embedding task - task = current_celery.send_task('create_embeddings', args=[tenant_id, new_doc_vers.id], queue='embeddings') - current_app.logger.info(f'Embedding creation started for document {doc_id} on version {new_doc_vers.id} ' - f'with task id: {task.id}.') - - return new_doc_vers, task.id diff --git a/eveai_api/__init__.py b/eveai_api/__init__.py index 5e85bd4..2ce019d 100644 --- a/eveai_api/__init__.py +++ b/eveai_api/__init__.py @@ -1,6 +1,6 @@ import traceback -from flask import Flask, jsonify, request +from flask import Flask, jsonify, request, redirect from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request from sqlalchemy.exc import SQLAlchemyError from werkzeug.exceptions import HTTPException @@ -103,14 +103,19 @@ def create_app(config_file=None): @app.route('/api/v1') def swagger(): - return api_rest.render_doc() + return redirect('/api/v1/') return app def register_extensions(app): db.init_app(app) - api_rest.init_app(app, title='EveAI API', version='1.0', description='EveAI API') + api_rest.init_app(app, + title='EveAI API', + version='1.0', + description='EveAI API', + doc='/api/v1/', + prefix='/api/v1'), jwt.init_app(app) minio_client.init_app(app) simple_encryption.init_app(app) diff --git a/eveai_api/api/document_api.py b/eveai_api/api/document_api.py index c6f8b82..47f4ac4 100644 --- a/eveai_api/api/document_api.py +++ b/eveai_api/api/document_api.py @@ -1,14 +1,18 @@ import io import json from datetime import datetime +from typing import Tuple, Any import pytz import requests from flask import current_app, request from flask_restx import Namespace, Resource, fields, reqparse from flask_jwt_extended import jwt_required, get_jwt_identity +from sqlalchemy import desc from werkzeug.datastructures import FileStorage from werkzeug.utils import secure_filename + +from common.models.document import DocumentVersion from common.utils.document_utils import ( create_document_stack, process_url, start_embedding_task, EveAIInvalidLanguageException, EveAIDoubleURLException, EveAIUnsupportedFileType, @@ -37,7 +41,8 @@ document_ns = Namespace('documents', description='Document related operations') # Define models for request parsing and response serialization upload_parser = reqparse.RequestParser() -upload_parser.add_argument('catalog_id', location='form', type=int, required=True, help='The catalog to add the file to') +upload_parser.add_argument('catalog_id', location='form', type=int, required=True, + help='The catalog to add the file to') upload_parser.add_argument('file', location='files', type=FileStorage, required=True, help='The file to upload') upload_parser.add_argument('name', location='form', type=str, required=False, help='Name of the document') upload_parser.add_argument('language', location='form', type=str, required=True, help='Language of the document') @@ -69,7 +74,11 @@ class AddDocument(Resource): @document_ns.response(500, 'Internal Server Error') def post(self): """ - Add a new document by providing the content of a file (Multipart/form-data). + Upload a new document to EveAI by directly providing the file content. + + This endpoint accepts multipart/form-data with the file content and metadata. It processes + the file, creates a new document in the specified catalog, and initiates the embedding + process. """ tenant_id = get_jwt_identity() current_app.logger.info(f'Adding document for tenant {tenant_id}') @@ -126,10 +135,11 @@ add_document_through_url = document_ns.model('AddDocumentThroughURL', { 'valid_from': fields.String(required=False, description='Valid from date for the document'), 'user_metadata': fields.String(required=False, description='User metadata for the document'), 'system_metadata': fields.String(required=False, description='System metadata for the document'), - 'catalog_properties': fields.String(required=False, description='The catalog configuration to be passed along (JSON ' - 'format). Validity is against catalog requirements ' - 'is not checked, and is the responsibility of the ' - 'calling client.'), + 'catalog_properties': fields.String(required=False, + description='The catalog configuration to be passed along (JSON ' + 'format). Validity is against catalog requirements ' + 'is not checked, and is the responsibility of the ' + 'calling client.'), }) add_document_through_url_response = document_ns.model('AddDocumentThroughURLResponse', { @@ -139,6 +149,7 @@ add_document_through_url_response = document_ns.model('AddDocumentThroughURLResp 'task_id': fields.String(description='ID of the embedding task') }) + @document_ns.route('/add_document_through_url') class AddDocumentThroughURL(Resource): @jwt_required() @@ -150,8 +161,10 @@ class AddDocumentThroughURL(Resource): @document_ns.response(500, 'Internal Server Error') def post(self): """ - Add a new document using a URL. The URL can be temporary, and will not be stored. - Mainly used for passing temporary URLs like used in e.g. Zapier + Add a new document to EveAI using a temporary URL. + + This endpoint is primarily used for integration with services that provide temporary URLs + (like Zapier). The URL content is downloaded and processed as a new document. """ tenant_id = get_jwt_identity() current_app.logger.info(f'Adding document through url for tenant {tenant_id}') @@ -164,29 +177,19 @@ class AddDocumentThroughURL(Resource): raise try: - # Step 1: Download from stashed URL - stashed_url = args['temp_url'] - current_app.logger.info(f"Downloading stashed file from URL: {stashed_url}") - response = requests.get(stashed_url, stream=True) - response.raise_for_status() - - hydration_url = response.text.strip() - current_app.logger.info(f"Downloading actual file from URL: {hydration_url}") - # Step 2: Download from hydration URL - actual_file_response = requests.get(hydration_url, stream=True) - actual_file_response.raise_for_status() - hydrated_file_content = actual_file_response.content + user_metadata = json.loads(args.get('user_metadata', '{}')) + actual_file_content, actual_file_content_type = download_file_content(args['temp_url'], user_metadata) # Get filename from URL or use provided name filename = secure_filename(args.get('name')) extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' # Create FileStorage object from downloaded content - file_content = io.BytesIO(hydrated_file_content) + file_content = io.BytesIO(actual_file_content) file = FileStorage( stream=file_content, filename=filename, - content_type=response.headers.get('content-type', 'application/octet-stream') + content_type=actual_file_content_type ) current_app.logger.info(f"Successfully downloaded file: {filename}") @@ -233,10 +236,11 @@ add_url_model = document_ns.model('AddURL', { 'valid_from': fields.String(required=False, description='Valid from date for the document'), 'user_metadata': fields.String(required=False, description='User metadata for the document'), 'system_metadata': fields.String(required=False, description='System metadata for the document'), - 'catalog_properties': fields.String(required=False, description='The catalog configuration to be passed along (JSON ' - 'format). Validity is against catalog requirements ' - 'is not checked, and is the responsibility of the ' - 'calling client.'), + 'catalog_properties': fields.String(required=False, + description='The catalog configuration to be passed along (JSON ' + 'format). Validity is against catalog requirements ' + 'is not checked, and is the responsibility of the ' + 'calling client.'), }) add_url_response = document_ns.model('AddURLResponse', { @@ -257,8 +261,10 @@ class AddURL(Resource): @document_ns.response(500, 'Internal Server Error') def post(self): """ - Add a new document from URL. The URL in this case is stored and can be used to refresh the document. - As a consequence, this must be a permanent and accessible URL. + Add a new document to EveAI from a permanent URL. + + This endpoint is used for URLs that will remain accessible. The URL is stored and can + be used to refresh the document's content later. """ tenant_id = get_jwt_identity() current_app.logger.info(f'Adding document from URL for tenant {tenant_id}') @@ -383,7 +389,8 @@ class DocumentVersionResource(Resource): """Edit a document version""" data = request.json tenant_id = get_jwt_identity() - updated_version, error = edit_document_version(tenant_id, version_id, data['user_context'], data.get('catalog_properties')) + updated_version, error = edit_document_version(tenant_id, version_id, data['user_context'], + data.get('catalog_properties')) if updated_version: return {'message': f'Document Version {updated_version.id} updated successfully'}, 200 else: @@ -518,58 +525,87 @@ class DocumentLookup(Resource): return {'message': f'Missing required field: {str(e)}'}, 400 -refresh_content_model = document_ns.model('RefreshDocumentContent', { - 'file_content': fields.Raw(required=True, description='The new file content'), +refresh_url_model = document_ns.model('RefreshDocumentThroughURL', { + 'temp_url': fields.String(required=True, description='Temporary URL of the updated document content'), 'language': fields.String(required=False, description='Language of the document'), 'user_context': fields.String(required=False, description='User context for the document'), 'user_metadata': fields.Raw(required=False, description='Custom metadata fields'), 'catalog_properties': fields.Raw(required=False, description='Catalog-specific properties'), - 'trigger_service': fields.String(required=False, description='Service that triggered the update') }) -@document_ns.route('//refresh_content') -class RefreshDocumentContent(Resource): +@document_ns.route('//refresh_through_url') +class RefreshDocumentThroughURL(Resource): @jwt_required() @requires_service('DOCAPI') - @document_ns.expect(refresh_content_model) + @document_ns.expect(refresh_url_model) @document_ns.response(200, 'Document refreshed successfully') def post(self, document_id): - """Refresh a document with new content""" + """Refresh a document using content from a URL""" tenant_id = get_jwt_identity() + current_app.logger.info(f'Refreshing document {document_id} through URL for tenant {tenant_id}') + try: - data = request.json - file_content = data['file_content'] + # Get filename from the existing version + old_doc_vers = (DocumentVersion.query.filter_by(doc_id=document_id). + order_by(desc(DocumentVersion.id)).first()) + filename = f"{old_doc_vers.id}.{old_doc_vers.file_type}" - # Build user_metadata by merging: - # 1. Existing metadata (if any) - # 2. New metadata from request - # 3. Zapier-specific fields - user_metadata = data.get('user_metadata', {}) - user_metadata.update({ - 'source': 'zapier', - 'trigger_service': data.get('trigger_service') - }) - data['user_metadata'] = user_metadata + args = request.json + user_metadata = json.loads(args.get('user_metadata', '{}')) - # Keep catalog_properties separate - if 'catalog_properties' in data: - # We could add validation here against catalog configuration - data['catalog_properties'] = data['catalog_properties'] + try: + actual_file_content, actual_file_content_type = download_file_content(args['temp_url'], user_metadata) + file_content = io.BytesIO(actual_file_content) + file = FileStorage( + stream=file_content, + filename=filename, + content_type=actual_file_content_type + ) - new_version, task_id = refresh_document_with_content( - document_id, - tenant_id, - file_content, - data - ) + new_version, task_id = refresh_document_with_content( + document_id, + tenant_id, + actual_file_content, + args + ) - return { - 'message': f'Document refreshed successfully. New version: {new_version.id}. Task ID: {task_id}', - 'document_id': document_id, - 'document_version_id': new_version.id, - 'task_id': task_id - }, 200 + return { + 'message': f'Document refreshed successfully. New version: {new_version.id}. Task ID: {task_id}', + 'document_id': document_id, + 'document_version_id': new_version.id, + 'task_id': task_id + }, 200 + + except requests.RequestException as e: + current_app.logger.error(f"Error downloading file: {str(e)}") + return {'message': f'Error downloading file: {str(e)}'}, 422 except EveAIException as e: return e.to_dict(), e.status_code + + +def download_file_content(url: str, user_metadata: dict) -> tuple[Any, Any]: + if user_metadata and 'service' in user_metadata and 'Zapier' in user_metadata['service']: + # Zapier uses a system of Stashed URLs + # Step 1: Download from stashed URL + stashed_url = url + current_app.logger.info(f"Downloading stashed file from URL: {stashed_url}") + response = requests.get(stashed_url, stream=True) + response.raise_for_status() + + hydration_url = response.text.strip() + current_app.logger.info(f"Downloading actual file from URL: {hydration_url}") + # Step 2: Download from hydration URL + actual_file_response = requests.get(hydration_url, stream=True) + actual_file_response.raise_for_status() + actual_file_content = actual_file_response.content + else: + actual_url = url + actual_file_response = requests.get(actual_url, stream=True) + actual_file_response.raise_for_status() + actual_file_content = actual_file_response.content + + actual_file_content_type = actual_file_response.headers.get('content-type', 'application/octet-stream') + + return actual_file_content, actual_file_content_type diff --git a/eveai_workers/tasks.py b/eveai_workers/tasks.py index 2ac5166..05de7f5 100644 --- a/eveai_workers/tasks.py +++ b/eveai_workers/tasks.py @@ -35,6 +35,7 @@ def ping(): @current_celery.task(name='create_embeddings', queue='embeddings') def create_embeddings(tenant_id, document_version_id): + document_version = None try: # Retrieve Tenant for which we are processing tenant = Tenant.query.get(tenant_id) @@ -66,6 +67,8 @@ def create_embeddings(tenant_id, document_version_id): f'for badly configured document version {document_version_id} ' f'for tenant {tenant_id}, ' f'error: {e}') + if document_version: + document_version.processing_error = str(e) raise # BusinessEvent creates a context, which is why we need to use it with a with block diff --git a/integrations/Zapier/eveai_integration/creates/add_document.js b/integrations/Zapier/eveai_integration/creates/add_document.js index 0669a5b..9d99ce0 100644 --- a/integrations/Zapier/eveai_integration/creates/add_document.js +++ b/integrations/Zapier/eveai_integration/creates/add_document.js @@ -138,7 +138,6 @@ module.exports = { // Get the file URL from Zapier const tempFileUrl = await z.stashFile(bundle.inputData.file); - // Log the temporary URL for debugging z.console.log('Temporary URL created:', tempFileUrl); // Create request data as an object @@ -190,73 +189,5 @@ module.exports = { throw error; } } - // perform: async (z, bundle) => { - // try { - // z.console.log("Starting New Log Trace for add_document") - // z.console.log("=======================================") - // - // // Prepare base metadata - // const baseMetadata = { - // service: bundle.inputData.metadata_service || 'Zapier', - // source: bundle.inputData.metadata_source, - // unique_id: bundle.inputData.metadata_unique_id, - // unique_url: bundle.inputData.metadata_unique_url, - // }; - // - // // If there's additional metadata, merge it with the base metadata - // if (bundle.inputData.additional_metadata) { - // Object.assign(baseMetadata, bundle.inputData.additional_metadata); - // } - // - // const requestData = { - // - // catalog_id: bundle.inputData.catalog_id, - // language: bundle.inputData.language, - // - // // Add optional fields if they exist - // name: bundle.inputData.name || undefined, - // user_context: bundle.inputData.user_context || undefined, - // valid_from: bundle.inputData.valid_from || undefined, - // user_metadata: JSON.stringify(baseMetadata), - // catalog_properties: JSON.stringify(bundle.inputData.catalog_properties) || undefined, - // file: z.stashFile(bundle.inputData.file), - // } - // - // // Make request to your API - // const response = await z.request({ - // url: 'https://evie.askeveai.com/api/api/v1/documents/add_document', - // method: 'POST', - // body: requestData, - // headers: { - // 'Authorization': `Bearer ${bundle.authData.access_token}`, - // 'Content-Type': 'multipart/form-data', - // }, - // }); - // - // // Log the response for debugging - // z.console.log('API Response:', { - // status: response.status, - // body: response.data - // }); - // // Return the parsed response - // return response.json; - // } catch (error) { - // // Enhanced error logging - // z.console.error('Error details:', { - // message: error.message, - // response: error.response ? { - // status: error.response.status, - // headers: error.response.headers, - // data: error.response.data - // } : 'No response', - // request: error.request ? { - // method: error.request.method, - // url: error.request.url, - // headers: error.request.headers - // } : 'No request' - // }); - // throw error; - // } - // } } }; diff --git a/integrations/Zapier/eveai_integration/creates/refresh_document.js b/integrations/Zapier/eveai_integration/creates/refresh_document.js new file mode 100644 index 0000000..bca5d83 --- /dev/null +++ b/integrations/Zapier/eveai_integration/creates/refresh_document.js @@ -0,0 +1,160 @@ +const EveAIApiClient = require('../api_client'); + +module.exports = { + display: { + description: "Refresh an existing document in Evie's Library with new content", + hidden: false, + label: 'Refresh Document in Evie', + }, + key: 'refresh_document', + noun: 'Document', + operation: { + inputFields: [ + { + key: 'file', + label: 'The Updated File', + type: 'file', + helpText: "The new content to replace the existing document", + required: true, + }, + { + key: 'language', + label: 'Document Language', + type: 'string', + default: 'en', + helpText: 'Two-letter-code of the language the document is written in.', + required: true, + }, + { + key: 'user_context', + label: 'User Context', + type: 'text', + helpText: + 'Contextual information you want to add to the Document. If you have structured information to be shared, you can better add this information to the User Metadata, which allows for json to be uploaded.', + required: false, + list: false, + altersDynamicFields: false, + }, + { + key: 'metadata_service', + label: 'Service', + type: 'string', + default: 'Zapier', + helpText: "By default we use 'Zapier' as service name. However, if you need to change that to e.g. give an indication of the Zapier flow, you can change this value.", + required: true, + }, + { + key: 'metadata_source', + label: 'Source App', + type: 'string', + helpText: "The source app of the document's origin. e.g. 'Dropbox' if the document is provided through Dropbox, or 'Google Docs' if that happens to be the origin of the document.", + required: false, + }, + { + key: 'metadata_unique_id', + label: 'Unique ID', + type: 'string', + helpText: 'An unique identifier, provided by the source system, if that is available.', + required: false, + }, + { + key: 'metadata_unique_url', + label: 'Unique URL', + type: 'string', + helpText: "A unique URL that is provided by the source system, if that's available", + required: false, + }, + { + key: 'additional_metadata', + label: 'Additional Metadata', + helpText: "Extra metadata you'd like to add to the document", + dict: true, + required: false, + altersDynamicFields: false, + }, + { + key: 'catalog_properties', + label: 'Catalog Properties', + helpText: + 'Depending on the Catalog ID provided, you can add the required key-value pairs here.', + dict: true, + required: false, + altersDynamicFields: false, + }, + ], + perform: async (z, bundle) => { + try { + z.console.log("Starting New Log Trace for refresh_document"); + z.console.log("==========================================="); + + const client = new EveAIApiClient(z, bundle); + + // Prepare base metadata + const baseMetadata = { + service: bundle.inputData.metadata_service || 'Zapier', + source: bundle.inputData.metadata_source || '', + unique_id: bundle.inputData.metadata_unique_id || '', + unique_url: bundle.inputData.metadata_unique_url || '', + }; + + // If there's additional metadata, merge it + if (bundle.inputData.additional_metadata) { + Object.assign(baseMetadata, bundle.inputData.additional_metadata); + } + + // First, lookup the document by unique_id + const lookupResponse = await client.make_request('POST', '/documents/lookup', { + lookup_criteria: { unique_id: bundle.inputData.metadata_unique_id }, + metadata_type: 'user_metadata' + }); + + const documentId = lookupResponse.document_id; + z.console.log("Found Document ID: ", documentId) + + // Get the temporary URL from Zapier's file storage + const tempFileUrl = await z.stashFile(bundle.inputData.file); + z.console.log('Temporary URL created:', tempFileUrl); + + // Prepare the refresh request + const requestData = { + temp_url: tempFileUrl, + language: bundle.inputData.language, + user_metadata: JSON.stringify(baseMetadata), + }; + + // Add user_context property if it exists + if (bundle.inputData.user_context) { + requestData.user_context = bundle.inputData.user_context; + } + + // Add catalog properties if they exist + if (bundle.inputData.catalog_properties) { + requestData.catalog_properties = JSON.stringify(bundle.inputData.catalog_properties); + } + + // Make the refresh request + return await client.make_request( + 'POST', + `/documents/${documentId}/refresh_through_url`, + requestData + ); + + } catch (error) { + z.console.error('Error details:', { + message: error.message, + response: error.response ? { + status: error.response.status, + headers: error.response.headers, + data: error.response.data + } : 'No response', + request: error.request ? { + method: error.request.method, + url: error.request.url, + headers: error.request.headers + } : 'No request' + }); + throw error; + } + }, + }, +}; \ No newline at end of file diff --git a/integrations/Zapier/eveai_integration/index.js b/integrations/Zapier/eveai_integration/index.js index 916e6f4..d4078c7 100644 --- a/integrations/Zapier/eveai_integration/index.js +++ b/integrations/Zapier/eveai_integration/index.js @@ -1,5 +1,6 @@ const authentication = require('./authentication'); const addDocument = require('./creates/add_document'); +const refreshDocument = require('./creates/refresh_document'); // Add this line module.exports = { // This is just shorthand to reference the installed dependencies you have. @@ -18,7 +19,8 @@ module.exports = { // If you want your creates to show up, you better include it here! creates: { - [addDocument.key]: addDocument + [addDocument.key]: addDocument, + [refreshDocument.key]: refreshDocument, }, resources: {}, diff --git a/integrations/Zapier/eveai_integration/package.json b/integrations/Zapier/eveai_integration/package.json index 2685e11..3840c07 100644 --- a/integrations/Zapier/eveai_integration/package.json +++ b/integrations/Zapier/eveai_integration/package.json @@ -1,13 +1,13 @@ { "name": "eveai_integration", - "version": "1.0.3", + "version": "1.0.5", "description": "", "main": "index.js", "scripts": { "test": "jest --testTimeout 10000" }, "dependencies": { - "zapier-platform-core": "15.19.0" + "zapier-platform-core": "16.0.0" }, "devDependencies": { "jest": "^29.6.0"