diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..008cf57 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is this? + +TypeScript implementation of [sgraph](https://github.com/softagram/sgraph) — a library for parsing, manipulating, and converting software dependency graph models stored in XML format. Used as an npm package (`sgraph.js`). + +## Commands + +```bash +npm test # Run all tests (Jest + ts-jest) +npx jest test/sgraph/sgraph.test.ts # Run a single test file +npx jest --testNamePattern "finds dir" # Run tests matching a name pattern +npm run compile # Build TypeScript → dist/ (rimraf + tsc) +``` + +CI uses `yarn install` and `yarn test` on Node 16.x and 18.x. + +## Architecture + +The core data model is a tree of `SElement` nodes connected by `SElementAssociation` edges: + +- **SElement** (`src/selement/selement.ts`) — Tree node with `name`, `parent`, `children[]`, `childrenObject{}` (name-keyed lookup), `outgoing[]`/`incoming[]` associations, and key-value `attrs`. Uses `object-hash` for identity. Slashes in names are encoded as `__slash__`. Constructor auto-attaches to parent. +- **SElementAssociation** (`src/selement/selementAssociation.ts`) — Directed edge between two SElements with a `deptype` (e.g. `"function_ref"`) and attributes. `initElems()` must be called to wire into both elements' outgoing/incoming arrays. `createUniqueElementAssociation` prevents duplicates. +- **SGraph** (`src/sgraph/sgraph.ts`) — Wraps a root SElement. Provides `parseXml()` (in-memory string) and `parseXmlFileOrZippedXml()` (file/zip on disk). `toXml()` serializes back. `createOrGetElementFromPath()` builds tree paths on demand. +- **SGraphXMLParser** (`src/sgraph/sgraphXmlParser.ts`) — SAX-based streaming parser. XML uses `` for elements (with `n=name`, `t=type`, `i=id`), `` for references (with `R=target-id`, `T=type`), `` for attributes. After parsing, `translateReferences()` resolves numeric ids to SElement objects. +- **ModelApi** (`src/modelapi.ts`) — Higher-level API over SGraph: `getElementsByName`, `getCalledFunctions`/`getCallingFunctions`, `filterModel` (subgraph extraction with three modes: Ignore, Direct, DirectAndIndirect), `getCyclicDependencyCycles`. +- **ModelLoader** (`src/loaders/modelLoader.ts`) — Loads model XML and optionally associated CSV attribute files from a Softagram analysis output directory structure. +- **Converters** (`src/converters/`) — `sgraphToEcharts` (graph visualization data) and `sgraphToDot` (Graphviz DOT format). + +### XML model format + +```xml + + + + + + ... + + + +``` + +Zipped models contain `modelfile.xml` inside the zip archive. + +### Browser vs Node + +`src/utils/browser.ts` detects the runtime. `SGraph.parseXmlFileOrZippedXml` and `ModelLoader` use `eval('require')` for Node-only modules (`adm-zip`, `fs/promises`) to avoid bundler issues. ModelLoader throws in browser context. + +## Key patterns + +- SElement constructor with a parent automatically adds itself to that parent's children. Creating duplicate child names throws. +- Associations need `initElems()` after construction to register in both elements' arrays — except when using the static `createUniqueElementAssociation`. +- Test fixtures use `test/modelfile.xml` and `test/modelfile.xml.zip`. diff --git a/PYTHON_MAPPING.md b/PYTHON_MAPPING.md new file mode 100644 index 0000000..35d7242 --- /dev/null +++ b/PYTHON_MAPPING.md @@ -0,0 +1,345 @@ +# Python sgraph ↔ TypeScript sgraph.js Mapping + +This document maps the Python `sgraph` library (`../sgraph/src/sgraph/`) to the TypeScript `sgraph.js` implementation, to keep the two in sync. + +## Naming conventions + +| Python | TypeScript | +|--------|-----------| +| `snake_case` methods | `camelCase` methods | +| `childrenDict` | `childrenObject` | +| `attrs` (direct) | `attrs` (private, via `getAttributes()`/`setAttributes()`) | +| `human_readable_name` | `humanReadableName` | +| `SElementAssociation` | `SElementAssociation` | + +--- + +## SElement + +**Source:** Python `selement.py` → TypeScript `src/selement/selement.ts` + +### Constructor & Children + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `__init__(parent, name)` | `constructor(name, parent?)` | Arg order differs | +| `addChild(child)` | `addChild(child)` | | +| `detachChild(child)` | `detachChild(child)` | | +| `addChildIgnoreWithSameName(child, elem)` | — | Not ported | + +### Attributes + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `addAttribute(name, value)` | `addAttribute(name, value)` | | +| `setType(t)` | `setType(t)` | | +| `getType()` | `getType()` | | +| `hasType()` | `hasType()` | | +| `typeEquals(t)` | `typeEquals(t)` | | +| `getAttributes()` (via `.attrs`) | `getAttributes()` | | +| `setAttributes(attrs)` (via `.attrs =`) | `setAttributes(attrs)` | | +| `createAttributesFrom(attrs)` | — | Not ported (batch set, use `setAttributes`) | +| `equalsAttributes(e)` | — | Not ported | +| `cumulateAttribute(name, value)` | — | Not ported (numeric accumulation) | +| `cumulateListAttribute(name, value, avoid_dup)` | — | Not ported | +| `cumulateIntAttribute(name, value)` | — | Not ported | + +### Hierarchy navigation + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `getPath()` | `getPath()` | | +| `getPathAsList()` | `getPathAsList()` | | +| `getLevel()` | `getLevel()` | | +| `getRoot()` | `getRoot()` | | +| `getAncestors()` | `getAncestors()` | | +| `getAncestorOfType(t)` | `getAncestorOfType(type)` | | +| `getAncestorOfTypes(types)` | `getAncestorOfTypes(types)` | Accepts `string[]` or `Set` | +| `getAncestorOfLevel(level)` | `getAncestorOfLevel(level)` | | +| `isDescendantOf(ancestor)` | `isDescendantOf(ancestor)` | | +| `getChildByName(name)` | `getChildByName(name)` | | +| `findElement(path)` | `findElement(name)` | | +| `createOrGetElement(path)` | `createOrGetElement(n)` | | +| `create_or_get_element(path)` | — | Use `createOrGetElement` (no `isNew` variant on SElement) | +| `get_ancestor_names_list()` | — | Not ported (use `getAncestors().map(a => a.name)`) | +| `getElementsByNameOnLevel(name, level, cur)` | — | Not ported | + +### Traversal & descendants + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `traverseElements(visit)` | `traverseElements(visit)` | | +| `getDescendants(list)` | `getDescendants()` | TS returns new array instead of mutating | +| `getNodeCount()` | `getNodeCount()` | | +| `getMaxDepth(cur_depth)` | `getMaxDepth(currentDepth)` | | +| `traverseIncoming(visited)` | — | Not ported | +| `getAllUsers(outside_level)` | — | Not ported | +| `recurseIncomingDependencies(...)` | — | Not ported | +| `hasSiblingsRecursive()` | — | Not ported | +| `getNextSiblingRecursive()` | — | Not ported | + +### Association queries + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `getEACount()` | `getEACount()` | | +| `getEATypes(theSet)` | `getEATypes(typeSet)` | | +| `getEATypeCounts(d)` | `getEATypeCounts(counts)` | | +| `getCyclicDependencies()` | `getCyclicDependencies()` | | + +### Mutation + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `merge(other, ignore_type, ignore_attrs)` | `merge(other, ignoreType?, ignoreAttrs?)` | | +| `remove(leaveParentUntouched)` | `remove(leaveParentUntouched?)` | | +| `rename(new_name)` | `rename(newName)` | | +| `removeDescendantsIf(checker)` | `removeDescendantsIf(checker)` | | +| `removeElements(path)` | — | Not ported (use `findElement` + `remove`) | +| `removeElementsWithList(splitted)` | — | Not ported | +| `update_children_dict()` | — | Not ported | +| `clean_duplicate_associations()` | — | Not ported | + +### Element creation + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `createElementChain(elemid)` | `createElementChain(id)` | | +| `createElements(elems, startFrom)` | `createElements(elements, startFrom)` | | + +### Identity & comparison + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `__eq__` (identity) | `equals(other)` | TS uses hash comparison | +| `getHash()` (not in Python) | `getHash()` | TS-specific, uses `object-hash` | +| `updateHash()` (not in Python) | `updateHash()` | TS-specific | +| `sibling_with(elem)` | — | Not ported | +| `elem_location_matches(elem)` | — | Not ported | +| `isExternalElement()` | — | Not ported | +| `enclosingFilenameEndswith(postfix)` | — | Not ported | +| `verify(elems, i)` | — | Not ported | + +--- + +## SElementAssociation + +**Source:** Python `selementassociation.py` → TypeScript `src/selement/selementAssociation.ts` + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `__init__(from, to, deptype, depattrs)` | `constructor(from, to, deptype?, depattrs?)` | | +| `create_unique_element_association(...)` | `createUniqueElementAssociation(...)` | Static method | +| `initElems()` | `initElems()` | | +| `remove()` | `remove()` | | +| `getFromPath()` | `getFromPath()` | | +| `getToPath()` | `getToPath()` | | +| `getType()` | `getType()` | | +| `getAttributes()` | `getAttributes()` | | +| `addAttribute(name, value)` | `addAttribute(name, value)` | | +| `setAttrMap(attrmap)` | `setAttrs(a)` | | +| `get_dependency_length()` | `getDependencyLength()` | | +| `calculateCompareStatus()` | `calculateCompareStatus()` | | +| `check_attr(attr, val)` | — | Not ported | +| `getHashNum()` | — | Not ported | +| `initOrExtendListAttribute(a, v)` | — | Not ported | +| `match_ea_from_other_sgraph(ea, ea_list)` | — | Not ported | + +--- + +## SGraph + +**Source:** Python `sgraph.py` → TypeScript `src/sgraph/sgraph.ts` + +### Parsing & loading + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `parse_xml(filename_or_stream, ...)` | `parseXml({data, ...})` | TS takes string data | +| `parse_xml_or_zipped_xml(path, ...)` | `parseXmlFileOrZippedXml({filePath, ...})` | Async, Node-only | +| `parse_xml_string(xml_string, ...)` | `parseXml({data})` | Same as parseXml in TS | +| `parse_xml_file_or_stream(stream, ...)` | — | Not ported (use parseXml with data) | +| `parse_deps(filename)` | — | Not ported (deps format) | +| `parse_deps_lines(content)` | — | Not ported (deps format) | + +### Element access + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `findElementFromPath(path)` | `findElementFromPath(path)` | | +| `createOrGetElementFromPath(path)` | `createOrGetElementFromPath(path)` | | +| `createOrGetElement(elem)` | `createOrGetElement(elem)` | | +| `create_or_get_element(elem)` | `createOrGetElementWithNew(element)` | Returns `{element, isNew}` | +| `getElement(elem)` | — | Not ported | + +### Serialization + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `to_xml(fname, stdout)` | `toXml()` | Returns string (no file write) | +| `to_deps(fname)` | — | Not ported (deps format) | +| `to_plantuml(fname)` | — | Not ported | +| `save(filename)` | — | Not ported | +| `produce_deps_tuples()` | — | Not ported | + +### Analysis + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `getDepth()` | `getDepth()` | | +| `calculate_model_stats()` | `calculateModelStats()` | Returns `{dependenciesCount, nodesCount, depTypeCounts, depToElemRatio}` | +| `verify(i)` | — | Not ported | +| `traverse(traverser)` | — | Use `rootNode.traverseElements()` | +| `groupea(eas)` | — | Internal use via `sgraph-utils.ts` | + +### Converters (via methods) + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| — | `toEcharts()` | TS-specific | + +### Metadata + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `modelAttrs` | `modelAttrs` | | +| `set_model_path(filepath)` | `setModelPath(filePath)` | | +| `metaAttrs` | — | Not ported | +| `setMetaAttrs(m)` | — | Not ported | +| `setModelAttrs(m)` | — | Not ported | +| `addPropagateAction(a, v)` | — | Not ported | +| `propagateActions` | — | Not ported | +| `totalModel` | — | Not ported | +| `__deepcopy__(memo)` | — | Not ported | +| `copy_ea_attrs_from_other_models(...)` | — | Not ported | +| `recurse_three_models(...)` | — | Not ported | +| `calculate_model_stats_delta(...)` | — | Not ported | + +--- + +## ModelApi + +**Source:** Python `modelapi.py` → TypeScript `src/modelapi.ts` + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| constructor (model or data) | `constructor({data} \| {model})` | | +| `getElementByPath(filepath)` | `getElementByPath(path)` | | +| `getElementsByName(name)` | `getElementsByName(name)` | | +| `filter(filterfunc)` | `filter(filterFunc)` | | +| `getChildrenByType(element, elemType)` | `getChildrenByType(element, elemType)` | | +| `getCalledFunctions(funcElem)` | `getCalledFunctions(element)` | | +| `getCallingFunctions(funcElem)` | `getCallingFunctions(element)` | | +| `getUsedElements(elem)` | `getUsedElements(element)` | | +| `getUserElements(elem)` | `getUserElements(element)` | | +| `filter_model(src_elem, src_graph, ...)` | `filterModel(sourceElement, sourceGraph, ...)` | | +| `create_descendants(related, new_elem, ...)` | `createDescendants(related, newOrExisting)` | | +| `getCyclicDependencyCycles()` | `getCyclicDependencyCycles()` | | +| `query_dependencies(...)` | — | Not ported | +| `query_dependencies_between(...)` | — | Not ported | +| `add_if_matches(...)` | — | Not ported | +| `matches_with_descendant(...)` | — | Not ported | +| `intra_file(ea)` | — | Not ported | +| `not_a_sibling_ref(ea)` | — | Not ported | + +### FilterAssociations enum + +| Python | TypeScript | +|--------|-----------| +| `FilterAssocations.Ignore` | `FilterAssociations.Ignore` | +| `FilterAssocations.Direct` | `FilterAssociations.Direct` | +| `FilterAssocations.DirectAndIndirect` | `FilterAssociations.DirectAndIndirect` | + +--- + +## Converters + +**Source:** Python `converters/` → TypeScript `src/converters/` + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `xml_to_dot.graph_to_dot(g)` | `sgraphToDot(graph)` | | +| — | `sgraphToEcharts(sg)` | TS-specific | +| `sgraph_to_cytoscape.graph_to_cyto(g)` | — | Not ported | +| `xml_to_json.XmlToNodesAndEdges` | — | Not ported | +| `xml_to_graphml` | — | Not ported | +| `xml_to_plantuml` | — | Not ported | +| `xml_to_deps` | — | Not ported | +| `xml_to_3dforcegraph` | — | Not ported | +| `xml_to_hierarchical_json` | — | Not ported | +| `graphml.sgraph_to_graphml_file(...)` | — | Not ported | +| `sbom_cyclonedx_generator.generate_from_sgraph(...)` | — | Not ported | + +--- + +## Loaders + +**Source:** Python `loader/` → TypeScript `src/loaders/` + +| Python | TypeScript | Notes | +|--------|-----------|-------| +| `ModelLoader.load_model(...)` | `ModelLoader.load(...)` | | +| `AttributeLoader.load_attrfile(...)` | `AttributeLoader.loadAttrFile(...)` | | +| `AttributeLoader.load_all_files(...)` | `AttributeLoader.loadAllFiles(...)` | | + +--- + +## Modules only in Python (not ported) + +| Module | Purpose | Porting notes | +|--------|---------|---------------| +| `algorithms/pagerank.py` | PageRank node importance | Separate algorithm, add when needed | +| `algorithms/generalizer.py` | Model abstraction to higher levels | Separate algorithm | +| `algorithms/sgraphanalysis.py` | Dynamic dep generation, flattening | Separate algorithm | +| `algorithms/sgraphfiltering.py` | Remove deps by type/path | Separate algorithm | +| `algorithms/sgraphmetrics.py` | Association density calculation | Separate algorithm | +| `compare/modelcompare.py` | Model version comparison/delta | Separate module | +| ~~`cypher.py`~~ | ~~Cypher query interface~~ | **Ported** — see Cypher section below | +| `graphdataservice.py` | High-level graph data extraction | Server-side utility | +| `metricsapi.py` | Metrics queries (LOC, tech debt) | Separate concern | +| `definitions.py` | `HaveAttributes` enum | Port when `filterModel` needs it | +| `exceptions.py` | `ModelNotFoundException` | Port when needed | +| `analyzers/` | Analyzer framework | Separate concern | + +--- + +## Cypher Query Support + +**Source:** Python `cypher.py` → TypeScript `src/cypher/` + +The TypeScript implementation uses a hand-written parser+executor instead of Python's sPyCy dependency. + +| Python (`cypher.py`) | TypeScript (`src/cypher/`) | Notes | +|---|---|---| +| `SGraphCypherBackend` | `CypherGraph` in `graph.ts` | Same 3-pass indexing | +| `cypher_query(model, query, include_hierarchy)` | `cypherQuery(model, query, {includeHierarchy})` | | +| `SGraphCypherExecutor` (sPyCy) | `CypherExecutor` in `executor.ts` | Hand-written Cypher subset | +| sPyCy parser | `parse()` in `parser.ts` | Recursive descent | +| Result: `pd.DataFrame` | Result: `CypherResult {columns, rows}` | | +| CLI: `python -m sgraph.cypher` | CLI: `node dist/cypher/cli.js` | | +| `_extract_subgraph()` | — | Not ported | +| Graph output formats (xml, dot, plantuml, graphml, cytoscape) | — | Not ported (use existing converters) | +| Full openCypher via sPyCy | Subset: MATCH, WHERE, RETURN, ORDER BY, LIMIT | No OPTIONAL MATCH, UNION, WITH, variable-length paths | + +### Supported Cypher subset (TypeScript) + +- MATCH with node patterns (labels, inline properties) +- MATCH with relationship patterns (directed, undirected, typed) +- WHERE: `=`, `<>`, `<`, `>`, `<=`, `>=`, `CONTAINS`, `STARTS WITH`, `ENDS WITH`, `AND`, `OR`, `NOT` +- RETURN with property access, variables, aliases, DISTINCT +- Functions: `COUNT()`, `COLLECT()`, `type()` +- ORDER BY (ASC/DESC), LIMIT + +--- + +## TypeScript-only features + +| Feature | Location | Notes | +|---------|----------|-------| +| `SElement.getHash()` / `updateHash()` | `selement.ts` | Hash-based identity using `object-hash` | +| `SElement.humanReadableName` | `selement.ts` | Used in hash | +| `SGraph.toEcharts()` | `sgraph.ts` | ECharts visualization converter | +| `sgraphToEcharts()` | `converters/sgraphToEcharts.ts` | ECharts format output | +| `isBrowser` detection | `utils/browser.ts` | Browser/Node runtime detection | +| `SGraphXMLParser.onlyRoot` | `sgraphXmlParser.ts` | Abort after root element | +| `lowestCommonAncestor()` | `selementUtils.ts` | Exists in Python as `algorithms/selementutils.py` | diff --git a/dist/converters/sgraphToDot.d.ts b/dist/converters/sgraphToDot.d.ts index 814d3d8..bce64ee 100644 --- a/dist/converters/sgraphToDot.d.ts +++ b/dist/converters/sgraphToDot.d.ts @@ -1,3 +1,9 @@ import { SGraph } from '../sgraph'; +/** + * Converts SGraph to DOT format used by Graphviz. + * @see https://graphviz.org/doc/info/lang.html + * @param graph SGraph + * @returns DOT string + */ declare const sgraphToDot: (graph: SGraph) => string; export { sgraphToDot }; diff --git a/dist/converters/sgraphToDot.js b/dist/converters/sgraphToDot.js index 4abdf06..3b4c433 100644 --- a/dist/converters/sgraphToDot.js +++ b/dist/converters/sgraphToDot.js @@ -2,6 +2,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.sgraphToDot = void 0; const tab = '\t'; +/** + * Converts SGraph to DOT format used by Graphviz. + * @see https://graphviz.org/doc/info/lang.html + * @param graph SGraph + * @returns DOT string + */ const sgraphToDot = (graph) => { const dot = []; const deps = []; diff --git a/dist/cypher/cli.d.ts b/dist/cypher/cli.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/cypher/cli.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/cypher/cli.js b/dist/cypher/cli.js new file mode 100644 index 0000000..9167d3d --- /dev/null +++ b/dist/cypher/cli.js @@ -0,0 +1,171 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const sgraph_1 = require("../sgraph"); +const graph_1 = require("./graph"); +const executor_1 = require("./executor"); +const parser_1 = require("./parser"); +function printUsage() { + console.error('Usage: node cli.js [query] [--format table|json|csv] [--no-hierarchy]'); + console.error(''); + console.error(' model-file Path to .xml or .xml.zip sgraph model'); + console.error(' query Cypher query to execute (omit for REPL mode)'); + console.error(' --format Output format: table (default), json, csv'); + console.error(' --no-hierarchy Exclude CONTAINS hierarchy edges'); +} +function parseArgs(argv) { + let format = 'table'; + let includeHierarchy = true; + const positional = []; + let i = 0; + while (i < argv.length) { + const arg = argv[i]; + if (arg === '--format' && i + 1 < argv.length) { + format = argv[i + 1]; + i += 2; + } + else if (arg === '--no-hierarchy') { + includeHierarchy = false; + i++; + } + else if (arg === '--help' || arg === '-h') { + printUsage(); + process.exit(0); + } + else { + positional.push(arg); + i++; + } + } + return { + modelPath: positional[0], + query: positional[1], + format, + includeHierarchy, + }; +} +function printResult(result, format) { + if (result.rows.length === 0) { + if (format === 'table') + console.log('(no results)'); + else if (format === 'json') + console.log('[]'); + return; + } + if (format === 'json') { + console.log(JSON.stringify(result.rows, null, 2)); + } + else if (format === 'csv') { + console.log(result.columns.join(',')); + for (const row of result.rows) { + console.log(result.columns + .map((c) => { + const v = row[c]; + const s = typeof v === 'object' && v !== null + ? JSON.stringify(v) + : String(v !== null && v !== void 0 ? v : ''); + return s.includes(',') ? `"${s}"` : s; + }) + .join(',')); + } + } + else { + // table format -- calculate column widths and pad + const cols = result.columns; + const widths = cols.map((c) => c.length); + const stringRows = result.rows.map((row) => cols.map((c, i) => { + const v = row[c]; + const s = typeof v === 'object' && v !== null + ? JSON.stringify(v) + : String(v !== null && v !== void 0 ? v : ''); + widths[i] = Math.max(widths[i], s.length); + return s; + })); + // Header + console.log(cols.map((c, i) => c.padEnd(widths[i])).join(' ')); + console.log(cols.map((_, i) => '-'.repeat(widths[i])).join(' ')); + // Rows + for (const row of stringRows) { + console.log(row.map((v, i) => v.padEnd(widths[i])).join(' ')); + } + } +} +function runRepl(executor, format) { + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + console.error('Enter Cypher queries. End with ; or blank line. Type "quit" to exit.'); + let lines = []; + const prompt = () => rl.question(lines.length === 0 ? 'cypher> ' : ' > ', handleLine); + function handleLine(line) { + const trimmed = line.trim(); + if (trimmed === 'quit' || trimmed === 'exit') { + rl.close(); + return; + } + lines.push(line); + if (trimmed.endsWith(';') || trimmed === '') { + let queryStr = lines.join(' ').trim(); + if (queryStr.endsWith(';')) + queryStr = queryStr.slice(0, -1).trim(); + lines = []; + if (queryStr) { + try { + const t0 = Date.now(); + const ast = (0, parser_1.parse)(queryStr); + const result = executor.execute(ast); + printResult(result, format); + console.error(`(${result.rows.length} rows, ${Date.now() - t0}ms)`); + } + catch (e) { + console.error('Error:', e.message); + } + } + } + prompt(); + } + prompt(); +} +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.modelPath) { + printUsage(); + process.exit(1); + } + const t0 = Date.now(); + const sgraph = await sgraph_1.SGraph.parseXmlFileOrZippedXml({ + filePath: args.modelPath, + }); + if (!sgraph) { + console.error(`Error: Could not load model from '${args.modelPath}'`); + process.exit(1); + } + const graph = new graph_1.CypherGraph(sgraph.rootNode, args.includeHierarchy); + const loadTime = Date.now() - t0; + console.error(`Loaded ${graph.nodeCount} nodes, ${graph.edgeCount} edges (${loadTime}ms)`); + const executor = new executor_1.CypherExecutor(graph); + if (args.query) { + // Single query mode + try { + const ast = (0, parser_1.parse)(args.query); + const result = executor.execute(ast); + printResult(result, args.format); + } + catch (e) { + if (e instanceof parser_1.CypherSyntaxError) { + console.error('Syntax error:', e.message); + process.exit(1); + } + throw e; + } + } + else { + // REPL mode + runRepl(executor, args.format); + } +} +main().catch((err) => { + console.error('Fatal error:', err.message || err); + process.exit(1); +}); diff --git a/dist/cypher/executor.d.ts b/dist/cypher/executor.d.ts new file mode 100644 index 0000000..ebd2024 --- /dev/null +++ b/dist/cypher/executor.d.ts @@ -0,0 +1,27 @@ +import { CypherGraph } from './graph'; +import { CypherQuery } from './parser'; +export interface CypherResult { + columns: string[]; + rows: Record[]; +} +export declare class CypherExecutor { + private graph; + constructor(graph: CypherGraph); + execute(query: CypherQuery): CypherResult; + private matchPattern; + private nodeMatchesPattern; + private expandBindings; + private getDirectedEdges; + private getTargetNode; + private evaluateWhere; + private evaluateExpr; + private resolveBindingEntry; + private compareValues; + private compareScalar; + private projectReturn; + private isAggregation; + private projectWithAggregation; + private getColumnName; + private exprColumnName; + private resolveReturnValue; +} diff --git a/dist/cypher/executor.js b/dist/cypher/executor.js new file mode 100644 index 0000000..73c19c4 --- /dev/null +++ b/dist/cypher/executor.js @@ -0,0 +1,463 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CypherExecutor = void 0; +class CypherExecutor { + constructor(graph) { + this.graph = graph; + } + execute(query) { + // 1. Pattern matching + let bindings = this.matchPattern(query.matchClause.pattern); + // 2. WHERE filtering + if (query.whereClause) { + bindings = bindings.filter((binding) => this.evaluateWhere(query.whereClause, binding)); + } + // 3. RETURN projection (with aggregation detection) + let { columns, rows } = this.projectReturn(query.returnClause.items, bindings); + // 5. DISTINCT + if (query.returnClause.distinct) { + const seen = new Set(); + rows = rows.filter((row) => { + const key = JSON.stringify(row); + if (seen.has(key)) + return false; + seen.add(key); + return true; + }); + } + // 6. ORDER BY + if (query.orderByClause) { + const orderItems = query.orderByClause.items; + rows.sort((a, b) => { + for (const item of orderItems) { + const colName = this.exprColumnName(item.expression); + const valA = a[colName]; + const valB = b[colName]; + let cmp = 0; + if (valA < valB) + cmp = -1; + else if (valA > valB) + cmp = 1; + if (cmp !== 0) + return item.descending ? -cmp : cmp; + } + return 0; + }); + } + // 7. LIMIT + if (query.limit !== undefined) { + rows = rows.slice(0, query.limit); + } + return { columns, rows }; + } + // --- 1. Pattern Matching --- + matchPattern(pattern) { + if (pattern.length === 0) + return []; + const firstNode = pattern[0]; + let bindings = []; + // Scan all nodes for the first node pattern + for (const node of this.graph.getAllNodes()) { + if (this.nodeMatchesPattern(node, firstNode)) { + const binding = new Map(); + if (firstNode.variable) { + binding.set(firstNode.variable, { kind: 'node', id: node.id }); + } + bindings.push(binding); + } + } + // Process subsequent relationship-node pairs + for (let i = 1; i < pattern.length; i += 2) { + const relPattern = pattern[i]; + const nextNodePattern = pattern[i + 1]; + bindings = this.expandBindings(bindings, relPattern, nextNodePattern, pattern[i - 1]); + } + return bindings; + } + nodeMatchesPattern(node, pattern) { + // Check labels: every label in pattern must be in node's labels + for (const label of pattern.labels) { + if (!node.labels.has(label)) + return false; + } + // Check inline properties + if (pattern.properties) { + for (const [key, value] of Object.entries(pattern.properties)) { + if (node.properties[key] !== value) + return false; + } + } + return true; + } + expandBindings(bindings, relPattern, nextNodePattern, prevNodePattern) { + const expanded = []; + for (const binding of bindings) { + // Get the previously bound node + const prevVarName = prevNodePattern.variable; + if (!prevVarName) + continue; + const prevEntry = binding.get(prevVarName); + if (!prevEntry || prevEntry.kind !== 'node') + continue; + const prevNodeId = prevEntry.id; + // Get edges based on direction + const edgeIds = this.getDirectedEdges(prevNodeId, relPattern.direction); + for (const edgeId of edgeIds) { + const edge = this.graph.getEdge(edgeId); + // Check type filter + if (relPattern.types.length > 0) { + if (!relPattern.types.includes(edge.type)) + continue; + } + // Get the other end of the edge + const targetNodeId = this.getTargetNode(edgeId, prevNodeId, relPattern.direction); + const targetNode = this.graph.getNode(targetNodeId); + // Check target node pattern + if (!this.nodeMatchesPattern(targetNode, nextNodePattern)) + continue; + // Create expanded binding + const newBinding = new Map(binding); + if (relPattern.variable) { + newBinding.set(relPattern.variable, { kind: 'edge', id: edgeId }); + } + if (nextNodePattern.variable) { + newBinding.set(nextNodePattern.variable, { + kind: 'node', + id: targetNodeId, + }); + } + expanded.push(newBinding); + } + } + return expanded; + } + getDirectedEdges(nodeId, direction) { + if (direction === 'out') + return this.graph.getOutEdges(nodeId); + if (direction === 'in') + return this.graph.getInEdges(nodeId); + // both: combine out + in + return [ + ...this.graph.getOutEdges(nodeId), + ...this.graph.getInEdges(nodeId), + ]; + } + getTargetNode(edgeId, fromNodeId, direction) { + if (direction === 'out') + return this.graph.getEdgeDst(edgeId); + if (direction === 'in') + return this.graph.getEdgeSrc(edgeId); + // both: whichever end is not fromNodeId + const src = this.graph.getEdgeSrc(edgeId); + const dst = this.graph.getEdgeDst(edgeId); + return src === fromNodeId ? dst : src; + } + // --- 2. WHERE Evaluation --- + evaluateWhere(expr, binding) { + const result = this.evaluateExpr(expr, binding); + return !!result; + } + evaluateExpr(expr, binding) { + switch (expr.kind) { + case 'literal': + return expr.value; + case 'variable': { + const varRef = expr; + const entry = binding.get(varRef.name); + if (!entry) + return undefined; + return entry.id; + } + case 'propertyAccess': { + const pa = expr; + const entry = binding.get(pa.variable); + if (!entry) + return undefined; + if (entry.kind === 'node') { + return this.graph.getNode(entry.id).properties[pa.property]; + } + else { + return this.graph.getEdge(entry.id).properties[pa.property]; + } + } + case 'functionCall': { + const fc = expr; + if (fc.name === 'TYPE') { + const argEntry = this.resolveBindingEntry(fc.argument, binding); + if (argEntry && argEntry.kind === 'edge') { + return this.graph.getEdge(argEntry.id).type; + } + return undefined; + } + if (fc.name === 'COUNT' || fc.name === 'COLLECT') { + // Aggregation functions are handled in projectReturn + return undefined; + } + return undefined; + } + case 'comparison': { + const cmp = expr; + const left = this.evaluateExpr(cmp.left, binding); + const right = this.evaluateExpr(cmp.right, binding); + return this.compareValues(left, right, cmp.operator); + } + case 'logical': { + const logical = expr; + if (logical.op === 'AND') { + return (this.evaluateWhere(logical.left, binding) && + this.evaluateWhere(logical.right, binding)); + } + else { + return (this.evaluateWhere(logical.left, binding) || + this.evaluateWhere(logical.right, binding)); + } + } + case 'not': { + const notExpr = expr; + return !this.evaluateWhere(notExpr.expr, binding); + } + default: + return undefined; + } + } + resolveBindingEntry(expr, binding) { + if (expr.kind === 'variable') { + return binding.get(expr.name); + } + return undefined; + } + compareValues(left, right, operator) { + // Handle array-valued properties + if (Array.isArray(left)) { + if (operator === '=') + return left.includes(right); + if (operator === '<>') + return !left.includes(right); + // For other operators on arrays, check if ANY element matches + return left.some((item) => this.compareScalar(item, right, operator)); + } + return this.compareScalar(left, right, operator); + } + compareScalar(left, right, operator) { + switch (operator) { + case '=': + return left === right; + case '<>': + return left !== right; + case '<': + return left < right; + case '>': + return left > right; + case '<=': + return left <= right; + case '>=': + return left >= right; + case 'CONTAINS': + return String(left).includes(String(right)); + case 'STARTS WITH': + return String(left).startsWith(String(right)); + case 'ENDS WITH': + return String(left).endsWith(String(right)); + default: + return false; + } + } + // --- 3. RETURN Projection --- + projectReturn(items, bindings) { + // Check if any item has aggregation + const hasAggregation = items.some((item) => this.isAggregation(item.expression)); + if (hasAggregation) { + return this.projectWithAggregation(items, bindings); + } + // Non-aggregated projection + const columns = []; + const rows = []; + // Determine columns from first binding (or from items if no bindings) + for (const item of items) { + if (item.expression.kind === 'variable' && + item.expression.name === '*') { + // Expand wildcard: use first binding's keys + if (bindings.length > 0) { + for (const varName of bindings[0].keys()) { + if (!columns.includes(varName)) + columns.push(varName); + } + } + } + else { + columns.push(this.getColumnName(item)); + } + } + for (const binding of bindings) { + const row = {}; + for (const item of items) { + if (item.expression.kind === 'variable' && + item.expression.name === '*') { + // Expand wildcard + for (const [varName, entry] of binding) { + row[varName] = this.resolveReturnValue({ kind: 'variable', name: varName }, binding); + } + } + else { + const colName = this.getColumnName(item); + row[colName] = this.resolveReturnValue(item.expression, binding); + } + } + rows.push(row); + } + return { columns, rows }; + } + isAggregation(expr) { + if (expr.kind === 'functionCall') { + const fc = expr; + return fc.name === 'COUNT' || fc.name === 'COLLECT'; + } + return false; + } + projectWithAggregation(items, bindings) { + const columns = items.map((item) => this.getColumnName(item)); + // Separate group-by keys from aggregations + const groupByItems = []; + const aggItems = []; + items.forEach((item, index) => { + if (this.isAggregation(item.expression)) { + aggItems.push({ item, index }); + } + else { + groupByItems.push(item); + } + }); + // Group bindings + const groups = new Map(); + if (groupByItems.length === 0) { + // All items are aggregations -> single group + groups.set('__all__', bindings); + } + else { + for (const binding of bindings) { + const keyParts = []; + for (const gbItem of groupByItems) { + keyParts.push(this.resolveReturnValue(gbItem.expression, binding)); + } + const key = JSON.stringify(keyParts); + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key).push(binding); + } + } + const rows = []; + for (const [, groupBindings] of groups) { + const row = {}; + for (const item of items) { + const colName = this.getColumnName(item); + if (this.isAggregation(item.expression)) { + const fc = item.expression; + if (fc.name === 'COUNT') { + row[colName] = groupBindings.length; + } + else if (fc.name === 'COLLECT') { + row[colName] = groupBindings.map((b) => this.resolveReturnValue(fc.argument, b)); + } + } + else { + // Non-aggregated: take from first binding in group + row[colName] = this.resolveReturnValue(item.expression, groupBindings[0]); + } + } + rows.push(row); + } + return { columns, rows }; + } + getColumnName(item) { + if (item.alias) + return item.alias; + return this.exprColumnName(item.expression); + } + exprColumnName(expr) { + switch (expr.kind) { + case 'propertyAccess': { + const pa = expr; + return `${pa.variable}.${pa.property}`; + } + case 'variable': { + return expr.name; + } + case 'functionCall': { + const fc = expr; + if (fc.name === 'TYPE' && fc.argument.kind === 'variable') { + return `type(${fc.argument.name})`; + } + if (fc.name === 'COUNT') { + return `COUNT(${this.exprColumnName(fc.argument)})`; + } + if (fc.name === 'COLLECT') { + return `COLLECT(${this.exprColumnName(fc.argument)})`; + } + return `${fc.name}(...)`; + } + default: + return '?'; + } + } + resolveReturnValue(expr, binding) { + switch (expr.kind) { + case 'propertyAccess': { + const pa = expr; + const entry = binding.get(pa.variable); + if (!entry) + return undefined; + if (entry.kind === 'node') { + return this.graph.getNode(entry.id).properties[pa.property]; + } + else { + return this.graph.getEdge(entry.id).properties[pa.property]; + } + } + case 'variable': { + const varRef = expr; + const entry = binding.get(varRef.name); + if (!entry) + return undefined; + if (entry.kind === 'node') { + const node = this.graph.getNode(entry.id); + const labels = Array.from(node.labels); + return { + name: node.properties.name, + path: node.properties.path, + type: labels.length > 0 ? labels[0] : undefined, + properties: Object.assign({}, node.properties), + }; + } + else { + const edge = this.graph.getEdge(entry.id); + const srcNode = this.graph.getNode(edge.src); + const dstNode = this.graph.getNode(edge.dst); + return { + from: srcNode.properties.path, + to: dstNode.properties.path, + type: edge.type, + properties: Object.assign({}, edge.properties), + }; + } + } + case 'functionCall': { + const fc = expr; + if (fc.name === 'TYPE') { + const argEntry = this.resolveBindingEntry(fc.argument, binding); + if (argEntry && argEntry.kind === 'edge') { + return this.graph.getEdge(argEntry.id).type; + } + return undefined; + } + // COUNT and COLLECT handled in aggregation path + return undefined; + } + case 'literal': + return expr.value; + default: + return undefined; + } + } +} +exports.CypherExecutor = CypherExecutor; diff --git a/dist/cypher/graph.d.ts b/dist/cypher/graph.d.ts new file mode 100644 index 0000000..1fd6fdf --- /dev/null +++ b/dist/cypher/graph.d.ts @@ -0,0 +1,40 @@ +import { SElement } from '../selement/selement'; +import { SElementAssociation } from '../selement/selementAssociation'; +export interface NodeData { + id: number; + labels: Set; + properties: Record; +} +export interface EdgeData { + id: number; + type: string; + properties: Record; + src: number; + dst: number; +} +export declare class CypherGraph { + private nodeData; + private edgeData; + private nodeOutEdges; + private nodeInEdges; + private pathToNode; + private nodeToElem; + private elemToNode; + private edgeToAssoc; + constructor(root: SElement, includeHierarchy?: boolean); + private static normalizePath; + private buildIndex; + get nodeCount(): number; + get edgeCount(): number; + getAllNodes(): NodeData[]; + getAllEdges(): EdgeData[]; + getNode(id: number): NodeData; + getEdge(id: number): EdgeData; + getOutEdges(nodeId: number): number[]; + getInEdges(nodeId: number): number[]; + getEdgeSrc(edgeId: number): number; + getEdgeDst(edgeId: number): number; + getNodeByPath(path: string): NodeData | undefined; + getElemForNode(nodeId: number): SElement | undefined; + getAssocForEdge(edgeId: number): SElementAssociation | null | undefined; +} diff --git a/dist/cypher/graph.js b/dist/cypher/graph.js new file mode 100644 index 0000000..76d4d7d --- /dev/null +++ b/dist/cypher/graph.js @@ -0,0 +1,146 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CypherGraph = void 0; +const selement_1 = require("../selement/selement"); +class CypherGraph { + constructor(root, includeHierarchy = true) { + this.nodeData = new Map(); + this.edgeData = new Map(); + this.nodeOutEdges = new Map(); + this.nodeInEdges = new Map(); + this.pathToNode = new Map(); + this.nodeToElem = new Map(); + this.elemToNode = new Map(); + this.edgeToAssoc = new Map(); + this.buildIndex(root, includeHierarchy); + } + static normalizePath(p) { + return p.startsWith('/') ? p.slice(1) : p; + } + buildIndex(root, includeHierarchy) { + let nextNodeId = 0; + let nextEdgeId = 0; + // Pass 1 -- Nodes + root.traverseElements((element) => { + const id = nextNodeId++; + const type = element.getType(); + const labels = new Set(); + if (type !== selement_1.NOT_KNOWN_TYPE) { + labels.add(type); + } + const attrs = element.getAttributes(); + const properties = {}; + for (const [key, value] of Object.entries(attrs)) { + if (key === 'type') + continue; + if (Array.isArray(value) && value.length === 1) { + properties[key] = value[0]; + } + else { + properties[key] = value; + } + } + const path = CypherGraph.normalizePath(element.getPath()); + properties.name = element.name; + properties.path = path; + const node = { id, labels, properties }; + this.nodeData.set(id, node); + this.nodeToElem.set(id, element); + this.elemToNode.set(element, id); + this.nodeOutEdges.set(id, []); + this.nodeInEdges.set(id, []); + this.pathToNode.set(path, id); + }); + // Pass 2 -- Edges (associations) + const seenAssocs = new Set(); + root.traverseElements((element) => { + for (const assoc of element.outgoing) { + if (seenAssocs.has(assoc)) + continue; + seenAssocs.add(assoc); + const srcId = this.elemToNode.get(assoc.fromElement); + const dstId = this.elemToNode.get(assoc.toElement); + if (srcId === undefined || dstId === undefined) + continue; + const edgeId = nextEdgeId++; + const edge = { + id: edgeId, + type: assoc.deptype || 'unknown', + properties: Object.assign({}, (assoc.attrs || {})), + src: srcId, + dst: dstId, + }; + this.edgeData.set(edgeId, edge); + this.edgeToAssoc.set(edgeId, assoc); + this.nodeOutEdges.get(srcId).push(edgeId); + this.nodeInEdges.get(dstId).push(edgeId); + } + }); + // Pass 3 -- Hierarchy edges (CONTAINS) + if (includeHierarchy) { + root.traverseElements((element) => { + const parentId = this.elemToNode.get(element); + if (parentId === undefined) + return; + for (const child of element.children) { + const childId = this.elemToNode.get(child); + if (childId === undefined) + continue; + const edgeId = nextEdgeId++; + const edge = { + id: edgeId, + type: 'CONTAINS', + properties: {}, + src: parentId, + dst: childId, + }; + this.edgeData.set(edgeId, edge); + this.edgeToAssoc.set(edgeId, null); + this.nodeOutEdges.get(parentId).push(edgeId); + this.nodeInEdges.get(childId).push(edgeId); + } + }); + } + } + get nodeCount() { + return this.nodeData.size; + } + get edgeCount() { + return this.edgeData.size; + } + getAllNodes() { + return Array.from(this.nodeData.values()); + } + getAllEdges() { + return Array.from(this.edgeData.values()); + } + getNode(id) { + return this.nodeData.get(id); + } + getEdge(id) { + return this.edgeData.get(id); + } + getOutEdges(nodeId) { + return this.nodeOutEdges.get(nodeId) || []; + } + getInEdges(nodeId) { + return this.nodeInEdges.get(nodeId) || []; + } + getEdgeSrc(edgeId) { + return this.edgeData.get(edgeId).src; + } + getEdgeDst(edgeId) { + return this.edgeData.get(edgeId).dst; + } + getNodeByPath(path) { + const id = this.pathToNode.get(CypherGraph.normalizePath(path)); + return id !== undefined ? this.nodeData.get(id) : undefined; + } + getElemForNode(nodeId) { + return this.nodeToElem.get(nodeId); + } + getAssocForEdge(edgeId) { + return this.edgeToAssoc.get(edgeId); + } +} +exports.CypherGraph = CypherGraph; diff --git a/dist/cypher/index.d.ts b/dist/cypher/index.d.ts new file mode 100644 index 0000000..8cceed0 --- /dev/null +++ b/dist/cypher/index.d.ts @@ -0,0 +1,8 @@ +import { SGraph } from '../sgraph'; +import { CypherResult } from './executor'; +export { CypherSyntaxError, CypherExecutionError } from './parser'; +export type { CypherResult } from './executor'; +export interface CypherQueryOptions { + includeHierarchy?: boolean; +} +export declare function cypherQuery(model: SGraph, query: string, options?: CypherQueryOptions): CypherResult; diff --git a/dist/cypher/index.js b/dist/cypher/index.js new file mode 100644 index 0000000..ba6bd34 --- /dev/null +++ b/dist/cypher/index.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cypherQuery = exports.CypherExecutionError = exports.CypherSyntaxError = void 0; +const graph_1 = require("./graph"); +const executor_1 = require("./executor"); +const parser_1 = require("./parser"); +var parser_2 = require("./parser"); +Object.defineProperty(exports, "CypherSyntaxError", { enumerable: true, get: function () { return parser_2.CypherSyntaxError; } }); +Object.defineProperty(exports, "CypherExecutionError", { enumerable: true, get: function () { return parser_2.CypherExecutionError; } }); +function cypherQuery(model, query, options = {}) { + const { includeHierarchy = true } = options; + const graph = new graph_1.CypherGraph(model.rootNode, includeHierarchy); + const executor = new executor_1.CypherExecutor(graph); + const ast = (0, parser_1.parse)(query); + return executor.execute(ast); +} +exports.cypherQuery = cypherQuery; diff --git a/dist/cypher/parser.d.ts b/dist/cypher/parser.d.ts new file mode 100644 index 0000000..510f0e6 --- /dev/null +++ b/dist/cypher/parser.d.ts @@ -0,0 +1,130 @@ +export declare class CypherSyntaxError extends Error { + constructor(message: string); +} +export declare class CypherExecutionError extends Error { + constructor(message: string); +} +export declare enum TokenType { + MATCH = "MATCH", + WHERE = "WHERE", + RETURN = "RETURN", + AS = "AS", + LIMIT = "LIMIT", + ORDER = "ORDER", + BY = "BY", + ASC = "ASC", + DESC = "DESC", + DISTINCT = "DISTINCT", + AND = "AND", + OR = "OR", + NOT = "NOT", + CONTAINS = "CONTAINS", + STARTS = "STARTS", + ENDS = "ENDS", + WITH = "WITH", + COUNT = "COUNT", + COLLECT = "COLLECT", + TYPE = "TYPE", + TRUE = "TRUE", + FALSE = "FALSE", + STAR = "STAR", + LPAREN = "LPAREN", + RPAREN = "RPAREN", + LBRACKET = "LBRACKET", + RBRACKET = "RBRACKET", + LBRACE = "LBRACE", + RBRACE = "RBRACE", + DASH = "DASH", + GT = "GT", + LT = "LT", + COLON = "COLON", + DOT = "DOT", + COMMA = "COMMA", + EQ = "EQ", + NEQ = "NEQ", + LTE = "LTE", + GTE = "GTE", + IDENTIFIER = "IDENTIFIER", + STRING = "STRING", + NUMBER = "NUMBER", + EOF = "EOF" +} +export interface Token { + type: TokenType; + value: string; +} +export declare function tokenize(input: string): Token[]; +export interface NodePattern { + kind: 'node'; + variable?: string; + labels: string[]; + properties?: Record; +} +export interface RelationshipPattern { + kind: 'relationship'; + variable?: string; + types: string[]; + direction: 'out' | 'in' | 'both'; +} +export declare type PatternElement = NodePattern | RelationshipPattern; +export interface MatchClause { + pattern: PatternElement[]; +} +export interface Comparison { + kind: 'comparison'; + operator: string; + left: WhereExpr; + right: WhereExpr; +} +export interface LogicalExpr { + kind: 'logical'; + op: 'AND' | 'OR'; + left: WhereExpr; + right: WhereExpr; +} +export interface NotExpr { + kind: 'not'; + expr: WhereExpr; +} +export interface PropertyAccess { + kind: 'propertyAccess'; + variable: string; + property: string; +} +export interface VariableRef { + kind: 'variable'; + name: string; +} +export interface FunctionCall { + kind: 'functionCall'; + name: string; + argument: WhereExpr; +} +export interface Literal { + kind: 'literal'; + value: string | number | boolean | null; +} +export declare type WhereExpr = Comparison | LogicalExpr | NotExpr | PropertyAccess | VariableRef | FunctionCall | Literal; +export interface ReturnItem { + expression: WhereExpr; + alias?: string; +} +export interface ReturnClause { + distinct: boolean; + items: ReturnItem[]; +} +export interface OrderByItem { + expression: WhereExpr; + descending: boolean; +} +export interface OrderByClause { + items: OrderByItem[]; +} +export interface CypherQuery { + matchClause: MatchClause; + whereClause?: WhereExpr; + returnClause: ReturnClause; + orderByClause?: OrderByClause; + limit?: number; +} +export declare function parse(input: string): CypherQuery; diff --git a/dist/cypher/parser.js b/dist/cypher/parser.js new file mode 100644 index 0000000..51222c7 --- /dev/null +++ b/dist/cypher/parser.js @@ -0,0 +1,711 @@ +"use strict"; +// Cypher subset parser for sgraph.js +// Tokenizer, AST types, and recursive descent parser +Object.defineProperty(exports, "__esModule", { value: true }); +exports.parse = exports.tokenize = exports.TokenType = exports.CypherExecutionError = exports.CypherSyntaxError = void 0; +// --- Error classes --- +class CypherSyntaxError extends Error { + constructor(message) { + super(message); + this.name = 'CypherSyntaxError'; + } +} +exports.CypherSyntaxError = CypherSyntaxError; +class CypherExecutionError extends Error { + constructor(message) { + super(message); + this.name = 'CypherExecutionError'; + } +} +exports.CypherExecutionError = CypherExecutionError; +// --- Token types --- +var TokenType; +(function (TokenType) { + // Keywords + TokenType["MATCH"] = "MATCH"; + TokenType["WHERE"] = "WHERE"; + TokenType["RETURN"] = "RETURN"; + TokenType["AS"] = "AS"; + TokenType["LIMIT"] = "LIMIT"; + TokenType["ORDER"] = "ORDER"; + TokenType["BY"] = "BY"; + TokenType["ASC"] = "ASC"; + TokenType["DESC"] = "DESC"; + TokenType["DISTINCT"] = "DISTINCT"; + TokenType["AND"] = "AND"; + TokenType["OR"] = "OR"; + TokenType["NOT"] = "NOT"; + TokenType["CONTAINS"] = "CONTAINS"; + TokenType["STARTS"] = "STARTS"; + TokenType["ENDS"] = "ENDS"; + TokenType["WITH"] = "WITH"; + TokenType["COUNT"] = "COUNT"; + TokenType["COLLECT"] = "COLLECT"; + TokenType["TYPE"] = "TYPE"; + TokenType["TRUE"] = "TRUE"; + TokenType["FALSE"] = "FALSE"; + TokenType["STAR"] = "STAR"; + // Symbols + TokenType["LPAREN"] = "LPAREN"; + TokenType["RPAREN"] = "RPAREN"; + TokenType["LBRACKET"] = "LBRACKET"; + TokenType["RBRACKET"] = "RBRACKET"; + TokenType["LBRACE"] = "LBRACE"; + TokenType["RBRACE"] = "RBRACE"; + TokenType["DASH"] = "DASH"; + TokenType["GT"] = "GT"; + TokenType["LT"] = "LT"; + TokenType["COLON"] = "COLON"; + TokenType["DOT"] = "DOT"; + TokenType["COMMA"] = "COMMA"; + TokenType["EQ"] = "EQ"; + TokenType["NEQ"] = "NEQ"; + TokenType["LTE"] = "LTE"; + TokenType["GTE"] = "GTE"; + // Values + TokenType["IDENTIFIER"] = "IDENTIFIER"; + TokenType["STRING"] = "STRING"; + TokenType["NUMBER"] = "NUMBER"; + TokenType["EOF"] = "EOF"; +})(TokenType = exports.TokenType || (exports.TokenType = {})); +// --- Keyword lookup --- +const KEYWORDS = { + MATCH: TokenType.MATCH, + WHERE: TokenType.WHERE, + RETURN: TokenType.RETURN, + AS: TokenType.AS, + LIMIT: TokenType.LIMIT, + ORDER: TokenType.ORDER, + BY: TokenType.BY, + ASC: TokenType.ASC, + DESC: TokenType.DESC, + DISTINCT: TokenType.DISTINCT, + AND: TokenType.AND, + OR: TokenType.OR, + NOT: TokenType.NOT, + CONTAINS: TokenType.CONTAINS, + STARTS: TokenType.STARTS, + ENDS: TokenType.ENDS, + WITH: TokenType.WITH, + COUNT: TokenType.COUNT, + COLLECT: TokenType.COLLECT, + TRUE: TokenType.TRUE, + FALSE: TokenType.FALSE, +}; +// --- Tokenizer --- +function tokenize(input) { + const tokens = []; + let i = 0; + while (i < input.length) { + // Skip whitespace + if (/\s/.test(input[i])) { + i++; + continue; + } + // Two-char operators (check before single-char) + if (i + 1 < input.length) { + const twoChar = input[i] + input[i + 1]; + if (twoChar === '<>') { + tokens.push({ type: TokenType.NEQ, value: '<>' }); + i += 2; + continue; + } + if (twoChar === '<=') { + tokens.push({ type: TokenType.LTE, value: '<=' }); + i += 2; + continue; + } + if (twoChar === '>=') { + tokens.push({ type: TokenType.GTE, value: '>=' }); + i += 2; + continue; + } + } + // Single-char symbols + const ch = input[i]; + if (ch === '(') { + tokens.push({ type: TokenType.LPAREN, value: '(' }); + i++; + continue; + } + if (ch === ')') { + tokens.push({ type: TokenType.RPAREN, value: ')' }); + i++; + continue; + } + if (ch === '[') { + tokens.push({ type: TokenType.LBRACKET, value: '[' }); + i++; + continue; + } + if (ch === ']') { + tokens.push({ type: TokenType.RBRACKET, value: ']' }); + i++; + continue; + } + if (ch === '{') { + tokens.push({ type: TokenType.LBRACE, value: '{' }); + i++; + continue; + } + if (ch === '}') { + tokens.push({ type: TokenType.RBRACE, value: '}' }); + i++; + continue; + } + if (ch === '-') { + tokens.push({ type: TokenType.DASH, value: '-' }); + i++; + continue; + } + if (ch === '>') { + tokens.push({ type: TokenType.GT, value: '>' }); + i++; + continue; + } + if (ch === '<') { + tokens.push({ type: TokenType.LT, value: '<' }); + i++; + continue; + } + if (ch === ':') { + tokens.push({ type: TokenType.COLON, value: ':' }); + i++; + continue; + } + if (ch === '.') { + tokens.push({ type: TokenType.DOT, value: '.' }); + i++; + continue; + } + if (ch === ',') { + tokens.push({ type: TokenType.COMMA, value: ',' }); + i++; + continue; + } + if (ch === '=') { + tokens.push({ type: TokenType.EQ, value: '=' }); + i++; + continue; + } + if (ch === '*') { + tokens.push({ type: TokenType.STAR, value: '*' }); + i++; + continue; + } + // String literals + if (ch === "'" || ch === '"') { + const quote = ch; + i++; // skip opening quote + let str = ''; + while (i < input.length && input[i] !== quote) { + str += input[i]; + i++; + } + if (i >= input.length) { + throw new CypherSyntaxError(`Unterminated string literal`); + } + i++; // skip closing quote + tokens.push({ type: TokenType.STRING, value: str }); + continue; + } + // Numbers + if (/[0-9]/.test(ch)) { + let num = ''; + while (i < input.length && /[0-9]/.test(input[i])) { + num += input[i]; + i++; + } + if (i < input.length && input[i] === '.') { + num += '.'; + i++; + while (i < input.length && /[0-9]/.test(input[i])) { + num += input[i]; + i++; + } + } + tokens.push({ type: TokenType.NUMBER, value: num }); + continue; + } + // Identifiers and keywords + if (/[a-zA-Z_]/.test(ch)) { + let ident = ''; + while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) { + ident += input[i]; + i++; + } + const upper = ident.toUpperCase(); + if (upper in KEYWORDS) { + tokens.push({ type: KEYWORDS[upper], value: upper }); + } + else { + tokens.push({ type: TokenType.IDENTIFIER, value: ident }); + } + continue; + } + throw new CypherSyntaxError(`Unexpected character: '${ch}'`); + } + tokens.push({ type: TokenType.EOF, value: '' }); + return tokens; +} +exports.tokenize = tokenize; +// --- Parser --- +class Parser { + constructor(tokens) { + this.tokens = tokens; + this.pos = 0; + } + peek() { + return this.tokens[this.pos]; + } + advance() { + const token = this.tokens[this.pos]; + this.pos++; + return token; + } + expect(type) { + const token = this.peek(); + if (token.type !== type) { + throw new CypherSyntaxError(`Expected ${type} but got ${token.type} ('${token.value}')`); + } + return this.advance(); + } + match(type) { + if (this.peek().type === type) { + this.advance(); + return true; + } + return false; + } + expectIdentifierOrKeyword() { + const token = this.peek(); + // Accept IDENTIFIER or any keyword token as a name + if (token.type === TokenType.IDENTIFIER || token.type in TokenType) { + // Reject structural tokens (symbols, EOF, literals other than identifiers/keywords) + const nonNameTypes = new Set([ + TokenType.LPAREN, TokenType.RPAREN, TokenType.LBRACKET, TokenType.RBRACKET, + TokenType.LBRACE, TokenType.RBRACE, TokenType.DASH, TokenType.GT, TokenType.LT, + TokenType.COLON, TokenType.DOT, TokenType.COMMA, TokenType.EQ, TokenType.NEQ, + TokenType.LTE, TokenType.GTE, TokenType.STRING, TokenType.NUMBER, + TokenType.EOF, TokenType.STAR, + ]); + if (!nonNameTypes.has(token.type)) { + return this.advance(); + } + } + throw new CypherSyntaxError(`Expected identifier or keyword but got ${token.type} ('${token.value}')`); + } + parseQuery() { + // MATCH clause + this.expect(TokenType.MATCH); + const matchClause = this.parseMatch(); + // Optional WHERE clause + let whereClause; + if (this.peek().type === TokenType.WHERE) { + this.advance(); + whereClause = this.parseOrExpr(); + } + // RETURN clause + this.expect(TokenType.RETURN); + const returnClause = this.parseReturn(); + // Optional ORDER BY + let orderByClause; + if (this.peek().type === TokenType.ORDER) { + orderByClause = this.parseOrderBy(); + } + // Optional LIMIT + let limit; + if (this.peek().type === TokenType.LIMIT) { + limit = this.parseLimit(); + } + return { + matchClause, + whereClause, + returnClause, + orderByClause, + limit, + }; + } + parseMatch() { + const pattern = []; + // First element must be a node + pattern.push(this.parseNodePattern()); + // Then alternating relationship-node pairs + while (this.peek().type === TokenType.DASH || this.peek().type === TokenType.LT) { + const rel = this.parseRelationshipPattern(); + pattern.push(rel); + pattern.push(this.parseNodePattern()); + } + return { pattern }; + } + parseNodePattern() { + this.expect(TokenType.LPAREN); + let variable; + const labels = []; + let properties; + // Optional variable name (identifier that is NOT followed by nothing special, + // or is followed by colon for labels, or RPAREN) + if (this.peek().type === TokenType.IDENTIFIER) { + variable = this.advance().value; + } + // Optional labels (:Label) + while (this.peek().type === TokenType.COLON) { + this.advance(); // consume ':' + const labelToken = this.expect(TokenType.IDENTIFIER); + labels.push(labelToken.value); + } + // Optional inline properties { key: value, ... } + if (this.peek().type === TokenType.LBRACE) { + properties = this.parseInlineProperties(); + } + this.expect(TokenType.RPAREN); + return { kind: 'node', variable, labels, properties }; + } + parseInlineProperties() { + this.expect(TokenType.LBRACE); + const props = {}; + if (this.peek().type !== TokenType.RBRACE) { + // Parse first property + const key = this.expect(TokenType.IDENTIFIER).value; + this.expect(TokenType.COLON); + const value = this.parseLiteralValue(); + props[key] = value; + // Parse additional properties + while (this.match(TokenType.COMMA)) { + const k = this.expect(TokenType.IDENTIFIER).value; + this.expect(TokenType.COLON); + const v = this.parseLiteralValue(); + props[k] = v; + } + } + this.expect(TokenType.RBRACE); + return props; + } + parseLiteralValue() { + const token = this.peek(); + if (token.type === TokenType.STRING) { + this.advance(); + return token.value; + } + if (token.type === TokenType.NUMBER) { + this.advance(); + const num = parseFloat(token.value); + return num; + } + if (token.type === TokenType.TRUE) { + this.advance(); + return true; + } + if (token.type === TokenType.FALSE) { + this.advance(); + return false; + } + throw new CypherSyntaxError(`Expected literal value but got ${token.type} ('${token.value}')`); + } + parseRelationshipPattern() { + // Possible patterns: + // -[...]-> outgoing + // <-[...]- incoming + // -[...]- both (undirected) + let direction; + let leftArrow = false; + // Check for incoming arrow start: <- + if (this.peek().type === TokenType.LT) { + this.advance(); // consume '<' + this.expect(TokenType.DASH); // consume '-' + leftArrow = true; + } + else { + this.expect(TokenType.DASH); // consume '-' + } + // Parse bracket contents [variable:TYPE] + this.expect(TokenType.LBRACKET); + let variable; + const types = []; + if (this.peek().type === TokenType.IDENTIFIER) { + variable = this.advance().value; + } + // Optional type (:TYPE) — allow keywords as type names (e.g. CONTAINS) + while (this.peek().type === TokenType.COLON) { + this.advance(); // consume ':' + const typeToken = this.expectIdentifierOrKeyword(); + types.push(typeToken.value); + } + this.expect(TokenType.RBRACKET); + // Check for outgoing arrow end: -> + this.expect(TokenType.DASH); // consume '-' + let rightArrow = false; + if (this.peek().type === TokenType.GT) { + this.advance(); // consume '>' + rightArrow = true; + } + if (leftArrow && !rightArrow) { + direction = 'in'; + } + else if (!leftArrow && rightArrow) { + direction = 'out'; + } + else { + direction = 'both'; + } + return { kind: 'relationship', variable, types, direction }; + } + // --- WHERE expression parsing (precedence climbing) --- + parseOrExpr() { + let left = this.parseAndExpr(); + while (this.peek().type === TokenType.OR) { + this.advance(); + const right = this.parseAndExpr(); + left = { kind: 'logical', op: 'OR', left, right }; + } + return left; + } + parseAndExpr() { + let left = this.parseNotExpr(); + while (this.peek().type === TokenType.AND) { + this.advance(); + const right = this.parseNotExpr(); + left = { kind: 'logical', op: 'AND', left, right }; + } + return left; + } + parseNotExpr() { + if (this.peek().type === TokenType.NOT) { + this.advance(); + const expr = this.parseComparison(); + return { kind: 'not', expr }; + } + return this.parseComparison(); + } + parseComparison() { + const left = this.parseAtom(); + // Check for comparison operators + const token = this.peek(); + let operator; + switch (token.type) { + case TokenType.EQ: + operator = '='; + this.advance(); + break; + case TokenType.NEQ: + operator = '<>'; + this.advance(); + break; + case TokenType.LT: + operator = '<'; + this.advance(); + break; + case TokenType.GT: + operator = '>'; + this.advance(); + break; + case TokenType.LTE: + operator = '<='; + this.advance(); + break; + case TokenType.GTE: + operator = '>='; + this.advance(); + break; + case TokenType.CONTAINS: + operator = 'CONTAINS'; + this.advance(); + break; + case TokenType.STARTS: + this.advance(); // consume STARTS + this.expect(TokenType.WITH); // consume WITH + operator = 'STARTS WITH'; + break; + case TokenType.ENDS: + this.advance(); // consume ENDS + this.expect(TokenType.WITH); // consume WITH + operator = 'ENDS WITH'; + break; + default: + return left; + } + const right = this.parseAtom(); + return { kind: 'comparison', operator, left, right }; + } + parseAtom() { + const token = this.peek(); + // String literal + if (token.type === TokenType.STRING) { + this.advance(); + return { kind: 'literal', value: token.value }; + } + // Number literal + if (token.type === TokenType.NUMBER) { + this.advance(); + return { kind: 'literal', value: parseFloat(token.value) }; + } + // Boolean literals + if (token.type === TokenType.TRUE) { + this.advance(); + return { kind: 'literal', value: true }; + } + if (token.type === TokenType.FALSE) { + this.advance(); + return { kind: 'literal', value: false }; + } + // Parenthesized expression + if (token.type === TokenType.LPAREN) { + this.advance(); + const expr = this.parseOrExpr(); + this.expect(TokenType.RPAREN); + return expr; + } + // Function call keyword: COUNT(x), COLLECT(x) + if (this.isFunctionKeyword(token.type)) { + const name = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(name), argument }; + } + // Not a function call - could be property access or variable ref + if (this.peek().type === TokenType.DOT) { + this.advance(); // consume '.' + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: name, property }; + } + return { kind: 'variable', name }; + } + // Identifier: could be propertyAccess (n.name), functionCall (type(r)), or variableRef (n) + if (token.type === TokenType.IDENTIFIER) { + const ident = this.advance().value; + if (this.peek().type === TokenType.DOT) { + this.advance(); // consume '.' + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: ident, property }; + } + // Check if it's a function call: ident(...) + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(ident), argument }; + } + return { kind: 'variable', name: ident }; + } + throw new CypherSyntaxError(`Unexpected token in expression: ${token.type} ('${token.value}')`); + } + isFunctionKeyword(type) { + return type === TokenType.COUNT || type === TokenType.COLLECT; + } + normalizeFunctionName(name) { + const upper = name.toUpperCase(); + if (Parser.KNOWN_FUNCTIONS.has(upper)) { + return upper; + } + return name; + } + // --- RETURN clause --- + parseReturn() { + let distinct = false; + if (this.peek().type === TokenType.DISTINCT) { + this.advance(); + distinct = true; + } + const items = []; + items.push(this.parseReturnItem()); + while (this.match(TokenType.COMMA)) { + items.push(this.parseReturnItem()); + } + return { distinct, items }; + } + parseReturnItem() { + let expression; + // Handle RETURN * + if (this.peek().type === TokenType.STAR) { + this.advance(); + expression = { kind: 'variable', name: '*' }; + } + else { + expression = this.parseReturnExpression(); + } + // Optional alias: AS name + let alias; + if (this.peek().type === TokenType.AS) { + this.advance(); + alias = this.expect(TokenType.IDENTIFIER).value; + } + return { expression, alias }; + } + parseReturnExpression() { + const token = this.peek(); + // Function call keyword: COUNT(x), COLLECT(x) + if (this.isFunctionKeyword(token.type)) { + const name = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(name), argument }; + } + // Fallthrough: treat as variable or property access + if (this.peek().type === TokenType.DOT) { + this.advance(); + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: name, property }; + } + return { kind: 'variable', name }; + } + // Identifier: could be propertyAccess, functionCall (type(r)), or variableRef + if (token.type === TokenType.IDENTIFIER) { + const ident = this.advance().value; + if (this.peek().type === TokenType.DOT) { + this.advance(); + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: ident, property }; + } + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(ident), argument }; + } + return { kind: 'variable', name: ident }; + } + throw new CypherSyntaxError(`Unexpected token in RETURN clause: ${token.type} ('${token.value}')`); + } + // --- ORDER BY --- + parseOrderBy() { + this.expect(TokenType.ORDER); + this.expect(TokenType.BY); + const items = []; + items.push(this.parseOrderByItem()); + while (this.match(TokenType.COMMA)) { + items.push(this.parseOrderByItem()); + } + return { items }; + } + parseOrderByItem() { + const expression = this.parseReturnExpression(); + let descending = false; + if (this.peek().type === TokenType.DESC) { + this.advance(); + descending = true; + } + else if (this.peek().type === TokenType.ASC) { + this.advance(); + descending = false; + } + return { expression, descending }; + } + // --- LIMIT --- + parseLimit() { + this.expect(TokenType.LIMIT); + const token = this.expect(TokenType.NUMBER); + return parseInt(token.value, 10); + } +} +Parser.KNOWN_FUNCTIONS = new Set(['COUNT', 'COLLECT', 'TYPE']); +// --- Public parse function --- +function parse(input) { + const tokens = tokenize(input); + const parser = new Parser(tokens); + return parser.parseQuery(); +} +exports.parse = parse; diff --git a/dist/index.d.ts b/dist/index.d.ts index b013bf2..e1eee13 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -3,3 +3,4 @@ export * from './loaders'; export * from './sgraph'; export * from './selement'; export * from './converters'; +export * from './cypher'; diff --git a/dist/index.js b/dist/index.js index 73b4d60..f57534f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -19,3 +19,4 @@ __exportStar(require("./loaders"), exports); __exportStar(require("./sgraph"), exports); __exportStar(require("./selement"), exports); __exportStar(require("./converters"), exports); +__exportStar(require("./cypher"), exports); diff --git a/dist/modelapi.d.ts b/dist/modelapi.d.ts index 1f6eaf9..33ec9b1 100644 --- a/dist/modelapi.d.ts +++ b/dist/modelapi.d.ts @@ -20,6 +20,9 @@ declare class ModelApi { getCallingFunctions: (element: SElement) => SElement[]; getUsedElements: (element: SElement) => SElement[]; getUserElements: (element: SElement) => SElement[]; + getElementByPath(path: string): SElement | undefined; + filter(filterFunc: (element: SElement) => boolean): SElement[]; + getChildrenByType(element: SElement, elemType: string): SElement[]; createDescendants: (relatedElement: SElement, newOrExistingReferredElement: SElement) => void; /** Filter a sub graph from source graph related to source element. diff --git a/dist/modelapi.js b/dist/modelapi.js index 2d284bc..384508f 100644 --- a/dist/modelapi.js +++ b/dist/modelapi.js @@ -17,7 +17,7 @@ class ModelApi { this.getCallingFunctions = (element) => element.incoming .filter((ea) => ea.deptype === 'function_ref') .map((ea) => ea.fromElement); - this.getUsedElements = (element) => element.outgoing.map((ea) => ea.fromElement); + this.getUsedElements = (element) => element.outgoing.map((ea) => ea.toElement); this.getUserElements = (element) => element.incoming.map((ea) => ea.fromElement); this.createDescendants = (relatedElement, newOrExistingReferredElement) => { const stack = [{ relatedElement, newOrExistingReferredElement }]; @@ -167,6 +167,26 @@ class ModelApi { recursiveTraverse(this.model.rootNode); return matching; } + getElementByPath(path) { + return this.egm.findElementFromPath(path); + } + filter(filterFunc) { + const matched = []; + const recurse = (element) => { + if (filterFunc(element)) + matched.push(element); + for (const child of element.children) { + recurse(child); + } + }; + for (const child of this.egm.rootNode.children) { + recurse(child); + } + return matched; + } + getChildrenByType(element, elemType) { + return element.children.filter((child) => child.typeEquals(elemType)); + } getCyclicDependencyCycles() { const cycles = []; this.egm.rootNode.traverseElements((element) => { diff --git a/dist/selement/selement.d.ts b/dist/selement/selement.d.ts index 90dfe56..db5dd72 100644 --- a/dist/selement/selement.d.ts +++ b/dist/selement/selement.d.ts @@ -23,6 +23,24 @@ declare class SElement { createElementChain(id: string): SElement; traverseElements(visit: (e: SElement) => void): void; getAncestors(): SElement[]; + getLevel(): number; + getRoot(): SElement; + isDescendantOf(ancestor: SElement): boolean; + getAncestorOfType(type: string): SElement | undefined; + getAncestorOfTypes(types: string[] | Set): SElement | undefined; + getAncestorOfLevel(level: number): SElement | undefined; + getDescendants(): SElement[]; + getNodeCount(): number; + getEACount(): number; + getEATypes(typeSet: Set): void; + getEATypeCounts(counts: { + [key: string]: number; + }): void; + getMaxDepth(currentDepth: number): number; + hasType(): boolean; + remove(leaveParentUntouched?: boolean): void; + rename(newName: string): void; + removeDescendantsIf(checker: (e: SElement) => boolean): void; addAttribute(name: string, value: string): void; setAttributes(attributes: any): void; getAttributes(): { diff --git a/dist/selement/selement.js b/dist/selement/selement.js index 5942cf9..0ee0460 100644 --- a/dist/selement/selement.js +++ b/dist/selement/selement.js @@ -226,6 +226,160 @@ class SElement { } return ancestors; } + getLevel() { + let e = this.parent; + let level = 0; + while (e !== undefined) { + e = e.parent; + level += 1; + } + return level; + } + getRoot() { + let p = this; + while (p.parent !== undefined) { + p = p.parent; + } + return p; + } + isDescendantOf(ancestor) { + if (this.equals(ancestor)) + return false; + let p = this.parent; + while (p !== undefined) { + if (p.equals(ancestor)) + return true; + p = p.parent; + } + return false; + } + getAncestorOfType(type) { + if (this.typeEquals(type)) + return this; + let ancestor = this; + while ((ancestor === null || ancestor === void 0 ? void 0 : ancestor.parent) !== undefined) { + ancestor = ancestor.parent; + if (ancestor.typeEquals(type)) + return ancestor; + } + return undefined; + } + getAncestorOfTypes(types) { + const typeSet = types instanceof Set ? types : new Set(types); + if (typeSet.has(this.getType())) + return this; + let ancestor = this; + while ((ancestor === null || ancestor === void 0 ? void 0 : ancestor.parent) !== undefined) { + ancestor = ancestor.parent; + if (typeSet.has(ancestor.getType())) + return ancestor; + } + return undefined; + } + getAncestorOfLevel(level) { + let delta = this.getLevel() - level; + let ancestor = this; + while (delta > 0 && ancestor !== undefined) { + ancestor = ancestor.parent; + delta -= 1; + } + return ancestor; + } + getDescendants() { + const descendants = []; + const collect = (element) => { + for (const child of element.children) { + descendants.push(child); + collect(child); + } + }; + collect(this); + return descendants; + } + getNodeCount() { + let count = 1; + for (const child of this.children) { + count += child.getNodeCount(); + } + return count; + } + getEACount() { + let count = this.outgoing.length; + for (const child of this.children) { + count += child.getEACount(); + } + return count; + } + getEATypes(typeSet) { + for (const ea of this.outgoing) { + if (ea.deptype) + typeSet.add(ea.deptype); + } + for (const child of this.children) { + child.getEATypes(typeSet); + } + } + getEATypeCounts(counts) { + for (const ea of this.outgoing) { + if (ea.deptype) { + counts[ea.deptype] = (counts[ea.deptype] || 0) + 1; + } + } + for (const child of this.children) { + child.getEATypeCounts(counts); + } + } + getMaxDepth(currentDepth) { + if (this.children.length === 0) + return currentDepth; + let depth = 0; + for (const child of this.children) { + depth = Math.max(child.getMaxDepth(currentDepth + 1), depth); + } + return depth === 0 ? currentDepth : depth; + } + hasType() { + return 'type' in this.attrs && this.attrs.type !== ''; + } + remove(leaveParentUntouched = false) { + if (!leaveParentUntouched && this.parent !== undefined) { + this.parent.detachChild(this); + } + for (const ea of [...this.outgoing]) { + ea.remove(); + } + this.outgoing = []; + for (const ea of [...this.incoming]) { + ea.remove(); + } + this.incoming = []; + for (const child of this.children) { + child.remove(true); + } + this.children = []; + this.childrenObject = {}; + } + rename(newName) { + if (this.parent === undefined) { + this.name = newName; + this.updateHash(); + return; + } + delete this.parent.childrenObject[this.name]; + this.name = newName; + this.parent.childrenObject[newName] = this; + this.updateHash(); + } + removeDescendantsIf(checker) { + for (const child of [...this.children]) { + if (checker(child)) { + child.remove(); + } + } + for (const child of this.children) { + child.removeDescendantsIf(checker); + } + } addAttribute(name, value) { var _a; var _b; diff --git a/dist/selement/selementAssociation.d.ts b/dist/selement/selementAssociation.d.ts index 243e5a6..8158abd 100644 --- a/dist/selement/selementAssociation.d.ts +++ b/dist/selement/selementAssociation.d.ts @@ -26,6 +26,7 @@ declare class SElementAssociation { [key: string]: any; }): void; remove(): void; + addAttribute(name: string, value: string): void; getDependencyLength(): number; getFromPath: () => string; getToPath: () => string; diff --git a/dist/selement/selementAssociation.js b/dist/selement/selementAssociation.js index 42f31e4..d2e818f 100644 --- a/dist/selement/selementAssociation.js +++ b/dist/selement/selementAssociation.js @@ -78,7 +78,12 @@ class SElementAssociation { remove() { var _a, _b, _c, _d; (_a = this.fromElement) === null || _a === void 0 ? void 0 : _a.outgoing.splice((_b = this.fromElement) === null || _b === void 0 ? void 0 : _b.outgoing.indexOf(this), 1); - (_c = this.fromElement) === null || _c === void 0 ? void 0 : _c.incoming.splice((_d = this.fromElement) === null || _d === void 0 ? void 0 : _d.incoming.indexOf(this), 1); + (_c = this.toElement) === null || _c === void 0 ? void 0 : _c.incoming.splice((_d = this.toElement) === null || _d === void 0 ? void 0 : _d.incoming.indexOf(this), 1); + } + addAttribute(name, value) { + if (!this.attrs) + this.attrs = {}; + this.attrs[name] = value; } getDependencyLength() { var _a; diff --git a/dist/sgraph/sgraph.d.ts b/dist/sgraph/sgraph.d.ts index 1428425..96a769a 100644 --- a/dist/sgraph/sgraph.d.ts +++ b/dist/sgraph/sgraph.d.ts @@ -32,6 +32,15 @@ declare class SGraph { findElementFromPath(i: string): SElement | undefined; setModelPath(filePath: string): void; toEcharts(): EChartsOptions; + getDepth(): number; + calculateModelStats(): { + dependenciesCount: number; + nodesCount: number; + depTypeCounts: { + [key: string]: number; + }; + depToElemRatio: number; + }; toXml(): string; } export { SGraph }; diff --git a/dist/sgraph/sgraph.js b/dist/sgraph/sgraph.js index b651938..fca70c0 100644 --- a/dist/sgraph/sgraph.js +++ b/dist/sgraph/sgraph.js @@ -111,6 +111,23 @@ class SGraph { const ec = (0, converters_1.sgraphToEcharts)(this); return ec; } + getDepth() { + let depth = 0; + for (const child of this.rootNode.children) { + depth = Math.max(child.getMaxDepth(1), depth); + } + return depth; + } + calculateModelStats() { + const dependenciesCount = this.rootNode.getEACount(); + const nodesCount = this.rootNode.getNodeCount(); + const depTypeCounts = {}; + this.rootNode.getEATypeCounts(depTypeCounts); + const depToElemRatio = nodesCount > 0 + ? Math.round((dependenciesCount / nodesCount) * 100) / 100 + : 0; + return { dependenciesCount, nodesCount, depTypeCounts, depToElemRatio }; + } toXml() { const rootNode = this.rootNode; const counter = new sgraph_utils_2.Counter(); diff --git a/docs/superpowers/plans/2026-03-16-cypher-support.md b/docs/superpowers/plans/2026-03-16-cypher-support.md new file mode 100644 index 0000000..076887c --- /dev/null +++ b/docs/superpowers/plans/2026-03-16-cypher-support.md @@ -0,0 +1,1104 @@ +# Cypher Query Support Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a lightweight Cypher query interpreter to sgraph.js that can query SGraph models, with both a programmatic API and a CLI. + +**Architecture:** Hand-written tokenizer + recursive descent parser produces an AST. A graph index maps SElement/SElementAssociation to labeled property graph nodes/edges. An executor matches patterns, filters with WHERE, and projects RETURN results. Zero external dependencies. + +**Tech Stack:** TypeScript, Jest (testing), existing sgraph.js types (SElement, SElementAssociation, SGraph) + +**Spec:** `docs/superpowers/specs/2026-03-16-cypher-support-design.md` + +--- + +## File Structure + +| File | Responsibility | +|------|---------------| +| `src/cypher/parser.ts` | Tokenizer + recursive descent parser → AST types + parse function | +| `src/cypher/graph.ts` | CypherGraph class — indexes SGraph into labeled property graph | +| `src/cypher/executor.ts` | CypherExecutor — pattern matching, WHERE eval, RETURN projection | +| `src/cypher/index.ts` | Public API: `cypherQuery()` function, re-exports types | +| `src/cypher/cli.ts` | CLI entry point with REPL | +| `src/index.ts` | Modify: add `export * from './cypher'` | +| `test/cypher/parser.test.ts` | Parser unit tests | +| `test/cypher/graph.test.ts` | Graph index unit tests | +| `test/cypher/executor.test.ts` | Executor unit tests | +| `test/cypher/integration.test.ts` | End-to-end queries against test model | + +--- + +## Chunk 1: Parser + +### Task 1: AST Types and Tokenizer + +**Files:** +- Create: `src/cypher/parser.ts` +- Create: `test/cypher/parser.test.ts` + +- [ ] **Step 1: Write tokenizer tests** + +Create `test/cypher/parser.test.ts`: + +```typescript +import { tokenize, TokenType } from '../../src/cypher/parser'; + +describe('Cypher Tokenizer', () => { + it('tokenizes a simple MATCH RETURN', () => { + const tokens = tokenize('MATCH (n) RETURN n'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.MATCH, TokenType.LPAREN, TokenType.IDENTIFIER, + TokenType.RPAREN, TokenType.RETURN, TokenType.IDENTIFIER, TokenType.EOF, + ]); + }); + + it('tokenizes labels and types', () => { + const tokens = tokenize('(n:file)'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.LPAREN, TokenType.IDENTIFIER, TokenType.COLON, + TokenType.IDENTIFIER, TokenType.RPAREN, TokenType.EOF, + ]); + }); + + it('tokenizes string literals', () => { + const tokens = tokenize("'hello'"); + expect(tokens[0].type).toBe(TokenType.STRING); + expect(tokens[0].value).toBe('hello'); + }); + + it('tokenizes double-quoted strings', () => { + const tokens = tokenize('"world"'); + expect(tokens[0].type).toBe(TokenType.STRING); + expect(tokens[0].value).toBe('world'); + }); + + it('tokenizes numbers', () => { + const tokens = tokenize('42'); + expect(tokens[0].type).toBe(TokenType.NUMBER); + expect(tokens[0].value).toBe('42'); + }); + + it('tokenizes comparison operators', () => { + const tokens = tokenize('<> <= >= = < >'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.NEQ, TokenType.LTE, TokenType.GTE, + TokenType.EQ, TokenType.LT, TokenType.GT, TokenType.EOF, + ]); + }); + + it('tokenizes relationship arrow -[r:TYPE]->', () => { + const tokens = tokenize('-[r:TYPE]->'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.DASH, TokenType.LBRACKET, TokenType.IDENTIFIER, + TokenType.COLON, TokenType.IDENTIFIER, TokenType.RBRACKET, + TokenType.DASH, TokenType.GT, TokenType.EOF, + ]); + }); + + it('tokenizes keywords case-insensitively', () => { + const tokens = tokenize('match WHERE Return'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.MATCH, TokenType.WHERE, TokenType.RETURN, TokenType.EOF, + ]); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx jest test/cypher/parser.test.ts --no-cache` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement tokenizer and AST types** + +Create `src/cypher/parser.ts` with: +- `TokenType` enum (all keywords + symbols + IDENTIFIER, STRING, NUMBER, EOF) +- `Token` interface `{ type: TokenType, value: string }` +- `tokenize(input: string): Token[]` function +- All AST type interfaces: `CypherQuery`, `MatchClause`, `NodePattern`, `RelationshipPattern`, `ReturnClause`, `ReturnItem`, `WhereExpr` (union type), `Comparison`, `LogicalExpr`, `NotExpr`, `PropertyAccess`, `VariableRef`, `FunctionCall`, `Literal`, `OrderByClause` +- Error classes: `CypherSyntaxError`, `CypherExecutionError` + +Tokenizer rules: +- Skip whitespace +- Keywords are case-insensitive: check if identifier matches keyword list +- Strings: `'...'` or `"..."`, no escape sequences needed +- Numbers: sequence of digits (optionally with `.`) +- Two-char operators: `<>`, `<=`, `>=` +- Single-char symbols: `( ) [ ] { } - > < : . , =` + +- [ ] **Step 4: Run tokenizer tests** + +Run: `npx jest test/cypher/parser.test.ts --no-cache` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/cypher/parser.ts test/cypher/parser.test.ts +git commit -m "feat(cypher): add tokenizer and AST types" +``` + +### Task 2: Recursive Descent Parser + +**Files:** +- Modify: `src/cypher/parser.ts` +- Modify: `test/cypher/parser.test.ts` + +- [ ] **Step 1: Write parser tests** + +Add to `test/cypher/parser.test.ts`: + +```typescript +import { parse, tokenize, TokenType } from '../../src/cypher/parser'; + +describe('Cypher Parser', () => { + it('parses simple MATCH (n) RETURN n', () => { + const ast = parse('MATCH (n) RETURN n'); + expect(ast.matchClause.pattern).toHaveLength(1); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', labels: [], + }); + expect(ast.returnClause.items).toHaveLength(1); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'variable', name: 'n', + }); + }); + + it('parses node with label: (n:file)', () => { + const ast = parse('MATCH (n:file) RETURN n'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', labels: ['file'], + }); + }); + + it('parses node with inline properties: (n {name: "foo"})', () => { + const ast = parse('MATCH (n {name: "foo"}) RETURN n'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', properties: { name: 'foo' }, + }); + }); + + it('parses outgoing relationship: (a)-[r:import]->(b)', () => { + const ast = parse('MATCH (a)-[r:import]->(b) RETURN a, b'); + expect(ast.matchClause.pattern).toHaveLength(3); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', variable: 'r', types: ['import'], direction: 'out', + }); + }); + + it('parses incoming relationship: (a)<-[r:import]-(b)', () => { + const ast = parse('MATCH (a)<-[r:import]-(b) RETURN a'); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', direction: 'in', + }); + }); + + it('parses undirected relationship: (a)-[r]-(b)', () => { + const ast = parse('MATCH (a)-[r]-(b) RETURN a'); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', direction: 'both', + }); + }); + + it('parses anonymous node and relationship: ()-[]->()', () => { + const ast = parse('MATCH ()-[]->() RETURN *'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: undefined, + }); + }); + + it('parses WHERE with comparison', () => { + const ast = parse("MATCH (n) WHERE n.name = 'foo' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', + operator: '=', + left: { kind: 'propertyAccess', variable: 'n', property: 'name' }, + right: { kind: 'literal', value: 'foo' }, + }); + }); + + it('parses WHERE with AND/OR', () => { + const ast = parse("MATCH (n) WHERE n.name = 'a' AND n.path = 'b' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'logical', op: 'AND', + }); + }); + + it('parses WHERE with NOT', () => { + const ast = parse("MATCH (n) WHERE NOT n.name = 'a' RETURN n"); + expect(ast.whereClause).toMatchObject({ kind: 'not' }); + }); + + it('parses WHERE with CONTAINS', () => { + const ast = parse("MATCH (n) WHERE n.name CONTAINS 'util' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'CONTAINS', + }); + }); + + it('parses WHERE with STARTS WITH', () => { + const ast = parse("MATCH (n) WHERE n.name STARTS WITH 'src' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'STARTS WITH', + }); + }); + + it('parses WHERE with ENDS WITH', () => { + const ast = parse("MATCH (n) WHERE n.name ENDS WITH '.js' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'ENDS WITH', + }); + }); + + it('parses RETURN with property access', () => { + const ast = parse('MATCH (n) RETURN n.name, n.path'); + expect(ast.returnClause.items).toHaveLength(2); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'propertyAccess', variable: 'n', property: 'name', + }); + }); + + it('parses RETURN with alias', () => { + const ast = parse('MATCH (n) RETURN n.name AS fileName'); + expect(ast.returnClause.items[0].alias).toBe('fileName'); + }); + + it('parses RETURN DISTINCT', () => { + const ast = parse('MATCH (n) RETURN DISTINCT n.name'); + expect(ast.returnClause.distinct).toBe(true); + }); + + it('parses COUNT function', () => { + const ast = parse('MATCH (n) RETURN COUNT(n)'); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'functionCall', name: 'COUNT', + }); + }); + + it('parses type() function', () => { + const ast = parse('MATCH (a)-[r]->(b) RETURN type(r)'); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'functionCall', name: 'TYPE', + }); + }); + + it('parses LIMIT', () => { + const ast = parse('MATCH (n) RETURN n LIMIT 10'); + expect(ast.limit).toBe(10); + }); + + it('parses ORDER BY', () => { + const ast = parse('MATCH (n) RETURN n.name ORDER BY n.name DESC'); + expect(ast.orderByClause!.items[0].descending).toBe(true); + }); + + it('throws CypherSyntaxError on bad input', () => { + expect(() => parse('MATCH RETURN')).toThrow(); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx jest test/cypher/parser.test.ts --no-cache` +Expected: FAIL — `parse` not exported + +- [ ] **Step 3: Implement the parser** + +Add `parse(input: string): CypherQuery` function to `src/cypher/parser.ts`. + +Parser structure (recursive descent): +- `parseQuery()` → calls `parseMatch()`, optionally `parseWhere()`, `parseReturn()`, optionally `parseOrderBy()`, optionally `parseLimit()` +- `parseMatch()` → parses pattern chain: alternating `parseNodePattern()` and `parseRelationshipPattern()` +- `parseNodePattern()` → `(` optional-variable optional-`:label` optional-`{props}` `)` +- `parseRelationshipPattern()` → detects direction from leading `<-` or `-`, parses `[` optional-variable optional-`:TYPE` `]`, detects trailing `->` or `-` +- `parseWhere()` → `parseOrExpr()` → `parseAndExpr()` → `parseNotExpr()` → `parseComparison()` +- `parseComparison()` → `parseAtom() (operator parseAtom())?` +- `parseAtom()` → literal | property access (`ident.ident`) | variable ref | function call | `(` expr `)` +- `parseReturn()` → optional DISTINCT, comma-separated return items, each with optional `AS alias` +- `parseReturnItem()` → function call | property access | variable ref +- `parseOrderBy()` → comma-separated `expression (ASC|DESC)?` +- `parseLimit()` → NUMBER + +Use a `Parser` class internally that tracks `tokens[]` and `pos`, with helpers `peek()`, `advance()`, `expect(type)`, `match(type)`. + +- [ ] **Step 4: Run parser tests** + +Run: `npx jest test/cypher/parser.test.ts --no-cache` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/cypher/parser.ts test/cypher/parser.test.ts +git commit -m "feat(cypher): add recursive descent parser" +``` + +--- + +## Chunk 2: Graph Index + +### Task 3: CypherGraph + +**Files:** +- Create: `src/cypher/graph.ts` +- Create: `test/cypher/graph.test.ts` + +- [ ] **Step 1: Write graph index tests** + +Create `test/cypher/graph.test.ts`: + +```typescript +import { SElement, SElementAssociation } from '../../src/selement'; +import { CypherGraph } from '../../src/cypher/graph'; + +describe('CypherGraph', () => { + let graph: CypherGraph; + + beforeAll(() => { + const root = new SElement('', undefined); + const project = new SElement('myproject', root); + project.setType('dir'); + const fileA = new SElement('a.js', project); + fileA.setType('file'); + fileA.addAttribute('hash', 'abc123'); + const fileB = new SElement('b.js', project); + fileB.setType('file'); + const ea = new SElementAssociation(fileA, fileB, 'import'); + ea.initElems(); + + graph = new CypherGraph(root); + }); + + it('indexes all elements as nodes', () => { + // root + project + a.js + b.js = 4 + expect(graph.nodeCount).toBe(4); + }); + + it('indexes associations as edges', () => { + // 1 import edge + expect(graph.edgeCount).toBeGreaterThanOrEqual(1); + }); + + it('adds CONTAINS edges by default', () => { + // root->project, project->a.js, project->b.js = 3 CONTAINS + // plus 1 import = 4 total + expect(graph.edgeCount).toBe(4); + }); + + it('skips CONTAINS edges when includeHierarchy is false', () => { + const root = new SElement('', undefined); + const child = new SElement('c', root); + child.setType('file'); + const g = new CypherGraph(root, false); + expect(g.edgeCount).toBe(0); + }); + + it('sets node labels from element type', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node).toBeDefined(); + expect(node!.labels.has('file')).toBe(true); + }); + + it('sets node properties including name and path', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.name).toBe('a.js'); + expect(node!.properties.path).toBe('myproject/a.js'); + }); + + it('flattens single-element array attrs to scalars', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.hash).toBe('abc123'); + }); + + it('does not include type in properties', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.type).toBeUndefined(); + }); + + it('sets edge type from deptype', () => { + const edges = graph.getAllEdges(); + const importEdge = edges.find(e => e.type === 'import'); + expect(importEdge).toBeDefined(); + }); + + it('provides outgoing and incoming edge lookups', () => { + const nodeA = graph.getNodeByPath('myproject/a.js'); + const outEdges = graph.getOutEdges(nodeA!.id); + const importEdges = outEdges.filter(eid => graph.getEdge(eid).type === 'import'); + expect(importEdges.length).toBe(1); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx jest test/cypher/graph.test.ts --no-cache` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement CypherGraph** + +Create `src/cypher/graph.ts`: + +```typescript +import { SElement } from '../selement'; +import { SElementAssociation } from '../selement'; + +interface NodeData { + id: number; + labels: Set; + properties: Record; +} + +interface EdgeData { + id: number; + type: string; + properties: Record; + src: number; + dst: number; +} + +class CypherGraph { + private nodeData: Map = new Map(); + private edgeData: Map = new Map(); + private nodeOutEdges: Map = new Map(); + private nodeInEdges: Map = new Map(); + private nodeToElem: Map = new Map(); + private edgeToAssoc: Map = new Map(); + private pathToNode: Map = new Map(); + + constructor(root: SElement, includeHierarchy = true) { + this.buildIndex(root, includeHierarchy); + } + + // ... three-pass indexing as specified in design + // Pass 1: traverse all elements, create nodes + // Pass 2: traverse all outgoing associations, create edges (dedup by identity) + // Pass 3: if includeHierarchy, create CONTAINS edges + + // Flatten attributes: single-element arrays → scalars + // Add name, path; remove type from properties + + // Public accessors: + get nodeCount(): number + get edgeCount(): number + getAllNodes(): NodeData[] + getAllEdges(): EdgeData[] + getNode(id: number): NodeData + getEdge(id: number): EdgeData + getOutEdges(nodeId: number): number[] + getInEdges(nodeId: number): number[] + getEdgeSrc(edgeId: number): number + getEdgeDst(edgeId: number): number + getNodeByPath(path: string): NodeData | undefined + getElemForNode(nodeId: number): SElement | undefined + getAssocForEdge(edgeId: number): SElementAssociation | null | undefined +} +``` + +- [ ] **Step 4: Run graph tests** + +Run: `npx jest test/cypher/graph.test.ts --no-cache` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/cypher/graph.ts test/cypher/graph.test.ts +git commit -m "feat(cypher): add CypherGraph index" +``` + +--- + +## Chunk 3: Executor + +### Task 4: Executor — Pattern Matching, WHERE, Aggregation + +**Files:** +- Create: `src/cypher/executor.ts` +- Create: `test/cypher/executor.test.ts` + +**Note:** `CypherResult` is defined in `executor.ts` and re-exported from `index.ts` (Task 6) to avoid circular imports. + +- [ ] **Step 1: Write ALL executor tests (pattern matching + WHERE + aggregation)** + +Create `test/cypher/executor.test.ts`: + +```typescript +import { SElement, SElementAssociation } from '../../src/selement'; +import { CypherGraph } from '../../src/cypher/graph'; +import { CypherExecutor } from '../../src/cypher/executor'; +import { parse } from '../../src/cypher/parser'; + +function buildTestGraph(): CypherGraph { + const root = new SElement('', undefined); + const project = new SElement('myproject', root); + project.setType('dir'); + const fileA = new SElement('a.js', project); + fileA.setType('file'); + const fileB = new SElement('b.js', project); + fileB.setType('file'); + const ea = new SElementAssociation(fileA, fileB, 'import'); + ea.initElems(); + return new CypherGraph(root); +} + +let graph: CypherGraph; +let executor: CypherExecutor; + +beforeAll(() => { + graph = buildTestGraph(); + executor = new CypherExecutor(graph); +}); + +describe('CypherExecutor pattern matching', () => { + it('matches all nodes', () => { + const result = executor.execute(parse('MATCH (n) RETURN n.name')); + expect(result.columns).toContain('n.name'); + expect(result.rows.length).toBe(4); // root + project + a.js + b.js + }); + + it('matches nodes by label', () => { + const result = executor.execute(parse('MATCH (n:file) RETURN n.name')); + expect(result.rows.length).toBe(2); + const names = result.rows.map(r => r['n.name']); + expect(names).toContain('a.js'); + expect(names).toContain('b.js'); + }); + + it('matches nodes by inline property', () => { + const result = executor.execute(parse("MATCH (n {name: 'a.js'}) RETURN n.name")); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('a.js'); + }); + + it('matches outgoing relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['a.name']).toBe('a.js'); + expect(result.rows[0]['b.name']).toBe('b.js'); + }); + + it('matches incoming relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)<-[r:import]-(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['a.name']).toBe('b.js'); + expect(result.rows[0]['b.name']).toBe('a.js'); + }); + + it('matches undirected relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)-[r:import]-(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(2); // both directions + }); + + it('matches CONTAINS relationships', () => { + const result = executor.execute(parse( + 'MATCH (d:dir)-[:CONTAINS]->(f:file) RETURN d.name, f.name' + )); + expect(result.rows.length).toBe(2); + }); + + it('returns full node object for variable return', () => { + const result = executor.execute(parse("MATCH (n {name: 'a.js'}) RETURN n")); + expect(result.rows[0]['n']).toMatchObject({ + name: 'a.js', type: 'file', + }); + expect(result.rows[0]['n'].path).toBeDefined(); + }); + + it('returns edge info for relationship variable', () => { + const result = executor.execute(parse( + 'MATCH (a)-[r:import]->(b) RETURN r' + )); + expect(result.rows[0]['r']).toMatchObject({ type: 'import' }); + expect(result.rows[0]['r'].from).toBeDefined(); + expect(result.rows[0]['r'].to).toBeDefined(); + }); + + it('returns type() function result', () => { + const result = executor.execute(parse( + "MATCH (a)-[r]->(b) WHERE type(r) = 'import' RETURN type(r)" + )); + expect(result.rows.length).toBeGreaterThanOrEqual(1); + expect(result.rows[0]['type(r)']).toBe('import'); + }); + + it('applies LIMIT', () => { + const result = executor.execute(parse('MATCH (n) RETURN n.name LIMIT 2')); + expect(result.rows.length).toBe(2); + }); +}); + +describe('CypherExecutor WHERE', () => { + it('filters by string equality', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE n.name = 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('a.js'); + }); + + it('filters by inequality', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE n.name <> 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('b.js'); + }); + + it('filters with CONTAINS', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name CONTAINS '.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with STARTS WITH', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name STARTS WITH 'a' RETURN n.name" + )); + const names = result.rows.map(r => r['n.name']); + expect(names).toContain('a.js'); + }); + + it('filters with ENDS WITH', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name ENDS WITH '.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with AND', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name CONTAINS '.js' AND n.name STARTS WITH 'a' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + }); + + it('filters with OR', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name = 'a.js' OR n.name = 'b.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with NOT', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE NOT n.name = 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + }); +}); + +describe('CypherExecutor aggregation', () => { + it('COUNT returns correct count', () => { + const result = executor.execute(parse('MATCH (n:file) RETURN COUNT(n)')); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['COUNT(n)']).toBe(2); + }); + + it('COUNT with grouping', () => { + const result = executor.execute(parse( + 'MATCH (n) RETURN n.name, COUNT(n) ORDER BY n.name' + )); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('COLLECT aggregates values', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN COLLECT(n.name)' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['COLLECT(n.name)']).toContain('a.js'); + expect(result.rows[0]['COLLECT(n.name)']).toContain('b.js'); + }); + + it('RETURN DISTINCT deduplicates', () => { + const result = executor.execute(parse( + 'MATCH (n) RETURN DISTINCT n.name' + )); + const names = result.rows.map(r => r['n.name']); + expect(new Set(names).size).toBe(names.length); + }); + + it('ORDER BY sorts results', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN n.name ORDER BY n.name ASC' + )); + expect(result.rows[0]['n.name']).toBe('a.js'); + expect(result.rows[1]['n.name']).toBe('b.js'); + }); + + it('ORDER BY DESC', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN n.name ORDER BY n.name DESC' + )); + expect(result.rows[0]['n.name']).toBe('b.js'); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx jest test/cypher/executor.test.ts --no-cache` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement executor** + +Create `src/cypher/executor.ts` with the full executor including pattern matching, WHERE evaluation, and aggregation: + +```typescript +import { CypherGraph } from './graph'; +import { + CypherQuery, CypherExecutionError, + // ... other AST types as needed +} from './parser'; + +export interface CypherResult { + columns: string[]; + rows: Record[]; +} + +type Binding = Map; + +export class CypherExecutor { + constructor(private graph: CypherGraph) {} + + execute(query: CypherQuery): CypherResult { + // 1. Pattern match → bindings + // 2. WHERE filter + // 3. RETURN project (with aggregation grouping) + // 4. DISTINCT + // 5. ORDER BY + // 6. LIMIT + } +} +``` + +Key implementation details: +- Pattern matching: iterate node patterns and relationship patterns alternately. For each node pattern, if it's the first in the chain, scan all graph nodes. For subsequent nodes (connected by a relationship), expand from the previously bound node via edges. +- WHERE comparison with array properties: if the property value is an array, check if any element matches. +- Aggregation grouping: partition bindings by non-aggregated return items, then compute COUNT/COLLECT per group. When all items are aggregations, all bindings form a single group. +- `type(r)` resolves to the edge's type string. +- RETURN `n` (bare variable): for nodes → `{name, path, type, properties}`, for edges → `{from, to, type, properties}` where `from`/`to` are path strings. +- `CypherResult` is exported from this file (re-exported by `index.ts` in Task 6). + +- [ ] **Step 4: Run executor tests** + +Run: `npx jest test/cypher/executor.test.ts --no-cache` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/cypher/executor.ts test/cypher/executor.test.ts +git commit -m "feat(cypher): add query executor with pattern matching, WHERE, and aggregation" +``` + +--- + +## Chunk 4: Public API, Integration Tests, and CLI + +### Task 6: Public API and Integration Tests + +**Files:** +- Create: `src/cypher/index.ts` +- Modify: `src/index.ts` +- Create: `test/cypher/integration.test.ts` + +- [ ] **Step 1: Write integration tests** + +Create `test/cypher/integration.test.ts`: + +```typescript +import { readFile } from 'fs/promises'; +import { SGraph, cypherQuery } from '../../src'; + +describe('Cypher integration (modelfile.xml)', () => { + let model: SGraph; + + beforeAll(async () => { + const data = await readFile('test/modelfile.xml', 'utf8'); + model = SGraph.parseXml({ data }); + }); + + it('finds all file elements', () => { + const result = cypherQuery(model, 'MATCH (n:file) RETURN n.name'); + expect(result.rows.length).toBeGreaterThan(0); + const names = result.rows.map(r => r['n.name']); + expect(names).toContain('index.js'); + expect(names).toContain('math.js'); + expect(names).toContain('models.js'); + }); + + it('finds import relationships', () => { + const result = cypherQuery(model, + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name, b.name' + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('finds elements by name', () => { + const result = cypherQuery(model, + "MATCH (n {name: 'math.js'}) RETURN n.name, n.path" + ); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.path']).toBe('mock-project/src/utils/math.js'); + }); + + it('queries CONTAINS hierarchy', () => { + const result = cypherQuery(model, + "MATCH (d {name: 'src'})-[:CONTAINS]->(f) RETURN f.name" + ); + // src contains: database, index.js, utils + expect(result.rows.length).toBe(3); + }); + + it('counts elements by type using type()', () => { + const result = cypherQuery(model, + 'MATCH (a)-[r]->(b) RETURN type(r), COUNT(r)' + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('works without hierarchy — no CONTAINS edges', () => { + const result = cypherQuery(model, + 'MATCH (a)-[r:CONTAINS]->(b) RETURN a.name', + { includeHierarchy: false } + ); + expect(result.rows.length).toBe(0); + }); + + it('works without hierarchy — non-CONTAINS edges still present', () => { + const result = cypherQuery(model, + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name', + { includeHierarchy: false } + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('LIMIT works', () => { + const result = cypherQuery(model, 'MATCH (n) RETURN n.name LIMIT 3'); + expect(result.rows.length).toBe(3); + }); +}); +``` + +- [ ] **Step 2: Create public API** + +Create `src/cypher/index.ts`: + +```typescript +import { SGraph } from '../sgraph'; +import { CypherGraph } from './graph'; +import { CypherExecutor } from './executor'; +import { parse } from './parser'; + +export { CypherSyntaxError, CypherExecutionError } from './parser'; +export type { CypherResult } from './executor'; + +export interface CypherQueryOptions { + includeHierarchy?: boolean; +} + +export function cypherQuery( + model: SGraph, + query: string, + options: CypherQueryOptions = {} +): CypherResult { + const { includeHierarchy = true } = options; + const graph = new CypherGraph(model.rootNode, includeHierarchy); + const executor = new CypherExecutor(graph); + const ast = parse(query); + return executor.execute(ast); +} +``` + +- [ ] **Step 3: Add export to main index** + +Modify `src/index.ts` — add: + +```typescript +export * from './cypher'; +``` + +- [ ] **Step 4: Run integration tests** + +Run: `npx jest test/cypher/integration.test.ts --no-cache` +Expected: All PASS + +- [ ] **Step 5: Run ALL tests to verify nothing is broken** + +Run: `npx jest --no-cache` +Expected: All suites PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/cypher/index.ts src/index.ts test/cypher/integration.test.ts +git commit -m "feat(cypher): add public API and integration tests" +``` + +### Task 7: CLI + +**Files:** +- Create: `src/cypher/cli.ts` + +- [ ] **Step 1: Implement CLI** + +Create `src/cypher/cli.ts`: + +```typescript +import { SGraph } from '../sgraph'; +import { CypherGraph } from './graph'; +import { CypherExecutor } from './executor'; +import { parse, CypherSyntaxError, CypherExecutionError } from './parser'; +import type { CypherResult } from './index'; + +// Parse CLI args manually (no external deps) +// argv: [node, script, model-path, optional-query, --format, --no-hierarchy] + +// Format functions: +// table: pad columns, align +// json: JSON.stringify(result.rows, null, 2) +// csv: header line + comma-separated values + +// Single query mode: parse model, build graph, execute query, print result +// REPL mode: readline interface, cypher> prompt, ; or blank line to execute +// quit/exit to exit, show timing + +async function main() { + const args = process.argv.slice(2); + // parse --format, --no-hierarchy flags + // remaining positional: modelPath, optional query + + const t0 = Date.now(); + const model = await SGraph.parseXmlFileOrZippedXml({ filePath: modelPath }); + if (!model) { console.error('Failed to load model'); process.exit(1); } + console.error(`Loaded in ${Date.now() - t0}ms`); + + const t1 = Date.now(); + const graph = new CypherGraph(model.rootNode, includeHierarchy); + console.error(`Index: ${graph.nodeCount} nodes, ${graph.edgeCount} edges (${Date.now() - t1}ms)`); + + const executor = new CypherExecutor(graph); + + if (query) { + // single query mode + const ast = parse(query); + const result = executor.execute(ast); + printResult(result, format); + } else { + // REPL mode using readline + runRepl(executor, format); + } +} + +main().catch(e => { console.error(e.message); process.exit(1); }); +``` + +- [ ] **Step 2: Compile and test CLI manually** + +Run: +```bash +npm run compile +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) RETURN n.name" +``` +Expected: Table showing 5 file rows (package.json, 2x index.js, models.js, math.js) + +```bash +node dist/cypher/cli.js test/modelfile.xml "MATCH (a)-[r:import]->(b) RETURN a.name, b.name" +``` +Expected: Table showing import dependencies + +```bash +node dist/cypher/cli.js test/modelfile.xml "MATCH (n) RETURN n.name, n.path" --format json +``` +Expected: JSON array of objects + +```bash +node dist/cypher/cli.js test/modelfile.xml "MATCH (a)-[r]->(b) RETURN type(r), COUNT(r)" +``` +Expected: Table showing dependency type counts + +- [ ] **Step 3: Commit** + +```bash +git add src/cypher/cli.ts +git commit -m "feat(cypher): add CLI with REPL support" +``` + +### Task 8: Final Verification + +- [ ] **Step 1: Run full test suite** + +Run: `npx jest --no-cache` +Expected: All suites PASS + +- [ ] **Step 2: Compile** + +Run: `npm run compile` +Expected: No errors + +- [ ] **Step 3: Run CLI end-to-end smoke tests** + +```bash +# Basic queries +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) RETURN n.name" +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:dir) RETURN n.name, n.path" +node dist/cypher/cli.js test/modelfile.xml "MATCH (a:file)-[r:import]->(b:file) RETURN a.name, b.name" +node dist/cypher/cli.js test/modelfile.xml "MATCH (n {name: 'math.js'}) RETURN n.path" + +# WHERE filtering +node dist/cypher/cli.js test/modelfile.xml "MATCH (n) WHERE n.name CONTAINS '.js' RETURN n.name" +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) WHERE n.name STARTS WITH 'index' RETURN n.name, n.path" + +# Aggregation +node dist/cypher/cli.js test/modelfile.xml "MATCH (n) RETURN COUNT(n)" +node dist/cypher/cli.js test/modelfile.xml "MATCH (a)-[r]->(b) RETURN type(r), COUNT(r)" +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) RETURN COLLECT(n.name)" + +# Hierarchy +node dist/cypher/cli.js test/modelfile.xml "MATCH (d:dir)-[:CONTAINS]->(f:file) RETURN d.name, f.name" + +# Formats +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) RETURN n.name" --format json +node dist/cypher/cli.js test/modelfile.xml "MATCH (n:file) RETURN n.name" --format csv + +# No hierarchy +node dist/cypher/cli.js test/modelfile.xml "MATCH (n)-[:CONTAINS]->(m) RETURN n.name" --no-hierarchy +``` + +Verify each produces sensible output matching the model data. + +- [ ] **Step 4: Update PYTHON_MAPPING.md** + +Add Cypher section to `/Users/ville/code/sgraph.js/PYTHON_MAPPING.md`: + +```markdown +## Cypher Query Support + +| Python (`cypher.py`) | TypeScript (`src/cypher/`) | +|---|---| +| `SGraphCypherBackend` | `CypherGraph` in `graph.ts` | +| `cypher_query(model, query, include_hierarchy)` | `cypherQuery(model, query, {includeHierarchy})` | +| `SGraphCypherExecutor` (sPyCy) | `CypherExecutor` in `executor.ts` | +| Result: `pd.DataFrame` | Result: `CypherResult {columns, rows}` | +| CLI: `python -m sgraph.cypher` | CLI: `node dist/cypher/cli.js` | +| `_extract_subgraph()` | Not ported | +| Graph output formats | Not ported (use existing converters) | +``` + +- [ ] **Step 5: Final commit** + +```bash +git add PYTHON_MAPPING.md +git commit -m "docs: update Python mapping with Cypher support" +``` diff --git a/docs/superpowers/specs/2026-03-16-cypher-support-design.md b/docs/superpowers/specs/2026-03-16-cypher-support-design.md new file mode 100644 index 0000000..af9ef7a --- /dev/null +++ b/docs/superpowers/specs/2026-03-16-cypher-support-design.md @@ -0,0 +1,226 @@ +# Cypher Query Support for sgraph.js + +## Overview + +Lightweight Cypher query interpreter for sgraph.js, enabling read-only Cypher queries against SGraph models. Mirrors the Python sgraph `cypher.py` module's API but uses a hand-written parser+executor instead of the sPyCy dependency. Zero external dependencies. + +## Motivation + +The Python sgraph library supports Cypher queries via sPyCy. No equivalent JS library exists with a compatible license (MIT). This implements a purpose-built Cypher subset interpreter that covers the practical query patterns used with code analysis models. + +## Supported Cypher Subset + +### In scope + +- `MATCH` with node patterns: `(n)`, `(n:label)`, `(n:label {prop: 'value'})` +- `MATCH` with relationship patterns: `-[r:TYPE]->`, `<-[r:TYPE]-`, `-[r:TYPE]-` +- `WHERE` with comparisons: `=`, `<>`, `<`, `>`, `<=`, `>=` +- `WHERE` string operators: `CONTAINS`, `STARTS WITH`, `ENDS WITH` +- `WHERE` logical operators: `AND`, `OR`, `NOT` +- `RETURN` with property access (`n.name`), variables (`n`, `r`), aliases (`AS`) +- `RETURN DISTINCT` +- `COUNT()`, `COLLECT()`, `type()` functions +- `ORDER BY` (ASC/DESC) +- `LIMIT` +- Inline property matching: `(n {name: 'foo'})` +- Undirected relationships: `(a)-[r]-(b)` + +### Out of scope + +- Write operations (`CREATE`, `SET`, `DELETE`, `MERGE`) +- Variable-length paths (`[*1..3]`) +- `OPTIONAL MATCH`, `UNION`, `WITH` (as clause), subqueries +- `UNWIND`, `CASE`, list comprehensions +- User-defined functions + +## Architecture + +``` +src/cypher/ + parser.ts -- tokenizer + recursive descent parser -> AST + graph.ts -- indexed graph backend (SElement -> nodes, associations -> edges) + executor.ts -- pattern matching, WHERE evaluation, RETURN projection + index.ts -- public API: cypherQuery() + cli.ts -- CLI entry point +``` + +### graph.ts -- Graph Index + +Maps SGraph to a labeled property graph. Three-pass indexing (mirrors Python `SGraphCypherBackend`): + +**Pass 1 -- Nodes:** Each SElement becomes a node with: +- Numeric ID (sequential) +- Labels from element type (e.g., `file`, `dir`, `function`) +- Properties from element attributes + auto-added `name` and `path` (with `type` removed from properties since it's already a label) +- The virtual root SElement (unnamed container) is indexed but gets an empty-string `path` +- **Attribute flattening:** SElement attributes are stored as `string[]` arrays (except `type`). When building node properties, single-element arrays are flattened to scalar strings (e.g., `['63']` becomes `'63'`). Multi-element arrays are kept as arrays. This matches the natural expectation for WHERE comparisons. + +**Pass 2 -- Edges:** Each SElementAssociation becomes an edge with: +- Numeric ID (sequential) +- Type from `deptype` (e.g., `function_ref`, `import`) +- Properties from association attributes +- Deduplicated by association identity + +**Pass 3 -- Hierarchy (optional, default on):** Parent-child relationships become `:CONTAINS` edges. These are directional (parent -> child). Under undirected pattern matching `(a)-[r]-(b)`, CONTAINS edges match in both directions like all other edges. + +**Index structures:** +- `nodeData[id]` -> `{labels: Set, properties: Record}` +- `edgeData[id]` -> `{type: string, properties: Record}` +- `nodeOut[id]` -> outgoing edge IDs +- `nodeIn[id]` -> incoming edge IDs +- `edgeSrc[id]` / `edgeDst[id]` -> source/target node IDs +- Reverse maps: `nodeToElem[id]` -> SElement, `edgeToAssoc[id]` -> SElementAssociation | null + +### parser.ts -- Tokenizer + Parser + +**Tokenizer** produces tokens: +- Keywords: MATCH, WHERE, RETURN, AS, LIMIT, ORDER, BY, ASC, DESC, DISTINCT, AND, OR, NOT, CONTAINS, STARTS, ENDS, WITH, COUNT, COLLECT, TYPE, TRUE, FALSE +- Symbols: `( ) [ ] - > < { } : . , = <> <= >=` +- Identifiers, strings (single/double quoted), numbers + +**Note on multi-token operators:** `STARTS WITH` and `ENDS WITH` are parsed as two-token sequences in the WHERE expression grammar. The `WITH` keyword is tokenized but only valid as part of `STARTS WITH` / `ENDS WITH`; standalone `WITH` as a clause keyword is rejected by the parser. + +**Recursive descent parser** produces AST: + +``` +CypherQuery { + matchClause: MatchClause + whereClause?: WhereClause + returnClause: ReturnClause + orderByClause?: OrderByClause + limit?: number +} + +MatchClause { + pattern: PatternElement[] -- alternating NodePattern and RelationshipPattern +} + +NodePattern { + variable?: string + labels: string[] + properties?: Record +} + +RelationshipPattern { + variable?: string + types: string[] + direction: 'out' | 'in' | 'both' +} + +WhereClause -- expression tree: + Comparison { left, operator, right } + LogicalExpr { op: 'AND'|'OR', left, right } + NotExpr { expr } + PropertyAccess { variable, property } + Literal { value: string | number | boolean | null } + +ReturnClause { + distinct: boolean + items: ReturnItem[] +} + +ReturnItem { + expression: PropertyAccess | VariableRef | FunctionCall + alias?: string +} + +FunctionCall { + name: 'COUNT' | 'COLLECT' | 'TYPE' + argument: Expression +} + +OrderByClause { + items: { expression: Expression, descending: boolean }[] +} +``` + +**Error handling:** The parser throws `CypherSyntaxError` on malformed queries with a message indicating the position and unexpected token. The executor throws `CypherExecutionError` for runtime errors (e.g., unknown variable in WHERE). Both extend `Error`. + +### executor.ts -- Query Executor + +**Execution pipeline:** + +1. **Pattern matching:** For each node pattern, scan all graph nodes filtered by label and inline properties. For each relationship pattern, follow edges from bound source nodes in the specified direction, filtered by type. Produces a list of bindings (variable -> nodeId/edgeId maps). + +2. **WHERE filtering:** Evaluate expression tree recursively against each binding. Property access resolves variable to node/edge, then reads the property. Comparisons apply the operator. When comparing against array-valued properties, the comparison checks if any element in the array matches (membership semantics). AND/OR short-circuit. NOT inverts. + +3. **RETURN projection:** For each binding, resolve return expressions: + - `n.prop` -> node/edge property value + - `n` -> `{name, path, type, properties}` for nodes + - `r` -> `{from, to, type, properties}` for edges, where `from` and `to` are the source/target node paths (strings) + - `type(r)` -> edge type string (the `deptype`) + - `COUNT(x)` / `COLLECT(x)` -> aggregate across bindings grouped by non-aggregated return items. When all RETURN items are aggregations (no group-by columns), all bindings form a single group. + - `COLLECT(x)` returns `any[]` in the result column + +4. **ORDER BY + LIMIT:** Sort result rows, then truncate. + +**Result type:** +```typescript +interface CypherResult { + columns: string[]; + rows: Record[]; +} +``` + +### index.ts -- Public API + +```typescript +function cypherQuery( + model: SGraph, + query: string, + options?: { includeHierarchy?: boolean } +): CypherResult; +``` + +Exported from main `src/index.ts`. Maps to Python's `cypher_query(model, query, include_hierarchy)`. + +### cli.ts -- CLI + +Invoked via compiled output (no `ts-node` dependency): + +``` +npm run compile && node dist/cypher/cli.js [query] [--format table|json|csv] [--no-hierarchy] +``` + +- Single query mode: pass query as second arg +- Interactive REPL: omit query arg. `cypher>` prompt, `;` or blank line to execute, `quit` to exit. +- Reports load time, node/edge count on startup +- Output formats: `table` (default), `json`, `csv` + +## Python Mapping + +| Python (`cypher.py`) | TypeScript (`src/cypher/`) | +|---|---| +| `SGraphCypherBackend` class | `CypherGraph` class in `graph.ts` | +| `cypher_query(model, query, include_hierarchy)` | `cypherQuery(model, query, {includeHierarchy})` | +| `SGraphCypherExecutor` (sPyCy) | `CypherExecutor` class in `executor.ts` | +| Result: `pd.DataFrame` | Result: `CypherResult {columns, rows}` | +| CLI: `python -m sgraph.cypher` | CLI: `node dist/cypher/cli.js` | +| `_extract_subgraph()` | Not ported (add later if needed) | +| Graph output formats (xml, dot, plantuml) | Not ported (use existing converters separately) | + +## Testing Strategy + +- **Parser unit tests:** Various Cypher patterns -> correct AST structures +- **Executor unit tests:** Queries against programmatically built graphs -> expected bindings +- **Integration tests:** Full queries against `test/modelfile.xml` -> expected result rows +- **CLI manual verification:** Run CLI against test model during development to verify end-to-end + +## Example Queries (for testing) + +```cypher +-- Find all files +MATCH (n:file) RETURN n.name + +-- Find dependencies between files +MATCH (a:file)-[r:function_ref]->(b:file) RETURN a.name, b.name, r + +-- Find elements by name +MATCH (n {name: 'index.js'}) RETURN n.name, n.path + +-- Find what a directory contains +MATCH (d:dir)-[:CONTAINS]->(f) RETURN d.name, f.name + +-- Count dependencies by type +MATCH (a)-[r]->(b) RETURN type(r), COUNT(r) +``` diff --git a/src/cypher/cli.ts b/src/cypher/cli.ts new file mode 100644 index 0000000..67080d2 --- /dev/null +++ b/src/cypher/cli.ts @@ -0,0 +1,205 @@ +import { SGraph } from '../sgraph'; +import { CypherGraph } from './graph'; +import { CypherExecutor } from './executor'; +import { parse, CypherSyntaxError } from './parser'; +import type { CypherResult } from './executor'; + +function printUsage() { + console.error( + 'Usage: node cli.js [query] [--format table|json|csv] [--no-hierarchy]' + ); + console.error(''); + console.error(' model-file Path to .xml or .xml.zip sgraph model'); + console.error(' query Cypher query to execute (omit for REPL mode)'); + console.error(' --format Output format: table (default), json, csv'); + console.error(' --no-hierarchy Exclude CONTAINS hierarchy edges'); +} + +function parseArgs(argv: string[]): { + modelPath: string | undefined; + query: string | undefined; + format: string; + includeHierarchy: boolean; +} { + let format = 'table'; + let includeHierarchy = true; + const positional: string[] = []; + + let i = 0; + while (i < argv.length) { + const arg = argv[i]; + if (arg === '--format' && i + 1 < argv.length) { + format = argv[i + 1]; + i += 2; + } else if (arg === '--no-hierarchy') { + includeHierarchy = false; + i++; + } else if (arg === '--help' || arg === '-h') { + printUsage(); + process.exit(0); + } else { + positional.push(arg); + i++; + } + } + + return { + modelPath: positional[0], + query: positional[1], + format, + includeHierarchy, + }; +} + +function printResult(result: CypherResult, format: string): void { + if (result.rows.length === 0) { + if (format === 'table') console.log('(no results)'); + else if (format === 'json') console.log('[]'); + return; + } + + if (format === 'json') { + console.log(JSON.stringify(result.rows, null, 2)); + } else if (format === 'csv') { + console.log(result.columns.join(',')); + for (const row of result.rows) { + console.log( + result.columns + .map((c) => { + const v = row[c]; + const s = + typeof v === 'object' && v !== null + ? JSON.stringify(v) + : String(v ?? ''); + return s.includes(',') ? `"${s}"` : s; + }) + .join(',') + ); + } + } else { + // table format -- calculate column widths and pad + const cols = result.columns; + const widths = cols.map((c) => c.length); + const stringRows = result.rows.map((row) => + cols.map((c, i) => { + const v = row[c]; + const s = + typeof v === 'object' && v !== null + ? JSON.stringify(v) + : String(v ?? ''); + widths[i] = Math.max(widths[i], s.length); + return s; + }) + ); + // Header + console.log(cols.map((c, i) => c.padEnd(widths[i])).join(' ')); + console.log(cols.map((_, i) => '-'.repeat(widths[i])).join(' ')); + // Rows + for (const row of stringRows) { + console.log(row.map((v, i) => v.padEnd(widths[i])).join(' ')); + } + } +} + +function runRepl(executor: CypherExecutor, format: string): void { + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + + console.error( + 'Enter Cypher queries. End with ; or blank line. Type "quit" to exit.' + ); + + let lines: string[] = []; + + const prompt = () => + rl.question( + lines.length === 0 ? 'cypher> ' : ' > ', + handleLine + ); + + function handleLine(line: string) { + const trimmed = line.trim(); + if (trimmed === 'quit' || trimmed === 'exit') { + rl.close(); + return; + } + + lines.push(line); + if (trimmed.endsWith(';') || trimmed === '') { + let queryStr = lines.join(' ').trim(); + if (queryStr.endsWith(';')) queryStr = queryStr.slice(0, -1).trim(); + lines = []; + + if (queryStr) { + try { + const t0 = Date.now(); + const ast = parse(queryStr); + const result = executor.execute(ast); + printResult(result, format); + console.error( + `(${result.rows.length} rows, ${Date.now() - t0}ms)` + ); + } catch (e: any) { + console.error('Error:', e.message); + } + } + } + prompt(); + } + + prompt(); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (!args.modelPath) { + printUsage(); + process.exit(1); + } + + const t0 = Date.now(); + const sgraph = await SGraph.parseXmlFileOrZippedXml({ + filePath: args.modelPath, + }); + + if (!sgraph) { + console.error(`Error: Could not load model from '${args.modelPath}'`); + process.exit(1); + } + + const graph = new CypherGraph(sgraph.rootNode, args.includeHierarchy); + const loadTime = Date.now() - t0; + + console.error( + `Loaded ${graph.nodeCount} nodes, ${graph.edgeCount} edges (${loadTime}ms)` + ); + + const executor = new CypherExecutor(graph); + + if (args.query) { + // Single query mode + try { + const ast = parse(args.query); + const result = executor.execute(ast); + printResult(result, args.format); + } catch (e) { + if (e instanceof CypherSyntaxError) { + console.error('Syntax error:', e.message); + process.exit(1); + } + throw e; + } + } else { + // REPL mode + runRepl(executor, args.format); + } +} + +main().catch((err) => { + console.error('Fatal error:', err.message || err); + process.exit(1); +}); diff --git a/src/cypher/executor.ts b/src/cypher/executor.ts new file mode 100644 index 0000000..a653ca4 --- /dev/null +++ b/src/cypher/executor.ts @@ -0,0 +1,573 @@ +import { CypherGraph, NodeData, EdgeData } from './graph'; +import { + CypherQuery, + CypherExecutionError, + WhereExpr, + PatternElement, + NodePattern, + RelationshipPattern, + ReturnItem, + PropertyAccess, + VariableRef, + FunctionCall, + Literal, + Comparison, + LogicalExpr, + NotExpr, +} from './parser'; + +export interface CypherResult { + columns: string[]; + rows: Record[]; +} + +interface BindingEntry { + kind: 'node' | 'edge'; + id: number; +} + +type Binding = Map; + +export class CypherExecutor { + constructor(private graph: CypherGraph) {} + + execute(query: CypherQuery): CypherResult { + // 1. Pattern matching + let bindings = this.matchPattern(query.matchClause.pattern); + + // 2. WHERE filtering + if (query.whereClause) { + bindings = bindings.filter((binding) => + this.evaluateWhere(query.whereClause!, binding) + ); + } + + // 3. RETURN projection (with aggregation detection) + let { columns, rows } = this.projectReturn( + query.returnClause.items, + bindings + ); + + // 5. DISTINCT + if (query.returnClause.distinct) { + const seen = new Set(); + rows = rows.filter((row) => { + const key = JSON.stringify(row); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + } + + // 6. ORDER BY + if (query.orderByClause) { + const orderItems = query.orderByClause.items; + rows.sort((a, b) => { + for (const item of orderItems) { + const colName = this.exprColumnName(item.expression); + const valA = a[colName]; + const valB = b[colName]; + let cmp = 0; + if (valA < valB) cmp = -1; + else if (valA > valB) cmp = 1; + if (cmp !== 0) return item.descending ? -cmp : cmp; + } + return 0; + }); + } + + // 7. LIMIT + if (query.limit !== undefined) { + rows = rows.slice(0, query.limit); + } + + return { columns, rows }; + } + + // --- 1. Pattern Matching --- + + private matchPattern(pattern: PatternElement[]): Binding[] { + if (pattern.length === 0) return []; + + const firstNode = pattern[0] as NodePattern; + let bindings: Binding[] = []; + + // Scan all nodes for the first node pattern + for (const node of this.graph.getAllNodes()) { + if (this.nodeMatchesPattern(node, firstNode)) { + const binding: Binding = new Map(); + if (firstNode.variable) { + binding.set(firstNode.variable, { kind: 'node', id: node.id }); + } + bindings.push(binding); + } + } + + // Process subsequent relationship-node pairs + for (let i = 1; i < pattern.length; i += 2) { + const relPattern = pattern[i] as RelationshipPattern; + const nextNodePattern = pattern[i + 1] as NodePattern; + bindings = this.expandBindings(bindings, relPattern, nextNodePattern, pattern[i - 1] as NodePattern); + } + + return bindings; + } + + private nodeMatchesPattern(node: NodeData, pattern: NodePattern): boolean { + // Check labels: every label in pattern must be in node's labels + for (const label of pattern.labels) { + if (!node.labels.has(label)) return false; + } + + // Check inline properties + if (pattern.properties) { + for (const [key, value] of Object.entries(pattern.properties)) { + if (node.properties[key] !== value) return false; + } + } + + return true; + } + + private expandBindings( + bindings: Binding[], + relPattern: RelationshipPattern, + nextNodePattern: NodePattern, + prevNodePattern: NodePattern + ): Binding[] { + const expanded: Binding[] = []; + + for (const binding of bindings) { + // Get the previously bound node + const prevVarName = prevNodePattern.variable; + if (!prevVarName) continue; + const prevEntry = binding.get(prevVarName); + if (!prevEntry || prevEntry.kind !== 'node') continue; + const prevNodeId = prevEntry.id; + + // Get edges based on direction + const edgeIds = this.getDirectedEdges(prevNodeId, relPattern.direction); + + for (const edgeId of edgeIds) { + const edge = this.graph.getEdge(edgeId); + + // Check type filter + if (relPattern.types.length > 0) { + if (!relPattern.types.includes(edge.type)) continue; + } + + // Get the other end of the edge + const targetNodeId = this.getTargetNode( + edgeId, + prevNodeId, + relPattern.direction + ); + + const targetNode = this.graph.getNode(targetNodeId); + + // Check target node pattern + if (!this.nodeMatchesPattern(targetNode, nextNodePattern)) continue; + + // Create expanded binding + const newBinding: Binding = new Map(binding); + if (relPattern.variable) { + newBinding.set(relPattern.variable, { kind: 'edge', id: edgeId }); + } + if (nextNodePattern.variable) { + newBinding.set(nextNodePattern.variable, { + kind: 'node', + id: targetNodeId, + }); + } + expanded.push(newBinding); + } + } + + return expanded; + } + + private getDirectedEdges( + nodeId: number, + direction: 'out' | 'in' | 'both' + ): number[] { + if (direction === 'out') return this.graph.getOutEdges(nodeId); + if (direction === 'in') return this.graph.getInEdges(nodeId); + // both: combine out + in + return [ + ...this.graph.getOutEdges(nodeId), + ...this.graph.getInEdges(nodeId), + ]; + } + + private getTargetNode( + edgeId: number, + fromNodeId: number, + direction: 'out' | 'in' | 'both' + ): number { + if (direction === 'out') return this.graph.getEdgeDst(edgeId); + if (direction === 'in') return this.graph.getEdgeSrc(edgeId); + // both: whichever end is not fromNodeId + const src = this.graph.getEdgeSrc(edgeId); + const dst = this.graph.getEdgeDst(edgeId); + return src === fromNodeId ? dst : src; + } + + // --- 2. WHERE Evaluation --- + + private evaluateWhere(expr: WhereExpr, binding: Binding): boolean { + const result = this.evaluateExpr(expr, binding); + return !!result; + } + + private evaluateExpr(expr: WhereExpr, binding: Binding): any { + switch (expr.kind) { + case 'literal': + return (expr as Literal).value; + + case 'variable': { + const varRef = expr as VariableRef; + const entry = binding.get(varRef.name); + if (!entry) return undefined; + return entry.id; + } + + case 'propertyAccess': { + const pa = expr as PropertyAccess; + const entry = binding.get(pa.variable); + if (!entry) return undefined; + if (entry.kind === 'node') { + return this.graph.getNode(entry.id).properties[pa.property]; + } else { + return this.graph.getEdge(entry.id).properties[pa.property]; + } + } + + case 'functionCall': { + const fc = expr as FunctionCall; + if (fc.name === 'TYPE') { + const argEntry = this.resolveBindingEntry(fc.argument, binding); + if (argEntry && argEntry.kind === 'edge') { + return this.graph.getEdge(argEntry.id).type; + } + return undefined; + } + if (fc.name === 'COUNT' || fc.name === 'COLLECT') { + // Aggregation functions are handled in projectReturn + return undefined; + } + return undefined; + } + + case 'comparison': { + const cmp = expr as Comparison; + const left = this.evaluateExpr(cmp.left, binding); + const right = this.evaluateExpr(cmp.right, binding); + return this.compareValues(left, right, cmp.operator); + } + + case 'logical': { + const logical = expr as LogicalExpr; + if (logical.op === 'AND') { + return ( + this.evaluateWhere(logical.left, binding) && + this.evaluateWhere(logical.right, binding) + ); + } else { + return ( + this.evaluateWhere(logical.left, binding) || + this.evaluateWhere(logical.right, binding) + ); + } + } + + case 'not': { + const notExpr = expr as NotExpr; + return !this.evaluateWhere(notExpr.expr, binding); + } + + default: + return undefined; + } + } + + private resolveBindingEntry( + expr: WhereExpr, + binding: Binding + ): BindingEntry | undefined { + if (expr.kind === 'variable') { + return binding.get((expr as VariableRef).name); + } + return undefined; + } + + private compareValues(left: any, right: any, operator: string): boolean { + // Handle array-valued properties + if (Array.isArray(left)) { + if (operator === '=') return left.includes(right); + if (operator === '<>') return !left.includes(right); + // For other operators on arrays, check if ANY element matches + return left.some((item: any) => + this.compareScalar(item, right, operator) + ); + } + + return this.compareScalar(left, right, operator); + } + + private compareScalar(left: any, right: any, operator: string): boolean { + switch (operator) { + case '=': + return left === right; + case '<>': + return left !== right; + case '<': + return left < right; + case '>': + return left > right; + case '<=': + return left <= right; + case '>=': + return left >= right; + case 'CONTAINS': + return String(left).includes(String(right)); + case 'STARTS WITH': + return String(left).startsWith(String(right)); + case 'ENDS WITH': + return String(left).endsWith(String(right)); + default: + return false; + } + } + + // --- 3. RETURN Projection --- + + private projectReturn( + items: ReturnItem[], + bindings: Binding[] + ): { columns: string[]; rows: Record[] } { + // Check if any item has aggregation + const hasAggregation = items.some((item) => + this.isAggregation(item.expression) + ); + + if (hasAggregation) { + return this.projectWithAggregation(items, bindings); + } + + // Non-aggregated projection + const columns: string[] = []; + const rows: Record[] = []; + + // Determine columns from first binding (or from items if no bindings) + for (const item of items) { + if ( + item.expression.kind === 'variable' && + (item.expression as VariableRef).name === '*' + ) { + // Expand wildcard: use first binding's keys + if (bindings.length > 0) { + for (const varName of bindings[0].keys()) { + if (!columns.includes(varName)) columns.push(varName); + } + } + } else { + columns.push(this.getColumnName(item)); + } + } + + for (const binding of bindings) { + const row: Record = {}; + for (const item of items) { + if ( + item.expression.kind === 'variable' && + (item.expression as VariableRef).name === '*' + ) { + // Expand wildcard + for (const [varName, entry] of binding) { + row[varName] = this.resolveReturnValue( + { kind: 'variable', name: varName }, + binding + ); + } + } else { + const colName = this.getColumnName(item); + row[colName] = this.resolveReturnValue(item.expression, binding); + } + } + rows.push(row); + } + + return { columns, rows }; + } + + private isAggregation(expr: WhereExpr): boolean { + if (expr.kind === 'functionCall') { + const fc = expr as FunctionCall; + return fc.name === 'COUNT' || fc.name === 'COLLECT'; + } + return false; + } + + private projectWithAggregation( + items: ReturnItem[], + bindings: Binding[] + ): { columns: string[]; rows: Record[] } { + const columns: string[] = items.map((item) => this.getColumnName(item)); + + // Separate group-by keys from aggregations + const groupByItems: ReturnItem[] = []; + const aggItems: { item: ReturnItem; index: number }[] = []; + + items.forEach((item, index) => { + if (this.isAggregation(item.expression)) { + aggItems.push({ item, index }); + } else { + groupByItems.push(item); + } + }); + + // Group bindings + const groups = new Map(); + + if (groupByItems.length === 0) { + // All items are aggregations -> single group + groups.set('__all__', bindings); + } else { + for (const binding of bindings) { + const keyParts: any[] = []; + for (const gbItem of groupByItems) { + keyParts.push( + this.resolveReturnValue(gbItem.expression, binding) + ); + } + const key = JSON.stringify(keyParts); + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(binding); + } + } + + const rows: Record[] = []; + + for (const [, groupBindings] of groups) { + const row: Record = {}; + + for (const item of items) { + const colName = this.getColumnName(item); + if (this.isAggregation(item.expression)) { + const fc = item.expression as FunctionCall; + if (fc.name === 'COUNT') { + row[colName] = groupBindings.length; + } else if (fc.name === 'COLLECT') { + row[colName] = groupBindings.map((b) => + this.resolveReturnValue(fc.argument, b) + ); + } + } else { + // Non-aggregated: take from first binding in group + row[colName] = this.resolveReturnValue( + item.expression, + groupBindings[0] + ); + } + } + + rows.push(row); + } + + return { columns, rows }; + } + + private getColumnName(item: ReturnItem): string { + if (item.alias) return item.alias; + return this.exprColumnName(item.expression); + } + + private exprColumnName(expr: WhereExpr): string { + switch (expr.kind) { + case 'propertyAccess': { + const pa = expr as PropertyAccess; + return `${pa.variable}.${pa.property}`; + } + case 'variable': { + return (expr as VariableRef).name; + } + case 'functionCall': { + const fc = expr as FunctionCall; + if (fc.name === 'TYPE' && fc.argument.kind === 'variable') { + return `type(${(fc.argument as VariableRef).name})`; + } + if (fc.name === 'COUNT') { + return `COUNT(${this.exprColumnName(fc.argument)})`; + } + if (fc.name === 'COLLECT') { + return `COLLECT(${this.exprColumnName(fc.argument)})`; + } + return `${fc.name}(...)`; + } + default: + return '?'; + } + } + + private resolveReturnValue(expr: WhereExpr, binding: Binding): any { + switch (expr.kind) { + case 'propertyAccess': { + const pa = expr as PropertyAccess; + const entry = binding.get(pa.variable); + if (!entry) return undefined; + if (entry.kind === 'node') { + return this.graph.getNode(entry.id).properties[pa.property]; + } else { + return this.graph.getEdge(entry.id).properties[pa.property]; + } + } + + case 'variable': { + const varRef = expr as VariableRef; + const entry = binding.get(varRef.name); + if (!entry) return undefined; + if (entry.kind === 'node') { + const node = this.graph.getNode(entry.id); + const labels = Array.from(node.labels); + return { + name: node.properties.name, + path: node.properties.path, + type: labels.length > 0 ? labels[0] : undefined, + properties: { ...node.properties }, + }; + } else { + const edge = this.graph.getEdge(entry.id); + const srcNode = this.graph.getNode(edge.src); + const dstNode = this.graph.getNode(edge.dst); + return { + from: srcNode.properties.path, + to: dstNode.properties.path, + type: edge.type, + properties: { ...edge.properties }, + }; + } + } + + case 'functionCall': { + const fc = expr as FunctionCall; + if (fc.name === 'TYPE') { + const argEntry = this.resolveBindingEntry(fc.argument, binding); + if (argEntry && argEntry.kind === 'edge') { + return this.graph.getEdge(argEntry.id).type; + } + return undefined; + } + // COUNT and COLLECT handled in aggregation path + return undefined; + } + + case 'literal': + return (expr as Literal).value; + + default: + return undefined; + } + } +} diff --git a/src/cypher/graph.ts b/src/cypher/graph.ts new file mode 100644 index 0000000..f87abf1 --- /dev/null +++ b/src/cypher/graph.ts @@ -0,0 +1,180 @@ +import { SElement, NOT_KNOWN_TYPE } from '../selement/selement'; +import { SElementAssociation } from '../selement/selementAssociation'; + +export interface NodeData { + id: number; + labels: Set; + properties: Record; +} + +export interface EdgeData { + id: number; + type: string; + properties: Record; + src: number; + dst: number; +} + +export class CypherGraph { + private nodeData: Map = new Map(); + private edgeData: Map = new Map(); + private nodeOutEdges: Map = new Map(); + private nodeInEdges: Map = new Map(); + private pathToNode: Map = new Map(); + private nodeToElem: Map = new Map(); + private elemToNode: Map = new Map(); + private edgeToAssoc: Map = new Map(); + + constructor(root: SElement, includeHierarchy = true) { + this.buildIndex(root, includeHierarchy); + } + + private static normalizePath(p: string): string { + return p.startsWith('/') ? p.slice(1) : p; + } + + private buildIndex(root: SElement, includeHierarchy: boolean): void { + let nextNodeId = 0; + let nextEdgeId = 0; + + // Pass 1 -- Nodes + root.traverseElements((element: SElement) => { + const id = nextNodeId++; + const type = element.getType(); + const labels = new Set(); + if (type !== NOT_KNOWN_TYPE) { + labels.add(type); + } + + const attrs = element.getAttributes(); + const properties: Record = {}; + for (const [key, value] of Object.entries(attrs)) { + if (key === 'type') continue; + if (Array.isArray(value) && value.length === 1) { + properties[key] = value[0]; + } else { + properties[key] = value; + } + } + const path = CypherGraph.normalizePath(element.getPath()); + properties.name = element.name; + properties.path = path; + + const node: NodeData = { id, labels, properties }; + this.nodeData.set(id, node); + this.nodeToElem.set(id, element); + this.elemToNode.set(element, id); + this.nodeOutEdges.set(id, []); + this.nodeInEdges.set(id, []); + this.pathToNode.set(path, id); + }); + + // Pass 2 -- Edges (associations) + const seenAssocs = new Set(); + + root.traverseElements((element: SElement) => { + for (const assoc of element.outgoing) { + if (seenAssocs.has(assoc)) continue; + seenAssocs.add(assoc); + + const srcId = this.elemToNode.get(assoc.fromElement); + const dstId = this.elemToNode.get(assoc.toElement); + if (srcId === undefined || dstId === undefined) continue; + + const edgeId = nextEdgeId++; + const edge: EdgeData = { + id: edgeId, + type: assoc.deptype || 'unknown', + properties: { ...(assoc.attrs || {}) }, + src: srcId, + dst: dstId, + }; + + this.edgeData.set(edgeId, edge); + this.edgeToAssoc.set(edgeId, assoc); + this.nodeOutEdges.get(srcId)!.push(edgeId); + this.nodeInEdges.get(dstId)!.push(edgeId); + } + }); + + // Pass 3 -- Hierarchy edges (CONTAINS) + if (includeHierarchy) { + root.traverseElements((element: SElement) => { + const parentId = this.elemToNode.get(element); + if (parentId === undefined) return; + + for (const child of element.children) { + const childId = this.elemToNode.get(child); + if (childId === undefined) continue; + + const edgeId = nextEdgeId++; + const edge: EdgeData = { + id: edgeId, + type: 'CONTAINS', + properties: {}, + src: parentId, + dst: childId, + }; + + this.edgeData.set(edgeId, edge); + this.edgeToAssoc.set(edgeId, null); + this.nodeOutEdges.get(parentId)!.push(edgeId); + this.nodeInEdges.get(childId)!.push(edgeId); + } + }); + } + } + + get nodeCount(): number { + return this.nodeData.size; + } + + get edgeCount(): number { + return this.edgeData.size; + } + + getAllNodes(): NodeData[] { + return Array.from(this.nodeData.values()); + } + + getAllEdges(): EdgeData[] { + return Array.from(this.edgeData.values()); + } + + getNode(id: number): NodeData { + return this.nodeData.get(id)!; + } + + getEdge(id: number): EdgeData { + return this.edgeData.get(id)!; + } + + getOutEdges(nodeId: number): number[] { + return this.nodeOutEdges.get(nodeId) || []; + } + + getInEdges(nodeId: number): number[] { + return this.nodeInEdges.get(nodeId) || []; + } + + getEdgeSrc(edgeId: number): number { + return this.edgeData.get(edgeId)!.src; + } + + getEdgeDst(edgeId: number): number { + return this.edgeData.get(edgeId)!.dst; + } + + getNodeByPath(path: string): NodeData | undefined { + const id = this.pathToNode.get(CypherGraph.normalizePath(path)); + return id !== undefined ? this.nodeData.get(id) : undefined; + } + + getElemForNode(nodeId: number): SElement | undefined { + return this.nodeToElem.get(nodeId); + } + + getAssocForEdge(edgeId: number): SElementAssociation | null | undefined { + return this.edgeToAssoc.get(edgeId); + } +} diff --git a/src/cypher/index.ts b/src/cypher/index.ts new file mode 100644 index 0000000..540c88f --- /dev/null +++ b/src/cypher/index.ts @@ -0,0 +1,23 @@ +import { SGraph } from '../sgraph'; +import { CypherGraph } from './graph'; +import { CypherExecutor, CypherResult } from './executor'; +import { parse } from './parser'; + +export { CypherSyntaxError, CypherExecutionError } from './parser'; +export type { CypherResult } from './executor'; + +export interface CypherQueryOptions { + includeHierarchy?: boolean; +} + +export function cypherQuery( + model: SGraph, + query: string, + options: CypherQueryOptions = {} +): CypherResult { + const { includeHierarchy = true } = options; + const graph = new CypherGraph(model.rootNode, includeHierarchy); + const executor = new CypherExecutor(graph); + const ast = parse(query); + return executor.execute(ast); +} diff --git a/src/cypher/parser.ts b/src/cypher/parser.ts new file mode 100644 index 0000000..64e045e --- /dev/null +++ b/src/cypher/parser.ts @@ -0,0 +1,862 @@ +// Cypher subset parser for sgraph.js +// Tokenizer, AST types, and recursive descent parser + +// --- Error classes --- + +export class CypherSyntaxError extends Error { + constructor(message: string) { + super(message); + this.name = 'CypherSyntaxError'; + } +} + +export class CypherExecutionError extends Error { + constructor(message: string) { + super(message); + this.name = 'CypherExecutionError'; + } +} + +// --- Token types --- + +export enum TokenType { + // Keywords + MATCH = 'MATCH', + WHERE = 'WHERE', + RETURN = 'RETURN', + AS = 'AS', + LIMIT = 'LIMIT', + ORDER = 'ORDER', + BY = 'BY', + ASC = 'ASC', + DESC = 'DESC', + DISTINCT = 'DISTINCT', + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + CONTAINS = 'CONTAINS', + STARTS = 'STARTS', + ENDS = 'ENDS', + WITH = 'WITH', + COUNT = 'COUNT', + COLLECT = 'COLLECT', + TYPE = 'TYPE', + TRUE = 'TRUE', + FALSE = 'FALSE', + STAR = 'STAR', + + // Symbols + LPAREN = 'LPAREN', + RPAREN = 'RPAREN', + LBRACKET = 'LBRACKET', + RBRACKET = 'RBRACKET', + LBRACE = 'LBRACE', + RBRACE = 'RBRACE', + DASH = 'DASH', + GT = 'GT', + LT = 'LT', + COLON = 'COLON', + DOT = 'DOT', + COMMA = 'COMMA', + EQ = 'EQ', + NEQ = 'NEQ', + LTE = 'LTE', + GTE = 'GTE', + + // Values + IDENTIFIER = 'IDENTIFIER', + STRING = 'STRING', + NUMBER = 'NUMBER', + EOF = 'EOF', +} + +export interface Token { + type: TokenType; + value: string; +} + +// --- Keyword lookup --- + +const KEYWORDS: Record = { + MATCH: TokenType.MATCH, + WHERE: TokenType.WHERE, + RETURN: TokenType.RETURN, + AS: TokenType.AS, + LIMIT: TokenType.LIMIT, + ORDER: TokenType.ORDER, + BY: TokenType.BY, + ASC: TokenType.ASC, + DESC: TokenType.DESC, + DISTINCT: TokenType.DISTINCT, + AND: TokenType.AND, + OR: TokenType.OR, + NOT: TokenType.NOT, + CONTAINS: TokenType.CONTAINS, + STARTS: TokenType.STARTS, + ENDS: TokenType.ENDS, + WITH: TokenType.WITH, + COUNT: TokenType.COUNT, + COLLECT: TokenType.COLLECT, + TRUE: TokenType.TRUE, + FALSE: TokenType.FALSE, +}; + +// --- Tokenizer --- + +export function tokenize(input: string): Token[] { + const tokens: Token[] = []; + let i = 0; + + while (i < input.length) { + // Skip whitespace + if (/\s/.test(input[i])) { + i++; + continue; + } + + // Two-char operators (check before single-char) + if (i + 1 < input.length) { + const twoChar = input[i] + input[i + 1]; + if (twoChar === '<>') { + tokens.push({ type: TokenType.NEQ, value: '<>' }); + i += 2; + continue; + } + if (twoChar === '<=') { + tokens.push({ type: TokenType.LTE, value: '<=' }); + i += 2; + continue; + } + if (twoChar === '>=') { + tokens.push({ type: TokenType.GTE, value: '>=' }); + i += 2; + continue; + } + } + + // Single-char symbols + const ch = input[i]; + if (ch === '(') { tokens.push({ type: TokenType.LPAREN, value: '(' }); i++; continue; } + if (ch === ')') { tokens.push({ type: TokenType.RPAREN, value: ')' }); i++; continue; } + if (ch === '[') { tokens.push({ type: TokenType.LBRACKET, value: '[' }); i++; continue; } + if (ch === ']') { tokens.push({ type: TokenType.RBRACKET, value: ']' }); i++; continue; } + if (ch === '{') { tokens.push({ type: TokenType.LBRACE, value: '{' }); i++; continue; } + if (ch === '}') { tokens.push({ type: TokenType.RBRACE, value: '}' }); i++; continue; } + if (ch === '-') { tokens.push({ type: TokenType.DASH, value: '-' }); i++; continue; } + if (ch === '>') { tokens.push({ type: TokenType.GT, value: '>' }); i++; continue; } + if (ch === '<') { tokens.push({ type: TokenType.LT, value: '<' }); i++; continue; } + if (ch === ':') { tokens.push({ type: TokenType.COLON, value: ':' }); i++; continue; } + if (ch === '.') { tokens.push({ type: TokenType.DOT, value: '.' }); i++; continue; } + if (ch === ',') { tokens.push({ type: TokenType.COMMA, value: ',' }); i++; continue; } + if (ch === '=') { tokens.push({ type: TokenType.EQ, value: '=' }); i++; continue; } + if (ch === '*') { tokens.push({ type: TokenType.STAR, value: '*' }); i++; continue; } + + // String literals + if (ch === "'" || ch === '"') { + const quote = ch; + i++; // skip opening quote + let str = ''; + while (i < input.length && input[i] !== quote) { + str += input[i]; + i++; + } + if (i >= input.length) { + throw new CypherSyntaxError(`Unterminated string literal`); + } + i++; // skip closing quote + tokens.push({ type: TokenType.STRING, value: str }); + continue; + } + + // Numbers + if (/[0-9]/.test(ch)) { + let num = ''; + while (i < input.length && /[0-9]/.test(input[i])) { + num += input[i]; + i++; + } + if (i < input.length && input[i] === '.') { + num += '.'; + i++; + while (i < input.length && /[0-9]/.test(input[i])) { + num += input[i]; + i++; + } + } + tokens.push({ type: TokenType.NUMBER, value: num }); + continue; + } + + // Identifiers and keywords + if (/[a-zA-Z_]/.test(ch)) { + let ident = ''; + while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) { + ident += input[i]; + i++; + } + const upper = ident.toUpperCase(); + if (upper in KEYWORDS) { + tokens.push({ type: KEYWORDS[upper], value: upper }); + } else { + tokens.push({ type: TokenType.IDENTIFIER, value: ident }); + } + continue; + } + + throw new CypherSyntaxError(`Unexpected character: '${ch}'`); + } + + tokens.push({ type: TokenType.EOF, value: '' }); + return tokens; +} + +// --- AST Types --- + +export interface NodePattern { + kind: 'node'; + variable?: string; + labels: string[]; + properties?: Record; +} + +export interface RelationshipPattern { + kind: 'relationship'; + variable?: string; + types: string[]; + direction: 'out' | 'in' | 'both'; +} + +export type PatternElement = NodePattern | RelationshipPattern; + +export interface MatchClause { + pattern: PatternElement[]; +} + +// WHERE expression tree +export interface Comparison { + kind: 'comparison'; + operator: string; + left: WhereExpr; + right: WhereExpr; +} + +export interface LogicalExpr { + kind: 'logical'; + op: 'AND' | 'OR'; + left: WhereExpr; + right: WhereExpr; +} + +export interface NotExpr { + kind: 'not'; + expr: WhereExpr; +} + +export interface PropertyAccess { + kind: 'propertyAccess'; + variable: string; + property: string; +} + +export interface VariableRef { + kind: 'variable'; + name: string; +} + +export interface FunctionCall { + kind: 'functionCall'; + name: string; + argument: WhereExpr; +} + +export interface Literal { + kind: 'literal'; + value: string | number | boolean | null; +} + +export type WhereExpr = + | Comparison + | LogicalExpr + | NotExpr + | PropertyAccess + | VariableRef + | FunctionCall + | Literal; + +export interface ReturnItem { + expression: WhereExpr; + alias?: string; +} + +export interface ReturnClause { + distinct: boolean; + items: ReturnItem[]; +} + +export interface OrderByItem { + expression: WhereExpr; + descending: boolean; +} + +export interface OrderByClause { + items: OrderByItem[]; +} + +export interface CypherQuery { + matchClause: MatchClause; + whereClause?: WhereExpr; + returnClause: ReturnClause; + orderByClause?: OrderByClause; + limit?: number; +} + +// --- Parser --- + +class Parser { + private tokens: Token[]; + private pos: number; + + constructor(tokens: Token[]) { + this.tokens = tokens; + this.pos = 0; + } + + private peek(): Token { + return this.tokens[this.pos]; + } + + private advance(): Token { + const token = this.tokens[this.pos]; + this.pos++; + return token; + } + + private expect(type: TokenType): Token { + const token = this.peek(); + if (token.type !== type) { + throw new CypherSyntaxError( + `Expected ${type} but got ${token.type} ('${token.value}')` + ); + } + return this.advance(); + } + + private match(type: TokenType): boolean { + if (this.peek().type === type) { + this.advance(); + return true; + } + return false; + } + + private expectIdentifierOrKeyword(): Token { + const token = this.peek(); + // Accept IDENTIFIER or any keyword token as a name + if (token.type === TokenType.IDENTIFIER || token.type in TokenType) { + // Reject structural tokens (symbols, EOF, literals other than identifiers/keywords) + const nonNameTypes = new Set([ + TokenType.LPAREN, TokenType.RPAREN, TokenType.LBRACKET, TokenType.RBRACKET, + TokenType.LBRACE, TokenType.RBRACE, TokenType.DASH, TokenType.GT, TokenType.LT, + TokenType.COLON, TokenType.DOT, TokenType.COMMA, TokenType.EQ, TokenType.NEQ, + TokenType.LTE, TokenType.GTE, TokenType.STRING, TokenType.NUMBER, + TokenType.EOF, TokenType.STAR, + ]); + if (!nonNameTypes.has(token.type)) { + return this.advance(); + } + } + throw new CypherSyntaxError( + `Expected identifier or keyword but got ${token.type} ('${token.value}')` + ); + } + + parseQuery(): CypherQuery { + // MATCH clause + this.expect(TokenType.MATCH); + const matchClause = this.parseMatch(); + + // Optional WHERE clause + let whereClause: WhereExpr | undefined; + if (this.peek().type === TokenType.WHERE) { + this.advance(); + whereClause = this.parseOrExpr(); + } + + // RETURN clause + this.expect(TokenType.RETURN); + const returnClause = this.parseReturn(); + + // Optional ORDER BY + let orderByClause: OrderByClause | undefined; + if (this.peek().type === TokenType.ORDER) { + orderByClause = this.parseOrderBy(); + } + + // Optional LIMIT + let limit: number | undefined; + if (this.peek().type === TokenType.LIMIT) { + limit = this.parseLimit(); + } + + return { + matchClause, + whereClause, + returnClause, + orderByClause, + limit, + }; + } + + private parseMatch(): MatchClause { + const pattern: PatternElement[] = []; + + // First element must be a node + pattern.push(this.parseNodePattern()); + + // Then alternating relationship-node pairs + while (this.peek().type === TokenType.DASH || this.peek().type === TokenType.LT) { + const rel = this.parseRelationshipPattern(); + pattern.push(rel); + pattern.push(this.parseNodePattern()); + } + + return { pattern }; + } + + private parseNodePattern(): NodePattern { + this.expect(TokenType.LPAREN); + + let variable: string | undefined; + const labels: string[] = []; + let properties: Record | undefined; + + // Optional variable name (identifier that is NOT followed by nothing special, + // or is followed by colon for labels, or RPAREN) + if (this.peek().type === TokenType.IDENTIFIER) { + variable = this.advance().value; + } + + // Optional labels (:Label) + while (this.peek().type === TokenType.COLON) { + this.advance(); // consume ':' + const labelToken = this.expect(TokenType.IDENTIFIER); + labels.push(labelToken.value); + } + + // Optional inline properties { key: value, ... } + if (this.peek().type === TokenType.LBRACE) { + properties = this.parseInlineProperties(); + } + + this.expect(TokenType.RPAREN); + + return { kind: 'node', variable, labels, properties }; + } + + private parseInlineProperties(): Record { + this.expect(TokenType.LBRACE); + const props: Record = {}; + + if (this.peek().type !== TokenType.RBRACE) { + // Parse first property + const key = this.expect(TokenType.IDENTIFIER).value; + this.expect(TokenType.COLON); + const value = this.parseLiteralValue(); + props[key] = value; + + // Parse additional properties + while (this.match(TokenType.COMMA)) { + const k = this.expect(TokenType.IDENTIFIER).value; + this.expect(TokenType.COLON); + const v = this.parseLiteralValue(); + props[k] = v; + } + } + + this.expect(TokenType.RBRACE); + return props; + } + + private parseLiteralValue(): string | number | boolean | null { + const token = this.peek(); + if (token.type === TokenType.STRING) { + this.advance(); + return token.value; + } + if (token.type === TokenType.NUMBER) { + this.advance(); + const num = parseFloat(token.value); + return num; + } + if (token.type === TokenType.TRUE) { + this.advance(); + return true; + } + if (token.type === TokenType.FALSE) { + this.advance(); + return false; + } + throw new CypherSyntaxError( + `Expected literal value but got ${token.type} ('${token.value}')` + ); + } + + private parseRelationshipPattern(): RelationshipPattern { + // Possible patterns: + // -[...]-> outgoing + // <-[...]- incoming + // -[...]- both (undirected) + + let direction: 'out' | 'in' | 'both'; + let leftArrow = false; + + // Check for incoming arrow start: <- + if (this.peek().type === TokenType.LT) { + this.advance(); // consume '<' + this.expect(TokenType.DASH); // consume '-' + leftArrow = true; + } else { + this.expect(TokenType.DASH); // consume '-' + } + + // Parse bracket contents [variable:TYPE] + this.expect(TokenType.LBRACKET); + + let variable: string | undefined; + const types: string[] = []; + + if (this.peek().type === TokenType.IDENTIFIER) { + variable = this.advance().value; + } + + // Optional type (:TYPE) — allow keywords as type names (e.g. CONTAINS) + while (this.peek().type === TokenType.COLON) { + this.advance(); // consume ':' + const typeToken = this.expectIdentifierOrKeyword(); + types.push(typeToken.value); + } + + this.expect(TokenType.RBRACKET); + + // Check for outgoing arrow end: -> + this.expect(TokenType.DASH); // consume '-' + let rightArrow = false; + if (this.peek().type === TokenType.GT) { + this.advance(); // consume '>' + rightArrow = true; + } + + if (leftArrow && !rightArrow) { + direction = 'in'; + } else if (!leftArrow && rightArrow) { + direction = 'out'; + } else { + direction = 'both'; + } + + return { kind: 'relationship', variable, types, direction }; + } + + // --- WHERE expression parsing (precedence climbing) --- + + private parseOrExpr(): WhereExpr { + let left = this.parseAndExpr(); + while (this.peek().type === TokenType.OR) { + this.advance(); + const right = this.parseAndExpr(); + left = { kind: 'logical', op: 'OR', left, right }; + } + return left; + } + + private parseAndExpr(): WhereExpr { + let left = this.parseNotExpr(); + while (this.peek().type === TokenType.AND) { + this.advance(); + const right = this.parseNotExpr(); + left = { kind: 'logical', op: 'AND', left, right }; + } + return left; + } + + private parseNotExpr(): WhereExpr { + if (this.peek().type === TokenType.NOT) { + this.advance(); + const expr = this.parseComparison(); + return { kind: 'not', expr }; + } + return this.parseComparison(); + } + + private parseComparison(): WhereExpr { + const left = this.parseAtom(); + + // Check for comparison operators + const token = this.peek(); + let operator: string | undefined; + + switch (token.type) { + case TokenType.EQ: + operator = '='; + this.advance(); + break; + case TokenType.NEQ: + operator = '<>'; + this.advance(); + break; + case TokenType.LT: + operator = '<'; + this.advance(); + break; + case TokenType.GT: + operator = '>'; + this.advance(); + break; + case TokenType.LTE: + operator = '<='; + this.advance(); + break; + case TokenType.GTE: + operator = '>='; + this.advance(); + break; + case TokenType.CONTAINS: + operator = 'CONTAINS'; + this.advance(); + break; + case TokenType.STARTS: + this.advance(); // consume STARTS + this.expect(TokenType.WITH); // consume WITH + operator = 'STARTS WITH'; + break; + case TokenType.ENDS: + this.advance(); // consume ENDS + this.expect(TokenType.WITH); // consume WITH + operator = 'ENDS WITH'; + break; + default: + return left; + } + + const right = this.parseAtom(); + return { kind: 'comparison', operator, left, right }; + } + + private parseAtom(): WhereExpr { + const token = this.peek(); + + // String literal + if (token.type === TokenType.STRING) { + this.advance(); + return { kind: 'literal', value: token.value }; + } + + // Number literal + if (token.type === TokenType.NUMBER) { + this.advance(); + return { kind: 'literal', value: parseFloat(token.value) }; + } + + // Boolean literals + if (token.type === TokenType.TRUE) { + this.advance(); + return { kind: 'literal', value: true }; + } + if (token.type === TokenType.FALSE) { + this.advance(); + return { kind: 'literal', value: false }; + } + + // Parenthesized expression + if (token.type === TokenType.LPAREN) { + this.advance(); + const expr = this.parseOrExpr(); + this.expect(TokenType.RPAREN); + return expr; + } + + // Function call keyword: COUNT(x), COLLECT(x) + if (this.isFunctionKeyword(token.type)) { + const name = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(name), argument }; + } + // Not a function call - could be property access or variable ref + if (this.peek().type === TokenType.DOT) { + this.advance(); // consume '.' + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: name, property }; + } + return { kind: 'variable', name }; + } + + // Identifier: could be propertyAccess (n.name), functionCall (type(r)), or variableRef (n) + if (token.type === TokenType.IDENTIFIER) { + const ident = this.advance().value; + if (this.peek().type === TokenType.DOT) { + this.advance(); // consume '.' + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: ident, property }; + } + // Check if it's a function call: ident(...) + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(ident), argument }; + } + return { kind: 'variable', name: ident }; + } + + throw new CypherSyntaxError( + `Unexpected token in expression: ${token.type} ('${token.value}')` + ); + } + + private isFunctionKeyword(type: TokenType): boolean { + return type === TokenType.COUNT || type === TokenType.COLLECT; + } + + private static KNOWN_FUNCTIONS = new Set(['COUNT', 'COLLECT', 'TYPE']); + + private normalizeFunctionName(name: string): string { + const upper = name.toUpperCase(); + if (Parser.KNOWN_FUNCTIONS.has(upper)) { + return upper; + } + return name; + } + + // --- RETURN clause --- + + private parseReturn(): ReturnClause { + let distinct = false; + if (this.peek().type === TokenType.DISTINCT) { + this.advance(); + distinct = true; + } + + const items: ReturnItem[] = []; + items.push(this.parseReturnItem()); + + while (this.match(TokenType.COMMA)) { + items.push(this.parseReturnItem()); + } + + return { distinct, items }; + } + + private parseReturnItem(): ReturnItem { + let expression: WhereExpr; + + // Handle RETURN * + if (this.peek().type === TokenType.STAR) { + this.advance(); + expression = { kind: 'variable', name: '*' }; + } else { + expression = this.parseReturnExpression(); + } + + // Optional alias: AS name + let alias: string | undefined; + if (this.peek().type === TokenType.AS) { + this.advance(); + alias = this.expect(TokenType.IDENTIFIER).value; + } + + return { expression, alias }; + } + + private parseReturnExpression(): WhereExpr { + const token = this.peek(); + + // Function call keyword: COUNT(x), COLLECT(x) + if (this.isFunctionKeyword(token.type)) { + const name = this.advance().value; + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(name), argument }; + } + // Fallthrough: treat as variable or property access + if (this.peek().type === TokenType.DOT) { + this.advance(); + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: name, property }; + } + return { kind: 'variable', name }; + } + + // Identifier: could be propertyAccess, functionCall (type(r)), or variableRef + if (token.type === TokenType.IDENTIFIER) { + const ident = this.advance().value; + if (this.peek().type === TokenType.DOT) { + this.advance(); + const property = this.expect(TokenType.IDENTIFIER).value; + return { kind: 'propertyAccess', variable: ident, property }; + } + if (this.peek().type === TokenType.LPAREN) { + this.advance(); // consume '(' + const argument = this.parseAtom(); + this.expect(TokenType.RPAREN); + return { kind: 'functionCall', name: this.normalizeFunctionName(ident), argument }; + } + return { kind: 'variable', name: ident }; + } + + throw new CypherSyntaxError( + `Unexpected token in RETURN clause: ${token.type} ('${token.value}')` + ); + } + + // --- ORDER BY --- + + private parseOrderBy(): OrderByClause { + this.expect(TokenType.ORDER); + this.expect(TokenType.BY); + + const items: OrderByItem[] = []; + items.push(this.parseOrderByItem()); + + while (this.match(TokenType.COMMA)) { + items.push(this.parseOrderByItem()); + } + + return { items }; + } + + private parseOrderByItem(): OrderByItem { + const expression = this.parseReturnExpression(); + let descending = false; + + if (this.peek().type === TokenType.DESC) { + this.advance(); + descending = true; + } else if (this.peek().type === TokenType.ASC) { + this.advance(); + descending = false; + } + + return { expression, descending }; + } + + // --- LIMIT --- + + private parseLimit(): number { + this.expect(TokenType.LIMIT); + const token = this.expect(TokenType.NUMBER); + return parseInt(token.value, 10); + } +} + +// --- Public parse function --- + +export function parse(input: string): CypherQuery { + const tokens = tokenize(input); + const parser = new Parser(tokens); + return parser.parseQuery(); +} diff --git a/src/index.ts b/src/index.ts index b013bf2..e1eee13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './loaders'; export * from './sgraph'; export * from './selement'; export * from './converters'; +export * from './cypher'; diff --git a/src/modelapi.ts b/src/modelapi.ts index 1cf3672..a8b7680 100644 --- a/src/modelapi.ts +++ b/src/modelapi.ts @@ -57,11 +57,33 @@ class ModelApi { .map((ea) => ea.fromElement); getUsedElements = (element: SElement) => - element.outgoing.map((ea) => ea.fromElement); + element.outgoing.map((ea) => ea.toElement); getUserElements = (element: SElement) => element.incoming.map((ea) => ea.fromElement); + getElementByPath(path: string): SElement | undefined { + return this.egm.findElementFromPath(path); + } + + filter(filterFunc: (element: SElement) => boolean): SElement[] { + const matched: SElement[] = []; + const recurse = (element: SElement) => { + if (filterFunc(element)) matched.push(element); + for (const child of element.children) { + recurse(child); + } + }; + for (const child of this.egm.rootNode.children) { + recurse(child); + } + return matched; + } + + getChildrenByType(element: SElement, elemType: string): SElement[] { + return element.children.filter((child) => child.typeEquals(elemType)); + } + createDescendants = ( relatedElement: SElement, newOrExistingReferredElement: SElement diff --git a/src/selement/selement.ts b/src/selement/selement.ts index ac2a4bb..7d29d64 100644 --- a/src/selement/selement.ts +++ b/src/selement/selement.ts @@ -263,6 +263,171 @@ class SElement { return ancestors; } + getLevel(): number { + let e = this.parent; + let level = 0; + while (e !== undefined) { + e = e.parent; + level += 1; + } + return level; + } + + getRoot(): SElement { + let p: SElement = this; + while (p.parent !== undefined) { + p = p.parent; + } + return p; + } + + isDescendantOf(ancestor: SElement): boolean { + if (this.equals(ancestor)) return false; + let p = this.parent; + while (p !== undefined) { + if (p.equals(ancestor)) return true; + p = p.parent; + } + return false; + } + + getAncestorOfType(type: string): SElement | undefined { + if (this.typeEquals(type)) return this; + let ancestor: SElement | undefined = this; + while (ancestor?.parent !== undefined) { + ancestor = ancestor.parent; + if (ancestor.typeEquals(type)) return ancestor; + } + return undefined; + } + + getAncestorOfTypes(types: string[] | Set): SElement | undefined { + const typeSet = types instanceof Set ? types : new Set(types); + if (typeSet.has(this.getType())) return this; + let ancestor: SElement | undefined = this; + while (ancestor?.parent !== undefined) { + ancestor = ancestor.parent; + if (typeSet.has(ancestor.getType())) return ancestor; + } + return undefined; + } + + getAncestorOfLevel(level: number): SElement | undefined { + let delta = this.getLevel() - level; + let ancestor: SElement | undefined = this; + while (delta > 0 && ancestor !== undefined) { + ancestor = ancestor.parent; + delta -= 1; + } + return ancestor; + } + + getDescendants(): SElement[] { + const descendants: SElement[] = []; + const collect = (element: SElement) => { + for (const child of element.children) { + descendants.push(child); + collect(child); + } + }; + collect(this); + return descendants; + } + + getNodeCount(): number { + let count = 1; + for (const child of this.children) { + count += child.getNodeCount(); + } + return count; + } + + getEACount(): number { + let count = this.outgoing.length; + for (const child of this.children) { + count += child.getEACount(); + } + return count; + } + + getEATypes(typeSet: Set): void { + for (const ea of this.outgoing) { + if (ea.deptype) typeSet.add(ea.deptype); + } + for (const child of this.children) { + child.getEATypes(typeSet); + } + } + + getEATypeCounts(counts: { [key: string]: number }): void { + for (const ea of this.outgoing) { + if (ea.deptype) { + counts[ea.deptype] = (counts[ea.deptype] || 0) + 1; + } + } + for (const child of this.children) { + child.getEATypeCounts(counts); + } + } + + getMaxDepth(currentDepth: number): number { + if (this.children.length === 0) return currentDepth; + let depth = 0; + for (const child of this.children) { + depth = Math.max(child.getMaxDepth(currentDepth + 1), depth); + } + return depth === 0 ? currentDepth : depth; + } + + hasType(): boolean { + return 'type' in this.attrs && this.attrs.type !== ''; + } + + remove(leaveParentUntouched = false): void { + if (!leaveParentUntouched && this.parent !== undefined) { + this.parent.detachChild(this); + } + + for (const ea of [...this.outgoing]) { + ea.remove(); + } + this.outgoing = []; + + for (const ea of [...this.incoming]) { + ea.remove(); + } + this.incoming = []; + + for (const child of this.children) { + child.remove(true); + } + this.children = []; + this.childrenObject = {}; + } + + rename(newName: string): void { + if (this.parent === undefined) { + this.name = newName; + this.updateHash(); + return; + } + delete this.parent.childrenObject[this.name]; + this.name = newName; + this.parent.childrenObject[newName] = this; + this.updateHash(); + } + + removeDescendantsIf(checker: (e: SElement) => boolean): void { + for (const child of [...this.children]) { + if (checker(child)) { + child.remove(); + } + } + for (const child of this.children) { + child.removeDescendantsIf(checker); + } + } + addAttribute(name: string, value: string) { if (name === 'type') { this.attrs[name] = value; diff --git a/src/selement/selementAssociation.ts b/src/selement/selementAssociation.ts index 3ae283a..fa32c5d 100644 --- a/src/selement/selementAssociation.ts +++ b/src/selement/selementAssociation.ts @@ -92,12 +92,17 @@ class SElementAssociation { this.fromElement?.outgoing.indexOf(this), 1 ); - this.fromElement?.incoming.splice( - this.fromElement?.incoming.indexOf(this), + this.toElement?.incoming.splice( + this.toElement?.incoming.indexOf(this), 1 ); } + addAttribute(name: string, value: string) { + if (!this.attrs) this.attrs = {}; + this.attrs[name] = value; + } + getDependencyLength() { if (this.fromElement?.equals(this.toElement!)) { return 0; diff --git a/src/sgraph/sgraph.ts b/src/sgraph/sgraph.ts index bd994a0..2d1cde8 100644 --- a/src/sgraph/sgraph.ts +++ b/src/sgraph/sgraph.ts @@ -139,6 +139,31 @@ class SGraph { return ec; } + getDepth(): number { + let depth = 0; + for (const child of this.rootNode.children) { + depth = Math.max(child.getMaxDepth(1), depth); + } + return depth; + } + + calculateModelStats(): { + dependenciesCount: number; + nodesCount: number; + depTypeCounts: { [key: string]: number }; + depToElemRatio: number; + } { + const dependenciesCount = this.rootNode.getEACount(); + const nodesCount = this.rootNode.getNodeCount(); + const depTypeCounts: { [key: string]: number } = {}; + this.rootNode.getEATypeCounts(depTypeCounts); + const depToElemRatio = + nodesCount > 0 + ? Math.round((dependenciesCount / nodesCount) * 100) / 100 + : 0; + return { dependenciesCount, nodesCount, depTypeCounts, depToElemRatio }; + } + toXml() { const rootNode = this.rootNode; const counter = new Counter(); diff --git a/test/cypher/executor.test.ts b/test/cypher/executor.test.ts new file mode 100644 index 0000000..4a931a0 --- /dev/null +++ b/test/cypher/executor.test.ts @@ -0,0 +1,218 @@ +import { SElement } from '../../src/selement/selement'; +import { SElementAssociation } from '../../src/selement/selementAssociation'; +import { CypherGraph } from '../../src/cypher/graph'; +import { CypherExecutor } from '../../src/cypher/executor'; +import { parse } from '../../src/cypher/parser'; + +function buildTestGraph(): CypherGraph { + const root = new SElement('', undefined); + const project = new SElement('myproject', root); + project.setType('dir'); + const fileA = new SElement('a.js', project); + fileA.setType('file'); + const fileB = new SElement('b.js', project); + fileB.setType('file'); + const ea = new SElementAssociation(fileA, fileB, 'import'); + ea.initElems(); + return new CypherGraph(root); +} + +let graph: CypherGraph; +let executor: CypherExecutor; + +beforeAll(() => { + graph = buildTestGraph(); + executor = new CypherExecutor(graph); +}); + +describe('CypherExecutor pattern matching', () => { + it('matches all nodes', () => { + const result = executor.execute(parse('MATCH (n) RETURN n.name')); + expect(result.columns).toContain('n.name'); + expect(result.rows.length).toBe(4); + }); + + it('matches nodes by label', () => { + const result = executor.execute(parse('MATCH (n:file) RETURN n.name')); + expect(result.rows.length).toBe(2); + const names = result.rows.map(r => r['n.name']); + expect(names).toContain('a.js'); + expect(names).toContain('b.js'); + }); + + it('matches nodes by inline property', () => { + const result = executor.execute(parse("MATCH (n {name: 'a.js'}) RETURN n.name")); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('a.js'); + }); + + it('matches outgoing relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['a.name']).toBe('a.js'); + expect(result.rows[0]['b.name']).toBe('b.js'); + }); + + it('matches incoming relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)<-[r:import]-(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['a.name']).toBe('b.js'); + expect(result.rows[0]['b.name']).toBe('a.js'); + }); + + it('matches undirected relationships', () => { + const result = executor.execute(parse( + 'MATCH (a:file)-[r:import]-(b:file) RETURN a.name, b.name' + )); + expect(result.rows.length).toBe(2); + }); + + it('matches CONTAINS relationships', () => { + const result = executor.execute(parse( + 'MATCH (d:dir)-[:CONTAINS]->(f:file) RETURN d.name, f.name' + )); + expect(result.rows.length).toBe(2); + }); + + it('returns full node object for variable return', () => { + const result = executor.execute(parse("MATCH (n {name: 'a.js'}) RETURN n")); + expect(result.rows[0]['n']).toMatchObject({ + name: 'a.js', type: 'file', + }); + expect(result.rows[0]['n'].path).toBeDefined(); + }); + + it('returns edge info for relationship variable', () => { + const result = executor.execute(parse( + 'MATCH (a)-[r:import]->(b) RETURN r' + )); + expect(result.rows[0]['r']).toMatchObject({ type: 'import' }); + expect(result.rows[0]['r'].from).toBeDefined(); + expect(result.rows[0]['r'].to).toBeDefined(); + }); + + it('returns type() function result', () => { + const result = executor.execute(parse( + "MATCH (a)-[r]->(b) WHERE type(r) = 'import' RETURN type(r)" + )); + expect(result.rows.length).toBeGreaterThanOrEqual(1); + expect(result.rows[0]['type(r)']).toBe('import'); + }); + + it('applies LIMIT', () => { + const result = executor.execute(parse('MATCH (n) RETURN n.name LIMIT 2')); + expect(result.rows.length).toBe(2); + }); +}); + +describe('CypherExecutor WHERE', () => { + it('filters by string equality', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE n.name = 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('a.js'); + }); + + it('filters by inequality', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE n.name <> 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.name']).toBe('b.js'); + }); + + it('filters with CONTAINS', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name CONTAINS '.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with STARTS WITH', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name STARTS WITH 'a' RETURN n.name" + )); + const names = result.rows.map(r => r['n.name']); + expect(names).toContain('a.js'); + }); + + it('filters with ENDS WITH', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name ENDS WITH '.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with AND', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name CONTAINS '.js' AND n.name STARTS WITH 'a' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + }); + + it('filters with OR', () => { + const result = executor.execute(parse( + "MATCH (n) WHERE n.name = 'a.js' OR n.name = 'b.js' RETURN n.name" + )); + expect(result.rows.length).toBe(2); + }); + + it('filters with NOT', () => { + const result = executor.execute(parse( + "MATCH (n:file) WHERE NOT n.name = 'a.js' RETURN n.name" + )); + expect(result.rows.length).toBe(1); + }); +}); + +describe('CypherExecutor aggregation', () => { + it('COUNT returns correct count', () => { + const result = executor.execute(parse('MATCH (n:file) RETURN COUNT(n)')); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['COUNT(n)']).toBe(2); + }); + + it('COUNT with grouping', () => { + const result = executor.execute(parse( + 'MATCH (n) RETURN n.name, COUNT(n) ORDER BY n.name' + )); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('COLLECT aggregates values', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN COLLECT(n.name)' + )); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['COLLECT(n.name)']).toContain('a.js'); + expect(result.rows[0]['COLLECT(n.name)']).toContain('b.js'); + }); + + it('RETURN DISTINCT deduplicates', () => { + const result = executor.execute(parse( + 'MATCH (n) RETURN DISTINCT n.name' + )); + const names = result.rows.map(r => r['n.name']); + expect(new Set(names).size).toBe(names.length); + }); + + it('ORDER BY sorts results', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN n.name ORDER BY n.name ASC' + )); + expect(result.rows[0]['n.name']).toBe('a.js'); + expect(result.rows[1]['n.name']).toBe('b.js'); + }); + + it('ORDER BY DESC', () => { + const result = executor.execute(parse( + 'MATCH (n:file) RETURN n.name ORDER BY n.name DESC' + )); + expect(result.rows[0]['n.name']).toBe('b.js'); + }); +}); diff --git a/test/cypher/graph.test.ts b/test/cypher/graph.test.ts new file mode 100644 index 0000000..857fdcd --- /dev/null +++ b/test/cypher/graph.test.ts @@ -0,0 +1,78 @@ +import { SElement } from '../../src/selement/selement'; +import { SElementAssociation } from '../../src/selement/selementAssociation'; +import { CypherGraph } from '../../src/cypher/graph'; + +describe('CypherGraph', () => { + let graph: CypherGraph; + + beforeAll(() => { + const root = new SElement('', undefined); + const project = new SElement('myproject', root); + project.setType('dir'); + const fileA = new SElement('a.js', project); + fileA.setType('file'); + fileA.addAttribute('hash', 'abc123'); + const fileB = new SElement('b.js', project); + fileB.setType('file'); + const ea = new SElementAssociation(fileA, fileB, 'import'); + ea.initElems(); + + graph = new CypherGraph(root); + }); + + it('indexes all elements as nodes', () => { + expect(graph.nodeCount).toBe(4); + }); + + it('indexes associations as edges', () => { + expect(graph.edgeCount).toBeGreaterThanOrEqual(1); + }); + + it('adds CONTAINS edges by default', () => { + // 1 import edge + 3 CONTAINS edges (root->myproject, myproject->a.js, myproject->b.js) + expect(graph.edgeCount).toBe(4); + }); + + it('skips CONTAINS edges when includeHierarchy is false', () => { + const root = new SElement('', undefined); + const child = new SElement('c', root); + child.setType('file'); + const g = new CypherGraph(root, false); + expect(g.edgeCount).toBe(0); + }); + + it('sets node labels from element type', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node).toBeDefined(); + expect(node!.labels.has('file')).toBe(true); + }); + + it('sets node properties including name and path', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.name).toBe('a.js'); + expect(node!.properties.path).toBe('myproject/a.js'); + }); + + it('flattens single-element array attrs to scalars', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.hash).toBe('abc123'); + }); + + it('does not include type in properties', () => { + const node = graph.getNodeByPath('myproject/a.js'); + expect(node!.properties.type).toBeUndefined(); + }); + + it('sets edge type from deptype', () => { + const edges = graph.getAllEdges(); + const importEdge = edges.find(e => e.type === 'import'); + expect(importEdge).toBeDefined(); + }); + + it('provides outgoing and incoming edge lookups', () => { + const nodeA = graph.getNodeByPath('myproject/a.js'); + const outEdges = graph.getOutEdges(nodeA!.id); + const importEdges = outEdges.filter(eid => graph.getEdge(eid).type === 'import'); + expect(importEdges.length).toBe(1); + }); +}); diff --git a/test/cypher/integration.test.ts b/test/cypher/integration.test.ts new file mode 100644 index 0000000..c81cc95 --- /dev/null +++ b/test/cypher/integration.test.ts @@ -0,0 +1,71 @@ +import { readFile } from 'fs/promises'; +import { SGraph, cypherQuery } from '../../src'; + +describe('Cypher integration (modelfile.xml)', () => { + let model: SGraph; + + beforeAll(async () => { + const data = await readFile('test/modelfile.xml', 'utf8'); + model = SGraph.parseXml({ data }); + }); + + it('finds all file elements', () => { + const result = cypherQuery(model, 'MATCH (n:file) RETURN n.name'); + expect(result.rows.length).toBeGreaterThan(0); + const names = result.rows.map((r: Record) => r['n.name']); + expect(names).toContain('index.js'); + expect(names).toContain('math.js'); + expect(names).toContain('models.js'); + }); + + it('finds import relationships', () => { + const result = cypherQuery(model, + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name, b.name' + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('finds elements by name', () => { + const result = cypherQuery(model, + "MATCH (n {name: 'math.js'}) RETURN n.name, n.path" + ); + expect(result.rows.length).toBe(1); + expect(result.rows[0]['n.path']).toBe('mock-project/src/utils/math.js'); + }); + + it('queries CONTAINS hierarchy', () => { + const result = cypherQuery(model, + "MATCH (d {name: 'src'})-[:CONTAINS]->(f) RETURN f.name" + ); + // src contains: database, index.js, utils + expect(result.rows.length).toBe(3); + }); + + it('counts elements by type using type()', () => { + const result = cypherQuery(model, + 'MATCH (a)-[r]->(b) RETURN type(r), COUNT(r)' + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('works without hierarchy — no CONTAINS edges', () => { + const result = cypherQuery(model, + 'MATCH (a)-[r:CONTAINS]->(b) RETURN a.name', + { includeHierarchy: false } + ); + expect(result.rows.length).toBe(0); + }); + + it('works without hierarchy — non-CONTAINS edges still present', () => { + const result = cypherQuery(model, + 'MATCH (a:file)-[r:import]->(b:file) RETURN a.name', + { includeHierarchy: false } + ); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it('LIMIT works', () => { + const result = cypherQuery(model, 'MATCH (n) RETURN n.name LIMIT 3'); + expect(result.rows.length).toBe(3); + }); +}); diff --git a/test/cypher/parser.test.ts b/test/cypher/parser.test.ts new file mode 100644 index 0000000..fc75b21 --- /dev/null +++ b/test/cypher/parser.test.ts @@ -0,0 +1,207 @@ +import { tokenize, parse, TokenType } from '../../src/cypher/parser'; + +describe('Cypher Tokenizer', () => { + it('tokenizes a simple MATCH RETURN', () => { + const tokens = tokenize('MATCH (n) RETURN n'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.MATCH, TokenType.LPAREN, TokenType.IDENTIFIER, + TokenType.RPAREN, TokenType.RETURN, TokenType.IDENTIFIER, TokenType.EOF, + ]); + }); + + it('tokenizes labels and types', () => { + const tokens = tokenize('(n:file)'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.LPAREN, TokenType.IDENTIFIER, TokenType.COLON, + TokenType.IDENTIFIER, TokenType.RPAREN, TokenType.EOF, + ]); + }); + + it('tokenizes string literals', () => { + const tokens = tokenize("'hello'"); + expect(tokens[0].type).toBe(TokenType.STRING); + expect(tokens[0].value).toBe('hello'); + }); + + it('tokenizes double-quoted strings', () => { + const tokens = tokenize('"world"'); + expect(tokens[0].type).toBe(TokenType.STRING); + expect(tokens[0].value).toBe('world'); + }); + + it('tokenizes numbers', () => { + const tokens = tokenize('42'); + expect(tokens[0].type).toBe(TokenType.NUMBER); + expect(tokens[0].value).toBe('42'); + }); + + it('tokenizes comparison operators', () => { + const tokens = tokenize('<> <= >= = < >'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.NEQ, TokenType.LTE, TokenType.GTE, + TokenType.EQ, TokenType.LT, TokenType.GT, TokenType.EOF, + ]); + }); + + it('tokenizes relationship arrow -[r:TYPE]->', () => { + const tokens = tokenize('-[r:TYPE]->'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.DASH, TokenType.LBRACKET, TokenType.IDENTIFIER, + TokenType.COLON, TokenType.IDENTIFIER, TokenType.RBRACKET, + TokenType.DASH, TokenType.GT, TokenType.EOF, + ]); + }); + + it('tokenizes keywords case-insensitively', () => { + const tokens = tokenize('match WHERE Return'); + expect(tokens.map(t => t.type)).toEqual([ + TokenType.MATCH, TokenType.WHERE, TokenType.RETURN, TokenType.EOF, + ]); + }); +}); + +describe('Cypher Parser', () => { + it('parses simple MATCH (n) RETURN n', () => { + const ast = parse('MATCH (n) RETURN n'); + expect(ast.matchClause.pattern).toHaveLength(1); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', labels: [], + }); + expect(ast.returnClause.items).toHaveLength(1); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'variable', name: 'n', + }); + }); + + it('parses node with label: (n:file)', () => { + const ast = parse('MATCH (n:file) RETURN n'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', labels: ['file'], + }); + }); + + it('parses node with inline properties: (n {name: "foo"})', () => { + const ast = parse('MATCH (n {name: "foo"}) RETURN n'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: 'n', properties: { name: 'foo' }, + }); + }); + + it('parses outgoing relationship: (a)-[r:import]->(b)', () => { + const ast = parse('MATCH (a)-[r:import]->(b) RETURN a, b'); + expect(ast.matchClause.pattern).toHaveLength(3); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', variable: 'r', types: ['import'], direction: 'out', + }); + }); + + it('parses incoming relationship: (a)<-[r:import]-(b)', () => { + const ast = parse('MATCH (a)<-[r:import]-(b) RETURN a'); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', direction: 'in', + }); + }); + + it('parses undirected relationship: (a)-[r]-(b)', () => { + const ast = parse('MATCH (a)-[r]-(b) RETURN a'); + expect(ast.matchClause.pattern[1]).toMatchObject({ + kind: 'relationship', direction: 'both', + }); + }); + + it('parses anonymous node and relationship: ()-[]->()', () => { + const ast = parse('MATCH ()-[]->() RETURN *'); + expect(ast.matchClause.pattern[0]).toMatchObject({ + kind: 'node', variable: undefined, + }); + }); + + it('parses WHERE with comparison', () => { + const ast = parse("MATCH (n) WHERE n.name = 'foo' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', + operator: '=', + left: { kind: 'propertyAccess', variable: 'n', property: 'name' }, + right: { kind: 'literal', value: 'foo' }, + }); + }); + + it('parses WHERE with AND/OR', () => { + const ast = parse("MATCH (n) WHERE n.name = 'a' AND n.path = 'b' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'logical', op: 'AND', + }); + }); + + it('parses WHERE with NOT', () => { + const ast = parse("MATCH (n) WHERE NOT n.name = 'a' RETURN n"); + expect(ast.whereClause).toMatchObject({ kind: 'not' }); + }); + + it('parses WHERE with CONTAINS', () => { + const ast = parse("MATCH (n) WHERE n.name CONTAINS 'util' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'CONTAINS', + }); + }); + + it('parses WHERE with STARTS WITH', () => { + const ast = parse("MATCH (n) WHERE n.name STARTS WITH 'src' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'STARTS WITH', + }); + }); + + it('parses WHERE with ENDS WITH', () => { + const ast = parse("MATCH (n) WHERE n.name ENDS WITH '.js' RETURN n"); + expect(ast.whereClause).toMatchObject({ + kind: 'comparison', operator: 'ENDS WITH', + }); + }); + + it('parses RETURN with property access', () => { + const ast = parse('MATCH (n) RETURN n.name, n.path'); + expect(ast.returnClause.items).toHaveLength(2); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'propertyAccess', variable: 'n', property: 'name', + }); + }); + + it('parses RETURN with alias', () => { + const ast = parse('MATCH (n) RETURN n.name AS fileName'); + expect(ast.returnClause.items[0].alias).toBe('fileName'); + }); + + it('parses RETURN DISTINCT', () => { + const ast = parse('MATCH (n) RETURN DISTINCT n.name'); + expect(ast.returnClause.distinct).toBe(true); + }); + + it('parses COUNT function', () => { + const ast = parse('MATCH (n) RETURN COUNT(n)'); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'functionCall', name: 'COUNT', + }); + }); + + it('parses type() function', () => { + const ast = parse('MATCH (a)-[r]->(b) RETURN type(r)'); + expect(ast.returnClause.items[0].expression).toMatchObject({ + kind: 'functionCall', name: 'TYPE', + }); + }); + + it('parses LIMIT', () => { + const ast = parse('MATCH (n) RETURN n LIMIT 10'); + expect(ast.limit).toBe(10); + }); + + it('parses ORDER BY', () => { + const ast = parse('MATCH (n) RETURN n.name ORDER BY n.name DESC'); + expect(ast.orderByClause!.items[0].descending).toBe(true); + }); + + it('throws CypherSyntaxError on bad input', () => { + expect(() => parse('MATCH RETURN')).toThrow(); + }); +}); diff --git a/test/new-methods.test.ts b/test/new-methods.test.ts new file mode 100644 index 0000000..5c9e654 --- /dev/null +++ b/test/new-methods.test.ts @@ -0,0 +1,406 @@ +import { readFile } from 'fs/promises'; +import { + SElement, + SElementAssociation, + SGraph, + ModelApi, + ModelLoader, +} from '../src'; + +/** + * Tests for methods ported from Python sgraph library. + */ + +describe('SElement new methods', () => { + let root: SElement; + let project: SElement; + let src: SElement; + let utils: SElement; + let mathFile: SElement; + let helperFile: SElement; + + beforeEach(() => { + root = new SElement('', undefined); + project = new SElement('project', root); + project.setType('repository'); + src = new SElement('src', project); + src.setType('dir'); + utils = new SElement('utils', src); + utils.setType('dir'); + mathFile = new SElement('math.js', utils); + mathFile.setType('file'); + helperFile = new SElement('helper.js', src); + helperFile.setType('file'); + }); + + describe('getLevel', () => { + it('returns 0 for root element', () => { + expect(root.getLevel()).toBe(0); + }); + + it('returns correct level for nested elements', () => { + expect(project.getLevel()).toBe(1); + expect(src.getLevel()).toBe(2); + expect(utils.getLevel()).toBe(3); + expect(mathFile.getLevel()).toBe(4); + }); + }); + + describe('getRoot', () => { + it('returns self for root element', () => { + expect(root.getRoot()).toBe(root); + }); + + it('returns root for deeply nested element', () => { + expect(mathFile.getRoot()).toBe(root); + }); + + it('returns root for direct child', () => { + expect(project.getRoot()).toBe(root); + }); + }); + + describe('isDescendantOf', () => { + it('returns true for direct child', () => { + expect(src.isDescendantOf(project)).toBe(true); + }); + + it('returns true for deeply nested descendant', () => { + expect(mathFile.isDescendantOf(project)).toBe(true); + expect(mathFile.isDescendantOf(root)).toBe(true); + }); + + it('returns false for self', () => { + expect(project.isDescendantOf(project)).toBe(false); + }); + + it('returns false for non-ancestor', () => { + expect(project.isDescendantOf(mathFile)).toBe(false); + expect(helperFile.isDescendantOf(utils)).toBe(false); + }); + }); + + describe('getAncestorOfType', () => { + it('returns self if type matches', () => { + expect(mathFile.getAncestorOfType('file')).toBe(mathFile); + }); + + it('returns ancestor with matching type', () => { + expect(mathFile.getAncestorOfType('dir')).toBe(utils); + expect(mathFile.getAncestorOfType('repository')).toBe(project); + }); + + it('returns undefined if no ancestor matches', () => { + expect(mathFile.getAncestorOfType('class')).toBeUndefined(); + }); + }); + + describe('getAncestorOfTypes', () => { + it('finds ancestor matching any of the types (array)', () => { + expect(mathFile.getAncestorOfTypes(['class', 'repository'])).toBe( + project + ); + }); + + it('finds ancestor matching any of the types (Set)', () => { + expect(mathFile.getAncestorOfTypes(new Set(['dir', 'class']))).toBe( + utils + ); + }); + + it('returns self if type matches', () => { + expect(mathFile.getAncestorOfTypes(['file'])).toBe(mathFile); + }); + + it('returns undefined if no type matches', () => { + expect(mathFile.getAncestorOfTypes(['class', 'method'])).toBeUndefined(); + }); + }); + + describe('getAncestorOfLevel', () => { + it('returns self for own level', () => { + expect(mathFile.getAncestorOfLevel(4)).toBe(mathFile); + }); + + it('returns correct ancestor for higher level', () => { + expect(mathFile.getAncestorOfLevel(3)).toBe(utils); + expect(mathFile.getAncestorOfLevel(2)).toBe(src); + expect(mathFile.getAncestorOfLevel(1)).toBe(project); + }); + }); + + describe('getDescendants', () => { + it('returns empty for leaf element', () => { + expect(mathFile.getDescendants()).toEqual([]); + }); + + it('returns all descendants', () => { + const descendants = project.getDescendants(); + expect(descendants).toContain(src); + expect(descendants).toContain(utils); + expect(descendants).toContain(mathFile); + expect(descendants).toContain(helperFile); + expect(descendants.length).toBe(4); + }); + + it('returns subtree descendants', () => { + const descendants = src.getDescendants(); + expect(descendants).toContain(utils); + expect(descendants).toContain(mathFile); + expect(descendants).toContain(helperFile); + expect(descendants.length).toBe(3); + }); + }); + + describe('getNodeCount', () => { + it('returns 1 for leaf element', () => { + expect(mathFile.getNodeCount()).toBe(1); + }); + + it('counts all nodes in subtree', () => { + expect(project.getNodeCount()).toBe(5); // project + src + utils + math.js + helper.js + expect(src.getNodeCount()).toBe(4); // src + utils + math.js + helper.js + }); + }); + + describe('getEACount', () => { + it('returns 0 when no associations', () => { + expect(project.getEACount()).toBe(0); + }); + + it('counts associations in subtree', () => { + const ea = new SElementAssociation(helperFile, mathFile, 'import'); + ea.initElems(); + expect(src.getEACount()).toBe(1); + expect(project.getEACount()).toBe(1); + expect(mathFile.getEACount()).toBe(0); // math.js has no outgoing + }); + }); + + describe('getEATypes', () => { + it('collects association types', () => { + const ea1 = new SElementAssociation(helperFile, mathFile, 'import'); + ea1.initElems(); + const ea2 = new SElementAssociation(mathFile, helperFile, 'function_ref'); + ea2.initElems(); + + const types = new Set(); + project.getEATypes(types); + expect(types.has('import')).toBe(true); + expect(types.has('function_ref')).toBe(true); + expect(types.size).toBe(2); + }); + }); + + describe('getEATypeCounts', () => { + it('counts association types', () => { + const ea1 = new SElementAssociation(helperFile, mathFile, 'import'); + ea1.initElems(); + const ea2 = new SElementAssociation(mathFile, helperFile, 'import'); + ea2.initElems(); + const ea3 = new SElementAssociation( + mathFile, + helperFile, + 'function_ref' + ); + ea3.initElems(); + + const counts: { [key: string]: number } = {}; + project.getEATypeCounts(counts); + expect(counts['import']).toBe(2); + expect(counts['function_ref']).toBe(1); + }); + }); + + describe('getMaxDepth', () => { + it('returns current depth for leaf', () => { + expect(mathFile.getMaxDepth(0)).toBe(0); + }); + + it('returns max depth from starting point', () => { + expect(project.getMaxDepth(0)).toBe(3); // project -> src -> utils -> math.js + }); + }); + + describe('hasType', () => { + it('returns true when type is set', () => { + expect(project.hasType()).toBe(true); + }); + + it('returns false when no type', () => { + expect(root.hasType()).toBe(false); + }); + }); + + describe('remove', () => { + it('removes element from parent', () => { + expect(src.children.length).toBe(2); + helperFile.remove(); + expect(src.children.length).toBe(1); + expect(src.getChildByName('helper.js')).toBeUndefined(); + }); + + it('clears associations on removal', () => { + const ea = new SElementAssociation(helperFile, mathFile, 'import'); + ea.initElems(); + expect(mathFile.incoming.length).toBe(1); + + helperFile.remove(); + expect(mathFile.incoming.length).toBe(0); + }); + + it('removes descendants recursively', () => { + utils.remove(); + expect(src.children.length).toBe(1); // only helper.js left + expect(src.getChildByName('utils')).toBeUndefined(); + }); + }); + + describe('rename', () => { + it('renames element and updates parent childrenObject', () => { + helperFile.rename('utils.js'); + expect(helperFile.name).toBe('utils.js'); + expect(src.getChildByName('utils.js')).toBe(helperFile); + expect(src.getChildByName('helper.js')).toBeUndefined(); + }); + + it('renames root element without parent', () => { + root.rename('newroot'); + expect(root.name).toBe('newroot'); + }); + }); + + describe('removeDescendantsIf', () => { + it('removes matching descendants', () => { + project.removeDescendantsIf((e) => e.getType() === 'file'); + expect(utils.children.length).toBe(0); + expect(src.getChildByName('helper.js')).toBeUndefined(); + }); + + it('keeps non-matching descendants', () => { + project.removeDescendantsIf((e) => e.name === 'helper.js'); + expect(src.children.length).toBe(1); // utils remains + expect(utils.children.length).toBe(1); // math.js remains + }); + }); +}); + +describe('SElementAssociation.addAttribute', () => { + it('adds attribute to association', () => { + const a = new SElement('a', undefined); + const b = new SElement('b', undefined); + const ea = new SElementAssociation(a, b, 'import'); + ea.addAttribute('source_line', '42'); + expect(ea.getAttributes()!['source_line']).toBe('42'); + }); + + it('initializes attrs if undefined', () => { + const a = new SElement('a', undefined); + const b = new SElement('b', undefined); + const ea = new SElementAssociation(a, b, 'import'); + ea.attrs = undefined as any; + ea.addAttribute('key', 'val'); + expect(ea.getAttributes()!['key']).toBe('val'); + }); +}); + +describe('SGraph new methods', () => { + let model: SGraph; + + beforeAll(async () => { + const data = await readFile('test/modelfile.xml', 'utf8'); + model = SGraph.parseXml({ data }); + }); + + describe('getDepth', () => { + it('returns max depth of the graph', () => { + const depth = model.getDepth(); + expect(depth).toBeGreaterThan(0); + }); + }); + + describe('calculateModelStats', () => { + it('returns node count, edge count, type counts, and ratio', () => { + const stats = model.calculateModelStats(); + expect(stats.nodesCount).toBeGreaterThan(0); + expect(stats.dependenciesCount).toBeGreaterThanOrEqual(0); + expect(typeof stats.depToElemRatio).toBe('number'); + expect(typeof stats.depTypeCounts).toBe('object'); + }); + + it('node count matches rootNode.getNodeCount()', () => { + const stats = model.calculateModelStats(); + expect(stats.nodesCount).toBe(model.rootNode.getNodeCount()); + }); + }); +}); + +describe('ModelApi new methods', () => { + let model: SGraph; + let modelApi: ModelApi; + + beforeAll(async () => { + const loaded = await new ModelLoader().load('test/modelfile.xml'); + if (loaded) { + model = loaded; + modelApi = new ModelApi({ model }); + } + }); + + describe('getElementByPath', () => { + it('finds element by path', () => { + const elem = modelApi.getElementByPath('mock-project/src/utils'); + expect(elem).toBeDefined(); + expect(elem!.name).toBe('utils'); + }); + + it('returns undefined for non-existent path', () => { + expect(modelApi.getElementByPath('nonexistent/path')).toBeUndefined(); + }); + }); + + describe('filter', () => { + it('filters elements by predicate', () => { + const jsFiles = modelApi.filter((e) => e.name.endsWith('.js')); + expect(jsFiles.length).toBeGreaterThan(0); + jsFiles.forEach((e) => expect(e.name).toMatch(/\.js$/)); + }); + + it('returns empty array when nothing matches', () => { + const result = modelApi.filter((e) => e.name === 'nonexistent'); + expect(result).toEqual([]); + }); + }); + + describe('getChildrenByType', () => { + it('returns children matching type', () => { + const project = model.rootNode.children[0]; + const dirs = modelApi.getChildrenByType(project, 'dir'); + dirs.forEach((d) => expect(d.getType()).toBe('dir')); + }); + + it('returns empty array when no children match', () => { + const project = model.rootNode.children[0]; + const result = modelApi.getChildrenByType(project, 'class'); + expect(result).toEqual([]); + }); + }); +}); + +describe('SElementAssociation.remove bug fix', () => { + it('removes from toElement.incoming (not fromElement.incoming)', () => { + const root = new SElement('', undefined); + const a = new SElement('a', root); + const b = new SElement('b', root); + const ea = new SElementAssociation(a, b, 'dep'); + ea.initElems(); + + expect(a.outgoing.length).toBe(1); + expect(b.incoming.length).toBe(1); + + ea.remove(); + + expect(a.outgoing.length).toBe(0); + expect(b.incoming.length).toBe(0); + }); +}); diff --git a/yarn.lock b/yarn.lock index e315d8f..4528a30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,7 +4,7 @@ "@ampproject/remapping@^2.1.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz" integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== dependencies: "@jridgewell/gen-mapping" "^0.1.0" @@ -12,19 +12,19 @@ "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: "@babel/highlight" "^7.18.6" "@babel/compat-data@^7.18.8": version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz" integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== -"@babel/core@^7.11.6", "@babel/core@^7.12.3": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz" integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== dependencies: "@ampproject/remapping" "^2.1.0" @@ -45,7 +45,7 @@ "@babel/generator@^7.18.9", "@babel/generator@^7.7.2": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz" integrity sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug== dependencies: "@babel/types" "^7.18.9" @@ -54,7 +54,7 @@ "@babel/helper-compilation-targets@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz" integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== dependencies: "@babel/compat-data" "^7.18.8" @@ -64,12 +64,12 @@ "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== "@babel/helper-function-name@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" + resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz" integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== dependencies: "@babel/template" "^7.18.6" @@ -77,21 +77,21 @@ "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== dependencies: "@babel/types" "^7.18.6" "@babel/helper-module-imports@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz" integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-module-transforms@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz" integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== dependencies: "@babel/helper-environment-visitor" "^7.18.9" @@ -105,36 +105,36 @@ "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz" integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== "@babel/helper-simple-access@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz" integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== dependencies: "@babel/types" "^7.18.6" "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-validator-identifier@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz" integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== "@babel/helper-validator-option@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== "@babel/helpers@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz" integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== dependencies: "@babel/template" "^7.18.6" @@ -143,7 +143,7 @@ "@babel/highlight@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== dependencies: "@babel/helper-validator-identifier" "^7.18.6" @@ -152,103 +152,103 @@ "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.9": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz" integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-top-level-await@^7.8.3": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz" integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/template@^7.18.6", "@babel/template@^7.3.3": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz" integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw== dependencies: "@babel/code-frame" "^7.18.6" @@ -257,7 +257,7 @@ "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.2": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz" integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg== dependencies: "@babel/code-frame" "^7.18.6" @@ -273,7 +273,7 @@ "@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz" integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg== dependencies: "@babel/helper-validator-identifier" "^7.18.6" @@ -281,12 +281,12 @@ "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -297,12 +297,12 @@ "@istanbuljs/schema@^0.1.2": version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@jest/console@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.3.tgz#2030606ec03a18c31803b8a36382762e447655df" + resolved "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz" integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== dependencies: "@jest/types" "^28.1.3" @@ -314,7 +314,7 @@ "@jest/core@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-28.1.3.tgz#0ebf2bd39840f1233cd5f2d1e6fc8b71bd5a1ac7" + resolved "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz" integrity sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA== dependencies: "@jest/console" "^28.1.3" @@ -349,7 +349,7 @@ "@jest/environment@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.3.tgz#abed43a6b040a4c24fdcb69eab1f97589b2d663e" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz" integrity sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA== dependencies: "@jest/fake-timers" "^28.1.3" @@ -359,14 +359,14 @@ "@jest/expect-utils@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz" integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== dependencies: jest-get-type "^28.0.2" "@jest/expect@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-28.1.3.tgz#9ac57e1d4491baca550f6bdbd232487177ad6a72" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz" integrity sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw== dependencies: expect "^28.1.3" @@ -374,7 +374,7 @@ "@jest/fake-timers@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-28.1.3.tgz#230255b3ad0a3d4978f1d06f70685baea91c640e" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz" integrity sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw== dependencies: "@jest/types" "^28.1.3" @@ -386,7 +386,7 @@ "@jest/globals@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.3.tgz#a601d78ddc5fdef542728309894895b4a42dc333" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz" integrity sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA== dependencies: "@jest/environment" "^28.1.3" @@ -395,7 +395,7 @@ "@jest/reporters@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.3.tgz#9adf6d265edafc5fc4a434cfb31e2df5a67a369a" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz" integrity sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -426,14 +426,14 @@ "@jest/schemas@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz" integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== dependencies: "@sinclair/typebox" "^0.24.1" "@jest/source-map@^28.1.2": version "28.1.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-28.1.2.tgz#7fe832b172b497d6663cdff6c13b0a920e139e24" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz" integrity sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww== dependencies: "@jridgewell/trace-mapping" "^0.3.13" @@ -442,7 +442,7 @@ "@jest/test-result@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.3.tgz#5eae945fd9f4b8fcfce74d239e6f725b6bf076c5" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz" integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== dependencies: "@jest/console" "^28.1.3" @@ -452,7 +452,7 @@ "@jest/test-sequencer@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz#9d0c283d906ac599c74bde464bc0d7e6a82886c3" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz" integrity sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw== dependencies: "@jest/test-result" "^28.1.3" @@ -462,7 +462,7 @@ "@jest/transform@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-28.1.3.tgz#59d8098e50ab07950e0f2fc0fc7ec462371281b0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz" integrity sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA== dependencies: "@babel/core" "^7.11.6" @@ -481,9 +481,9 @@ slash "^3.0.0" write-file-atomic "^4.0.1" -"@jest/types@^28.1.3": +"@jest/types@^28.0.0", "@jest/types@^28.1.3": version "28.1.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + resolved "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz" integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== dependencies: "@jest/schemas" "^28.1.3" @@ -495,7 +495,7 @@ "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== dependencies: "@jridgewell/set-array" "^1.0.0" @@ -503,7 +503,7 @@ "@jridgewell/gen-mapping@^0.3.2": version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== dependencies: "@jridgewell/set-array" "^1.0.1" @@ -512,22 +512,22 @@ "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": version "0.3.14" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz" integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" @@ -535,33 +535,33 @@ "@sinclair/typebox@^0.24.1": version "0.24.20" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.20.tgz" integrity sha512-kVaO5aEFZb33nPMTZBxiPEkY+slxiPtqC7QX8f9B3eGOMBvEfuMfxp9DSTTCsRJPumPKjrge4yagyssO4q6qzQ== "@sinonjs/commons@^1.7.0": version "1.8.3" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^9.1.2": version "9.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz" integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== dependencies: "@sinonjs/commons" "^1.7.0" "@types/adm-zip@^0.5.0": version "0.5.0" - resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.0.tgz#94c90a837ce02e256c7c665a6a1eb295906333c1" + resolved "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz" integrity sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw== dependencies: "@types/node" "*" "@types/babel__core@^7.1.14": version "7.1.19" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" @@ -572,14 +572,14 @@ "@types/babel__generator@*": version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== dependencies: "@babel/parser" "^7.1.0" @@ -587,128 +587,123 @@ "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.17.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz" integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== dependencies: "@babel/types" "^7.3.0" "@types/graceful-fs@^4.1.3": version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== dependencies: "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@^28.1.6": version "28.1.6" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.6.tgz#d6a9cdd38967d2d746861fb5be6b120e38284dd4" + resolved "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz" integrity sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ== dependencies: jest-matcher-utils "^28.0.0" pretty-format "^28.0.0" -"@types/node@*": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" - integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== - -"@types/node@^18.0.6": +"@types/node@*", "@types/node@^18.0.6": version "18.0.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7" + resolved "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz" integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw== "@types/object-hash@^2.2.1": version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-2.2.1.tgz#67c169f8f033e0b62abbf81df2d00f4598d540b9" + resolved "https://registry.npmjs.org/@types/object-hash/-/object-hash-2.2.1.tgz" integrity sha512-i/rtaJFCsPljrZvP/akBqEwUP2y5cZLOmvO+JaYnz01aPknrQ+hB5MRcO7iqCUsFaYfTG8kGfKUyboA07xeDHQ== "@types/prettier@^2.1.5": version "2.6.3" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz" integrity sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg== "@types/sax@^1.2.4": version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.4.tgz#8221affa7f4f3cb21abd22f244cfabfa63e6a69e" + resolved "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz" integrity sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw== dependencies: "@types/node" "*" "@types/stack-utils@^2.0.0": version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== "@types/yargs-parser@*": version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": version "17.0.10" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz" integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== dependencies: "@types/yargs-parser" "*" adm-zip@^0.5.9: version "0.5.9" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" + resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz" integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^5.0.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== anymatch@^3.0.3: version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" @@ -716,14 +711,14 @@ anymatch@^3.0.3: argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" -babel-jest@^28.1.3: +babel-jest@^28.0.0, babel-jest@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz" integrity sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q== dependencies: "@jest/transform" "^28.1.3" @@ -736,7 +731,7 @@ babel-jest@^28.1.3: babel-plugin-istanbul@^6.1.1: version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -747,7 +742,7 @@ babel-plugin-istanbul@^6.1.1: babel-plugin-jest-hoist@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz#1952c4d0ea50f2d6d794353762278d1d8cca3fbe" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz" integrity sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q== dependencies: "@babel/template" "^7.3.3" @@ -757,7 +752,7 @@ babel-plugin-jest-hoist@^28.1.3: babel-preset-current-node-syntax@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -775,7 +770,7 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz#5dfc20b99abed5db994406c2b9ab94c73aaa419d" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz" integrity sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A== dependencies: babel-plugin-jest-hoist "^28.1.3" @@ -783,12 +778,12 @@ babel-preset-jest@^28.1.3: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -796,14 +791,14 @@ brace-expansion@^1.1.7: braces@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.20.2: +browserslist@^4.20.2, "browserslist@>= 4.21.0": version "4.21.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz" integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA== dependencies: caniuse-lite "^1.0.30001366" @@ -813,46 +808,46 @@ browserslist@^4.20.2: bs-logger@0.x: version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== dependencies: fast-json-stable-stringify "2.x" bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001366: version "1.0.30001370" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001370.tgz#0a30d4f20d38b9e108cc5ae7cc62df9fe66cd5ba" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001370.tgz" integrity sha512-3PDmaP56wz/qz7G508xzjx8C+MC2qEm4SYhSEzC9IBROo+dGXFWRuaXkWti0A9tuI00g+toiriVqxtWMgl350g== chalk@^2.0.0: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -861,7 +856,7 @@ chalk@^2.0.0: chalk@^4.0.0: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -869,22 +864,22 @@ chalk@^4.0.0: char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== ci-info@^3.2.0: version "3.3.2" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz" integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== cjs-module-lexer@^1.0.0: version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== cliui@^7.0.2: version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" @@ -893,53 +888,53 @@ cliui@^7.0.2: co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collect-v8-coverage@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== dependencies: safe-buffer "~5.1.1" cross-spawn@^7.0.3: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" @@ -948,81 +943,81 @@ cross-spawn@^7.0.3: csv-parse@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.3.0.tgz#85cc02fc9d1c89bd1b02e69069c960f8b8064322" + resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.0.tgz" integrity sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ== debug@^4.1.0, debug@^4.1.1: version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" dedent@^0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== deepmerge@^4.2.2: version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== diff-sequences@^28.1.1: version "28.1.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== electron-to-chromium@^1.4.188: version "1.4.200" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.200.tgz#6e4c5266106688965b4ea7caa11f0dd315586854" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.200.tgz" integrity sha512-nPyI7oHc8T64oSqRXrAt99gNMpk0SAgPHw/o+hkNKyb5+bcdnFtZcSO9FUJES5cVkVZvo8u4qiZ1gQILl8UXsA== emittery@^0.10.2: version "0.10.2" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" escalade@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== esprima@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== execa@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -1037,12 +1032,12 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== expect@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" + resolved "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz" integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== dependencies: "@jest/expect-utils" "^28.1.3" @@ -1051,28 +1046,28 @@ expect@^28.1.3: jest-message-util "^28.1.3" jest-util "^28.1.3" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@2.x: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fb-watchman@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz" integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: bser "2.1.1" fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== dependencies: to-regex-range "^5.0.1" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -1080,42 +1075,37 @@ find-up@^4.0.0, find-up@^4.1.0: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - function-bind@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== glob@^7.1.3, glob@^7.1.4: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -1127,44 +1117,44 @@ glob@^7.1.3, glob@^7.1.4: globals@^11.1.0: version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== graceful-fs@^4.2.9: version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== import-local@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" @@ -1172,12 +1162,12 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -1185,54 +1175,54 @@ inflight@^1.0.4: inherits@2: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-core-module@^2.9.0: version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== dependencies: has "^1.0.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz" integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== dependencies: "@babel/core" "^7.12.3" @@ -1243,7 +1233,7 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-report@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -1252,7 +1242,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" @@ -1261,7 +1251,7 @@ istanbul-lib-source-maps@^4.0.0: istanbul-reports@^3.1.3: version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== dependencies: html-escaper "^2.0.0" @@ -1269,7 +1259,7 @@ istanbul-reports@^3.1.3: jest-changed-files@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-28.1.3.tgz#d9aeee6792be3686c47cb988a8eaf82ff4238831" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz" integrity sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA== dependencies: execa "^5.0.0" @@ -1277,7 +1267,7 @@ jest-changed-files@^28.1.3: jest-circus@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-28.1.3.tgz#d14bd11cf8ee1a03d69902dc47b6bd4634ee00e4" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz" integrity sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow== dependencies: "@jest/environment" "^28.1.3" @@ -1302,7 +1292,7 @@ jest-circus@^28.1.3: jest-cli@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-28.1.3.tgz#558b33c577d06de55087b8448d373b9f654e46b2" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz" integrity sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ== dependencies: "@jest/core" "^28.1.3" @@ -1320,7 +1310,7 @@ jest-cli@^28.1.3: jest-config@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-28.1.3.tgz#e315e1f73df3cac31447eed8b8740a477392ec60" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz" integrity sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ== dependencies: "@babel/core" "^7.11.6" @@ -1348,7 +1338,7 @@ jest-config@^28.1.3: jest-diff@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz" integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== dependencies: chalk "^4.0.0" @@ -1358,14 +1348,14 @@ jest-diff@^28.1.3: jest-docblock@^28.1.1: version "28.1.1" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-28.1.1.tgz#6f515c3bf841516d82ecd57a62eed9204c2f42a8" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz" integrity sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA== dependencies: detect-newline "^3.0.0" jest-each@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-28.1.3.tgz#bdd1516edbe2b1f3569cfdad9acd543040028f81" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz" integrity sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g== dependencies: "@jest/types" "^28.1.3" @@ -1376,7 +1366,7 @@ jest-each@^28.1.3: jest-environment-node@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.3.tgz#7e74fe40eb645b9d56c0c4b70ca4357faa349be5" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz" integrity sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A== dependencies: "@jest/environment" "^28.1.3" @@ -1388,12 +1378,12 @@ jest-environment-node@^28.1.3: jest-get-type@^28.0.2: version "28.0.2" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz" integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== jest-haste-map@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-28.1.3.tgz#abd5451129a38d9841049644f34b034308944e2b" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz" integrity sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA== dependencies: "@jest/types" "^28.1.3" @@ -1412,7 +1402,7 @@ jest-haste-map@^28.1.3: jest-leak-detector@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz#a6685d9b074be99e3adee816ce84fd30795e654d" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz" integrity sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA== dependencies: jest-get-type "^28.0.2" @@ -1420,7 +1410,7 @@ jest-leak-detector@^28.1.3: jest-matcher-utils@^28.0.0, jest-matcher-utils@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz" integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== dependencies: chalk "^4.0.0" @@ -1430,7 +1420,7 @@ jest-matcher-utils@^28.0.0, jest-matcher-utils@^28.1.3: jest-message-util@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz" integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== dependencies: "@babel/code-frame" "^7.12.13" @@ -1445,7 +1435,7 @@ jest-message-util@^28.1.3: jest-mock@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.3.tgz#d4e9b1fc838bea595c77ab73672ebf513ab249da" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz" integrity sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA== dependencies: "@jest/types" "^28.1.3" @@ -1453,25 +1443,25 @@ jest-mock@^28.1.3: jest-pnp-resolver@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^28.0.2: version "28.0.2" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz" integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== jest-resolve-dependencies@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz#8c65d7583460df7275c6ea2791901fa975c1fe66" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz" integrity sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA== dependencies: jest-regex-util "^28.0.2" jest-snapshot "^28.1.3" -jest-resolve@^28.1.3: +jest-resolve@*, jest-resolve@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-28.1.3.tgz#cfb36100341ddbb061ec781426b3c31eb51aa0a8" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz" integrity sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ== dependencies: chalk "^4.0.0" @@ -1486,7 +1476,7 @@ jest-resolve@^28.1.3: jest-runner@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-28.1.3.tgz#5eee25febd730b4713a2cdfd76bdd5557840f9a1" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz" integrity sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA== dependencies: "@jest/console" "^28.1.3" @@ -1513,7 +1503,7 @@ jest-runner@^28.1.3: jest-runtime@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-28.1.3.tgz#a57643458235aa53e8ec7821949e728960d0605f" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz" integrity sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw== dependencies: "@jest/environment" "^28.1.3" @@ -1541,7 +1531,7 @@ jest-runtime@^28.1.3: jest-snapshot@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.3.tgz#17467b3ab8ddb81e2f605db05583d69388fc0668" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz" integrity sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg== dependencies: "@babel/core" "^7.11.6" @@ -1570,7 +1560,7 @@ jest-snapshot@^28.1.3: jest-util@^28.0.0, jest-util@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz" integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== dependencies: "@jest/types" "^28.1.3" @@ -1582,7 +1572,7 @@ jest-util@^28.0.0, jest-util@^28.1.3: jest-validate@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-28.1.3.tgz#e322267fd5e7c64cea4629612c357bbda96229df" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz" integrity sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA== dependencies: "@jest/types" "^28.1.3" @@ -1594,7 +1584,7 @@ jest-validate@^28.1.3: jest-watcher@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.3.tgz#c6023a59ba2255e3b4c57179fc94164b3e73abd4" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz" integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== dependencies: "@jest/test-result" "^28.1.3" @@ -1608,16 +1598,16 @@ jest-watcher@^28.1.3: jest-worker@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^28.1.3: +jest@^28.0.0, jest@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-28.1.3.tgz#e9c6a7eecdebe3548ca2b18894a50f45b36dfc6b" + resolved "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz" integrity sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA== dependencies: "@jest/core" "^28.1.3" @@ -1627,12 +1617,12 @@ jest@^28.1.3: js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" @@ -1640,80 +1630,80 @@ js-yaml@^3.13.1: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-parse-even-better-errors@^2.3.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json5@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== kleur@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" lodash.memoize@4.x: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" make-dir@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" make-error@1.x: version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== makeerror@1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== micromatch@^4.0.4: version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: braces "^3.0.2" @@ -1721,96 +1711,96 @@ micromatch@^4.0.4: mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== normalize-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" object-hash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-limit@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -1820,49 +1810,49 @@ parse-json@^5.2.0: path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== picocolors@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pirates@^4.0.4: version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" pretty-format@^28.0.0, pretty-format@^28.1.3: version "28.1.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz" integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== dependencies: "@jest/schemas" "^28.1.3" @@ -1872,7 +1862,7 @@ pretty-format@^28.0.0, pretty-format@^28.1.3: prompts@^2.0.1: version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" @@ -1880,34 +1870,34 @@ prompts@^2.0.1: react-is@^18.0.0: version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve.exports@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== resolve@^1.20.0: version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: is-core-module "^2.9.0" @@ -1916,63 +1906,70 @@ resolve@^1.20.0: rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== sax@^1.2.4: version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -semver@7.x, semver@^7.3.5: +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.5: version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@7.x: + version "7.3.7" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== source-map-support@0.5.13: version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" @@ -1980,24 +1977,24 @@ source-map-support@0.5.13: source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stack-utils@^2.0.3: version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz" integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== dependencies: escape-string-regexp "^2.0.0" string-length@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" @@ -2005,7 +2002,7 @@ string-length@^4.0.1: string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -2014,50 +2011,50 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0: version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz" integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== dependencies: has-flag "^4.0.0" @@ -2065,12 +2062,12 @@ supports-hyperlinks@^2.0.0: supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== terminal-link@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== dependencies: ansi-escapes "^4.2.1" @@ -2078,7 +2075,7 @@ terminal-link@^2.0.0: test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -2087,24 +2084,24 @@ test-exclude@^6.0.0: tmpl@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" ts-jest@^28.0.7: version "28.0.7" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.7.tgz#e18757a9e44693da9980a79127e5df5a98b37ac6" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.7.tgz" integrity sha512-wWXCSmTwBVmdvWrOpYhal79bDpioDy4rTT+0vyUnE3ZzM7LOAAGG9NXwzkEL/a516rQEgnMmS/WKP9jBPCVJyA== dependencies: bs-logger "0.x" @@ -2118,22 +2115,22 @@ ts-jest@^28.0.7: type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typescript@^4.7.4: +typescript@^4.7.4, typescript@>=4.3: version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== update-browserslist-db@^1.0.4: version "1.0.5" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz" integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== dependencies: escalade "^3.1.1" @@ -2141,7 +2138,7 @@ update-browserslist-db@^1.0.4: v8-to-istanbul@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz" integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== dependencies: "@jridgewell/trace-mapping" "^0.3.12" @@ -2150,21 +2147,21 @@ v8-to-istanbul@^9.0.1: walker@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -2173,12 +2170,12 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.1.tgz#9faa33a964c1c85ff6f849b80b42a88c2c537c8f" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz" integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ== dependencies: imurmurhash "^0.1.4" @@ -2186,22 +2183,22 @@ write-file-atomic@^4.0.1: y18n@^5.0.5: version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yargs-parser@^21.0.0, yargs-parser@^21.0.1: version "21.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz" integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== yargs@^17.3.1: version "17.5.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz" integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== dependencies: cliui "^7.0.2" @@ -2214,5 +2211,5 @@ yargs@^17.3.1: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==