@narrative.io/data-collaboration-sdk-ts 2.67.0 → 2.69.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.
@@ -1,4 +1,6 @@
1
+ import { normalizePathValue } from "../utils/path-helpers";
1
2
  import { buildSelectAlias, resolveAttributeExpression, } from "../utils/sql-helpers";
3
+ import { filterToSql } from "./filter-builder";
2
4
  import { traversePolicy } from "./policy-traverser";
3
5
  import { validatePolicyAgainstDataset } from "./policy-validator";
4
6
  function buildAttributeIndexes(attributes) {
@@ -10,45 +12,199 @@ function buildAttributeIndexes(attributes) {
10
12
  }
11
13
  return { byId, byName };
12
14
  }
15
+ /**
16
+ * Converts an ISO 8601 duration string to milliseconds for comparison.
17
+ * This is an approximation for human-readable durations (months/years vary in length).
18
+ *
19
+ * @param duration - ISO 8601 duration string (e.g., "PT1H", "P1D", "P1M")
20
+ * @returns Duration in milliseconds
21
+ */
22
+ function parseDurationToMilliseconds(duration) {
23
+ const regex = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
24
+ const matches = duration.match(regex);
25
+ if (!matches) {
26
+ throw new Error(`Invalid ISO 8601 duration format: ${duration}`);
27
+ }
28
+ const [, years, months, weeks, days, hours, minutes, seconds] = matches.map((m) => Number.parseInt(m || "0", 10));
29
+ // Approximate conversions (months = 30 days, years = 365 days)
30
+ const milliseconds = years * 365 * 24 * 60 * 60 * 1000 +
31
+ months * 30 * 24 * 60 * 60 * 1000 +
32
+ weeks * 7 * 24 * 60 * 60 * 1000 +
33
+ days * 24 * 60 * 60 * 1000 +
34
+ hours * 60 * 60 * 1000 +
35
+ minutes * 60 * 1000 +
36
+ seconds * 1000;
37
+ return milliseconds;
38
+ }
39
+ /**
40
+ * Finds the smallest (most frequent) refresh schedule from a list of ISO 8601 duration strings.
41
+ * The smallest duration means the most frequent refresh rate.
42
+ *
43
+ * @param schedules - Array of ISO 8601 duration strings
44
+ * @returns The smallest duration in milliseconds, or 2592000000 (30 days/1 month) if no schedules provided
45
+ */
13
46
  function getSmallestRefreshSchedule(schedules) {
47
+ // Default to monthly (30 days in milliseconds)
48
+ const defaultMonthlyMs = 30 * 24 * 60 * 60 * 1000;
14
49
  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"];
50
+ return defaultMonthlyMs;
51
+ }
52
+ if (schedules.length === 1) {
53
+ try {
54
+ return parseDurationToMilliseconds(schedules[0]);
55
+ }
56
+ catch (error) {
57
+ console.warn(`Invalid refresh schedule: ${schedules[0]}, using default`, error);
58
+ return defaultMonthlyMs;
59
+ }
60
+ }
61
+ // Find the schedule with the smallest duration (most frequent refresh)
62
+ let smallestValue = Number.MAX_SAFE_INTEGER;
28
63
  for (const schedule of schedules) {
29
- if (schedule in scheduleHierarchy) {
30
- const value = scheduleHierarchy[schedule];
64
+ try {
65
+ const value = parseDurationToMilliseconds(schedule);
31
66
  if (value < smallestValue) {
32
67
  smallestValue = value;
33
- smallestSchedule = schedule;
34
68
  }
35
69
  }
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;
70
+ catch (error) {
71
+ // If parsing fails, skip this schedule
72
+ console.warn(`Skipping invalid refresh schedule: ${schedule}`, error);
40
73
  }
41
74
  }
42
- return smallestSchedule;
75
+ // If all schedules failed to parse, return default
76
+ return smallestValue === Number.MAX_SAFE_INTEGER
77
+ ? defaultMonthlyMs
78
+ : smallestValue;
79
+ }
80
+ /**
81
+ * Builds structured WHERE clauses for a single branch node
82
+ */
83
+ function buildBranchWhere(node, attributeByName, datasetName) {
84
+ if (!node || typeof node !== "object") {
85
+ return null;
86
+ }
87
+ // Handle field nodes (leaf nodes with attributes)
88
+ if ("field" in node) {
89
+ const extendedNode = node;
90
+ const field = extendedNode.field;
91
+ const attributeName = field.attribute_name;
92
+ const attribute = attributeByName.get(attributeName);
93
+ if (!attribute) {
94
+ return null;
95
+ }
96
+ const clauses = [];
97
+ // Add IS NOT NULL filters for additional_required_properties
98
+ if (field.additional_required_properties) {
99
+ for (const path of field.additional_required_properties) {
100
+ const pathStr = normalizePathValue(path);
101
+ const expression = `company_data."${datasetName}"."_rosetta_stone"."${attribute.name}"."${pathStr}"`;
102
+ clauses.push(`(${expression} IS NOT NULL)`);
103
+ }
104
+ }
105
+ // Add filters
106
+ if (extendedNode.filters) {
107
+ for (const filter of extendedNode.filters) {
108
+ const sql = filterToSql(filter, attributeByName, datasetName);
109
+ clauses.push(sql);
110
+ }
111
+ }
112
+ // If only one clause, return as string
113
+ if (clauses.length === 0) {
114
+ return null;
115
+ }
116
+ if (clauses.length === 1) {
117
+ return clauses[0];
118
+ }
119
+ // Multiple clauses for a single field - AND them together
120
+ return {
121
+ operation: "AND",
122
+ fragments: clauses,
123
+ };
124
+ }
125
+ // Handle anyOf nodes
126
+ if ("anyOf" in node && Array.isArray(node.anyOf)) {
127
+ const satisfiedBranches = [];
128
+ for (const child of node.anyOf) {
129
+ if (isBranchSatisfied(child, attributeByName)) {
130
+ const branchWhere = buildBranchWhere(child, attributeByName, datasetName);
131
+ if (branchWhere) {
132
+ satisfiedBranches.push(branchWhere);
133
+ }
134
+ }
135
+ }
136
+ if (satisfiedBranches.length === 0) {
137
+ return null;
138
+ }
139
+ if (satisfiedBranches.length === 1) {
140
+ return satisfiedBranches[0];
141
+ }
142
+ // Multiple satisfied anyOf branches - OR them together
143
+ return {
144
+ operation: "OR",
145
+ fragments: satisfiedBranches,
146
+ };
147
+ }
148
+ // Handle allOf nodes
149
+ if ("allOf" in node && Array.isArray(node.allOf)) {
150
+ const allBranches = [];
151
+ for (const child of node.allOf) {
152
+ const branchWhere = buildBranchWhere(child, attributeByName, datasetName);
153
+ if (branchWhere) {
154
+ allBranches.push(branchWhere);
155
+ }
156
+ }
157
+ if (allBranches.length === 0) {
158
+ return null;
159
+ }
160
+ if (allBranches.length === 1) {
161
+ return allBranches[0];
162
+ }
163
+ // Multiple allOf branches - AND them together
164
+ return {
165
+ operation: "AND",
166
+ fragments: allBranches,
167
+ };
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * Checks if a branch is satisfied (all required attributes exist)
173
+ */
174
+ function isBranchSatisfied(node, attributeByName) {
175
+ if (!node || typeof node !== "object") {
176
+ return true;
177
+ }
178
+ if ("field" in node) {
179
+ const field = node
180
+ .field;
181
+ if (field.type === "attribute") {
182
+ const attributeName = field.attribute_name;
183
+ return attributeByName.has(attributeName);
184
+ }
185
+ return true;
186
+ }
187
+ if ("anyOf" in node && Array.isArray(node.anyOf)) {
188
+ return node.anyOf.some((child) => isBranchSatisfied(child, attributeByName));
189
+ }
190
+ if ("allOf" in node && Array.isArray(node.allOf)) {
191
+ return node.allOf.every((child) => isBranchSatisfied(child, attributeByName));
192
+ }
193
+ return true;
43
194
  }
44
195
  function buildPolicySqlUsingIndexes(policies, attributeByName, datasetName) {
45
196
  const globalRequiredAttributePaths = new Map();
46
- const policyWhereClauses = [];
197
+ const policyWhereGroups = [];
47
198
  for (const policy of policies) {
48
199
  const policyRequiredAttributePaths = new Map();
49
200
  const policyWhereClausesSet = new Set();
201
+ // Still use traversePolicy to collect required attributes for SELECT
50
202
  traversePolicy(policy, attributeByName, policyRequiredAttributePaths, policyWhereClausesSet, datasetName);
51
- policyWhereClauses.push(Array.from(policyWhereClausesSet));
203
+ // Build structured WHERE for this policy
204
+ const policyWhere = buildBranchWhere(policy.policy.definition.structure, attributeByName, datasetName);
205
+ if (policyWhere) {
206
+ policyWhereGroups.push(policyWhere);
207
+ }
52
208
  for (const [attributeName, paths] of policyRequiredAttributePaths) {
53
209
  const globalPaths = globalRequiredAttributePaths.get(attributeName) ?? new Set();
54
210
  for (const path of paths) {
@@ -69,9 +225,26 @@ function buildPolicySqlUsingIndexes(policies, attributeByName, datasetName) {
69
225
  const alias = buildSelectAlias(attributeName, "");
70
226
  selectClauses.add(`${expression} AS ${alias}`);
71
227
  }
228
+ // Build the root AND structure
229
+ // Root level is always AND (combines policies)
230
+ // Each policy's branches are already OR'd/AND'd by buildBranchWhere
231
+ let where;
232
+ if (policyWhereGroups.length === 0) {
233
+ where = [];
234
+ }
235
+ else {
236
+ // Wrap all policies in root AND
237
+ // Each policy in policyWhereGroups is already a complete policy structure
238
+ where = [
239
+ {
240
+ fragments: policyWhereGroups,
241
+ operation: "AND",
242
+ },
243
+ ];
244
+ }
72
245
  return {
73
246
  select: Array.from(selectClauses),
74
- where: policyWhereClauses,
247
+ where,
75
248
  };
76
249
  }
77
250
  export function buildPolicySqlWithValidation(dataset, policies, attributes) {
@@ -7,9 +7,13 @@ export type ExtendedAttributeNode = Extract<StructureNode, {
7
7
  export type PolicyFilter = NonNullable<PolicyDefinition["filters"]>[number];
8
8
  export type PathValue = NonNullable<ExtendedAttributeNode["field"]["additional_required_properties"]>[number];
9
9
  export type AttributePathIndex = Map<string, Set<string>>;
10
+ export interface PolicySqlGroup {
11
+ fragments: Array<string | PolicySqlGroup>;
12
+ operation: "AND" | "OR";
13
+ }
10
14
  export interface PolicySqlFragments {
11
15
  select: string[];
12
- where: string[][];
16
+ where: Array<PolicySqlGroup | string>;
13
17
  }
14
18
  export interface PolicyMatchResult extends PolicySqlFragments {
15
19
  appliedPolicies: string[];
@@ -19,6 +23,7 @@ export interface PolicyMatchResult extends PolicySqlFragments {
19
23
  details: string[];
20
24
  }>;
21
25
  warnings: string[];
22
- refresh_schedule: string;
26
+ /** The minimum refresh schedule in milliseconds across all evaluated policies */
27
+ refresh_schedule: number;
23
28
  }
24
29
  export {};
@@ -1,4 +1,4 @@
1
1
  export { buildPolicySql, buildPolicySqlWithValidation, categorizePolicies, } from "./core/sql-builder";
2
- export type { AttributePathIndex, ExtendedAttributeNode, PathValue, PolicyFilter, PolicyMatchResult, PolicySqlFragments, StructureNode, } from "./core/types";
2
+ export type { AttributePathIndex, ExtendedAttributeNode, PathValue, PolicyFilter, PolicyMatchResult, PolicySqlFragments, PolicySqlGroup, StructureNode, } from "./core/types";
3
3
  export type { CollaborationPolicyInput, CollaborationPolicyType, } from "./types";
4
4
  export { buildCollaborationPolicyJsonSchema, CollaborationPolicy, } from "./types";
@@ -12,13 +12,7 @@ declare const CollaborationPolicy: z.ZodObject<{
12
12
  metadata: z.ZodObject<{
13
13
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
14
  refresh_schedule: z.ZodOptional<z.ZodObject<{
15
- max: z.ZodUnion<readonly [z.ZodEnum<{
16
- "@hourly": "@hourly";
17
- "@daily": "@daily";
18
- "@weekly": "@weekly";
19
- "@monthly": "@monthly";
20
- "@once": "@once";
21
- }>, z.ZodString]>;
15
+ max: z.ZodString;
22
16
  }, z.core.$strict>>;
23
17
  }, z.core.$strip>;
24
18
  definition: z.ZodObject<{
@@ -7,15 +7,27 @@ const CollaborationPolicyIdentifier = z
7
7
  .meta({
8
8
  id: "collaboration_policy_identifier",
9
9
  });
10
- /** Refresh Schedule Schema */
10
+ /**
11
+ * Refresh Schedule Schema
12
+ *
13
+ * Specifies how often the destination should be refreshed to ensure data doesn't expire.
14
+ * This is the maximum time interval allowed between destination updates. Waiting longer
15
+ * than this duration may result in the destination expiring data that should be retained.
16
+ *
17
+ * Uses ISO 8601 duration format (e.g., "PT1H" for 1 hour, "P1D" for 1 day).
18
+ * Supported values:
19
+ * - PT1H: Hourly (1 hour)
20
+ * - P1D: Daily (1 day)
21
+ * - P7D: Weekly (7 days)
22
+ * - P1M: Monthly (1 month)
23
+ * - P0D: Once (no recurring refresh)
24
+ * - Custom ISO 8601 duration strings (e.g., "PT30M" for 30 minutes, "P2W" for 2 weeks)
25
+ */
11
26
  const RefreshScheduleSchema = z
12
27
  .object({
13
- max: z.union([
14
- z.enum(["@hourly", "@daily", "@weekly", "@monthly", "@once"]),
15
- z
16
- .string()
17
- .regex(/^([0-5]?[0-9]|\*) ([01]?[0-9]|2[0-3]|\*) ([0-2]?[0-9]|3[01]|\*) ([0]?[1-9]|1[0-2]|\*) ([0-6]|\*)$/, "Invalid cron expression"),
18
- ]),
28
+ max: z
29
+ .string()
30
+ .regex(/^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/, "Invalid ISO 8601 duration format. Expected format like 'PT1H' (1 hour), 'P1D' (1 day), 'P7D' (7 days), 'P1M' (1 month), or 'P0D' (once)"),
19
31
  })
20
32
  .strict()
21
33
  .meta({
@@ -1,5 +1,5 @@
1
1
  import type { Attribute } from "../../attributes/types";
2
+ import type { PolicySqlGroup } from "../core/types";
2
3
  export declare function resolveAttributeExpression(attribute: Attribute, path: string, datasetName: string): string;
3
4
  export declare function buildSelectAlias(attributeName: string, path: string): string;
4
- export declare function buildPolicyWhereGroups(policyWhereClauses: string[][]): string[];
5
- export declare function formatWhereClause(policyWhereClauses: string[][]): string;
5
+ export declare function formatWhereClause(where: Array<PolicySqlGroup | string>): string;
@@ -35,29 +35,35 @@ export function buildSelectAlias(attributeName, path) {
35
35
  const suffix = path.replace(/[^a-zA-Z0-9]+/g, "_");
36
36
  return quoteIdentifier(`${attributeName}_${suffix}`);
37
37
  }
38
- export function buildPolicyWhereGroups(policyWhereClauses) {
39
- const groups = [];
40
- for (const clauses of policyWhereClauses) {
41
- if (clauses.length === 0) {
42
- continue;
43
- }
44
- const firstClause = clauses[0];
45
- if (firstClause === undefined) {
46
- continue;
47
- }
48
- groups.push(clauses.length === 1 ? firstClause : `(${clauses.join(" AND ")})`);
38
+ /**
39
+ * Recursively builds SQL string from PolicySqlGroup structure
40
+ */
41
+ function buildSqlFromGroup(item) {
42
+ if (typeof item === "string") {
43
+ return item;
49
44
  }
50
- return groups;
45
+ const { fragments, operation } = item;
46
+ const separator = operation === "AND" ? " AND " : " OR ";
47
+ const parts = fragments.map(buildSqlFromGroup);
48
+ if (parts.length === 0) {
49
+ return "";
50
+ }
51
+ if (parts.length === 1) {
52
+ return parts[0];
53
+ }
54
+ return `(${parts.join(separator)})`;
51
55
  }
52
- export function formatWhereClause(policyWhereClauses) {
53
- const groups = buildPolicyWhereGroups(policyWhereClauses);
54
- if (groups.length === 0) {
56
+ export function formatWhereClause(where) {
57
+ if (where.length === 0) {
55
58
  return "";
56
59
  }
57
- if (groups.length === 1) {
58
- return `\nWHERE\n ${groups[0]}`;
60
+ const sqlParts = where
61
+ .map(buildSqlFromGroup)
62
+ .filter((part) => part.length > 0);
63
+ if (sqlParts.length === 0) {
64
+ return "";
59
65
  }
60
- return `\nWHERE\n ${groups.join(" OR\n ")}`;
66
+ return `\nWHERE\n ${sqlParts.join("\n ")}`;
61
67
  }
62
68
  function quoteIdentifier(identifier) {
63
69
  const safe = identifier.replace(/"/g, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/data-collaboration-sdk-ts",
3
- "version": "2.67.0",
3
+ "version": "2.69.0",
4
4
  "main": "build/index.js",
5
5
  "repository": "github:narrative-io/data-collaboration-sdk-ts",
6
6
  "source": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "@types/jest": "30.0.0",
34
34
  "babel-jest": "30.2.0",
35
35
  "jest": "30.2.0",
36
- "lefthook": "1.13.6",
36
+ "lefthook": "2.0.0",
37
37
  "ts-jest": "29.4.4"
38
38
  },
39
39
  "dependencies": {