@leodamours/jsonapi-client 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Léo Damours
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # @leodamours/jsonapi-client
2
+
3
+ [![CI](https://github.com/LeoDamours/json-api-client/actions/workflows/ci.yml/badge.svg)](https://github.com/LeoDamours/json-api-client/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@leodamours/jsonapi-client)](https://www.npmjs.com/package/@leodamours/jsonapi-client)
5
+
6
+ A typed HTTP client for [JSON:API](https://jsonapi.org/) — full CRUD with automatic type inference, powered by Axios.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @leodamours/jsonapi-client @leodamours/jsonapi-dsl axios
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```ts
17
+ import { defineResource, t, hasOne, hasMany } from "@leodamours/jsonapi-dsl";
18
+ import { createClient } from "@leodamours/jsonapi-client";
19
+
20
+ const User = defineResource(
21
+ "users",
22
+ {
23
+ name: t.string(),
24
+ email: t.string(),
25
+ age: t.nullable(t.number()),
26
+ },
27
+ {
28
+ posts: hasMany("posts"),
29
+ company: hasOne("companies"),
30
+ }
31
+ );
32
+
33
+ const api = createClient({ baseURL: "https://api.example.com" });
34
+
35
+ // CREATE — attributes fully typed, relationships type-checked
36
+ const created = await api.create(User, {
37
+ attributes: { name: "Leo", email: "leo@example.com", age: null },
38
+ relationships: { company: { type: "companies", id: "1" } },
39
+ });
40
+ console.log(created.data.attributes.name); // string
41
+
42
+ // FIND ONE
43
+ const user = await api.findOne(User, "1", { include: "posts" });
44
+ console.log(user.data.attributes.email); // string
45
+
46
+ // FIND ALL with pagination
47
+ const users = await api.findAll(User, {
48
+ page: { size: 10, number: 1 },
49
+ filter: { active: true },
50
+ sort: ["-createdAt", "name"],
51
+ });
52
+ for (const u of users.data) {
53
+ console.log(u.attributes.name);
54
+ }
55
+
56
+ // UPDATE — attributes are Partial
57
+ await api.update(User, "1", {
58
+ attributes: { name: "Updated" },
59
+ });
60
+
61
+ // DELETE
62
+ await api.remove(User, "1");
63
+ ```
64
+
65
+ ## Client Configuration
66
+
67
+ ```ts
68
+ import { createClient } from "@leodamours/jsonapi-client";
69
+
70
+ const api = createClient({
71
+ baseURL: "https://api.example.com",
72
+ jwtToken: "your-jwt-token", // sets Authorization: Bearer ...
73
+ headers: { "X-Custom": "value" }, // extra headers
74
+ timeout: 5000, // request timeout in ms
75
+ });
76
+
77
+ // Update config at runtime (e.g. after login)
78
+ api.updateConfig({ jwtToken: "new-token" });
79
+
80
+ // Clear auth
81
+ api.updateConfig({ jwtToken: null as any });
82
+
83
+ // Access underlying Axios instance
84
+ const axiosInstance = api.getAxiosInstance();
85
+ axiosInstance.interceptors.response.use(/* ... */);
86
+ ```
87
+
88
+ ## Per-Verb Attribute Narrowing
89
+
90
+ When a resource is defined with `create`/`update` options, the client enforces which attributes each verb accepts:
91
+
92
+ ```ts
93
+ const Author = defineResource(
94
+ "authors",
95
+ {
96
+ name: t.string(),
97
+ email: t.string(),
98
+ createdAt: t.string(),
99
+ },
100
+ { posts: hasMany("posts") },
101
+ {
102
+ create: ["name", "email"] as const,
103
+ update: ["name"] as const,
104
+ }
105
+ );
106
+
107
+ // Only name and email accepted (createdAt is rejected at compile time)
108
+ await api.create(Author, {
109
+ attributes: { name: "Leo", email: "leo@example.com" },
110
+ });
111
+
112
+ // Only name accepted
113
+ await api.update(Author, "1", {
114
+ attributes: { name: "Updated" },
115
+ });
116
+
117
+ // Response still has all attributes
118
+ const doc = await api.findOne(Author, "1");
119
+ doc.data.attributes.createdAt; // string
120
+ ```
121
+
122
+ ## Payload Types
123
+
124
+ Use `CreatePayload` and `UpdatePayload` for standalone typing:
125
+
126
+ ```ts
127
+ import type { CreatePayload, UpdatePayload } from "@leodamours/jsonapi-client";
128
+
129
+ type CreateUser = CreatePayload<typeof User>;
130
+ // { attributes: { name: string; email: string; age: number | null }; relationships?: ... }
131
+
132
+ type UpdateUser = UpdatePayload<typeof User>;
133
+ // { attributes?: Partial<{ name: string; email: string; age: number | null }>; relationships?: ... }
134
+ ```
135
+
136
+ ## Query Parameters
137
+
138
+ The client serializes query params following JSON:API conventions:
139
+
140
+ ```ts
141
+ await api.findAll(User, {
142
+ page: { size: 10, number: 2 }, // → page[size]=10&page[number]=2
143
+ filter: { active: true }, // → filter[active]=true
144
+ sort: ["-createdAt", "name"], // → sort=-createdAt,name
145
+ include: ["posts", "company"], // → include=posts,company
146
+ fields: { users: ["name", "email"] },// → fields[users]=name,email
147
+ });
148
+ ```
149
+
150
+ ## Serialization Utilities
151
+
152
+ Lower-level serialization functions are exported for custom use:
153
+
154
+ ```ts
155
+ import {
156
+ serializeCreatePayload,
157
+ serializeUpdatePayload,
158
+ serializeQueryParams,
159
+ serializeRelationships,
160
+ } from "@leodamours/jsonapi-client";
161
+ ```
162
+
163
+ ## Axios Integration
164
+
165
+ The client uses Axios under the hood. All CRUD methods accept an optional `AxiosRequestConfig` as the last argument:
166
+
167
+ ```ts
168
+ await api.findOne(User, "1", undefined, {
169
+ signal: abortController.signal,
170
+ headers: { "X-Request-Id": "abc" },
171
+ });
172
+ ```
173
+
174
+ ## License
175
+
176
+ MIT
@@ -0,0 +1,130 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+ import { ResourceDefinition, InferAttributesSimple, RelationshipsSchema, JsonApiDocument, InferResource, JsonApiCollectionDocument, JsonApiRelationships } from '@leodamours/jsonapi-dsl';
3
+
4
+ type SimplifiedHasOne<T extends string> = {
5
+ type: T;
6
+ id: string;
7
+ } | null;
8
+ type SimplifiedHasMany<T extends string> = Array<{
9
+ type: T;
10
+ id: string;
11
+ }>;
12
+ /**
13
+ * Indexed lookup for simplified relationship shapes — avoids conditional types.
14
+ */
15
+ interface SimplifiedRelTypeMap<T extends string> {
16
+ one: SimplifiedHasOne<T>;
17
+ many: SimplifiedHasMany<T>;
18
+ }
19
+ type SimplifiedRelationships<Schema extends RelationshipsSchema> = {
20
+ [K in keyof Schema]: SimplifiedRelTypeMap<Schema[K]["type"]>[Schema[K]["_rel"]];
21
+ };
22
+ type RelationshipClause<Rels extends RelationshipsSchema> = {
23
+ relationships?: Partial<SimplifiedRelationships<Rels>>;
24
+ };
25
+ /**
26
+ * Payload for creating a resource.
27
+ * When the definition has `createKeys`, only those attribute keys are accepted.
28
+ */
29
+ type CreatePayload<Def extends ResourceDefinition> = Def extends {
30
+ readonly createKeys: readonly (infer CK extends string)[];
31
+ } ? {
32
+ attributes: Pick<InferAttributesSimple<Def["attributes"]>, CK & keyof Def["attributes"]>;
33
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
34
+ } : {
35
+ attributes: InferAttributesSimple<Def["attributes"]>;
36
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
37
+ };
38
+ /**
39
+ * Payload for updating a resource.
40
+ * When the definition has `updateKeys`, only those attribute keys are accepted.
41
+ */
42
+ type UpdatePayload<Def extends ResourceDefinition> = Def extends {
43
+ readonly updateKeys: readonly (infer UK extends string)[];
44
+ } ? {
45
+ attributes?: Partial<Pick<InferAttributesSimple<Def["attributes"]>, UK & keyof Def["attributes"]>>;
46
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
47
+ } : {
48
+ attributes?: Partial<InferAttributesSimple<Def["attributes"]>>;
49
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
50
+ };
51
+ interface ClientConfig {
52
+ baseURL: string;
53
+ jwtToken?: string;
54
+ headers?: Record<string, string>;
55
+ timeout?: number;
56
+ }
57
+ interface QueryParams {
58
+ page?: Record<string, string | number>;
59
+ filter?: Record<string, string | number | boolean>;
60
+ sort?: string | string[];
61
+ include?: string | string[];
62
+ fields?: Record<string, string | string[]>;
63
+ [key: string]: unknown;
64
+ }
65
+
66
+ declare class JsonApiClient {
67
+ private axios;
68
+ constructor(config: ClientConfig);
69
+ /**
70
+ * POST /{resourceType}
71
+ */
72
+ create<Def extends ResourceDefinition>(definition: Def, payload: CreatePayload<Def>, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
73
+ /**
74
+ * GET /{resourceType}/{id}
75
+ */
76
+ findOne<Def extends ResourceDefinition>(definition: Def, id: string, params?: QueryParams, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
77
+ /**
78
+ * GET /{resourceType}
79
+ */
80
+ findAll<Def extends ResourceDefinition>(definition: Def, params?: QueryParams, options?: AxiosRequestConfig): Promise<JsonApiCollectionDocument<InferResource<Def>>>;
81
+ /**
82
+ * PATCH /{resourceType}/{id}
83
+ */
84
+ update<Def extends ResourceDefinition>(definition: Def, id: string, payload: UpdatePayload<Def>, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
85
+ /**
86
+ * DELETE /{resourceType}/{id}
87
+ */
88
+ remove(definition: ResourceDefinition, id: string, options?: AxiosRequestConfig): Promise<void>;
89
+ /**
90
+ * Update client configuration at runtime.
91
+ */
92
+ updateConfig(config: Partial<ClientConfig>): void;
93
+ /**
94
+ * Access the underlying Axios instance for advanced use.
95
+ */
96
+ getAxiosInstance(): AxiosInstance;
97
+ }
98
+ declare function createClient(config: ClientConfig): JsonApiClient;
99
+
100
+ /**
101
+ * Wrap simplified relationship values in `{ data: ... }` per JSON:API spec.
102
+ */
103
+ declare function serializeRelationships(simplified: Record<string, unknown> | undefined, definition: ResourceDefinition): JsonApiRelationships | undefined;
104
+ /**
105
+ * Build a JSON:API request body for creating a resource.
106
+ */
107
+ declare function serializeCreatePayload(definition: ResourceDefinition, payload: {
108
+ attributes: Record<string, unknown>;
109
+ relationships?: Record<string, unknown>;
110
+ }): {
111
+ data: Record<string, unknown>;
112
+ };
113
+ /**
114
+ * Build a JSON:API request body for updating a resource.
115
+ */
116
+ declare function serializeUpdatePayload(definition: ResourceDefinition, id: string, payload: {
117
+ attributes?: Record<string, unknown>;
118
+ relationships?: Record<string, unknown>;
119
+ }): {
120
+ data: Record<string, unknown>;
121
+ };
122
+ /**
123
+ * Encode query params using JSON:API bracket conventions.
124
+ *
125
+ * `{ page: { size: 10 } }` → `"page[size]=10"`
126
+ * `{ sort: ["-createdAt", "title"] }` → `"sort=-createdAt,title"`
127
+ */
128
+ declare function serializeQueryParams(params: Record<string, unknown>): Record<string, string>;
129
+
130
+ export { type ClientConfig, type CreatePayload, JsonApiClient, type QueryParams, type RelationshipClause, type SimplifiedHasMany, type SimplifiedHasOne, type SimplifiedRelationships, type UpdatePayload, createClient, serializeCreatePayload, serializeQueryParams, serializeRelationships, serializeUpdatePayload };
@@ -0,0 +1,130 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+ import { ResourceDefinition, InferAttributesSimple, RelationshipsSchema, JsonApiDocument, InferResource, JsonApiCollectionDocument, JsonApiRelationships } from '@leodamours/jsonapi-dsl';
3
+
4
+ type SimplifiedHasOne<T extends string> = {
5
+ type: T;
6
+ id: string;
7
+ } | null;
8
+ type SimplifiedHasMany<T extends string> = Array<{
9
+ type: T;
10
+ id: string;
11
+ }>;
12
+ /**
13
+ * Indexed lookup for simplified relationship shapes — avoids conditional types.
14
+ */
15
+ interface SimplifiedRelTypeMap<T extends string> {
16
+ one: SimplifiedHasOne<T>;
17
+ many: SimplifiedHasMany<T>;
18
+ }
19
+ type SimplifiedRelationships<Schema extends RelationshipsSchema> = {
20
+ [K in keyof Schema]: SimplifiedRelTypeMap<Schema[K]["type"]>[Schema[K]["_rel"]];
21
+ };
22
+ type RelationshipClause<Rels extends RelationshipsSchema> = {
23
+ relationships?: Partial<SimplifiedRelationships<Rels>>;
24
+ };
25
+ /**
26
+ * Payload for creating a resource.
27
+ * When the definition has `createKeys`, only those attribute keys are accepted.
28
+ */
29
+ type CreatePayload<Def extends ResourceDefinition> = Def extends {
30
+ readonly createKeys: readonly (infer CK extends string)[];
31
+ } ? {
32
+ attributes: Pick<InferAttributesSimple<Def["attributes"]>, CK & keyof Def["attributes"]>;
33
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
34
+ } : {
35
+ attributes: InferAttributesSimple<Def["attributes"]>;
36
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
37
+ };
38
+ /**
39
+ * Payload for updating a resource.
40
+ * When the definition has `updateKeys`, only those attribute keys are accepted.
41
+ */
42
+ type UpdatePayload<Def extends ResourceDefinition> = Def extends {
43
+ readonly updateKeys: readonly (infer UK extends string)[];
44
+ } ? {
45
+ attributes?: Partial<Pick<InferAttributesSimple<Def["attributes"]>, UK & keyof Def["attributes"]>>;
46
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
47
+ } : {
48
+ attributes?: Partial<InferAttributesSimple<Def["attributes"]>>;
49
+ relationships?: Partial<SimplifiedRelationships<Def["relationships"]>>;
50
+ };
51
+ interface ClientConfig {
52
+ baseURL: string;
53
+ jwtToken?: string;
54
+ headers?: Record<string, string>;
55
+ timeout?: number;
56
+ }
57
+ interface QueryParams {
58
+ page?: Record<string, string | number>;
59
+ filter?: Record<string, string | number | boolean>;
60
+ sort?: string | string[];
61
+ include?: string | string[];
62
+ fields?: Record<string, string | string[]>;
63
+ [key: string]: unknown;
64
+ }
65
+
66
+ declare class JsonApiClient {
67
+ private axios;
68
+ constructor(config: ClientConfig);
69
+ /**
70
+ * POST /{resourceType}
71
+ */
72
+ create<Def extends ResourceDefinition>(definition: Def, payload: CreatePayload<Def>, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
73
+ /**
74
+ * GET /{resourceType}/{id}
75
+ */
76
+ findOne<Def extends ResourceDefinition>(definition: Def, id: string, params?: QueryParams, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
77
+ /**
78
+ * GET /{resourceType}
79
+ */
80
+ findAll<Def extends ResourceDefinition>(definition: Def, params?: QueryParams, options?: AxiosRequestConfig): Promise<JsonApiCollectionDocument<InferResource<Def>>>;
81
+ /**
82
+ * PATCH /{resourceType}/{id}
83
+ */
84
+ update<Def extends ResourceDefinition>(definition: Def, id: string, payload: UpdatePayload<Def>, options?: AxiosRequestConfig): Promise<JsonApiDocument<InferResource<Def>>>;
85
+ /**
86
+ * DELETE /{resourceType}/{id}
87
+ */
88
+ remove(definition: ResourceDefinition, id: string, options?: AxiosRequestConfig): Promise<void>;
89
+ /**
90
+ * Update client configuration at runtime.
91
+ */
92
+ updateConfig(config: Partial<ClientConfig>): void;
93
+ /**
94
+ * Access the underlying Axios instance for advanced use.
95
+ */
96
+ getAxiosInstance(): AxiosInstance;
97
+ }
98
+ declare function createClient(config: ClientConfig): JsonApiClient;
99
+
100
+ /**
101
+ * Wrap simplified relationship values in `{ data: ... }` per JSON:API spec.
102
+ */
103
+ declare function serializeRelationships(simplified: Record<string, unknown> | undefined, definition: ResourceDefinition): JsonApiRelationships | undefined;
104
+ /**
105
+ * Build a JSON:API request body for creating a resource.
106
+ */
107
+ declare function serializeCreatePayload(definition: ResourceDefinition, payload: {
108
+ attributes: Record<string, unknown>;
109
+ relationships?: Record<string, unknown>;
110
+ }): {
111
+ data: Record<string, unknown>;
112
+ };
113
+ /**
114
+ * Build a JSON:API request body for updating a resource.
115
+ */
116
+ declare function serializeUpdatePayload(definition: ResourceDefinition, id: string, payload: {
117
+ attributes?: Record<string, unknown>;
118
+ relationships?: Record<string, unknown>;
119
+ }): {
120
+ data: Record<string, unknown>;
121
+ };
122
+ /**
123
+ * Encode query params using JSON:API bracket conventions.
124
+ *
125
+ * `{ page: { size: 10 } }` → `"page[size]=10"`
126
+ * `{ sort: ["-createdAt", "title"] }` → `"sort=-createdAt,title"`
127
+ */
128
+ declare function serializeQueryParams(params: Record<string, unknown>): Record<string, string>;
129
+
130
+ export { type ClientConfig, type CreatePayload, JsonApiClient, type QueryParams, type RelationshipClause, type SimplifiedHasMany, type SimplifiedHasOne, type SimplifiedRelationships, type UpdatePayload, createClient, serializeCreatePayload, serializeQueryParams, serializeRelationships, serializeUpdatePayload };
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var axios__default = /*#__PURE__*/_interopDefault(axios);
8
+
9
+ // src/client.ts
10
+
11
+ // src/serialize.ts
12
+ function serializeRelationships(simplified, definition) {
13
+ if (!simplified) return void 0;
14
+ const result = {};
15
+ let hasKeys = false;
16
+ for (const key of Object.keys(simplified)) {
17
+ const value = simplified[key];
18
+ if (value === void 0) continue;
19
+ const relDef = definition.relationships[key];
20
+ if (!relDef) continue;
21
+ hasKeys = true;
22
+ if (relDef._rel === "one") {
23
+ result[key] = { data: value };
24
+ } else {
25
+ result[key] = { data: value };
26
+ }
27
+ }
28
+ return hasKeys ? result : void 0;
29
+ }
30
+ function serializeCreatePayload(definition, payload) {
31
+ const data = {
32
+ type: definition.resourceType,
33
+ attributes: payload.attributes
34
+ };
35
+ const rels = serializeRelationships(payload.relationships, definition);
36
+ if (rels) data.relationships = rels;
37
+ return { data };
38
+ }
39
+ function serializeUpdatePayload(definition, id, payload) {
40
+ const data = {
41
+ type: definition.resourceType,
42
+ id
43
+ };
44
+ if (payload.attributes && Object.keys(payload.attributes).length > 0) {
45
+ data.attributes = payload.attributes;
46
+ }
47
+ const rels = serializeRelationships(payload.relationships, definition);
48
+ if (rels) data.relationships = rels;
49
+ return { data };
50
+ }
51
+ function serializeQueryParams(params) {
52
+ const out = {};
53
+ for (const [key, value] of Object.entries(params)) {
54
+ if (value == null) continue;
55
+ if (typeof value === "object" && !Array.isArray(value)) {
56
+ for (const [nested, nv] of Object.entries(value)) {
57
+ if (nv != null) out[`${key}[${nested}]`] = String(nv);
58
+ }
59
+ } else if (Array.isArray(value)) {
60
+ out[key] = value.join(",");
61
+ } else {
62
+ out[key] = String(value);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ // src/client.ts
69
+ var JsonApiClient = class {
70
+ constructor(config) {
71
+ const headers = {
72
+ "Content-Type": "application/vnd.api+json",
73
+ Accept: "application/vnd.api+json",
74
+ ...config.headers
75
+ };
76
+ if (config.jwtToken) {
77
+ headers.Authorization = `Bearer ${config.jwtToken}`;
78
+ }
79
+ this.axios = axios__default.default.create({
80
+ baseURL: config.baseURL,
81
+ headers,
82
+ timeout: config.timeout
83
+ });
84
+ }
85
+ /**
86
+ * POST /{resourceType}
87
+ */
88
+ async create(definition, payload, options) {
89
+ const body = serializeCreatePayload(definition, payload);
90
+ const res = await this.axios.post(`/${definition.resourceType}`, body, options);
91
+ return res.data;
92
+ }
93
+ /**
94
+ * GET /{resourceType}/{id}
95
+ */
96
+ async findOne(definition, id, params, options) {
97
+ const res = await this.axios.get(`/${definition.resourceType}/${id}`, {
98
+ params: params ? serializeQueryParams(params) : void 0,
99
+ ...options
100
+ });
101
+ return res.data;
102
+ }
103
+ /**
104
+ * GET /{resourceType}
105
+ */
106
+ async findAll(definition, params, options) {
107
+ const res = await this.axios.get(`/${definition.resourceType}`, {
108
+ params: params ? serializeQueryParams(params) : void 0,
109
+ ...options
110
+ });
111
+ return res.data;
112
+ }
113
+ /**
114
+ * PATCH /{resourceType}/{id}
115
+ */
116
+ async update(definition, id, payload, options) {
117
+ const body = serializeUpdatePayload(definition, id, payload);
118
+ const res = await this.axios.patch(`/${definition.resourceType}/${id}`, body, options);
119
+ return res.data;
120
+ }
121
+ /**
122
+ * DELETE /{resourceType}/{id}
123
+ */
124
+ async remove(definition, id, options) {
125
+ await this.axios.delete(`/${definition.resourceType}/${id}`, options);
126
+ }
127
+ /**
128
+ * Update client configuration at runtime.
129
+ */
130
+ updateConfig(config) {
131
+ if (config.baseURL) {
132
+ this.axios.defaults.baseURL = config.baseURL;
133
+ }
134
+ if (config.headers) {
135
+ Object.assign(this.axios.defaults.headers.common, config.headers);
136
+ }
137
+ if (config.jwtToken) {
138
+ this.axios.defaults.headers.common.Authorization = `Bearer ${config.jwtToken}`;
139
+ } else if (config.jwtToken === null) {
140
+ delete this.axios.defaults.headers.common.Authorization;
141
+ }
142
+ }
143
+ /**
144
+ * Access the underlying Axios instance for advanced use.
145
+ */
146
+ getAxiosInstance() {
147
+ return this.axios;
148
+ }
149
+ };
150
+ function createClient(config) {
151
+ return new JsonApiClient(config);
152
+ }
153
+
154
+ exports.JsonApiClient = JsonApiClient;
155
+ exports.createClient = createClient;
156
+ exports.serializeCreatePayload = serializeCreatePayload;
157
+ exports.serializeQueryParams = serializeQueryParams;
158
+ exports.serializeRelationships = serializeRelationships;
159
+ exports.serializeUpdatePayload = serializeUpdatePayload;
160
+ //# sourceMappingURL=index.js.map
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serialize.ts","../src/client.ts"],"names":["axios"],"mappings":"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","file":"index.js","sourcesContent":["import type { ResourceDefinition, JsonApiRelationships } from \"@leodamours/jsonapi-dsl\";\n\n/**\n * Wrap simplified relationship values in `{ data: ... }` per JSON:API spec.\n */\nexport function serializeRelationships(\n simplified: Record<string, unknown> | undefined,\n definition: ResourceDefinition,\n): JsonApiRelationships | undefined {\n if (!simplified) return undefined;\n\n const result: JsonApiRelationships = {};\n let hasKeys = false;\n\n for (const key of Object.keys(simplified)) {\n const value = simplified[key];\n if (value === undefined) continue;\n\n const relDef = definition.relationships[key];\n if (!relDef) continue;\n\n hasKeys = true;\n\n if (relDef._rel === \"one\") {\n result[key] = { data: value as { type: string; id: string } | null };\n } else {\n result[key] = { data: value as { type: string; id: string }[] };\n }\n }\n\n return hasKeys ? result : undefined;\n}\n\n/**\n * Build a JSON:API request body for creating a resource.\n */\nexport function serializeCreatePayload(\n definition: ResourceDefinition,\n payload: { attributes: Record<string, unknown>; relationships?: Record<string, unknown> },\n): { data: Record<string, unknown> } {\n const data: Record<string, unknown> = {\n type: definition.resourceType,\n attributes: payload.attributes,\n };\n\n const rels = serializeRelationships(payload.relationships, definition);\n if (rels) data.relationships = rels;\n\n return { data };\n}\n\n/**\n * Build a JSON:API request body for updating a resource.\n */\nexport function serializeUpdatePayload(\n definition: ResourceDefinition,\n id: string,\n payload: { attributes?: Record<string, unknown>; relationships?: Record<string, unknown> },\n): { data: Record<string, unknown> } {\n const data: Record<string, unknown> = {\n type: definition.resourceType,\n id,\n };\n\n if (payload.attributes && Object.keys(payload.attributes).length > 0) {\n data.attributes = payload.attributes;\n }\n\n const rels = serializeRelationships(payload.relationships, definition);\n if (rels) data.relationships = rels;\n\n return { data };\n}\n\n/**\n * Encode query params using JSON:API bracket conventions.\n *\n * `{ page: { size: 10 } }` → `\"page[size]=10\"`\n * `{ sort: [\"-createdAt\", \"title\"] }` → `\"sort=-createdAt,title\"`\n */\nexport function serializeQueryParams(params: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n\n if (typeof value === \"object\" && !Array.isArray(value)) {\n for (const [nested, nv] of Object.entries(value as Record<string, unknown>)) {\n if (nv != null) out[`${key}[${nested}]`] = String(nv);\n }\n } else if (Array.isArray(value)) {\n out[key] = value.join(\",\");\n } else {\n out[key] = String(value);\n }\n }\n\n return out;\n}\n","import axios, { type AxiosInstance, type AxiosRequestConfig } from \"axios\";\nimport type {\n ResourceDefinition,\n InferResource,\n JsonApiDocument,\n JsonApiCollectionDocument,\n} from \"@leodamours/jsonapi-dsl\";\nimport type { ClientConfig, QueryParams, CreatePayload, UpdatePayload } from \"./types\";\nimport { serializeCreatePayload, serializeUpdatePayload, serializeQueryParams } from \"./serialize\";\n\nexport class JsonApiClient {\n private axios: AxiosInstance;\n\n constructor(config: ClientConfig) {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/vnd.api+json\",\n Accept: \"application/vnd.api+json\",\n ...config.headers,\n };\n\n if (config.jwtToken) {\n headers.Authorization = `Bearer ${config.jwtToken}`;\n }\n\n this.axios = axios.create({\n baseURL: config.baseURL,\n headers,\n timeout: config.timeout,\n });\n }\n\n /**\n * POST /{resourceType}\n */\n async create<Def extends ResourceDefinition>(\n definition: Def,\n payload: CreatePayload<Def>,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const body = serializeCreatePayload(definition, payload as any);\n const res = await this.axios.post(`/${definition.resourceType}`, body, options);\n return res.data;\n }\n\n /**\n * GET /{resourceType}/{id}\n */\n async findOne<Def extends ResourceDefinition>(\n definition: Def,\n id: string,\n params?: QueryParams,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const res = await this.axios.get(`/${definition.resourceType}/${id}`, {\n params: params ? serializeQueryParams(params) : undefined,\n ...options,\n });\n return res.data;\n }\n\n /**\n * GET /{resourceType}\n */\n async findAll<Def extends ResourceDefinition>(\n definition: Def,\n params?: QueryParams,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiCollectionDocument<InferResource<Def>>> {\n const res = await this.axios.get(`/${definition.resourceType}`, {\n params: params ? serializeQueryParams(params) : undefined,\n ...options,\n });\n return res.data;\n }\n\n /**\n * PATCH /{resourceType}/{id}\n */\n async update<Def extends ResourceDefinition>(\n definition: Def,\n id: string,\n payload: UpdatePayload<Def>,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const body = serializeUpdatePayload(definition, id, payload as any);\n const res = await this.axios.patch(`/${definition.resourceType}/${id}`, body, options);\n return res.data;\n }\n\n /**\n * DELETE /{resourceType}/{id}\n */\n async remove(\n definition: ResourceDefinition,\n id: string,\n options?: AxiosRequestConfig,\n ): Promise<void> {\n await this.axios.delete(`/${definition.resourceType}/${id}`, options);\n }\n\n /**\n * Update client configuration at runtime.\n */\n updateConfig(config: Partial<ClientConfig>): void {\n if (config.baseURL) {\n this.axios.defaults.baseURL = config.baseURL;\n }\n if (config.headers) {\n Object.assign(this.axios.defaults.headers.common, config.headers);\n }\n if (config.jwtToken) {\n this.axios.defaults.headers.common.Authorization = `Bearer ${config.jwtToken}`;\n } else if (config.jwtToken === null) {\n delete this.axios.defaults.headers.common.Authorization;\n }\n }\n\n /**\n * Access the underlying Axios instance for advanced use.\n */\n getAxiosInstance(): AxiosInstance {\n return this.axios;\n }\n}\n\nexport function createClient(config: ClientConfig): JsonApiClient {\n return new JsonApiClient(config);\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,150 @@
1
+ import axios from 'axios';
2
+
3
+ // src/client.ts
4
+
5
+ // src/serialize.ts
6
+ function serializeRelationships(simplified, definition) {
7
+ if (!simplified) return void 0;
8
+ const result = {};
9
+ let hasKeys = false;
10
+ for (const key of Object.keys(simplified)) {
11
+ const value = simplified[key];
12
+ if (value === void 0) continue;
13
+ const relDef = definition.relationships[key];
14
+ if (!relDef) continue;
15
+ hasKeys = true;
16
+ if (relDef._rel === "one") {
17
+ result[key] = { data: value };
18
+ } else {
19
+ result[key] = { data: value };
20
+ }
21
+ }
22
+ return hasKeys ? result : void 0;
23
+ }
24
+ function serializeCreatePayload(definition, payload) {
25
+ const data = {
26
+ type: definition.resourceType,
27
+ attributes: payload.attributes
28
+ };
29
+ const rels = serializeRelationships(payload.relationships, definition);
30
+ if (rels) data.relationships = rels;
31
+ return { data };
32
+ }
33
+ function serializeUpdatePayload(definition, id, payload) {
34
+ const data = {
35
+ type: definition.resourceType,
36
+ id
37
+ };
38
+ if (payload.attributes && Object.keys(payload.attributes).length > 0) {
39
+ data.attributes = payload.attributes;
40
+ }
41
+ const rels = serializeRelationships(payload.relationships, definition);
42
+ if (rels) data.relationships = rels;
43
+ return { data };
44
+ }
45
+ function serializeQueryParams(params) {
46
+ const out = {};
47
+ for (const [key, value] of Object.entries(params)) {
48
+ if (value == null) continue;
49
+ if (typeof value === "object" && !Array.isArray(value)) {
50
+ for (const [nested, nv] of Object.entries(value)) {
51
+ if (nv != null) out[`${key}[${nested}]`] = String(nv);
52
+ }
53
+ } else if (Array.isArray(value)) {
54
+ out[key] = value.join(",");
55
+ } else {
56
+ out[key] = String(value);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ // src/client.ts
63
+ var JsonApiClient = class {
64
+ constructor(config) {
65
+ const headers = {
66
+ "Content-Type": "application/vnd.api+json",
67
+ Accept: "application/vnd.api+json",
68
+ ...config.headers
69
+ };
70
+ if (config.jwtToken) {
71
+ headers.Authorization = `Bearer ${config.jwtToken}`;
72
+ }
73
+ this.axios = axios.create({
74
+ baseURL: config.baseURL,
75
+ headers,
76
+ timeout: config.timeout
77
+ });
78
+ }
79
+ /**
80
+ * POST /{resourceType}
81
+ */
82
+ async create(definition, payload, options) {
83
+ const body = serializeCreatePayload(definition, payload);
84
+ const res = await this.axios.post(`/${definition.resourceType}`, body, options);
85
+ return res.data;
86
+ }
87
+ /**
88
+ * GET /{resourceType}/{id}
89
+ */
90
+ async findOne(definition, id, params, options) {
91
+ const res = await this.axios.get(`/${definition.resourceType}/${id}`, {
92
+ params: params ? serializeQueryParams(params) : void 0,
93
+ ...options
94
+ });
95
+ return res.data;
96
+ }
97
+ /**
98
+ * GET /{resourceType}
99
+ */
100
+ async findAll(definition, params, options) {
101
+ const res = await this.axios.get(`/${definition.resourceType}`, {
102
+ params: params ? serializeQueryParams(params) : void 0,
103
+ ...options
104
+ });
105
+ return res.data;
106
+ }
107
+ /**
108
+ * PATCH /{resourceType}/{id}
109
+ */
110
+ async update(definition, id, payload, options) {
111
+ const body = serializeUpdatePayload(definition, id, payload);
112
+ const res = await this.axios.patch(`/${definition.resourceType}/${id}`, body, options);
113
+ return res.data;
114
+ }
115
+ /**
116
+ * DELETE /{resourceType}/{id}
117
+ */
118
+ async remove(definition, id, options) {
119
+ await this.axios.delete(`/${definition.resourceType}/${id}`, options);
120
+ }
121
+ /**
122
+ * Update client configuration at runtime.
123
+ */
124
+ updateConfig(config) {
125
+ if (config.baseURL) {
126
+ this.axios.defaults.baseURL = config.baseURL;
127
+ }
128
+ if (config.headers) {
129
+ Object.assign(this.axios.defaults.headers.common, config.headers);
130
+ }
131
+ if (config.jwtToken) {
132
+ this.axios.defaults.headers.common.Authorization = `Bearer ${config.jwtToken}`;
133
+ } else if (config.jwtToken === null) {
134
+ delete this.axios.defaults.headers.common.Authorization;
135
+ }
136
+ }
137
+ /**
138
+ * Access the underlying Axios instance for advanced use.
139
+ */
140
+ getAxiosInstance() {
141
+ return this.axios;
142
+ }
143
+ };
144
+ function createClient(config) {
145
+ return new JsonApiClient(config);
146
+ }
147
+
148
+ export { JsonApiClient, createClient, serializeCreatePayload, serializeQueryParams, serializeRelationships, serializeUpdatePayload };
149
+ //# sourceMappingURL=index.mjs.map
150
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serialize.ts","../src/client.ts"],"names":[],"mappings":";;;;;AAKO,SAAS,sBAAA,CACd,YACA,UAAA,EACkC;AAClC,EAAA,IAAI,CAAC,YAAY,OAAO,MAAA;AAExB,EAAA,MAAM,SAA+B,EAAC;AACtC,EAAA,IAAI,OAAA,GAAU,KAAA;AAEd,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,EAAG;AACzC,IAAA,MAAM,KAAA,GAAQ,WAAW,GAAG,CAAA;AAC5B,IAAA,IAAI,UAAU,MAAA,EAAW;AAEzB,IAAA,MAAM,MAAA,GAAS,UAAA,CAAW,aAAA,CAAc,GAAG,CAAA;AAC3C,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,IAAA,OAAA,GAAU,IAAA;AAEV,IAAA,IAAI,MAAA,CAAO,SAAS,KAAA,EAAO;AACzB,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,EAAE,IAAA,EAAM,KAAA,EAA6C;AAAA,IACrE,CAAA,MAAO;AACL,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,EAAE,IAAA,EAAM,KAAA,EAAwC;AAAA,IAChE;AAAA,EACF;AAEA,EAAA,OAAO,UAAU,MAAA,GAAS,MAAA;AAC5B;AAKO,SAAS,sBAAA,CACd,YACA,OAAA,EACmC;AACnC,EAAA,MAAM,IAAA,GAAgC;AAAA,IACpC,MAAM,UAAA,CAAW,YAAA;AAAA,IACjB,YAAY,OAAA,CAAQ;AAAA,GACtB;AAEA,EAAA,MAAM,IAAA,GAAO,sBAAA,CAAuB,OAAA,CAAQ,aAAA,EAAe,UAAU,CAAA;AACrE,EAAA,IAAI,IAAA,OAAW,aAAA,GAAgB,IAAA;AAE/B,EAAA,OAAO,EAAE,IAAA,EAAK;AAChB;AAKO,SAAS,sBAAA,CACd,UAAA,EACA,EAAA,EACA,OAAA,EACmC;AACnC,EAAA,MAAM,IAAA,GAAgC;AAAA,IACpC,MAAM,UAAA,CAAW,YAAA;AAAA,IACjB;AAAA,GACF;AAEA,EAAA,IAAI,OAAA,CAAQ,cAAc,MAAA,CAAO,IAAA,CAAK,QAAQ,UAAU,CAAA,CAAE,SAAS,CAAA,EAAG;AACpE,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAAA,EAC5B;AAEA,EAAA,MAAM,IAAA,GAAO,sBAAA,CAAuB,OAAA,CAAQ,aAAA,EAAe,UAAU,CAAA;AACrE,EAAA,IAAI,IAAA,OAAW,aAAA,GAAgB,IAAA;AAE/B,EAAA,OAAO,EAAE,IAAA,EAAK;AAChB;AAQO,SAAS,qBAAqB,MAAA,EAAyD;AAC5F,EAAA,MAAM,MAA8B,EAAC;AAErC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,IAAA,IAAI,SAAS,IAAA,EAAM;AAEnB,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtD,MAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,EAAE,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAgC,CAAA,EAAG;AAC3E,QAAA,IAAI,EAAA,IAAM,IAAA,EAAM,GAAA,CAAI,CAAA,EAAG,GAAG,IAAI,MAAM,CAAA,CAAA,CAAG,CAAA,GAAI,MAAA,CAAO,EAAE,CAAA;AAAA,MACtD;AAAA,IACF,CAAA,MAAA,IAAW,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC/B,MAAA,GAAA,CAAI,GAAG,CAAA,GAAI,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAAA,IAC3B,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,GAAG,CAAA,GAAI,MAAA,CAAO,KAAK,CAAA;AAAA,IACzB;AAAA,EACF;AAEA,EAAA,OAAO,GAAA;AACT;;;ACxFO,IAAM,gBAAN,MAAoB;AAAA,EAGzB,YAAY,MAAA,EAAsB;AAChC,IAAA,MAAM,OAAA,GAAkC;AAAA,MACtC,cAAA,EAAgB,0BAAA;AAAA,MAChB,MAAA,EAAQ,0BAAA;AAAA,MACR,GAAG,MAAA,CAAO;AAAA,KACZ;AAEA,IAAA,IAAI,OAAO,QAAA,EAAU;AACnB,MAAA,OAAA,CAAQ,aAAA,GAAgB,CAAA,OAAA,EAAU,MAAA,CAAO,QAAQ,CAAA,CAAA;AAAA,IACnD;AAEA,IAAA,IAAA,CAAK,KAAA,GAAQ,MAAM,MAAA,CAAO;AAAA,MACxB,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,OAAA;AAAA,MACA,SAAS,MAAA,CAAO;AAAA,KACjB,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAA,CACJ,UAAA,EACA,OAAA,EACA,OAAA,EAC8C;AAC9C,IAAA,MAAM,IAAA,GAAO,sBAAA,CAAuB,UAAA,EAAY,OAAc,CAAA;AAC9D,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAI,UAAA,CAAW,YAAY,CAAA,CAAA,EAAI,IAAA,EAAM,OAAO,CAAA;AAC9E,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,CACJ,UAAA,EACA,EAAA,EACA,QACA,OAAA,EAC8C;AAC9C,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,UAAA,CAAW,YAAY,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,EAAI;AAAA,MACpE,MAAA,EAAQ,MAAA,GAAS,oBAAA,CAAqB,MAAM,CAAA,GAAI,MAAA;AAAA,MAChD,GAAG;AAAA,KACJ,CAAA;AACD,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,CACJ,UAAA,EACA,MAAA,EACA,OAAA,EACwD;AACxD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,CAAA,EAAI,UAAA,CAAW,YAAY,CAAA,CAAA,EAAI;AAAA,MAC9D,MAAA,EAAQ,MAAA,GAAS,oBAAA,CAAqB,MAAM,CAAA,GAAI,MAAA;AAAA,MAChD,GAAG;AAAA,KACJ,CAAA;AACD,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAA,CACJ,UAAA,EACA,EAAA,EACA,SACA,OAAA,EAC8C;AAC9C,IAAA,MAAM,IAAA,GAAO,sBAAA,CAAuB,UAAA,EAAY,EAAA,EAAI,OAAc,CAAA;AAClE,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,KAAA,CAAM,CAAA,CAAA,EAAI,UAAA,CAAW,YAAY,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,EAAI,IAAA,EAAM,OAAO,CAAA;AACrF,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAA,CACJ,UAAA,EACA,EAAA,EACA,OAAA,EACe;AACf,IAAA,MAAM,IAAA,CAAK,MAAM,MAAA,CAAO,CAAA,CAAA,EAAI,WAAW,YAAY,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,EAAI,OAAO,CAAA;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,MAAA,EAAqC;AAChD,IAAA,IAAI,OAAO,OAAA,EAAS;AAClB,MAAA,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,OAAA,GAAU,MAAA,CAAO,OAAA;AAAA,IACvC;AACA,IAAA,IAAI,OAAO,OAAA,EAAS;AAClB,MAAA,MAAA,CAAO,OAAO,IAAA,CAAK,KAAA,CAAM,SAAS,OAAA,CAAQ,MAAA,EAAQ,OAAO,OAAO,CAAA;AAAA,IAClE;AACA,IAAA,IAAI,OAAO,QAAA,EAAU;AACnB,MAAA,IAAA,CAAK,MAAM,QAAA,CAAS,OAAA,CAAQ,OAAO,aAAA,GAAgB,CAAA,OAAA,EAAU,OAAO,QAAQ,CAAA,CAAA;AAAA,IAC9E,CAAA,MAAA,IAAW,MAAA,CAAO,QAAA,KAAa,IAAA,EAAM;AACnC,MAAA,OAAO,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,OAAA,CAAQ,MAAA,CAAO,aAAA;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAA,GAAkC;AAChC,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF;AAEO,SAAS,aAAa,MAAA,EAAqC;AAChE,EAAA,OAAO,IAAI,cAAc,MAAM,CAAA;AACjC","file":"index.mjs","sourcesContent":["import type { ResourceDefinition, JsonApiRelationships } from \"@leodamours/jsonapi-dsl\";\n\n/**\n * Wrap simplified relationship values in `{ data: ... }` per JSON:API spec.\n */\nexport function serializeRelationships(\n simplified: Record<string, unknown> | undefined,\n definition: ResourceDefinition,\n): JsonApiRelationships | undefined {\n if (!simplified) return undefined;\n\n const result: JsonApiRelationships = {};\n let hasKeys = false;\n\n for (const key of Object.keys(simplified)) {\n const value = simplified[key];\n if (value === undefined) continue;\n\n const relDef = definition.relationships[key];\n if (!relDef) continue;\n\n hasKeys = true;\n\n if (relDef._rel === \"one\") {\n result[key] = { data: value as { type: string; id: string } | null };\n } else {\n result[key] = { data: value as { type: string; id: string }[] };\n }\n }\n\n return hasKeys ? result : undefined;\n}\n\n/**\n * Build a JSON:API request body for creating a resource.\n */\nexport function serializeCreatePayload(\n definition: ResourceDefinition,\n payload: { attributes: Record<string, unknown>; relationships?: Record<string, unknown> },\n): { data: Record<string, unknown> } {\n const data: Record<string, unknown> = {\n type: definition.resourceType,\n attributes: payload.attributes,\n };\n\n const rels = serializeRelationships(payload.relationships, definition);\n if (rels) data.relationships = rels;\n\n return { data };\n}\n\n/**\n * Build a JSON:API request body for updating a resource.\n */\nexport function serializeUpdatePayload(\n definition: ResourceDefinition,\n id: string,\n payload: { attributes?: Record<string, unknown>; relationships?: Record<string, unknown> },\n): { data: Record<string, unknown> } {\n const data: Record<string, unknown> = {\n type: definition.resourceType,\n id,\n };\n\n if (payload.attributes && Object.keys(payload.attributes).length > 0) {\n data.attributes = payload.attributes;\n }\n\n const rels = serializeRelationships(payload.relationships, definition);\n if (rels) data.relationships = rels;\n\n return { data };\n}\n\n/**\n * Encode query params using JSON:API bracket conventions.\n *\n * `{ page: { size: 10 } }` → `\"page[size]=10\"`\n * `{ sort: [\"-createdAt\", \"title\"] }` → `\"sort=-createdAt,title\"`\n */\nexport function serializeQueryParams(params: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n\n if (typeof value === \"object\" && !Array.isArray(value)) {\n for (const [nested, nv] of Object.entries(value as Record<string, unknown>)) {\n if (nv != null) out[`${key}[${nested}]`] = String(nv);\n }\n } else if (Array.isArray(value)) {\n out[key] = value.join(\",\");\n } else {\n out[key] = String(value);\n }\n }\n\n return out;\n}\n","import axios, { type AxiosInstance, type AxiosRequestConfig } from \"axios\";\nimport type {\n ResourceDefinition,\n InferResource,\n JsonApiDocument,\n JsonApiCollectionDocument,\n} from \"@leodamours/jsonapi-dsl\";\nimport type { ClientConfig, QueryParams, CreatePayload, UpdatePayload } from \"./types\";\nimport { serializeCreatePayload, serializeUpdatePayload, serializeQueryParams } from \"./serialize\";\n\nexport class JsonApiClient {\n private axios: AxiosInstance;\n\n constructor(config: ClientConfig) {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/vnd.api+json\",\n Accept: \"application/vnd.api+json\",\n ...config.headers,\n };\n\n if (config.jwtToken) {\n headers.Authorization = `Bearer ${config.jwtToken}`;\n }\n\n this.axios = axios.create({\n baseURL: config.baseURL,\n headers,\n timeout: config.timeout,\n });\n }\n\n /**\n * POST /{resourceType}\n */\n async create<Def extends ResourceDefinition>(\n definition: Def,\n payload: CreatePayload<Def>,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const body = serializeCreatePayload(definition, payload as any);\n const res = await this.axios.post(`/${definition.resourceType}`, body, options);\n return res.data;\n }\n\n /**\n * GET /{resourceType}/{id}\n */\n async findOne<Def extends ResourceDefinition>(\n definition: Def,\n id: string,\n params?: QueryParams,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const res = await this.axios.get(`/${definition.resourceType}/${id}`, {\n params: params ? serializeQueryParams(params) : undefined,\n ...options,\n });\n return res.data;\n }\n\n /**\n * GET /{resourceType}\n */\n async findAll<Def extends ResourceDefinition>(\n definition: Def,\n params?: QueryParams,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiCollectionDocument<InferResource<Def>>> {\n const res = await this.axios.get(`/${definition.resourceType}`, {\n params: params ? serializeQueryParams(params) : undefined,\n ...options,\n });\n return res.data;\n }\n\n /**\n * PATCH /{resourceType}/{id}\n */\n async update<Def extends ResourceDefinition>(\n definition: Def,\n id: string,\n payload: UpdatePayload<Def>,\n options?: AxiosRequestConfig,\n ): Promise<JsonApiDocument<InferResource<Def>>> {\n const body = serializeUpdatePayload(definition, id, payload as any);\n const res = await this.axios.patch(`/${definition.resourceType}/${id}`, body, options);\n return res.data;\n }\n\n /**\n * DELETE /{resourceType}/{id}\n */\n async remove(\n definition: ResourceDefinition,\n id: string,\n options?: AxiosRequestConfig,\n ): Promise<void> {\n await this.axios.delete(`/${definition.resourceType}/${id}`, options);\n }\n\n /**\n * Update client configuration at runtime.\n */\n updateConfig(config: Partial<ClientConfig>): void {\n if (config.baseURL) {\n this.axios.defaults.baseURL = config.baseURL;\n }\n if (config.headers) {\n Object.assign(this.axios.defaults.headers.common, config.headers);\n }\n if (config.jwtToken) {\n this.axios.defaults.headers.common.Authorization = `Bearer ${config.jwtToken}`;\n } else if (config.jwtToken === null) {\n delete this.axios.defaults.headers.common.Authorization;\n }\n }\n\n /**\n * Access the underlying Axios instance for advanced use.\n */\n getAxiosInstance(): AxiosInstance {\n return this.axios;\n }\n}\n\nexport function createClient(config: ClientConfig): JsonApiClient {\n return new JsonApiClient(config);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@leodamours/jsonapi-client",
3
+ "version": "1.0.2",
4
+ "description": "A typed HTTP client for JSON:API — powered by Axios with full type inference",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run"
20
+ },
21
+ "keywords": [
22
+ "jsonapi",
23
+ "json:api",
24
+ "typescript",
25
+ "http-client",
26
+ "axios",
27
+ "type-safe"
28
+ ],
29
+ "author": "Leo Damours",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/LeoDamours/json-api-client.git",
34
+ "directory": "packages/client"
35
+ },
36
+ "homepage": "https://github.com/LeoDamours/json-api-client/tree/main/packages/client",
37
+ "bugs": "https://github.com/LeoDamours/json-api-client/issues",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "@leodamours/jsonapi-dsl": "^1.0.0",
43
+ "axios": "^1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@leodamours/jsonapi-dsl": "^1.0.0",
47
+ "axios": "^1.7.0",
48
+ "typescript": "^5.4.0",
49
+ "tsup": "^8.0.0"
50
+ },
51
+ "files": [
52
+ "dist/**/*",
53
+ "LICENSE"
54
+ ]
55
+ }