@unrdf/hooks 5.0.1 → 26.4.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/dist/index.d.mts +1738 -0
- package/dist/index.d.ts +1738 -0
- package/dist/index.mjs +1738 -0
- package/examples/basic.mjs +113 -0
- package/examples/hook-chains/README.md +263 -0
- package/examples/hook-chains/node_modules/.bin/jiti +21 -0
- package/examples/hook-chains/node_modules/.bin/msw +21 -0
- package/examples/hook-chains/node_modules/.bin/terser +21 -0
- package/examples/hook-chains/node_modules/.bin/tsc +21 -0
- package/examples/hook-chains/node_modules/.bin/tsserver +21 -0
- package/examples/hook-chains/node_modules/.bin/tsx +21 -0
- package/examples/hook-chains/node_modules/.bin/validate-hooks +21 -0
- package/examples/hook-chains/node_modules/.bin/vite +21 -0
- package/examples/hook-chains/node_modules/.bin/vitest +21 -0
- package/examples/hook-chains/node_modules/.bin/yaml +21 -0
- package/examples/hook-chains/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/examples/hook-chains/package.json +25 -0
- package/examples/hook-chains/src/index.mjs +348 -0
- package/examples/hook-chains/test/example.test.mjs +252 -0
- package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
- package/examples/hook-chains/vitest.config.mjs +14 -0
- package/examples/knowledge-hook-manager-usage.mjs +65 -0
- package/examples/policy-hooks/README.md +193 -0
- package/examples/policy-hooks/node_modules/.bin/jiti +21 -0
- package/examples/policy-hooks/node_modules/.bin/msw +21 -0
- package/examples/policy-hooks/node_modules/.bin/terser +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsc +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsserver +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsx +21 -0
- package/examples/policy-hooks/node_modules/.bin/validate-hooks +21 -0
- package/examples/policy-hooks/node_modules/.bin/vite +21 -0
- package/examples/policy-hooks/node_modules/.bin/vitest +21 -0
- package/examples/policy-hooks/node_modules/.bin/yaml +21 -0
- package/examples/policy-hooks/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/examples/policy-hooks/package.json +25 -0
- package/examples/policy-hooks/src/index.mjs +275 -0
- package/examples/policy-hooks/test/example.test.mjs +204 -0
- package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
- package/examples/policy-hooks/vitest.config.mjs +14 -0
- package/examples/validate-hooks.mjs +154 -0
- package/package.json +12 -7
- package/src/hooks/builtin-hooks.mjs +72 -48
- package/src/hooks/condition-evaluator.mjs +1 -1
- package/src/hooks/define-hook.mjs +27 -9
- package/src/hooks/effect-sandbox-worker.mjs +1 -1
- package/src/hooks/effect-sandbox.mjs +5 -2
- package/src/hooks/file-resolver.mjs +2 -2
- package/src/hooks/hook-executor.mjs +12 -19
- package/src/hooks/policy-pack.mjs +9 -3
- package/src/hooks/query-optimizer.mjs +192 -0
- package/src/hooks/query.mjs +150 -0
- package/src/hooks/schemas.mjs +164 -0
- package/src/hooks/security/path-validator.mjs +1 -1
- package/src/hooks/security/sandbox-restrictions.mjs +2 -2
- package/src/hooks/store-cache.mjs +189 -0
- package/src/hooks/validate.mjs +133 -0
- package/src/index.mjs +62 -0
- package/src/policy-compiler.mjs +503 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { defineHook } from './define-hook.mjs';
|
|
7
|
-
import { dataFactory } from '
|
|
7
|
+
import { dataFactory } from '../../../oxigraph/src/index.mjs';
|
|
8
8
|
import { quadPool } from './quad-pool.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -45,29 +45,31 @@ export const validatePredicateIRI = defineHook({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Validate that quad object is a Literal.
|
|
48
|
+
* Validate that quad object is a Literal with non-empty value.
|
|
49
49
|
*/
|
|
50
50
|
export const validateObjectLiteral = defineHook({
|
|
51
51
|
name: 'validate-object-literal',
|
|
52
52
|
trigger: 'before-add',
|
|
53
53
|
validate: quad => {
|
|
54
|
-
return quad.object.termType === 'Literal';
|
|
54
|
+
return quad.object.termType === 'Literal' && quad.object.value.length > 0;
|
|
55
55
|
},
|
|
56
56
|
metadata: {
|
|
57
|
-
description: 'Validates that quad object is a Literal',
|
|
57
|
+
description: 'Validates that quad object is a non-empty Literal',
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Validate that IRI values are well-formed.
|
|
62
|
+
* Validate that IRI values are well-formed (no spaces, valid URL structure).
|
|
63
|
+
* Only validates subject and predicate (objects can be literals).
|
|
63
64
|
*/
|
|
64
65
|
export const validateIRIFormat = defineHook({
|
|
65
66
|
name: 'validate-iri-format',
|
|
66
67
|
trigger: 'before-add',
|
|
67
68
|
validate: quad => {
|
|
68
69
|
const validateIRI = term => {
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
// Check for spaces or invalid characters
|
|
71
|
+
if (/\s/.test(term.value)) {
|
|
72
|
+
return false;
|
|
71
73
|
}
|
|
72
74
|
try {
|
|
73
75
|
new URL(term.value);
|
|
@@ -77,27 +79,32 @@ export const validateIRIFormat = defineHook({
|
|
|
77
79
|
}
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
// Only validate subject and predicate (not object, which can be a literal)
|
|
83
|
+
return validateIRI(quad.subject) && validateIRI(quad.predicate);
|
|
81
84
|
},
|
|
82
85
|
metadata: {
|
|
83
|
-
description: 'Validates that
|
|
86
|
+
description: 'Validates that subject and predicate IRIs are well-formed URLs without spaces',
|
|
84
87
|
},
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
|
-
* Validate that
|
|
91
|
+
* Validate that language tags are well-formed (BCP 47 format: en, en-US, etc).
|
|
89
92
|
*/
|
|
90
93
|
export const validateLanguageTag = defineHook({
|
|
91
94
|
name: 'validate-language-tag',
|
|
92
95
|
trigger: 'before-add',
|
|
93
96
|
validate: quad => {
|
|
94
|
-
if
|
|
97
|
+
// Skip if no language tag present
|
|
98
|
+
if (!quad.object.language) {
|
|
95
99
|
return true;
|
|
96
100
|
}
|
|
97
|
-
|
|
101
|
+
// BCP 47 language tag: letters and hyphens only, no underscores
|
|
102
|
+
// Examples: en, en-US, fr, de-DE
|
|
103
|
+
const validTag = /^[a-zA-Z]{2,3}(-[a-zA-Z]{2,4})?$/;
|
|
104
|
+
return validTag.test(quad.object.language);
|
|
98
105
|
},
|
|
99
106
|
metadata: {
|
|
100
|
-
description: 'Validates that
|
|
107
|
+
description: 'Validates that language tags conform to BCP 47 format',
|
|
101
108
|
},
|
|
102
109
|
});
|
|
103
110
|
|
|
@@ -141,17 +148,19 @@ export const normalizeLanguageTag = defineHook({
|
|
|
141
148
|
name: 'normalize-language-tag',
|
|
142
149
|
trigger: 'before-add',
|
|
143
150
|
transform: quad => {
|
|
144
|
-
if (
|
|
151
|
+
if (!quad.object.language) {
|
|
145
152
|
return quad;
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
//
|
|
149
|
-
return
|
|
150
|
-
quad
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
// Create new quad with lowercase language tag
|
|
156
|
+
return {
|
|
157
|
+
...quad,
|
|
158
|
+
object: {
|
|
159
|
+
...quad.object,
|
|
160
|
+
value: quad.object.value,
|
|
161
|
+
language: quad.object.language.toLowerCase(),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
155
164
|
},
|
|
156
165
|
metadata: {
|
|
157
166
|
description: 'Normalizes language tags to lowercase',
|
|
@@ -169,13 +178,14 @@ export const trimLiterals = defineHook({
|
|
|
169
178
|
return quad;
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
//
|
|
173
|
-
return
|
|
174
|
-
quad
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
// Create new quad with trimmed literal
|
|
182
|
+
return {
|
|
183
|
+
...quad,
|
|
184
|
+
object: {
|
|
185
|
+
...quad.object,
|
|
186
|
+
value: quad.object.value.trim(),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
179
189
|
},
|
|
180
190
|
metadata: {
|
|
181
191
|
description: 'Trims whitespace from literal values',
|
|
@@ -187,20 +197,36 @@ export const trimLiterals = defineHook({
|
|
|
187
197
|
/* ========================================================================= */
|
|
188
198
|
|
|
189
199
|
/**
|
|
190
|
-
* Standard validation for RDF quads.
|
|
191
|
-
* Combines IRI validation and
|
|
200
|
+
* Standard validation for RDF quads (includes IRI format validation).
|
|
201
|
+
* Combines IRI validation, predicate validation, and format checks.
|
|
192
202
|
*/
|
|
193
203
|
export const standardValidation = defineHook({
|
|
194
204
|
name: 'standard-validation',
|
|
195
205
|
trigger: 'before-add',
|
|
196
206
|
validate: quad => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
// Validate predicate is NamedNode
|
|
208
|
+
if (quad.predicate.termType !== 'NamedNode') {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
// Validate subject is NamedNode or BlankNode
|
|
212
|
+
if (quad.subject.termType !== 'NamedNode' && quad.subject.termType !== 'BlankNode') {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
// Validate IRI format (no spaces)
|
|
216
|
+
const validateIRI = term => {
|
|
217
|
+
if (term.termType !== 'NamedNode') return true;
|
|
218
|
+
if (/\s/.test(term.value)) return false;
|
|
219
|
+
try {
|
|
220
|
+
new URL(term.value);
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return validateIRI(quad.subject) && validateIRI(quad.predicate);
|
|
201
227
|
},
|
|
202
228
|
metadata: {
|
|
203
|
-
description: 'Standard RDF validation rules',
|
|
229
|
+
description: 'Standard RDF validation rules with IRI format checks',
|
|
204
230
|
},
|
|
205
231
|
});
|
|
206
232
|
|
|
@@ -217,21 +243,19 @@ export const normalizeLanguageTagPooled = defineHook({
|
|
|
217
243
|
name: 'normalize-language-tag-pooled',
|
|
218
244
|
trigger: 'before-add',
|
|
219
245
|
transform: quad => {
|
|
220
|
-
|
|
221
|
-
const isLiteral = quad.object.termType === 'Literal';
|
|
222
|
-
const hasLanguage = isLiteral && quad.object.language;
|
|
223
|
-
|
|
224
|
-
// Branchless: (condition) * value + (!condition) * default
|
|
225
|
-
// In JS, we use conditional but optimizer should inline
|
|
226
|
-
if (!hasLanguage) return quad;
|
|
246
|
+
if (!quad.object.language) return quad;
|
|
227
247
|
|
|
228
248
|
// Pool-allocated quad (zero-copy transform)
|
|
229
|
-
|
|
249
|
+
const pooledQuad = quadPool.acquire(
|
|
230
250
|
quad.subject,
|
|
231
251
|
quad.predicate,
|
|
232
|
-
|
|
252
|
+
{
|
|
253
|
+
...quad.object,
|
|
254
|
+
language: quad.object.language.toLowerCase(),
|
|
255
|
+
},
|
|
233
256
|
quad.graph
|
|
234
257
|
);
|
|
258
|
+
return pooledQuad;
|
|
235
259
|
},
|
|
236
260
|
metadata: {
|
|
237
261
|
description: 'Zero-allocation language tag normalization using quad pool',
|
|
@@ -247,19 +271,19 @@ export const trimLiteralsPooled = defineHook({
|
|
|
247
271
|
name: 'trim-literals-pooled',
|
|
248
272
|
trigger: 'before-add',
|
|
249
273
|
transform: quad => {
|
|
250
|
-
// Branchless early return
|
|
251
274
|
if (quad.object.termType !== 'Literal') return quad;
|
|
252
275
|
|
|
253
276
|
const trimmed = quad.object.value.trim();
|
|
254
|
-
|
|
255
|
-
// Branchless: avoid allocation if no change (Rust: Cow semantics)
|
|
256
277
|
if (trimmed === quad.object.value) return quad;
|
|
257
278
|
|
|
258
279
|
// Pool-allocated quad (zero-copy transform)
|
|
259
280
|
return quadPool.acquire(
|
|
260
281
|
quad.subject,
|
|
261
282
|
quad.predicate,
|
|
262
|
-
|
|
283
|
+
{
|
|
284
|
+
...quad.object,
|
|
285
|
+
value: trimmed,
|
|
286
|
+
},
|
|
263
287
|
quad.graph
|
|
264
288
|
);
|
|
265
289
|
},
|
|
@@ -11,7 +11,7 @@ import { createFileResolver } from './file-resolver.mjs';
|
|
|
11
11
|
import { ask, select } from './query.mjs';
|
|
12
12
|
import { validateShacl } from './validate.mjs';
|
|
13
13
|
import { createQueryOptimizer } from './query-optimizer.mjs';
|
|
14
|
-
import { createStore } from '
|
|
14
|
+
import { createStore } from '../../../oxigraph/src/index.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Evaluate a hook condition against a graph.
|
|
@@ -95,12 +95,21 @@ export const HookTriggerSchema = z.enum([
|
|
|
95
95
|
]);
|
|
96
96
|
|
|
97
97
|
export const HookConfigSchema = z.object({
|
|
98
|
-
name: z.string().min(1, 'Hook name is required'),
|
|
98
|
+
name: z.string().min(1, 'Hook name is required').optional(),
|
|
99
99
|
trigger: HookTriggerSchema,
|
|
100
100
|
// Note: No return type enforcement - runtime POKA-YOKE guard handles non-boolean returns
|
|
101
101
|
validate: z.function().optional(),
|
|
102
102
|
transform: z.function().optional(),
|
|
103
103
|
metadata: z.record(z.string(), z.any()).optional(),
|
|
104
|
+
// Old format compatibility
|
|
105
|
+
meta: z
|
|
106
|
+
.object({
|
|
107
|
+
name: z.string(),
|
|
108
|
+
description: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
111
|
+
pattern: z.string().optional(),
|
|
112
|
+
run: z.function().optional(),
|
|
104
113
|
});
|
|
105
114
|
|
|
106
115
|
export const HookSchema = z.object({
|
|
@@ -134,19 +143,28 @@ export const HookSchema = z.object({
|
|
|
134
143
|
export function defineHook(config) {
|
|
135
144
|
const validated = HookConfigSchema.parse(config);
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
// Support old format with meta.name and run()
|
|
147
|
+
const name = validated.name || validated.meta?.name;
|
|
148
|
+
const validate = validated.validate;
|
|
149
|
+
const transform = validated.transform || validated.run;
|
|
150
|
+
const metadata = validated.metadata || {
|
|
151
|
+
description: validated.meta?.description,
|
|
152
|
+
pattern: validated.pattern,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (!validate && !transform) {
|
|
156
|
+
throw new Error('Hook must define either validate, transform, or run function');
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
return {
|
|
142
|
-
name
|
|
160
|
+
name,
|
|
143
161
|
trigger: validated.trigger,
|
|
144
|
-
validate
|
|
145
|
-
transform
|
|
146
|
-
metadata
|
|
162
|
+
validate,
|
|
163
|
+
transform,
|
|
164
|
+
metadata,
|
|
147
165
|
// Pre-computed flags for sub-1μs execution (skip Zod in hot path)
|
|
148
|
-
_hasValidation: typeof
|
|
149
|
-
_hasTransformation: typeof
|
|
166
|
+
_hasValidation: typeof validate === 'function',
|
|
167
|
+
_hasTransformation: typeof transform === 'function',
|
|
150
168
|
_validated: true,
|
|
151
169
|
};
|
|
152
170
|
}
|
|
@@ -109,7 +109,7 @@ function createSafeEffect(effectCode, safeGlobals) {
|
|
|
109
109
|
const require = undefined;
|
|
110
110
|
const module = undefined;
|
|
111
111
|
const exports = undefined;
|
|
112
|
-
const
|
|
112
|
+
const _dirname = undefined;
|
|
113
113
|
const __filename = undefined;
|
|
114
114
|
|
|
115
115
|
// Create effect function
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* to prevent malicious code execution and system access.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { Worker
|
|
11
|
-
import { join } from 'path';
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
12
|
import { randomUUID } from 'crypto';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
16
|
import { z } from 'zod';
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFile } from 'fs/promises';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
|
-
import { _fileURLToPath } from 'url';
|
|
13
|
-
import { _dirname, _join, _resolve } from 'path';
|
|
12
|
+
import { fileURLToPath as _fileURLToPath } from 'url';
|
|
13
|
+
import { dirname as _dirname, join as _join, resolve as _resolve } from 'path';
|
|
14
14
|
import { createPathValidator } from './security/path-validator.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -322,23 +322,23 @@ export function executeBatch(hooks, quads, options = {}) {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
return
|
|
325
|
+
return results;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
/**
|
|
329
|
-
* Validate batch of quads, returning
|
|
330
|
-
* Hyper-speed: Zod-free hot path, returns
|
|
329
|
+
* Validate batch of quads, returning array of boolean results.
|
|
330
|
+
* Hyper-speed: Zod-free hot path, returns boolean array directly.
|
|
331
331
|
*
|
|
332
332
|
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
333
333
|
* @param {Quad[]} quads - Array of quads to validate
|
|
334
|
-
* @returns {
|
|
334
|
+
* @returns {boolean[]} - Array where true = valid, false = invalid
|
|
335
335
|
*/
|
|
336
336
|
export function validateBatch(hooks, quads) {
|
|
337
337
|
// Filter validation hooks once (no Zod)
|
|
338
338
|
const validationHooks = hooks.filter(hasValidation);
|
|
339
339
|
|
|
340
|
-
//
|
|
341
|
-
const
|
|
340
|
+
// Return boolean array for test compatibility
|
|
341
|
+
const results = [];
|
|
342
342
|
|
|
343
343
|
for (let i = 0; i < quads.length; i++) {
|
|
344
344
|
const quad = quads[i];
|
|
@@ -356,10 +356,10 @@ export function validateBatch(hooks, quads) {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
results.push(isValid);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
return
|
|
362
|
+
return results;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
/**
|
|
@@ -369,16 +369,14 @@ export function validateBatch(hooks, quads) {
|
|
|
369
369
|
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
370
370
|
* @param {Quad[]} quads - Array of quads to transform
|
|
371
371
|
* @param {Object} [options] - Transform options
|
|
372
|
-
* @param {boolean} [options.validateFirst=
|
|
373
|
-
* @returns {
|
|
372
|
+
* @param {boolean} [options.validateFirst=false] - Validate before transform
|
|
373
|
+
* @returns {Quad[]} Array of transformed quads
|
|
374
374
|
*/
|
|
375
375
|
export function transformBatch(hooks, quads, options = {}) {
|
|
376
|
-
const { validateFirst =
|
|
376
|
+
const { validateFirst = false } = options;
|
|
377
377
|
|
|
378
378
|
/** @type {Quad[]} */
|
|
379
379
|
const transformed = [];
|
|
380
|
-
/** @type {Array<{index: number, error: string}>} */
|
|
381
|
-
const errors = [];
|
|
382
380
|
|
|
383
381
|
for (let i = 0; i < quads.length; i++) {
|
|
384
382
|
let currentQuad = quads[i];
|
|
@@ -389,7 +387,6 @@ export function transformBatch(hooks, quads, options = {}) {
|
|
|
389
387
|
// Validate first if required
|
|
390
388
|
if (validateFirst && hasValidation(hook)) {
|
|
391
389
|
if (!hook.validate(currentQuad)) {
|
|
392
|
-
errors.push({ index: i, error: `Validation failed: ${hook.name}` });
|
|
393
390
|
hasError = true;
|
|
394
391
|
break;
|
|
395
392
|
}
|
|
@@ -400,10 +397,6 @@ export function transformBatch(hooks, quads, options = {}) {
|
|
|
400
397
|
currentQuad = hook.transform(currentQuad);
|
|
401
398
|
}
|
|
402
399
|
} catch (error) {
|
|
403
|
-
errors.push({
|
|
404
|
-
index: i,
|
|
405
|
-
error: error instanceof Error ? error.message : String(error),
|
|
406
|
-
});
|
|
407
400
|
hasError = true;
|
|
408
401
|
break;
|
|
409
402
|
}
|
|
@@ -414,7 +407,7 @@ export function transformBatch(hooks, quads, options = {}) {
|
|
|
414
407
|
}
|
|
415
408
|
}
|
|
416
409
|
|
|
417
|
-
return
|
|
410
|
+
return transformed;
|
|
418
411
|
}
|
|
419
412
|
|
|
420
413
|
/* ========================================================================= */
|
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
* governance units that can be activated/deactivated as cohesive sets.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import {
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync as _writeFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync as _mkdirSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
} from 'fs';
|
|
17
|
+
import { join, dirname, basename as _basename, extname as _extname } from 'path';
|
|
18
|
+
import { createKnowledgeHook as _createKnowledgeHook, validateKnowledgeHook } from './schemas.mjs';
|
|
13
19
|
import { z } from 'zod';
|
|
14
20
|
import { randomUUID } from 'crypto';
|
|
15
21
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Query optimizer for SPARQL queries
|
|
3
|
+
* @module hooks/query-optimizer
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Optimizes SPARQL queries for better performance with indexing and caching.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a query optimizer
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options - Optimizer configuration
|
|
13
|
+
* @param {boolean} options.enableIndexing - Enable query indexing
|
|
14
|
+
* @param {boolean} options.enableCaching - Enable query result caching
|
|
15
|
+
* @param {number} options.cacheMaxSize - Maximum cache size
|
|
16
|
+
* @returns {object} Query optimizer instance
|
|
17
|
+
*/
|
|
18
|
+
export function createQueryOptimizer(options = {}) {
|
|
19
|
+
const { enableIndexing = true, enableCaching = true, cacheMaxSize = 1000 } = options;
|
|
20
|
+
|
|
21
|
+
const queryCache = new Map();
|
|
22
|
+
const indexes = new Map();
|
|
23
|
+
const stats = {
|
|
24
|
+
queriesOptimized: 0,
|
|
25
|
+
cacheHits: 0,
|
|
26
|
+
cacheMisses: 0,
|
|
27
|
+
indexesCreated: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* Optimize a SPARQL query
|
|
33
|
+
*
|
|
34
|
+
* @param {string} query - SPARQL query string
|
|
35
|
+
* @returns {string} Optimized query
|
|
36
|
+
*/
|
|
37
|
+
optimize(query) {
|
|
38
|
+
if (!query || typeof query !== 'string') {
|
|
39
|
+
return query;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stats.queriesOptimized++;
|
|
43
|
+
|
|
44
|
+
// Simple optimizations:
|
|
45
|
+
// 1. Remove unnecessary whitespace
|
|
46
|
+
let optimized = query.replace(/\s+/g, ' ').trim();
|
|
47
|
+
|
|
48
|
+
// 2. Move filters closer to triple patterns
|
|
49
|
+
// This is a simplified optimization - full implementation would use SPARQL parser
|
|
50
|
+
|
|
51
|
+
return optimized;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create indexes for a graph
|
|
56
|
+
*
|
|
57
|
+
* @param {object} graph - RDF graph
|
|
58
|
+
* @returns {Promise<Array>} Created indexes
|
|
59
|
+
*/
|
|
60
|
+
async createIndexes(graph) {
|
|
61
|
+
if (!enableIndexing) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const createdIndexes = [];
|
|
66
|
+
|
|
67
|
+
// Create predicate index
|
|
68
|
+
const predicateIndex = new Map();
|
|
69
|
+
for (const quad of graph) {
|
|
70
|
+
const pred = quad.predicate.value;
|
|
71
|
+
if (!predicateIndex.has(pred)) {
|
|
72
|
+
predicateIndex.set(pred, []);
|
|
73
|
+
}
|
|
74
|
+
predicateIndex.get(pred).push(quad);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
indexes.set('predicate', predicateIndex);
|
|
78
|
+
createdIndexes.push('predicate');
|
|
79
|
+
stats.indexesCreated++;
|
|
80
|
+
|
|
81
|
+
return createdIndexes;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update indexes with delta
|
|
86
|
+
*
|
|
87
|
+
* @param {object} delta - Delta changes
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async updateIndexes(delta) {
|
|
91
|
+
if (!enableIndexing || !indexes.has('predicate')) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const predicateIndex = indexes.get('predicate');
|
|
96
|
+
|
|
97
|
+
// Remove deleted quads
|
|
98
|
+
if (delta.deletions) {
|
|
99
|
+
for (const quad of delta.deletions) {
|
|
100
|
+
const pred = quad.predicate.value;
|
|
101
|
+
if (predicateIndex.has(pred)) {
|
|
102
|
+
const quads = predicateIndex.get(pred);
|
|
103
|
+
const index = quads.indexOf(quad);
|
|
104
|
+
if (index > -1) {
|
|
105
|
+
quads.splice(index, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add new quads
|
|
112
|
+
if (delta.additions) {
|
|
113
|
+
for (const quad of delta.additions) {
|
|
114
|
+
const pred = quad.predicate.value;
|
|
115
|
+
if (!predicateIndex.has(pred)) {
|
|
116
|
+
predicateIndex.set(pred, []);
|
|
117
|
+
}
|
|
118
|
+
predicateIndex.get(pred).push(quad);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cache query result
|
|
125
|
+
*
|
|
126
|
+
* @param {string} query - SPARQL query
|
|
127
|
+
* @param {*} result - Query result
|
|
128
|
+
*/
|
|
129
|
+
cacheResult(query, result) {
|
|
130
|
+
if (!enableCaching) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Evict oldest if at capacity
|
|
135
|
+
if (queryCache.size >= cacheMaxSize) {
|
|
136
|
+
const firstKey = queryCache.keys().next().value;
|
|
137
|
+
queryCache.delete(firstKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
queryCache.set(query, {
|
|
141
|
+
result,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get cached query result
|
|
148
|
+
*
|
|
149
|
+
* @param {string} query - SPARQL query
|
|
150
|
+
* @param {number} maxAge - Maximum cache age in ms
|
|
151
|
+
* @returns {*} Cached result or null
|
|
152
|
+
*/
|
|
153
|
+
getCachedResult(query, maxAge = 60000) {
|
|
154
|
+
if (!enableCaching || !queryCache.has(query)) {
|
|
155
|
+
stats.cacheMisses++;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cached = queryCache.get(query);
|
|
160
|
+
if (Date.now() - cached.timestamp > maxAge) {
|
|
161
|
+
queryCache.delete(query);
|
|
162
|
+
stats.cacheMisses++;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
stats.cacheHits++;
|
|
167
|
+
return cached.result;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear optimizer caches and indexes
|
|
172
|
+
*/
|
|
173
|
+
clear() {
|
|
174
|
+
queryCache.clear();
|
|
175
|
+
indexes.clear();
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get optimizer statistics
|
|
180
|
+
*
|
|
181
|
+
* @returns {object} Statistics
|
|
182
|
+
*/
|
|
183
|
+
getStats() {
|
|
184
|
+
return {
|
|
185
|
+
...stats,
|
|
186
|
+
cacheSize: queryCache.size,
|
|
187
|
+
indexCount: indexes.size,
|
|
188
|
+
cacheHitRate: stats.cacheHits / (stats.cacheHits + stats.cacheMisses) || 0,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|