@unito/integration-api
Version:
The Unito Integration API
163 lines (162 loc) • 5.24 kB
JavaScript
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;
}