klaim 1.7.0-alpha.2 → 1.7.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 +143 -36
- package/deno.json +1 -1
- package/dist/klaim.cjs +1 -1
- package/dist/klaim.es.js +190 -182
- package/dist/klaim.umd.js +1 -1
- package/eslint.config.mjs +211 -211
- package/package.json +1 -1
- package/src/core/Api.ts +49 -27
- package/src/core/Group.ts +252 -224
- package/src/core/Klaim.ts +65 -79
- package/src/core/Registry.ts +80 -106
- package/tests/08.group.test.ts +83 -25
package/README.md
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
- [Basic API Configuration](#basic-api-configuration)
|
|
9
9
|
- [Route Definition](#route-definition)
|
|
10
10
|
- [Request Handling](#request-handling)
|
|
11
|
+
- [Groups](#groups)
|
|
12
|
+
- [API Groups](#api-groups)
|
|
13
|
+
- [Route Groups](#route-groups)
|
|
14
|
+
- [Nested Groups](#nested-groups)
|
|
15
|
+
- [Group Configuration](#group-configuration)
|
|
11
16
|
- [Middleware Usage](#middleware-usage)
|
|
12
17
|
- [Hook Subscription](#hook-subscription)
|
|
13
|
-
- [Group Usage](#group-usage)
|
|
14
18
|
- [Caching Requests](#caching-requests)
|
|
15
19
|
- [Retry Mechanism](#retry-mechanism)
|
|
16
20
|
- [Response Validation](#response-validation)
|
|
@@ -21,12 +25,13 @@
|
|
|
21
25
|
## 🚀 Features
|
|
22
26
|
|
|
23
27
|
- **Efficient API Management**: Easily manage multiple APIs with streamlined integration and interaction capabilities.
|
|
28
|
+
- **API Grouping**: Organize related APIs into logical groups with shared settings and configuration.
|
|
29
|
+
- **Route Grouping**: Organize related routes into logical groups with inherited settings.
|
|
24
30
|
- **Request Recording**: Seamlessly track requests for debugging and monitoring.
|
|
25
31
|
- **User Experience Optimization**: Focused on performance and usability for a smooth user experience.
|
|
26
32
|
- **Lightweight**: Minimal footprint for fast load times and minimal performance impact.
|
|
27
33
|
- **Middleware Support**: Easily add middleware to modify requests and responses (`before` and `after`).
|
|
28
34
|
- **Hook System**: Subscribe to hooks to monitor and react to specific events.
|
|
29
|
-
- **Group Organization**: Organize related routes into logical groups with inherited settings.
|
|
30
35
|
- **Caching**: Enable caching on requests to reduce network load and improve performance.
|
|
31
36
|
- **Retry Mechanism**: Automatically retry failed requests to enhance reliability.
|
|
32
37
|
- **TypeScript Support**: Fully typed for enhanced code quality and developer experience.
|
|
@@ -76,19 +81,147 @@ Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
|
|
|
76
81
|
|
|
77
82
|
### Route Definition
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
Routes represent endpoints in your API and can be defined with different HTTP methods. Routes can include parameters and custom configurations:
|
|
80
85
|
|
|
81
86
|
```typescript
|
|
82
|
-
Api.create("
|
|
83
|
-
//
|
|
84
|
-
Route.get
|
|
87
|
+
Api.create("api", "https://api.example.com", () => {
|
|
88
|
+
// Basic GET route
|
|
89
|
+
Route.get("listUsers", "/users");
|
|
90
|
+
|
|
91
|
+
// GET route with URL parameter
|
|
92
|
+
Route.get("getUser", "/users/[id]");
|
|
93
|
+
|
|
94
|
+
// POST route with custom headers and body
|
|
95
|
+
Route.post("createUser", "/users", {
|
|
96
|
+
"Content-Type": "application/json"
|
|
97
|
+
}, { userId: 1, name: "John Doe" });
|
|
98
|
+
|
|
99
|
+
// PUT route with parameter
|
|
100
|
+
Route.put("updateUser", "/users/[id]");
|
|
101
|
+
|
|
102
|
+
// DELETE route
|
|
103
|
+
Route.delete("deleteUser", "/users/[id]");
|
|
104
|
+
|
|
105
|
+
// PATCH route
|
|
106
|
+
Route.patch("updateUserStatus", "/users/[id]/status");
|
|
107
|
+
|
|
108
|
+
// OPTIONS route
|
|
109
|
+
Route.options("userOptions", "/users");
|
|
110
|
+
});
|
|
111
|
+
```
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
Route.get<Todo>("getTodo", "todos/[id]");
|
|
113
|
+
### Groups
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
Klaim provides powerful grouping capabilities for both APIs and routes. Groups can be used to organize related elements, share configuration, and maintain a clean structure in your application.
|
|
116
|
+
|
|
117
|
+
#### API Groups
|
|
118
|
+
|
|
119
|
+
Organize multiple APIs that serve related purposes:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import {Group, Api, Route} from 'klaim';
|
|
123
|
+
|
|
124
|
+
// Create a group for user-related services
|
|
125
|
+
Group.create("userServices", () => {
|
|
126
|
+
// Authentication API
|
|
127
|
+
Api.create("auth", "https://auth.example.com", () => {
|
|
128
|
+
Route.post("login", "/login");
|
|
129
|
+
Route.post("register", "/register");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// User Management API
|
|
133
|
+
Api.create("users", "https://users.example.com", () => {
|
|
134
|
+
Route.get("list", "/users");
|
|
135
|
+
Route.get("getOne", "/users/[id]");
|
|
136
|
+
});
|
|
137
|
+
}).withRetry(3); // Apply retry mechanism to all APIs in the group
|
|
138
|
+
|
|
139
|
+
// Access grouped APIs
|
|
140
|
+
await Klaim.userServices.auth.login({}, { username: "user", password: "pass" });
|
|
141
|
+
await Klaim.userServices.users.list();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Route Groups
|
|
145
|
+
|
|
146
|
+
Organize routes within an API into logical groups:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
Api.create("hello", "https://api.example.com/", () => {
|
|
150
|
+
// Group user-related routes
|
|
151
|
+
Group.create("users", () => {
|
|
152
|
+
Route.get<User[]>("list", "/users");
|
|
153
|
+
Route.get<User>("getOne", "/users/[id]");
|
|
154
|
+
Route.post<User>("create", "/users");
|
|
155
|
+
}).withCache(60); // Cache all user routes for 60 seconds
|
|
156
|
+
|
|
157
|
+
// Group product-related routes
|
|
158
|
+
Group.create("products", () => {
|
|
159
|
+
Route.get("list", "/products");
|
|
160
|
+
Route.get("getOne", "/products/[id]");
|
|
161
|
+
});
|
|
91
162
|
});
|
|
163
|
+
|
|
164
|
+
// Use grouped routes
|
|
165
|
+
const users = await Klaim.hello.users.list();
|
|
166
|
+
const product = await Klaim.hello.products.getOne({ id: 1 });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Nested Groups
|
|
170
|
+
|
|
171
|
+
Create complex hierarchies with nested groups:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
Group.create("services", () => {
|
|
175
|
+
// Internal services group
|
|
176
|
+
Group.create("internal", () => {
|
|
177
|
+
Api.create("logs", "https://logs.internal.example.com", () => {
|
|
178
|
+
Route.post("write", "/logs");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
Api.create("metrics", "https://metrics.internal.example.com", () => {
|
|
182
|
+
Route.post("track", "/metrics");
|
|
183
|
+
});
|
|
184
|
+
}).withRetry(5); // More retries for internal services
|
|
185
|
+
|
|
186
|
+
// External services group
|
|
187
|
+
Group.create("external", () => {
|
|
188
|
+
Api.create("weather", "https://api.weather.com", () => {
|
|
189
|
+
Route.get("forecast", "/forecast/[city]");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
Api.create("geocoding", "https://api.geocoding.com", () => {
|
|
193
|
+
Route.get("search", "/search/[query]");
|
|
194
|
+
});
|
|
195
|
+
}).withCache(300); // Cache external services longer
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Access nested groups
|
|
199
|
+
await Klaim.services.internal.logs.write({}, { message: "Log entry" });
|
|
200
|
+
await Klaim.services.external.weather.forecast({ city: "Paris" });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Group Configuration
|
|
204
|
+
|
|
205
|
+
Groups can share configuration among all their members:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
Group.create("apis", () => {
|
|
209
|
+
Api.create("service1", "https://api1.example.com", () => {
|
|
210
|
+
Route.get("test", "/test");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
Api.create("service2", "https://api2.example.com", () => {
|
|
214
|
+
Route.get("test", "/test");
|
|
215
|
+
});
|
|
216
|
+
})
|
|
217
|
+
.withCache(60) // Enable caching for all APIs
|
|
218
|
+
.withRetry(3) // Enable retries for all APIs
|
|
219
|
+
.before(({ config }) => { // Add authentication for all APIs
|
|
220
|
+
config.headers.Authorization = `Bearer ${getToken()}`;
|
|
221
|
+
})
|
|
222
|
+
.after(({ data }) => { // Process all responses
|
|
223
|
+
logResponse(data);
|
|
224
|
+
});
|
|
92
225
|
```
|
|
93
226
|
|
|
94
227
|
### Request Handling
|
|
@@ -143,32 +276,6 @@ Hook.subscribe("hello.getFirstTodo", ({url}) => {
|
|
|
143
276
|
});
|
|
144
277
|
```
|
|
145
278
|
|
|
146
|
-
### Group Usage
|
|
147
|
-
|
|
148
|
-
Use groups to organize related routes and share common settings:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import {Api, Group, Route} from 'klaim';
|
|
152
|
-
|
|
153
|
-
Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
|
|
154
|
-
// Create a group for user-related routes
|
|
155
|
-
Group.create("users", () => {
|
|
156
|
-
Route.get("list", "/users");
|
|
157
|
-
Route.get("getOne", "/users/[id]");
|
|
158
|
-
|
|
159
|
-
// Nested group for user posts
|
|
160
|
-
Group.create("posts", () => {
|
|
161
|
-
Route.get("list", "/users/[userId]/posts");
|
|
162
|
-
}).withCache(); // Cache enabled for all routes in this group
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Use the grouped routes
|
|
167
|
-
const users = await Klaim.hello.users.list();
|
|
168
|
-
const user = await Klaim.hello.users.getOne({id: 1});
|
|
169
|
-
const userPosts = await Klaim.hello.users.posts.list({userId: 1});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
279
|
### Caching Requests
|
|
173
280
|
|
|
174
281
|
Enable caching on requests to reduce network load and improve performance. By default, the cache duration is 20 seconds,
|
package/deno.json
CHANGED
package/dist/klaim.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var T=Object.defineProperty;var R=(s,t,e)=>t in s?T(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>R(s,typeof t!="symbol"?t+"":t,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function b(s){return s.replace(/([-_][a-z])/gi,t=>t.toUpperCase().replace("-","").replace("_","")).replace(/(^\w)/,t=>t.toLowerCase())}function K(s){return s.trim().replace(/^\/|\/$/g,"")}class C{constructor(t,e,a,r={}){o(this,"type");o(this,"name");o(this,"url");o(this,"headers");o(this,"parent");o(this,"method");o(this,"arguments",new Set);o(this,"schema");o(this,"callbacks",{before:null,after:null,call:null});o(this,"cache",!1);o(this,"retry",!1);this.type=t,this.name=b(e),this.name!==e&&console.warn(`Name "${e}" has been camelCased to "${this.name}"`),this.url=K(a),this.headers=r||{}}before(t){return this.callbacks.before=t,this}after(t){return this.callbacks.after=t,this}onCall(t){return this.callbacks.call=t,this}withCache(t=20){return this.cache=t,this}withRetry(t=2){return this.retry=t,this}}const p=class p{constructor(){o(this,"cache");this.cache=new Map}static get i(){return p._instance||(p._instance=new p),p._instance}set(t,e,a=0){const r=Date.now()+a;this.cache.set(t,{data:e,expiry:r})}has(t){const e=this.cache.get(t);return e?Date.now()>e.expiry?(this.cache.delete(t),!1):!0:!1}get(t){return this.has(t)?this.cache.get(t).data:null}};o(p,"_instance");let g=p;function N(s){let a=2166136261;for(let n=0;n<s.length;n++)a^=s.charCodeAt(n),a*=16777619;let r=(a>>>0).toString(16).padStart(8,"0");for(;r.length<32;)a^=r.charCodeAt(r.length%r.length),a*=16777619,r+=(a>>>0).toString(16).padStart(8,"0");return r.substring(0,32)}async function j(s,t,e){const a=`${s.toString()}${JSON.stringify(t)}`,r=N(a);if(g.i.has(r))return g.i.get(r);const c=await(await fetch(s,t)).json();return g.i.set(r,c,e),c}class E{static subscribe(t,e){this._callbacks.set(t,e)}static run(t){const e=this._callbacks.get(t);e&&e()}}o(E,"_callbacks",new Map);const m={};async function x(s,t,e={},a={}){const r=s.split(".");let n;for(let y=0;y<r.length;y++){const v=r[y];if(n=i.i.getApi(v),n)break}if(!t||!n||t.type!=="route"||n.type!=="api")throw new Error(`Invalid path: ${s}.${t.name}`);let c=O(`${n.url}/${t.url}`,t,e),l={};a&&t.method!=="GET"&&(l.body=JSON.stringify(a)),l.headers={"Content-Type":"application/json",...n.headers,...t.headers},l.method=t.method;const{beforeRoute:h,beforeApi:f,beforeUrl:d,beforeConfig:P}=U({route:t,api:n,url:c,config:l});c=d,l=P,i.updateElement(f),i.updateElement(h);let w=await G(n,t,c,l);t.schema&&"validate"in t.schema&&(w=await t.schema.validate(w));const{afterRoute:_,afterApi:F,afterData:S}=H({route:t,api:n,response:w,data:w});return i.updateElement(F),i.updateElement(_),E.run(`${n.name}.${t.name}`),S}async function D(s,t,e,a){return s?await j(t,e,a.cache):await(await fetch(t,e)).json()}async function G(s,t,e,a){var f,d;const r=s.cache||t.cache,n=t.retry||s.retry||0;let c,l=!1,h=0;for(;h<=n&&!l;)try{(f=t.callbacks)!=null&&f.call?t.callbacks.call({}):(d=s.callbacks)!=null&&d.call&&s.callbacks.call({}),c=await D(!!r,e,a,s),l=!0}catch(P){if(h++,h>n)throw P.message=`Failed to fetch ${e} after ${n} attempts`,P}return c}function O(s,t,e){let a=s;return t.arguments.forEach(r=>{if(e[r]===void 0)throw new Error(`Argument ${r} is missing`);a=a.replace(`[${r}]`,e[r])}),a}function U({route:s,api:t,url:e,config:a}){var n,c;const r=(c=(n=s.callbacks).before)==null?void 0:c.call(n,{route:s,api:t,url:e,config:a});return{beforeRoute:(r==null?void 0:r.route)||s,beforeApi:(r==null?void 0:r.api)||t,beforeUrl:(r==null?void 0:r.url)||e,beforeConfig:(r==null?void 0:r.config)||a}}function H({route:s,api:t,response:e,data:a}){var n,c;const r=(c=(n=s.callbacks).after)==null?void 0:c.call(n,{route:s,api:t,response:e,data:a});return{afterRoute:(r==null?void 0:r.route)||s,afterApi:(r==null?void 0:r.api)||t,afterResponse:(r==null?void 0:r.response)||e,afterData:(r==null?void 0:r.data)||a}}const u=class u{constructor(){o(this,"_elements",new Map);o(this,"_currentParent",null)}static get i(){return u._instance||(u._instance=new u),u._instance}registerElement(t){const e=this._currentParent;e&&(t.parent=this.getFullPath(e));const a=this.getElementKey(t);if(this._elements.set(a,t),t.type==="api"||t.type==="group"){let r=m;if(e){const n=this.getFullPath(e).split(".");for(const c of n)r[c]||(r[c]={}),r=r[c]}r[t.name]||(r[t.name]={})}}getCurrentParent(){return this._currentParent}setCurrentParent(t){const e=this._elements.get(t);if(!e||e.type!=="api"&&e.type!=="group")throw new Error(`Element ${t} not found or not a valid parent type`);this._currentParent=e}clearCurrentParent(){this._currentParent=null}registerRoute(t){if(!this._currentParent)throw new Error("No current parent set, use Route only inside Api or Group create callback");t.parent=this.getFullPath(this._currentParent);const e=this.getElementKey(t);this._elements.set(e,t),this.addToKlaimRoute(t)}addToKlaimRoute(t){if(!t.parent)return;let e=m;const a=t.parent.split(".");for(const r of a)e[r]||(e[r]={}),e=e[r];e[t.name]=function(r={},n={}){return x(t.parent,t,r,n)}}getElementKey(t){return t?t.parent?`${t.parent}.${t.name}`:t.name:""}getFullPath(t){if(!t)return"";if(!t.parent)return t.name;const e=[t.name];let a=t;for(;a.parent;){const r=this._elements.get(a.parent);if(!r)break;e.unshift(r.name),a=r}return e.join(".")}getRoute(t,e){return this._elements.get(`${t}.${e}`)}getChildren(t){const e=[];return this._elements.forEach(a=>{a.parent===t&&e.push(a)}),e}static updateElement(t){return u.i._elements.get(u.i.getElementKey(t))||t}getApi(t){const e=this._elements.get(t);if(!e){for(const[a,r]of this._elements.entries())if(r.type==="api"&&a.endsWith(`.${t}`))return r;return}return e.type==="api"?e:this.findApi(e)}findApi(t){if(!t||!t.parent)return;const e=t.parent.split(".");for(let a=e.length;a>=0;a--){const r=e.slice(0,a).join("."),n=this._elements.get(r);if((n==null?void 0:n.type)==="api")return n}}};o(u,"_instance");let i=u;class $ extends C{static create(t,e,a,r={}){const n=b(t);n!==t&&console.warn(`API name "${t}" has been camelCased to "${n}"`);const c=new $(n,e,r),l=i.i.getCurrentParent();i.i.registerElement(c);const h=l?i.i.getFullPath(l):"",f=h?`${h}.${n}`:n;return i.i.setCurrentParent(f),a(),l?i.i.setCurrentParent(i.i.getFullPath(l)):i.i.clearCurrentParent(),c}constructor(t,e,a={}){super("api",t,e,a)}}class k extends C{static create(t,e){const a=b(t),r=i.i.getCurrentParent(),n=r?i.i.getFullPath(r):"",c=n?`${n}.${a}`:a,l=new k(a,"");a!==t&&console.warn(`Group name "${t}" has been camelCased to "${a}"`),i.i.registerElement(l);const h=i.i.getCurrentParent();return i.i.setCurrentParent(c),e(),h?i.i.setCurrentParent(i.i.getFullPath(h)):i.i.clearCurrentParent(),l}constructor(t,e,a={}){super("group",t,e,a)}withCache(t=20){return super.withCache(t),i.i.getChildren(i.i.getFullPath(this)).forEach(e=>{e.cache||(e.cache=t)}),this}withRetry(t=2){return super.withRetry(t),i.i.getChildren(i.i.getFullPath(this)).forEach(e=>{e.retry||(e.retry=t)}),this}before(t){return super.before(t),i.i.getChildren(i.i.getFullPath(this)).forEach(e=>{e.callbacks.before||(e.callbacks.before=t)}),this}after(t){return super.after(t),i.i.getChildren(i.i.getFullPath(this)).forEach(e=>{e.callbacks.after||(e.callbacks.after=t)}),this}onCall(t){return super.onCall(t),i.i.getChildren(i.i.getFullPath(this)).forEach(e=>{e.callbacks.call||(e.callbacks.call=t)}),this}}class A extends C{constructor(t,e,a={},r="GET"){super("route",t,e,a),this.method=r,this.detectArguments()}static createRoute(t,e,a={},r){const n=new A(t,e,a,r);return i.i.registerRoute(n),n}static get(t,e,a={}){return this.createRoute(t,e,a,"GET")}static post(t,e,a={}){return this.createRoute(t,e,a,"POST")}static put(t,e,a={}){return this.createRoute(t,e,a,"PUT")}static delete(t,e,a={}){return this.createRoute(t,e,a,"DELETE")}static patch(t,e,a={}){return this.createRoute(t,e,a,"PATCH")}static options(t,e,a={}){return this.createRoute(t,e,a,"OPTIONS")}detectArguments(){const t=this.url.match(/\[([^\]]+)]/g);t&&t.forEach(e=>{const a=e.replace("[","").replace("]","");this.arguments.add(a)})}validate(t){return this.schema=t,this}}exports.Api=$;exports.Group=k;exports.Hook=E;exports.Klaim=m;exports.Registry=i;exports.Route=A;
|