UNPKG

@unito/integration-api

Version:

The Unito Integration API

803 lines (797 loc) 29.5 kB
'use strict'; /** * A FieldValueType determines the type of an item's value. * The type represents a unique scalar value such as a string or an integer. * For more complex types, use an Item instead. */ const FieldValueTypes = { BLOB: 'blob', BOOLEAN: 'boolean', DATE: 'date', DATE_RANGE: 'dateRange', DATETIME: 'datetime', DATETIME_RANGE: 'datetimeRange', DURATION: 'duration', EMAIL: 'email', RICH_TEXT_HTML: 'html', INTEGER: 'integer', RICH_TEXT_MARKDOWN: 'markdown', NUMBER: 'number', OBJECT: 'object', REFERENCE: 'reference', STRING: 'string', URL: 'url', }; /** * A Semantic gives meaning to an Item or a Field in the Unito platform. * An object with a specified semantic is expected to hold a certain type of value that can later be used * by the consumers of the spec to better understand what that value means. */ const Semantics = { CREATED_AT: 'createdAt', DESCRIPTION: 'description', DISPLAY_NAME: 'displayName', PROVIDER_URL: 'providerUrl', UPDATED_AT: 'updatedAt', USER: 'user', PARENT: 'parent', }; /** * A Relation Semantic gives meaning to a Relation in the Unito platform. A relation with a * specified semantic is expected to hold a certain type of item that can later be used by * the consumers of the spec for specific usage. */ const RelationSemantics = { COMMENTS: 'comments', SUBTASKS: 'subtasks', USERS: 'users', ATTACHMENTS: 'attachments', }; /** * An OperatorType represents an operator used in a filter. */ const OperatorTypes = { EQUAL: '=', NOT_EQUAL: '!=', GREATER_THAN: '>', GREATER_THAN_OR_EQUAL: '>=', LESSER_THAN: '<', LESSER_THAN_OR_EQUAL: '<=', INCLUDE: '*=', EXCLUDE: '!*=', START_WITH: '^=', END_WITH: '$=', IS_NULL: '!!', IS_NOT_NULL: '!', }; /** * HTTP code returned by the integration */ const StatusCodes = { OK: 200, CREATED: 201, ACCEPTED: 202, NO_CONTENT: 204, BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, NOT_ACCEPTABLE: 406, REQUEST_TIMEOUT: 408, UNPROCESSABLE_CONTENT: 422, LOCKED: 423, TOO_MANY_REQUESTS: 429, INTERNAL_SERVER_ERROR: 500, }; /** * Checks if the input is a record<unknown, unknown> * @param potentialObject - The value to check. * @returns True if the value is a record, false otherwise. */ function isObject(potentialObject) { return typeof potentialObject === 'object' && potentialObject !== null && !Array.isArray(potentialObject); } /** * Checks if the input is a string. * @param potentialString - The value to check. * @returns True if the value is a string, false otherwise. */ function isString(potentialString) { return typeof potentialString === 'string'; } /** * Checks if the input is undefined. * @param potentialUndefined - The value to check. * @returns True if the value is undefined, false otherwise. */ function isUndefined(potentialUndefined) { return potentialUndefined === undefined; } /** * Checks if the input is an Api.ItemSummary object. * @param potentialItemSummary - The value to check. * @returns True if the value is an ItemSummary, false otherwise. */ function isItemSummary(potentialItemSummary) { return (isObject(potentialItemSummary) && isString(potentialItemSummary['path']) && (isUndefined(potentialItemSummary['fields']) || isObject(potentialItemSummary['fields'])) && (isUndefined(potentialItemSummary['relations']) || (Array.isArray(potentialItemSummary['relations']) && potentialItemSummary['relations'].every(isString)))); } /** * Checks if the input is an Api.Item object. * @param potentialItem - The value to check. * @returns True if the value is an Item, false otherwise. */ function isItem(potentialItem) { return (isObject(potentialItem) && isObject(potentialItem['fields']) && Array.isArray(potentialItem['relations']) && potentialItem['relations'].every(isRelation)); } /** * Checks if the input is an Api.ReferenceRelation object. * @param potentialReferenceRelation - The value to check. * @returns True if the value is a ReferenceRelation, false otherwise. */ function isReferenceRelation(potentialReferenceRelation) { return (isObject(potentialReferenceRelation) && isString(potentialReferenceRelation['path']) && isString(potentialReferenceRelation['label']) && isRelationSchemaOrSelf(potentialReferenceRelation['schema'])); } /** * Checks if the input is an Api.RelationSummary object. * @param potentialRelationSummary - The value to check. * @returns True if the value is a RelationSummary, false otherwise. */ function isRelationSummary(potentialRelationSummary) { return (isObject(potentialRelationSummary) && isString(potentialRelationSummary['name']) && isString(potentialRelationSummary['label']) && isRelationSchemaOrSelf(potentialRelationSummary['schema'])); } /** * Checks if the input is an Api.Relation object. * @param potentialRelation - The value to check. * @returns True if the value is a Relation, false otherwise. */ function isRelation(potentialRelation) { return (isObject(potentialRelation) && isString(potentialRelation['name']) && isString(potentialRelation['path']) && isString(potentialRelation['label']) && isRelationSchema(potentialRelation['schema'])); } /** * Checks if the input is an Api.RelationSchema object. * @param potentialRelationSchema - The value to check. * @returns True if the value is a RelationSchema, false otherwise. */ function isRelationSchema(potentialRelationSchema) { return (isObject(potentialRelationSchema) && isString(potentialRelationSchema['label']) && Array.isArray(potentialRelationSchema['fields']) && potentialRelationSchema['fields'].every(isFieldSchema) && (isUndefined(potentialRelationSchema['relations']) || (Array.isArray(potentialRelationSchema['relations']) && potentialRelationSchema['relations'].every(isRelationSummary)))); } /** * Checks if the input is an Api.RelationSchema object or the string '__self'. * @param potentialRelationSchema - The value to check. * @returns True if the value is a RelationSchema or '__self', false otherwise. */ function isRelationSchemaOrSelf(potentialRelationSchemaOrSelf) { return isRelationSchema(potentialRelationSchemaOrSelf) || potentialRelationSchemaOrSelf === '__self'; } /** * Checks if the input is an Api.FieldValueType. * @param potentialFieldValueType - The value to check. * @returns True if the value is a FieldValueType, false otherwise. */ function isFieldValueType(potentialFieldValueType) { return Object.values(FieldValueTypes).includes(potentialFieldValueType); } /** * Checks if the input is an Api.Semantic value. * @param potentialSemantic - The value to check. * @returns True if the value is a Semantic, false otherwise. */ function isSemantic(potentialSemantic) { return Object.values(Semantics).includes(potentialSemantic); } /** * Checks if the input is an Api.FieldSchema object. * @param potentialFieldSchema - The value to check. * @returns True if the value is a FieldSchema, false otherwise. */ function isFieldSchema(potentialFieldSchema) { return (isObject(potentialFieldSchema) && isString(potentialFieldSchema['name']) && isString(potentialFieldSchema['label']) && isString(potentialFieldSchema['type']) && isFieldValueType(potentialFieldSchema['type']) && (isUndefined(potentialFieldSchema['semantic']) || isSemantic(potentialFieldSchema['semantic']))); } const referenceToStringLikeConfiguration = { field: { type: 'sourceField', defaults: [{ value: 'semantic:displayName' }], description: 'The field of the referenced entry to use for the string value.', required: true, }, }; const stringLikeToReferenceConfiguration = { field: { type: 'targetField', defaults: [{ value: 'semantic:displayName' }], description: 'The field against which to match the string value.', required: true, }, }; const dateToStringlikeConfiguration = { locale: { type: 'enum', defaults: [{ value: 'en-CA' }], values: ['en-CA'], description: 'The locale to use for formatting the date.', }, timezone: { type: 'enum', defaults: [{ value: 'UTC' }], values: ['UTC'], description: 'The timezone to use for formatting the date.', }, }; const htmlToRichTextConfiguration = { decoratorTemplate: { type: 'stringWithSourceItemFields', description: 'Template for the decorator. Use {author} and {date} placeholders.', defaults: [ { sourceRelationSemantic: RelationSemantics.COMMENTS, sourceFieldSemantic: Semantics.DESCRIPTION, targetRelationSemantic: RelationSemantics.COMMENTS, targetFieldSemantic: Semantics.DESCRIPTION, value: 'On <strong>{semantic:createdAt}</strong>, <em>{semantic:user.fields.semantic:displayName}</em> commented:', }, ], hidden: true, }, decoratorPrefix: { type: 'string', description: `Prefix for the decorator. Defaults to "➤".`, defaults: [ { sourceRelationSemantic: RelationSemantics.COMMENTS, sourceFieldSemantic: Semantics.DESCRIPTION, targetRelationSemantic: RelationSemantics.COMMENTS, targetFieldSemantic: Semantics.DESCRIPTION, value: '➤', }, ], hidden: true, }, }; const markdownToRichTextConfiguration = { decoratorTemplate: { type: 'stringWithSourceItemFields', description: 'Template for the decorator. Use {author} and {date} placeholders.', defaults: [ { sourceRelationSemantic: RelationSemantics.COMMENTS, sourceFieldSemantic: Semantics.DESCRIPTION, targetRelationSemantic: RelationSemantics.COMMENTS, targetFieldSemantic: Semantics.DESCRIPTION, value: 'On **{semantic:createdAt}**, __{semantic:user.fields.semantic:displayName}__ commented:', }, ], hidden: true, }, decoratorPrefix: { type: 'string', description: `Prefix for the decorator. Defaults to "➤".`, defaults: [ { sourceRelationSemantic: RelationSemantics.COMMENTS, sourceFieldSemantic: Semantics.DESCRIPTION, targetRelationSemantic: RelationSemantics.COMMENTS, targetFieldSemantic: Semantics.DESCRIPTION, value: '➤', }, ], hidden: true, }, }; const fieldTypeCompatibilityMatrix = { [FieldValueTypes.BLOB]: { [FieldValueTypes.BLOB]: {}, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: null, [FieldValueTypes.RICH_TEXT_MARKDOWN]: null, [FieldValueTypes.STRING]: null, [FieldValueTypes.URL]: null, }, [FieldValueTypes.BOOLEAN]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: {}, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.DATE]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: {}, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: {}, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: dateToStringlikeConfiguration, [FieldValueTypes.RICH_TEXT_MARKDOWN]: dateToStringlikeConfiguration, [FieldValueTypes.STRING]: dateToStringlikeConfiguration, [FieldValueTypes.URL]: null, }, [FieldValueTypes.DATE_RANGE]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: null, [FieldValueTypes.RICH_TEXT_MARKDOWN]: null, [FieldValueTypes.STRING]: null, [FieldValueTypes.URL]: null, }, [FieldValueTypes.DATETIME]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: {}, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: {}, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: dateToStringlikeConfiguration, [FieldValueTypes.RICH_TEXT_MARKDOWN]: dateToStringlikeConfiguration, [FieldValueTypes.STRING]: dateToStringlikeConfiguration, [FieldValueTypes.URL]: null, }, [FieldValueTypes.DATETIME_RANGE]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: null, [FieldValueTypes.RICH_TEXT_MARKDOWN]: null, [FieldValueTypes.STRING]: null, [FieldValueTypes.URL]: null, }, [FieldValueTypes.DURATION]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: {}, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.EMAIL]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: {}, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: stringLikeToReferenceConfiguration, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.INTEGER]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: {}, [FieldValueTypes.DATE]: {}, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: {}, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.NUMBER]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: {}, [FieldValueTypes.DATE]: {}, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: {}, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.OBJECT]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: null, [FieldValueTypes.RICH_TEXT_MARKDOWN]: null, [FieldValueTypes.STRING]: null, [FieldValueTypes.URL]: null, }, [FieldValueTypes.REFERENCE]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: referenceToStringLikeConfiguration, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: { sideIds: { type: 'sideIdOfSameCollection', description: 'If specified, only the references kept in sync in the specified link sides will be evaluated. An empty array means no side will be evaluated.', isArray: true, defaults: [{ value: [] }], }, type: { type: 'enum', description: 'The type of mapping, as selected by the customer.', hidden: true, isArray: false, values: ['manualOnly', 'referenceOnly'], required: true, }, }, [FieldValueTypes.RICH_TEXT_HTML]: referenceToStringLikeConfiguration, [FieldValueTypes.RICH_TEXT_MARKDOWN]: referenceToStringLikeConfiguration, [FieldValueTypes.STRING]: referenceToStringLikeConfiguration, [FieldValueTypes.URL]: null, }, [FieldValueTypes.RICH_TEXT_HTML]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: htmlToRichTextConfiguration, [FieldValueTypes.RICH_TEXT_MARKDOWN]: htmlToRichTextConfiguration, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.RICH_TEXT_MARKDOWN]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: markdownToRichTextConfiguration, [FieldValueTypes.RICH_TEXT_MARKDOWN]: markdownToRichTextConfiguration, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.STRING]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: {}, [FieldValueTypes.DATE]: {}, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: {}, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: {}, [FieldValueTypes.NUMBER]: {}, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: stringLikeToReferenceConfiguration, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: null, }, [FieldValueTypes.URL]: { [FieldValueTypes.BLOB]: null, [FieldValueTypes.BOOLEAN]: null, [FieldValueTypes.DATE]: null, [FieldValueTypes.DATE_RANGE]: null, [FieldValueTypes.DATETIME]: null, [FieldValueTypes.DATETIME_RANGE]: null, [FieldValueTypes.DURATION]: null, [FieldValueTypes.EMAIL]: null, [FieldValueTypes.INTEGER]: null, [FieldValueTypes.NUMBER]: null, [FieldValueTypes.OBJECT]: null, [FieldValueTypes.REFERENCE]: null, [FieldValueTypes.RICH_TEXT_HTML]: {}, [FieldValueTypes.RICH_TEXT_MARKDOWN]: {}, [FieldValueTypes.STRING]: {}, [FieldValueTypes.URL]: {}, }, }; /** * JSONPath parser that returns a relation that is guaranteed to have its schema populated. */ function findRelationByJSONPath(item, query) { const tokens = parseJSONPath(query); const schemas = []; let current = item; for (const token of tokens) { if (current === '__self') { const previousSchema = schemas[schemas.length - 1]; if (!previousSchema) { throw new Error(`Invalid use of __self`); } current = previousSchema; } const result = applyToken(current, token); if (isObject(result) && isRelationSchema(result['schema'])) { schemas.push(result['schema']); } current = result; } if (isRelation(current)) { return current; } if (isReferenceRelation(current) || isRelationSummary(current)) { const latestSchema = schemas[schemas.length - 1]; if (latestSchema === undefined) { throw new Error(`No schema found for relation ${current.label}`); } return { ...current, schema: latestSchema }; } 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 (!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 (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; } exports.FieldValueTypes = FieldValueTypes; exports.OperatorTypes = OperatorTypes; exports.RelationSemantics = RelationSemantics; exports.Semantics = Semantics; exports.StatusCodes = StatusCodes; exports.fieldTypeCompatibilityMatrix = fieldTypeCompatibilityMatrix; exports.findRelationByJSONPath = findRelationByJSONPath; exports.isFieldSchema = isFieldSchema; exports.isFieldValueType = isFieldValueType; exports.isItem = isItem; exports.isItemSummary = isItemSummary; exports.isObject = isObject; exports.isReferenceRelation = isReferenceRelation; exports.isRelation = isRelation; exports.isRelationSchema = isRelationSchema; exports.isRelationSchemaOrSelf = isRelationSchemaOrSelf; exports.isRelationSummary = isRelationSummary; exports.isSemantic = isSemantic; exports.isString = isString; exports.isUndefined = isUndefined;