From 2de89ad718a89ee392d5a71277d9eacace35a950 Mon Sep 17 00:00:00 2001 From: repi Date: Tue, 10 Mar 2026 17:11:41 +0000 Subject: [PATCH] feat(tools): add REST query tools (sage_query, sage_read, sage_search) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus --- src/tools/__tests__/sage-query.test.ts | 199 ++++++++++++++++++++++ src/tools/__tests__/sage-read.test.ts | 160 ++++++++++++++++++ src/tools/__tests__/sage-search.test.ts | 211 ++++++++++++++++++++++++ src/tools/sage-query.ts | 46 ++++++ src/tools/sage-read.ts | 34 ++++ src/tools/sage-search.ts | 53 ++++++ 6 files changed, 703 insertions(+) create mode 100644 src/tools/__tests__/sage-query.test.ts create mode 100644 src/tools/__tests__/sage-read.test.ts create mode 100644 src/tools/__tests__/sage-search.test.ts create mode 100644 src/tools/sage-query.ts create mode 100644 src/tools/sage-read.ts create mode 100644 src/tools/sage-search.ts diff --git a/src/tools/__tests__/sage-query.test.ts b/src/tools/__tests__/sage-query.test.ts new file mode 100644 index 0000000..140d18c --- /dev/null +++ b/src/tools/__tests__/sage-query.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerQueryTool } from '../sage-query.js'; +import type { RestClient } from '../../clients/rest-client.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +function createMockRestClient(overrides: Partial = {}): RestClient { + return { + query: vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }), + read: vi.fn(), + listEntities: vi.fn(), + healthCheck: vi.fn(), + ...overrides, + } as unknown as RestClient; +} + +describe('registerQueryTool', () => { + let server: McpServer; + let registerToolSpy: ReturnType; + + beforeEach(() => { + server = new McpServer({ name: 'test-server', version: '1.0.0' }); + registerToolSpy = vi.spyOn(server, 'registerTool'); + }); + + it('registers sage_query tool with correct metadata', () => { + const restClient = createMockRestClient(); + registerQueryTool(server, restClient); + + expect(registerToolSpy).toHaveBeenCalledOnce(); + const [name, config] = registerToolSpy.mock.calls[0]; + expect(name).toBe('sage_query'); + expect(config).toMatchObject({ + description: expect.stringContaining('Query Sage X3'), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }); + }); + + it('has inputSchema with expected fields', () => { + const restClient = createMockRestClient(); + registerQueryTool(server, restClient); + + const config = registerToolSpy.mock.calls[0][1] as Record; + const schema = config.inputSchema as Record; + expect(schema).toHaveProperty('entity'); + expect(schema).toHaveProperty('representation'); + expect(schema).toHaveProperty('where'); + expect(schema).toHaveProperty('orderBy'); + expect(schema).toHaveProperty('count'); + expect(schema).toHaveProperty('nextUrl'); + expect(schema).toHaveProperty('select'); + }); + + describe('handler', () => { + async function callHandler( + restClient: RestClient, + args: Record = { entity: 'BPCUSTOMER' }, + ): Promise { + registerQueryTool(server, restClient); + const handler = registerToolSpy.mock.calls[0][2] as ( + args: Record, + ) => Promise; + return handler(args); + } + + it('returns paginated records on success', async () => { + const mockRecords = [ + { BPCNUM: 'C001', BPCNAM: 'Acme Corp' }, + { BPCNUM: 'C002', BPCNAM: 'Widget Inc' }, + ]; + const restClient = createMockRestClient({ + query: vi.fn().mockResolvedValue({ + records: mockRecords, + pagination: { returned: 2, hasMore: true, nextUrl: 'https://x3.example.com/next' }, + }), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBeUndefined(); + expect(result.content).toHaveLength(1); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.records).toEqual(mockRecords); + expect(parsed.pagination).toEqual({ + returned: 2, + hasMore: true, + nextUrl: 'https://x3.example.com/next', + }); + }); + + it('passes all query parameters to restClient.query', async () => { + const queryMock = vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }); + const restClient = createMockRestClient({ query: queryMock }); + + await callHandler(restClient, { + entity: 'SINVOICE', + representation: 'SINV$query', + where: "BPCNUM eq 'C001'", + orderBy: 'ACCDAT desc', + count: 50, + select: 'NUM,BPCNUM', + }); + + expect(queryMock).toHaveBeenCalledWith({ + entity: 'SINVOICE', + representation: 'SINV$query', + where: "BPCNUM eq 'C001'", + orderBy: 'ACCDAT desc', + count: 50, + nextUrl: undefined, + select: 'NUM,BPCNUM', + }); + }); + + it('passes nextUrl for pagination continuation', async () => { + const queryMock = vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }); + const restClient = createMockRestClient({ query: queryMock }); + + await callHandler(restClient, { + entity: 'BPCUSTOMER', + nextUrl: 'https://x3.example.com/api1/x3/erp/SEED/BPCUSTOMER?next=abc', + }); + + expect(queryMock).toHaveBeenCalledWith( + expect.objectContaining({ + nextUrl: 'https://x3.example.com/api1/x3/erp/SEED/BPCUSTOMER?next=abc', + }), + ); + }); + + it('returns empty records when no results found', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }), + }); + + const result = await callHandler(restClient); + + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.records).toEqual([]); + expect(parsed.pagination.returned).toBe(0); + expect(parsed.pagination.hasMore).toBe(false); + }); + + it('returns error with hint on authentication failure', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockRejectedValue(new Error('HTTP 401: Unauthorized')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('401'); + expect(text).toContain('Hint:'); + }); + + it('returns error with hint on timeout', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockRejectedValue(new Error('Request timeout')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('timeout'); + }); + + it('returns error with hint on connection failure', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('ECONNREFUSED'); + expect(text).toContain('Hint:'); + }); + }); +}); diff --git a/src/tools/__tests__/sage-read.test.ts b/src/tools/__tests__/sage-read.test.ts new file mode 100644 index 0000000..f976f76 --- /dev/null +++ b/src/tools/__tests__/sage-read.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerReadTool } from '../sage-read.js'; +import type { RestClient } from '../../clients/rest-client.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +function createMockRestClient(overrides: Partial = {}): RestClient { + return { + query: vi.fn(), + read: vi.fn().mockResolvedValue({ record: {} }), + listEntities: vi.fn(), + healthCheck: vi.fn(), + ...overrides, + } as unknown as RestClient; +} + +describe('registerReadTool', () => { + let server: McpServer; + let registerToolSpy: ReturnType; + + beforeEach(() => { + server = new McpServer({ name: 'test-server', version: '1.0.0' }); + registerToolSpy = vi.spyOn(server, 'registerTool'); + }); + + it('registers sage_read tool with correct metadata', () => { + const restClient = createMockRestClient(); + registerReadTool(server, restClient); + + expect(registerToolSpy).toHaveBeenCalledOnce(); + const [name, config] = registerToolSpy.mock.calls[0]; + expect(name).toBe('sage_read'); + expect(config).toMatchObject({ + description: expect.stringContaining('Read a single Sage X3 record'), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }); + }); + + it('has inputSchema with entity, key, and representation fields', () => { + const restClient = createMockRestClient(); + registerReadTool(server, restClient); + + const config = registerToolSpy.mock.calls[0][1] as Record; + const schema = config.inputSchema as Record; + expect(schema).toHaveProperty('entity'); + expect(schema).toHaveProperty('key'); + expect(schema).toHaveProperty('representation'); + }); + + describe('handler', () => { + async function callHandler( + restClient: RestClient, + args: Record = { entity: 'BPCUSTOMER', key: 'C001' }, + ): Promise { + registerReadTool(server, restClient); + const handler = registerToolSpy.mock.calls[0][2] as ( + args: Record, + ) => Promise; + return handler(args); + } + + it('returns full record on success', async () => { + const mockRecord = { + BPCNUM: 'C001', + BPCNAM: 'Acme Corp', + BPCADD: '123 Main St', + CRY: 'US', + }; + const restClient = createMockRestClient({ + read: vi.fn().mockResolvedValue({ record: mockRecord }), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBeUndefined(); + expect(result.content).toHaveLength(1); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.record).toEqual(mockRecord); + }); + + it('passes entity, key, and representation to restClient.read', async () => { + const readMock = vi.fn().mockResolvedValue({ record: {} }); + const restClient = createMockRestClient({ read: readMock }); + + await callHandler(restClient, { + entity: 'SINVOICE', + key: 'INV001', + representation: 'SINV$details', + }); + + expect(readMock).toHaveBeenCalledWith('SINVOICE', 'INV001', 'SINV$details'); + }); + + it('passes undefined representation when not provided', async () => { + const readMock = vi.fn().mockResolvedValue({ record: {} }); + const restClient = createMockRestClient({ read: readMock }); + + await callHandler(restClient, { entity: 'BPCUSTOMER', key: 'C001' }); + + expect(readMock).toHaveBeenCalledWith('BPCUSTOMER', 'C001', undefined); + }); + + it('returns error with not_found hint on 404', async () => { + const restClient = createMockRestClient({ + read: vi.fn().mockRejectedValue(new Error('HTTP 404: Not Found')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('404'); + expect(text).toContain('Hint:'); + expect(text).toContain('not found'); + }); + + it('returns error with auth hint on 401', async () => { + const restClient = createMockRestClient({ + read: vi.fn().mockRejectedValue(new Error('HTTP 401: Unauthorized')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('401'); + expect(text).toContain('Hint:'); + }); + + it('handles key with special characters', async () => { + const readMock = vi.fn().mockResolvedValue({ record: { NUM: 'INV/2024/001' } }); + const restClient = createMockRestClient({ read: readMock }); + + const result = await callHandler(restClient, { + entity: 'SINVOICE', + key: 'INV/2024/001', + }); + + expect(readMock).toHaveBeenCalledWith('SINVOICE', 'INV/2024/001', undefined); + expect(result.isError).toBeUndefined(); + }); + + it('returns minified JSON response', async () => { + const restClient = createMockRestClient({ + read: vi.fn().mockResolvedValue({ record: { A: 1 } }), + }); + + const result = await callHandler(restClient); + + const text = (result.content[0] as { text: string }).text; + expect(text).not.toContain('\n'); + expect(text).toBe('{"record":{"A":1}}'); + }); + }); +}); diff --git a/src/tools/__tests__/sage-search.test.ts b/src/tools/__tests__/sage-search.test.ts new file mode 100644 index 0000000..3491258 --- /dev/null +++ b/src/tools/__tests__/sage-search.test.ts @@ -0,0 +1,211 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerSearchTool, buildWhereClause } from '../sage-search.js'; +import type { RestClient } from '../../clients/rest-client.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +function createMockRestClient(overrides: Partial = {}): RestClient { + return { + query: vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }), + read: vi.fn(), + listEntities: vi.fn(), + healthCheck: vi.fn(), + ...overrides, + } as unknown as RestClient; +} + +describe('buildWhereClause', () => { + it('builds clause with default fields based on entity name', () => { + const result = buildWhereClause('BPCUSTOMER', 'acme'); + expect(result).toBe("(contains(BPCUSTOMERNUM,'acme') or contains(BPCUSTOMERNAM,'acme'))"); + }); + + it('builds clause with custom searchFields', () => { + const result = buildWhereClause('BPCUSTOMER', 'acme', ['BPCNAM', 'BPCNUM']); + expect(result).toBe("(contains(BPCNAM,'acme') or contains(BPCNUM,'acme'))"); + }); + + it('builds clause with single custom field (no parens wrapping)', () => { + const result = buildWhereClause('BPCUSTOMER', 'acme', ['BPCNAM']); + expect(result).toBe("contains(BPCNAM,'acme')"); + }); + + it('uses default fields when searchFields is empty array', () => { + const result = buildWhereClause('SINVOICE', 'INV001', []); + expect(result).toBe("(contains(SINVOICENUM,'INV001') or contains(SINVOICENAM,'INV001'))"); + }); + + it('handles search term with spaces', () => { + const result = buildWhereClause('BPCUSTOMER', 'Acme Corp', ['BPCNAM']); + expect(result).toBe("contains(BPCNAM,'Acme Corp')"); + }); + + it('builds clause with three fields', () => { + const result = buildWhereClause('ITMMASTER', 'widget', ['ITMREF', 'ITMDES', 'ITMDES2']); + expect(result).toBe( + "(contains(ITMREF,'widget') or contains(ITMDES,'widget') or contains(ITMDES2,'widget'))", + ); + }); +}); + +describe('registerSearchTool', () => { + let server: McpServer; + let registerToolSpy: ReturnType; + + beforeEach(() => { + server = new McpServer({ name: 'test-server', version: '1.0.0' }); + registerToolSpy = vi.spyOn(server, 'registerTool'); + }); + + it('registers sage_search tool with correct metadata', () => { + const restClient = createMockRestClient(); + registerSearchTool(server, restClient); + + expect(registerToolSpy).toHaveBeenCalledOnce(); + const [name, config] = registerToolSpy.mock.calls[0]; + expect(name).toBe('sage_search'); + expect(config).toMatchObject({ + description: expect.stringContaining('Search Sage X3'), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }); + }); + + it('has inputSchema with expected fields', () => { + const restClient = createMockRestClient(); + registerSearchTool(server, restClient); + + const config = registerToolSpy.mock.calls[0][1] as Record; + const schema = config.inputSchema as Record; + expect(schema).toHaveProperty('entity'); + expect(schema).toHaveProperty('searchTerm'); + expect(schema).toHaveProperty('searchFields'); + expect(schema).toHaveProperty('count'); + }); + + describe('handler', () => { + async function callHandler( + restClient: RestClient, + args: Record = { entity: 'BPCUSTOMER', searchTerm: 'acme' }, + ): Promise { + registerSearchTool(server, restClient); + const handler = registerToolSpy.mock.calls[0][2] as ( + args: Record, + ) => Promise; + return handler(args); + } + + it('calls restClient.query with built where clause using default fields', async () => { + const queryMock = vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }); + const restClient = createMockRestClient({ query: queryMock }); + + await callHandler(restClient, { entity: 'BPCUSTOMER', searchTerm: 'acme' }); + + expect(queryMock).toHaveBeenCalledWith({ + entity: 'BPCUSTOMER', + where: "(contains(BPCUSTOMERNUM,'acme') or contains(BPCUSTOMERNAM,'acme'))", + count: undefined, + }); + }); + + it('calls restClient.query with custom searchFields', async () => { + const queryMock = vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }); + const restClient = createMockRestClient({ query: queryMock }); + + await callHandler(restClient, { + entity: 'BPCUSTOMER', + searchTerm: 'acme', + searchFields: ['BPCNAM', 'BPCNUM'], + }); + + expect(queryMock).toHaveBeenCalledWith({ + entity: 'BPCUSTOMER', + where: "(contains(BPCNAM,'acme') or contains(BPCNUM,'acme'))", + count: undefined, + }); + }); + + it('passes count to restClient.query', async () => { + const queryMock = vi.fn().mockResolvedValue({ + records: [], + pagination: { returned: 0, hasMore: false }, + }); + const restClient = createMockRestClient({ query: queryMock }); + + await callHandler(restClient, { + entity: 'BPCUSTOMER', + searchTerm: 'acme', + count: 50, + }); + + expect(queryMock).toHaveBeenCalledWith( + expect.objectContaining({ count: 50 }), + ); + }); + + it('returns matching records on success', async () => { + const mockRecords = [{ BPCNUM: 'C001', BPCNAM: 'Acme Corp' }]; + const restClient = createMockRestClient({ + query: vi.fn().mockResolvedValue({ + records: mockRecords, + pagination: { returned: 1, hasMore: false }, + }), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.records).toEqual(mockRecords); + expect(parsed.pagination.returned).toBe(1); + }); + + it('returns empty results when no matches found', async () => { + const restClient = createMockRestClient(); + + const result = await callHandler(restClient); + + const parsed = JSON.parse((result.content[0] as { text: string }).text); + expect(parsed.records).toEqual([]); + expect(parsed.pagination.returned).toBe(0); + }); + + it('returns error with hint on failure', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockRejectedValue(new Error('HTTP 401: Unauthorized')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('401'); + expect(text).toContain('Hint:'); + }); + + it('returns error with hint on connection failure', async () => { + const restClient = createMockRestClient({ + query: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')), + }); + + const result = await callHandler(restClient); + + expect(result.isError).toBe(true); + const text = (result.content[0] as { text: string }).text; + expect(text).toContain('ECONNREFUSED'); + }); + }); +}); diff --git a/src/tools/sage-query.ts b/src/tools/sage-query.ts new file mode 100644 index 0000000..3cc7640 --- /dev/null +++ b/src/tools/sage-query.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { RestClient } from '../clients/rest-client.js'; +import { formatQueryResponse } from '../utils/response.js'; +import { formatToolError, classifyError, getErrorHint } from '../utils/errors.js'; + +export function registerQueryTool(server: McpServer, restClient: RestClient): void { + server.registerTool( + 'sage_query', + { + description: + 'Query Sage X3 business objects via REST. Returns paginated records. Use entity names like BPCUSTOMER, SINVOICE, SORDER, PORDER, ITMMASTER, STOCK.', + inputSchema: { + entity: z.string(), + representation: z.string().optional(), + where: z.string().optional(), + orderBy: z.string().optional(), + count: z.number().min(1).max(200).optional(), + nextUrl: z.string().optional(), + select: z.string().optional(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (args) => { + try { + const result = await restClient.query({ + entity: args.entity, + representation: args.representation, + where: args.where, + orderBy: args.orderBy, + count: args.count, + nextUrl: args.nextUrl, + select: args.select, + }); + return formatQueryResponse(result.records, result.pagination); + } catch (error) { + return formatToolError(error, getErrorHint(classifyError(error))); + } + }, + ); +} diff --git a/src/tools/sage-read.ts b/src/tools/sage-read.ts new file mode 100644 index 0000000..bf08fb3 --- /dev/null +++ b/src/tools/sage-read.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { RestClient } from '../clients/rest-client.js'; +import { formatReadResponse } from '../utils/response.js'; +import { formatToolError, classifyError, getErrorHint } from '../utils/errors.js'; + +export function registerReadTool(server: McpServer, restClient: RestClient): void { + server.registerTool( + 'sage_read', + { + description: + 'Read a single Sage X3 record by its primary key. Returns full record details. Example: entity=SINVOICE, key=INV001.', + inputSchema: { + entity: z.string(), + key: z.string(), + representation: z.string().optional(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (args) => { + try { + const result = await restClient.read(args.entity, args.key, args.representation); + return formatReadResponse(result.record); + } catch (error) { + return formatToolError(error, getErrorHint(classifyError(error))); + } + }, + ); +} diff --git a/src/tools/sage-search.ts b/src/tools/sage-search.ts new file mode 100644 index 0000000..a26b2a3 --- /dev/null +++ b/src/tools/sage-search.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { RestClient } from '../clients/rest-client.js'; +import { formatQueryResponse } from '../utils/response.js'; +import { formatToolError, classifyError, getErrorHint } from '../utils/errors.js'; + +function buildWhereClause(entity: string, searchTerm: string, searchFields?: string[]): string { + const fields = searchFields && searchFields.length > 0 + ? searchFields + : [`${entity}NUM`, `${entity}NAM`]; + + const conditions = fields.map((field) => `contains(${field},'${searchTerm}')`); + return conditions.length === 1 + ? conditions[0] + : `(${conditions.join(' or ')})`; +} + +export function registerSearchTool(server: McpServer, restClient: RestClient): void { + server.registerTool( + 'sage_search', + { + description: + 'Search Sage X3 records with flexible text matching. Builds SData where clauses from a search term across common fields.', + inputSchema: { + entity: z.string(), + searchTerm: z.string(), + searchFields: z.array(z.string()).optional(), + count: z.number().min(1).max(200).optional(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (args) => { + try { + const where = buildWhereClause(args.entity, args.searchTerm, args.searchFields); + const result = await restClient.query({ + entity: args.entity, + where, + count: args.count, + }); + return formatQueryResponse(result.records, result.pagination); + } catch (error) { + return formatToolError(error, getErrorHint(classifyError(error))); + } + }, + ); +} + +export { buildWhereClause };