-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tng 1261 endpoint integration (#196)
* feat(fsxa-api integration): offer utilities for rest-endpoint implementation backed by fsxa-api Expose Parameter Validators and Wrappers with error handling to use in an endpoint implementation. Refactor default Express integration to use those wrappers * fix(integrationtests): pass integrationtests Minor Adjustments to pass integrationtests * minor test adjustment * docs(documentation): add Short Readme about integration and annotate exposed functions * expose utils
- Loading branch information
1 parent
17c067f
commit beb4a1a
Showing
9 changed files
with
654 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Endpoint Integration Utilities | ||
|
||
A standard use-case for the javascript-content-api is to integrate it into some RESTful API or other API that exposes Endpoints on the web. | ||
|
||
To facilitate that usecase, [validation utilities](../src/integrations/parameterValidation.ts) and [wrappers](../src/integrations/endpointIntegrationWrapper.ts.ts) for different functionalities like _fetchByFilter_ or _fetchNavigation_ are provided. | ||
|
||
### Validation Utilities | ||
|
||
The Validation Utilities offer a simple way to test for required Paramters before passing them to the fsxa-api. They do not check the parameters semantically, as this is up to the fsxa-api implementation. | ||
|
||
### Integration Wrappers | ||
|
||
The Integration Wrappers are meant to be used by a webserver (like express) to safely pass parameters to the fsxa-api with error handling taken care of. | ||
They return an Object containing the result of the Operation, with indicators of sensible return Codes and Error Messages on Errors. | ||
|
||
Evaluating those Results and sending an appropriate response is quite easy and up to the user of the utilities. [See the reference Implementation using Express](../src/integrations/express.ts). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
import { HttpError } from '../exceptions' | ||
import { FSXARemoteApi, Logger } from '../modules' | ||
import { | ||
FetchByFilterBody, | ||
FetchElementRouteBody, | ||
FetchNavigationRouteBody, | ||
FetchProjectPropertiesBody, | ||
} from '../routes' | ||
import { | ||
ParamValidationError, | ||
validateParamsFetchElement, | ||
validateParamsFetchByFilter, | ||
validateParamsFetchNavigation, | ||
validateParamsFetchProjectProperties, | ||
} from './parameterValidation' | ||
import { | ||
FetchResponse, | ||
NavigationData, | ||
NormalizedProjectPropertyResponse, | ||
ProjectProperties, | ||
} from '../types' | ||
|
||
export type FetchWrapperResult<T = any> = | ||
| FetchWrapperSuccess<T> | ||
| FetchWrapperError | ||
|
||
type FetchWrapperSuccess<T = any> = { | ||
success: true | ||
data: T | ||
} | ||
|
||
type FetchWrapperError = { | ||
success: false | ||
error: { | ||
statusCode: number | ||
message: string | ||
} | ||
originalError: Error | ParamValidationError | ||
} | ||
|
||
/** | ||
* Useful Error Messages for Validation Errors. | ||
* Can be used as an Error Message to send to the client | ||
*/ | ||
export const PARAM_VALIDATION_ERROR_TO_MESSAGE: Record< | ||
ParamValidationError['reason'], | ||
string | ||
> = { | ||
MISSING_ID: | ||
'Please specify a locale in the body through: e.g. "locale": "de_DE" ', | ||
MISSING_LOCALE: 'Please Provide an id in the body', | ||
MISSING_PARAM_OBJECT: | ||
"Please provide required params in the body. required: 'id', 'locale'", | ||
MISSING_FILTERS: 'Please provide non-empty filters array', | ||
} as const | ||
|
||
/** | ||
* Provides Wrapper Functions fot the given api. | ||
* @param api FSXAApi to wrap | ||
* @param loggerName Creates a logger with the given name | ||
* @returns | ||
*/ | ||
export const useEndpointIntegrationWrapper = ( | ||
api: FSXARemoteApi, | ||
loggerName = 'Endpoint-Utils' | ||
) => { | ||
const logger = new Logger(api.logLevel, loggerName) | ||
|
||
/** | ||
* Fetches an Element by Id via {@link FSXARemoteApi} | ||
* @param params | ||
* @param logContext | ||
* @returns A ResultWrapper, that either indicates success and contains the data, or indicates an Error and a Reason | ||
*/ | ||
const fetchElementWrapper = async ( | ||
params: FetchElementRouteBody, | ||
logContext: string = '[fetchElement]' | ||
): Promise<FetchWrapperResult> => { | ||
logger.debug(logContext, 'Called') | ||
|
||
try { | ||
const validationResult = validateParamsFetchElement(params) | ||
|
||
if (!validationResult.valid) { | ||
return handleValidationError(validationResult, params, logContext) | ||
} | ||
|
||
logger.debug(logContext, 'Params valid, fetching from Api') | ||
|
||
const response = await api.fetchElement({ | ||
id: params.id, | ||
locale: params.locale, | ||
additionalParams: params.additionalParams, | ||
remoteProject: params.remote, | ||
filterContext: params.filterContext, | ||
normalized: params.normalized, | ||
}) | ||
logger.debug(logContext, 'fetch successful') | ||
|
||
return { | ||
success: true, | ||
data: response, | ||
} | ||
} catch (error: unknown) { | ||
return handleRuntimeError(error, logContext) | ||
} | ||
} | ||
|
||
/** | ||
* Fetches Navigation via {@link FSXARemoteApi} | ||
* @param params | ||
* @param logContext | ||
* @returns A ResultWrapper, that either indicates success and contains the data, or indicates an Error and a Reason | ||
*/ | ||
const fetchNavigationWrapper = async ( | ||
params: FetchNavigationRouteBody, | ||
logContext: string = '[fetchNavigation]' | ||
): Promise<FetchWrapperResult<NavigationData | null>> => { | ||
logger.debug(logContext, 'Called') | ||
|
||
try { | ||
const validationResult = validateParamsFetchNavigation(params) | ||
|
||
if (!validationResult.valid) { | ||
return handleValidationError(validationResult, params, logContext) | ||
} | ||
logger.debug(logContext, 'Params valid, fetching from Api') | ||
|
||
const response = await api.fetchNavigation({ | ||
initialPath: params.initialPath || '/', | ||
locale: params.locale, | ||
filterContext: params.filterContext, | ||
}) | ||
|
||
logger.debug(logContext, 'fetch successful') | ||
return { | ||
success: true, | ||
data: response, | ||
} | ||
} catch (error: unknown) { | ||
return handleRuntimeError(error, logContext) | ||
} | ||
} | ||
|
||
/** | ||
* Fetches Elements by Filter via {@link FSXARemoteApi} | ||
* @param params | ||
* @param logContext | ||
* @returns A ResultWrapper, that either indicates success and contains the data, or indicates an Error and a Reason | ||
*/ | ||
const fetchByFilterWrapper = async ( | ||
params: FetchByFilterBody, | ||
logContext: string = '[fetchByFilter]' | ||
): Promise<FetchWrapperResult<FetchResponse>> => { | ||
logger.debug(logContext, 'Called') | ||
|
||
try { | ||
const validationResult = validateParamsFetchByFilter(params) | ||
if (!validationResult.valid) { | ||
return handleValidationError(validationResult, params, logContext) | ||
} | ||
|
||
logger.debug(logContext, 'Params valid, fetching from Api') | ||
|
||
const response = await api.fetchByFilter({ | ||
filters: params.filters || [], | ||
locale: params.locale, | ||
page: params.page ? params.page : undefined, | ||
pagesize: params.pagesize ? params.pagesize : undefined, | ||
sort: params.sort ? params.sort : [], | ||
additionalParams: params.additionalParams || {}, | ||
remoteProject: params.remote ? params.remote : undefined, | ||
filterContext: params.filterContext, | ||
normalized: params?.normalized, | ||
}) | ||
logger.debug(logContext, 'fetch successful') | ||
|
||
return { | ||
success: true, | ||
data: response, | ||
} | ||
} catch (error: unknown) { | ||
return handleRuntimeError(error, logContext) | ||
} | ||
} | ||
|
||
/** | ||
* Fetches ProjectProperties via {@link FSXARemoteApi} | ||
* @param params | ||
* @param logContext | ||
* @returns A ResultWrapper, that either indicates success and contains the data, or indicates an Error and a Reason | ||
*/ | ||
const fetchProjectPropertiesWrapper = async ( | ||
params: FetchProjectPropertiesBody, | ||
logContext: string = '[fetchProjectProperties]' | ||
): Promise< | ||
FetchWrapperResult< | ||
ProjectProperties | NormalizedProjectPropertyResponse | null | ||
> | ||
> => { | ||
logger.debug(logContext, 'Called') | ||
|
||
try { | ||
const validationResult = validateParamsFetchProjectProperties(params) | ||
if (!validationResult.valid) { | ||
return handleValidationError(validationResult, params, logContext) | ||
} | ||
|
||
logger.debug(logContext, 'Params valid, fetching from Api') | ||
|
||
const response = await api.fetchProjectProperties({ | ||
locale: params.locale, | ||
additionalParams: params.additionalParams, | ||
resolve: params.resolver, | ||
filterContext: params.filterContext, | ||
normalized: params.normalized, | ||
}) | ||
logger.debug(logContext, 'fetch successful') | ||
|
||
return { | ||
success: true, | ||
data: response, | ||
} | ||
} catch (error: unknown) { | ||
return handleRuntimeError(error, logContext) | ||
} | ||
} | ||
|
||
const handleValidationError = ( | ||
validationResult: ParamValidationError, | ||
params: any, | ||
logContext: string | ||
): FetchWrapperError => { | ||
logger.error(logContext, validationResult.reason, params) | ||
return { | ||
success: false, | ||
error: { | ||
statusCode: validationResult.statusCode, | ||
message: PARAM_VALIDATION_ERROR_TO_MESSAGE[validationResult.reason], | ||
}, | ||
originalError: validationResult, | ||
} | ||
} | ||
|
||
function assertIsError(error: unknown): asserts error is Error { | ||
// rethrow unknown error if its not an instance of error, since we do not know how to handle that | ||
if (!(error instanceof Error)) { | ||
throw error | ||
} | ||
} | ||
|
||
const handleRuntimeError = ( | ||
error: unknown, | ||
logContext: string | ||
): FetchWrapperError => { | ||
assertIsError(error) | ||
logger.error(logContext, error) | ||
|
||
const errorObj: FetchWrapperError['error'] = { | ||
statusCode: (error as HttpError).statusCode || 500, | ||
message: error.message, | ||
} | ||
|
||
return { | ||
success: false, | ||
error: errorObj, | ||
originalError: error, | ||
} | ||
} | ||
|
||
return { | ||
fetchElementWrapper, | ||
fetchNavigationWrapper, | ||
fetchByFilterWrapper, | ||
fetchProjectPropertiesWrapper, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.