@legalize-dev/sdk 0.1.0 → 0.2.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/CHANGELOG.md +21 -0
- package/README.md +25 -0
- package/dist/index.cjs +119 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +119 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning][semver].
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.2.0] — 2026-06-17
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `requestRaw(method, path, options?)` on `Legalize` — the low-level
|
|
17
|
+
escape hatch for content negotiation. It fetches any endpoint in a
|
|
18
|
+
non-JSON wire format and returns a `RawResponse` (`.statusCode`,
|
|
19
|
+
`.content` as raw bytes, `.text`, `.contentType`, `.headers`) without
|
|
20
|
+
JSON-decoding. `options.format` controls the `Accept` header:
|
|
21
|
+
`"xml"` (the default) sends `application/xml`, `"json"` sends
|
|
22
|
+
`application/json`, and any other value is used verbatim as the media
|
|
23
|
+
type. Same retry policy, `lastResponse` population, and typed error
|
|
24
|
+
hierarchy as `request`.
|
|
25
|
+
- `RawResponse` type, exported from the package, with a `.json()`
|
|
26
|
+
convenience parser. The SDK ships no XML parser and keeps zero runtime
|
|
27
|
+
dependencies — parse `.text`/`.content` with your own library.
|
|
28
|
+
- `RequestRawOptions` type and the `formatToAccept` helper are exported.
|
|
29
|
+
|
|
30
|
+
The typed resource methods still return JSON-parsed models; XML is opt-in
|
|
31
|
+
per call via `requestRaw`. See the "Response formats" API docs.
|
|
32
|
+
|
|
12
33
|
## [0.1.0] — 2026-04-20
|
|
13
34
|
|
|
14
35
|
### Added
|
package/README.md
CHANGED
|
@@ -65,6 +65,31 @@ const past = await client.laws.atCommit("es", "ley_organica_3_2018", oldest);
|
|
|
65
65
|
console.log(past.content_md); // Markdown at that revision
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### XML (and other raw formats)
|
|
69
|
+
|
|
70
|
+
The typed methods always return JSON-parsed models. When your app speaks
|
|
71
|
+
XML, use `requestRaw` to fetch any endpoint in another wire format via
|
|
72
|
+
content negotiation — it sets `Accept` and hands you the body untouched:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const res = await client.requestRaw("GET", "/api/v1/es/laws/BOE-A-1978-31229");
|
|
76
|
+
res.contentType; // "application/xml; charset=utf-8"
|
|
77
|
+
const xmlText = res.text; // the raw XML string
|
|
78
|
+
res.content; // the raw bytes (Uint8Array)
|
|
79
|
+
|
|
80
|
+
// format: "json" or any explicit media type works the same way:
|
|
81
|
+
const data = (
|
|
82
|
+
await client.requestRaw("GET", "/api/v1/countries", { format: "json" })
|
|
83
|
+
).json();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`requestRaw` defaults to `format: "xml"`. The SDK has **zero runtime
|
|
87
|
+
dependencies and ships no XML parser** — parse `res.text` (or
|
|
88
|
+
`res.content`) with your own library (`fast-xml-parser`,
|
|
89
|
+
`@xmldom/xmldom`, …). Errors raise the same typed exceptions as the JSON
|
|
90
|
+
methods (the error body is in the negotiated format). See the
|
|
91
|
+
[Response formats](https://legalize.dev/docs/formats) docs.
|
|
92
|
+
|
|
68
93
|
### Abort + timeout
|
|
69
94
|
|
|
70
95
|
Every method accepts a standard `AbortSignal`:
|
package/dist/index.cjs
CHANGED
|
@@ -744,7 +744,7 @@ var Webhooks = class {
|
|
|
744
744
|
};
|
|
745
745
|
|
|
746
746
|
// src/version.ts
|
|
747
|
-
var SDK_VERSION = "0.
|
|
747
|
+
var SDK_VERSION = "0.2.0";
|
|
748
748
|
|
|
749
749
|
// src/client.ts
|
|
750
750
|
function defaultUserAgent() {
|
|
@@ -755,6 +755,22 @@ function stripTrailingSlashes(s) {
|
|
|
755
755
|
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
756
756
|
return s.slice(0, end);
|
|
757
757
|
}
|
|
758
|
+
var FORMAT_ALIASES = {
|
|
759
|
+
xml: "application/xml",
|
|
760
|
+
json: "application/json"
|
|
761
|
+
};
|
|
762
|
+
function formatToAccept(format) {
|
|
763
|
+
const key = (format ?? "").trim().toLowerCase();
|
|
764
|
+
if (!key) return "application/xml";
|
|
765
|
+
return FORMAT_ALIASES[key] ?? format;
|
|
766
|
+
}
|
|
767
|
+
function headersToRecord(headers) {
|
|
768
|
+
const out = {};
|
|
769
|
+
headers.forEach((value, key) => {
|
|
770
|
+
out[key] = value;
|
|
771
|
+
});
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
758
774
|
var Legalize = class {
|
|
759
775
|
countries;
|
|
760
776
|
jurisdictions;
|
|
@@ -801,19 +817,100 @@ var Legalize = class {
|
|
|
801
817
|
*/
|
|
802
818
|
async request(method, path, options = {}) {
|
|
803
819
|
const upperMethod = method.toUpperCase();
|
|
804
|
-
const url = this.buildUrl(path, options.params);
|
|
805
820
|
const headers = { ...this._headers };
|
|
806
821
|
if (options.extraHeaders) Object.assign(headers, options.extraHeaders);
|
|
807
822
|
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
808
823
|
const hasJson = options.json !== void 0;
|
|
809
824
|
if (hasJson) headers["Content-Type"] = "application/json";
|
|
825
|
+
const response = await this.sendWithRetry(upperMethod, path, headers, options, hasJson);
|
|
826
|
+
this._lastResponse = response;
|
|
827
|
+
if (response.status === 204) return null;
|
|
828
|
+
const text = await response.text();
|
|
829
|
+
if (!text) return null;
|
|
830
|
+
try {
|
|
831
|
+
return JSON.parse(text);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
throw new APIError({
|
|
834
|
+
message: "Server returned non-JSON body",
|
|
835
|
+
statusCode: response.status,
|
|
836
|
+
body: text,
|
|
837
|
+
response,
|
|
838
|
+
cause: err
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Execute a request and return the raw, non-JSON-decoded body.
|
|
844
|
+
*
|
|
845
|
+
* The escape hatch for content negotiation: the typed resource methods
|
|
846
|
+
* always return JSON models, but `requestRaw` lets you fetch any
|
|
847
|
+
* endpoint in another wire format. `format` controls the `Accept`
|
|
848
|
+
* header — `"xml"` (the default) requests `application/xml`, `"json"`
|
|
849
|
+
* requests `application/json`, and any other value is sent verbatim as
|
|
850
|
+
* the media type.
|
|
851
|
+
*
|
|
852
|
+
* The SDK has zero runtime deps and ships no XML parser — parse the
|
|
853
|
+
* returned `.text` (or `.content`) with your own library.
|
|
854
|
+
*
|
|
855
|
+
* Example:
|
|
856
|
+
*
|
|
857
|
+
* const res = await client.requestRaw("GET", "/api/v1/es/laws/BOE-A-1978-31229");
|
|
858
|
+
* res.contentType; // "application/xml; charset=utf-8"
|
|
859
|
+
* const xmlText = res.text; // already application/xml
|
|
860
|
+
*
|
|
861
|
+
* Throws the same APIError subclasses as {@link request} on a non-2xx
|
|
862
|
+
* response; the error body is in whatever format you negotiated.
|
|
863
|
+
*/
|
|
864
|
+
async requestRaw(method, path, options = {}) {
|
|
865
|
+
const upperMethod = method.toUpperCase();
|
|
866
|
+
const headers = { ...this._headers };
|
|
867
|
+
headers["Accept"] = formatToAccept(options.format);
|
|
868
|
+
if (options.extraHeaders) Object.assign(headers, options.extraHeaders);
|
|
869
|
+
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
870
|
+
const response = await this.sendWithRetry(upperMethod, path, headers, options, false);
|
|
871
|
+
this._lastResponse = response;
|
|
872
|
+
return rawFromResponse(response);
|
|
873
|
+
}
|
|
874
|
+
/** Release any resources held by the client. Kept for API symmetry. */
|
|
875
|
+
async close() {
|
|
876
|
+
}
|
|
877
|
+
/** TS 5.2+ `using` / `await using` support. */
|
|
878
|
+
async [Symbol.asyncDispose]() {
|
|
879
|
+
await this.close();
|
|
880
|
+
}
|
|
881
|
+
// ---- internals --------------------------------------------------------
|
|
882
|
+
buildUrl(path, params) {
|
|
883
|
+
let base;
|
|
884
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
885
|
+
base = path;
|
|
886
|
+
} else {
|
|
887
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
888
|
+
base = this._baseUrl + p;
|
|
889
|
+
}
|
|
890
|
+
if (!params) return base;
|
|
891
|
+
const query = buildQueryString(params);
|
|
892
|
+
if (!query) return base;
|
|
893
|
+
return base.includes("?") ? `${base}&${query}` : `${base}?${query}`;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Run the retry loop and return the raw 2xx `Response`.
|
|
897
|
+
*
|
|
898
|
+
* Shared by {@link request} (which then JSON-parses) and
|
|
899
|
+
* {@link requestRaw} (which builds a `RawResponse`). Applies the retry
|
|
900
|
+
* policy on transport errors and retryable statuses, populates
|
|
901
|
+
* `lastResponse` on the failing response before raising, and throws the
|
|
902
|
+
* typed `APIError` hierarchy on a final non-2xx. The returned response
|
|
903
|
+
* body is left unread for the caller to consume.
|
|
904
|
+
*/
|
|
905
|
+
async sendWithRetry(method, path, headers, options, hasJson) {
|
|
906
|
+
const url = this.buildUrl(path, options.params);
|
|
810
907
|
let attempt = 0;
|
|
811
908
|
while (true) {
|
|
812
909
|
let response;
|
|
813
910
|
try {
|
|
814
|
-
response = await this.sendOnce(url,
|
|
911
|
+
response = await this.sendOnce(url, method, headers, options, hasJson);
|
|
815
912
|
} catch (err) {
|
|
816
|
-
const shouldRetry2 = this._retry.shouldRetry(attempt, { method
|
|
913
|
+
const shouldRetry2 = this._retry.shouldRetry(attempt, { method });
|
|
817
914
|
if (!shouldRetry2) {
|
|
818
915
|
throw wrapTransportError(err);
|
|
819
916
|
}
|
|
@@ -823,25 +920,11 @@ var Legalize = class {
|
|
|
823
920
|
continue;
|
|
824
921
|
}
|
|
825
922
|
if (response.status >= 200 && response.status < 300) {
|
|
826
|
-
|
|
827
|
-
if (response.status === 204) return null;
|
|
828
|
-
const text = await response.text();
|
|
829
|
-
if (!text) return null;
|
|
830
|
-
try {
|
|
831
|
-
return JSON.parse(text);
|
|
832
|
-
} catch (err) {
|
|
833
|
-
throw new APIError({
|
|
834
|
-
message: "Server returned non-JSON body",
|
|
835
|
-
statusCode: response.status,
|
|
836
|
-
body: text,
|
|
837
|
-
response,
|
|
838
|
-
cause: err
|
|
839
|
-
});
|
|
840
|
-
}
|
|
923
|
+
return response;
|
|
841
924
|
}
|
|
842
925
|
const shouldRetry = this._retry.shouldRetry(attempt, {
|
|
843
926
|
status: response.status,
|
|
844
|
-
method
|
|
927
|
+
method
|
|
845
928
|
});
|
|
846
929
|
if (!shouldRetry) {
|
|
847
930
|
this._lastResponse = response;
|
|
@@ -857,27 +940,6 @@ var Legalize = class {
|
|
|
857
940
|
attempt += 1;
|
|
858
941
|
}
|
|
859
942
|
}
|
|
860
|
-
/** Release any resources held by the client. Kept for API symmetry. */
|
|
861
|
-
async close() {
|
|
862
|
-
}
|
|
863
|
-
/** TS 5.2+ `using` / `await using` support. */
|
|
864
|
-
async [Symbol.asyncDispose]() {
|
|
865
|
-
await this.close();
|
|
866
|
-
}
|
|
867
|
-
// ---- internals --------------------------------------------------------
|
|
868
|
-
buildUrl(path, params) {
|
|
869
|
-
let base;
|
|
870
|
-
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
871
|
-
base = path;
|
|
872
|
-
} else {
|
|
873
|
-
const p = path.startsWith("/") ? path : `/${path}`;
|
|
874
|
-
base = this._baseUrl + p;
|
|
875
|
-
}
|
|
876
|
-
if (!params) return base;
|
|
877
|
-
const query = buildQueryString(params);
|
|
878
|
-
if (!query) return base;
|
|
879
|
-
return base.includes("?") ? `${base}&${query}` : `${base}?${query}`;
|
|
880
|
-
}
|
|
881
943
|
async sendOnce(url, method, headers, options, hasJson) {
|
|
882
944
|
const controller = new AbortController();
|
|
883
945
|
const timeoutMs = this._timeout;
|
|
@@ -955,6 +1017,21 @@ async function errorFromResponse(response) {
|
|
|
955
1017
|
}
|
|
956
1018
|
return APIError.fromResponse(response, text, data);
|
|
957
1019
|
}
|
|
1020
|
+
async function rawFromResponse(response) {
|
|
1021
|
+
const buffer = await response.arrayBuffer();
|
|
1022
|
+
const content = new Uint8Array(buffer);
|
|
1023
|
+
const text = new TextDecoder().decode(content);
|
|
1024
|
+
return {
|
|
1025
|
+
statusCode: response.status,
|
|
1026
|
+
content,
|
|
1027
|
+
text,
|
|
1028
|
+
contentType: response.headers.get("content-type") ?? "",
|
|
1029
|
+
headers: headersToRecord(response.headers),
|
|
1030
|
+
json() {
|
|
1031
|
+
return JSON.parse(text);
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
958
1035
|
function wrapTransportError(err) {
|
|
959
1036
|
if (err instanceof Error) {
|
|
960
1037
|
const isAbort = err.name === "AbortError" || err.code === "ABORT_ERR";
|
|
@@ -1120,6 +1197,7 @@ exports.WebhookVerificationError = WebhookVerificationError;
|
|
|
1120
1197
|
exports.Webhooks = Webhooks;
|
|
1121
1198
|
exports.buildQueryString = buildQueryString;
|
|
1122
1199
|
exports.defaultUserAgent = defaultUserAgent;
|
|
1200
|
+
exports.formatToAccept = formatToAccept;
|
|
1123
1201
|
exports.parseRetryAfter = parseRetryAfter;
|
|
1124
1202
|
exports.resolveApiKey = resolveApiKey;
|
|
1125
1203
|
exports.resolveApiVersion = resolveApiVersion;
|