@ecodrix/erix-client 1.0.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 +65 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +196 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @ecodrix/erix-client
|
|
2
|
+
|
|
3
|
+
Type-safe HTTP client for [erix-store](../README.md) — the ECODrIx in-memory data sidecar.
|
|
4
|
+
|
|
5
|
+
**Zero extra dependencies.** Uses native `fetch` (Node 18+).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ecodrix/erix-client
|
|
11
|
+
# or: copy client/src/index.ts directly into your project
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { ErixClient } from '@ecodrix/erix-client'
|
|
18
|
+
|
|
19
|
+
const store = new ErixClient({
|
|
20
|
+
baseUrl: process.env.ERIX_STORE_URL!, // https://erix-store.onrender.com
|
|
21
|
+
apiKey: process.env.ERIX_STORE_API_KEY!, // erix_xxxxxxxxxxxxxxxx
|
|
22
|
+
tenantId: req.user.orgId, // your tenant namespace
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Key / Value
|
|
26
|
+
await store.set('user:42', { name: 'Dhanesh' }, 3600) // TTL optional
|
|
27
|
+
const user = await store.get<User>('user:42')
|
|
28
|
+
|
|
29
|
+
// Queue
|
|
30
|
+
await store.queue.push('send-email', { to: 'a@b.com', template: 'welcome' })
|
|
31
|
+
const job = await store.queue.pop<EmailJob>('send-email')
|
|
32
|
+
|
|
33
|
+
// Rate limiter
|
|
34
|
+
const { allowed } = await store.rateLimit(`api:${orgId}`, 100, 60)
|
|
35
|
+
|
|
36
|
+
// Hash
|
|
37
|
+
await store.hash.hset('settings:org1', 'theme', 'dark')
|
|
38
|
+
const theme = await store.hash.hget<string>('settings:org1', 'theme')
|
|
39
|
+
|
|
40
|
+
// Health
|
|
41
|
+
const { status } = await store.ping()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See [PRODUCTION.md](../PRODUCTION.md) for full patterns, multi-tenant setup, and troubleshooting.
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
| Method | Description |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `store.set(key, value, ttl?)` | Store a JSON value with optional TTL in seconds |
|
|
51
|
+
| `store.get<T>(key)` | Retrieve value or `null` if missing/expired |
|
|
52
|
+
| `store.del(key)` | Delete a key |
|
|
53
|
+
| `store.hash.hset(key, field, value)` | Set a hash field |
|
|
54
|
+
| `store.hash.hget<T>(key, field)` | Get one hash field |
|
|
55
|
+
| `store.hash.hgetall(key)` | Get all fields of a hash |
|
|
56
|
+
| `store.list.lpush(key, value)` | Prepend to a list |
|
|
57
|
+
| `store.list.rpush(key, value)` | Append to a list |
|
|
58
|
+
| `store.list.lpop<T>(key)` | Pop from list front |
|
|
59
|
+
| `store.queue.push(name, data)` | Enqueue a job |
|
|
60
|
+
| `store.queue.pop<T>(name)` | Dequeue a job (`null` if empty) |
|
|
61
|
+
| `store.pubsub.publish(channel, msg)` | Publish an event |
|
|
62
|
+
| `store.rateLimit(key, limit, window)` | Check rate limit |
|
|
63
|
+
| `store.set_.sadd(key, member)` | Add to a set |
|
|
64
|
+
| `store.set_.smembers(key)` | Get all set members |
|
|
65
|
+
| `store.ping()` | Health check |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ecodrix/erix-client
|
|
3
|
+
*
|
|
4
|
+
* Type-safe HTTP client for the erix-store in-memory data service.
|
|
5
|
+
* Connects to a running erix-store instance without requiring the
|
|
6
|
+
* consumer to know any of the underlying HTTP route structure.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { ErixClient } from '@ecodrix/erix-client'
|
|
11
|
+
*
|
|
12
|
+
* const store = new ErixClient({
|
|
13
|
+
* baseUrl: 'https://erix-store.onrender.com',
|
|
14
|
+
* apiKey: process.env.ERIX_API_KEY!,
|
|
15
|
+
* tenantId: 'org_abc123',
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* await store.set('session:user1', { role: 'admin' }, 3600)
|
|
19
|
+
* const session = await store.get<{ role: string }>('session:user1')
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/** Any value that can be serialized to and from JSON */
|
|
23
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
24
|
+
[key: string]: JsonValue;
|
|
25
|
+
};
|
|
26
|
+
export interface ErixClientOptions {
|
|
27
|
+
/** Full URL of the deployed erix-store instance, e.g. https://erix-store.onrender.com */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** Shared secret generated at deploy time — must match ERIX_API_KEY on the server */
|
|
30
|
+
apiKey: string;
|
|
31
|
+
/** Tenant namespace — all keys are automatically namespaced under this prefix */
|
|
32
|
+
tenantId: string;
|
|
33
|
+
/** Optional request timeout in milliseconds (default: 5000) */
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface RateLimitResult {
|
|
37
|
+
allowed: boolean;
|
|
38
|
+
remaining: number;
|
|
39
|
+
resetAt: number;
|
|
40
|
+
}
|
|
41
|
+
export declare class ErixClient {
|
|
42
|
+
private readonly baseUrl;
|
|
43
|
+
private readonly headers;
|
|
44
|
+
private readonly timeoutMs;
|
|
45
|
+
constructor(options: ErixClientOptions);
|
|
46
|
+
private req;
|
|
47
|
+
/** Check if the erix-store instance is reachable */
|
|
48
|
+
ping(): Promise<{
|
|
49
|
+
status: string;
|
|
50
|
+
uptime: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Store any JSON value under a key.
|
|
54
|
+
* @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
|
|
55
|
+
*/
|
|
56
|
+
set(key: string, value: JsonValue, ttlSeconds?: number): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Retrieve a value by key. Returns `null` if the key doesn't exist or has expired.
|
|
59
|
+
*/
|
|
60
|
+
get<T = JsonValue>(key: string): Promise<T | null>;
|
|
61
|
+
/** Delete a key */
|
|
62
|
+
del(key: string): Promise<void>;
|
|
63
|
+
hash: {
|
|
64
|
+
/** Set a field in a hash */
|
|
65
|
+
hset: (key: string, field: string, value: JsonValue) => Promise<void>;
|
|
66
|
+
/** Get a single field from a hash */
|
|
67
|
+
hget: <T = JsonValue>(key: string, field: string) => Promise<T | null>;
|
|
68
|
+
/** Get all fields of a hash */
|
|
69
|
+
hgetall: (key: string) => Promise<Record<string, string>>;
|
|
70
|
+
};
|
|
71
|
+
list: {
|
|
72
|
+
/** Prepend a value to a list */
|
|
73
|
+
lpush: (key: string, value: JsonValue) => Promise<void>;
|
|
74
|
+
/** Append a value to a list */
|
|
75
|
+
rpush: (key: string, value: JsonValue) => Promise<void>;
|
|
76
|
+
/** Pop from the left (FIFO front) */
|
|
77
|
+
lpop: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
78
|
+
};
|
|
79
|
+
queue: {
|
|
80
|
+
/** Enqueue a job payload into a named queue */
|
|
81
|
+
push: (name: string, data: JsonValue) => Promise<void>;
|
|
82
|
+
/** Dequeue the oldest job from a named queue. Returns `null` if empty. */
|
|
83
|
+
pop: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
84
|
+
};
|
|
85
|
+
pubsub: {
|
|
86
|
+
/** Publish a message to a channel */
|
|
87
|
+
publish: (channel: string, message: JsonValue) => Promise<void>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Check if an operation is within rate limits.
|
|
91
|
+
* @param key Identifier (e.g. `"api:org_123"`)
|
|
92
|
+
* @param limit Maximum allowed operations in the window
|
|
93
|
+
* @param window Window duration in seconds
|
|
94
|
+
*/
|
|
95
|
+
rateLimit(key: string, limit: number, window: number): Promise<RateLimitResult>;
|
|
96
|
+
set_: {
|
|
97
|
+
/** Add a member to a set */
|
|
98
|
+
sadd: (key: string, value: string) => Promise<{
|
|
99
|
+
added: number;
|
|
100
|
+
}>;
|
|
101
|
+
/** Get all members of a set */
|
|
102
|
+
smembers: (key: string) => Promise<string[]>;
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ecodrix/erix-client
|
|
3
|
+
*
|
|
4
|
+
* Type-safe HTTP client for the erix-store in-memory data service.
|
|
5
|
+
* Connects to a running erix-store instance without requiring the
|
|
6
|
+
* consumer to know any of the underlying HTTP route structure.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { ErixClient } from '@ecodrix/erix-client'
|
|
11
|
+
*
|
|
12
|
+
* const store = new ErixClient({
|
|
13
|
+
* baseUrl: 'https://erix-store.onrender.com',
|
|
14
|
+
* apiKey: process.env.ERIX_API_KEY!,
|
|
15
|
+
* tenantId: 'org_abc123',
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* await store.set('session:user1', { role: 'admin' }, 3600)
|
|
19
|
+
* const session = await store.get<{ role: string }>('session:user1')
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
23
|
+
export class ErixClient {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
// ─── Hash ─────────────────────────────────────────────────────────────
|
|
26
|
+
this.hash = {
|
|
27
|
+
/** Set a field in a hash */
|
|
28
|
+
hset: async (key, field, value) => {
|
|
29
|
+
await this.req("POST", "/hash/hset", {
|
|
30
|
+
key,
|
|
31
|
+
field,
|
|
32
|
+
value: JSON.stringify(value),
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
/** Get a single field from a hash */
|
|
36
|
+
hget: async (key, field) => {
|
|
37
|
+
const data = await this.req("GET", "/hash/hget", undefined, { key, field });
|
|
38
|
+
if (!data.value)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(data.value);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return data.value;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
/** Get all fields of a hash */
|
|
48
|
+
hgetall: async (key) => {
|
|
49
|
+
const data = await this.req("GET", "/hash/hgetall", undefined, { key });
|
|
50
|
+
return data.data;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
// ─── List ─────────────────────────────────────────────────────────────
|
|
54
|
+
this.list = {
|
|
55
|
+
/** Prepend a value to a list */
|
|
56
|
+
lpush: async (key, value) => {
|
|
57
|
+
await this.req("POST", "/list/lpush", {
|
|
58
|
+
key,
|
|
59
|
+
value: JSON.stringify(value),
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
/** Append a value to a list */
|
|
63
|
+
rpush: async (key, value) => {
|
|
64
|
+
await this.req("POST", "/list/rpush", {
|
|
65
|
+
key,
|
|
66
|
+
value: JSON.stringify(value),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
/** Pop from the left (FIFO front) */
|
|
70
|
+
lpop: async (key) => {
|
|
71
|
+
const data = await this.req("GET", "/list/lpop", undefined, { key });
|
|
72
|
+
if (!data.value)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(data.value);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return data.value;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
// ─── Queue (FIFO job queue, built on List) ────────────────────────────
|
|
83
|
+
this.queue = {
|
|
84
|
+
/** Enqueue a job payload into a named queue */
|
|
85
|
+
push: async (name, data) => {
|
|
86
|
+
await this.list.rpush(`q:${name}`, data);
|
|
87
|
+
},
|
|
88
|
+
/** Dequeue the oldest job from a named queue. Returns `null` if empty. */
|
|
89
|
+
pop: async (name) => {
|
|
90
|
+
return this.list.lpop(`q:${name}`);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
// ─── PubSub ───────────────────────────────────────────────────────────
|
|
94
|
+
this.pubsub = {
|
|
95
|
+
/** Publish a message to a channel */
|
|
96
|
+
publish: async (channel, message) => {
|
|
97
|
+
await this.req("POST", "/pubsub/publish", { channel, message });
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
// ─── Set ──────────────────────────────────────────────────────────────
|
|
101
|
+
this.set_ = {
|
|
102
|
+
/** Add a member to a set */
|
|
103
|
+
sadd: async (key, value) => {
|
|
104
|
+
return this.req("POST", "/set/sadd", { key, value });
|
|
105
|
+
},
|
|
106
|
+
/** Get all members of a set */
|
|
107
|
+
smembers: async (key) => {
|
|
108
|
+
const data = await this.req("GET", "/set/smembers", undefined, { key });
|
|
109
|
+
return data.members;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, ""); // strip trailing slash
|
|
113
|
+
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
114
|
+
this.headers = {
|
|
115
|
+
"x-erix-key": options.apiKey,
|
|
116
|
+
"x-tenant-id": options.tenantId,
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ── Internal fetch helper ─────────────────────────────────────────────
|
|
121
|
+
async req(method, path, body, params) {
|
|
122
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
123
|
+
if (params) {
|
|
124
|
+
for (const [k, v] of Object.entries(params)) {
|
|
125
|
+
url.searchParams.set(k, v);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(url.toString(), {
|
|
132
|
+
method,
|
|
133
|
+
headers: this.headers,
|
|
134
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const err = (await res.json().catch(() => ({})));
|
|
139
|
+
throw new Error(`[erix-store] ${method} ${path} → ${res.status}: ${err.error ?? res.statusText}`);
|
|
140
|
+
}
|
|
141
|
+
return (await res.json());
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ─── Health ───────────────────────────────────────────────────────────
|
|
148
|
+
/** Check if the erix-store instance is reachable */
|
|
149
|
+
async ping() {
|
|
150
|
+
return this.req("GET", "/health");
|
|
151
|
+
}
|
|
152
|
+
// ─── Core: Key / Value ────────────────────────────────────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Store any JSON value under a key.
|
|
155
|
+
* @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
|
|
156
|
+
*/
|
|
157
|
+
async set(key, value, ttlSeconds) {
|
|
158
|
+
await this.req("POST", "/core/set", {
|
|
159
|
+
key,
|
|
160
|
+
value: JSON.stringify(value),
|
|
161
|
+
ttl: ttlSeconds,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Retrieve a value by key. Returns `null` if the key doesn't exist or has expired.
|
|
166
|
+
*/
|
|
167
|
+
async get(key) {
|
|
168
|
+
const data = await this.req("GET", "/core/get", undefined, { key });
|
|
169
|
+
if (!data.value)
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(data.value);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return data.value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Delete a key */
|
|
179
|
+
async del(key) {
|
|
180
|
+
await this.req("DELETE", "/core/del", { key });
|
|
181
|
+
}
|
|
182
|
+
// ─── Rate Limiter ─────────────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* Check if an operation is within rate limits.
|
|
185
|
+
* @param key Identifier (e.g. `"api:org_123"`)
|
|
186
|
+
* @param limit Maximum allowed operations in the window
|
|
187
|
+
* @param window Window duration in seconds
|
|
188
|
+
*/
|
|
189
|
+
async rateLimit(key, limit, window) {
|
|
190
|
+
return this.req("POST", "/ratelimit", {
|
|
191
|
+
key,
|
|
192
|
+
limit,
|
|
193
|
+
window,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecodrix/erix-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type-safe HTTP client for the erix-store in-memory data service. Zero dependencies — uses native fetch.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"erix-store",
|
|
24
|
+
"cache",
|
|
25
|
+
"queue",
|
|
26
|
+
"rate-limit",
|
|
27
|
+
"pubsub",
|
|
28
|
+
"ecodrix",
|
|
29
|
+
"redis-alternative",
|
|
30
|
+
"in-memory"
|
|
31
|
+
],
|
|
32
|
+
"author": "dhanesh125",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.12.7",
|
|
40
|
+
"typescript": "^5.4.5"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/dhanesh125/erix-store"
|
|
45
|
+
}
|
|
46
|
+
}
|