@innobrain/onoffice-adapter-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/dist/activity.d.ts +12 -0
- package/dist/activity.js +67 -0
- package/dist/client.d.ts +69 -0
- package/dist/client.js +363 -0
- package/dist/constants.d.ts +55 -0
- package/dist/constants.js +53 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +9 -0
- package/dist/fields.d.ts +8 -0
- package/dist/fields.js +31 -0
- package/dist/files.d.ts +14 -0
- package/dist/files.js +73 -0
- package/dist/hmac.d.ts +1 -0
- package/dist/hmac.js +27 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/repositories.d.ts +120 -0
- package/dist/repositories.js +211 -0
- package/dist/search-criteria.d.ts +51 -0
- package/dist/search-criteria.js +166 -0
- package/dist/settings.d.ts +36 -0
- package/dist/settings.js +124 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +31 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# onoffice-adapter-js
|
|
2
|
+
|
|
3
|
+
A JavaScript/TypeScript client for the onOffice API, modeled after the Laravel adapter.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install onoffice-adapter-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
OnOfficeAction,
|
|
16
|
+
OnOfficeClient,
|
|
17
|
+
OnOfficeResourceType,
|
|
18
|
+
createRepositories,
|
|
19
|
+
} from "onoffice-adapter-js";
|
|
20
|
+
|
|
21
|
+
const client = new OnOfficeClient({
|
|
22
|
+
token: process.env.ON_OFFICE_TOKEN ?? "",
|
|
23
|
+
secret: process.env.ON_OFFICE_SECRET ?? "",
|
|
24
|
+
apiClaim: process.env.ON_OFFICE_API_CLAIM,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { raw, estate, address, activity, fields, searchCriteria, file, marketplace, settings } =
|
|
28
|
+
createRepositories(client);
|
|
29
|
+
|
|
30
|
+
const estates = await estate
|
|
31
|
+
.query()
|
|
32
|
+
.select(["Id", "kaufpreis"])
|
|
33
|
+
.where("status", 1)
|
|
34
|
+
.orderByField("kaufpreis")
|
|
35
|
+
.get();
|
|
36
|
+
|
|
37
|
+
// Count records without fetching
|
|
38
|
+
const totalEstates = await estate.query().where("status", 1).count();
|
|
39
|
+
|
|
40
|
+
// Iterate over all records
|
|
41
|
+
await estate.query().select(["Id"]).each((record) => {
|
|
42
|
+
console.log(record.id);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Delete a record
|
|
46
|
+
await estate.query().delete(12345);
|
|
47
|
+
|
|
48
|
+
// Raw repository for custom resource types
|
|
49
|
+
const custom = await raw.query(OnOfficeResourceType.Estate).select(["Id"]).find(12345);
|
|
50
|
+
|
|
51
|
+
const uploaded = await file.upload().uploadInBlocks(20480).saveAndLink("base64EncodedFile", {
|
|
52
|
+
module: "estate",
|
|
53
|
+
relatedRecordId: 12345,
|
|
54
|
+
file: "offer.pdf",
|
|
55
|
+
Art: "Dokument",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const firstEstate = await estate.query().select(["Id"]).find(12345);
|
|
59
|
+
|
|
60
|
+
const rawResponse = await client.request(OnOfficeAction.Read, OnOfficeResourceType.Estate, {
|
|
61
|
+
parameters: {
|
|
62
|
+
data: ["Id"],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const addresses = await address.query().select(["Name", "Vorname"]).get();
|
|
67
|
+
|
|
68
|
+
const activities = await activity
|
|
69
|
+
.query()
|
|
70
|
+
.estateId(123)
|
|
71
|
+
.addressIds([456, 789])
|
|
72
|
+
.select(["Nr", "Aktionsart", "Aktionstyp", "Datum", "Bemerkung"])
|
|
73
|
+
.get();
|
|
74
|
+
|
|
75
|
+
const fieldDefinitions = await fields.query().withModules(["estate", "address"]).get();
|
|
76
|
+
|
|
77
|
+
const criteria = await searchCriteria.query().mode("searchcriteria").ids([29]).get();
|
|
78
|
+
const criteriaFields = await searchCriteria.fields().get();
|
|
79
|
+
const criteriaMatches = await searchCriteria
|
|
80
|
+
.search()
|
|
81
|
+
.searchData({ range_plz: "52074" })
|
|
82
|
+
.outputAll()
|
|
83
|
+
.get();
|
|
84
|
+
|
|
85
|
+
const estateFiles = await file.estateFiles(12345, { includeImageUrl: "small" });
|
|
86
|
+
const addressFiles = await file.addressFiles(6789);
|
|
87
|
+
|
|
88
|
+
const users = await settings.users().select(["Nr", "Vorname"]).get();
|
|
89
|
+
const regions = await settings.regions().get();
|
|
90
|
+
const imprint = await settings.imprint().select(["Id"]).first();
|
|
91
|
+
const actions = await settings.actions().get();
|
|
92
|
+
|
|
93
|
+
const invoiceRecipient = await marketplace.invoiceRecipient({
|
|
94
|
+
transactionId: 1100135697,
|
|
95
|
+
userId: 42,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Repository classes are exported (`RawRepository`, `EstateRepository`, `AddressRepository`,
|
|
100
|
+
`ActivityRepository`, `FieldRepository`, `SearchCriteriaRepository`, `FileRepository`,
|
|
101
|
+
`MarketplaceRepository`, `SettingRepository`) if you want to wire your own container.
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- Provide a `fetcher` in the client config if your runtime does not expose `fetch` (Node < 18).
|
|
106
|
+
- Retries happen only on network errors by default; set `retry.onlyOnNetworkError` to `false` for all errors. Non-2xx HTTP responses throw `OnOfficeError` with status details.
|
|
107
|
+
- Query builders are generic so you can type records (e.g. `estate.query<MyEstate>()`).
|
|
108
|
+
- Query builders support Eloquent-style methods: `where()`, `whereIn()`, `first()`, `get()`, `find()`, `create()`, `modify()`, `delete()`, `count()`, `each()`.
|
|
109
|
+
- The `where()` method accepts typed operators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `in`, `not in`, `between`, `like`, `not like`.
|
|
110
|
+
- File access uses `file.estateFiles`/`file.addressFiles` and requires a record ID.
|
|
111
|
+
- File uploads for the `estate` module require an `Art` value (e.g. `Dokument`, `Foto`).
|
|
112
|
+
- Pagination defaults to `MAX_PAGE_SIZE` (500), which is also exported as a constant.
|
|
113
|
+
|
|
114
|
+
## Tooling
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run build
|
|
118
|
+
npm run test
|
|
119
|
+
npm run lint
|
|
120
|
+
npm run format
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Local testing
|
|
124
|
+
|
|
125
|
+
Create a `.env` in `onoffice-adapter-js` with:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
ON_OFFICE_TOKEN=...
|
|
129
|
+
ON_OFFICE_SECRET=...
|
|
130
|
+
ON_OFFICE_API_CLAIM=... # optional
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then run:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install
|
|
137
|
+
npm run dev
|
|
138
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OnOfficeQueryBuilder, OnOfficeClient } from "./client";
|
|
2
|
+
export declare class ActivityQueryBuilder<TRecord = unknown> extends OnOfficeQueryBuilder<TRecord> {
|
|
3
|
+
private estateIdValue?;
|
|
4
|
+
private addressIdsValue;
|
|
5
|
+
constructor(client: OnOfficeClient);
|
|
6
|
+
estateId(id: number): this;
|
|
7
|
+
addressIds(ids: number | number[]): this;
|
|
8
|
+
get(): Promise<TRecord[]>;
|
|
9
|
+
first(): Promise<TRecord | null>;
|
|
10
|
+
create(data: Record<string, unknown>): Promise<TRecord | null>;
|
|
11
|
+
private buildEstateOrAddressParameters;
|
|
12
|
+
}
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { OnOfficeAction, OnOfficeResourceType, ParameterKeys } from "./constants";
|
|
2
|
+
import { OnOfficeQueryBuilder } from "./client";
|
|
3
|
+
import { extractFirstRecord } from "./utils";
|
|
4
|
+
export class ActivityQueryBuilder extends OnOfficeQueryBuilder {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
super(client, OnOfficeResourceType.Activity);
|
|
7
|
+
this.addressIdsValue = [];
|
|
8
|
+
}
|
|
9
|
+
estateId(id) {
|
|
10
|
+
this.estateIdValue = id;
|
|
11
|
+
return this;
|
|
12
|
+
}
|
|
13
|
+
addressIds(ids) {
|
|
14
|
+
this.addressIdsValue = Array.isArray(ids) ? ids : [ids];
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
async get() {
|
|
18
|
+
return this.client.requestAll(OnOfficeAction.Read, this.resourceType, {
|
|
19
|
+
parameters: {
|
|
20
|
+
...this.buildEstateOrAddressParameters(false),
|
|
21
|
+
[ParameterKeys.data]: this.columns,
|
|
22
|
+
[ParameterKeys.filter]: this.buildFilters(),
|
|
23
|
+
[ParameterKeys.sortBy]: this.buildOrderBy(),
|
|
24
|
+
...this.customParameters,
|
|
25
|
+
},
|
|
26
|
+
}, {
|
|
27
|
+
pageSize: this.pageSizeValue,
|
|
28
|
+
offset: this.offsetValue,
|
|
29
|
+
limit: this.limitValue,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async first() {
|
|
33
|
+
const listLimit = this.limitValue > -1 ? this.limitValue : 1;
|
|
34
|
+
const response = await this.client.request(OnOfficeAction.Read, this.resourceType, {
|
|
35
|
+
parameters: {
|
|
36
|
+
...this.buildEstateOrAddressParameters(false),
|
|
37
|
+
[ParameterKeys.data]: this.columns,
|
|
38
|
+
[ParameterKeys.filter]: this.buildFilters(),
|
|
39
|
+
[ParameterKeys.listLimit]: listLimit,
|
|
40
|
+
[ParameterKeys.listOffset]: this.offsetValue,
|
|
41
|
+
[ParameterKeys.sortBy]: this.buildOrderBy(),
|
|
42
|
+
...this.customParameters,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return extractFirstRecord(response);
|
|
46
|
+
}
|
|
47
|
+
async create(data) {
|
|
48
|
+
const response = await this.client.request(OnOfficeAction.Create, this.resourceType, {
|
|
49
|
+
parameters: {
|
|
50
|
+
...this.buildEstateOrAddressParameters(true),
|
|
51
|
+
[ParameterKeys.data]: data,
|
|
52
|
+
...this.customParameters,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return extractFirstRecord(response);
|
|
56
|
+
}
|
|
57
|
+
buildEstateOrAddressParameters(isCreate) {
|
|
58
|
+
const parameters = {};
|
|
59
|
+
if (this.estateIdValue !== undefined) {
|
|
60
|
+
parameters.estateid = this.estateIdValue;
|
|
61
|
+
}
|
|
62
|
+
if (this.addressIdsValue.length > 0) {
|
|
63
|
+
parameters[isCreate ? "addressids" : "addressid"] = this.addressIdsValue;
|
|
64
|
+
}
|
|
65
|
+
return parameters;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OnOfficeAction } from "./constants";
|
|
2
|
+
import type { FilterOperator, OnOfficeApiResponse, OnOfficeClientConfig, OnOfficeRequestOptions, PaginationOptions } from "./types";
|
|
3
|
+
export declare class OnOfficeClient {
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly token;
|
|
6
|
+
private readonly secret;
|
|
7
|
+
private readonly apiClaim?;
|
|
8
|
+
private readonly headers;
|
|
9
|
+
private readonly retryCount;
|
|
10
|
+
private readonly retryDelayMs;
|
|
11
|
+
private readonly onlyOnNetworkError;
|
|
12
|
+
private readonly fetcher;
|
|
13
|
+
private readonly onRequest?;
|
|
14
|
+
private readonly onResponse?;
|
|
15
|
+
constructor(config: OnOfficeClientConfig);
|
|
16
|
+
resource<TRecord = unknown>(resourceType: string): OnOfficeQueryBuilder<TRecord>;
|
|
17
|
+
request<TRecord = unknown>(actionId: OnOfficeAction, resourceType: string, options?: OnOfficeRequestOptions): Promise<OnOfficeApiResponse<TRecord>>;
|
|
18
|
+
requestAll<TRecord = unknown>(actionId: OnOfficeAction, resourceType: string, options: OnOfficeRequestOptions, pagination?: PaginationOptions): Promise<TRecord[]>;
|
|
19
|
+
private sendRequest;
|
|
20
|
+
private throwIfResponseFailed;
|
|
21
|
+
private buildRequestPayload;
|
|
22
|
+
}
|
|
23
|
+
export declare class OnOfficeQueryBuilder<TRecord = unknown> {
|
|
24
|
+
protected readonly client: OnOfficeClient;
|
|
25
|
+
protected readonly resourceType: string;
|
|
26
|
+
protected columns: string[];
|
|
27
|
+
protected filters: Record<string, Array<{
|
|
28
|
+
op: string;
|
|
29
|
+
val: unknown;
|
|
30
|
+
}>>;
|
|
31
|
+
protected modifies: Record<string, unknown>;
|
|
32
|
+
protected orderBy: Array<[string, string]>;
|
|
33
|
+
protected limitValue: number;
|
|
34
|
+
protected pageSizeValue: number;
|
|
35
|
+
protected offsetValue: number;
|
|
36
|
+
protected customParameters: Record<string, unknown>;
|
|
37
|
+
constructor(client: OnOfficeClient, resourceType: string);
|
|
38
|
+
select(columns?: string[] | string): this;
|
|
39
|
+
addSelect(column: string[] | string): this;
|
|
40
|
+
orderByField(column: string, direction?: "asc" | "desc"): this;
|
|
41
|
+
orderByDesc(column: string): this;
|
|
42
|
+
addModify(column: string | Record<string, unknown>, value?: unknown): this;
|
|
43
|
+
offset(value: number): this;
|
|
44
|
+
limit(value: number): this;
|
|
45
|
+
pageSize(value: number): this;
|
|
46
|
+
where(column: string, value: unknown): this;
|
|
47
|
+
where(column: string, operator: FilterOperator, value: unknown): this;
|
|
48
|
+
whereNot(column: string, value: string | number): this;
|
|
49
|
+
whereIn(column: string, values: unknown[]): this;
|
|
50
|
+
whereNotIn(column: string, values: unknown[]): this;
|
|
51
|
+
whereBetween(column: string, start: string | number, end: string | number): this;
|
|
52
|
+
whereLike(column: string, value: string): this;
|
|
53
|
+
whereNotLike(column: string, value: string): this;
|
|
54
|
+
parameter(key: string, value: unknown): this;
|
|
55
|
+
parameters(parameters: Record<string, unknown>): this;
|
|
56
|
+
get(): Promise<TRecord[]>;
|
|
57
|
+
first(): Promise<TRecord | null>;
|
|
58
|
+
find(id: string | number): Promise<TRecord | null>;
|
|
59
|
+
create(data: Record<string, unknown>): Promise<TRecord | null>;
|
|
60
|
+
modify(id: string | number, data?: Record<string, unknown>): Promise<boolean>;
|
|
61
|
+
delete(id: string | number): Promise<boolean>;
|
|
62
|
+
count(): Promise<number>;
|
|
63
|
+
each(callback: (record: TRecord) => void | Promise<void>): Promise<void>;
|
|
64
|
+
protected buildFilters(): Record<string, Array<{
|
|
65
|
+
op: string;
|
|
66
|
+
val: unknown;
|
|
67
|
+
}>>;
|
|
68
|
+
protected buildOrderBy(): Record<string, string>;
|
|
69
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { MAX_PAGE_SIZE, OnOfficeAction, ParameterKeys } from "./constants";
|
|
2
|
+
import { OnOfficeError } from "./errors";
|
|
3
|
+
import { createHmacSignature } from "./hmac";
|
|
4
|
+
import { extractFirstRecord } from "./utils";
|
|
5
|
+
const DEFAULT_BASE_URL = "https://api.onoffice.de/api/stable/api.php";
|
|
6
|
+
const DEFAULT_HEADERS = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
Accept: "application/json",
|
|
9
|
+
};
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
export class OnOfficeClient {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
16
|
+
this.token = config.token;
|
|
17
|
+
this.secret = config.secret;
|
|
18
|
+
this.apiClaim = config.apiClaim;
|
|
19
|
+
this.headers = {
|
|
20
|
+
...DEFAULT_HEADERS,
|
|
21
|
+
...config.headers,
|
|
22
|
+
};
|
|
23
|
+
this.retryCount = Math.max(1, config.retry?.count ?? 3);
|
|
24
|
+
this.retryDelayMs = Math.max(1, config.retry?.delayMs ?? 200);
|
|
25
|
+
this.onlyOnNetworkError = config.retry?.onlyOnNetworkError ?? true;
|
|
26
|
+
this.fetcher = config.fetcher ?? globalThis.fetch?.bind(globalThis);
|
|
27
|
+
this.onRequest = config.onRequest;
|
|
28
|
+
this.onResponse = config.onResponse;
|
|
29
|
+
if (!this.fetcher) {
|
|
30
|
+
throw new Error("Fetch is not available. Provide a fetcher in the config.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
resource(resourceType) {
|
|
34
|
+
return new OnOfficeQueryBuilder(this, resourceType);
|
|
35
|
+
}
|
|
36
|
+
async request(actionId, resourceType, options = {}) {
|
|
37
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
38
|
+
const payload = await this.buildRequestPayload(actionId, resourceType, options, timestamp);
|
|
39
|
+
return this.sendRequest(payload);
|
|
40
|
+
}
|
|
41
|
+
async requestAll(actionId, resourceType, options, pagination = {}) {
|
|
42
|
+
const pageSize = pagination.pageSize ?? 500;
|
|
43
|
+
let offset = pagination.offset ?? 0;
|
|
44
|
+
const limit = pagination.limit ?? -1;
|
|
45
|
+
let maxPage = 0;
|
|
46
|
+
const records = [];
|
|
47
|
+
do {
|
|
48
|
+
const response = await this.request(actionId, resourceType, {
|
|
49
|
+
...options,
|
|
50
|
+
parameters: {
|
|
51
|
+
...options.parameters,
|
|
52
|
+
[ParameterKeys.listLimit]: pageSize,
|
|
53
|
+
[ParameterKeys.listOffset]: offset,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const dataRecords = response.response?.results?.[0]?.data?.records ?? [];
|
|
57
|
+
if (maxPage === 0) {
|
|
58
|
+
const countAbsolute = response.response?.results?.[0]?.data?.meta?.cntabsolute ?? 0;
|
|
59
|
+
maxPage = Math.ceil(countAbsolute / pageSize);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(dataRecords)) {
|
|
62
|
+
records.push(...dataRecords);
|
|
63
|
+
}
|
|
64
|
+
if (limit > -1 && records.length > limit) {
|
|
65
|
+
return records.slice(0, limit);
|
|
66
|
+
}
|
|
67
|
+
offset += pageSize;
|
|
68
|
+
const currentPage = offset / pageSize;
|
|
69
|
+
if (maxPage <= currentPage) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
} while (maxPage > 0);
|
|
73
|
+
return records;
|
|
74
|
+
}
|
|
75
|
+
async sendRequest(payload) {
|
|
76
|
+
let lastError;
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
this.onRequest?.({
|
|
79
|
+
url: this.baseUrl,
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { ...this.headers },
|
|
82
|
+
body: payload,
|
|
83
|
+
timestamp: startTime,
|
|
84
|
+
});
|
|
85
|
+
for (let attempt = 0; attempt < this.retryCount; attempt += 1) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await this.fetcher(this.baseUrl, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: this.headers,
|
|
90
|
+
body: JSON.stringify(payload),
|
|
91
|
+
});
|
|
92
|
+
if (response.ok === false) {
|
|
93
|
+
const body = typeof response.text === "function" ? await response.text().catch(() => "") : "";
|
|
94
|
+
throw new OnOfficeError(`HTTP ${response.status}: ${response.statusText}`, response.status, {
|
|
95
|
+
response: body
|
|
96
|
+
? { status: response.status, statusText: response.statusText, body }
|
|
97
|
+
: { status: response.status, statusText: response.statusText },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const data = (await response.json());
|
|
101
|
+
this.onResponse?.({
|
|
102
|
+
url: this.baseUrl,
|
|
103
|
+
status: response.status,
|
|
104
|
+
statusText: response.statusText,
|
|
105
|
+
body: data,
|
|
106
|
+
durationMs: Date.now() - startTime,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
this.throwIfResponseFailed(data);
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
lastError = error;
|
|
114
|
+
const shouldRetry = !this.onlyOnNetworkError || error instanceof TypeError;
|
|
115
|
+
if (!shouldRetry || attempt >= this.retryCount - 1) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
await sleep(this.retryDelayMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw lastError;
|
|
122
|
+
}
|
|
123
|
+
throwIfResponseFailed(response) {
|
|
124
|
+
const status = response.status ?? {};
|
|
125
|
+
const statusCode = status.code ?? 500;
|
|
126
|
+
const statusErrorCode = status.errorcode ?? 0;
|
|
127
|
+
const responseStatusCode = response.response?.results?.[0]?.status?.errorcode ?? 0;
|
|
128
|
+
const errorMessage = status.message || `Status code: ${statusCode}`;
|
|
129
|
+
const responseErrorMessage = response.response?.results?.[0]?.status?.message || `Status code: ${responseStatusCode}`;
|
|
130
|
+
if (statusCode >= 300 && statusErrorCode > 0 && responseStatusCode === 0) {
|
|
131
|
+
throw new OnOfficeError(errorMessage, statusErrorCode, {
|
|
132
|
+
isResponseError: true,
|
|
133
|
+
response,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (statusCode >= 300 && statusErrorCode <= 0 && responseStatusCode === 0) {
|
|
137
|
+
throw new OnOfficeError(errorMessage, statusCode, { response });
|
|
138
|
+
}
|
|
139
|
+
if (responseStatusCode > 0) {
|
|
140
|
+
throw new OnOfficeError(responseErrorMessage, responseStatusCode, {
|
|
141
|
+
isResponseError: true,
|
|
142
|
+
response,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async buildRequestPayload(actionId, resourceType, options, timestamp) {
|
|
147
|
+
const resourceId = options.resourceId ?? "";
|
|
148
|
+
const identifier = options.identifier ?? "";
|
|
149
|
+
const parameters = {
|
|
150
|
+
...(this.apiClaim ? { [ParameterKeys.extendedClaim]: this.apiClaim } : {}),
|
|
151
|
+
...(options.parameters ?? {}),
|
|
152
|
+
};
|
|
153
|
+
const message = `${timestamp}${this.token}${resourceType}${actionId}`;
|
|
154
|
+
const hmac = await createHmacSignature(message, this.secret);
|
|
155
|
+
return {
|
|
156
|
+
token: this.token,
|
|
157
|
+
request: {
|
|
158
|
+
actions: [
|
|
159
|
+
{
|
|
160
|
+
actionid: actionId,
|
|
161
|
+
resourceid: resourceId,
|
|
162
|
+
resourcetype: resourceType,
|
|
163
|
+
identifier,
|
|
164
|
+
timestamp,
|
|
165
|
+
hmac,
|
|
166
|
+
hmac_version: 2,
|
|
167
|
+
parameters,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export class OnOfficeQueryBuilder {
|
|
175
|
+
constructor(client, resourceType) {
|
|
176
|
+
this.client = client;
|
|
177
|
+
this.resourceType = resourceType;
|
|
178
|
+
this.columns = [];
|
|
179
|
+
this.filters = {};
|
|
180
|
+
this.modifies = {};
|
|
181
|
+
this.orderBy = [];
|
|
182
|
+
this.limitValue = -1;
|
|
183
|
+
this.pageSizeValue = MAX_PAGE_SIZE;
|
|
184
|
+
this.offsetValue = 0;
|
|
185
|
+
this.customParameters = {};
|
|
186
|
+
}
|
|
187
|
+
select(columns = ["ID"]) {
|
|
188
|
+
this.columns = Array.isArray(columns) ? columns : [columns];
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
addSelect(column) {
|
|
192
|
+
const next = Array.isArray(column) ? column : [column];
|
|
193
|
+
this.columns = [...this.columns, ...next];
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
orderByField(column, direction = "asc") {
|
|
197
|
+
this.orderBy.push([column, direction.toUpperCase()]);
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
orderByDesc(column) {
|
|
201
|
+
return this.orderByField(column, "desc");
|
|
202
|
+
}
|
|
203
|
+
addModify(column, value) {
|
|
204
|
+
if (typeof column === "string") {
|
|
205
|
+
this.modifies[column] = value;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.modifies = { ...this.modifies, ...column };
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
offset(value) {
|
|
213
|
+
this.offsetValue = Math.max(0, value);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
limit(value) {
|
|
217
|
+
this.limitValue = Math.max(-1, value);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
pageSize(value) {
|
|
221
|
+
const bounded = Math.min(MAX_PAGE_SIZE, Math.max(1, value));
|
|
222
|
+
this.pageSizeValue = bounded;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
where(column, operatorOrValue, value) {
|
|
226
|
+
let operator = "=";
|
|
227
|
+
let filterValue;
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
filterValue = operatorOrValue;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
operator = operatorOrValue;
|
|
233
|
+
filterValue = value;
|
|
234
|
+
}
|
|
235
|
+
if (!this.filters[column]) {
|
|
236
|
+
this.filters[column] = [];
|
|
237
|
+
}
|
|
238
|
+
this.filters[column].push({ op: operator, val: filterValue });
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
whereNot(column, value) {
|
|
242
|
+
return this.where(column, "!=", value);
|
|
243
|
+
}
|
|
244
|
+
whereIn(column, values) {
|
|
245
|
+
return this.where(column, "in", values);
|
|
246
|
+
}
|
|
247
|
+
whereNotIn(column, values) {
|
|
248
|
+
return this.where(column, "not in", values);
|
|
249
|
+
}
|
|
250
|
+
whereBetween(column, start, end) {
|
|
251
|
+
return this.where(column, "between", [start, end]);
|
|
252
|
+
}
|
|
253
|
+
whereLike(column, value) {
|
|
254
|
+
return this.where(column, "like", value);
|
|
255
|
+
}
|
|
256
|
+
whereNotLike(column, value) {
|
|
257
|
+
return this.where(column, "not like", value);
|
|
258
|
+
}
|
|
259
|
+
parameter(key, value) {
|
|
260
|
+
this.customParameters[key] = value;
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
parameters(parameters) {
|
|
264
|
+
this.customParameters = {
|
|
265
|
+
...this.customParameters,
|
|
266
|
+
...parameters,
|
|
267
|
+
};
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
async get() {
|
|
271
|
+
return this.client.requestAll(OnOfficeAction.Read, this.resourceType, {
|
|
272
|
+
parameters: {
|
|
273
|
+
[ParameterKeys.data]: this.columns,
|
|
274
|
+
[ParameterKeys.filter]: this.buildFilters(),
|
|
275
|
+
[ParameterKeys.sortBy]: this.buildOrderBy(),
|
|
276
|
+
...this.customParameters,
|
|
277
|
+
},
|
|
278
|
+
}, {
|
|
279
|
+
pageSize: this.pageSizeValue,
|
|
280
|
+
offset: this.offsetValue,
|
|
281
|
+
limit: this.limitValue,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async first() {
|
|
285
|
+
const listLimit = this.limitValue > -1 ? this.limitValue : 1;
|
|
286
|
+
const response = await this.client.request(OnOfficeAction.Read, this.resourceType, {
|
|
287
|
+
parameters: {
|
|
288
|
+
[ParameterKeys.data]: this.columns,
|
|
289
|
+
[ParameterKeys.filter]: this.buildFilters(),
|
|
290
|
+
[ParameterKeys.listLimit]: listLimit,
|
|
291
|
+
[ParameterKeys.listOffset]: this.offsetValue,
|
|
292
|
+
[ParameterKeys.sortBy]: this.buildOrderBy(),
|
|
293
|
+
...this.customParameters,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
return extractFirstRecord(response);
|
|
297
|
+
}
|
|
298
|
+
async find(id) {
|
|
299
|
+
const response = await this.client.request(OnOfficeAction.Read, this.resourceType, {
|
|
300
|
+
resourceId: id,
|
|
301
|
+
parameters: {
|
|
302
|
+
[ParameterKeys.data]: this.columns,
|
|
303
|
+
...this.customParameters,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
return extractFirstRecord(response);
|
|
307
|
+
}
|
|
308
|
+
async create(data) {
|
|
309
|
+
const response = await this.client.request(OnOfficeAction.Create, this.resourceType, {
|
|
310
|
+
parameters: {
|
|
311
|
+
[ParameterKeys.data]: data,
|
|
312
|
+
...this.customParameters,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
return extractFirstRecord(response);
|
|
316
|
+
}
|
|
317
|
+
async modify(id, data) {
|
|
318
|
+
const payload = data ?? this.modifies;
|
|
319
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
320
|
+
throw new Error("No data provided for modify().");
|
|
321
|
+
}
|
|
322
|
+
await this.client.request(OnOfficeAction.Modify, this.resourceType, {
|
|
323
|
+
resourceId: id,
|
|
324
|
+
parameters: {
|
|
325
|
+
[ParameterKeys.data]: payload,
|
|
326
|
+
...this.customParameters,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
async delete(id) {
|
|
332
|
+
await this.client.request(OnOfficeAction.Delete, this.resourceType, {
|
|
333
|
+
resourceId: id,
|
|
334
|
+
parameters: {
|
|
335
|
+
...this.customParameters,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
async count() {
|
|
341
|
+
const response = await this.client.request(OnOfficeAction.Read, this.resourceType, {
|
|
342
|
+
parameters: {
|
|
343
|
+
[ParameterKeys.data]: [],
|
|
344
|
+
[ParameterKeys.filter]: this.buildFilters(),
|
|
345
|
+
[ParameterKeys.listLimit]: 1,
|
|
346
|
+
...this.customParameters,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
return response.response?.results?.[0]?.data?.meta?.cntabsolute ?? 0;
|
|
350
|
+
}
|
|
351
|
+
async each(callback) {
|
|
352
|
+
const records = await this.get();
|
|
353
|
+
for (const record of records) {
|
|
354
|
+
await callback(record);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
buildFilters() {
|
|
358
|
+
return { ...this.filters };
|
|
359
|
+
}
|
|
360
|
+
buildOrderBy() {
|
|
361
|
+
return Object.fromEntries(this.orderBy.map(([column, direction]) => [column, direction]));
|
|
362
|
+
}
|
|
363
|
+
}
|