@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.
Files changed (33) hide show
  1. package/build/collaboration-policy/core/filter-builder.d.ts +3 -0
  2. package/build/collaboration-policy/core/filter-builder.js +90 -0
  3. package/build/collaboration-policy/core/filter-utils.d.ts +6 -0
  4. package/build/collaboration-policy/core/filter-utils.js +49 -0
  5. package/build/collaboration-policy/core/policy-traverser.d.ts +4 -0
  6. package/build/collaboration-policy/core/policy-traverser.js +96 -0
  7. package/build/collaboration-policy/core/policy-validator.d.ts +11 -0
  8. package/build/collaboration-policy/core/policy-validator.js +148 -0
  9. package/build/collaboration-policy/core/sql-builder.d.ts +37 -0
  10. package/build/collaboration-policy/core/sql-builder.js +173 -0
  11. package/build/collaboration-policy/core/types.d.ts +24 -0
  12. package/build/collaboration-policy/core/types.js +1 -0
  13. package/build/collaboration-policy/index.d.ts +4 -0
  14. package/build/collaboration-policy/index.js +3 -0
  15. package/build/collaboration-policy/scripts/generate-policy-schema.d.ts +2 -0
  16. package/build/collaboration-policy/scripts/generate-policy-schema.js +4 -0
  17. package/build/collaboration-policy/types/collaboration-policy.d.ts +697 -0
  18. package/build/collaboration-policy/types/collaboration-policy.js +262 -0
  19. package/build/collaboration-policy/types/index.d.ts +2 -0
  20. package/build/collaboration-policy/types/index.js +1 -0
  21. package/build/collaboration-policy/useCollaborationPolicy.d.ts +8 -0
  22. package/build/collaboration-policy/useCollaborationPolicy.js +14 -0
  23. package/build/collaboration-policy/utils/path-helpers.d.ts +4 -0
  24. package/build/collaboration-policy/utils/path-helpers.js +36 -0
  25. package/build/collaboration-policy/utils/sql-helpers.d.ts +5 -0
  26. package/build/collaboration-policy/utils/sql-helpers.js +65 -0
  27. package/build/index.d.ts +1 -0
  28. package/build/index.js +1 -0
  29. package/build/jobs/types.d.ts +7 -0
  30. package/build/nql/AstParser.js +5 -4
  31. package/build/nql/NqlBuilder.js +1 -1
  32. package/build/nql/types.d.ts +13 -0
  33. package/package.json +10 -10
@@ -0,0 +1,3 @@
1
+ import type { Attribute } from "../../attributes/types";
2
+ import type { PolicyFilter } from "./types";
3
+ export declare function filterToSql(filter: PolicyFilter, attributeByName: Map<string, Attribute>, datasetName: string): string;
@@ -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";
@@ -0,0 +1,3 @@
1
+ // Export the main functions
2
+ export { buildPolicySql, buildPolicySqlWithValidation, categorizePolicies, } from "./core/sql-builder";
3
+ export { buildCollaborationPolicyJsonSchema, CollaborationPolicy, } from "./types";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { buildCollaborationPolicyJsonSchema } from "../types";
3
+ const schema = buildCollaborationPolicyJsonSchema();
4
+ console.log(JSON.stringify(schema, null, 2));