@narrative.io/data-collaboration-sdk-ts 2.64.1 → 2.66.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.
- package/README.md +137 -1
- package/build/collaboration-policy/core/filter-builder.d.ts +3 -0
- package/build/collaboration-policy/core/filter-builder.js +90 -0
- package/build/collaboration-policy/core/filter-utils.d.ts +6 -0
- package/build/collaboration-policy/core/filter-utils.js +49 -0
- package/build/collaboration-policy/core/policy-traverser.d.ts +4 -0
- package/build/collaboration-policy/core/policy-traverser.js +100 -0
- package/build/collaboration-policy/core/policy-validator.d.ts +11 -0
- package/build/collaboration-policy/core/policy-validator.js +148 -0
- package/build/collaboration-policy/core/sql-builder.d.ts +37 -0
- package/build/collaboration-policy/core/sql-builder.js +173 -0
- package/build/collaboration-policy/core/types.d.ts +24 -0
- package/build/collaboration-policy/core/types.js +1 -0
- package/build/collaboration-policy/index.d.ts +4 -0
- package/build/collaboration-policy/index.js +3 -0
- package/build/collaboration-policy/scripts/generate-policy-schema.d.ts +2 -0
- package/build/collaboration-policy/scripts/generate-policy-schema.js +4 -0
- package/build/collaboration-policy/types/collaboration-policy.d.ts +697 -0
- package/build/collaboration-policy/types/collaboration-policy.js +262 -0
- package/build/collaboration-policy/types/index.d.ts +2 -0
- package/build/collaboration-policy/types/index.js +1 -0
- package/build/collaboration-policy/useCollaborationPolicy.d.ts +8 -0
- package/build/collaboration-policy/useCollaborationPolicy.js +14 -0
- package/build/collaboration-policy/utils/path-helpers.d.ts +4 -0
- package/build/collaboration-policy/utils/path-helpers.js +39 -0
- package/build/collaboration-policy/utils/sql-helpers.d.ts +5 -0
- package/build/collaboration-policy/utils/sql-helpers.js +65 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/jobs/types.d.ts +7 -0
- package/build/nql/AstParser.js +5 -4
- package/build/nql/NqlBuilder.js +1 -1
- package/build/nql/types.d.ts +13 -0
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -313,4 +313,140 @@ try {
|
|
|
313
313
|
|
|
314
314
|
## API Reference
|
|
315
315
|
|
|
316
|
-
For detailed API documentation, please visit the [Narrative.io API Documentation](https://api.narrative.dev).
|
|
316
|
+
For detailed API documentation, please visit the [Narrative.io API Documentation](https://api.narrative.dev).
|
|
317
|
+
|
|
318
|
+
## Local Development: Use This SDK Locally in a Consumer App (No Publish Needed)
|
|
319
|
+
|
|
320
|
+
You can link this SDK into another repo (e.g., a Nuxt 3 app) and iterate without publishing to npm each time. Two supported flows:
|
|
321
|
+
|
|
322
|
+
- **Linked mode (fastest)** — symlink your local SDK into the app
|
|
323
|
+
- **Tarball mode (exact publish parity)** — install from a local `npm pack` tarball
|
|
324
|
+
|
|
325
|
+
Both approaches use the SDK’s **built output** in `build/`, exactly like the published package.
|
|
326
|
+
|
|
327
|
+
### Prerequisites
|
|
328
|
+
|
|
329
|
+
- Node & npm
|
|
330
|
+
- This repo builds TypeScript to `build/`
|
|
331
|
+
- This repo includes `scripts/ensureBuild.js` to verify artifacts exist
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# In the SDK repo (this one)
|
|
335
|
+
npm i
|
|
336
|
+
npm run build # one-time; emits build/**
|
|
337
|
+
node scripts/ensureBuild.js
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
### Option A — Linked Mode (Fastest Dev Feedback)
|
|
343
|
+
|
|
344
|
+
1. **Keep the SDK building on save**
|
|
345
|
+
In the SDK repo (leave this running):
|
|
346
|
+
```bash
|
|
347
|
+
npm run dev # tsc -w → continuously updates build/**
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
2. **Register the SDK globally**
|
|
351
|
+
In the SDK repo (new terminal):
|
|
352
|
+
```bash
|
|
353
|
+
npm run link:global # runs ensureBuild + npm link
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
3. **Link into your consumer app**
|
|
357
|
+
In your app repo (e.g., Nuxt project):
|
|
358
|
+
```bash
|
|
359
|
+
# optional safety if a link exists
|
|
360
|
+
npm unlink @narrative.io/data-collaboration-sdk-ts --no-save || true
|
|
361
|
+
|
|
362
|
+
# link the globally-registered SDK into this app’s node_modules
|
|
363
|
+
npm link @narrative.io/data-collaboration-sdk-ts
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
4. **Verify the app resolves to your local SDK**
|
|
367
|
+
```bash
|
|
368
|
+
node -e "console.log(require.resolve('@narrative.io/data-collaboration-sdk-ts/package.json'))"
|
|
369
|
+
```
|
|
370
|
+
You should see a path inside your SDK repo (not a versioned folder under the app’s `node_modules`).
|
|
371
|
+
|
|
372
|
+
5. **Run your app**
|
|
373
|
+
```bash
|
|
374
|
+
npm run dev
|
|
375
|
+
```
|
|
376
|
+
Edit files under `SDK/src/**` → the watcher writes to `build/**` → your app hot-reloads.
|
|
377
|
+
|
|
378
|
+
**Revert to the registry version (in the app):**
|
|
379
|
+
```bash
|
|
380
|
+
npm unlink @narrative.io/data-collaboration-sdk-ts --no-save
|
|
381
|
+
npm i
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
### Option B — Tarball Mode (Exact Publish Parity)
|
|
387
|
+
|
|
388
|
+
1. **Pack the SDK (same artifact you would publish)**
|
|
389
|
+
```bash
|
|
390
|
+
# in the SDK repo
|
|
391
|
+
npm run build
|
|
392
|
+
npm pack # creates ./<name>-<version>.tgz
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
2. **Install the tarball in the app**
|
|
396
|
+
```bash
|
|
397
|
+
# in the app repo
|
|
398
|
+
npm i /ABS/PATH/TO/<name>-<version>.tgz
|
|
399
|
+
npm run dev
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Revert to a registry version (in the app):**
|
|
403
|
+
```bash
|
|
404
|
+
npm i @narrative.io/data-collaboration-sdk-ts@<semver>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### Troubleshooting (Nuxt 3 / Vite / SSR)
|
|
410
|
+
|
|
411
|
+
- **“Cannot use import statement outside a module” (SSR):** Inline the SDK so Nitro bundles it.
|
|
412
|
+
```ts
|
|
413
|
+
// nuxt.config.ts
|
|
414
|
+
export default defineNuxtConfig({
|
|
415
|
+
nitro: { externals: { inline: ['@narrative.io/data-collaboration-sdk-ts'] } }
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- **Build artifacts not updating:** Ensure `npm run dev` is running in the SDK repo (TypeScript watch), and that the app is linked (or reinstalled from a fresh tarball).
|
|
420
|
+
|
|
421
|
+
- **Accidental imports from `src/*`:** Consumers should import from the package root only. This SDK publishes `build/**` and defines `main`/`types` (and may include an `exports` map) to prevent `src/*` imports.
|
|
422
|
+
|
|
423
|
+
- **Type/version drift (e.g., Zod):** If your app and the SDK use different major versions of a library whose types appear in public APIs, align versions or declare that library as a `peerDependency` in the SDK.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
### Quick Reference
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
# SDK (this repo)
|
|
431
|
+
npm i
|
|
432
|
+
npm run build
|
|
433
|
+
npm run dev
|
|
434
|
+
npm run link:global
|
|
435
|
+
|
|
436
|
+
# App (consumer repo)
|
|
437
|
+
npm unlink @narrative.io/data-collaboration-sdk-ts --no-save || true
|
|
438
|
+
npm link @narrative.io/data-collaboration-sdk-ts
|
|
439
|
+
npm run dev
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Alternative (publish-parity):
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
# SDK
|
|
446
|
+
npm run build
|
|
447
|
+
npm pack
|
|
448
|
+
|
|
449
|
+
# App
|
|
450
|
+
npm i /ABS/PATH/TO/*.tgz
|
|
451
|
+
npm run dev
|
|
452
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalizePathValue } from "../utils/path-helpers";
|
|
2
|
+
import { resolveAttributeExpression } from "../utils/sql-helpers";
|
|
3
|
+
import { isAttributeReference } from "./filter-utils";
|
|
4
|
+
export function filterToSql(filter, attributeByName, datasetName) {
|
|
5
|
+
const op = filter.op.toLowerCase();
|
|
6
|
+
switch (op) {
|
|
7
|
+
case "and":
|
|
8
|
+
case "or": {
|
|
9
|
+
const args = filter.args;
|
|
10
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
11
|
+
throw new Error(`Logical filter "${filter.op}" requires at least one argument.`);
|
|
12
|
+
}
|
|
13
|
+
const pieces = args.map((arg) => expressionToSql(arg, attributeByName, datasetName));
|
|
14
|
+
const joiner = op === "and" ? " AND " : " OR ";
|
|
15
|
+
return `(${pieces.join(joiner)})`;
|
|
16
|
+
}
|
|
17
|
+
case "not": {
|
|
18
|
+
const args = filter.args;
|
|
19
|
+
if (!Array.isArray(args) || args.length !== 1) {
|
|
20
|
+
throw new Error("NOT filter must have exactly one argument.");
|
|
21
|
+
}
|
|
22
|
+
return `(NOT ${expressionToSql(args[0], attributeByName, datasetName)})`;
|
|
23
|
+
}
|
|
24
|
+
case "is_null":
|
|
25
|
+
case "is_not_null": {
|
|
26
|
+
const left = filter.left;
|
|
27
|
+
const leftSql = expressionToSql(left, attributeByName, datasetName);
|
|
28
|
+
return `(${leftSql} ${op === "is_null" ? "IS NULL" : "IS NOT NULL"})`;
|
|
29
|
+
}
|
|
30
|
+
case "in":
|
|
31
|
+
case "not in": {
|
|
32
|
+
const left = filter.left;
|
|
33
|
+
const right = filter.right;
|
|
34
|
+
if (!Array.isArray(right) || right.length === 0) {
|
|
35
|
+
throw new Error(`Filter "${filter.op}" requires a non-empty array of values.`);
|
|
36
|
+
}
|
|
37
|
+
const leftSql = expressionToSql(left, attributeByName, datasetName);
|
|
38
|
+
const rightSql = right
|
|
39
|
+
.map((value) => expressionToSql(value, attributeByName, datasetName))
|
|
40
|
+
.join(", ");
|
|
41
|
+
const operator = op === "in" ? "IN" : "NOT IN";
|
|
42
|
+
return `(${leftSql} ${operator} (${rightSql}))`;
|
|
43
|
+
}
|
|
44
|
+
case "between": {
|
|
45
|
+
const operand = filter.operand;
|
|
46
|
+
const lower = filter.lower;
|
|
47
|
+
const upper = filter.upper;
|
|
48
|
+
const operandSql = expressionToSql(operand, attributeByName, datasetName);
|
|
49
|
+
const lowerSql = expressionToSql(lower, attributeByName, datasetName);
|
|
50
|
+
const upperSql = expressionToSql(upper, attributeByName, datasetName);
|
|
51
|
+
return `(${operandSql} BETWEEN ${lowerSql} AND ${upperSql})`;
|
|
52
|
+
}
|
|
53
|
+
default: {
|
|
54
|
+
const left = filter.left;
|
|
55
|
+
const right = filter.right;
|
|
56
|
+
const leftSql = expressionToSql(left, attributeByName, datasetName);
|
|
57
|
+
const rightSql = expressionToSql(right, attributeByName, datasetName);
|
|
58
|
+
return `(${leftSql} ${filter.op} ${rightSql})`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function expressionToSql(expression, attributeByName, datasetName) {
|
|
63
|
+
if (expression === null || expression === undefined) {
|
|
64
|
+
throw new Error("Filter expressions cannot be null or undefined.");
|
|
65
|
+
}
|
|
66
|
+
if (typeof expression === "number") {
|
|
67
|
+
return expression.toString();
|
|
68
|
+
}
|
|
69
|
+
if (typeof expression === "boolean") {
|
|
70
|
+
return expression ? "TRUE" : "FALSE";
|
|
71
|
+
}
|
|
72
|
+
if (typeof expression === "string") {
|
|
73
|
+
return `'${expression.replace(/'/g, "''")}'`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof expression === "object") {
|
|
76
|
+
if (isAttributeReference(expression)) {
|
|
77
|
+
const attributeName = expression.attribute_name;
|
|
78
|
+
const attribute = attributeByName.get(attributeName);
|
|
79
|
+
if (!attribute) {
|
|
80
|
+
throw new Error(`Attribute "${attributeName}" referenced in filter but not provided.`);
|
|
81
|
+
}
|
|
82
|
+
const path = normalizePathValue(expression.path);
|
|
83
|
+
return resolveAttributeExpression(attribute, path, datasetName);
|
|
84
|
+
}
|
|
85
|
+
if ("op" in expression) {
|
|
86
|
+
return filterToSql(expression, attributeByName, datasetName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unsupported filter expression: ${JSON.stringify(expression)}`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PathValue, PolicyFilter } from "./types";
|
|
2
|
+
export declare function visitFilterAttributes(filter: PolicyFilter, visitor: (attributeName: string, normalizedPath: string) => void): void;
|
|
3
|
+
export declare function isAttributeReference(node: unknown): node is {
|
|
4
|
+
attribute_name: string;
|
|
5
|
+
path?: PathValue;
|
|
6
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { normalizePathValue } from "../utils/path-helpers";
|
|
2
|
+
export function visitFilterAttributes(filter, visitor) {
|
|
3
|
+
traverseFilter(filter, visitor);
|
|
4
|
+
}
|
|
5
|
+
function traverseFilter(node, visitor) {
|
|
6
|
+
if (!node || typeof node !== "object") {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (isAttributeReference(node)) {
|
|
10
|
+
const normalizedPath = normalizePathValue(node.path);
|
|
11
|
+
visitor(node.attribute_name, normalizedPath);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if ("left" in node) {
|
|
15
|
+
traverseFilter(node.left, visitor);
|
|
16
|
+
}
|
|
17
|
+
if ("right" in node) {
|
|
18
|
+
const right = node.right;
|
|
19
|
+
if (Array.isArray(right)) {
|
|
20
|
+
for (const item of right) {
|
|
21
|
+
traverseFilter(item, visitor);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
traverseFilter(right, visitor);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if ("args" in node && Array.isArray(node.args)) {
|
|
29
|
+
for (const arg of node.args) {
|
|
30
|
+
traverseFilter(arg, visitor);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if ("operand" in node) {
|
|
34
|
+
traverseFilter(node.operand, visitor);
|
|
35
|
+
}
|
|
36
|
+
if ("lower" in node) {
|
|
37
|
+
traverseFilter(node.lower, visitor);
|
|
38
|
+
}
|
|
39
|
+
if ("upper" in node) {
|
|
40
|
+
traverseFilter(node.upper, visitor);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function isAttributeReference(node) {
|
|
44
|
+
return (!!node &&
|
|
45
|
+
typeof node === "object" &&
|
|
46
|
+
"type" in node &&
|
|
47
|
+
node.type === "attribute" &&
|
|
48
|
+
"attribute_name" in node);
|
|
49
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Attribute } from "../../attributes/types";
|
|
2
|
+
import type { CollaborationPolicyType } from "../types";
|
|
3
|
+
import type { AttributePathIndex } from "./types";
|
|
4
|
+
export declare function traversePolicy(policy: CollaborationPolicyType, attributeByName: Map<string, Attribute>, requiredAttributePaths: AttributePathIndex, whereClauses: Set<string>, datasetName: string): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { markAttributePath, normalizePathValue } from "../utils/path-helpers";
|
|
2
|
+
import { filterToSql } from "./filter-builder";
|
|
3
|
+
import { visitFilterAttributes } from "./filter-utils";
|
|
4
|
+
export function traversePolicy(policy, attributeByName, requiredAttributePaths, whereClauses, datasetName) {
|
|
5
|
+
const definition = policy.policy.definition;
|
|
6
|
+
// Only traverse satisfied parts of the structure
|
|
7
|
+
traverseSatisfiedStructure(definition.structure, attributeByName, (node) => {
|
|
8
|
+
const field = node.field;
|
|
9
|
+
const attributeName = field.attribute_name;
|
|
10
|
+
const attribute = attributeByName.get(attributeName);
|
|
11
|
+
if (!attribute) {
|
|
12
|
+
throw new Error(`Attribute "${attributeName}" referenced in policy but not provided.`);
|
|
13
|
+
}
|
|
14
|
+
// Always mark the root attribute (without path) for SELECT clause
|
|
15
|
+
markAttributePath(requiredAttributePaths, attribute, normalizePathValue(undefined));
|
|
16
|
+
if (field.additional_required_properties) {
|
|
17
|
+
for (const path of field.additional_required_properties) {
|
|
18
|
+
// Add IS NOT NULL filter for each additional required property
|
|
19
|
+
const pathStr = normalizePathValue(path);
|
|
20
|
+
const expression = `company_data."${datasetName}"."_rosetta_stone"."${attribute.name}"."${pathStr}"`;
|
|
21
|
+
whereClauses.add(`(${expression} IS NOT NULL)`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (node.filters) {
|
|
25
|
+
for (const filter of node.filters) {
|
|
26
|
+
visitFilterAttributes(filter, (attributeName, path) => {
|
|
27
|
+
const attribute = attributeByName.get(attributeName);
|
|
28
|
+
if (!attribute) {
|
|
29
|
+
throw new Error(`Attribute "${attributeName}" referenced in filter but not provided.`);
|
|
30
|
+
}
|
|
31
|
+
markAttributePath(requiredAttributePaths, attribute, path);
|
|
32
|
+
});
|
|
33
|
+
const sql = filterToSql(filter, attributeByName, datasetName);
|
|
34
|
+
whereClauses.add(sql);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (definition.filters) {
|
|
39
|
+
for (const filter of definition.filters) {
|
|
40
|
+
visitFilterAttributes(filter, (attributeName, path) => {
|
|
41
|
+
const attribute = attributeByName.get(attributeName);
|
|
42
|
+
if (!attribute) {
|
|
43
|
+
throw new Error(`Attribute "${attributeName}" referenced in filter but not provided.`);
|
|
44
|
+
}
|
|
45
|
+
markAttributePath(requiredAttributePaths, attribute, path);
|
|
46
|
+
});
|
|
47
|
+
const sql = filterToSql(filter, attributeByName, datasetName);
|
|
48
|
+
whereClauses.add(sql);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function traverseSatisfiedStructure(node, attributeByName, visitor) {
|
|
53
|
+
if (!node || typeof node !== "object") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if ("field" in node) {
|
|
57
|
+
visitor(node);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if ("anyOf" in node && Array.isArray(node.anyOf)) {
|
|
61
|
+
// For anyOf, only traverse the FIRST satisfied branch
|
|
62
|
+
for (const child of node.anyOf) {
|
|
63
|
+
if (isBranchSatisfied(child, attributeByName)) {
|
|
64
|
+
traverseSatisfiedStructure(child, attributeByName, visitor);
|
|
65
|
+
return; // Only process the first satisfied branch
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if ("allOf" in node && Array.isArray(node.allOf)) {
|
|
71
|
+
// For allOf, traverse all branches (they must all be satisfied)
|
|
72
|
+
for (const child of node.allOf) {
|
|
73
|
+
traverseSatisfiedStructure(child, attributeByName, visitor);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isBranchSatisfied(node, attributeByName) {
|
|
78
|
+
if (!node || typeof node !== "object") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if ("field" in node) {
|
|
82
|
+
const field = node
|
|
83
|
+
.field;
|
|
84
|
+
if (field.type === "attribute") {
|
|
85
|
+
const attributeName = field.attribute_name;
|
|
86
|
+
// Only check if attribute exists, not mappings since we use company_data format
|
|
87
|
+
return attributeByName.has(attributeName);
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if ("anyOf" in node && Array.isArray(node.anyOf)) {
|
|
92
|
+
// For anyOf, at least one child must be satisfied
|
|
93
|
+
return node.anyOf.some((child) => isBranchSatisfied(child, attributeByName));
|
|
94
|
+
}
|
|
95
|
+
if ("allOf" in node && Array.isArray(node.allOf)) {
|
|
96
|
+
// For allOf, all children must be satisfied
|
|
97
|
+
return node.allOf.every((child) => isBranchSatisfied(child, attributeByName));
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Attribute } from "../../attributes/types";
|
|
2
|
+
import type { Dataset } from "../../datasets/types";
|
|
3
|
+
import type { CollaborationPolicyType } from "../types";
|
|
4
|
+
interface PolicyValidationResult {
|
|
5
|
+
isValid: boolean;
|
|
6
|
+
missingAttributes: string[];
|
|
7
|
+
missingMappings: string[];
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function validatePolicyAgainstDataset(policy: CollaborationPolicyType, _dataset: Dataset, attributes: Attribute[]): PolicyValidationResult;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { visitFilterAttributes } from "./filter-utils";
|
|
2
|
+
export function validatePolicyAgainstDataset(policy, _dataset, attributes) {
|
|
3
|
+
const result = {
|
|
4
|
+
isValid: true,
|
|
5
|
+
missingAttributes: [],
|
|
6
|
+
missingMappings: [],
|
|
7
|
+
errors: [],
|
|
8
|
+
};
|
|
9
|
+
const attributeByName = new Map();
|
|
10
|
+
for (const attribute of attributes) {
|
|
11
|
+
attributeByName.set(attribute.name, attribute);
|
|
12
|
+
}
|
|
13
|
+
// Validate the policy structure considering logical operators
|
|
14
|
+
const validation = validatePolicyStructure(policy.policy.definition.structure, attributeByName);
|
|
15
|
+
if (!validation.isValid) {
|
|
16
|
+
result.isValid = false;
|
|
17
|
+
result.errors.push(...validation.errors);
|
|
18
|
+
// Extract unique missing attributes for backwards compatibility
|
|
19
|
+
const allMissingAttributes = new Set();
|
|
20
|
+
for (const error of validation.errors) {
|
|
21
|
+
if (error.includes("Missing attributes:")) {
|
|
22
|
+
const attrs = error
|
|
23
|
+
.split("Missing attributes:")[1]
|
|
24
|
+
?.split(",")
|
|
25
|
+
.map((s) => s.trim()) || [];
|
|
26
|
+
for (const attr of attrs) {
|
|
27
|
+
allMissingAttributes.add(attr);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
result.missingAttributes = Array.from(allMissingAttributes);
|
|
32
|
+
}
|
|
33
|
+
const missingFilterAttributes = new Set();
|
|
34
|
+
const filters = policy.policy.definition.filters ?? [];
|
|
35
|
+
for (const filter of filters) {
|
|
36
|
+
collectMissingAttributesFromFilter(filter, attributeByName, missingFilterAttributes);
|
|
37
|
+
}
|
|
38
|
+
visitStructureFilters(policy.policy.definition.structure, (filter) => collectMissingAttributesFromFilter(filter, attributeByName, missingFilterAttributes));
|
|
39
|
+
if (missingFilterAttributes.size > 0) {
|
|
40
|
+
result.isValid = false;
|
|
41
|
+
const missingList = Array.from(missingFilterAttributes).sort();
|
|
42
|
+
result.errors.push(`Filters reference missing attributes: ${missingList.join(", ")}`);
|
|
43
|
+
for (const attr of missingList) {
|
|
44
|
+
if (!result.missingAttributes.includes(attr)) {
|
|
45
|
+
result.missingAttributes.push(attr);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function validatePolicyStructure(structure, attributeByName) {
|
|
52
|
+
if (!structure || typeof structure !== "object") {
|
|
53
|
+
return { isValid: true, errors: [] };
|
|
54
|
+
}
|
|
55
|
+
const struct = structure;
|
|
56
|
+
// Handle anyOf - at least one must be valid
|
|
57
|
+
if (struct.anyOf && Array.isArray(struct.anyOf)) {
|
|
58
|
+
for (const option of struct.anyOf) {
|
|
59
|
+
const optionValidation = validatePolicyStructure(option, attributeByName);
|
|
60
|
+
if (optionValidation.isValid) {
|
|
61
|
+
// If any option is valid, the whole anyOf is valid
|
|
62
|
+
return { isValid: true, errors: [] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// If no options are valid, collect all errors
|
|
66
|
+
const allErrors = [];
|
|
67
|
+
for (const option of struct.anyOf) {
|
|
68
|
+
const optionValidation = validatePolicyStructure(option, attributeByName);
|
|
69
|
+
allErrors.push(...optionValidation.errors);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
isValid: false,
|
|
73
|
+
errors: [
|
|
74
|
+
`None of the anyOf options are satisfied. Issues: ${allErrors.join("; ")}`,
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Handle allOf - all must be valid
|
|
79
|
+
if (struct.allOf && Array.isArray(struct.allOf)) {
|
|
80
|
+
const allErrors = [];
|
|
81
|
+
for (const requirement of struct.allOf) {
|
|
82
|
+
const reqValidation = validatePolicyStructure(requirement, attributeByName);
|
|
83
|
+
if (!reqValidation.isValid) {
|
|
84
|
+
allErrors.push(...reqValidation.errors);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return allErrors.length > 0
|
|
88
|
+
? { isValid: false, errors: allErrors }
|
|
89
|
+
: { isValid: true, errors: [] };
|
|
90
|
+
}
|
|
91
|
+
// Handle single field with attribute
|
|
92
|
+
if (struct.field &&
|
|
93
|
+
struct.field.type ===
|
|
94
|
+
"attribute") {
|
|
95
|
+
const attributeName = struct.field
|
|
96
|
+
.attribute_name;
|
|
97
|
+
const errors = [];
|
|
98
|
+
// Check if attribute exists - we don't need to check mappings anymore
|
|
99
|
+
// since we use company_data.<attribute_name> format
|
|
100
|
+
if (!attributeByName.has(attributeName)) {
|
|
101
|
+
errors.push(`Missing attributes: ${attributeName}`);
|
|
102
|
+
}
|
|
103
|
+
return errors.length > 0
|
|
104
|
+
? { isValid: false, errors }
|
|
105
|
+
: { isValid: true, errors: [] };
|
|
106
|
+
}
|
|
107
|
+
// For other structures, recursively validate
|
|
108
|
+
for (const value of Object.values(struct)) {
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
const childValidation = validatePolicyStructure(value, attributeByName);
|
|
111
|
+
if (!childValidation.isValid) {
|
|
112
|
+
return childValidation;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { isValid: true, errors: [] };
|
|
117
|
+
}
|
|
118
|
+
function visitStructureFilters(node, visitor) {
|
|
119
|
+
if (!node || typeof node !== "object") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if ("field" in node) {
|
|
123
|
+
const filters = node.filters;
|
|
124
|
+
if (filters) {
|
|
125
|
+
for (const filter of filters) {
|
|
126
|
+
visitor(filter);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if ("anyOf" in node && Array.isArray(node.anyOf)) {
|
|
132
|
+
for (const child of node.anyOf) {
|
|
133
|
+
visitStructureFilters(child, visitor);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ("allOf" in node && Array.isArray(node.allOf)) {
|
|
137
|
+
for (const child of node.allOf) {
|
|
138
|
+
visitStructureFilters(child, visitor);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function collectMissingAttributesFromFilter(filter, attributesByName, missingAttributes) {
|
|
143
|
+
visitFilterAttributes(filter, (attributeName) => {
|
|
144
|
+
if (!attributesByName.has(attributeName)) {
|
|
145
|
+
missingAttributes.add(attributeName);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Attribute } from "../../attributes/types";
|
|
2
|
+
import type { Dataset } from "../../datasets/types";
|
|
3
|
+
import type { CollaborationPolicyType } from "../types";
|
|
4
|
+
import type { PolicyMatchResult, PolicySqlFragments } from "./types";
|
|
5
|
+
export declare function buildPolicySqlWithValidation(dataset: Dataset, policies: CollaborationPolicyType[], attributes: Attribute[]): PolicyMatchResult;
|
|
6
|
+
/**
|
|
7
|
+
* Categorizes collaboration policies into matching and non-matching groups based on dataset compatibility.
|
|
8
|
+
*
|
|
9
|
+
* This function validates each policy against the provided dataset and attributes to determine
|
|
10
|
+
* whether the policy can be successfully applied. Policies are categorized as matching if they
|
|
11
|
+
* have all required attributes available, or non-matching if they're missing required attributes
|
|
12
|
+
* or mappings.
|
|
13
|
+
*
|
|
14
|
+
* @param policies - Array of collaboration policies to categorize
|
|
15
|
+
* @param dataset - The dataset to validate policies against
|
|
16
|
+
* @param attributes - Available attributes for validation
|
|
17
|
+
* @returns Object containing matching policies, non-matching policies, and detailed skip information
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const { matching, nonMatching, skippedDetails } = categorizePolicies(
|
|
22
|
+
* allPolicies,
|
|
23
|
+
* myDataset,
|
|
24
|
+
* availableAttributes
|
|
25
|
+
* );
|
|
26
|
+
*
|
|
27
|
+
* console.log(`${matching.length} policies can be applied`);
|
|
28
|
+
* console.log(`${nonMatching.length} policies were skipped`);
|
|
29
|
+
* skippedDetails.forEach(skip => console.log(`Skipped ${skip.name}: ${skip.reason}`));
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function categorizePolicies(policies: CollaborationPolicyType[], dataset: Dataset, attributes: Attribute[]): {
|
|
33
|
+
matching: CollaborationPolicyType[];
|
|
34
|
+
nonMatching: CollaborationPolicyType[];
|
|
35
|
+
skippedDetails: PolicyMatchResult["skippedPolicies"];
|
|
36
|
+
};
|
|
37
|
+
export declare function buildPolicySql(dataset: Dataset, policies: CollaborationPolicyType[], attributes: Attribute[]): PolicySqlFragments;
|