Comparing version 0.2.51 to 0.3.0-beta1
{ | ||
"name": "enonic-fp", | ||
"version": "0.2.51", | ||
"version": "0.3.0-beta1", | ||
"sideEffects": false, | ||
"description": "Functional programming helpers for Enonic XP", | ||
"main": "lib/index.js", | ||
"typings": "lib/index.d.ts", | ||
"scripts": { | ||
"clean": "rimraf lib/*", | ||
"clean": "rimraf ./*.d.ts && rimraf ./*.js", | ||
"build": "npm run clean && tsc", | ||
@@ -29,9 +27,9 @@ "lint": "eslint --fix 'src/**/*.ts'", | ||
"dependencies": { | ||
"enonic-types": "^0.0.76", | ||
"enonic-types": "^0.1.2", | ||
"fp-ts": "^2.8.2" | ||
}, | ||
"devDependencies": { | ||
"@typescript-eslint/eslint-plugin": "^4.1.0", | ||
"@typescript-eslint/parser": "^4.1.0", | ||
"eslint": "^7.8.1", | ||
"@typescript-eslint/eslint-plugin": "^4.1.1", | ||
"@typescript-eslint/parser": "^4.1.1", | ||
"eslint": "^7.9.0", | ||
"rimraf": "^3.0.2", | ||
@@ -38,0 +36,0 @@ "typescript": "^4.0.2" |
331
README.md
@@ -5,11 +5,11 @@ # Enonic FP | ||
Functional programming helpers for Enonic XP. This library provides [fp-ts](https://github.com/gcanti/fp-ts) wrappers around the Enonic-interfaces provided by [enonic-types](https://github.com/ItemConsulting/enonic-types) and the official standard libraries. | ||
Functional programming helpers for Enonic XP. This library provides [fp-ts](https://github.com/gcanti/fp-ts) wrappers | ||
around the Enonic-interfaces provided by [enonic-types](https://github.com/ItemConsulting/enonic-types) and the official | ||
standard libraries. | ||
## Enonic-wizardry | ||
*Enonic-fp* aims to _only_ provide functional wrappers for the standard libraries, and nothing else. So we recommend also installing [enonic-wizardry](https://github.com/ItemConsulting/enonic-wizardry). *Enonic-wizardry* provides useful useful utilities and helper functions that every project needs. | ||
## Code generation | ||
We recommend using this library together with its sister library: [enonic-ts-codegen](https://github.com/ItemConsulting/enonic-ts-codegen). *enonic-ts-codegen* will create TypeScript `interfaces` for your content-types. Those interfaces will be very useful together with this library. | ||
We recommend using this library together with its sister library: | ||
[enonic-ts-codegen](https://github.com/ItemConsulting/enonic-ts-codegen). *enonic-ts-codegen* will create TypeScript | ||
`interfaces` for your content-types. Those interfaces will be very useful together with this library. | ||
@@ -23,3 +23,4 @@ ## Requirements | ||
Most functions in this library wraps the result in an [IOEither<EnonicError, A>](https://gcanti.github.io/fp-ts/modules/IOEither.ts.html). | ||
Most functions in this library wraps the result in an | ||
[IOEither<EnonicError, A>](https://gcanti.github.io/fp-ts/modules/IOEither.ts.html). | ||
@@ -30,12 +31,7 @@ This gives us two things: | ||
2. It allows us to `pipe` the results from one operation into the next using `chain` (or `map`). Chain expects another | ||
`IOEither<EnonicError, A>` to be returned, and when the first `left<EnonicError>` is returned, the pipe will short circuit to the error case in `fold`. | ||
`IOEither<EnonicError, A>` to be returned, and when the first `left<EnonicError>` is returned, the pipe will short | ||
circuit to the error case in `fold`. | ||
This style of programming encourages us to write re-usable functions that we can compose together using `pipe`. | ||
## Building the project | ||
```bash | ||
npm run build | ||
``` | ||
## Usage | ||
@@ -49,34 +45,18 @@ | ||
```typescript | ||
import { io } from "fp-ts/lib/IO"; | ||
import { fold } from "fp-ts/lib/IOEither"; | ||
import { pipe } from "fp-ts/lib/pipeable"; | ||
import { Request, Response } from "enonic-types/lib/controller"; | ||
import { get as getContent } from "enonic-fp/lib/content"; | ||
import { Article } from "../../site/content-types/article/article"; // 1 | ||
import {fold} from "fp-ts/IOEither"; | ||
import {pipe} from "fp-ts/pipeable"; | ||
import {Request, Response} from "enonic-types/controller"; | ||
import {get as getContent} from "enonic-fp/content"; | ||
import {Article} from "../../site/content-types/article/article"; // 1 | ||
import {internalServerError, ok} from "enonic-fp/controller"; | ||
export function get(req: Request): Response { // 2 | ||
const program = pipe( // 3 | ||
getContent<Article>({ // 4 | ||
key: req.params.key! | ||
}), | ||
getContent<Article>(req.params.key!), // 4 | ||
fold( // 5 | ||
(err) => | ||
io.of( | ||
{ // 6 | ||
status: 500, | ||
body: err, | ||
contentType: "application/json" | ||
} as Response | ||
), | ||
(content) => | ||
io.of( | ||
{ // 7 | ||
status: 200, | ||
body: content.data, | ||
contentType: "application/json" | ||
} | ||
) | ||
internalServerError, // 6 | ||
ok // 7 | ||
) | ||
); | ||
return program(); // 8 | ||
@@ -86,10 +66,16 @@ } | ||
1. We import an `interface Article { ... }` generated by [enonic-ts-codegen](https://github.com/ItemConsulting/enonic-ts-codegen). | ||
1. We import an `interface Article { ... }` generated by | ||
[enonic-ts-codegen](https://github.com/ItemConsulting/enonic-ts-codegen). | ||
2. We use the imported `Request` and `Response` to control the shape of our controller. | ||
3. We use the `pipe` function from *fp-ts* to pipe the result of one function into the next one. | ||
4. We use the `get` function from `content` – here renamed `getContent` so it won't collide with the `get` function in the controller – to return some content where the type is `IOEither<EnonicError, Content<Article>>`. | ||
5. The last thing we usually do in a controller is to unpack the `IOEither`. This is done with `fold(handleError, handleSuccess)`. We create two functions (`handleError`, and `handleSuccess`), that both return `IO<Response>`. | ||
4. We use the `get` function from `content` – here renamed `getContent` so it won't collide with the `get` function in | ||
the controller – to return some content where the type is `IOEither<EnonicError, Content<Article>>`. | ||
5. The last thing we usually do in a controller is to unpack the `IOEither`. This is done with | ||
`fold(handleError, handleSuccess)`. We create two functions (`handleError`, and `handleSuccess`), that both return | ||
`IO<Response>`. | ||
6. We create the `Response` object for the error case | ||
7. We create the `Response` object for the success case | ||
8. We have so far constructed a constant `program` of type `IO<Response>`, but we have not yet performed a single sideeffect. It's time to perform those side effects, so we run the `IO` by calling it. | ||
8. We have so far constructed a constant `program` of type `IO<Response>`, but we have not yet performed a single | ||
side effect. It's time to perform those side effects, so we run the `IO` by calling it, and return the `Response` we | ||
get back. | ||
@@ -106,80 +92,38 @@ | ||
```typescript | ||
import { IO, io } from "fp-ts/lib/IO"; | ||
import { chain, fold, IOEither, map } from "fp-ts/lib/IOEither"; | ||
import { pipe } from "fp-ts/lib/pipeable"; | ||
import { Request, Response } from "enonic-types/lib/controller"; | ||
import { publish, remove } from "enonic-fp/lib/content"; | ||
import { EnonicError } from "enonic-fp/lib/errors"; | ||
import { run } from "enonic-fp/lib/context"; | ||
import {chain, fold} from "fp-ts/IOEither"; | ||
import {pipe} from "fp-ts/pipeable"; | ||
import {Request, Response} from "enonic-types/controller"; | ||
import {publish, remove} from "enonic-fp/content"; | ||
import {run} from "enonic-fp/context"; | ||
import {errorResponse, noContent} from "enonic-fp/controller"; | ||
function del(req: Request): Response { | ||
const key = req.params.key!; | ||
const program = pipe( | ||
runInDraftContext( | ||
remove({ key }) // 1 | ||
runOnBranchDraft( | ||
remove(req.params.key!) // 1 | ||
), | ||
chain(publishContentByKey(key)), // 2 | ||
chain(() => publish(req.params.key!)), // 2 | ||
fold( // 3 | ||
(err) => | ||
io.of( | ||
{ | ||
body: err, | ||
contentType: "application/json", | ||
status: errorKeyToStatus[err.errorKey] | ||
} as Response | ||
), | ||
() => | ||
io.of( | ||
{ | ||
body: "", | ||
status: 204 // 4 | ||
} as Response | ||
) | ||
errorResponse(req), | ||
noContent // 4 | ||
) | ||
); | ||
return program(); // 5 | ||
} | ||
export { del as delete }; // 6 | ||
/** | ||
* This function is found in the "enonic-wizardry" package | ||
*/ | ||
function runInDraftContext<A>(a: IO<A>): IO<A> { | ||
return run<A>({ | ||
branch: 'draft' | ||
})(a); | ||
return program(); | ||
} | ||
/** | ||
* This function is found in the "enonic-wizardry" package | ||
*/ | ||
export function publishContentByKey<A>(key: string): (a: A) => IOEither<EnonicError, A> { | ||
return (a): IOEither<EnonicError, A> => { | ||
return pipe( | ||
publish({ | ||
keys: [key], | ||
sourceBranch: 'draft', | ||
targetBranch: 'master', | ||
}), | ||
map(() => a) | ||
); | ||
} | ||
} | ||
export {del as delete}; // 5 | ||
const errorKeyToStatus: { [key: string]: number } = { | ||
InternalServerError: 500, | ||
NotFoundError: 404, | ||
PublishError: 500 | ||
}; | ||
const runOnBranchDraft = run({ branch: 'draft' }); | ||
``` | ||
1. We call the `remove` function with the `key` to delete some content. We want to do this on the _draft_ branch, so we wrap the call in `runInDraftContext` (which I have copied in from the *enonic-wizardry* package for this example). Remove returns `IOEither<EnonicError, void>`. If the content didn't exist, it will return a `EnonicError` with `errorKey="NotFoundError"`, that can be handled in the `fold`. | ||
2. We want to publish our change from the _draft_ branch to the _master_ branch. So we call another function that we have copied in from *enonic-wizardry*, called `publishContentByKey`. | ||
3.To create our `Response` we call `fold`, where we handle the error and success cases, and return `IO<Response>`. | ||
1. We call the `remove` function with the `key` to delete some content. We want to do this on the _draft_ branch, so we | ||
wrap the call in `runInDraftContext` (which I have copied in from the *enonic-wizardry* package for this example). | ||
Remove returns `IOEither<EnonicError, void>`. If the content didn't exist, it will return a `EnonicError` with | ||
`errorKey="NotFoundError"`, that can be handled in the `fold`. | ||
2. We want to publish our change from the _draft_ branch to the _master_ branch. So we call another function that we | ||
have copied in from *enonic-wizardry*, called `publishContentByKey`. | ||
3. To create our `Response` we call `fold`, where we handle the error and success cases, and return `IO<Response>`. | ||
4. Since this is a delete operation we return a `204` on the success case, which means "no content". | ||
5. We have so far constructed a constant `program` of type `IO<Response>`, but we have not yet performed a single sideeffect. It's time to perform those side effects, so we run the `IO` by calling it. | ||
6. Since delete is a keyword in JavaScript and TypeScript, we have to do this hack to return the `delete` function. | ||
5. Since delete is a keyword in JavaScript and TypeScript, we have to do this hack to return the `delete` function. | ||
@@ -201,88 +145,123 @@ ### Multiple queries, and http request | ||
```typescript | ||
import { sequenceT } from "fp-ts/lib/Apply"; | ||
import { parseJSON } from "fp-ts/lib/Either"; | ||
import { io } from "fp-ts/lib/IO"; | ||
import { chain, fold, fromEither, ioEither, IOEither, map } from "fp-ts/lib/IOEither"; | ||
import { pipe } from "fp-ts/lib/pipeable"; | ||
import { Request, Response } from "enonic-types/lib/controller"; | ||
import { QueryResponse } from "enonic-types/lib/content"; | ||
import { HttpResponse } from "enonic-types/lib/http"; | ||
import { EnonicError } from "enonic-fp/lib/errors"; | ||
import { get as getContent, query } from "enonic-fp/lib/content"; | ||
import { request } from "enonic-fp/lib/http"; | ||
import { Article } from "../../site/content-types/article/article"; | ||
import { Comment } from "../../site/content-types/comment/comment"; | ||
import {sequenceT} from "fp-ts/Apply"; | ||
import {Json} from "fp-ts/Either"; | ||
import {chain, fold, ioEither, IOEither, map} from "fp-ts/IOEither"; | ||
import {pipe} from "fp-ts/pipeable"; | ||
import {Request, Response} from "enonic-types/controller"; | ||
import {Content, QueryResponse} from "enonic-types/content"; | ||
import {EnonicError} from "enonic-fp/errors"; | ||
import {get as getContent, query} from "enonic-fp/content"; | ||
import {bodyAsJson, request} from "enonic-fp/http"; | ||
import {Article} from "../../site/content-types/article/article"; | ||
import {Comment} from "../../site/content-types/comment/comment"; | ||
import {errorResponse, ok} from "enonic-fp/controller"; | ||
import {tupled} from "fp-ts/function"; | ||
export function get(req: Request): Response { | ||
const articleKey = req.params.key!!; | ||
const articleId = req.params.key!; | ||
const program = pipe( | ||
return pipe( | ||
sequenceT(ioEither)( | ||
getContent<Article>({ key: articleKey }), | ||
getCommentsByArticleKey(articleKey), | ||
getContent<Article>(articleId), | ||
getCommentsByArticleKey(articleId), | ||
getOpenPositionsOverHttp() | ||
), | ||
map(([article, comments, openPositions]) => | ||
({ | ||
...article, | ||
comments: comments.hits, | ||
openPositions | ||
}) | ||
), | ||
map(tupled(createResponse)), | ||
fold( | ||
(err: EnonicError) => | ||
io.of( | ||
{ | ||
body: err, | ||
contentType: "application/json", | ||
status: errorKeyToStatus[err.errorKey] | ||
} as Response | ||
), | ||
(res) => | ||
io.of( | ||
{ | ||
body: res, | ||
contentType: "application/json", | ||
status: 200 | ||
} | ||
) | ||
errorResponse(req, 'errors'), | ||
ok | ||
) | ||
); | ||
return program(); | ||
)(); | ||
} | ||
const errorKeyToStatus: { [key: string]: number } = { | ||
BadGatewayError: 502, | ||
InternalServerError: 500, | ||
NotFoundError: 404 | ||
}; | ||
function getCommentsByArticleKey( | ||
articleId: string | ||
): IOEither<EnonicError, QueryResponse<Comment>> { | ||
return query({ | ||
function getCommentsByArticleKey(articleId: string): IOEither<EnonicError, QueryResponse<Comment>> { | ||
return query<Comment>({ | ||
contentTypes: ["com.example:comment"], | ||
count: 100, | ||
query: `data.articleId = ${articleId}` | ||
query: `data.articleId = '${articleId}'` | ||
}); | ||
} | ||
function getOpenPositionsOverHttp(): IOEither<EnonicError, any> { | ||
function getOpenPositionsOverHttp(): IOEither<EnonicError, Json> { | ||
return pipe( | ||
request({ | ||
url: "https://example.com/api/open-positions" | ||
}), | ||
chain((res: HttpResponse) => | ||
fromEither( | ||
parseJSON(res.body!, (reason: any) => | ||
({ | ||
cause: String(reason), | ||
errorKey: "BadGatewayError" | ||
} as EnonicError) | ||
) | ||
) | ||
) | ||
request("https://example.com/api/open-positions"), | ||
chain(bodyAsJson) | ||
); | ||
} | ||
function createResponse( | ||
article: Content<Article>, | ||
comments: QueryResponse<Comment>, | ||
openPositions: Json | ||
) { | ||
return { | ||
...article, | ||
comments: comments.hits, | ||
openPositions | ||
}; | ||
} | ||
``` | ||
## i18n for error messages | ||
### Custom error messages for every endpoint | ||
There is support for adding internationalization for error-messages. This is done, when you generate the `Response` | ||
using the `errorResponse(req: Request, i18nPrefix: string)` method. | ||
The i18n-key to use to look up the message has the following shape: `${i18nPrefix}.title.${typeString}` where | ||
`typeString` is the last section of `EnonicError.type`. To support every error in *enonic-fp*, `typeString` can only be | ||
one of these: | ||
* bad-request-error | ||
* not-found | ||
* internal-server-error | ||
* missing-id-provider | ||
* publish-error | ||
* unpublish-error | ||
* bad-gateway | ||
If your `i18nPrefix` is e.g `"getArticleError"`, then you can add the following to your *phrases.properties* to get | ||
customized error messages for different endpoints. | ||
```properties | ||
getArticleError.title.bad-request-error=Problems with client parameters | ||
getArticleError.title.not-found=No Article Found | ||
getArticleError.title.internal-server-error=Can not retreive article. | ||
getArticleError.title.missing-id-provider=Missing ID Provider. | ||
getArticleError.title.publish-error=Unable to publish the article. | ||
getArticleError.title.unpublish-error=Unable to unpublish the article | ||
getArticleError.title.bad-gateway=Unable to retreive open positions. | ||
``` | ||
### Fallback error messages | ||
We recommend adding the following (but translated) keys to your *phrases.properties* file, as they will provide backup | ||
error messages for all instances where custom error messages have not been specified. | ||
```properties | ||
errors.title.bad-request-error=Bad request error | ||
errors.title.not-found=Not found | ||
errors.title.internal-server-error=Internal Server Error | ||
errors.title.missing-id-provider=Missing ID Provider. | ||
errors.title.publish-error=Unable to publish data | ||
errors.title.unpublish-error=Unable to unpublish data | ||
errors.title.bad-gateway=Bad gateway | ||
``` | ||
Alternatively you could use the status number as the `typeString`-part of the key. But this will not be able to separate | ||
different errors with the same `status` (e.g both *internal-server-error*, *missing-id-provider* and *publish-error* | ||
has status = *500*). | ||
```properties | ||
errors.title.400=Bad request error | ||
errors.title.404=Not found | ||
errors.title.500=Internal Server Error | ||
errors.title.502=Bad gateway | ||
``` | ||
## Building the project | ||
```bash | ||
npm run build | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
96752
48
1918
261
1
+ Addedenonic-types@0.1.32(transitive)
- Removedenonic-types@0.0.76(transitive)
Updatedenonic-types@^0.1.2