Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

enonic-fp

Package Overview
Dependencies
Maintainers
1
Versions
99
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

enonic-fp - npm Package Compare versions

Comparing version 0.2.51 to 0.3.0-beta1

array.d.ts

14

package.json
{
"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"

@@ -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
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc