@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.
- package/LICENSE.md +22 -0
- package/index.cjs +8 -0
- package/index.d.cts +986 -0
- package/index.d.mts +986 -0
- package/index.mjs +8 -0
- package/package.json +25 -22
- package/auth/store.ts +0 -75
- package/auth/types.ts +0 -38
- package/blobs/store.ts +0 -209
- package/blobs/types.ts +0 -28
- package/collections/store.ts +0 -88
- package/collections/types.ts +0 -11
- package/container.ts +0 -93
- package/core/client.ts +0 -408
- package/core/collection.ts +0 -145
- package/core/errors.ts +0 -55
- package/core/pager.ts +0 -162
- package/core/types.ts +0 -279
- package/index.ts +0 -39
- package/system/api-keys/store.ts +0 -107
- package/system/api-keys/types.ts +0 -31
- package/system/capabilities/store.ts +0 -67
- package/system/capabilities/types.ts +0 -1
- package/system/identity/store.ts +0 -113
- package/system/identity/types.ts +0 -31
- package/system/policies/store.ts +0 -177
- package/system/policies/types.ts +0 -43
- package/test-setup.ts +0 -53
- package/utils/index.ts +0 -1
- package/utils/pager.ts +0 -230
- package/vitest.config.ts +0 -8
package/system/identity/store.ts
DELETED
|
@@ -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
|
-
}
|
package/system/identity/types.ts
DELETED
|
@@ -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
|
-
}
|
package/system/policies/store.ts
DELETED
|
@@ -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
|
-
}
|
package/system/policies/types.ts
DELETED
|
@@ -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
|
-
}
|