Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API v3 support #1

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 122 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CiviCRM API

JavaScript (and TypeScript) client for [CiviCRM API v4](https://docs.civicrm.org/dev/en/latest/api/v4/usage/).
JavaScript (and TypeScript) client for the [CiviCRM API](https://docs.civicrm.org/dev/en/latest/api/v4/usage/).

Currently only tested in Node.js, browser support is work in progress.

Expand All @@ -24,6 +24,28 @@ const client = createClient({
const contactRequest = client.contact.get({ where: { id: 1 } }).one();
```

### API v3

You can optionally create an [API v3](https://docs.civicrm.org/dev/en/latest/api/v3/) client by providing the relevant configuration:

```ts
const client = createClient({
// ...
api3: {
enabled: true,
entities: {
contact: {
name: "Contact",
actions: {
getList: "getlist",
},
},
},
});

const contactsRequest = client.contact.getList();
```

## API

### `createClient(options: ClientOptions): Client`
Expand All @@ -48,6 +70,7 @@ entity in CiviCRM:

```ts
const client = createClient({
// ...
entities: {
contact: "Contact",
activity: "Activity",
Expand All @@ -68,7 +91,42 @@ Headers will be merged with the default headers.

Enable logging request and response details to the console.

### `client.<entity>: RequestBuilder`
#### options.api3.enabled

Enable API v3 client.

```ts
const client = createClient({
// ...
api3: {
enabled: true,
},
});
```

#### options.api3.entities

An object containing entities and actions the API v3 client will be used to make requests for.
Keys will be used to reference the entity within the client. The value contains the name of the entity in API v3, and an object of actions, where the key is used to reference the action within the client, and the value is the action in API v3.

```ts
const client = createClient({
// ...
api3: {
enabled: true,
entities: {
contact: {
name: "Contact",
actions: {
getList: "getlist",
},
},
},
},
});
```

### `client.<entity>: Api4.RequestBuilder`

Create a request builder for a configured entity.

Expand All @@ -92,17 +150,17 @@ client.contact
const contact = await client.contact.get({ where: { id: 1 } }).one();
```

#### `get(params?: Params): RequestBuilder`
#### `get(params?: Api4.Params): Api4.RequestBuilder`

#### `create(params?: Params): RequestBuilder`
#### `create(params?: Api4.Params): Api4.RequestBuilder`

#### `update(params?: Params): RequestBuilder`
#### `update(params?: Api4.Params): Api4.RequestBuilder`

#### `save(params?: Params): RequestBuilder`
#### `save(params?: Api4.Params): Api4.RequestBuilder`

#### `delete(params?: Params): RequestBuilder`
#### `delete(params?: Api4.Params): Api4.RequestBuilder`

#### `getChecksum(params?: Params): RequestBuilder`
#### `getChecksum(params?: Api4.Params): Api4.RequestBuilder`

Set the action for the request to the method name, and optionally set request
parameters.
Expand All @@ -115,11 +173,11 @@ including `select`, `where`, `having`, `join`,

Alternatively accepts a key-value object for methods like `getChecksum`.

#### `one(): RequestBuilder`
#### `one(): Api4.RequestBuilder`

Return a single record (i.e. set the index of the request to 0).

#### `chain(label: string, requestBuilder: RequestBuilder): RequestBuilder`
#### `chain(label: string, requestBuilder: Api4.RequestBuilder): Api4.RequestBuilder`

[Chain a request](https://docs.civicrm.org/dev/en/latest/api/v4/chaining/#apiv4-chaining)
for another entity within the current API call.
Expand All @@ -146,7 +204,7 @@ chained request within the response.

A request builder for the chained request.

#### `options(requestOptions: RequestInit): RequestBuilder`
#### `options(requestOptions: RequestInit): Api4.RequestBuilder`

Set request options.

Expand All @@ -166,6 +224,59 @@ client.contact.get().options({
});
```

## `client.api3.<entity>: Api3.RequestBuilder`

Create an API v3 request builder for a configured entity.

### Request builder

Request builders are used to build and execute requests.

Methods can be chained, and the request is executed by
calling `.then()` or starting a chain with `await`.

```ts
// Using .then()
client.api3.contact.getList({ input: "example" }).then((contacts) => {
//
});

// Using await
const contacts = await client.getList({ input: "example" });
```

#### `<action>(params?: Api3.Params): Api3.RequestBuilder`

Set the action for the request, and optionally set request parameters.

#### `addOption(option: string, value: Api3.Value): Api3.RequestBuilder`

Set [API options](https://docs.civicrm.org/dev/en/latest/api/v3/options/).

```ts
client.api3.contact.getList().addOption("limit", 10);
```

#### `options(requestOptions: RequestInit): Api3.RequestBuilder`

Set request options.

#### requestOptions

Accepts
the [same options as `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options).

Headers will be merged with the default headers.

```ts
client.api3.contact.getList().options({
headers: {
"X-Custom-Header": "value",
},
cache: "no-cache",
});
```

## Alternatives

- The [civicrm](https://www.npmjs.com/package/civicrm) package from [Tech to The People](https://github.com/TechToThePeople) offers a different approach to building requests and targets browsers and web workers as well as Node.js.
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"types": "./dist/index.d.ts",
"scripts": {
"build": "microbundle",
"prepublish": "microbundle",
"dev": "microbundle watch",
"test": "vitest"
},
Expand Down
29 changes: 29 additions & 0 deletions src/api3/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { forIn } from "lodash-es";

import { request } from "./request";
import { Api3 } from "./types";
import { RequestBuilder } from "./request-builder";
import { ClientConfig } from "../types";

export function createApi3Client<E extends Api3.EntitiesConfig>(
config: ClientConfig<any, E>,
) {
const client = {} as Api3.Client<E>;

forIn(config.api3!.entities, ({ name, actions }: any, entity: string) => {
Reflect.defineProperty(client, entity, {
get: () =>
new RequestBuilder(
name,
(requestParams, requestOptions) =>
request.bind(config)(requestParams, {
...config.requestOptions,
...requestOptions,
}),
actions,
),
});
});

return client;
}
36 changes: 36 additions & 0 deletions src/api3/request-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { forIn } from "lodash-es";

import { Api3 } from "./types";
import { RequestBuilder as BaseRequestBuilder } from "../lib/request-builder";

export class RequestBuilder<
A extends Api3.Actions,
Response = any,
> extends BaseRequestBuilder<Api3.RequestParams, Response> {
private action: string;
private params?: Api3.Params;
private opts: Record<string, Api3.Value> = {};

constructor(entity: string, request: Api3.RequestFn<Response>, actions: A) {
super(entity, request);

forIn(actions, (action: string, key: string) => {
this[key] = (params?: Api3.Params) => {
this.action = action as any;
this.params = params;

return this;
};
});
}

get requestParams(): Api3.RequestParams {
return [this.entity, this.action, this.params, this.opts];
}

addOption(option: string, value: Api3.Value) {
this.opts[option] = value;

return this;
}
}
26 changes: 26 additions & 0 deletions src/api3/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isEmpty } from "lodash-es";

import { Api3 } from "./types";
import { request as baseRequest } from "../lib/request";

const path = "civicrm/ajax/rest";

export async function request(
this: {
baseUrl: string;
apiKey: string;
debug?: boolean;
},
[entity, action, params, options]: Api3.RequestParams,
requestOptions: RequestInit = {},
) {
const json = isEmpty(options) ? { ...params } : { ...params, options };

const searchParams = new URLSearchParams({
entity: entity,
action: action,
json: JSON.stringify(json),
});

return baseRequest.bind(this)(path, searchParams, requestOptions);
}
41 changes: 41 additions & 0 deletions src/api3/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { RequestBuilder } from "./request-builder";
import { BaseRequestFn } from "../types";

export namespace Api3 {
export type EntitiesConfig = {
[key: string]: {
name: string;
actions: {
[key: string]: string;
};
};
};

export type Actions = {
[key: string]: string;
};
export type ActionMethods<A extends Actions> = Record<
keyof A,
(params?: Api3.Params) => RequestBuilder<A>
>;

export type Client<E extends EntitiesConfig> = {
[K in keyof E]: RequestBuilder<E[K]["actions"]> &
ActionMethods<E[K]["actions"]>;
};

export type Value =
| string
| number
| string[]
| number[]
| boolean
| boolean[]
| null;

export type Params = Record<string, Value>;
export type Options = Record<string, Value>;

export type RequestParams = [string, string, Params?, Options?];
export type RequestFn<Response> = BaseRequestFn<RequestParams, Response>;
}
26 changes: 26 additions & 0 deletions src/api4/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { forIn } from "lodash-es";

import { request } from "./request";
import { Api4 } from "./types";
import { RequestBuilder } from "./request-builder";
import { ClientConfig } from "../types";

export function createApi4Client<E extends Api4.EntitiesConfig>(
config: ClientConfig<E, any>,
) {
const client = {} as Api4.Client<E>;

forIn(config.entities, (entity: string, key: string) => {
Reflect.defineProperty(client, key, {
get: () =>
new RequestBuilder(entity, (requestParams, requestOptions) =>
request.bind(config)(requestParams, {
...config.requestOptions,
...requestOptions,
}),
),
});
});

return client;
}
Loading
Loading