@coasys/hooks-helpers 0.8.2-prerelease
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/package.json +24 -0
- package/src/cache.ts +70 -0
- package/src/factory/SubjectRepository.ts +257 -0
- package/src/factory/index.ts +1 -0
- package/src/factory/model.ts +57 -0
- package/src/getProfile.ts +26 -0
- package/src/index.ts +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coasys/hooks-helpers",
|
|
3
|
+
"version": "0.8.2-prerelease",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"module": "./src/index.ts",
|
|
7
|
+
"private": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@coasys/ad4m": "*",
|
|
21
|
+
"@coasys/ad4m-connect": "*",
|
|
22
|
+
"uuid": "*"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PerspectiveProxy } from "@coasys/ad4m";
|
|
2
|
+
|
|
3
|
+
const cache: Map<string, any> = new Map();
|
|
4
|
+
const subscribers: Map<string, Function[]> = new Map();
|
|
5
|
+
|
|
6
|
+
export function getCache<T>(key: string) {
|
|
7
|
+
const match: T | undefined = cache.get(key);
|
|
8
|
+
return match;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setCache<T>(key: string, value: T) {
|
|
12
|
+
cache.set(key, value);
|
|
13
|
+
getSubscribers(key).forEach((cb) => cb());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function subscribe(key: string, callback: Function) {
|
|
17
|
+
getSubscribers(key).push(callback);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unsubscribe(key: string, callback: Function) {
|
|
21
|
+
const subs = getSubscribers(key);
|
|
22
|
+
const index = subs.indexOf(callback);
|
|
23
|
+
if (index >= 0) {
|
|
24
|
+
subs.splice(index, 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getSubscribers(key: string) {
|
|
29
|
+
if (!subscribers.has(key)) subscribers.set(key, []);
|
|
30
|
+
return subscribers.get(key)!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function subscribeToPerspective(
|
|
34
|
+
perspective: PerspectiveProxy,
|
|
35
|
+
added: Function,
|
|
36
|
+
removed: Function
|
|
37
|
+
) {
|
|
38
|
+
const addedKey = `perspective-${perspective.uuid}-added`;
|
|
39
|
+
const removedKey = `perspective-${perspective.uuid}-removed`;
|
|
40
|
+
|
|
41
|
+
if (!subscribers.has(addedKey)) {
|
|
42
|
+
console.log("subscribing!");
|
|
43
|
+
perspective.addListener("link-added", (link) => {
|
|
44
|
+
subscribers.get(addedKey).forEach((cb) => cb(link));
|
|
45
|
+
return null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!subscribers.has(removedKey)) {
|
|
50
|
+
perspective.addListener("link-removed", (link) => {
|
|
51
|
+
subscribers.get(removedKey).forEach((cb) => cb(link));
|
|
52
|
+
return null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
subscribe(addedKey, added);
|
|
57
|
+
subscribe(removedKey, removed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function unsubscribeToPerspective(
|
|
61
|
+
perspective: PerspectiveProxy,
|
|
62
|
+
added: Function,
|
|
63
|
+
removed: Function
|
|
64
|
+
) {
|
|
65
|
+
const addedKey = `perspective-${perspective.uuid}-added`;
|
|
66
|
+
const removedKey = `perspective-${perspective.uuid}-removed`;
|
|
67
|
+
|
|
68
|
+
unsubscribe(addedKey, added);
|
|
69
|
+
unsubscribe(removedKey, removed);
|
|
70
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PerspectiveProxy,
|
|
3
|
+
Link,
|
|
4
|
+
Subject,
|
|
5
|
+
Literal,
|
|
6
|
+
LinkQuery,
|
|
7
|
+
} from "@coasys/ad4m";
|
|
8
|
+
import { setProperties } from "./model";
|
|
9
|
+
import { v4 as uuidv4 } from "uuid";
|
|
10
|
+
|
|
11
|
+
export const SELF = "ad4m://self";
|
|
12
|
+
|
|
13
|
+
export type ModelProps = {
|
|
14
|
+
perspective: PerspectiveProxy;
|
|
15
|
+
source?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class SubjectRepository<SubjectClass extends { [x: string]: any }> {
|
|
19
|
+
source = SELF;
|
|
20
|
+
subject: SubjectClass | string;
|
|
21
|
+
perspective: PerspectiveProxy;
|
|
22
|
+
tempSubject: any | string;
|
|
23
|
+
|
|
24
|
+
constructor(subject: { new (): SubjectClass } | string, props: ModelProps) {
|
|
25
|
+
this.perspective = props.perspective;
|
|
26
|
+
this.source = props.source || this.source;
|
|
27
|
+
this.subject = typeof subject === "string" ? subject : new subject();
|
|
28
|
+
this.tempSubject = subject;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get className(): string {
|
|
32
|
+
return typeof this.subject === "string"
|
|
33
|
+
? this.subject
|
|
34
|
+
: this.subject.className;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async ensureSubject() {
|
|
38
|
+
if (typeof this.tempSubject === "string") return;
|
|
39
|
+
await this.perspective.ensureSDNASubjectClass(this.tempSubject);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async create(
|
|
43
|
+
data: SubjectClass,
|
|
44
|
+
id?: string,
|
|
45
|
+
source?: string
|
|
46
|
+
): Promise<SubjectClass> {
|
|
47
|
+
await this.ensureSubject();
|
|
48
|
+
const base = id || Literal.from(uuidv4()).toUrl();
|
|
49
|
+
|
|
50
|
+
let newInstance = await this.perspective.createSubject(this.subject, base);
|
|
51
|
+
|
|
52
|
+
if (!newInstance) {
|
|
53
|
+
throw "Failed to create new instance of " + this.subject;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Connect new instance to source
|
|
57
|
+
await this.perspective.add(
|
|
58
|
+
new Link({
|
|
59
|
+
source: source || this.source,
|
|
60
|
+
predicate: "ad4m://has_child",
|
|
61
|
+
target: base,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
Object.keys(data).forEach((key) =>
|
|
66
|
+
data[key] === undefined || data[key] === null ? delete data[key] : {}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
setProperties(newInstance, data);
|
|
70
|
+
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
return this.getSubjectData(newInstance);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async update(id: string, data: QueryPartialEntity<SubjectClass>) {
|
|
76
|
+
await this.ensureSubject();
|
|
77
|
+
|
|
78
|
+
const instance = await this.get(id);
|
|
79
|
+
|
|
80
|
+
if (!instance) {
|
|
81
|
+
throw "Failed to find instance of " + this.subject + " with id " + id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Object.keys(data).forEach((key) =>
|
|
85
|
+
data[key] === undefined ? delete data[key] : {}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
setProperties(instance, data);
|
|
90
|
+
|
|
91
|
+
return this.getSubjectData(instance);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async remove(id: string) {
|
|
95
|
+
if (this.perspective) {
|
|
96
|
+
const linksTo = await this.perspective.get(new LinkQuery({ target: id }));
|
|
97
|
+
const linksFrom = await this.perspective.get(
|
|
98
|
+
new LinkQuery({ source: id })
|
|
99
|
+
);
|
|
100
|
+
this.perspective.removeLinks([...linksFrom, ...linksTo]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async get(id: string): Promise<SubjectClass | null> {
|
|
105
|
+
await this.ensureSubject();
|
|
106
|
+
if (id) {
|
|
107
|
+
const subjectProxy = await this.perspective.getSubjectProxy(
|
|
108
|
+
id,
|
|
109
|
+
this.subject
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
return subjectProxy || null;
|
|
114
|
+
} else {
|
|
115
|
+
const all = await this.getAll();
|
|
116
|
+
return all[0] || null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getData(id: string): Promise<SubjectClass | string | null> {
|
|
121
|
+
await this.ensureSubject();
|
|
122
|
+
const entry = await this.get(id);
|
|
123
|
+
if (entry) {
|
|
124
|
+
// @ts-ignore
|
|
125
|
+
return await this.getSubjectData(entry);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async getSubjectData(entry: any) {
|
|
132
|
+
let links = await this.perspective.get(
|
|
133
|
+
new LinkQuery({ source: entry.baseExpression })
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const getters = Object.entries(Object.getOwnPropertyDescriptors(entry))
|
|
137
|
+
.filter(([key, descriptor]) => typeof descriptor.get === "function")
|
|
138
|
+
.map(([key]) => key);
|
|
139
|
+
|
|
140
|
+
const promises = getters.map((getter) => entry[getter]);
|
|
141
|
+
return Promise.all(promises).then((values) => {
|
|
142
|
+
return getters.reduce((acc, getter, index) => {
|
|
143
|
+
let value = values[index];
|
|
144
|
+
if (this.tempSubject.prototype?.__properties[getter]?.transform) {
|
|
145
|
+
value =
|
|
146
|
+
this.tempSubject.prototype.__properties[getter].transform(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...acc,
|
|
151
|
+
id: entry.baseExpression,
|
|
152
|
+
timestamp: links[0].timestamp,
|
|
153
|
+
author: links[0].author,
|
|
154
|
+
[getter]: value,
|
|
155
|
+
};
|
|
156
|
+
}, {});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getAll(source?: string, query?: QueryOptions): Promise<SubjectClass[]> {
|
|
161
|
+
await this.ensureSubject();
|
|
162
|
+
|
|
163
|
+
const tempSource = source || this.source;
|
|
164
|
+
|
|
165
|
+
let res = [];
|
|
166
|
+
|
|
167
|
+
if (query) {
|
|
168
|
+
try {
|
|
169
|
+
const queryResponse = (
|
|
170
|
+
await this.perspective.infer(
|
|
171
|
+
`findall([Timestamp, Base], (subject_class("${this.className}", C), instance(C, Base), link("${tempSource}", Predicate, Base, Timestamp, Author)), AllData), length(AllData, DataLength), sort(AllData, SortedData).`
|
|
172
|
+
)
|
|
173
|
+
)[0];
|
|
174
|
+
|
|
175
|
+
if (queryResponse.SortedData >= query.size) {
|
|
176
|
+
const isOutofBound =
|
|
177
|
+
query.size * query.page > queryResponse.DataLength;
|
|
178
|
+
|
|
179
|
+
const newPageSize = isOutofBound
|
|
180
|
+
? queryResponse.DataLength - query.size * (query.page - 1)
|
|
181
|
+
: query.size;
|
|
182
|
+
|
|
183
|
+
const mainQuery = `findall([Timestamp, Base], (subject_class("${this.className}", C), instance(C, Base), link("${tempSource}", Predicate, Base, Timestamp, Author)), AllData), sort(AllData, SortedData), reverse(SortedData, ReverseSortedData), paginate(ReverseSortedData, ${query.page}, ${newPageSize}, PageData).`;
|
|
184
|
+
res = await this.perspective.infer(mainQuery);
|
|
185
|
+
|
|
186
|
+
res = res[0].PageData.map((r) => ({
|
|
187
|
+
Base: r[1],
|
|
188
|
+
Timestamp: r[0],
|
|
189
|
+
}));
|
|
190
|
+
} else {
|
|
191
|
+
res = await this.perspective.infer(
|
|
192
|
+
`subject_class("${this.className}", C), instance(C, Base), triple("${tempSource}", Predicate, Base).`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.log("Query failed", e);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
res = await this.perspective.infer(
|
|
200
|
+
`subject_class("${this.className}", C), instance(C, Base), triple("${tempSource}", Predicate, Base).`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const results =
|
|
205
|
+
res &&
|
|
206
|
+
res.filter(
|
|
207
|
+
(obj, index, self) =>
|
|
208
|
+
index === self.findIndex((t) => t.Base === obj.Base)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!res) return [];
|
|
212
|
+
|
|
213
|
+
const data = await Promise.all(
|
|
214
|
+
results.map(async (result) => {
|
|
215
|
+
let subject = new Subject(
|
|
216
|
+
this.perspective!,
|
|
217
|
+
result.Base,
|
|
218
|
+
this.className
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await subject.init();
|
|
222
|
+
|
|
223
|
+
return subject;
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// @ts-ignore
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async getAllData(
|
|
232
|
+
source?: string,
|
|
233
|
+
query?: QueryOptions
|
|
234
|
+
): Promise<SubjectClass[]> {
|
|
235
|
+
await this.ensureSubject();
|
|
236
|
+
|
|
237
|
+
const subjects = await this.getAll(source, query);
|
|
238
|
+
|
|
239
|
+
const entries = await Promise.all(
|
|
240
|
+
subjects.map((e) => this.getSubjectData(e))
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// @ts-ignore
|
|
244
|
+
return entries;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type QueryPartialEntity<T> = {
|
|
249
|
+
[P in keyof T]?: T[P] | (() => string);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type QueryOptions = {
|
|
253
|
+
page: number;
|
|
254
|
+
size: number;
|
|
255
|
+
infinite: boolean;
|
|
256
|
+
uniqueKey: string;
|
|
257
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./SubjectRepository";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type Target = String;
|
|
2
|
+
|
|
3
|
+
export type PropertyValueMap = {
|
|
4
|
+
[property: string]: Target | Target[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function capitalize(str: string) {
|
|
8
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// e.g. "name" -> "setName"
|
|
12
|
+
export function propertyNameToSetterName(property: string): string {
|
|
13
|
+
return `set${capitalize(property)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pluralToSingular(plural: string): string {
|
|
17
|
+
if (plural.endsWith("ies")) {
|
|
18
|
+
return plural.slice(0, -3) + "y";
|
|
19
|
+
} else if (plural.endsWith("s")) {
|
|
20
|
+
return plural.slice(0, -1);
|
|
21
|
+
} else {
|
|
22
|
+
return plural;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// e.g. "comments" -> "addComment"
|
|
27
|
+
export function collectionToAdderName(collection: string): string {
|
|
28
|
+
return `add${capitalize(collection)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function collectionToSetterName(collection: string): string {
|
|
32
|
+
return `setCollection${capitalize(collection)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setProperties(subject: any, properties: PropertyValueMap) {
|
|
36
|
+
Object.keys(properties).forEach((key) => {
|
|
37
|
+
if (Array.isArray(properties[key])) {
|
|
38
|
+
// it's a collection
|
|
39
|
+
const adderName = collectionToAdderName(key);
|
|
40
|
+
const adderFunction = subject[adderName];
|
|
41
|
+
if (adderFunction) {
|
|
42
|
+
adderFunction(properties[key]);
|
|
43
|
+
} else {
|
|
44
|
+
throw "No adder function found for collection: " + key;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// it's a property
|
|
48
|
+
const setterName = propertyNameToSetterName(key);
|
|
49
|
+
const setterFunction = subject[setterName];
|
|
50
|
+
if (setterFunction) {
|
|
51
|
+
setterFunction(properties[key]);
|
|
52
|
+
} else {
|
|
53
|
+
throw "No setter function found for property: " + key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Ad4mClient } from "@coasys/ad4m";
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { getAd4mClient } from "@coasys/ad4m-connect/utils";
|
|
4
|
+
import { LinkExpression } from "@coasys/ad4m";
|
|
5
|
+
|
|
6
|
+
export interface Payload {
|
|
7
|
+
url: string;
|
|
8
|
+
perspectiveUuid: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getProfile<T>(did: string, formatter?: (links: LinkExpression[]) => T): Promise<T | any> {
|
|
12
|
+
const cleanedDid = did.replace("did://", "");
|
|
13
|
+
const client: Ad4mClient = await getAd4mClient();
|
|
14
|
+
|
|
15
|
+
const agentPerspective = await client.agent.byDID(cleanedDid);
|
|
16
|
+
|
|
17
|
+
if (agentPerspective) {
|
|
18
|
+
const links = agentPerspective!.perspective!.links;
|
|
19
|
+
|
|
20
|
+
if (formatter) {
|
|
21
|
+
return formatter(links);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return agentPerspective
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED