UNPKG

ipsambeatae

Version:

Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH

213 lines (198 loc) 5.85 kB
import { gte } from 'semver'; import { ACCUMULATORS } from './accumulators'; import { BSON_TYPE_ALIASES } from './bson-type-aliases'; import { BSON_TYPES } from './bson-types'; import { CONVERSION_OPERATORS } from './conversion-operators'; import { EXPRESSION_OPERATORS } from './expression-operators'; import { JSON_SCHEMA } from './json-schema'; import { QUERY_OPERATORS } from './query-operators'; import { STAGE_OPERATORS } from './stage-operators'; import { SYSTEM_VARIABLES } from './system-variables'; const ALL_CONSTANTS = [ ...ACCUMULATORS, ...BSON_TYPES, ...BSON_TYPE_ALIASES, ...CONVERSION_OPERATORS, ...EXPRESSION_OPERATORS, ...JSON_SCHEMA, ...QUERY_OPERATORS, ...STAGE_OPERATORS, ...SYSTEM_VARIABLES, ]; const DEFAULT_SERVER_VERSION = '999.999.999'; export type Meta = | typeof ALL_CONSTANTS[number]['meta'] | 'field:identifier' | 'field:reference'; /** * Our completions are a mix of ace autocompleter types and some custom values * added on top, this interface provides a type definition for all required * properties that completer is using * * @internal */ export type Completion = { value: string; version: string; meta: Meta; description?: string; comment?: string; snippet?: string; score?: number; env?: string[]; namespace?: string[]; apiVersions?: number[]; outputStage?: boolean; fullScan?: boolean; firstStage?: boolean; geospatial?: boolean; }; export type StageFilterOptions = { env?: string | string[]; namespace?: string | string[]; apiVersion?: number | number[]; }; export type FilterOptions = { /** * Current server version (default is 999.999.999) */ serverVersion?: string; /** * Additional fields that are part of the document schema to add to * autocomplete as identifiers and identifier references */ fields?: (string | { name: string; description?: string })[]; /** * Filter completions by completion category */ meta?: (Meta | 'field:*' | 'accumulator:*' | 'expr:*')[]; /** * Stage-only filters */ stage?: StageFilterOptions; }; function matchesMeta(filter: string[], meta: string) { const metaParts = meta.split(':'); return filter.some((metaFilter) => { const filterParts = metaFilter.split(':'); return ( filterParts.length === metaParts.length && filterParts.every((part, index) => { return part === '*' || part === metaParts[index]; }) ); }); } function isIn<T extends string | number>( val: T | T[] | undefined, set: T[] | undefined ): boolean { // Do not filter when either value or match set is not provided if (typeof val === 'undefined' || typeof set === 'undefined') { return true; } // Otherwise check that value intersects with the match set val = Array.isArray(val) ? val : [val]; return val.some((v) => set.includes(v)); } export function createConstantFilter({ meta: filterMeta, serverVersion = DEFAULT_SERVER_VERSION, stage: filterStage = {}, }: Pick<FilterOptions, 'meta' | 'serverVersion' | 'stage'> = {}): ( completion: Completion ) => boolean { const currentServerVersion = /^(?<version>\d+?\.\d+?\.\d+?)/.exec(serverVersion)?.groups?.version ?? // Fallback to default server version if provided version doesn't match // regex so that semver doesn't throw when checking DEFAULT_SERVER_VERSION; return ({ version: minServerVersion, meta, env, namespace, apiVersions }) => { return ( gte(currentServerVersion, minServerVersion) && isIn(filterStage.env, env) && isIn(filterStage.namespace, namespace) && isIn(filterStage.apiVersion, apiVersions) && (!filterMeta || matchesMeta(filterMeta, meta)) ); }; } function isValidIdentifier(identifier: string) { // Quick check for common case first if (/[.\s"'()[\];={}:]/.test(identifier)) { return false; } try { // Everything else we check using eval as regex methods of checking are quite // hard to do (see https://mathiasbynens.be/notes/javascript-identifiers-es6) // eslint-disable-next-line @typescript-eslint/no-implied-eval new Function(`"use strict";let _ = { ${identifier}: 0 };`); return true; } catch { return false; } } /** * Helper method to conditionally wrap value if it's not a valid identifier */ export function wrapField(field: string, force = false): string { return force || !isValidIdentifier(field) ? `"${field.replace(/["\\]/g, '\\$&')}"` : field; } function normalizeField( field: string | { name: string; description?: string } ) { return typeof field === 'string' ? { value: field } : { value: field.name, description: field.description, }; } /** * Convenience method to filter list of mongodb constants based on constants * values * * @param options filter options * @param constants list of constants to filter, for testing purposes only * @returns filtered constants */ export function getFilteredCompletions( options: FilterOptions = {}, constants: Completion[] = ALL_CONSTANTS as Completion[] ): Completion[] { const { serverVersion = DEFAULT_SERVER_VERSION, fields = [], meta, stage, } = options; const completionsFilter = createConstantFilter({ serverVersion, meta, stage, }); const completionsWithFields = constants.concat( fields.flatMap((field) => { const { value, description } = normalizeField(field); return [ { value: value, meta: 'field:identifier', version: '0.0.0', description, }, { value: `$${value}`, meta: 'field:reference', version: '0.0.0', description, }, ]; }) ); return completionsWithFields.filter((completion) => { return completionsFilter(completion); }); }