@asaidimu/hestia 1.0.1 → 1.0.3

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.
@@ -1,113 +0,0 @@
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 { Document, Page, PagedData, StoreEvent } from "../../core/types";
6
- import type { DocumentStore } from "../../core/types";
7
- import type { UpdateUserRequest, UserData } from "./types";
8
-
9
- export class HestiaUsers implements DocumentStore<UserData, QueryDSL<UserData>, string, QueryDSL<UserData>, Record<string, unknown>, string, string, Record<string, unknown>> {
10
- private pagerOptions: any = {};
11
- private pager: PagedData<UserData>;
12
-
13
- constructor(
14
- private client: HestiaNetworkClient,
15
- ) {
16
- this.pager = createPagedController<UserData>(
17
- "users",
18
- new ReactiveDataStore<any>({}),
19
- this.pagerOptions,
20
- (query) => this.find(query),
21
- );
22
- }
23
-
24
- name() {
25
- return "users";
26
- }
27
-
28
- async find(query?: QueryDSL<UserData>): Promise<Page<UserData>> {
29
- const res = await this.client.post<{
30
- data: Document<UserData>[];
31
- metadata?: { page?: any };
32
- }>("/system/users/user/query", query ?? {});
33
- const data = res.data?.data ?? [];
34
- const pageMeta = res.data?.metadata?.page ?? {
35
- number: 1,
36
- size: data.length,
37
- count: data.length,
38
- total: data.length,
39
- pages: 1,
40
- };
41
- return { data, loading: false, page: pageMeta };
42
- }
43
-
44
- async list(options?: QueryDSL<UserData>): Promise<Page<UserData>> {
45
- return this.find(
46
- options ?? { pagination: { type: "offset", offset: 0, limit: 50 } },
47
- );
48
- }
49
-
50
- async read(id: string): Promise<Document<UserData> | undefined> {
51
- try {
52
- const res = await this.client.get<{ data: Document<UserData> }>(
53
- `/system/users/user/${encodeURIComponent(id)}`,
54
- );
55
- return res.data?.data;
56
- } catch (err: any) {
57
- if (err?.code === "SYNC-001-NF") return undefined;
58
- throw err;
59
- }
60
- }
61
-
62
- async update(props: { data: Partial<UserData>; options?: string }): Promise<Document<UserData> | undefined> {
63
- const id = props.options!;
64
- const res = await this.client.patch<{ data: Document<UserData> }>(
65
- `/system/users/user/${encodeURIComponent(id)}`,
66
- props.data as any,
67
- );
68
- return res.data!.data;
69
- }
70
-
71
- async delete(id: string): Promise<void> {
72
- await this.client.delete(`/system/users/user/${encodeURIComponent(id)}`);
73
- }
74
-
75
- async create(props: { data: Partial<UserData> }): Promise<Document<UserData> | undefined> {
76
- throw new Error("User creation requires email/password/name, use register endpoint");
77
- }
78
-
79
- async upload(_props: { file: File }): Promise<Document<UserData> | undefined> {
80
- throw new Error("Upload not supported for users");
81
- }
82
-
83
- async subscribe(_scope: string, _callback: (event: StoreEvent) => void): Promise<() => void> {
84
- throw new Error("Subscription not supported for users");
85
- }
86
-
87
- async notify(_event: StoreEvent): Promise<void> {
88
- throw new Error("Notify not supported for users");
89
- }
90
-
91
- stream(_options: Record<string, unknown>, _onStreamChange: () => void): {
92
- stream: () => AsyncIterable<Document<UserData>>;
93
- cancel: () => void;
94
- status: () => "active" | "cancelled" | "completed";
95
- } {
96
- throw new Error("Stream not supported for users");
97
- }
98
-
99
- page(_options?: Record<string, unknown>): PagedData<UserData> {
100
- return this.pager;
101
- }
102
-
103
- async changePassword(
104
- userId: string,
105
- current: string,
106
- newPassword: string,
107
- ): Promise<void> {
108
- await this.client.patch(`/system/users/password/${encodeURIComponent(userId)}`, {
109
- current,
110
- new: newPassword,
111
- });
112
- }
113
- }
@@ -1,31 +0,0 @@
1
- import type { Document } from "../../core/types"
2
-
3
- export interface UserData {
4
- email: string
5
- name: string
6
- verified: boolean
7
- permissions: string[]
8
- deleted?: string | null
9
- }
10
-
11
- export type UserIdentity = Document<UserData>
12
-
13
- export interface CreateUserRequest {
14
- email: string
15
- password: string
16
- name: string
17
- permissions?: string[]
18
- verified?: boolean
19
- }
20
-
21
- export interface UpdateUserRequest {
22
- name?: string
23
- email?: string
24
- permissions?: string[]
25
- verified?: boolean
26
- }
27
-
28
- export interface ChangePasswordRequest {
29
- current: string
30
- new: string
31
- }
@@ -1,177 +0,0 @@
1
- import type { QueryDSL } from "@asaidimu/query"
2
- import { HestiaNetworkClient } from "../../core/client"
3
- import { ReactiveDataStore } from "@asaidimu/utils-store"
4
- import { createPagedController } from "../../core/pager"
5
- import type { Document, Page, PagedData, PaginationInfo, StoreEvent } from "../../core/types"
6
- import type { DocumentStore } from "../../core/types"
7
- import type {
8
- OperationPolicy,
9
- IamRule,
10
- UpsertOperationRequest,
11
- UpsertRuleRequest,
12
- ReloadResult,
13
- } from "./types"
14
-
15
- const OPERATIONS_COLLECTION = "_operation_policy_"
16
- const RULES_COLLECTION = "_iam_rule_"
17
-
18
- export class HestiaPolicies implements DocumentStore<OperationPolicy, QueryDSL<OperationPolicy>, string, QueryDSL<OperationPolicy>, Record<string, unknown>, string, string, Record<string, unknown>> {
19
- private pager: PagedData<OperationPolicy>
20
- private operationsPath = "/system/policies/operation"
21
- private rulesPath = "/system/policies/rule"
22
-
23
- constructor(private client: HestiaNetworkClient) {
24
- this.pager = createPagedController<OperationPolicy>(
25
- "policies",
26
- new ReactiveDataStore<any>({}),
27
- {},
28
- (query) => this.find(query),
29
- )
30
- }
31
-
32
- private async collectionQuery<T extends Record<string, any>>(
33
- collection: string,
34
- query?: Record<string, unknown>,
35
- ): Promise<Page<T>> {
36
- const res = await this.client.post<{
37
- data: Document<T>[];
38
- metadata?: { page?: PaginationInfo };
39
- }>(`/system/collections/document/${encodeURIComponent(collection)}/query`, query ?? {})
40
- const items = res.data?.data ?? []
41
- const pageMeta = res.data?.metadata?.page ?? {
42
- number: 1,
43
- size: items.length,
44
- count: items.length,
45
- total: items.length,
46
- pages: 1,
47
- }
48
- return { data: items, loading: false, page: pageMeta, error: null }
49
- }
50
-
51
- async find(query?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
52
- return this.collectionQuery<OperationPolicy>(OPERATIONS_COLLECTION, query as Record<string, unknown> | undefined)
53
- }
54
-
55
- async list(options?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
56
- return this.find(options ?? { pagination: { type: "offset", offset: 0, limit: 50 } })
57
- }
58
-
59
- async read(id: string): Promise<Document<OperationPolicy> | undefined> {
60
- try {
61
- const res = await this.client.get<{ data: Document<OperationPolicy> }>(
62
- `${this.operationsPath}/${encodeURIComponent(id)}`,
63
- )
64
- return res.data?.data
65
- } catch (err: any) {
66
- if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
67
- throw err
68
- }
69
- }
70
-
71
- async create(props: { data: Partial<OperationPolicy> }): Promise<Document<OperationPolicy> | undefined> {
72
- const name = props.data.name
73
- if (!name) throw new Error("Operation name is required for create")
74
- const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
75
- `${this.operationsPath}/${encodeURIComponent(name)}`,
76
- props.data as UpsertOperationRequest,
77
- )
78
- return res.data!.data
79
- }
80
-
81
- async update(props: { data: Partial<OperationPolicy>; options?: string }): Promise<Document<OperationPolicy> | undefined> {
82
- const name = props.options!
83
- if (!name) throw new Error("Operation name is required for update")
84
- const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
85
- `${this.operationsPath}/${encodeURIComponent(name)}`,
86
- props.data as UpsertOperationRequest,
87
- )
88
- return res.data!.data
89
- }
90
-
91
- async delete(id: string): Promise<void> {
92
- await this.client.delete(`${this.operationsPath}/${encodeURIComponent(id)}`)
93
- }
94
-
95
- async upsertOperation(
96
- name: string,
97
- data: UpsertOperationRequest,
98
- ): Promise<Document<OperationPolicy>> {
99
- const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
100
- `${this.operationsPath}/${encodeURIComponent(name)}`,
101
- data,
102
- )
103
- return res.data!.data
104
- }
105
-
106
- async getRule(name: string): Promise<Document<IamRule> | undefined> {
107
- try {
108
- const res = await this.client.get<{ data: Document<IamRule> }>(
109
- `${this.rulesPath}/${encodeURIComponent(name)}`,
110
- )
111
- return res.data?.data
112
- } catch (err: any) {
113
- if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
114
- throw err
115
- }
116
- }
117
-
118
- async upsertRule(
119
- name: string,
120
- data: UpsertRuleRequest,
121
- ): Promise<Document<IamRule>> {
122
- const res = await this.client.patch<{ data: Document<IamRule> }>(
123
- `${this.rulesPath}/${encodeURIComponent(name)}`,
124
- data,
125
- )
126
- return res.data!.data
127
- }
128
-
129
- async deleteRule(name: string): Promise<void> {
130
- await this.client.delete(`${this.rulesPath}/${encodeURIComponent(name)}`)
131
- }
132
-
133
- async listRules(options?: Record<string, unknown>): Promise<Page<IamRule>> {
134
- return this.collectionQuery<IamRule>(RULES_COLLECTION, options)
135
- }
136
-
137
- async validateRule(
138
- expression: string,
139
- ): Promise<boolean> {
140
- const res = await this.client.post<{ data: { valid: boolean } }>(
141
- `${this.rulesPath}/validate`,
142
- { expression },
143
- )
144
- return res.data?.data?.valid ?? false
145
- }
146
-
147
- async reload(): Promise<ReloadResult> {
148
- const res = await this.client.get<{ data: ReloadResult }>(
149
- `${this.rulesPath}/reload`,
150
- )
151
- return res.data!.data
152
- }
153
-
154
- async upload(_props: { file: File }): Promise<Document<OperationPolicy> | undefined> {
155
- throw new Error("Upload not supported for policies")
156
- }
157
-
158
- async subscribe(_scope: string, _callback: (event: StoreEvent) => void): Promise<() => void> {
159
- throw new Error("Subscription not supported for policies")
160
- }
161
-
162
- async notify(_event: StoreEvent): Promise<void> {
163
- throw new Error("Notify not supported for policies")
164
- }
165
-
166
- stream(_options: Record<string, unknown>, _onStreamChange: () => void): {
167
- stream: () => AsyncIterable<Document<OperationPolicy>>;
168
- cancel: () => void;
169
- status: () => "active" | "cancelled" | "completed";
170
- } {
171
- throw new Error("Stream not supported for policies")
172
- }
173
-
174
- page(_options?: Record<string, unknown>): PagedData<OperationPolicy> {
175
- return this.pager
176
- }
177
- }
@@ -1,43 +0,0 @@
1
- export interface OperationPolicy {
2
- name: string
3
- ruleKey: string
4
- description?: string
5
- intentType?: "COMMAND" | "QUERY"
6
- }
7
-
8
- export interface UpsertOperationRequest {
9
- ruleKey: string
10
- description?: string
11
- intentType?: "COMMAND" | "QUERY"
12
- }
13
-
14
- export interface IamRule {
15
- name: string
16
- ruleType?: string
17
- expression?: string
18
- description?: string
19
- }
20
-
21
- export interface UpsertRuleRequest {
22
- expression: string
23
- ruleType?: string
24
- description?: string
25
- }
26
-
27
- export interface ValidateRuleRequest {
28
- expression: string
29
- identity?: Record<string, unknown>
30
- resource?: Record<string, unknown>
31
- environment?: Record<string, unknown>
32
- }
33
-
34
- export interface ValidateRuleResult {
35
- valid: boolean
36
- compile?: string
37
- result?: boolean
38
- }
39
-
40
- export interface ReloadResult {
41
- operations: number
42
- rules: number
43
- }
package/test-setup.ts DELETED
@@ -1,53 +0,0 @@
1
- import { spawn, type ChildProcess } from "child_process"
2
- import { resolve } from "path"
3
-
4
- const PROJECT_ROOT = resolve(import.meta.dirname, "..")
5
- const SERVER_BIN = resolve(PROJECT_ROOT, "test-server")
6
- const START_TIMEOUT = 30000
7
-
8
- let serverProc: ChildProcess | null = null
9
-
10
- function sleep(ms: number): Promise<void> {
11
- return new Promise((r) => setTimeout(r, ms))
12
- }
13
-
14
- export async function setup(): Promise<void> {
15
- await new Promise<void>((resolvePromise, reject) => {
16
- serverProc = spawn(SERVER_BIN, [], {
17
- cwd: PROJECT_ROOT,
18
- stdio: ["ignore", "pipe", "pipe"],
19
- })
20
-
21
- let started = false
22
- const onData = (data: Buffer) => {
23
- const text = data.toString().trim()
24
- if (!started && /^\d+$/.test(text)) {
25
- started = true
26
- resolvePromise()
27
- }
28
- }
29
-
30
- serverProc.stdout!.on("data", onData)
31
- serverProc.stderr!.on("data", () => {})
32
-
33
- serverProc.on("error", (err) => {
34
- if (!started) reject(err)
35
- })
36
- serverProc.on("exit", (code) => {
37
- if (!started) reject(new Error(`server exited with code ${code} before ready`))
38
- })
39
-
40
- setTimeout(() => {
41
- if (!started) reject(new Error("server start timeout"))
42
- }, START_TIMEOUT)
43
- })
44
-
45
- await sleep(1000)
46
- }
47
-
48
- export async function teardown(): Promise<void> {
49
- if (serverProc && !serverProc.killed) {
50
- serverProc.kill("SIGTERM")
51
- serverProc = null
52
- }
53
- }
package/utils/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./pager"
package/utils/pager.ts DELETED
@@ -1,230 +0,0 @@
1
- import { createMatcher, createSorter } from '@asaidimu/query'; //
2
- import type { QueryDSL, QueryFilter, SortConfiguration } from '@asaidimu/query'; //
3
- import { DELETE_SYMBOL, ReactiveDataStore } from "@asaidimu/utils-store"; //
4
- import type { Page, PagedData, PagerRefreshOptions } from '../core/types';
5
-
6
- export interface FetchResult<T extends Record<string, any>> {
7
- data: T[];
8
- replace: boolean;
9
- scope: 'page' | 'collection';
10
- total?: number;
11
- }
12
-
13
- // Extensible parameters object embracing your DataStore infra
14
- export interface ArrayPagerOptions<T extends Record<string, any>, F = any> {
15
- collectionName: string; // Unique identifier used to partition the cache matrix
16
- initialData?: T[];
17
- page?: number;
18
- size?: number;
19
- customFunctions?: Record<string, (...args: any[]) => boolean>;
20
- fetch?: (
21
- query: QueryDSL<T, F>
22
- ) => Promise<FetchResult<T>> | FetchResult<T>;
23
- }
24
-
25
- export function createArrayPager<T extends Record<string, any>, F = any>(
26
- options: ArrayPagerOptions<T, F>
27
- ): PagedData<T> {
28
- const CACHE_KEY = `${options.collectionName}_array_pager_state_`; //
29
- const store = new ReactiveDataStore({})
30
-
31
- // Baseline backing arrays managed by this adapter instance
32
- let pristineData = [...(options.initialData ?? [])];
33
- let activeScope: 'page' | 'collection' = 'collection';
34
- let remoteTotalRecords: number | undefined = undefined;
35
-
36
- // Track active filter parameters
37
- let currentPageNum = options.page ?? 1;
38
- let currentPageSize = options.size ?? 20; // Defaulting to 20 per your page controller sentinel
39
- let currentSort: SortConfiguration<T>[] = [];
40
- let currentFilter: QueryFilter<T> | undefined = undefined;
41
-
42
- // Tracker for errors across async horizons
43
- let operationalError: any = undefined;
44
-
45
- const { match } = createMatcher<T, F>(options.customFunctions ?? {}); //
46
- const { sort: localSort} = createSorter()
47
- const selector = store.select((s: any) => s[CACHE_KEY]); //
48
-
49
- // Default fallback layout conforming strictly to Page<T>
50
- const sentinel: Page<T> = { //
51
- data: [], //
52
- loading: true, //
53
- error: undefined,
54
- page: { //
55
- number: 1, //
56
- size: currentPageSize, //
57
- count: 0, //
58
- total: 0, //
59
- pages: 1, //
60
- },
61
- };
62
-
63
- /**
64
- * Constructs the current active QueryDSL footprint.
65
- */
66
- const buildQueryDSL = (): QueryDSL<T, F> => ({ //
67
- filters: currentFilter, //
68
- sort: currentSort, //
69
- pagination: { //
70
- type: 'offset', //
71
- offset: (currentPageNum - 1) * currentPageSize, //
72
- limit: currentPageSize //
73
- }
74
- });
75
-
76
- /**
77
- * Orchestrates the fetching lifecycle and syncs results into the centralized store.
78
- */
79
- const executeDataPipeline = async (isLoadingFallback = true) => {
80
- if (options.fetch) {
81
- operationalError = undefined;
82
-
83
- if (isLoadingFallback) {
84
- // Broadcast transitional loading state via the central store
85
- const previousState = selector.get() ?? sentinel;
86
- await store.set({
87
- [CACHE_KEY]: { ...previousState, loading: true, error: undefined }
88
- });
89
- }
90
-
91
- try {
92
- const queryDSL = buildQueryDSL();
93
- const result = await options.fetch(queryDSL);
94
-
95
- activeScope = result.scope;
96
- remoteTotalRecords = result.total;
97
-
98
- pristineData = result.replace
99
- ? [...result.data]
100
- : [...pristineData, ...result.data];
101
- } catch (error) {
102
- operationalError = error;
103
- }
104
- }
105
-
106
- // Process and dispatch to store
107
- const updatedPagePayload = computeProcessedPage(options.fetch ? isLoadingFallback : false);
108
- await store.set({ [CACHE_KEY]: updatedPagePayload }); //
109
- };
110
-
111
- /**
112
- * Coordinates local filtering, sorting and slicing transformations
113
- * based on the active structural scope strategy.
114
- */
115
- const computeProcessedPage = (isLoadingFlag: boolean): Page<T> => { //
116
- let records = [...pristineData];
117
-
118
- if (activeScope === 'collection') {
119
- // Execute standard local processing passes over the full source collection
120
- if (currentFilter) {
121
- records = records.filter((item) => match(item, currentFilter!)); //
122
- }
123
- if (currentSort.length > 0) {
124
- records = localSort(records, currentSort); //
125
- }
126
-
127
- const total = records.length;
128
- const pages = Math.ceil(total / currentPageSize) || 1;
129
-
130
- if (currentPageNum > pages) currentPageNum = pages;
131
- if (currentPageNum < 1) currentPageNum = 1;
132
-
133
- const startIdx = (currentPageNum - 1) * currentPageSize;
134
- const slicedData = records.slice(startIdx, startIdx + currentPageSize);
135
-
136
- return {
137
- data: slicedData as any,
138
- loading: isLoadingFlag,
139
- error: operationalError,
140
- page: { //
141
- number: currentPageNum,
142
- size: currentPageSize,
143
- count: slicedData.length,
144
- total,
145
- pages,
146
- }
147
- };
148
- }
149
-
150
- // Server-Driven mode: Data array has already been shaped for this target viewport
151
- const total = remoteTotalRecords ?? records.length;
152
- const pages = Math.ceil(total / currentPageSize) || 1;
153
-
154
- return {
155
- data: records as any,
156
- loading: isLoadingFlag,
157
- error: operationalError,
158
- page: { //
159
- number: currentPageNum,
160
- size: currentPageSize,
161
- count: records.length,
162
- total,
163
- pages,
164
- }
165
- };
166
- };
167
-
168
- // Kickstart immediate bootstrapping pass
169
- executeDataPipeline(true);
170
-
171
- return {
172
- id: () => CACHE_KEY,
173
- page: () => selector.get() ?? sentinel, //
174
-
175
- navigate: async (page: number) => {
176
- if (page < 1) throw new Error("Page number must be >= 1"); //
177
- currentPageNum = page;
178
- await executeDataPipeline(true);
179
- },
180
-
181
- resize: async (size: number, page: number) => {
182
- if (size < 1) throw new Error("Page size must be >= 1"); //
183
- currentPageSize = size;
184
- currentPageNum = page;
185
- await executeDataPipeline(true);
186
- },
187
-
188
- sort: async (sortConfig) => {
189
- currentSort = Array.isArray(sortConfig) ? sortConfig : [sortConfig];
190
- currentPageNum = 1; // Reset window index
191
- await executeDataPipeline(true);
192
- },
193
-
194
- filter: async (queryFilter) => {
195
- currentFilter = queryFilter;
196
- currentPageNum = 1; // Reset window index
197
- await executeDataPipeline(true);
198
- },
199
-
200
- refresh: async (opts?: PagerRefreshOptions) => { //
201
- const previousState = selector.get() ?? sentinel;
202
- await store.set({
203
- [CACHE_KEY]: { ...previousState, loading: true } //
204
- });
205
-
206
- const delay = opts?.delay ?? 600;
207
-
208
- await Promise.all([
209
- new Promise((resolve) => setTimeout(resolve, delay)), //
210
- executeDataPipeline(false) // Handle full resolution pipeline passing down false to avoid flashing double mutations
211
- ]);
212
-
213
- const finishedState = selector.get() ?? sentinel;
214
- await store.set({
215
- [CACHE_KEY]: { ...finishedState, loading: false } //
216
- });
217
- },
218
-
219
- subscribe: (listener) => {
220
- return selector.subscribe((state) => { //
221
- listener(state); //
222
- });
223
- },
224
-
225
- invalidate: async () => {
226
- await store.set({ [CACHE_KEY]: DELETE_SYMBOL }); //
227
- pristineData = [];
228
- },
229
- };
230
- }
package/vitest.config.ts DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from "vitest/config"
2
-
3
- export default defineConfig({
4
- test: {
5
- globalSetup: ["./test-setup.ts"],
6
- fileParallelism: false,
7
- },
8
- })