@manta-eu/client 0.3.0 → 0.4.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.
@@ -1,6 +1,9 @@
1
- import jsonld from "jsonld";
2
- import { encodeEntity, type Lens, type Schema } from "ldkit";
3
- import { xsd } from "ldkit/namespaces";
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { owlNothingIri } from "@manta-eu/rdf-lib/namespaces";
3
+ import { encodeEntity, type Identity, type Lens, type Property, type Schema, type SchemaInterface } from "ldkit";
4
+ import { rdf, xsd } from "ldkit/namespaces";
5
+ import { groupBy, uniq, uniqBy } from "ramda";
6
+ import type { GQLExecuteActionUpdateInput } from "../generated/graphql";
4
7
 
5
8
  export type DocumentTerm = { value: string; termType: string };
6
9
 
@@ -10,47 +13,97 @@ export type DocumentQuad = {
10
13
  object: DocumentTerm & { datatype?: { value: string }; language?: string };
11
14
  };
12
15
 
13
- export type InsertEntity<S extends Schema> = Parameters<Lens<S>["insert"]>[number];
16
+ type InsertEntity<S extends Schema> = Parameters<Lens<S>["insert"]>[number];
14
17
 
15
- // jsonld keeps the `_:` prefix in a blank node value
16
- function jsonldTerm(term: DocumentTerm): DocumentTerm {
17
- return { termType: term.termType, value: term.termType === "BlankNode" ? `_:${term.value}` : term.value };
18
+ export type ActionSchema = Schema & { "@type": string };
19
+
20
+ export type Writable<T> = T extends readonly (infer E)[]
21
+ ? Writable<E>[]
22
+ : T extends Record<string, unknown>
23
+ ? { [K in keyof T]?: Writable<T[K]> }
24
+ : T;
25
+
26
+ export type ActionEntity<S extends Schema> = Identity &
27
+ (string extends keyof S ? Record<string, unknown> : Writable<Omit<SchemaInterface<S>, "$id">>);
28
+
29
+ export type ActionInput = GQLExecuteActionUpdateInput;
30
+
31
+ const termKey = (term: DocumentTerm): string => (term.termType === "BlankNode" ? `_:${term.value}` : term.value);
32
+
33
+ const objectKey = ({ object }: DocumentQuad): string =>
34
+ `${termKey(object)} ${object.datatype?.value ?? ""} ${object.language ?? ""}`;
35
+
36
+ const oneOrMany = (values: readonly unknown[]): unknown => (values.length === 1 ? values[0] : values);
37
+
38
+ function literal(object: DocumentQuad["object"]): unknown {
39
+ const datatype = object.datatype?.value ?? xsd.string;
40
+
41
+ if (isPresent(object.language)) return { "@value": object.value, "@language": object.language };
42
+ if (datatype === xsd.string) return object.value;
43
+ return { "@value": object.value, "@type": datatype };
18
44
  }
19
45
 
20
- function jsonldQuad(quad: DocumentQuad) {
21
- const { subject, predicate, object } = quad;
22
-
23
- return {
24
- subject: jsonldTerm(subject),
25
- predicate: { termType: "NamedNode", value: predicate.value },
26
- object:
27
- object.termType === "Literal"
28
- ? {
29
- termType: "Literal",
30
- value: object.value,
31
- language: object.language,
32
- datatype: { value: object.datatype?.value ?? xsd.string },
33
- }
34
- : jsonldTerm(object),
35
- graph: { termType: "DefaultGraph", value: "" },
46
+ export function documentFromQuads(quads: readonly DocumentQuad[], subjectIri: string): Record<string, unknown> {
47
+ const described = quads.filter(({ subject }) => subject.value === subjectIri || subject.termType === "BlankNode");
48
+ const bySubject = groupBy((quad: DocumentQuad) => termKey(quad.subject), described);
49
+ const blankObjects = described.flatMap(({ object }) => (object.termType === "BlankNode" ? [termKey(object)] : []));
50
+ const shared = new Set(blankObjects.filter((key, index) => blankObjects.indexOf(key) !== index));
51
+ const labels = new Map<string, string>();
52
+
53
+ const label = (key: string): string => {
54
+ if (!labels.has(key)) labels.set(key, `_:b${labels.size}`);
55
+ return labels.get(key) ?? key;
36
56
  };
57
+
58
+ const value = (object: DocumentQuad["object"], path: readonly string[]): unknown => {
59
+ const key = termKey(object);
60
+
61
+ if (object.termType === "Literal") return literal(object);
62
+ if (object.termType !== "BlankNode") return { "@id": key };
63
+ if (path.includes(key)) return { "@id": label(key) };
64
+ return node(key, [...path, key]);
65
+ };
66
+
67
+ const node = (key: string, path: readonly string[]): Record<string, unknown> => {
68
+ const own = bySubject[key] ?? [];
69
+ const types = own.filter(({ predicate }) => predicate.value === rdf.type).map(({ object }) => termKey(object));
70
+ const others = own.filter(({ predicate }) => predicate.value !== rdf.type);
71
+ const byPredicate = groupBy((quad: DocumentQuad) => quad.predicate.value, others);
72
+
73
+ return {
74
+ ...(shared.has(key) && { "@id": label(key) }),
75
+ ...(isPresent(types) && { "@type": oneOrMany(uniq(types)) }),
76
+ ...Object.fromEntries(
77
+ Object.entries(byPredicate).map(([predicate, values = []]) => [
78
+ predicate,
79
+ oneOrMany(uniqBy(objectKey, values).map(({ object }) => value(object, path))),
80
+ ]),
81
+ ),
82
+ };
83
+ };
84
+
85
+ return { "@id": subjectIri, ...node(subjectIri, [subjectIri]) };
37
86
  }
38
87
 
39
- export async function documentFromQuads(
40
- quads: readonly DocumentQuad[],
41
- subjectIri: string,
42
- ): Promise<Record<string, unknown>> {
43
- const described = quads.filter(({ subject }) => subject.value === subjectIri || subject.termType === "BlankNode");
44
- const flattened = await jsonld.fromRDF(described.map(jsonldQuad));
88
+ export function entityActionDocument<S extends Schema>(schema: S, entity: ActionEntity<S>): Record<string, unknown> {
89
+ const quads = encodeEntity(schema, entity as unknown as InsertEntity<S>).filter(
90
+ (quad) => quad.object.termType !== "Variable",
91
+ );
45
92
 
46
- return jsonld.frame(flattened, { "@id": subjectIri, "@embed": "@always" });
93
+ return documentFromQuads(quads, entity.$id);
47
94
  }
48
95
 
49
- export function entityActionDocument<S extends Schema>(
96
+ const propertyIri = (definition: Property | string | readonly string[] | undefined): string =>
97
+ typeof definition === "object" && "@id" in definition ? definition["@id"] : String(definition);
98
+
99
+ export function entityActionInput<S extends ActionSchema>(
50
100
  schema: S,
51
- entity: InsertEntity<S>,
52
- ): Promise<Record<string, unknown>> {
53
- const quads = encodeEntity(schema, entity).filter((quad) => quad.object.termType !== "Variable");
101
+ actionShape: string,
102
+ entity: ActionEntity<S>,
103
+ cleared: readonly (Exclude<keyof S, "@type"> & string)[] = [],
104
+ ): ActionInput {
105
+ const document = entityActionDocument(schema, entity);
106
+ const nothing = Object.fromEntries(cleared.map((key) => [propertyIri(schema[key]), { "@id": owlNothingIri }]));
54
107
 
55
- return documentFromQuads(quads, entity.$id);
108
+ return { subjectIri: entity.$id, rdfType: schema["@type"], actionShape, properties: { ...document, ...nothing } };
56
109
  }
@@ -1,6 +1,9 @@
1
- import { nco, nmo } from "@manta-eu/ldo/namespaces";
1
+ import { NcoEmailAddressSchema } from "@manta-eu/ldo/schemas/nco";
2
+ import { NmoEmailSchema } from "@manta-eu/ldo/schemas/nmo";
2
3
  import { isPresent } from "@manta-eu/lib/present";
4
+ import type { Schema } from "ldkit";
3
5
  import { ExecuteAgentActionAdditionDocument, type GQLExecuteAgentActionAdditionMutation } from "../generated/graphql";
6
+ import { entityActionInput } from "./action-document";
4
7
 
5
8
  export type EmailGraphql = (document: string, variables: Record<string, unknown>) => Promise<unknown>;
6
9
 
@@ -16,22 +19,25 @@ export type RenderedEmail = {
16
19
 
17
20
  export type EmailStart = { ok: true; plan: string } | { ok: false; error: string };
18
21
 
19
- function renderedEmailAddition(terms: RealmEmailTerms, email: RenderedEmail) {
20
- return {
21
- subjectIri: email.subjectIri,
22
- rdfType: nmo.Email,
23
- actionShape: terms.emailMessageShape,
24
- properties: {
25
- "@id": email.subjectIri,
26
- "@type": nmo.Email,
27
- [nmo.messageFrom]: { "@type": nco.EmailAddress, [nco.emailAddress]: email.from },
28
- [nmo.emailTo]: { "@type": nco.EmailAddress, [nco.emailAddress]: email.to },
29
- [nmo.messageSubject]: email.subject,
30
- [nmo.contentMimeType]: "text/html",
31
- [terms.renderedFrom]: { "@id": email.renderedFrom },
32
- },
33
- };
34
- }
22
+ const renderedEmailSchema = (terms: RealmEmailTerms) =>
23
+ ({
24
+ "@type": NmoEmailSchema["@type"],
25
+ from: { "@id": NmoEmailSchema.messageFrom["@id"], "@schema": NcoEmailAddressSchema },
26
+ to: { "@id": NmoEmailSchema.emailTo["@id"], "@schema": NcoEmailAddressSchema },
27
+ subject: NmoEmailSchema.messageSubject,
28
+ contentMimeType: NmoEmailSchema.contentMimeType,
29
+ renderedFrom: { ...NmoEmailSchema.renderedFrom, "@id": terms.renderedFrom },
30
+ }) as const satisfies Schema;
31
+
32
+ const renderedEmailAddition = (terms: RealmEmailTerms, email: RenderedEmail) =>
33
+ entityActionInput(renderedEmailSchema(terms), terms.emailMessageShape, {
34
+ $id: email.subjectIri,
35
+ from: { emailAddress: email.from },
36
+ to: { emailAddress: email.to },
37
+ subject: email.subject,
38
+ contentMimeType: "text/html",
39
+ renderedFrom: email.renderedFrom,
40
+ });
35
41
 
36
42
  export async function startRenderedEmail(
37
43
  graphql: EmailGraphql,