@asaidimu/hestia 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 +139 -0
- package/auth/store.ts +75 -0
- package/auth/types.ts +38 -0
- package/blobs/store.ts +208 -0
- package/blobs/types.ts +28 -0
- package/collections/store.ts +59 -0
- package/collections/types.ts +11 -0
- package/container.ts +95 -0
- package/core/client.ts +247 -0
- package/core/collection.ts +121 -0
- package/core/errors.ts +55 -0
- package/core/pager.ts +159 -0
- package/core/types.ts +279 -0
- package/index.ts +62 -0
- package/package.json +43 -0
- package/system/api-keys/store.ts +70 -0
- package/system/api-keys/types.ts +29 -0
- package/system/capabilities/store.ts +23 -0
- package/system/capabilities/types.ts +1 -0
- package/system/identity/store.ts +90 -0
- package/system/identity/types.ts +31 -0
- package/system/logs/store.ts +147 -0
- package/system/logs/types.ts +38 -0
- package/system/policies/store.ts +74 -0
- package/system/policies/types.ts +43 -0
- package/test-setup.ts +53 -0
- package/utils/index.ts +1 -0
- package/utils/pager.ts +226 -0
- package/vitest.config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @asaidimu/hestia
|
|
2
|
+
|
|
3
|
+
TypeScript client SDK for the [Hestia](https://github.com/asaidimu/hestia) platform — a lightweight, modular backend framework.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auth** — login, register, refresh, logout, session management with auto-refresh on 401
|
|
8
|
+
- **Collections** — generic CRUD over any named collection via `HestiaCollection<T>`
|
|
9
|
+
- **API Keys** — create, list, get, update, rotate, delete API keys
|
|
10
|
+
- **Policies** — manage policy operations, rules, validation, and reload
|
|
11
|
+
- **Audit Logs** — query and filter audit entries
|
|
12
|
+
- **Blobs** — upload, download, list, and manage blob storage namespaces
|
|
13
|
+
- **Capabilities** — query available capabilities
|
|
14
|
+
- **Reactive Paging** — observable paginated data with resize/navigate/subscribe
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @asaidimu/hestia
|
|
20
|
+
# or
|
|
21
|
+
bun add @asaidimu/hestia
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { HestiaClient } from "@asaidimu/hestia"
|
|
28
|
+
|
|
29
|
+
const api = new HestiaClient({ baseUrl: "http://localhost:8070" })
|
|
30
|
+
|
|
31
|
+
// Login as admin
|
|
32
|
+
const { token, user } = await api.auth.login("admin@test.local", "password123")
|
|
33
|
+
console.log("Logged in as", user.email)
|
|
34
|
+
|
|
35
|
+
// List users
|
|
36
|
+
const { data: users } = await api.users.find()
|
|
37
|
+
console.log(`Found ${users.length} users`)
|
|
38
|
+
|
|
39
|
+
// Create an API key
|
|
40
|
+
const key = await api.keys.create({ name: "CI/CD Deploy Key" })
|
|
41
|
+
console.log("API key:", key.key)
|
|
42
|
+
|
|
43
|
+
// Reactive pager
|
|
44
|
+
const pager = api.users.page()
|
|
45
|
+
pager.subscribe((page) => console.log("Page:", page.page.number))
|
|
46
|
+
await pager.resize(25, 1)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `HestiaClient`
|
|
52
|
+
|
|
53
|
+
The main entry point. Construct with a `baseUrl` pointing to a Hestia server.
|
|
54
|
+
|
|
55
|
+
| Property | Type | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `auth` | `HestiaAuth` | Authentication (login, register, refresh, logout) |
|
|
58
|
+
| `users` | `HestiaUsers` | User management |
|
|
59
|
+
| `keys` | `HestiaKeyStore` | API key management |
|
|
60
|
+
| `policies` | `HestiaPolicies` | Policy operations and rules |
|
|
61
|
+
| `logs` | `HestiaLogs` | Audit log queries |
|
|
62
|
+
| `collections` | `HestiaCollections` | Generic collection metadata |
|
|
63
|
+
| `blobs` | `HestiaBlobClient` | Blob (file) storage |
|
|
64
|
+
| `capabilities` | `HestiaCapabilities` | Available capabilities |
|
|
65
|
+
| `client` | `HestiaNetworkClient` | Low-level HTTP client |
|
|
66
|
+
|
|
67
|
+
### Auth
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Login — stores tokens and identity in the reactive store
|
|
71
|
+
const result = await api.auth.login(email, password)
|
|
72
|
+
|
|
73
|
+
// Register a new user (requires admin session)
|
|
74
|
+
const user = await api.auth.register(email, password, name)
|
|
75
|
+
|
|
76
|
+
// Refresh tokens
|
|
77
|
+
const pair = await api.auth.refresh(refreshToken)
|
|
78
|
+
|
|
79
|
+
// Logout — clears stored tokens
|
|
80
|
+
await api.auth.logout()
|
|
81
|
+
|
|
82
|
+
// Health check (public endpoint, no auth required)
|
|
83
|
+
const health = await api.auth.health()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Collections
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// Get a typed collection handle
|
|
90
|
+
const docs = api.collection<MyType>("my_collection")
|
|
91
|
+
|
|
92
|
+
// CRUD operations
|
|
93
|
+
const list = await docs.find(query?)
|
|
94
|
+
const item = await docs.read(id)
|
|
95
|
+
const created = await docs.create(document)
|
|
96
|
+
const updated = await docs.update(id, partial)
|
|
97
|
+
await docs.delete(id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Reactive Paging
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const pager = api.users.page()
|
|
104
|
+
|
|
105
|
+
// Subscribe to page changes
|
|
106
|
+
const unsub = pager.subscribe((page) => render(page.data))
|
|
107
|
+
|
|
108
|
+
// Navigate, resize, sort, filter
|
|
109
|
+
await pager.navigate(2)
|
|
110
|
+
await pager.resize(50, 1)
|
|
111
|
+
await pager.sort({ field: "email", order: "asc" })
|
|
112
|
+
await pager.filter({ email: { $like: "%@example.com" } })
|
|
113
|
+
|
|
114
|
+
// Manual refresh
|
|
115
|
+
await pager.refresh()
|
|
116
|
+
|
|
117
|
+
// Cleanup
|
|
118
|
+
unsub()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Auto-Refresh
|
|
122
|
+
|
|
123
|
+
The client transparently refreshes expired JWT access tokens on 401 responses. Concurrent requests that receive 401 are deduplicated — only one refresh call is made. API-key requests (with `X-API-Key` header) are excluded from auto-refresh.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Install dependencies
|
|
130
|
+
bun install
|
|
131
|
+
|
|
132
|
+
# Run tests (requires a running test-server on :8070)
|
|
133
|
+
./test-server &
|
|
134
|
+
bun test
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/auth/store.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HestiaNetworkClient, IdentityProvider } from "../core/client";
|
|
2
|
+
import type { LoginResult, ServerHealth, TokenPair } from "./types";
|
|
3
|
+
|
|
4
|
+
export class HestiaAuth {
|
|
5
|
+
constructor(
|
|
6
|
+
private client: HestiaNetworkClient,
|
|
7
|
+
private provider: IdentityProvider,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async health(): Promise<ServerHealth> {
|
|
11
|
+
const res = await this.client.get<{ data: ServerHealth }>("/system/core/health");
|
|
12
|
+
return res.data!.data;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async login(email: string, password: string): Promise<LoginResult> {
|
|
16
|
+
const res = await this.client.post<{ data: LoginResult }>(
|
|
17
|
+
"/system/auth/session",
|
|
18
|
+
{ email, password },
|
|
19
|
+
);
|
|
20
|
+
const result = res.data!.data;
|
|
21
|
+
this.provider.setIdentity(result.user);
|
|
22
|
+
this.client.storeTokens(result.token.access, result.token.refresh);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async register(
|
|
27
|
+
email: string,
|
|
28
|
+
password: string,
|
|
29
|
+
name: string,
|
|
30
|
+
): Promise<{ _id_: string; email: string; name: string }> {
|
|
31
|
+
const res = await this.client.post<{
|
|
32
|
+
data: { _id_: string; email: string; name: string, scopes:string };
|
|
33
|
+
}>("/system/auth/user", { email, password, name });
|
|
34
|
+
return res.data!.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async refresh(refreshToken?: string): Promise<TokenPair> {
|
|
38
|
+
const body = refreshToken ? { refresh_token: refreshToken } : {};
|
|
39
|
+
const res = await this.client.patch<{ data: { token: TokenPair } }>(
|
|
40
|
+
"/system/auth/session",
|
|
41
|
+
body,
|
|
42
|
+
);
|
|
43
|
+
return res.data!.data.token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async logout(): Promise<void> {
|
|
47
|
+
const refresh = this.provider.token("refresh");
|
|
48
|
+
const body = refresh ? { refresh_token: refresh } : {};
|
|
49
|
+
await this.client.delete("/system/auth/session", body);
|
|
50
|
+
await this.provider.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async requestPasswordReset(email: string): Promise<void> {
|
|
54
|
+
await this.client.post("/system/auth/password", { email });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async confirmPasswordReset(
|
|
58
|
+
resetToken: string,
|
|
59
|
+
password: string,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
await this.client.patch(
|
|
62
|
+
"/system/auth/password",
|
|
63
|
+
{ password, token:resetToken },
|
|
64
|
+
{ headers: { Authorization: `Bearer ${resetToken}` } },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async bootstrap(key: string, password: string, email: string): Promise<void> {
|
|
69
|
+
await this.client.patch(
|
|
70
|
+
"/system/auth/bootstrap",
|
|
71
|
+
{ password, email },
|
|
72
|
+
{ headers: { "X-API-Key": key } },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/auth/types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { UserIdentity } from "../system/identity/types"
|
|
2
|
+
|
|
3
|
+
export interface TokenPair {
|
|
4
|
+
access: string
|
|
5
|
+
refresh: string
|
|
6
|
+
type: string
|
|
7
|
+
validity: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LoginResult {
|
|
11
|
+
token: TokenPair
|
|
12
|
+
user: UserIdentity
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ServerHealth {
|
|
16
|
+
bootstrapped: boolean
|
|
17
|
+
ok: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoginRequest {
|
|
21
|
+
email: string
|
|
22
|
+
password: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RegisterRequest {
|
|
26
|
+
email: string
|
|
27
|
+
password: string
|
|
28
|
+
name: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RefreshRequest {
|
|
32
|
+
refresh_token: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BootstrapPasswordRequest {
|
|
36
|
+
password: string
|
|
37
|
+
email: string
|
|
38
|
+
}
|
package/blobs/store.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { QueryDSL } from "@asaidimu/query";
|
|
2
|
+
import { ReactiveDataStore } from "@asaidimu/utils-store";
|
|
3
|
+
import { HestiaNetworkClient } from "../core/client";
|
|
4
|
+
import { createPagedController } from "../core/pager";
|
|
5
|
+
import type { Page, PagedData } from "../core/types";
|
|
6
|
+
import type {
|
|
7
|
+
BlobDocument,
|
|
8
|
+
BlobMeta,
|
|
9
|
+
CreateNamespaceRequest,
|
|
10
|
+
ListBlobsRequest,
|
|
11
|
+
NamespaceInfo,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
function asDoc(b: BlobMeta): BlobDocument {
|
|
15
|
+
return {
|
|
16
|
+
_id_: b.key,
|
|
17
|
+
_metadata_: {
|
|
18
|
+
checksum: "",
|
|
19
|
+
created: b.created_at,
|
|
20
|
+
updated: b.updated_at ?? b.created_at,
|
|
21
|
+
version: 1,
|
|
22
|
+
},
|
|
23
|
+
...b,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pageMeta<T extends Record<string,any>>(items: T[]): Page<T>["page"] {
|
|
28
|
+
return {
|
|
29
|
+
number: 1,
|
|
30
|
+
size: items.length,
|
|
31
|
+
count: items.length,
|
|
32
|
+
total: items.length,
|
|
33
|
+
pages: 1,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Namespace-scoped blob facade shaped like HestiaCollection.
|
|
39
|
+
* Wraps blob CRUD behind a DocumentStore-compatible interface
|
|
40
|
+
* so you can use DataTable + PagedData.
|
|
41
|
+
*/
|
|
42
|
+
export class BlobNamespace {
|
|
43
|
+
private pagerOptions = {};
|
|
44
|
+
private pager: PagedData<BlobMeta>;
|
|
45
|
+
private prefixFilter = "";
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private client: HestiaNetworkClient,
|
|
49
|
+
private ns: string,
|
|
50
|
+
) {
|
|
51
|
+
this.pager = createPagedController<BlobMeta>(
|
|
52
|
+
`blobs_${ns}`,
|
|
53
|
+
new ReactiveDataStore<any>({}),
|
|
54
|
+
this.pagerOptions,
|
|
55
|
+
(query) => this.find(query),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
name() {
|
|
60
|
+
return this.ns;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setPrefix(prefix: string) {
|
|
64
|
+
this.prefixFilter = prefix;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private basePath() {
|
|
68
|
+
return `/system/blobs/blob/${encodeURIComponent(this.ns)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async find(query?: QueryDSL<BlobMeta>): Promise<Page<BlobMeta>> {
|
|
72
|
+
const prefix = this.prefixFilter || (query as any)?.prefix || "";
|
|
73
|
+
const limit =
|
|
74
|
+
(query as any)?.limit ?? query?.pagination?.limit ?? 0;
|
|
75
|
+
|
|
76
|
+
const req: ListBlobsRequest = {};
|
|
77
|
+
if (prefix) req.prefix = prefix;
|
|
78
|
+
if (limit) req.limit = limit;
|
|
79
|
+
|
|
80
|
+
const res = await this.client.post<{
|
|
81
|
+
data: { blobs: BlobMeta[] };
|
|
82
|
+
}>(`${this.basePath()}/query`, req);
|
|
83
|
+
|
|
84
|
+
const items = res.data?.data?.blobs ?? [];
|
|
85
|
+
return { data: items.map(asDoc), loading: false, page: pageMeta(items), error: undefined };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async head(key: string): Promise<BlobDocument | undefined> {
|
|
89
|
+
try {
|
|
90
|
+
const res = await this.client.get<{ data: BlobMeta }>(
|
|
91
|
+
`${this.basePath()}/${encodeURIComponent(key)}`,
|
|
92
|
+
);
|
|
93
|
+
if (!res.data?.data) return undefined;
|
|
94
|
+
return asDoc(res.data.data);
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
if (
|
|
97
|
+
err?.code === "SYNC-001-NF" ||
|
|
98
|
+
(err?.code === "INTERNAL_ERROR" &&
|
|
99
|
+
typeof err?.message === "string" &&
|
|
100
|
+
err.message.includes("not found"))
|
|
101
|
+
)
|
|
102
|
+
return undefined;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async upload(
|
|
108
|
+
key: string,
|
|
109
|
+
data: Blob,
|
|
110
|
+
contentType?: string,
|
|
111
|
+
): Promise<BlobDocument> {
|
|
112
|
+
const headers: Record<string, string> = {};
|
|
113
|
+
const ct = contentType || data.type;
|
|
114
|
+
if (ct) headers["Content-Type"] = ct;
|
|
115
|
+
|
|
116
|
+
const res = await this.client.post<{ data: BlobMeta }>(
|
|
117
|
+
`${this.basePath()}/${encodeURIComponent(key)}`,
|
|
118
|
+
data,
|
|
119
|
+
{ headers, bodyType: "blob" },
|
|
120
|
+
);
|
|
121
|
+
return asDoc(res.data!.data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async download(
|
|
125
|
+
key: string,
|
|
126
|
+
): Promise<{ data: Blob; contentType: string }> {
|
|
127
|
+
const res = await this.client.get<Blob>(
|
|
128
|
+
`${this.basePath()}/${encodeURIComponent(key)}`,
|
|
129
|
+
{ responseType: "blob" },
|
|
130
|
+
);
|
|
131
|
+
const blob = res.data!;
|
|
132
|
+
return { data: blob, contentType: blob.type };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async updateMetadata(key: string, custom: Record<string, any>): Promise<BlobMeta> {
|
|
136
|
+
const res = await this.client.patch<{ data: BlobMeta }>(
|
|
137
|
+
`${this.basePath()}/${encodeURIComponent(key)}`,
|
|
138
|
+
{ custom },
|
|
139
|
+
);
|
|
140
|
+
return res.data!.data;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async delete(key: string): Promise<void> {
|
|
144
|
+
await this.client.delete(
|
|
145
|
+
`${this.basePath()}/${encodeURIComponent(key)}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async list(): Promise<BlobMeta[]> {
|
|
150
|
+
const res = await this.client.post<{
|
|
151
|
+
data: { blobs: BlobMeta[] };
|
|
152
|
+
}>(`${this.basePath()}/query`, {});
|
|
153
|
+
return res.data?.data?.blobs ?? [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
page(): PagedData<BlobMeta> {
|
|
157
|
+
return this.pager;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Top-level blob client. Entry point for all blob operations.
|
|
163
|
+
*
|
|
164
|
+
* Usage:
|
|
165
|
+
* client.blobs.listNamespaces()
|
|
166
|
+
* const ns = client.blobs.namespace("my-bucket")
|
|
167
|
+
* ns.find({ prefix: "images/" })
|
|
168
|
+
* ns.upload("logo.png", file)
|
|
169
|
+
* const { data } = await ns.download("logo.png")
|
|
170
|
+
*/
|
|
171
|
+
export class HestiaBlobClient {
|
|
172
|
+
constructor(private client: HestiaNetworkClient) {}
|
|
173
|
+
|
|
174
|
+
private nsBase = "/system/blobs";
|
|
175
|
+
|
|
176
|
+
// ── Namespace operations ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
async namespaces(): Promise<NamespaceInfo[]> {
|
|
179
|
+
const res = await this.client.post<{
|
|
180
|
+
data: { namespaces: NamespaceInfo[] };
|
|
181
|
+
}>(`${this.nsBase}/namespace/query`);
|
|
182
|
+
return res.data?.data?.namespaces ?? [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async createNamespace(data: CreateNamespaceRequest): Promise<NamespaceInfo> {
|
|
186
|
+
const res = await this.client.post<{ data: NamespaceInfo }>(
|
|
187
|
+
`${this.nsBase}/namespace`,
|
|
188
|
+
data,
|
|
189
|
+
);
|
|
190
|
+
return res.data!.data;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async deleteNamespace(ns: string): Promise<void> {
|
|
194
|
+
await this.client.delete(
|
|
195
|
+
`${this.nsBase}/namespace/${encodeURIComponent(ns)}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
blob(namespace: string, key:string) {
|
|
201
|
+
return `${this.client.base()}${this.nsBase}/blob/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`
|
|
202
|
+
}
|
|
203
|
+
// ── Namespace-scoped facade ───────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
namespace(ns: string): BlobNamespace {
|
|
206
|
+
return new BlobNamespace(this.client, ns);
|
|
207
|
+
}
|
|
208
|
+
}
|
package/blobs/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Document } from "../core/types"
|
|
2
|
+
|
|
3
|
+
export interface NamespaceInfo {
|
|
4
|
+
id: string
|
|
5
|
+
display_name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BlobMeta {
|
|
9
|
+
key: string
|
|
10
|
+
namespace_id: string
|
|
11
|
+
content_type: string
|
|
12
|
+
size: number
|
|
13
|
+
created_at: string
|
|
14
|
+
updated_at?: string
|
|
15
|
+
custom?: Record<string, any>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type BlobDocument = Document<BlobMeta>
|
|
19
|
+
|
|
20
|
+
export interface ListBlobsRequest {
|
|
21
|
+
prefix?: string
|
|
22
|
+
limit?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateNamespaceRequest {
|
|
26
|
+
display_name?: string
|
|
27
|
+
ns?: string
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { HestiaNetworkClient } from "../core/client"
|
|
2
|
+
import { HestiaCollection } from "../core/collection"
|
|
3
|
+
import type { Document } from "../core/types"
|
|
4
|
+
import type { CollectionMeta } from "./types"
|
|
5
|
+
|
|
6
|
+
export class HestiaCollections {
|
|
7
|
+
constructor(private client: HestiaNetworkClient) {}
|
|
8
|
+
|
|
9
|
+
async list(): Promise<{ collections: CollectionMeta[]; total: number }> {
|
|
10
|
+
const res = await this.client.get<{
|
|
11
|
+
data: { name: string; schema: any; created: string; updated: string }[]
|
|
12
|
+
}>("/system/collections/collection")
|
|
13
|
+
const items = res.data?.data ?? []
|
|
14
|
+
return {
|
|
15
|
+
collections: items.map((i) => ({
|
|
16
|
+
name: i.name,
|
|
17
|
+
schema: i.schema,
|
|
18
|
+
created: i.created,
|
|
19
|
+
updated: i.updated,
|
|
20
|
+
})),
|
|
21
|
+
total: items.length,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async read(name: string): Promise<CollectionMeta | undefined> {
|
|
26
|
+
try {
|
|
27
|
+
const res = await this.client.get<{ data: { name: string; schema: any; created: string; updated: string } }>(
|
|
28
|
+
`/system/collections/collection/${encodeURIComponent(name)}`,
|
|
29
|
+
)
|
|
30
|
+
if (!res.data) return undefined
|
|
31
|
+
return res.data.data
|
|
32
|
+
} catch (err: any) {
|
|
33
|
+
if (err?.code === "SYNC-001-NF") return undefined
|
|
34
|
+
throw err
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async create(schema: any): Promise<Document<{ schema: any }>> {
|
|
39
|
+
const res = await this.client.post<{ data: Document<{ schema: any }> }>("/system/collections/collection", schema)
|
|
40
|
+
return res.data!.data
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async update(name: string, schema: any): Promise<Document<{ schema: any }>> {
|
|
44
|
+
throw new Error("Method not implemented")
|
|
45
|
+
// const res = await this.client.patch<{ data: Document<{ schema: any }> }>(
|
|
46
|
+
// `/api/admin/collections/${encodeURIComponent(name)}`,
|
|
47
|
+
// schema,
|
|
48
|
+
// )
|
|
49
|
+
// return res.data!.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async delete(name: string): Promise<void> {
|
|
53
|
+
await this.client.delete(`/system/collections/collection/${encodeURIComponent(name)}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
documents<T extends Record<string, any>>(collectionName: string): HestiaCollection<T> {
|
|
57
|
+
return new HestiaCollection<T>(this.client,collectionName)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SchemaDefinition } from "@asaidimu/utils-schema"
|
|
2
|
+
import type { Document } from "../core/types"
|
|
3
|
+
|
|
4
|
+
export interface CollectionMeta {
|
|
5
|
+
name: string
|
|
6
|
+
schema: SchemaDefinition
|
|
7
|
+
created: string
|
|
8
|
+
updated: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CollectionDocument = Document<{ schema: any }>
|
package/container.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ArtifactContainer } from "@asaidimu/utils-artifacts";
|
|
2
|
+
import type { SimplePersistence } from "@asaidimu/utils-persistence";
|
|
3
|
+
import { ReactiveDataStore } from "@asaidimu/utils-store";
|
|
4
|
+
import { HestiaAuth } from "./auth/store";
|
|
5
|
+
import { HestiaCollections } from "./collections/store";
|
|
6
|
+
import {
|
|
7
|
+
HestiaNetworkClient,
|
|
8
|
+
type IdentityProvider,
|
|
9
|
+
} from "./core/client";
|
|
10
|
+
import { HestiaKeyStore } from "./system/api-keys/store";
|
|
11
|
+
import { HestiaUsers } from "./system/identity/store";
|
|
12
|
+
import { UserIdentity } from "./system/identity/types";
|
|
13
|
+
import { HestiaLogs } from "./system/logs/store";
|
|
14
|
+
import { HestiaPolicies } from "./system/policies/store";
|
|
15
|
+
import { HestiaBlobClient } from "./blobs/store";
|
|
16
|
+
import { HestiaCapabilities } from "./system/capabilities/store";
|
|
17
|
+
|
|
18
|
+
export interface HestiaConfig {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
persistence?: SimplePersistence<AuthState>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AuthState {
|
|
24
|
+
access: string | null;
|
|
25
|
+
refresh: string | null;
|
|
26
|
+
identity: UserIdentity | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Registry = Record<string, any>;
|
|
30
|
+
|
|
31
|
+
export class HestiaClient {
|
|
32
|
+
readonly store: ReactiveDataStore<AuthState>;
|
|
33
|
+
readonly container: ArtifactContainer<Registry, any>;
|
|
34
|
+
readonly client: HestiaNetworkClient;
|
|
35
|
+
readonly auth: HestiaAuth;
|
|
36
|
+
readonly users: HestiaUsers;
|
|
37
|
+
readonly keys: HestiaKeyStore;
|
|
38
|
+
readonly policies: HestiaPolicies;
|
|
39
|
+
readonly logs: HestiaLogs;
|
|
40
|
+
readonly collections: HestiaCollections;
|
|
41
|
+
readonly blobs: HestiaBlobClient;
|
|
42
|
+
readonly capabilities: HestiaCapabilities
|
|
43
|
+
private tokenProvider: IdentityProvider;
|
|
44
|
+
|
|
45
|
+
private onAuthStateChanged?: () => void;
|
|
46
|
+
|
|
47
|
+
constructor(config: HestiaConfig) {
|
|
48
|
+
this.store = new ReactiveDataStore<AuthState>(
|
|
49
|
+
{ access: null, refresh: null, identity: null },
|
|
50
|
+
config.persistence,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
this.container = new ArtifactContainer<Registry, any>(this.store);
|
|
54
|
+
|
|
55
|
+
const tokenProvider: IdentityProvider = {
|
|
56
|
+
identity: () => this.store.get().identity,
|
|
57
|
+
token: (key: "access" | "refresh") => this.store.get()[key],
|
|
58
|
+
setTokens: async (access: string, refresh: string) =>
|
|
59
|
+
void (await this.store.set({ access, refresh })),
|
|
60
|
+
setIdentity: async (identity) =>
|
|
61
|
+
void (await this.store.set({ identity })),
|
|
62
|
+
clear: async () =>
|
|
63
|
+
void (await this.store.set({ access: null, refresh: null })),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.tokenProvider = tokenProvider;
|
|
67
|
+
this.client = new HestiaNetworkClient(config.baseUrl, tokenProvider, () =>
|
|
68
|
+
this.onAuthStateChanged?.(),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
this.auth = new HestiaAuth(this.client, tokenProvider);
|
|
72
|
+
this.users = new HestiaUsers(this.client);
|
|
73
|
+
this.keys = new HestiaKeyStore(this.client,);
|
|
74
|
+
this.policies = new HestiaPolicies(this.client);
|
|
75
|
+
this.logs = new HestiaLogs(
|
|
76
|
+
this.client,
|
|
77
|
+
config.baseUrl,
|
|
78
|
+
);
|
|
79
|
+
this.collections = new HestiaCollections(this.client);
|
|
80
|
+
this.blobs = new HestiaBlobClient(this.client);
|
|
81
|
+
this.capabilities = new HestiaCapabilities(this.client)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onAuthStateChange(callback: () => void) {
|
|
85
|
+
this.onAuthStateChanged = callback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
authenticated(): boolean {
|
|
89
|
+
return this.tokenProvider.token("access") !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
collection<T extends Record<string, any>>(name: string) {
|
|
93
|
+
return this.collections.documents<T>(name);
|
|
94
|
+
}
|
|
95
|
+
}
|