-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathform-validator.ts
238 lines (217 loc) · 7.53 KB
/
form-validator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import { NamedNode, Literal } from 'rdflib';
import { FormDefinition } from './types';
import ForkingStore from 'forking-store';
import { sparqlEscapeUri } from 'mu';
import { QueryEngine } from '@comunica/query-sparql';
import N3 from 'n3';
import { getPathsForFieldsQuery } from './domain/data-access/getPathsForFields';
import { getPathsForGeneratorQuery } from './domain/data-access/getPathsForGenerators';
import { ttlToStore } from './helpers/ttl-helpers';
import {
DATATYPE,
PREDICATE,
updatePredicateInTtl,
} from './utils/update-predicate-in-ttl';
type PathSegment = { predicate?: string; step?: string };
type PathQueryResultItem = PathSegment & { previous?: string; field: string };
const buildPathChain = function (results: PathQueryResultItem[]) {
const fieldPathStarts: Record<string, PathSegment> = {};
const previousToNext: Record<string, PathSegment> = {};
results.forEach((result) => {
const { predicate, previous, step, field } = result;
if (!previous) {
fieldPathStarts[field] = {
predicate,
step,
};
} else {
previousToNext[previous] = {
predicate,
step,
};
}
});
return { fieldPathStarts, previousToNext };
};
const getPathsForFields = async function (formStore: N3.Store) {
const results = await getPathsForFieldsQuery(formStore);
const { fieldPathStarts, previousToNext } = buildPathChain(results);
const fullPaths: Record<string, string[]> = {};
Object.keys(fieldPathStarts).forEach((field) => {
const path = fieldPathStarts[field];
let current = path;
const pathSteps: string[] = [];
while (current) {
if (!current.predicate) {
break; // this can never happen for fields, but it can for generators
}
pathSteps.push(current.predicate);
current = previousToNext[current.step || ''];
if (!current) {
fullPaths[field] = pathSteps;
}
}
});
return fullPaths;
};
const getPathsForGenerators = async function (formStore: N3.Store) {
const results = await getPathsForGeneratorQuery(formStore);
const { fieldPathStarts, previousToNext } = buildPathChain(results);
const fullPaths: Record<string, string[]> = {};
Object.keys(fieldPathStarts).forEach((field) => {
const path = fieldPathStarts[field];
let current: PathSegment = path;
const pathSteps: string[] = [];
while (current) {
const predicate = current.predicate;
// predicate is null for simple paths without a scope, in that case, we don't want to add this empty node to the path
if (predicate) {
pathSteps.push(current.predicate as string);
}
current = previousToNext[current.step || ''];
if (!current) {
// for this query, the path is the scope (if any) and we should add the predicate to it to get the full path
fullPaths[field] = [...pathSteps, sparqlEscapeUri(field)];
}
}
});
return fullPaths;
};
const createPathTriple = (
currentOrigin: string,
predicate: string,
nextVariable: string,
) => {
return predicate.startsWith('^')
? `${nextVariable} ${predicate.substring(1)} ${currentOrigin}.`
: `${currentOrigin} ${predicate} ${nextVariable}.`;
};
const pathToConstructVariables = function (
path: string[],
fieldIndex: number,
instanceUri: string,
) {
const instance = sparqlEscapeUri(instanceUri);
const variables = path.map((predicate, index) => {
const isFirstPredicate = index === 0;
let result = '';
const currentOrigin = isFirstPredicate
? instance
: `?field${fieldIndex}var${index - 1}`;
const nextVariable = `?field${fieldIndex}var${index}`;
result += createPathTriple(currentOrigin, predicate, nextVariable);
if (!isFirstPredicate) {
// As these triples will be added to the same Optional as the first
// predicate in this path, if any of the other predicates do not have
// a mu:uuid or a type, the whole path will be skipped. This is fine
// because otherwise they would be rejected by mu-auth.
result += `${currentOrigin} mu:uuid ${nextVariable}Id .
${currentOrigin} a ${nextVariable}Type .`;
}
return result;
});
return variables.join('\n');
};
export type QueryOptions = {
afterPrefixesSnippet?: string;
beforeWhereSnippet?: string;
};
export const buildFormConstructQuery = async function (
formTtl,
instanceUri,
options?: QueryOptions,
) {
return await buildFormQuery(formTtl, instanceUri, 'CONSTRUCT', options);
};
export const buildFormDeleteQuery = async function (
formTtl: string,
instanceUri: string,
options?: QueryOptions,
) {
return await buildFormQuery(formTtl, instanceUri, 'DELETE', options);
};
export const buildFormQuery = async function (
formTtl: string,
instanceUri: string,
queryType: 'CONSTRUCT' | 'DELETE',
options?: QueryOptions,
) {
const formStore = await ttlToStore(formTtl);
const formPaths = await getPathsForFields(formStore);
const generatorPaths = await getPathsForGenerators(formStore);
const allPaths = { ...formPaths, ...generatorPaths };
const safeInstanceUri = sparqlEscapeUri(instanceUri);
const constructVariables = Object.keys(allPaths).map((field, index) => {
return pathToConstructVariables(allPaths[field], index, instanceUri);
});
const constructPaths = constructVariables.map((path) => {
return `OPTIONAL { ${path} }`; // TODO: For virtuoso, a UNION is faster, we may want to replace this BUT UNION is broken by comunica right now
});
return `
PREFIX form: <http://lblod.data.gift/vocabularies/forms/>
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX mu: <http://mu.semte.ch/vocabularies/core/>
PREFIX dcterms: <http://purl.org/dc/terms/>
${options?.afterPrefixesSnippet || ''}
${queryType} {
${safeInstanceUri} a ?type .
${safeInstanceUri} dcterms:modified ?modifiedAt .
${constructVariables.join('\n')}
}
${options?.beforeWhereSnippet || ''}
WHERE {
${safeInstanceUri} a ?type .
OPTIONAL {
OPTIONAL {
${safeInstanceUri} dcterms:modified ?modifiedAt .
}
${constructPaths.join('\n')}
}
}
`;
};
const extractFormDataTtl = async function (
dataTtl: string,
formTtl: string,
instanceUri: string,
): Promise<string> {
const constructQuery = await buildFormConstructQuery(formTtl, instanceUri);
const constructStore = await ttlToStore(dataTtl);
const engine = new QueryEngine();
const bindings = await engine.queryQuads(constructQuery, {
sources: [constructStore],
});
const quads = await bindings.toArray();
return new Promise((resolve, reject) => {
const writer = new N3.Writer({ format: 'text/turtle' });
writer.addQuads(quads);
writer.end((error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
export const cleanAndValidateFormInstance = async function (
instanceTtl: string,
definition: FormDefinition,
instanceUri: string,
) {
const definitionTtl = definition.formTtl;
const store = new ForkingStore();
const validationGraph = new NamedNode('http://data.lblod.info/validation');
await store.parse(instanceTtl, validationGraph);
const parsedTtl = await store.serializeDataMergedGraph(validationGraph);
const ttlWithModifiedAt = await updatePredicateInTtl(
new NamedNode(instanceUri),
PREDICATE.modified,
new Literal(new Date().toISOString(), undefined, DATATYPE.datetime),
parsedTtl,
);
const cleanedTtl = await extractFormDataTtl(
ttlWithModifiedAt,
definitionTtl,
instanceUri,
);
return cleanedTtl;
};