This commit is contained in:
abbas 2019-01-27 16:54:40 +05:30
Родитель 3f9ec00d86
Коммит f8f7d078b6
40 изменённых файлов: 9169 добавлений и 11 удалений

12
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,12 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

5
.gitignore поставляемый
Просмотреть файл

@ -37,7 +37,7 @@ node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# typings/
# Optional npm cache directory
.npm
@ -59,3 +59,6 @@ typings/
# next.js build output
.next
# generated files
lib

18
.npmignore Normal file
Просмотреть файл

@ -0,0 +1,18 @@
# gitignore
coverage/
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.nyc_output
lib
lib_test
input/
output/
# npmignore
src/
__tests__/
.vscode/

33
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug file",
"program": "${workspaceRoot}/lib/cli.js",
"args": [],
"cwd": "${workspaceRoot}",
"sourceMaps": true,
"smartStep": true,
"preLaunchTask": "build",
"outFiles": [
"${workspaceRoot}/lib/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "Debug test",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": [
"--findRelatedTests",
"${relativeFile}"
],
"cwd": "${workspaceRoot}"
}
]
}

3
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib/"
}

59
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,59 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Run current file",
"command": "ts-node ${relativeFile}",
"type": "shell",
"problemMatcher": []
},
{
"type": "npm",
"label": "clean",
"script": "clean",
"problemMatcher": []
},
{
"type": "npm",
"label": "build",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher":[]
},
{
"type": "npm",
"label": "format",
"script": "format",
"problemMatcher": []
},
{
"type": "npm",
"label": "coverage",
"script": "coverage",
"problemMatcher": []
},
{
"type": "npm",
"label": "test",
"script": "test",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": []
},
{
"type": "npm",
"label": "lint",
"script": "lint",
"problemMatcher": [
"$eslint-stylish"
]
}
]
}

13
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,13 @@
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

222
README.md
Просмотреть файл

@ -1,14 +1,216 @@
# Migrate To Graph
This tool allows you to migrate existing database to a graph database
# Contributing
Conversions supported:
- json to gremlin
- json to graph
- sql to graph
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
## Installation (CLI)
> npm i -g migrate-to-graph
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
## Usage (CLI)
Usage: migrate-to-graph [options] [command]
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
jsontogremlin <inputFile> <templateFile> <outputFile>
jsontograph <inputFile> <templateFile> <graphConfigFile>
sqltograph <sqlConfigFile> <query> <templateFile> <graphConfigFile>
## Installation (Lib)
> npm i --save migrate-to-graph
## Usage (Lib)
```js
var graphtool = require('migrate-to-graph');
var result = graphtool.jsonToGraph(json,template);
//or ES6
import {jsonToGraph} from 'migrate-to-graph';
```
### SQL Config File
```json5
{
"dialect":"mssql", //dialect to use, 'mysql'|'sqlite'|'postgres'|'mssql'
"username": "test",
"password": "password",
"host": "server",
"database": "database",
"options": {
"encrypt": true //set to true if you need encryption
}
}
```
### Graph Config File
```json5
{
"host":"server",
"password":"password",
"user": "username",
"port": "443",
"ssl": true,
"batchSize": 10, //No. of gremlin queries to execute in parallel (Default: 10)
"upsert": false //Set to true if you want to upsert vertices or edges (Default: false)
}
```
<b>Note: </b>For Azure cosmos graph DB , user is '/dbs/{dbName}/colls/{collectionName}' and password is its secretKey
## Template
To transform data to a graph, you need to transform the data into vertex and edge format.
Using a template you can convert a single data object into one/many vertexes and edges
We use handlebars to convert input to vertex / edge format
### Example
template:
```hbs
{
"vertices":[
{
"id": "{{myId}}",
"label": "vertexLabel",
"properties":{
"name": "{{myName}}"
}
},
{
"id": "{{myFriendId}}",
"label": "vertexLabel",
"properties":{
"name": "{{myFriendName}}"
}
}
],
"edges":[
{
"label": "friend",
"from": "{{myId}}",
"to": "{{myFriendId}}",
"properties": {
"value" : {{friendshipLvl}}
}
}
]
}
```
<b>Note: </b> You can specify as many vertices and edges as you want as long as it transforms to Vertex-Edge format
Input Data(a single entity from array of data):
```json
{
"myId": "1",
"myName": "abc",
"myFriendId": "2",
"myFriendName": "xyz",
"friendshipLvl": 3
}
```
Transformed Data:
```json
{
"vertices":[
{
"id": "1",
"label": "vertexLabel",
"properties":{
"name": "abc"
}
},
{
"id": "2",
"label": "vertexLabel",
"properties":{
"name": "xyz"
}
}
],
"edges":[
{
"label": "friend",
"from": "1",
"to": "2",
"properties": {
"value" : 3
}
}
]
}
```
## Vertex-Edge Format
This is a custom format inspired from the way Azure Cosmos Graph DB stores data. We use this format to convert it to gremlin queries so you need to provide a template which transforms to vertex-edge format
Model for Vertex and Edge
```ts
export interface Vertex {
id: string;
label: string; //label for the vertex
type: 'vertex';
properties: {
[key: string]: any; //Represents all the properties you wish to add to the vertex
};
}
export interface Edge {
id?: string;
label: string; //label for the edge
type: 'edge';
to: string; //id of vertex from which you want the edge to start
from: string; //id of vertex to which you want the edge to end
properties?: {
[key: string]: any; //Represents all the properties you wish to add to the edge
};
}
```
Vertex-Edge Format expects you specify an array of vertices and edges
```json
{
"vertices":[
{
"id": "1",
"label": "vertexLabel",
"properties":{
"name": "abc"
}
},
{
"id": "2",
"label": "vertexLabel",
"properties":{
"name": "xyz"
}
}
],
"edges":[
{
"label": "friend",
"from": "1",
"to": "2",
"properties": {
"value" : 3
}
}
]
}
```

Просмотреть файл

@ -0,0 +1,99 @@
import { Transformer } from '../../src/transformer/transformer';
import * as graphSchema from '../../src/schema/graph-schema.json';
const template = `{
"vertices":[
{
"id": "{{myId}}",
"label": "vertexLabel",
"properties":{
"name": "{{myName}}"
}
},
{
"id": "{{myFriendId}}",
"label": "vertexLabel",
"properties":{
"name": "{{myFriendName}}"
}
}
],
"edges":[
{
"label": "friend",
"from": "{{myId}}",
"to": "{{myFriendId}}",
"properties": {
"value" : {{friendshipLvl}}
}
}
]
}`;
describe('Given transformer is initialised', () => {
let transformer: Transformer;
beforeAll(() => {
transformer = new Transformer({});
});
it('when parseTemplate is called it should return the parsed data', () => {
const result = transformer.parseTemplate(`{{hello}}`, { hello: 'world' });
expect(result).toEqual('world');
});
it('when parseTemplate is called it with guid helper it should return the parsed data', () => {
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const result = transformer.parseTemplate(`{{$guid}}`, {});
expect(result).toMatch(guidRegex);
});
it('when transformJSON is called it should transform JSON', () => {
const data = [
{
myId: '1',
myName: 'abc',
myFriendId: '2',
myFriendName: 'xyz',
friendshipLvl: 3,
},
];
const expectedResult = {
vertices: [
{
id: '1',
label: 'vertexLabel',
properties: {
name: 'abc',
},
},
{
id: '2',
label: 'vertexLabel',
properties: {
name: 'xyz',
},
},
],
edges: [
{
from: '1',
label: 'friend',
to: '2',
properties: {
value: 3,
},
},
],
};
const result = transformer.transformJSON(template, data, graphSchema);
expect(result).toEqual(expectedResult);
});
it('when validateJSON is called and json does not match the schema, it should throw error', () => {
const json = { vertices: [{ test: 'data' }] };
expect(() => transformer.validateJSON(json, graphSchema)).toThrow();
});
});

5
__tests__/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,5 @@
{
"compilerOptions": {
"resolveJsonModule": true
}
}

7440
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

94
package.json Normal file
Просмотреть файл

@ -0,0 +1,94 @@
{
"name": "migrate-to-graph",
"version": "0.0.1",
"description": "A tool to migrate existing database to a graph database",
"license": "MIT",
"repository": "https://github.com/Microsoft/MigrateToGraph",
"author": {
"name": "Microsoft Corporation"
},
"keywords": [
"graph",
"gremlin",
"database",
"migrate",
"graph-database",
"migration-tool",
"cosmosdb"
],
"files": [
"lib"
],
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"scripts": {
"clean": "rimraf lib && rimraf coverage",
"gen-schema": "typescript-json-schema tsconfig.json GraphInfo --required -o src/schema/graph-schema.json",
"format": "prettier --write \"{src,__tests__}/**/*.ts\" --single-quote --trailing-comma es5",
"lint": "tslint --force --format verbose \"src/**/*.ts\"",
"prepublishOnly": "npm run build",
"prebuild": "npm run clean && npm run format && npm run lint && echo Using TypeScript && tsc --version",
"build": "tsc --pretty",
"test": "jest",
"coverage": "jest --coverage",
"watch": "npm run build -- --watch",
"watch:test": "jest --watch",
"start": "ts-node ./src/index.ts"
},
"dependencies": {
"ajv": "^6.5.4",
"async": "^2.6.1",
"commander": "^2.19.0",
"convert-hrtime": "^2.0.0",
"fs-extra": "^7.0.0",
"gremlin": "^2.7.0",
"handlebars": "^4.0.12",
"json-source-map": "^0.4.0",
"jsonlint": "^1.6.3",
"mssql": "^4.2.2",
"mysql2": "^1.6.1",
"pg": "^7.6.0",
"pg-hstore": "^2.3.2",
"sequelize": "^4.41.0",
"single-line-log": "^1.1.2",
"sqlite3": "^4.0.3",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/async": "^2.0.50",
"@types/convert-hrtime": "^2.0.0",
"@types/fs-extra": "^5.0.4",
"@types/handlebars": "^4.0.38",
"@types/jest": "^23.3.7",
"@types/mssql": "^4.0.8",
"@types/node": "^8.10.36",
"@types/sequelize": "^4.27.29",
"@types/single-line-log": "^1.1.0",
"@types/uuid": "^3.4.3",
"coveralls": "^3.0.2",
"jest": "^23.6.0",
"prettier": "^1.14.3",
"rimraf": "^2.0.0",
"ts-jest": "^23.6.0",
"ts-node": "^7.0.1",
"tslint": "^5.0.0",
"tslint-config-prettier": "^1.1.0",
"typescript": "^3.1.3",
"typescript-json-schema": "^0.34.0"
},
"engines": {
"node": ">=6.0.0"
},
"bin": "./lib/cli.js",
"jest": {
"transform": {
".(ts)": "ts-jest"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$",
"moduleFileExtensions": [
"ts",
"js"
],
"testEnvironment": "node"
}
}

60
src/cli.ts Normal file
Просмотреть файл

@ -0,0 +1,60 @@
#!/usr/bin/env node --no-warnings
import * as program from 'commander';
import {
jsonToGraphCmd,
jsonToGremlinCmd,
sqlToGraphCmd,
runCmd,
} from './index';
program
.version('0.0.1')
.command('run <configFile>')
.action((configFile: string) => {
console.log('Executing run command');
runCmd(configFile);
});
program
.command('jsontogremlin <inputFile> <templateFile> <outputFile>')
.action((inputFile: string, templateFile: string, outputFile: string) => {
console.log('Executing JSON to Gremlin command');
jsonToGremlinCmd(inputFile, templateFile, outputFile);
});
program
.command('jsontograph <inputFile> <templateFile> <graphConfigFile>')
.action(
(inputFile: string, templateFile: string, graphConfigFile: string) => {
console.log('Executing JSON to Graph command');
jsonToGraphCmd(inputFile, templateFile, graphConfigFile);
}
);
program
.command(
'sqltograph <sqlConfigFile> <query> <templateFile> <graphConfigFile>'
)
.action(
(
sqlConfigFile: string,
query: string,
templateFile: string,
graphConfigFile: string
) => {
console.log('Executing SQL to Graph command');
sqlToGraphCmd(sqlConfigFile, query, templateFile, graphConfigFile);
}
);
program.on('command:*', () => {
console.error(
'Invalid command: %s\nSee --help for a list of available commands.',
program.args.join(' ')
);
process.exit(1);
});
if (process.argv.length < 3) {
program.help();
}
program.parse(process.argv);

Просмотреть файл

@ -0,0 +1,37 @@
import * as fs from 'fs-extra';
import { GremlinConnector } from '../connectors/gremlin-connector';
import * as graphSchema from '../schema/graph-schema.json';
import { Transformer } from '../transformer/transformer';
export function jsonToGraphCmd(
inputFile: string,
templateFile: string,
graphConfigFile: string
) {
const inputJSON = fs.readJSONSync(inputFile) as any[];
const template = fs.readFileSync(templateFile, { encoding: 'utf-8' });
const graphConfig = fs.readJsonSync(graphConfigFile);
jsonToGraph(inputJSON, template, graphConfig, err => {
if (err) {
console.log(err.message);
}
});
}
export function jsonToGraph(
inputJSON: any[],
template: string,
graphConfig: any,
callback?: (err: any) => void
): void {
const transformer = new Transformer({});
const result = transformer.transformJSON(template, inputJSON, graphSchema);
const connector = new GremlinConnector(graphConfig);
connector.createGraph(result, (err: any) => {
connector.closeConnection();
if (callback) {
callback(err);
}
});
}

Просмотреть файл

@ -0,0 +1,31 @@
import * as fs from 'fs-extra';
import * as GraphHelper from '../helpers/graphHelper';
import * as graphSchema from '../schema/graph-schema.json';
import { Transformer } from '../transformer/transformer';
export function jsonToGremlinCmd(
inputFile: string,
templateFile: string,
outputFile: string
) {
const inputJSON = fs.readJSONSync(inputFile) as any[];
const template = fs.readFileSync(templateFile, { encoding: 'utf-8' });
try {
const graphCmdList: string[] = jsonToGremlin(inputJSON, template);
fs.writeFileSync(outputFile, graphCmdList.join('\n'));
} catch (err) {
console.log(err.message);
}
}
export function jsonToGremlin(inputJSON: any[], template: string): string[] {
const transformer = new Transformer({});
const result = transformer.transformJSON(template, inputJSON, graphSchema);
const vertexCmdList: string[] = result.vertices.map(vertex =>
GraphHelper.getAddVertexQuery(vertex)
);
const edgeCmdList: string[] = result.edges.map(edge =>
GraphHelper.getAddEdgeQuery(edge)
);
return vertexCmdList.concat(edgeCmdList);
}

42
src/commands/run.ts Normal file
Просмотреть файл

@ -0,0 +1,42 @@
import { RunConfig } from '../models/config-model';
import * as async from 'async';
import * as fs from 'fs-extra';
import { getInputConnector, getOutputConnector } from '../loaders/io-loader';
import { Transformer } from '../transformer/transformer';
import * as graphSchema from '../schema/graph-schema.json';
import { GraphInfo } from '../models/graph-model';
export function runCmd(configFile: string) {
const config = fs.readJSONSync(configFile);
run(config, (err: any) => {
if (err) {
console.log(err.message);
}
});
}
export function run(config: RunConfig, callback?: any) {
const inputConnector = getInputConnector(
config.input.type,
config.input.config
);
const transformer = new Transformer(config.transform);
const outputConnector = getOutputConnector(
config.output.type,
config.output.config
);
async.waterfall(
[
(cb: any) => inputConnector.readInput(cb),
(data: any[], cb: any) =>
transformer.transformInput(data, graphSchema, cb),
(data: GraphInfo, cb: any) => outputConnector.saveOutput(data, cb),
],
err => {
if (callback) {
callback(err);
}
}
);
}

Просмотреть файл

@ -0,0 +1,53 @@
import * as async from 'async';
import * as fs from 'fs-extra';
import { GremlinConnector } from '../connectors/gremlin-connector';
import { SQLInputConnnector } from '../connectors/sql-input-connector';
import * as graphSchema from '../schema/graph-schema.json';
import { Transformer } from '../transformer/transformer';
export function sqlToGraphCmd(
sqlConfigFile: string,
query: string,
templateFile: string,
graphConfigFile: string
) {
const sqlConfig = fs.readJSONSync(sqlConfigFile);
const template = fs.readFileSync(templateFile, { encoding: 'utf-8' });
const graphConfig: any = fs.readJSONSync(graphConfigFile);
sqlToGraph(sqlConfig, query, template, graphConfig, err => {
if (err) {
console.log(err.message);
}
});
}
export function sqlToGraph(
sqlConfig: any,
query: string,
template: string,
graphConfig: any,
callback?: (err: any) => void
) {
const sqlConnector = new SQLInputConnnector(sqlConfig);
const graphConnector = new GremlinConnector(graphConfig);
async.waterfall(
[
(cb: any) => {
sqlConnector.queryDatabase(query, cb);
},
(rows: any[], cb: any) => {
const transformer = new Transformer({});
const result = transformer.transformJSON(template, rows, graphSchema);
graphConnector.createGraph(result, cb);
},
],
err => {
sqlConnector.closeConnection();
graphConnector.closeConnection();
if (callback) {
callback(err);
}
}
);
}

Просмотреть файл

@ -0,0 +1,173 @@
import * as async from 'async';
import * as convertHrtime from 'convert-hrtime';
import * as Gremlin from 'gremlin';
import { stdout as log } from 'single-line-log';
import * as GraphHelper from '../helpers/graphHelper';
import { Edge, Etype, GraphInfo, Vertex } from '../models/graph-model';
import { isNullOrUndefined } from 'util';
import { OutputConnector } from '../models/connector-model';
export class GremlinConnector implements OutputConnector {
private client: Gremlin.GremlinClient;
private batchSize: number;
private upsert: boolean;
private retry: number;
private defaultRetry = 3;
private defaultBatchSize = 10;
constructor(config: any) {
this.client = Gremlin.createClient(config.port, config.host, {
password: config.password,
session: false,
ssl: true,
user: config.user,
});
this.batchSize = config.batchSize
? config.batchSize
: this.defaultBatchSize;
this.upsert = config.upsert;
this.retry = config.retry ? config.retry : this.defaultRetry;
}
public createGraph(graphInfo: GraphInfo, callback: any) {
async.series(
[
(cb: any) => {
this.addVertices(graphInfo.vertices, cb);
},
(cb: any) => {
this.addEdges(graphInfo.edges, cb);
},
],
err => callback(err)
);
}
public addVertices(vertices: Vertex[], callback: any) {
const timer = process.hrtime();
this.addToGraph(Etype.vertex, vertices, (err: any) => {
if (!err) {
console.log('\nFinished adding vertices');
const timeTaken = convertHrtime(process.hrtime(timer)).seconds;
console.log(
`Added ${vertices.length} vertices in ${timeTaken} seconds`
);
}
callback(err);
});
}
public addEdges(edges: Edge[], callback: any) {
const timer = process.hrtime();
this.addToGraph(Etype.edge, edges, (err: any) => {
if (!err) {
console.log('\nFinished adding edges');
const timeTaken = convertHrtime(process.hrtime(timer)).seconds;
console.log(`Added ${edges.length} edges in ${timeTaken} seconds`);
}
callback(err);
});
}
public saveOutput(data: GraphInfo, callback: any) {
this.createGraph(data, (err: any) => {
this.client.closeConnection();
callback(err);
});
}
public closeConnection() {
this.client.closeConnection();
}
public checkExists(type: Etype, id: string, callback: any) {
if (!this.upsert || isNullOrUndefined(id)) {
callback(null, false);
return;
}
let query: string;
if (type === Etype.vertex) {
query = `g.V().hasId('${id}').count()`;
} else {
query = `g.E().hasId('${id}').count()`;
}
this.client.execute(query, (err, res) => {
let exists = false;
if (res && res.length > 0 && res[0] > 0) {
exists = true;
}
callback(err, exists);
});
}
private addToGraph(type: Etype, arr: Vertex[] | Edge[], callback: any) {
const timer = process.hrtime();
const retryableIterator = this.getRetryable(
this.vertexEdgeIterator.bind(this)
);
let completedCnt = 0;
async.eachOfLimit(
arr as Vertex[] & Edge[],
this.batchSize,
(value, key, cb) => {
retryableIterator(type, value, (err: any) => {
if (!err) {
log(`Added(${type}): ${++completedCnt}/${arr.length}`);
}
cb(err);
});
},
err => {
callback(err);
}
);
}
private vertexEdgeIterator(type: Etype, value: Vertex | Edge, callback: any) {
const id = value.properties ? value.properties.id : null;
async.waterfall(
[
(cb: any) => this.checkExists(type, id, cb),
(res: boolean, cb: any) => {
const command = this.getCommand(type, value, res);
this.client.execute(command, cb);
},
],
callback
);
}
private getRetryable(task: any) {
if (this.retry > 0) {
return async.retryable(
{
interval: retryCount => {
return 500 * Math.pow(2, retryCount);
},
times: this.retry,
},
task
);
} else {
return task;
}
}
private getCommand(
type: Etype,
value: Vertex | Edge,
update: boolean = false
): string {
if (type === Etype.vertex) {
return update
? GraphHelper.getUpdateVertexQuery(value as Vertex)
: GraphHelper.getAddVertexQuery(value as Vertex);
} else {
return update
? GraphHelper.getUpdateEdgeQuery(value as Edge)
: GraphHelper.getAddEdgeQuery(value as Edge);
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
import { OutputConnector } from '../models/connector-model';
import * as fs from 'fs-extra';
import * as async from 'async';
import * as GraphHelper from '../helpers/graphHelper';
import { GraphInfo } from '../models/graph-model';
export class GremlinCmdOutputConnector implements OutputConnector {
private config: any;
constructor(config: any) {
this.config = config;
}
public saveOutput(data: GraphInfo, callback: any): void {
if (!this.config || !this.config.fileName) {
callback(new Error('fileName not specified in output config'));
}
const fileName = this.config.fileName;
const vertices = data.vertices.map(c => GraphHelper.getAddVertexQuery(c));
const edges = data.edges.map(c => GraphHelper.getAddEdgeQuery(c));
const commands = vertices.concat(edges);
fs.writeFile(
fileName,
commands.join('\n'),
{ encoding: 'utf-8' },
callback
);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
import { InputConnector } from '../models/connector-model';
import * as fs from 'fs-extra';
export class JSONInputConnector implements InputConnector {
private config: any;
constructor(config: any) {
this.config = config;
}
public readInput(callback: any): void {
if (this.config.data) {
callback(null, this.config.data);
} else if (this.config.filePath) {
fs.readJSON(this.config.filePath, { encoding: 'utf-8' }, callback);
} else {
callback(new Error('JSON filePath not provided in config'));
}
}
}

Просмотреть файл

@ -0,0 +1,53 @@
import * as async from 'async';
import * as Sequelize from 'sequelize';
import { InputConnector } from '../models/connector-model';
export class SQLInputConnnector implements InputConnector {
private connection: Sequelize.Sequelize;
private query: string;
constructor(config: any) {
config.options = config.options || {};
this.query = config.query;
config.options.rowCollectionOnRequestCompletion = true;
this.connection = new Sequelize(
config.database,
config.username,
config.password,
{
dialect: config.dialect,
dialectOptions: config.options,
host: config.host,
operatorsAliases: false,
pool: {
idle: 10000,
max: 5,
min: 0,
},
}
);
}
public queryDatabase(query: string, callback: any): void {
this.connection
.query(query, { raw: false, type: Sequelize.QueryTypes.SELECT })
.then(
response => {
callback(null, response);
},
error => {
callback(error);
}
);
}
public readInput(callback: any): void {
this.queryDatabase(this.query, (...args: any[]) => {
this.closeConnection();
callback(...args);
});
}
public closeConnection() {
this.connection.close();
}
}

Просмотреть файл

@ -0,0 +1,75 @@
import { Edge, Vertex } from '../models/graph-model';
import { escapeSingleQuote } from '../utils/safeString';
export function getAddVertexQuery(vertexObj: Vertex): string {
const label = escapeSingleQuote(vertexObj.label);
const query = `g.addV('${label}')`;
return query + getIdQuery(vertexObj) + getPropertiesQuery(vertexObj);
}
export function getUpdateVertexQuery(vertexObj: Vertex): string {
const id = escapeSingleQuote(vertexObj.properties.id);
const query = `g.V().hasId('${id}')`;
return query + getPropertiesQuery(vertexObj);
}
export function getUpdateEdgeQuery(edgeObj: Edge): string {
const id = escapeSingleQuote(edgeObj.properties.id);
const query = `g.E().hasId('${id}')`;
return query + getPropertiesQuery(edgeObj);
}
export function getAddEdgeQuery(edgeObj: Edge): string {
const from = escapeSingleQuote(edgeObj.from);
const to = escapeSingleQuote(edgeObj.to);
const label = escapeSingleQuote(edgeObj.label);
const query =
`g.V().has('id','${from}').addE('${label}')` +
`.to(g.V().has('id','${to}'))`;
return query + getIdQuery(edgeObj) + getPropertiesQuery(edgeObj);
}
export function getPropertiesQuery(obj: Vertex | Edge): string {
let query = '';
if (obj.properties) {
for (const key of Object.keys(obj.properties)) {
if (key === 'id') {
continue;
}
let value = obj.properties[key];
if (typeof value === 'string') {
value = escapeSingleQuote(value);
value = `'${value}'`;
}
const safeKey = escapeSingleQuote(key);
query += `.property('${safeKey}',${value})`;
}
}
return query;
}
export function removeDuplicateVertexes(vertexes: Vertex[]) {
const seen: { [key: string]: boolean } = {};
return vertexes.filter(vertex => {
return seen.hasOwnProperty(vertex.id) ? false : (seen[vertex.id] = true);
});
}
export function removeDuplicateEdges(edges: Edge[]) {
const seen: { [key: string]: boolean } = {};
return edges.filter(edge => {
let edgeId = `${edge.label}-${edge.from}-${edge.to}`;
if (edge.id) {
edgeId = edge.id;
}
return seen.hasOwnProperty(edgeId) ? false : (seen[edgeId] = true);
});
}
function getIdQuery(obj: Vertex | Edge) {
if (!obj.id) {
return '';
}
return `.property('id','${obj.id}')`;
}

4
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,4 @@
export * from './commands/jsonToGraph';
export * from './commands/jsonToGremlin';
export * from './commands/sqlToGraph';
export * from './commands/run';

40
src/loaders/io-loader.ts Normal file
Просмотреть файл

@ -0,0 +1,40 @@
import {
InputConnector,
InputType,
OutputType,
} from '../models/connector-model';
import { SQLInputConnnector } from '../connectors/sql-input-connector';
import { JSONInputConnector } from '../connectors/json-input-connector';
import { GremlinConnector } from '../connectors/gremlin-connector';
import { GremlinCmdOutputConnector } from '../connectors/gremlinCmd-output-connector';
export function getInputConnector(
type: InputType,
config: any
): InputConnector {
switch (type) {
case InputType.sql: {
return new SQLInputConnnector(config);
}
case InputType.json: {
return new JSONInputConnector(config);
}
default: {
throw new Error('Invalid input type');
}
}
}
export function getOutputConnector(type: OutputType, config: any) {
switch (type) {
case OutputType.gremlinGraph: {
return new GremlinConnector(config);
}
case OutputType.gremlinCmd: {
return new GremlinCmdOutputConnector(config);
}
default: {
throw new Error('Invalid output type');
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
import { InputType, OutputType } from './connector-model';
export interface RunConfig {
input: RunInputConfig;
transform?: RunTransformConfigStringTemplate | RunTransformConfigTemplatePath;
output: RunOutputConfig;
}
interface RunInputConfig {
type: InputType;
config: any;
}
interface RunOutputConfig {
type: OutputType;
config: any;
}
export interface RunTransformConfigStringTemplate {
template: string;
}
export interface RunTransformConfigTemplatePath {
templatePath: string;
}

Просмотреть файл

@ -0,0 +1,17 @@
export interface InputConnector {
readInput(callback: any): void;
}
export interface OutputConnector {
saveOutput(data: any, callback: any): void;
}
export enum InputType {
sql = 'sql',
json = 'json',
}
export enum OutputType {
gremlinGraph = 'gremlinGraph',
gremlinCmd = 'gremlinCmd',
}

37
src/models/graph-model.ts Normal file
Просмотреть файл

@ -0,0 +1,37 @@
export interface Vertex {
id: string;
label: string;
properties: {
[key: string]: any;
};
}
export interface Edge {
id?: string;
label: string;
to: string;
from: string;
properties?: {
[key: string]: any;
};
}
export interface GraphInfo {
vertices: Vertex[];
edges: Edge[];
}
export interface VertexEdgeArray {
[index: number]: Vertex | Edge;
}
// tslint:disable-next-line:interface-name
export interface GraphCmd {
vertexCmd: string[];
edgeCmd: string[];
}
export enum Etype {
vertex = 'vertex',
edge = 'edge',
}

Просмотреть файл

@ -0,0 +1,73 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Edge": {
"properties": {
"from": {
"type": "string"
},
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"properties": {
"additionalProperties": {
},
"type": "object"
},
"to": {
"type": "string"
}
},
"required": [
"from",
"label",
"to"
],
"type": "object"
},
"Vertex": {
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"properties": {
"additionalProperties": {
},
"type": "object"
}
},
"required": [
"id",
"label",
"properties"
],
"type": "object"
}
},
"properties": {
"edges": {
"items": {
"$ref": "#/definitions/Edge"
},
"type": "array"
},
"vertices": {
"items": {
"$ref": "#/definitions/Vertex"
},
"type": "array"
}
},
"required": [
"edges",
"vertices"
],
"type": "object"
}

Просмотреть файл

@ -0,0 +1,75 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"InputType": {
"enum": [
"json",
"sql"
],
"type": "string"
},
"OutputType": {
"enum": [
"gremlinCmd",
"gremlinGraph"
],
"type": "string"
},
"RunInputConfig": {
"properties": {
"config": {
},
"type": {
"$ref": "#/definitions/InputType"
}
},
"type": "object"
},
"RunOutputConfig": {
"properties": {
"config": {
},
"type": {
"$ref": "#/definitions/OutputType"
}
},
"type": "object"
},
"RunTransformConfigStringTemplate": {
"properties": {
"template": {
"type": "string"
}
},
"type": "object"
},
"RunTransformConfigTemplatePath": {
"properties": {
"templatePath": {
"type": "string"
}
},
"type": "object"
}
},
"properties": {
"input": {
"$ref": "#/definitions/RunInputConfig"
},
"output": {
"$ref": "#/definitions/RunOutputConfig"
},
"transform": {
"anyOf": [
{
"$ref": "#/definitions/RunTransformConfigStringTemplate"
},
{
"$ref": "#/definitions/RunTransformConfigTemplatePath"
}
]
}
},
"type": "object"
}

Просмотреть файл

@ -0,0 +1,101 @@
import * as Ajv from 'ajv';
import * as handlebars from 'handlebars';
import * as jsonlint from 'jsonlint';
import * as uuid from 'uuid';
import * as fs from 'fs-extra';
import * as GraphHelper from '../helpers/graphHelper';
import { Edge, GraphInfo, Vertex } from '../models/graph-model';
import { ajvErrorLint } from '../utils/ajvErrorLint';
import {
RunTransformConfigTemplatePath,
RunTransformConfigStringTemplate,
} from '../models/config-model';
export class Transformer {
private validator: Ajv.Ajv;
private config: RunTransformConfigStringTemplate &
RunTransformConfigTemplatePath;
constructor(config?: any) {
this.config = config;
this.registerHelpers();
this.validator = new Ajv({ jsonPointers: true });
}
public parseTemplate(template: string, data: object): string {
const compiledTemplate = this.getCompiledTemplate(template);
return compiledTemplate(data);
}
public transformJSON(
template: string,
jsonArray: object[],
validationSchema?: object
): GraphInfo {
let vertices: Vertex[] = [];
let edges: Edge[] = [];
const compiledTemplate = this.getCompiledTemplate(template);
jsonArray.forEach(doc => {
const transformedDoc = compiledTemplate(doc);
const result: GraphInfo = jsonlint.parse(transformedDoc);
if (validationSchema) {
this.validateJSON(result, validationSchema);
}
vertices = vertices.concat(result.vertices);
edges = edges.concat(result.edges);
});
vertices = GraphHelper.removeDuplicateVertexes(vertices);
edges = GraphHelper.removeDuplicateEdges(edges);
return { vertices, edges };
}
public transformInput(data: any[], schema: object, callback: any) {
const template = this.getTemplateFromConfig();
if (template) {
const result = this.transformJSON(template, data, schema);
callback(null, result);
} else {
callback(null, data);
}
}
public validateJSON(json: any, schema: object) {
const valid = this.validator.validate(schema, json);
if (!valid) {
const output = ajvErrorLint(
json,
this.validator.errors![0] as Ajv.ErrorObject,
this.validator.errorsText()
);
throw new Error('Schema validation error: \n' + output);
}
return valid;
}
private getTemplateFromConfig() {
let template = '';
if (this.config && this.config.template) {
template = this.config.template;
} else if (this.config && this.config.templatePath) {
template = fs.readFileSync(this.config.templatePath, {
encoding: 'utf-8',
});
}
return template;
}
private registerHelpers(): void {
handlebars.registerHelper('$guid', () => uuid.v4());
handlebars.registerHelper('toJSON', object => {
return new handlebars.SafeString(JSON.stringify(object));
});
}
private getCompiledTemplate(template: string) {
return handlebars.compile(template);
}
}

9
src/typings/async.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
import * as async from 'async';
declare module 'async' {
export function retryable<T, E = Error>(
opts:
| number
| { times: number; interval: number | ((retryCount: number) => number) },
task: AsyncFunction<T, E>
): AsyncFunction<T, E>;
}

136
src/typings/gremlin.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,136 @@
/* tslint:disable */
declare module 'gremlin' {
import { EventEmitter } from 'events';
import { Readable } from 'stream';
export interface Bindings {
[key: string]: any;
}
export interface QueryObject {
gremlin: string;
bindings: Bindings;
}
export class WebSocketGremlinConnection extends EventEmitter {
open: boolean;
sendMessage(message: string): void;
handleMessage(message: string): void;
}
export class MessageStream extends Readable {}
export class GremlinClient extends EventEmitter {
closeConnection: () => void;
port: number;
host: string;
options: ClientOptions;
useSession: boolean;
sessionId: string;
connected: boolean;
connection: WebSocketGremlinConnection;
execute<T>(
script: QueryObject,
callback: (err: Error | null, results: T[]) => void
): void;
execute<T>(
script: string,
callback: (err: Error | null, results: T[]) => void
): void;
execute<T>(
script: string,
bindings: Bindings,
callback: (err: Error | null, results: T[]) => void
): void;
execute<T>(
script: string,
bindings: Bindings,
message: any,
callback: (err: Error | null, results: T[]) => void
): void;
messageStream(script: QueryObject): MessageStream;
messageStream(script: string): MessageStream;
messageStream(script: string, bindings: Bindings): MessageStream;
messageStream(
script: string,
bindings: Bindings,
message: any
): MessageStream;
readable(script: QueryObject): Readable;
readable(script: string): Readable;
readable(script: string, bindings: Bindings): Readable;
readable(script: string, bindings: Bindings, message: any): Readable;
}
export interface ClientOptions {
/**
* Whether to use sessions or not (default: `false`).
*/
session?: boolean;
/**
* The script engine to use on the server, see your gremlin-server.yaml file (default: `"gremlin-groovy"`).
*/
language?: string;
/**
* The name of the "operation" to execute based on the available OpProcessor (default: `"eval"`).
*/
op?: string;
/**
* The name of the OpProcessor to utilize (default: `""`).
*/
processor?: string;
/**
* Mime type of returned responses, depending on the serializer (default: `"application/json"`).
*/
accept?: string;
/**
* A custom URL connection path if connecting to a Gremlin server behind a WebSocket proxy.
*/
path?: string;
ssl?: boolean;
user?: string;
password?: string;
}
export function createClient(
port: number,
host: string,
options?: ClientOptions
): GremlinClient;
export function createClient(
port: number,
options?: ClientOptions
): GremlinClient;
export function createClient(options?: ClientOptions): GremlinClient;
export interface QueryPromise<T> extends Promise<T> {
readonly query: string;
}
export interface TemplateTag {
<T>(gremlinChunks: TemplateStringsArray, ...values: any[]): QueryPromise<
T[]
>;
}
export function makeTemplateTag(client: GremlinClient): TemplateTag;
export interface Functions {
[key: string]: (...args: any[]) => QueryObject;
}
export interface BoundFunctions {
[key: string]: <T>(...args: any[]) => QueryPromise<T[]>;
}
export function bindForClient(
client: GremlinClient,
functions: Functions
): BoundFunctions;
}

7
src/typings/json-source-map.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
declare module 'json-source-map' {
export function stringify(
value: any,
replacer?: (key: string, value: any) => any,
space?: string | number
): { json: string; pointers: any };
}

9
src/typings/json.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
declare module '*/package.json' {
const version: string;
export { version };
}
declare module '*.json' {
const value: any;
export default value;
}

3
src/typings/jsonlint.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
declare module 'jsonlint' {
export function parse(input: string): any;
}

16
src/utils/ajvErrorLint.ts Normal file
Просмотреть файл

@ -0,0 +1,16 @@
import { ErrorObject } from 'ajv';
import * as jsonSrcMap from 'json-source-map';
export function ajvErrorLint(
data: any,
error: ErrorObject,
errorText?: string
) {
const srcMap = jsonSrcMap.stringify(data, undefined, 2);
const errorPtr = srcMap.pointers[error.dataPath];
const jsonStr = srcMap.json.split('\n');
for (let i = errorPtr.value.line; i <= errorPtr.valueEnd.line; i++) {
jsonStr[i] = ' * ' + jsonStr[i];
}
return errorText + '\n' + jsonStr.join('\n');
}

3
src/utils/safeString.ts Normal file
Просмотреть файл

@ -0,0 +1,3 @@
export function escapeSingleQuote(s: string): string {
return s.replace(/'/g, "\\'");
}

7
src/utils/timeit.ts Normal file
Просмотреть файл

@ -0,0 +1,7 @@
import * as convertHrtime from 'convert-hrtime';
export function timeit(fn: () => void): convertHrtime.HRTime {
const start = process.hrtime();
fn();
return convertHrtime(process.hrtime(process.hrtime()));
}

29
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,29 @@
{
"compilerOptions": {
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"lib": [
"esnext"
],
"target": "es2015",
"strict": true,
"resolveJsonModule": true,
"strictNullChecks": false,
"outDir": "./lib",
"preserveConstEnums": true,
"removeComments": true,
"inlineSourceMap": true,
"typeRoots": [
"./node_modules/@types",
"./src/typings"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*-spec.ts"
]
}

14
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,14 @@
{
"extends": [
"tslint:latest",
"tslint-config-prettier"
],
"rules":{
"no-console":false,
"interface-name":false,
"no-shadowed-variable": false,
"prefer-conditional-expression": false,
"ordered-imports": false
},
"defaultSeverity": "warn"
}