@manta-eu/client 0.12.0 → 0.13.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.
Files changed (75) hide show
  1. package/README.md +44 -0
  2. package/dist/generated/graphql.d.ts +2119 -24
  3. package/dist/generated/graphql.d.ts.map +1 -1
  4. package/dist/generated/graphql.js +470 -12
  5. package/dist/generated/graphql.js.map +1 -1
  6. package/dist/generated/operations.d.ts +2 -0
  7. package/dist/generated/operations.d.ts.map +1 -0
  8. package/dist/generated/operations.js +165 -0
  9. package/dist/generated/operations.js.map +1 -0
  10. package/dist/graphql-clients.d.ts +2 -1
  11. package/dist/graphql-clients.d.ts.map +1 -1
  12. package/dist/graphql-clients.js +2 -1
  13. package/dist/graphql-clients.js.map +1 -1
  14. package/dist/shared/agent-types.d.ts +15 -0
  15. package/dist/shared/agent-types.d.ts.map +1 -1
  16. package/dist/shared/chat-manager.d.ts +1 -1
  17. package/dist/shared/chat-manager.d.ts.map +1 -1
  18. package/dist/shared/chat-manager.js +1 -1
  19. package/dist/shared/chat-manager.js.map +1 -1
  20. package/dist/shared/constants.d.ts +6 -0
  21. package/dist/shared/constants.d.ts.map +1 -1
  22. package/dist/shared/constants.js +6 -0
  23. package/dist/shared/constants.js.map +1 -1
  24. package/dist/shared/datastore-path.d.ts +3 -0
  25. package/dist/shared/datastore-path.d.ts.map +1 -1
  26. package/dist/shared/datastore-path.js +3 -0
  27. package/dist/shared/datastore-path.js.map +1 -1
  28. package/dist/shared/login-status.d.ts +6 -0
  29. package/dist/shared/login-status.d.ts.map +1 -0
  30. package/dist/shared/login-status.js +2 -0
  31. package/dist/shared/login-status.js.map +1 -0
  32. package/dist/shared/memory-schema-limits.d.ts +17 -0
  33. package/dist/shared/memory-schema-limits.d.ts.map +1 -0
  34. package/dist/shared/memory-schema-limits.js +17 -0
  35. package/dist/shared/memory-schema-limits.js.map +1 -0
  36. package/dist/shared/memory-schema-pattern.d.ts +3 -0
  37. package/dist/shared/memory-schema-pattern.d.ts.map +1 -0
  38. package/dist/shared/memory-schema-pattern.js +16 -0
  39. package/dist/shared/memory-schema-pattern.js.map +1 -0
  40. package/dist/shared/memory-schema-property.d.ts +64 -0
  41. package/dist/shared/memory-schema-property.d.ts.map +1 -0
  42. package/dist/shared/memory-schema-property.js +195 -0
  43. package/dist/shared/memory-schema-property.js.map +1 -0
  44. package/dist/shared/memory-schema-values.d.ts +20 -0
  45. package/dist/shared/memory-schema-values.d.ts.map +1 -0
  46. package/dist/shared/memory-schema-values.js +139 -0
  47. package/dist/shared/memory-schema-values.js.map +1 -0
  48. package/dist/shared/operation-root-fields.d.ts +7 -0
  49. package/dist/shared/operation-root-fields.d.ts.map +1 -0
  50. package/dist/shared/operation-root-fields.js +33 -0
  51. package/dist/shared/operation-root-fields.js.map +1 -0
  52. package/dist/shared/rdf-namespaces.d.ts +1 -0
  53. package/dist/shared/rdf-namespaces.d.ts.map +1 -1
  54. package/dist/shared/rdf-namespaces.js +2 -0
  55. package/dist/shared/rdf-namespaces.js.map +1 -1
  56. package/dist/testing/manta-mock.d.ts +25 -0
  57. package/dist/testing/manta-mock.d.ts.map +1 -0
  58. package/dist/testing/manta-mock.js +165 -0
  59. package/dist/testing/manta-mock.js.map +1 -0
  60. package/package.json +32 -6
  61. package/src/generated/graphql.ts +2729 -55
  62. package/src/generated/operations.ts +164 -0
  63. package/src/graphql-clients.ts +3 -2
  64. package/src/shared/agent-types.ts +16 -0
  65. package/src/shared/chat-manager.ts +1 -1
  66. package/src/shared/constants.ts +11 -0
  67. package/src/shared/datastore-path.ts +6 -0
  68. package/src/shared/login-status.ts +1 -0
  69. package/src/shared/memory-schema-limits.ts +16 -0
  70. package/src/shared/memory-schema-pattern.ts +15 -0
  71. package/src/shared/memory-schema-property.ts +280 -0
  72. package/src/shared/memory-schema-values.ts +174 -0
  73. package/src/shared/operation-root-fields.ts +56 -0
  74. package/src/shared/rdf-namespaces.ts +3 -0
  75. package/src/testing/manta-mock.ts +208 -0
@@ -0,0 +1,174 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { match, P } from "ts-pattern";
3
+ import { isValidFullIri } from "./rdf-namespaces";
4
+
5
+ export const memorySchemaValueTypes = [
6
+ "text",
7
+ "longText",
8
+ "integer",
9
+ "decimal",
10
+ "boolean",
11
+ "date",
12
+ "dateTime",
13
+ "url",
14
+ "entity",
15
+ ] as const;
16
+
17
+ export type MemorySchemaValueType = (typeof memorySchemaValueTypes)[number];
18
+
19
+ export type MemoryValueConstraints = {
20
+ valueType: MemorySchemaValueType;
21
+ allowedValues: readonly string[];
22
+ minValue: string | null;
23
+ maxValue: string | null;
24
+ maxLength: number | null;
25
+ pattern: string | null;
26
+ };
27
+
28
+ const integerPattern = /^[+-]?\d+$/;
29
+ const decimalPattern = /^[+-]?(\d+(\.\d*)?|\.\d+)$/;
30
+ const datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
31
+ const dateTimePattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2})(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/;
32
+
33
+ function isCalendarDate(year: string, month: string, day: string): boolean {
34
+ const date = new Date(0);
35
+ date.setUTCFullYear(Number(year), Number(month) - 1, Number(day));
36
+ return (
37
+ date.getUTCFullYear() === Number(year) &&
38
+ date.getUTCMonth() === Number(month) - 1 &&
39
+ date.getUTCDate() === Number(day)
40
+ );
41
+ }
42
+
43
+ function canonicalInteger(value: string): string {
44
+ return BigInt(value).toString();
45
+ }
46
+
47
+ function canonicalDecimal(value: string): string {
48
+ const negative = value.startsWith("-");
49
+ const unsigned = value.replace(/^[+-]/, "");
50
+ const [whole = "", fraction = ""] = unsigned.split(".");
51
+ const wholeDigits = whole.replace(/^0+(?=\d)/, "") || "0";
52
+ const fractionDigits = fraction.replace(/0+$/, "");
53
+ const magnitude = isPresent(fractionDigits) ? `${wholeDigits}.${fractionDigits}` : wholeDigits;
54
+ return negative && magnitude !== "0" ? `-${magnitude}` : magnitude;
55
+ }
56
+
57
+ export function parseMemoryValue(valueType: MemorySchemaValueType, raw: string): string | null {
58
+ const value = raw.trim();
59
+ if (!isPresent(value)) return null;
60
+
61
+ return match(valueType)
62
+ .with("text", "longText", () => value)
63
+ .with("integer", () => (integerPattern.test(value) ? canonicalInteger(value) : null))
64
+ .with("decimal", () => (decimalPattern.test(value) ? canonicalDecimal(value) : null))
65
+ .with("boolean", () => {
66
+ const lowered = value.toLowerCase();
67
+ return lowered === "true" || lowered === "false" ? lowered : null;
68
+ })
69
+ .with("date", () => {
70
+ const parts = datePattern.exec(value);
71
+ return parts !== null && isCalendarDate(parts[1]!, parts[2]!, parts[3]!) ? value : null;
72
+ })
73
+ .with("dateTime", () => {
74
+ const parts = dateTimePattern.exec(value);
75
+ if (parts === null || !isCalendarDate(parts[1]!, parts[2]!, parts[3]!)) return null;
76
+ if (Number(parts[4]) >= 24 || Number(parts[5]) >= 60 || Number(parts[7] ?? 0) >= 60) return null;
77
+ return parts[6] === undefined ? withSeconds(value, parts[9]) : value;
78
+ })
79
+ .with("url", () => (isAbsoluteHttpUrl(value) ? value : null))
80
+ .with("entity", () => (isValidFullIri(value) ? value : null))
81
+ .exhaustive();
82
+ }
83
+
84
+ // xsd:dateTime requires seconds, and a datetime-local input leaves them out.
85
+ function withSeconds(value: string, zone: string | undefined): string {
86
+ const local = zone === undefined ? value : value.slice(0, -zone.length);
87
+ return `${local}:00${zone ?? ""}`;
88
+ }
89
+
90
+ function isAbsoluteHttpUrl(value: string): boolean {
91
+ try {
92
+ const url = new URL(value);
93
+ return url.protocol === "http:" || url.protocol === "https:";
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export function convertMemoryValue(
100
+ from: MemorySchemaValueType,
101
+ to: MemorySchemaValueType,
102
+ lexical: string,
103
+ ): string | null {
104
+ return match([from, to])
105
+ .with(["decimal", "integer"], () => {
106
+ const integral = /^([+-]?\d+)(\.0*)?$/.exec(lexical.trim());
107
+ return integral !== null ? canonicalInteger(integral[1]!) : null;
108
+ })
109
+ .with(["date", "dateTime"], () => {
110
+ const date = parseMemoryValue("date", lexical);
111
+ return date !== null ? `${date}T00:00:00` : null;
112
+ })
113
+ .with(["dateTime", "date"], () => {
114
+ const dateTime = parseMemoryValue("dateTime", lexical);
115
+ return dateTime !== null ? dateTime.slice(0, 10) : null;
116
+ })
117
+ .otherwise(() => parseMemoryValue(to, lexical));
118
+ }
119
+
120
+ function scaledDecimal(value: string, scale: number): bigint {
121
+ const [whole = "", fraction = ""] = value.replace(/^[+-]/, "").split(".");
122
+ const magnitude = BigInt(`${whole || "0"}${fraction.padEnd(scale, "0")}`);
123
+ return value.startsWith("-") ? -magnitude : magnitude;
124
+ }
125
+
126
+ // Exact on any number of digits, where a double would round a large integer or a long decimal.
127
+ function compareNumbers(a: string, b: string): number {
128
+ const [left, right] = [a.trim(), b.trim()];
129
+ if (!decimalPattern.test(left) || !decimalPattern.test(right)) return Number(left) - Number(right);
130
+ const scale = Math.max(left.split(".")[1]?.length ?? 0, right.split(".")[1]?.length ?? 0);
131
+ const difference = scaledDecimal(left, scale) - scaledDecimal(right, scale);
132
+ return difference === 0n ? 0 : difference < 0n ? -1 : 1;
133
+ }
134
+
135
+ function compareDateTimes(a: string, b: string): number {
136
+ return Date.parse(a) - Date.parse(b);
137
+ }
138
+
139
+ export function compareMemoryValues(valueType: MemorySchemaValueType, a: string, b: string): number {
140
+ return match(valueType)
141
+ .with(P.union("integer", "decimal"), () => compareNumbers(a, b))
142
+ .with("dateTime", () => compareDateTimes(a, b))
143
+ .otherwise(() => a.localeCompare(b));
144
+ }
145
+
146
+ export const memoryValueBoundTypes: readonly MemorySchemaValueType[] = ["integer", "decimal", "date", "dateTime"];
147
+
148
+ export const memoryValueTextTypes: readonly MemorySchemaValueType[] = ["text", "longText"];
149
+
150
+ // `compileMemoryValuePattern` from ./memory-schema-pattern; null skips patterns, for a browser that has not loaded it.
151
+ export type CompileMemoryValuePattern = (pattern: string) => { test: (value: string) => boolean };
152
+
153
+ export function memoryValueConstraintViolation(
154
+ property: MemoryValueConstraints,
155
+ value: string,
156
+ compilePattern: CompileMemoryValuePattern | null,
157
+ ): string | null {
158
+ if (isPresent(property.allowedValues) && !property.allowedValues.includes(value)) {
159
+ return `must be one of ${property.allowedValues.map((option) => `"${option}"`).join(", ")}`;
160
+ }
161
+ if (property.minValue !== null && compareMemoryValues(property.valueType, value, property.minValue) < 0) {
162
+ return `must be at least ${property.minValue}`;
163
+ }
164
+ if (property.maxValue !== null && compareMemoryValues(property.valueType, value, property.maxValue) > 0) {
165
+ return `must be at most ${property.maxValue}`;
166
+ }
167
+ if (property.maxLength !== null && [...value].length > property.maxLength) {
168
+ return `must be at most ${property.maxLength} characters`;
169
+ }
170
+ if (property.pattern !== null && compilePattern !== null && !compilePattern(property.pattern).test(value)) {
171
+ return `must match the pattern ${property.pattern}`;
172
+ }
173
+ return null;
174
+ }
@@ -0,0 +1,56 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { Kind, type DocumentNode, type FragmentDefinitionNode, type SelectionSetNode } from "graphql";
3
+ import { match } from "ts-pattern";
4
+
5
+ export type OperationRootFields = { name: string | undefined; fields: string[] };
6
+
7
+ type RootFieldsArgs = {
8
+ selectionSet: SelectionSetNode;
9
+ fragments: Map<string, FragmentDefinitionNode>;
10
+ visiting: ReadonlySet<string>;
11
+ };
12
+
13
+ function rootFields(args: RootFieldsArgs): string[] {
14
+ const { selectionSet, fragments, visiting } = args;
15
+
16
+ return selectionSet.selections.flatMap((selection) =>
17
+ match(selection)
18
+ .with({ kind: Kind.FIELD }, (field) => [field.name.value])
19
+ .with({ kind: Kind.INLINE_FRAGMENT }, (inline) =>
20
+ rootFields({ selectionSet: inline.selectionSet, fragments, visiting }),
21
+ )
22
+ .with({ kind: Kind.FRAGMENT_SPREAD }, (spread) => {
23
+ const fragment = fragments.get(spread.name.value);
24
+
25
+ if (!isPresent(fragment) || visiting.has(spread.name.value)) {
26
+ throw new Error(`Unresolvable fragment ${spread.name.value}`);
27
+ }
28
+
29
+ return rootFields({
30
+ selectionSet: fragment.selectionSet,
31
+ fragments,
32
+ visiting: new Set([...visiting, spread.name.value]),
33
+ });
34
+ })
35
+ .exhaustive(),
36
+ );
37
+ }
38
+
39
+ export function operationRootFieldsOf(document: DocumentNode): OperationRootFields[] {
40
+ const fragments = new Map(
41
+ document.definitions.flatMap((definition) =>
42
+ definition.kind === Kind.FRAGMENT_DEFINITION ? [[definition.name.value, definition] as const] : [],
43
+ ),
44
+ );
45
+
46
+ return document.definitions.flatMap((definition) =>
47
+ definition.kind === Kind.OPERATION_DEFINITION
48
+ ? [
49
+ {
50
+ name: definition.name?.value,
51
+ fields: [...new Set(rootFields({ selectionSet: definition.selectionSet, fragments, visiting: new Set() }))],
52
+ },
53
+ ]
54
+ : [],
55
+ );
56
+ }
@@ -8,6 +8,9 @@ import {
8
8
  // KGC uploads the realm ontology here (kgc/kgc/realms/lib/ontology.py: realm_base + "ontology").
9
9
  export const ontologyGraphIri = expandPrefixIri(":ontology");
10
10
 
11
+ // Each datastore's editable memory schemas; KGC never writes it (kgc ontology.py: memory_schemas_graph_iri).
12
+ export const memorySchemasGraphIri = expandPrefixIri(":memorySchemas");
13
+
11
14
  // Field mutability Inferal publishes on its property shapes (DAT-577).
12
15
  export const writeLifecycle = {
13
16
  predicate: expandPrefixIri("relay:writeLifecycle"),
@@ -0,0 +1,208 @@
1
+ import { QueryEngine } from "@comunica/query-sparql-rdfjs-lite";
2
+ import { isReadOnlySparqlQuery, validateDataManipulationUpdate } from "@manta-eu/datalog-parser";
3
+ import { isPresent } from "@manta-eu/lib/present";
4
+ import type { Term, Variable } from "@rdfjs/types";
5
+ import { Parser, Store, Writer } from "n3";
6
+ import { match, P } from "ts-pattern";
7
+ import { parseDatastorePath, sparqlRoutePath } from "../shared/datastore-path";
8
+ import { prependMissingSparqlPrefixes, toTurtlePrefixes } from "../shared/rdf-namespaces";
9
+
10
+ export type MantaMockOptions = { turtle?: string };
11
+
12
+ export type MantaMockRoute = {
13
+ request(): { url(): string; method(): string; headers(): Record<string, string>; postData(): string | null };
14
+ fulfill(response: { status: number; headers: Record<string, string>; body: string }): Promise<void>;
15
+ fallback(): Promise<void>;
16
+ };
17
+
18
+ export type MantaMock = {
19
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
20
+ route: (route: MantaMockRoute) => Promise<void>;
21
+ updates: readonly string[];
22
+ };
23
+
24
+ export const mantaMockRoutePattern = /\/sparql(-update)?(\?|$)/;
25
+
26
+ const updatePath = "/sparql-update";
27
+ type SparqlPath = typeof sparqlRoutePath | typeof updatePath;
28
+
29
+ const sparqlUpdateMediaType = "application/sparql-update";
30
+ const sparqlResultsMediaType = "application/sparql-results+json";
31
+ const nTriplesMediaType = "application/n-triples";
32
+ const turtleMediaType = "text/turtle";
33
+ const xsdString = "http://www.w3.org/2001/XMLSchema#string";
34
+
35
+ // Manta passes on the datastore's status, and the datastore answers 400 for a query or an update that it cannot run.
36
+ const datastoreRefusalStatus = 400;
37
+
38
+ type Datastore = { engine: QueryEngine; store: Store };
39
+
40
+ type SparqlCall = { request: Request; body: string };
41
+
42
+ export function createMantaMock({ turtle }: MantaMockOptions = {}): MantaMock {
43
+ const datastore = { engine: new QueryEngine(), store: new Store(seedQuads(turtle)) };
44
+ const updates: string[] = [];
45
+ let pending: Promise<unknown> = Promise.resolve();
46
+
47
+ function oneAtATime(task: () => Promise<Response>): Promise<Response> {
48
+ const run = pending.then(task);
49
+ pending = run.catch(() => undefined);
50
+ return run;
51
+ }
52
+
53
+ async function respond(path: SparqlPath, call: SparqlCall): Promise<Response> {
54
+ try {
55
+ if (path === sparqlRoutePath) return await answerQuery(datastore, call);
56
+ const response = await applyUpdate(datastore, call);
57
+ if (response.ok) updates.push(call.body);
58
+ return response;
59
+ } catch (error) {
60
+ const failed = path === sparqlRoutePath ? "SPARQL query failed" : "SPARQL update failed";
61
+ const details = error instanceof Error ? error.message : String(error);
62
+ return errorResponse(datastoreRefusalStatus, { error: `${failed}: ${datastoreRefusalStatus}`, details });
63
+ }
64
+ }
65
+
66
+ async function handle(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
67
+ const request = new Request(input, init);
68
+ const path = sparqlPath(request.url);
69
+ if (request.method !== "POST" || path === undefined) return errorResponse(404, { error: "Not found" });
70
+
71
+ const body = await request.text();
72
+ return oneAtATime(() => respond(path, { request, body }));
73
+ }
74
+
75
+ return { fetch: handle, route: routeTo(handle), updates };
76
+ }
77
+
78
+ function routeTo(handle: MantaMock["fetch"]): MantaMock["route"] {
79
+ return async (route) => {
80
+ const request = route.request();
81
+ if (sparqlPath(request.url()) === undefined) {
82
+ await route.fallback();
83
+ return;
84
+ }
85
+ if (request.method() === "OPTIONS") {
86
+ await route.fulfill({ status: 204, headers: preflightHeaders(request.headers()), body: "" });
87
+ return;
88
+ }
89
+ const response = await handle(request.url(), {
90
+ method: request.method(),
91
+ headers: request.headers(),
92
+ body: request.postData() ?? undefined,
93
+ });
94
+ await route.fulfill({
95
+ status: response.status,
96
+ headers: { ...Object.fromEntries(response.headers), ...corsHeaders(request.headers()) },
97
+ body: await response.text(),
98
+ });
99
+ };
100
+ }
101
+
102
+ function seedQuads(turtle: string | undefined) {
103
+ return turtle === undefined ? [] : new Parser().parse(`${toTurtlePrefixes()}\n${turtle}`);
104
+ }
105
+
106
+ function sparqlPath(url: string): SparqlPath | undefined {
107
+ const { path } = parseDatastorePath(new URL(url).pathname);
108
+ return path === sparqlRoutePath || path === updatePath ? path : undefined;
109
+ }
110
+
111
+ async function answerQuery({ engine, store }: Datastore, { request, body }: SparqlCall): Promise<Response> {
112
+ const sparql = prependMissingSparqlPrefixes(body);
113
+ const validation = isReadOnlySparqlQuery(sparql);
114
+ if (!validation.allowed) return errorResponse(403, { error: validation.error });
115
+
116
+ const result = await engine.query(sparql, { sources: [store] });
117
+ return match(result)
118
+ .with({ resultType: "boolean" }, async (booleans) => booleanResponse(await booleans.execute()))
119
+ .with({ resultType: "quads" }, async (quads) => quadsResponse(await (await quads.execute()).toArray(), request))
120
+ .with({ resultType: "bindings" }, async (bindings) => {
121
+ const { variables } = await bindings.metadata();
122
+ const rows = await (await bindings.execute()).toArray();
123
+ return bindingsResponse({ variables: variables.map((variable) => variable.value), rows });
124
+ })
125
+ .run();
126
+ }
127
+
128
+ async function applyUpdate({ engine, store }: Datastore, { request, body }: SparqlCall): Promise<Response> {
129
+ const mediaType = request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
130
+ if (mediaType !== sparqlUpdateMediaType) {
131
+ return errorResponse(415, { error: "A write must be sent as application/sparql-update" });
132
+ }
133
+ if (!body.trim()) return errorResponse(400, { error: "Empty update body" });
134
+
135
+ const sparql = prependMissingSparqlPrefixes(body);
136
+ const validation = validateDataManipulationUpdate(sparql, { defaultGraphOnly: true });
137
+ if (!validation.allowed) {
138
+ const status = isPresent(validation.blockedOperations) ? 403 : 400;
139
+ return errorResponse(status, { error: validation.error, blockedOperations: validation.blockedOperations });
140
+ }
141
+
142
+ await engine.queryVoid(sparql, { sources: [store] });
143
+ return new Response(null, { status: 204 });
144
+ }
145
+
146
+ function booleanResponse(answer: boolean): Response {
147
+ return sparqlResultsResponse({ head: {}, boolean: answer });
148
+ }
149
+
150
+ function quadsResponse(quads: Parameters<Writer["quadsToString"]>[0], request: Request): Response {
151
+ const { mediaType, format } = quadsFormat(request.headers.get("accept"));
152
+ return new Response(new Writer({ format }).quadsToString(quads), { headers: { "content-type": mediaType } });
153
+ }
154
+
155
+ type BindingsResult = { variables: string[]; rows: Iterable<[Variable, Term]>[] };
156
+
157
+ function bindingsResponse({ variables, rows }: BindingsResult): Response {
158
+ const bindings = rows.map((row) =>
159
+ Object.fromEntries([...row].map(([variable, term]) => [variable.value, bindingTerm(term)])),
160
+ );
161
+ return sparqlResultsResponse({ head: { vars: variables }, results: { bindings } });
162
+ }
163
+
164
+ function sparqlResultsResponse(body: unknown): Response {
165
+ return Response.json(body, { headers: { "content-type": sparqlResultsMediaType } });
166
+ }
167
+
168
+ function errorResponse(status: number, body: unknown): Response {
169
+ return Response.json(body, { status });
170
+ }
171
+
172
+ function quadsFormat(accept: string | null): { mediaType: string; format: string } {
173
+ return accept?.includes(nTriplesMediaType)
174
+ ? { mediaType: nTriplesMediaType, format: "N-Triples" }
175
+ : { mediaType: turtleMediaType, format: "Turtle" };
176
+ }
177
+
178
+ function bindingTerm(term: Term): Record<string, string> {
179
+ return match(term)
180
+ .with({ termType: "NamedNode" }, ({ value }) => ({ type: "uri", value }))
181
+ .with({ termType: "BlankNode" }, ({ value }) => ({ type: "bnode", value }))
182
+ .with({ termType: "Literal", language: P.string.minLength(1) }, ({ value, language }) => ({
183
+ type: "literal",
184
+ value,
185
+ "xml:lang": language,
186
+ }))
187
+ .with({ termType: "Literal", datatype: { value: xsdString } }, ({ value }) => ({ type: "literal", value }))
188
+ .with({ termType: "Literal" }, ({ value, datatype }) => ({ type: "literal", value, datatype: datatype.value }))
189
+ .otherwise(({ termType }) => {
190
+ throw new Error(`Cannot serialize a ${termType} in a result`);
191
+ });
192
+ }
193
+
194
+ function corsHeaders(requestHeaders: Record<string, string>): Record<string, string> {
195
+ const origin = requestHeaders.origin;
196
+ if (origin === undefined) return {};
197
+ return { "access-control-allow-origin": origin, "access-control-allow-credentials": "true", vary: "Origin" };
198
+ }
199
+
200
+ function preflightHeaders(requestHeaders: Record<string, string>): Record<string, string> {
201
+ const requested = requestHeaders["access-control-request-headers"];
202
+ return {
203
+ ...corsHeaders(requestHeaders),
204
+ "access-control-allow-methods": "POST",
205
+ ...(requested === undefined ? {} : { "access-control-allow-headers": requested }),
206
+ "access-control-max-age": "600",
207
+ };
208
+ }