UNPKG

@unito/integration-api

Version:

The Unito Integration API

163 lines (162 loc) 5.24 kB
import * as Api from './index.js'; /** * JSONPath parser that returns a relation that is guaranteed to have its schema populated. */ export function findRelationByJSONPath(item, query) { const tokens = parseJSONPath(query); const relations = []; let current = item; for (const token of tokens) { if (current === '__self') { const previousRelation = relations[relations.length - 1]; if (!previousRelation) { throw new Error(`Invalid use of __self`); } current = previousRelation.schema; } const result = applyToken(current, token); if (Api.isReferenceRelation(result) && Api.isRelationSchema(result.schema)) { relations.push(result); } else if (Api.isRelationSummary(result) && Api.isRelationSchema(result.schema)) { relations.push(result); } current = result; } if (Api.isReferenceRelation(current) || Api.isRelationSummary(current)) { return relations[relations.length - 1]; } return undefined; } /** * Parse JSONPath expression into tokens */ function parseJSONPath(query) { const tokens = []; let remaining = query; // Remove root $ if present if (remaining.startsWith('$')) { remaining = remaining.substring(1); } while (remaining.length > 0) { // Skip leading dots if (remaining.startsWith('.')) { remaining = remaining.substring(1); continue; } // Parse bracket notation [...] if (remaining.startsWith('[')) { const bracketMatch = remaining.match(/^\[([^\]]*)\]/); if (!bracketMatch) { throw new Error(`Unclosed bracket in JSONPath: ${query}`); } remaining = remaining.substring(bracketMatch[0].length); tokens.push(parseBracketExpression(String(bracketMatch[1]))); continue; } // Parse property name (until . or [ or end) const propertyMatch = remaining.match(/^([^.[]+)/); if (propertyMatch) { const propertyName = String(propertyMatch[1]); remaining = remaining.substring(propertyName.length); tokens.push({ type: 'property', name: propertyName }); } } return tokens; } /** * Parse bracket expression into a token */ function parseBracketExpression(content) { // Filter expression: ?(@.property == 'value') if (content.startsWith('?(')) { const filterExpr = content.substring(2, content.length - 1); return { type: 'filter', expression: parseFilterExpression(filterExpr) }; } // Array index: 0, 1, 2, etc. const index = parseInt(content, 10); if (!isNaN(index)) { return { type: 'index', value: index }; } throw new Error(`Unsupported bracket expression: ${content}`); } /** * Parse filter expression like @.name == 'value' */ function parseFilterExpression(expr) { const opIndex = expr.indexOf('=='); if (opIndex === -1) { throw new Error(`Filter expression must use == operator: ${expr}`); } const left = expr.substring(0, opIndex).trim(); const right = expr.substring(opIndex + 2).trim(); // Parse left side (should be @.property) if (!left.startsWith('@.')) { throw new Error(`Filter expression must start with @.: ${expr}`); } const property = left.substring(2); // Parse right side (value) using regex to extract quoted strings const quotedMatch = right.match(/^(?<quote>['"])(?<content>.*?)\k<quote>$/); if (!quotedMatch) { throw new Error(`Filter expression value must be a quoted string: ${expr}`); } return { property, value: quotedMatch.groups['content'] }; } /** * Apply a single token to the current value */ function applyToken(current, token) { switch (token.type) { case 'property': return applyProperty(current, token.name); case 'index': return applyIndex(current, token.value); case 'filter': return applyFilter(current, token.expression); default: throw new Error(`Unsupported token type: ${token.type}`); } } /** * Apply property access */ function applyProperty(current, property) { if (!Api.isObject(current) || !(property in current)) { return undefined; } return current[property]; } /** * Apply array index access */ function applyIndex(current, index) { if (!Array.isArray(current) || index < 0 || index >= current.length) { return undefined; } return current[index]; } /** * Apply filter expression * * This function returns the first item that matches the filter expression. */ function applyFilter(current, filter) { if (!Array.isArray(current)) { return undefined; } for (const item of current) { if (Api.isObject(item) && matchesFilter(item, filter)) { return item; } } return undefined; } /** * Check if an item matches a filter expression */ function matchesFilter(item, filter) { if (!(filter.property in item)) { return false; } return item[filter.property] === filter.value; }