// // Copyright 2024 DXOS.org // import { type QueryAST } from '@dxos/echo-protocol'; /** * Returns a human-readable string representation of a Filter AST. */ export const prettyFilter = (filter: QueryAST.Filter): string => { switch (filter.type) { case 'object': { // A type-less object filter with only a meta-key constraint is `Filter.key(...)`. if ( filter.typename === null && (filter.id === undefined || filter.id.length === 0) && Object.keys(filter.props).length === 0 && (filter.foreignKeys === undefined || filter.foreignKeys.length === 0) && filter.metaKey !== undefined ) { return filter.metaVersion !== undefined ? `Filter.key(${JSON.stringify(filter.metaKey)}, { version: ${JSON.stringify(filter.metaVersion)} })` : `Filter.key(${JSON.stringify(filter.metaKey)})`; } const parts: string[] = []; if (filter.typename !== null) { parts.push(String(filter.typename)); } if (filter.id !== undefined && filter.id.length > 0) { parts.push(`id: [${filter.id.join(', ')}]`); } const propEntries = Object.entries(filter.props); if (propEntries.length > 0) { const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', '); parts.push(`{ ${propsStr} }`); } if (filter.foreignKeys !== undefined && filter.foreignKeys.length > 0) { parts.push(`foreignKeys: [${filter.foreignKeys.map((fk) => JSON.stringify(fk)).join(', ')}]`); } if (filter.metaKey !== undefined) { parts.push( filter.metaVersion !== undefined ? `metaKey: ${JSON.stringify(filter.metaKey)} (${filter.metaVersion})` : `metaKey: ${JSON.stringify(filter.metaKey)}`, ); } return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()'; } case 'compare': return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`; case 'in': return `Filter.in([${filter.values.map((v) => JSON.stringify(v)).join(', ')}])`; case 'contains': return `Filter.contains(${JSON.stringify(filter.value)})`; case 'tag': return `Filter.tag(${JSON.stringify(filter.tag)})`; case 'range': return `Filter.range(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`; case 'text-search': return filter.searchKind ? `Filter.textSearch(${JSON.stringify(filter.text)}, ${JSON.stringify(filter.searchKind)})` : `Filter.textSearch(${JSON.stringify(filter.text)})`; case 'timestamp': return `Filter.${filter.field}.${filter.operator}(${filter.value})`; case 'child-of': return `Filter.childOf([${filter.parents.map((p) => JSON.stringify(p)).join(', ')}], { transitive: ${filter.transitive} })`; case 'not': return `Filter.not(${prettyFilter(filter.filter)})`; case 'and': return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`; case 'or': return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`; } }; /** * Returns a human-readable string representation of a Query AST. */ export const prettyQuery = (query: QueryAST.Query): string => { switch (query.type) { case 'select': return `Query.select(${prettyFilter(query.filter)})`; case 'filter': return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`; case 'reference-traversal': return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`; case 'incoming-references': { const args: string[] = []; if (query.typename !== null) { args.push(String(query.typename)); } if (query.property !== null) { args.push(JSON.stringify(query.property)); } return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`; } case 'relation': { const method = query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf'; const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : ''; return `${prettyQuery(query.anchor)}.${method}(${filterStr})`; } case 'relation-traversal': return `${prettyQuery(query.anchor)}.${query.direction}()`; case 'hierarchy-traversal': return query.direction === 'to-parent' ? `${prettyQuery(query.anchor)}.parent()` : `${prettyQuery(query.anchor)}.children()`; case 'union': return `Query.all(${query.queries.map(prettyQuery).join(', ')})`; case 'set-difference': return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`; case 'order': { const orders = query.order.map((o) => { if (o.kind === 'natural') { return 'Order.natural()'; } else if (o.kind === 'rank') { return `Order.rank(${JSON.stringify(o.direction)})`; } else if (o.kind === 'timestamp') { const fn = o.field === 'updatedAt' ? 'updated' : 'created'; return `Order.${fn}(${JSON.stringify(o.direction)})`; } else { return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`; } }); return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`; } case 'options': { const opts = query.options; const parts: string[] = []; if (opts.deleted !== undefined) { parts.push(`deleted: ${JSON.stringify(opts.deleted)}`); } if (opts.debugLabel !== undefined) { parts.push(`debugLabel: ${JSON.stringify(opts.debugLabel)}`); } return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`; } case 'from': { if (query.from._tag === 'scope') { if (query.from.scopes.length === 0) { return `${prettyQuery(query.query)}.from('all-accessible-spaces')`; } const scopeStrs = query.from.scopes.map((scope) => { if (scope._tag === 'space') { return scope.includeAllFeeds ? `{ space: ${JSON.stringify(scope.spaceId)}, includeAllFeeds: true }` : `{ space: ${JSON.stringify(scope.spaceId)} }`; } if (scope._tag === 'feed') { return `{ feed: ${JSON.stringify(String(scope.feedUri))} }`; } return `{ registry: ${JSON.stringify(scope.location)} }`; }); return `${prettyQuery(query.query)}.from([${scopeStrs.join(', ')}])`; } return `${prettyQuery(query.query)}.from(${prettyQuery(query.from.query)})`; } case 'limit': return `${prettyQuery(query.query)}.limit(${query.limit})`; } };