@narrative.io/data-collaboration-sdk-ts 2.64.1 → 2.65.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/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 +96 -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 +36 -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 +10 -10
|
@@ -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,96 @@
|
|
|
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
|
+
markAttributePath(requiredAttributePaths, attribute, normalizePathValue(undefined));
|
|
15
|
+
if (field.additional_required_properties) {
|
|
16
|
+
for (const path of field.additional_required_properties) {
|
|
17
|
+
markAttributePath(requiredAttributePaths, attribute, normalizePathValue(path));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (node.filters) {
|
|
21
|
+
for (const filter of node.filters) {
|
|
22
|
+
visitFilterAttributes(filter, (attributeName, path) => {
|
|
23
|
+
const attribute = attributeByName.get(attributeName);
|
|
24
|
+
if (!attribute) {
|
|
25
|
+
throw new Error(`Attribute "${attributeName}" referenced in filter but not provided.`);
|
|
26
|
+
}
|
|
27
|
+
markAttributePath(requiredAttributePaths, attribute, path);
|
|
28
|
+
});
|
|
29
|
+
const sql = filterToSql(filter, attributeByName, datasetName);
|
|
30
|
+
whereClauses.add(sql);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (definition.filters) {
|
|
35
|
+
for (const filter of definition.filters) {
|
|
36
|
+
visitFilterAttributes(filter, (attributeName, path) => {
|
|
37
|
+
const attribute = attributeByName.get(attributeName);
|
|
38
|
+
if (!attribute) {
|
|
39
|
+
throw new Error(`Attribute "${attributeName}" referenced in filter but not provided.`);
|
|
40
|
+
}
|
|
41
|
+
markAttributePath(requiredAttributePaths, attribute, path);
|
|
42
|
+
});
|
|
43
|
+
const sql = filterToSql(filter, attributeByName, datasetName);
|
|
44
|
+
whereClauses.add(sql);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function traverseSatisfiedStructure(node, attributeByName, visitor) {
|
|
49
|
+
if (!node || typeof node !== "object") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if ("field" in node) {
|
|
53
|
+
visitor(node);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if ("anyOf" in node && Array.isArray(node.anyOf)) {
|
|
57
|
+
// For anyOf, only traverse the FIRST satisfied branch
|
|
58
|
+
for (const child of node.anyOf) {
|
|
59
|
+
if (isBranchSatisfied(child, attributeByName)) {
|
|
60
|
+
traverseSatisfiedStructure(child, attributeByName, visitor);
|
|
61
|
+
return; // Only process the first satisfied branch
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if ("allOf" in node && Array.isArray(node.allOf)) {
|
|
67
|
+
// For allOf, traverse all branches (they must all be satisfied)
|
|
68
|
+
for (const child of node.allOf) {
|
|
69
|
+
traverseSatisfiedStructure(child, attributeByName, visitor);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isBranchSatisfied(node, attributeByName) {
|
|
74
|
+
if (!node || typeof node !== "object") {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if ("field" in node) {
|
|
78
|
+
const field = node
|
|
79
|
+
.field;
|
|
80
|
+
if (field.type === "attribute") {
|
|
81
|
+
const attributeName = field.attribute_name;
|
|
82
|
+
// Only check if attribute exists, not mappings since we use company_data format
|
|
83
|
+
return attributeByName.has(attributeName);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if ("anyOf" in node && Array.isArray(node.anyOf)) {
|
|
88
|
+
// For anyOf, at least one child must be satisfied
|
|
89
|
+
return node.anyOf.some((child) => isBranchSatisfied(child, attributeByName));
|
|
90
|
+
}
|
|
91
|
+
if ("allOf" in node && Array.isArray(node.allOf)) {
|
|
92
|
+
// For allOf, all children must be satisfied
|
|
93
|
+
return node.allOf.every((child) => isBranchSatisfied(child, attributeByName));
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { buildSelectAlias, resolveAttributeExpression, } from "../utils/sql-helpers";
|
|
2
|
+
import { traversePolicy } from "./policy-traverser";
|
|
3
|
+
import { validatePolicyAgainstDataset } from "./policy-validator";
|
|
4
|
+
function buildAttributeIndexes(attributes) {
|
|
5
|
+
const byId = new Map();
|
|
6
|
+
const byName = new Map();
|
|
7
|
+
for (const attribute of attributes) {
|
|
8
|
+
byId.set(attribute.id, attribute);
|
|
9
|
+
byName.set(attribute.name, attribute);
|
|
10
|
+
}
|
|
11
|
+
return { byId, byName };
|
|
12
|
+
}
|
|
13
|
+
function getSmallestRefreshSchedule(schedules) {
|
|
14
|
+
if (schedules.length === 0) {
|
|
15
|
+
return "@monthly";
|
|
16
|
+
}
|
|
17
|
+
// Define the hierarchy for refresh schedules (from smallest to largest)
|
|
18
|
+
const scheduleHierarchy = {
|
|
19
|
+
"@once": 0,
|
|
20
|
+
"@hourly": 1,
|
|
21
|
+
"@daily": 2,
|
|
22
|
+
"@weekly": 3,
|
|
23
|
+
"@monthly": 4,
|
|
24
|
+
};
|
|
25
|
+
// Find the schedule with the smallest value (most frequent)
|
|
26
|
+
let smallestSchedule = "@monthly";
|
|
27
|
+
let smallestValue = scheduleHierarchy["@monthly"];
|
|
28
|
+
for (const schedule of schedules) {
|
|
29
|
+
if (schedule in scheduleHierarchy) {
|
|
30
|
+
const value = scheduleHierarchy[schedule];
|
|
31
|
+
if (value < smallestValue) {
|
|
32
|
+
smallestValue = value;
|
|
33
|
+
smallestSchedule = schedule;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// If it's a custom cron expression, treat it as the most frequent (smallest)
|
|
38
|
+
// since we can't easily compare cron expressions
|
|
39
|
+
return schedule;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return smallestSchedule;
|
|
43
|
+
}
|
|
44
|
+
function buildPolicySqlUsingIndexes(policies, attributeByName, datasetName) {
|
|
45
|
+
const globalRequiredAttributePaths = new Map();
|
|
46
|
+
const policyWhereClauses = [];
|
|
47
|
+
for (const policy of policies) {
|
|
48
|
+
const policyRequiredAttributePaths = new Map();
|
|
49
|
+
const policyWhereClausesSet = new Set();
|
|
50
|
+
traversePolicy(policy, attributeByName, policyRequiredAttributePaths, policyWhereClausesSet, datasetName);
|
|
51
|
+
policyWhereClauses.push(Array.from(policyWhereClausesSet));
|
|
52
|
+
for (const [attributeName, paths] of policyRequiredAttributePaths) {
|
|
53
|
+
const globalPaths = globalRequiredAttributePaths.get(attributeName) ?? new Set();
|
|
54
|
+
for (const path of paths) {
|
|
55
|
+
globalPaths.add(path);
|
|
56
|
+
}
|
|
57
|
+
globalRequiredAttributePaths.set(attributeName, globalPaths);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const selectClauses = new Set();
|
|
61
|
+
for (const [attributeName, paths] of globalRequiredAttributePaths) {
|
|
62
|
+
const attribute = attributeByName.get(attributeName);
|
|
63
|
+
if (!attribute) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const path of paths) {
|
|
67
|
+
const expression = resolveAttributeExpression(attribute, path, datasetName);
|
|
68
|
+
const alias = buildSelectAlias(attributeName, path);
|
|
69
|
+
selectClauses.add(`${expression} AS ${alias}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
select: Array.from(selectClauses),
|
|
74
|
+
where: policyWhereClauses,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function buildPolicySqlWithValidation(dataset, policies, attributes) {
|
|
78
|
+
const attributeIndexes = buildAttributeIndexes(attributes);
|
|
79
|
+
const validPolicies = [];
|
|
80
|
+
const skippedPolicies = [];
|
|
81
|
+
const warnings = [];
|
|
82
|
+
// Validate each policy against the dataset
|
|
83
|
+
for (const policy of policies) {
|
|
84
|
+
const validation = validatePolicyAgainstDataset(policy, dataset, attributes);
|
|
85
|
+
if (validation.isValid) {
|
|
86
|
+
validPolicies.push(policy);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
skippedPolicies.push({
|
|
90
|
+
name: policy.display_name || policy.name,
|
|
91
|
+
reason: "Missing required attributes or mappings",
|
|
92
|
+
details: validation.errors,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Check if we have at least one valid policy
|
|
97
|
+
if (validPolicies.length === 0) {
|
|
98
|
+
const errorMessage = policies.length === 0
|
|
99
|
+
? "No policies provided"
|
|
100
|
+
: `No policies are compatible with dataset "${dataset.display_name || dataset.name}".`;
|
|
101
|
+
const details = skippedPolicies.length > 0
|
|
102
|
+
? [`Skipped policies: ${skippedPolicies.map((p) => p.name).join(", ")}`]
|
|
103
|
+
: [];
|
|
104
|
+
throw new Error(`${errorMessage}\n${details.join("\n")}`);
|
|
105
|
+
}
|
|
106
|
+
if (skippedPolicies.length > 0) {
|
|
107
|
+
warnings.push(`Skipped ${skippedPolicies.length} incompatible policies: ${skippedPolicies.map((p) => p.name).join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
// Build SQL for valid policies
|
|
110
|
+
const sqlFragments = buildPolicySqlUsingIndexes(validPolicies, attributeIndexes.byName, dataset.name);
|
|
111
|
+
// Calculate the smallest refresh schedule from applied policies
|
|
112
|
+
const refreshSchedules = validPolicies
|
|
113
|
+
.map((p) => p.policy.metadata?.refresh_schedule?.max)
|
|
114
|
+
.filter((schedule) => schedule !== undefined);
|
|
115
|
+
const refreshSchedule = getSmallestRefreshSchedule(refreshSchedules);
|
|
116
|
+
return {
|
|
117
|
+
...sqlFragments,
|
|
118
|
+
appliedPolicies: validPolicies.map((p) => p.name),
|
|
119
|
+
skippedPolicies,
|
|
120
|
+
warnings,
|
|
121
|
+
refresh_schedule: refreshSchedule,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Categorizes collaboration policies into matching and non-matching groups based on dataset compatibility.
|
|
126
|
+
*
|
|
127
|
+
* This function validates each policy against the provided dataset and attributes to determine
|
|
128
|
+
* whether the policy can be successfully applied. Policies are categorized as matching if they
|
|
129
|
+
* have all required attributes available, or non-matching if they're missing required attributes
|
|
130
|
+
* or mappings.
|
|
131
|
+
*
|
|
132
|
+
* @param policies - Array of collaboration policies to categorize
|
|
133
|
+
* @param dataset - The dataset to validate policies against
|
|
134
|
+
* @param attributes - Available attributes for validation
|
|
135
|
+
* @returns Object containing matching policies, non-matching policies, and detailed skip information
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const { matching, nonMatching, skippedDetails } = categorizePolicies(
|
|
140
|
+
* allPolicies,
|
|
141
|
+
* myDataset,
|
|
142
|
+
* availableAttributes
|
|
143
|
+
* );
|
|
144
|
+
*
|
|
145
|
+
* console.log(`${matching.length} policies can be applied`);
|
|
146
|
+
* console.log(`${nonMatching.length} policies were skipped`);
|
|
147
|
+
* skippedDetails.forEach(skip => console.log(`Skipped ${skip.name}: ${skip.reason}`));
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function categorizePolicies(policies, dataset, attributes) {
|
|
151
|
+
const matching = [];
|
|
152
|
+
const nonMatching = [];
|
|
153
|
+
const skippedDetails = [];
|
|
154
|
+
for (const policy of policies) {
|
|
155
|
+
const validation = validatePolicyAgainstDataset(policy, dataset, attributes);
|
|
156
|
+
if (validation.isValid) {
|
|
157
|
+
matching.push(policy);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
nonMatching.push(policy);
|
|
161
|
+
skippedDetails.push({
|
|
162
|
+
name: policy.display_name || policy.name,
|
|
163
|
+
reason: "Missing required attributes or mappings",
|
|
164
|
+
details: validation.errors,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { matching, nonMatching, skippedDetails };
|
|
169
|
+
}
|
|
170
|
+
export function buildPolicySql(dataset, policies, attributes) {
|
|
171
|
+
const attributeIndexes = buildAttributeIndexes(attributes);
|
|
172
|
+
return buildPolicySqlUsingIndexes(policies, attributeIndexes.byName, dataset.name);
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CollaborationPolicyType } from "../types";
|
|
2
|
+
type PolicyDefinition = CollaborationPolicyType["policy"]["definition"];
|
|
3
|
+
export type StructureNode = PolicyDefinition["structure"];
|
|
4
|
+
export type ExtendedAttributeNode = Extract<StructureNode, {
|
|
5
|
+
field: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
export type PolicyFilter = NonNullable<PolicyDefinition["filters"]>[number];
|
|
8
|
+
export type PathValue = NonNullable<ExtendedAttributeNode["field"]["additional_required_properties"]>[number];
|
|
9
|
+
export type AttributePathIndex = Map<string, Set<string>>;
|
|
10
|
+
export interface PolicySqlFragments {
|
|
11
|
+
select: string[];
|
|
12
|
+
where: string[][];
|
|
13
|
+
}
|
|
14
|
+
export interface PolicyMatchResult extends PolicySqlFragments {
|
|
15
|
+
appliedPolicies: string[];
|
|
16
|
+
skippedPolicies: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
details: string[];
|
|
20
|
+
}>;
|
|
21
|
+
warnings: string[];
|
|
22
|
+
refresh_schedule: string;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { buildPolicySql, buildPolicySqlWithValidation, categorizePolicies, } from "./core/sql-builder";
|
|
2
|
+
export type { AttributePathIndex, ExtendedAttributeNode, PathValue, PolicyFilter, PolicyMatchResult, PolicySqlFragments, StructureNode, } from "./core/types";
|
|
3
|
+
export type { CollaborationPolicyInput, CollaborationPolicyType, } from "./types";
|
|
4
|
+
export { buildCollaborationPolicyJsonSchema, CollaborationPolicy, } from "./types";
|