parent
ea21bc5fef
commit
14333dc7f1
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
npm-debug.log
|
||||||
|
.vscode/
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
class Generator {
|
||||||
|
/**
|
||||||
|
* Create a new generator
|
||||||
|
* @param {number} dimensions - The number of dimensions for the random vectors
|
||||||
|
*/
|
||||||
|
constructor(dimensions) {
|
||||||
|
this.dimensions = dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random vector with the specified number of dimensions.
|
||||||
|
* Each element of the vector is a random number between 0 and 1.
|
||||||
|
* @returns {number[]} A random vector of the given dimensions
|
||||||
|
*/
|
||||||
|
generate() {
|
||||||
|
return Array.from({ length: this.dimensions }, () => Math.random());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an array of a specified length, each element of which is a
|
||||||
|
* random vector of the given dimension.
|
||||||
|
* @param {number} count - The number of vectors to generate
|
||||||
|
* @returns {number[][]} an array of random vectors
|
||||||
|
*/
|
||||||
|
generateMany(count) {
|
||||||
|
return Array.from({ length: count }, () => this.generate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Generator;
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MTree API Interface</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>MTree API Interface</h1>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="queryPoint">Query Point (JSON Array):</label>
|
||||||
|
<input type="text" id="queryPoint" placeholder='e.g. [0.5, 0.5]' />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="radius">Radius:</label>
|
||||||
|
<input type="text" id="radius" placeholder="e.g. 1.0" />
|
||||||
|
</div>
|
||||||
|
<button onclick="performRangeQuery()">Perform Range Query</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function performRangeQuery() {
|
||||||
|
const queryPoint = document.getElementById('queryPoint').value;
|
||||||
|
const radius = document.getElementById('radius').value;
|
||||||
|
const response = await fetch(`/rangeQuery?queryPoint=${encodeURIComponent(queryPoint)}&radius=${encodeURIComponent(radius)}`);
|
||||||
|
//const result = await response.json();
|
||||||
|
//document.getElementById('result').innerText = JSON.stringify(result, null, 2);
|
||||||
|
document.getElementById('result').innerText = await response.text();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const MTree = require('./m-tree/mtree');
|
||||||
|
const dimensions = 2;
|
||||||
|
|
||||||
|
const Generator = require('./data/generator');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
|
||||||
|
function euclideanDistance(a, b) {
|
||||||
|
return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0));
|
||||||
|
}
|
||||||
|
const mtree = new MTree(dimensions, 10, euclideanDistance);
|
||||||
|
|
||||||
|
const generator = new Generator(dimensions);
|
||||||
|
const points = generator.generateMany(1000);
|
||||||
|
let i = 0;
|
||||||
|
points.forEach(point => { mtree.insert(point); i++; console.log(i); });
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/tree', (req, res) => {
|
||||||
|
res.send(JSON.parse(JSON.stringify(mtree, (key, value) => {
|
||||||
|
if (key === 'parent') return value && value.id;
|
||||||
|
return value;
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/insert', (req, res) => {
|
||||||
|
const point = req.body.point;
|
||||||
|
if (!point || !Array.isArray(point)) {
|
||||||
|
return res.status(400).send('Invalid point');
|
||||||
|
}
|
||||||
|
mtree.insert(point);
|
||||||
|
res.send('Point inserted');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/rangeQuery', (req, res) => {
|
||||||
|
const { queryPoint, radius } = req.query;
|
||||||
|
if (!queryPoint || !radius) {
|
||||||
|
return res.status(400).send('Invalid query parameters');
|
||||||
|
}
|
||||||
|
const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius));
|
||||||
|
res.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/kNNQuery', (req, res) => {
|
||||||
|
const { queryPoint, k } = req.query;
|
||||||
|
if (!queryPoint || !k) {
|
||||||
|
return res.status(400).send('Invalid query parameters');
|
||||||
|
}
|
||||||
|
const result = mtree.kNNQuery(JSON.parse(queryPoint), parseInt(k, 10));
|
||||||
|
res.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, () => {
|
||||||
|
console.log('MTree API is running on port 3000');
|
||||||
|
//console.log(mtree);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/recreate', (req, res) => {
|
||||||
|
const { dimensions } = req.body;
|
||||||
|
if (!dimensions || typeof dimensions !== 'number') {
|
||||||
|
return res.status(400).send('Invalid dimensions');
|
||||||
|
}
|
||||||
|
mtree = new MTree(dimensions, 10);
|
||||||
|
points = generator.generateMany(100);
|
||||||
|
points.forEach(point => mtree.insert(point));
|
||||||
|
res.send('MTree recreated');
|
||||||
|
});
|
||||||
@ -0,0 +1,254 @@
|
|||||||
|
const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./nodes');
|
||||||
|
|
||||||
|
class MTree {
|
||||||
|
/**
|
||||||
|
* Constructs a new MTree instance.
|
||||||
|
* @param {number} dimensions - The number of dimensions for data points.
|
||||||
|
* @param {number} capacity - The maximum number of entries a node can hold before splitting.
|
||||||
|
* @param {function} [distanceFunction] - A function to calculate the distance between two points. Defaults to Euclidean distance.
|
||||||
|
*/
|
||||||
|
constructor(dimensions, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) {
|
||||||
|
this.dimensions = dimensions;
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.distanceFunction = distanceFunction;
|
||||||
|
this.root = new LeafNode([], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new point into the MTree.
|
||||||
|
* @param {number[]} point - The point to insert
|
||||||
|
*/
|
||||||
|
insert(point) {
|
||||||
|
const node = this.findLeafNode(this.root, point);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('No leaf node found for insertion');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groundEntry = new GroundEntry(point);
|
||||||
|
node.entries.push(groundEntry);
|
||||||
|
console.log(node.entries.length, this.capacity);
|
||||||
|
if (node.entries.length > this.capacity) {
|
||||||
|
this.splitNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverses the MTree from the given node to find the leaf node
|
||||||
|
* that should hold the given point.
|
||||||
|
* @param {Node} node - The node to start searching from
|
||||||
|
* @param {number[]} point - The point to find the leaf node for
|
||||||
|
* @returns {LeafNode} The leaf node that should hold the given point
|
||||||
|
*/
|
||||||
|
findLeafNode(node, point) {
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node is undefined');
|
||||||
|
}
|
||||||
|
if (node.isLeaf) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const closestEntry = this.findClosestEntry(node.entries, point);
|
||||||
|
if (!closestEntry) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return this.findLeafNode(closestEntry.node, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the entry in the given array of entries that is closest to the given point.
|
||||||
|
* @param {NodeEntry[]} entries - The array of entries to search through
|
||||||
|
* @param {number[]} point - The point to find the closest entry to
|
||||||
|
* @returns {NodeEntry} The closest entry to the given point
|
||||||
|
*/
|
||||||
|
findClosestEntry(entries, point) {
|
||||||
|
let closestEntry = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const distance = this.distanceFunction(point, entry.point);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestEntry = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closestEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the given node into two nodes when the number of entries exceeds capacity.
|
||||||
|
* The original node retains half of the entries, and a new node is created with the other half.
|
||||||
|
* The new node is linked to the original node as a child by calculating and using the centroid
|
||||||
|
* as the connecting entry point.
|
||||||
|
*
|
||||||
|
* @param {Node} node - The node to be split.
|
||||||
|
*/
|
||||||
|
splitRoot(node) {
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node is undefined');
|
||||||
|
}
|
||||||
|
const entries = node.entries;
|
||||||
|
const mid = Math.ceil(entries.length / 2);
|
||||||
|
const leftEntries = entries.slice(0, mid);
|
||||||
|
const rightEntries = entries.slice(mid);
|
||||||
|
const leftNode = new LeafNode(leftEntries);
|
||||||
|
const rightNode = new LeafNode(rightEntries);
|
||||||
|
leftNode.parent = node; // Set parent of leftNode
|
||||||
|
rightNode.parent = node; // Set parent of rightNode
|
||||||
|
|
||||||
|
const leftCentroid = this.calculateCentroid(leftEntries);
|
||||||
|
const rightCentroid = this.calculateCentroid(rightEntries);
|
||||||
|
const leftRadius = this.calculateRadius(leftEntries, leftCentroid);
|
||||||
|
const rightRadius = this.calculateRadius(rightEntries, rightCentroid);
|
||||||
|
const leftEntry = new RoutingEntry(leftCentroid, leftNode, leftRadius);
|
||||||
|
const rightEntry = new RoutingEntry(rightCentroid, rightNode, rightRadius);
|
||||||
|
node.entries = [leftEntry, rightEntry];
|
||||||
|
node.isLeaf = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the given node into two nodes when the number of entries exceeds capacity.
|
||||||
|
* The given node will be left with half of the entries, and a new node is created with the other half.
|
||||||
|
* The new node is linked to the original node's parent as a sibling by calculating and using the centroid
|
||||||
|
* as the connecting entry point.
|
||||||
|
*
|
||||||
|
* @param {Node} node - The node to be split.
|
||||||
|
*/
|
||||||
|
splitNode(node) {
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node is undefined');
|
||||||
|
}
|
||||||
|
const entries = node.entries;
|
||||||
|
const mid = Math.ceil(entries.length / 2);
|
||||||
|
const leftEntries = entries.slice(0, mid);
|
||||||
|
const rightEntries = entries.slice(mid);
|
||||||
|
const newNode = new Node(rightEntries, node.isLeaf, node.parent);
|
||||||
|
|
||||||
|
if (node.parent) {
|
||||||
|
const rightCentroid = this.calculateCentroid(rightEntries);
|
||||||
|
const rightRadius = this.calculateRadius(rightEntries, rightCentroid);
|
||||||
|
const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius);
|
||||||
|
node.parent.entries.push(rightEntry);
|
||||||
|
|
||||||
|
if (node.parent.entries.length > this.capacity)
|
||||||
|
this.splitNode(node.parent);
|
||||||
|
|
||||||
|
node.entries = leftEntries;
|
||||||
|
} else {
|
||||||
|
this.splitRoot(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the centroid of a given set of entries.
|
||||||
|
* The centroid is the average position of all the points in the entries, calculated dimension-wise.
|
||||||
|
*
|
||||||
|
* @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers.
|
||||||
|
* @returns {number[]} The centroid of the entries as an array of numbers representing the average position.
|
||||||
|
*/
|
||||||
|
calculateCentroid(entries) {
|
||||||
|
const centroid = new Array(this.dimensions).fill(0);
|
||||||
|
for (const entry of entries) {
|
||||||
|
for (let i = 0; i < this.dimensions; i++) {
|
||||||
|
centroid[i] += entry.point[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return centroid.map(x => x / entries.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the maximum distance from the given centroid to any of the given entries.
|
||||||
|
*
|
||||||
|
* @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers.
|
||||||
|
* @param {number[]} centroid - The centroid to calculate the distance from.
|
||||||
|
* @returns {number} The maximum distance from the centroid to any of the entries.
|
||||||
|
*/
|
||||||
|
calculateRadius(entries, centroid) {
|
||||||
|
return Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all points in the tree that are within the given radius of the given query point.
|
||||||
|
* @param {number[]} queryPoint - The point to search around.
|
||||||
|
* @param {number} radius - The radius to search within.
|
||||||
|
* @returns {number[][]} An array of points that are within the radius of the query point.
|
||||||
|
*/
|
||||||
|
rangeQuery(queryPoint, radius) {
|
||||||
|
console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
||||||
|
const result = [];
|
||||||
|
this.rangeQueryRecursive(this.root, queryPoint, radius, result);
|
||||||
|
console.log(`rangeQuery: result=${JSON.stringify(result)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverses the MTree to find all points within the given radius of the given query point.
|
||||||
|
* @param {Node} node - The node to start searching from
|
||||||
|
* @param {number[]} queryPoint - The point to search around
|
||||||
|
* @param {number} radius - The radius to search within
|
||||||
|
* @param {number[][]} result - An array to store the result in
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
rangeQueryRecursive(node, queryPoint, radius, result) {
|
||||||
|
console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
||||||
|
if (node.isLeaf) {
|
||||||
|
for (const entry of node.entries) {
|
||||||
|
console.log(`rangeQueryRecursive: checking ground entry ${entry}`);
|
||||||
|
if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) {
|
||||||
|
result.push(entry.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const entry of node.entries) {
|
||||||
|
console.log(`rangeQueryRecursive: checking routing entry ${entry}`);
|
||||||
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
||||||
|
if (distance > radius + entry.radius) {
|
||||||
|
console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the k nearest neighbors to a given query point.
|
||||||
|
* @param {number[]} queryPoint - The point to find the nearest neighbors to
|
||||||
|
* @param {number} k - The number of nearest neighbors to find
|
||||||
|
* @returns {Object[]} An array of objects with 'point' and 'distance' properties, sorted by distance.
|
||||||
|
*/
|
||||||
|
kNNQuery(queryPoint, k) {
|
||||||
|
const result = Array(k).fill({ distance: Infinity });
|
||||||
|
this.kNNQueryRecursive(this.root, queryPoint, k, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverses the MTree to find the k nearest neighbors to a given query point.
|
||||||
|
* @param {Node} node - The node to start searching from
|
||||||
|
* @param {number[]} queryPoint - The point to find the nearest neighbors to
|
||||||
|
* @param {number} k - The number of nearest neighbors to find
|
||||||
|
* @param {Object[]} result - The result array to store the k nearest neighbors in, sorted by distance.
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
kNNQueryRecursive(node, queryPoint, k, result) {
|
||||||
|
if (node.isLeaf) {
|
||||||
|
for (const entry of node.entries) {
|
||||||
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
||||||
|
if (distance < result[k - 1].distance) {
|
||||||
|
result.push({ point: entry.point, distance });
|
||||||
|
result.sort((a, b) => a.distance - b.distance);
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const entry of node.entries) {
|
||||||
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
||||||
|
if (distance <= result[k - 1].distance + entry.radius) {
|
||||||
|
this.kNNQueryRecursive(entry.node, queryPoint, k, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MTree;
|
||||||
|
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
class RoutingEntry {
|
||||||
|
/**
|
||||||
|
* Create a new routing entry
|
||||||
|
* @param {number[]} point - The point of the entry
|
||||||
|
* @param {Node} node - The node associated with the entry
|
||||||
|
* @param {number} radius - The radius of the entry
|
||||||
|
*/
|
||||||
|
constructor(point, node, radius = 0) {
|
||||||
|
this.point = point;
|
||||||
|
this.node = node;
|
||||||
|
this.radius = radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroundEntry {
|
||||||
|
/**
|
||||||
|
* Create a new group entry
|
||||||
|
* @param {number[]} point - The point of the entry
|
||||||
|
* @param {number} data - The data associated with the entry
|
||||||
|
*/
|
||||||
|
constructor(point, data = null) {
|
||||||
|
this.point = point;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
static idCounter = 0; // Initialize a static counter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new node
|
||||||
|
* @param {GroundEntry[]|RoutingEntry[]} entries - The entries in the node
|
||||||
|
* @param {boolean} isLeaf - Whether the node is a leaf node
|
||||||
|
* @param {Node} parent - The parent node
|
||||||
|
*/
|
||||||
|
constructor(entries, isLeaf, parent) {
|
||||||
|
this.id = Node.idCounter++; // Assign a unique ID to the node
|
||||||
|
this.entries = entries;
|
||||||
|
this.isLeaf = isLeaf;
|
||||||
|
this.parent = parent;
|
||||||
|
//this.next = null;
|
||||||
|
//this.prev = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LeafNode extends Node {
|
||||||
|
/**
|
||||||
|
* Create a new leaf node
|
||||||
|
* @param {GroundEntry[]} entries - The entries in the node
|
||||||
|
* @param {Node} parent - The parent node
|
||||||
|
*/
|
||||||
|
constructor(entries, parent) {
|
||||||
|
super(entries, true, parent); // Call the Node constructor with isLeaf set to true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InternalNode extends Node {
|
||||||
|
/**
|
||||||
|
* Create a new internal node
|
||||||
|
* @param {RoutingEntry[]} entries - The entries in the node
|
||||||
|
* @param {Node} parent - The parent node
|
||||||
|
*/
|
||||||
|
constructor(entries, parent) {
|
||||||
|
super(entries, false, parent); // Call the Node constructor with isLeaf set to false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry };
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "ni-vmm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Basic backend for M-Trees",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@gitlab.fit.cvut.cz:spacefr1/ni-vmm.git"
|
||||||
|
},
|
||||||
|
"author": "František Špaček",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue