epicor-rest-node
v1.3.0
Published
This library helps make Epicor calls from an Node application
Readme
Epicor Rest Helper for Node
This library helps make Epicor calls from a Node application. This library is not official and has no direct relationship with Epicor (c). It is merely a helper library maintained by the community.
Table of Contents
Installation
npm i --save epicor-rest-node
Testing
This library includes a comprehensive test suite to verify functionality and help prevent regressions. See TESTING.md for detailed setup and usage instructions.
Quick Start:
# Copy environment template
cp .env.example .env
# Edit .env with your Epicor server details
# Then run tests
npm testThe test suite validates all library functionality including authentication, Business Object calls, BAQ queries, Epicor Functions, error handling, and more.
Usage
Setup
import { EpicorRestService, EpicorRestVersion, EpicorError } from 'epicor-rest-node';
import { EpicorLicenseType } from 'epicor-rest-node/dist/models/EpicorLicenseType'; // For single instance creation
let EpicorRest = new EpicorRestService();
// or injected into another class/service such as a controller
constructor(private readonly epicorSvc: EpicorService) {}
// Instance properties required to function
EpicorRest.AppPoolHost = 'subdomain.domain.tld';
EpicorRest.AppPoolInstance = 'Epicor10Instance';
EpicorRest.UserName = 'MyEpicorUserName';
EpicorRest.Password = 'MyEpicorPassword';
EpicorRest.APIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxx'; //Needed for V2
EpicorRest.Company = 'EPIC01';
EpicorRest.EpicorRestVersion = EpicorRestVersion.V2; //Defaults to V2
EpicorRest.License = EpicorLicenseType.WebService; //Defaults to Default
EpicorRest.EfxAttemptStagingRetry = false; //Defaults to false. Set to true only during development to retry EFX calls with staging endpointHandling Instance Scope
@Injectable({ scope: Scope.REQUEST }) tells Nest to treat your service class as a provider with request scope. In practical terms:
- @Injectable(): Marks the class so Nest can manage and inject it wherever needed (dependency injection).
- scope: Scope.REQUEST: Tells Nest to create a new instance of this class for each incoming request, rather than reusing a single global instance. This is useful if each request needs its own state or dependencies. (NOTE: This will also consume additional licenses in Epicor)
import { EpicorRestService } from 'epicor-rest-node';
// Adding REQUEST scope will force a new instance per request made vs a singleton
@Injectable({scope: Scope.REQUEST})
export class EpicorService extends EpicorRestService implements OnModuleInit {
public Plant: string = 'MfgSys';
/**
* Constructor
*/
constructor(
private readonly config: ConfigService
) {
super();
this.AppPoolHost = 'subdomain.domain.tld';
this.AppPoolInstance = 'Epicor10Instance';
this.UserName = 'MyEpicorUserName';
this.Password = 'MyEpicorPassword';
this.APIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxx'; //Needed for V2
this.Company = 'EPIC01';
this.EpicorRestVersion = EpicorRestVersion.V2; //Defaults to V2
this.License = EpicorLicenseType.WebService; //Defaults to Default
// TODO: Do we need to implement timezone offset
this.CallSettings = new CallSettings(this.Company, this.Plant, '', '', '');
console.log('My Custom EpicorService Constructor');
}
.
.
.
/**
* Switch Employee ID
* @param empID
*/
public async switchEmployee(empID: string) {
let switchRes = await this.BoPost<any>("Ice.Lib.SessionModSvc", "SetEmployee", { employeeID: empID });
if (EpicorError.isError(switchRes)) {
console.log(`Error switching employee: ${switchRes.message}`);
return false;
}
return switchRes;
}
.
.
.
}Call BO Methods
let params = new Map<string,string>();
params.set('$filter','ABCCode1 eq \'A\'');
// Using async/await with proper error handling
const result = await EpicorRest.BoGet<any>('Erp.BO.ABCCodeSvc','ABCCodes',params);
if (EpicorError.isError(result)) {
// Handle error
console.log(`Error ${result.status}: ${result.message}`);
} else {
// Handle success
console.log(result);
}
// POST example with async/await
const postResult = await EpicorRest.BoPost<any>('Erp.BO.ABCCodeSvc','ABCCodes',data);
if (EpicorError.isError(postResult)) {
console.log(`Error ${postResult.status}: ${postResult.message}`);
} else {
console.log(postResult);
}
// Or using traditional promise chain (still works)
EpicorRest.BoGet('Erp.BO.ABCCodeSvc','ABCCodes',params).then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
console.log(res);
}
});
// Patch and Delete are also available with the same patternCall BAQ
let params = new Map<string,string>();
params.set('$top','13');
// Using async/await with proper error handling
const baqResult = await EpicorRest.BaqGet<any>('zCustomer01', params);
if (EpicorError.isError(baqResult)) {
// Handle error
console.log(`Error ${baqResult.status}: ${baqResult.message}`);
} else {
// Handle success
console.log(baqResult);
}
// Or using traditional promise chain (still works)
EpicorRest.BaqGet('zCustomer01', params).then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
console.log(res);
}
});
//BAQ Patch is also available with the same patternGet BAQ Metadata
// Get BAQ metadata/schema information
const metaResult = await EpicorRest.BaqMetaData<any>('zCustomer01');
if (EpicorError.isError(metaResult)) {
// Handle error
console.log(`Error ${metaResult.status}: ${metaResult.message}`);
} else {
// Handle success - metaResult contains OData $metadata
console.log('BAQ Metadata:', metaResult);
// Use metadata to understand available fields, data types, relationships
}
// Or using traditional promise chain (still works)
EpicorRest.BaqMetaData('zCustomer01').then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
console.log('Metadata:', res);
}
});Use Cases for BAQ Metadata:
- Getting schema information about BAQ fields and data types
- Understanding relationships between tables in the BAQ
- Building dynamic queries based on available fields
- OData service discovery for client applications
- Validating field names before making BAQ queries
Get BAQ Swagger Documentation
// Get OpenAPI 3.0.1 swagger documentation for a specific BAQ
const swaggerResult = await EpicorRest.GetBaqSwagger<any>('zCustomer01');
if (EpicorError.isError(swaggerResult)) {
// Handle error
console.log(`Error ${swaggerResult.status}: ${swaggerResult.message}`);
} else {
// Handle success - swaggerResult contains the full OpenAPI specification
console.log('BAQ Info:', swaggerResult.info.title, swaggerResult.info.description);
console.log('Server URL:', swaggerResult.servers[0]?.url);
// Explore available endpoints
console.log('Available Endpoints:');
Object.keys(swaggerResult.paths).forEach(path => {
const pathInfo = swaggerResult.paths[path];
Object.keys(pathInfo).forEach(method => {
const operation = pathInfo[method];
console.log(` ${method.toUpperCase()} ${path}: ${operation.summary}`);
});
});
// Get schema information for return data
if (swaggerResult.components?.schemas) {
console.log('Available Schemas:');
Object.keys(swaggerResult.components.schemas).forEach(schemaName => {
const schema = swaggerResult.components.schemas[schemaName];
console.log(` ${schemaName}:`);
if (schema.properties) {
Object.keys(schema.properties).forEach(propName => {
const prop = schema.properties[propName];
console.log(` - ${propName}: ${prop.type} ${prop.description ? `(${prop.description})` : ''}`);
});
}
});
}
// Extract query item schema for understanding return structure
const queryItemSchema = swaggerResult.components?.schemas?.['Epicor.QueryItem'];
if (queryItemSchema?.properties) {
console.log('BAQ Return Fields:');
Object.keys(queryItemSchema.properties).forEach(fieldName => {
const field = queryItemSchema.properties[fieldName];
console.log(` ${fieldName}: ${field.type} ${field.required ? '(required)' : '(optional)'}`);
if (field.description) {
console.log(` Description: ${field.description}`);
}
});
}
}
// Or using traditional promise chain (still works)
EpicorRest.GetBaqSwagger('zCustomer01').then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
console.log('BAQ Swagger Documentation:', res);
}
});Use Cases for BAQ Swagger Documentation:
- Understanding available BAQ endpoints (
/Data,/$metadata, etc.) - Getting complete schema information about BAQ return types
- Discovering field descriptions and data types for BAQ results
- Building dynamic UIs based on BAQ field definitions
- API documentation and client code generation
- Validating BAQ capabilities and available operations
- Understanding OData query parameters supported by the BAQ
- Integration planning and data mapping
Call Epicor Function
let smsSend =
{
ToPhone:'123456789',
ToMsg:'Zup from Node'
};// Using async/await with proper error handling
const efxResult = await EpicorRest.EfxPost<any>('FacilityPaging','SendSMS',smsSend);
if (EpicorError.isError(efxResult)) {
// Handle error
console.log(`Error ${efxResult.status}: ${efxResult.message}`);
} else {
// Handle success
console.log(efxResult);
}
// Or using traditional promise chain (still works)
EpicorRest.EfxPost('FacilityPaging','SendSMS',smsSend).then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
console.log(res);
}
});Get Function Library List
// Get list of available EFX function libraries
const libraryResult = await EpicorRest.GetFunctionLibraryList();
if (EpicorError.isError(libraryResult)) {
// Handle error
console.log(`Error ${libraryResult.status}: ${libraryResult.message}`);
} else {
// Handle success - libraryResult is an array of function libraries
console.log('Available Function Libraries:');
libraryResult.forEach(library => {
console.log(`- ${library.LibraryId}: ${library.Description || 'No description'}`);
});
}
// Or using traditional promise chain (still works)
EpicorRest.GetFunctionLibraryList().then(res => {
if (EpicorError.isError(res)) {
console.log(`Error ${res.status}: ${res.message}`);
} else {
res.forEach(lib => {
console.log(`Library: ${lib.LibraryId}`);
});
}
});Use Cases for Function Library List:
- Discovering available EFX function libraries in your Epicor environment
- Building dynamic UIs that show available functions to users
- Validating library names before making EFX function calls
- Documentation and API exploration
- Integration planning and capability assessment
Get Function Library Specification
import { EpicorFunctionSpecParser } from 'epicor-rest-node';
// Get detailed OpenAPI specification for a specific function library
const specResult = await EpicorRest.GetFunctionLibrarySpec('CustomerPortal');
if (EpicorError.isError(specResult)) {
// Handle error
console.log(`Error ${specResult.status}: ${specResult.message}`);
} else {
// Handle success - specResult is the full OpenAPI 3.0.1 specification
console.log('Library Info:', specResult.info.title, specResult.info.version);
// Use the parser to extract function information
const parser = new EpicorFunctionSpecParser(specResult);
const functions = parser.getFunctions();
console.log('Available Functions:');
functions.forEach(func => {
console.log(`- ${func.functionName} (${func.method}): ${func.summary}`);
console.log(` Requires Input: ${func.requiresInput}`);
// Show input parameters
if (func.inputSchema) {
console.log(' Input Parameters:');
func.inputSchema.forEach(param => {
console.log(` - ${param.name}: ${param.type} ${param.required ? '(required)' : '(optional)'}`);
});
}
// Generate input template
const inputTemplate = parser.generateInputTemplate(func.functionName);
console.log(' Input Template:', JSON.stringify(inputTemplate, null, 2));
});
}
// Get specific function information
const loginFunc = parser.getFunction('Login');
if (loginFunc) {
console.log('Login Function Details:', loginFunc);
// Generate a typed input object for the Login function
const loginInput = parser.generateInputTemplate('Login');
// loginInput will be: { UserName: '', Password: '' }
// Now you can populate and use it with EfxPost
loginInput.UserName = 'testuser';
loginInput.Password = 'testpass';
const result = await EpicorRest.EfxPost('CustomerPortal', 'Login', loginInput);
}Use Cases for Function Library Specification:
- Understanding function signatures and parameter types
- Generating typed input objects for function calls
- Building dynamic forms based on function parameters
- API documentation and code generation
- Validating function inputs before making calls
- Creating SDKs or wrappers for specific function libraries
Get Environment Information
// Get Epicor environment details
const envResult = await EpicorRest.GetEnvironment();
if (EpicorError.isError(envResult)) {
console.log(`Error getting environment: ${envResult.message}`);
} else {
console.log('Environment info:', envResult);
// envResult contains environment details (which company the user has access to and which plants). No Api Required
// See Exported Class EpicorEnvironment
}Error Handling
All API methods return either the expected data type T or an EpicorError object. The library provides several ways to handle errors:
Using EpicorError.isError() Type Guard
import { EpicorError } from 'epicor-rest-node';
const result = await EpicorRest.BoGet<any>('Erp.BO.ABCCodeSvc','ABCCodes', params);
if (EpicorError.isError(result)) {
// TypeScript knows this is an EpicorError
console.log(`HTTP Status: ${result.status}`);
console.log(`Error Message: ${result.message}`);
console.log(`Raw Error:`, result.error);
} else {
// TypeScript knows this is your expected data type
console.log('Success:', result);
}Using instanceof Check
const result = await EpicorRest.BoGet<any>('Erp.BO.ABCCodeSvc','ABCCodes', params);
if (result instanceof EpicorError) {
console.log(`Error: ${result.message}`);
} else {
console.log('Success:', result);
}EpicorError Properties
status: number- HTTP status codemessage: string- Human-readable error messageerror?: any- Raw error object from the server (optional)
Common Error Scenarios
// Network/Connection errors typically have status 500
// Epicor authentication errors typically have status 401
// Not found errors typically have status 404
// Validation errors typically have status 400
const result = await EpicorRest.BoGet<any>('NonExistent.BO','Method', params);
if (EpicorError.isError(result)) {
switch (result.status) {
case 401:
console.log('Authentication failed - check credentials');
break;
case 404:
console.log('Business Object or method not found');
break;
case 500:
console.log('Server error or network issue');
break;
default:
console.log(`Unexpected error: ${result.message}`);
}
}Capturing Response Headers
All API methods support capturing HTTP response headers using the CapturedResponseHeaders class. This is useful for accessing server information, debugging, or extracting custom headers returned by Epicor.
import { CapturedResponseHeaders } from 'epicor-rest-node';
// Create a CapturedResponseHeaders instance
const capturedHeaders = new CapturedResponseHeaders();
// Pass it as the last parameter to any API method
const result = await EpicorRest.GetEnvironment(null, null, capturedHeaders);
if (!EpicorError.isError(result)) {
// Access all captured headers
console.log('All headers:', capturedHeaders.headers);
// Get specific headers (case-insensitive)
const contentType = capturedHeaders.getHeader('content-type');
const server = capturedHeaders.getHeader('server');
console.log('Content-Type:', contentType);
console.log('Server:', server);
// Check if a header exists
if (capturedHeaders.hasHeader('x-powered-by')) {
console.log('X-Powered-By:', capturedHeaders.getHeader('x-powered-by'));
}
// Get all header names
const headerNames = capturedHeaders.getHeaderNames();
console.log('Available headers:', headerNames);
}
// Works with all API methods
const params = new Map<string, string>();
params.set('$top', '5');
const boHeaders = new CapturedResponseHeaders();
const boResult = await EpicorRest.BoGet('Erp.BO.Customer', 'GetList', params, null, null, boHeaders);
const baqHeaders = new CapturedResponseHeaders();
const baqResult = await EpicorRest.BaqGet('MyBAQ', params, null, null, baqHeaders);
const efxHeaders = new CapturedResponseHeaders();
const efxResult = await EpicorRest.EfxPost('MyLibrary', 'MyFunction', {}, false, null, null, efxHeaders);Use Cases for Captured Response Headers:
- Debugging API issues by examining server response headers
- Accessing rate limiting information or quotas
- Extracting custom Epicor headers with business logic information
- Monitoring server performance metrics
- Implementing caching strategies based on cache-control headers
- Security auditing and compliance logging
Advanced Usage
Epicor Session
An Epicor session can be established at any point by invoking EpicorRest.CreateSession() and make sure to kill the session when you are done.
Note: Createsession() (lowercase 's') is deprecated. Use CreateSession() (capital 'S') instead.
const sessionCreated = await EpicorRest.CreateSession();
if (sessionCreated) {
try {
// Any calls made in here will use the above created session
let params = new Map<string,string>();
params.set('$filter','ABCCode1 eq \'A\'');
const result = await EpicorRest.BoGet<any>('Erp.BO.ABCCodeSvc','ABCCodes',params);
if (EpicorError.isError(result)) {
console.log(`Error ${result.status}: ${result.message}`);
} else {
console.log(result);
}
} finally {
await EpicorRest.DestroySession();
}
} else {
console.log('Failed to create session');
}An Epicor session can be killed manually by invoking EpicorRest.DestroySession() this needs to be done after the last call to the BO/BAQ/EFX etc.
Session Management Methods
Once a session is created, you can manage session context using these methods:
// Set the current employee for the session
const employeeSet = await EpicorRest.SetEmployee('EMP123');
if (employeeSet) {
console.log('Employee context updated');
}
// Set the current plant/site for the session
const plantSet = await EpicorRest.SetPlant('MfgSys');
if (plantSet) {
console.log('Plant context updated');
}
// Set the current workstation for the session
const workstationSet = await EpicorRest.SetWorkstation('WKST001');
if (workstationSet) {
console.log('Workstation context updated');
}
// Set client data for the session (sync client information)
const clientDataSet = await EpicorRest.SetClientData(
'john.doe', // clientUserName
'DESKTOP-ABC123', // clientComputerName
'M/d/yyyy', // clientDateFormat (optional, defaults to 'M/d/yyyy')
undefined, // appserver (optional, defaults to current instance URL)
0 // clientTerminalID (optional, defaults to 0)
);
if (clientDataSet) {
console.log('Client data synchronized');
}
// Get current session information
const sessionInfo = await EpicorRest.GetSessionInfo();
console.log('Session Info:', sessionInfo);
// Returns session details including user, company, plant, employee, version info, etc.
// Get theme and user options information
const themeInfo = await EpicorRest.GetThemeInfo();
console.log('Theme Info:', themeInfo);
// Returns user preferences, theme settings, and shell layout optionsImportant: All session management methods (SetEmployee, SetPlant, SetWorkstation, SetClientData) require an active session created with CreateSession(). They will throw an error if called without a valid session.
Use Cases:
- SetEmployee: Switch context to a different employee for labor tracking or permissions
- SetPlant: Change the manufacturing site context for operations
- SetWorkstation: Set the workstation for production or quality operations
- SetClientData: Synchronize client machine information with the server session
- GetSessionInfo: Retrieve current session state, user info, and Epicor version
- GetThemeInfo: Get user preferences and theme settings for UI customization
Epicor Call Context
Sending Call Context To Node
Managing call context as an object can be done by using the EpicorRest CallContext models.
Generate a header with the call context values you want in your client application and send it in the contextheader header of your request.
{
"Context":{
"BpmData":[
{
"Character01":"FOO",
"Character02":"BAR",
"Checkbox01":true,
"Date01":"2024-12-27"
}
]
}
}Handling Request Call Context
Below is an example controller endpoint that grabs the contextheader from the request headers and sends it to our EpicorService that implements the EpicorRestNode module. It also then takes the call context and sends it back to our client that made the request.
/**
* Post EFX Data
*/
@Post('PostEFX/:library/:method')
async postEfx(
@Param('company') company: string,
@Param('library') library: string,
@Param('method') method: string,
@Body() body: Record<string, any>, // Capture the entire JSON body
@Req() req,
@Res({ passthrough: true }) res: any, // passthrough is important to allow us to send the context headers back but allow interceptors to still run on our 'data' return if we need to
) {
// Pass the body directly as params
const { data, context } = await this.epicorSvc.callEFX(company, library, method, body, req.headers['contextheader']);
// Set the `callcontext` in the response headers to our client
res.setHeader('contextheader', JSON.stringify(context) || '');
// Send response
return data;
}In our actual service we send the headers to our EpicorRestNode module in the method signature.
import { BpmData, CallContext, Client, Context } from 'epicor-rest-node/dist/models/CallContext';
import { EpicorError } from 'epicor-rest-node';
public async callEFX(user: any, library: string, functionName: string, params: any, callContext: string | undefined = undefined): Promise<any> {
let efxData = undefined;
let respCallContext = undefined;
const sessionCreated = await EpicorRest.Createsession();
if (sessionCreated) {
try {
// Convert the string passed in to an object of CallContext
const reqCallContext = new CallContext(JSON.parse(callContext).Context);
// Modify the call context as you need
reqCallContext.Context.BpmData[0].Character01 = "MyCustomAppSentThis";
// Call our method in the EpicorRestNode module passing CallContext as a param
const efxResult = await this.EfxPost<any>(library, functionName, params, false, reqCallContext);
if (EpicorError.isError(efxResult)) {
console.log(`EFX Error ${efxResult.status}: ${efxResult.message}`);
efxData = null;
} else {
efxData = efxResult;
// Note: For call context response headers, you'll need to modify HttpJSON to return headers
// respCallContext = new CallContext(JSON.parse(res.headers['contextheader']).Context);
}
} finally {
await EpicorRest.DestroySession();
}
} else {
console.log('Failed to create session');
}
// Here we return our response data and the call context as two seperate objects to our controller above
return { data: efxData, context: respCallContext };
}Additionally if you wanted to you could create an interface for that return data so in your controller you can get the type cast autocomplete goodness
export interface EpicorResponse {
data: any;
context: CallContext;
}
// In our service we switch our promise from 'any'
public async callEFX(user: any, library: string, functionName: string, params: any, callContext: string | undefined = undefined): Promise<any> {
// to 'EpicorResponse'
public async callEFX(user: any, library: string, functionName: string, params: any, callContext: string | undefined = undefined): Promise<EpicorResponse> {