move community tools to separate repos (#3155)

This commit is contained in:
Rishichandra Wawhal 2019-10-26 09:32:32 +05:30 committed by Shahidh K Muhammed
parent 7664f1a528
commit 9d2ce81c70
219 changed files with 5 additions and 207856 deletions

View File

@ -1,2 +0,0 @@
./test/db.js
test-db

View File

@ -1,19 +0,0 @@
{
"extends": "oclif",
"rules": {
"max-params": "off",
"no-console": "off",
"max-depth": "off",
"one-var": "off",
"complexity": "off",
"unicorn/no-process-exit": "off",
"unicorn/filename-case": "off",
"no-process-exit": "off",
"no-throw-literal": "off",
"node/no-unsupported-features": "off",
"no-warning-comments": "off",
"semi": [1, "always"],
"camelcase": "off",
"guard-for-in": "off"
}
}

View File

@ -1,2 +0,0 @@
* text=auto
*.js text eol=lf

View File

@ -1,8 +0,0 @@
*-debug.log
*-error.log
/.nyc_output
/dist
/tmp
/yarn.lock
test-db
node_modules

View File

@ -1,29 +0,0 @@
# Contributing to firebase2graphql
## Issues
Please open an issue related to your work. Add the label `c/firebase2graphql`.
## Local developmet
1. Make changes and save
2. Run the executable in the `bin` directory to test your code. Treat the executable as the command. For example:
```
$ bin/run --help
```
## Testing
Please run the tests before making pull requests.
To run the tests locally, you will need an instance of [Hasura GraphQL Engine](https://github.com/hasura/graphql-engine) running. To run the tests, run the command:
```
$ TEST_HGE_URL=https://hge.herokuapp.com npm test
```
### Test data sets
Firebase RTD being a NoSQL database, there are very few data-sets available on the web. Since the tool takes heuristic based approach to convert the data, the more data we have the better results we can achieve. If you're aware of any such data-sets, please consider adding them to the test suite (test/data-sets).

View File

@ -1,508 +1 @@
# Firebase to GraphQL
A CLI tool to help you try realtime GraphQL on your firebase data. It takes data exported from firebase and imports it into Postgres via Hasura GraphQL engine.
[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
[![Version](https://img.shields.io/npm/v/firebase2graphql.svg)](https://npmjs.org/package/firebase2graphql)
![GIF](https://graphql-engine-cdn.hasura.io/assets/firebase2graphql/demo.gif)
## Contents
- [Getting started](#quick-start)
- [Installation](#installation)
- [Usage](#usage)
- [Command](#command)
- [Realtime](#realtime)
- [Usage comparison](#usage-comparison---firebase-sdk-vs-graphql)
- [Authentication](#authentication)
- [Next steps](#next-steps)
- [More information](#more-information)
- [Working](#working)
- [Normalization](#normalization)
- [Automatic](#automatic)
- [Manual](#manual)
- [Duplicates](#duplicates)
- [Overwrite](#overwrite)
- [Feedback](#feedback)
## Quick start
1. Quickly get the GraphQL Engine running by clicking this button:
[![Deploy to heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
Note the URL. It will be of the form: `https://<app-name>.herokuapp.com`
> Check [this page](https://docs.hasura.io/1.0/graphql/manual/deployment/index.html) for other deployment options
2. Go to `Firebase console > Database > Realtime Database` and click on `Export JSON` from the options on the upper right corner
![firebase-export](assets/firebase-export.png)
The exported JSON will be something like this:
```json
{
"Articles": {
"A1": {
"Title": "Title1",
"Body": "Body1",
"IsUnpublished": false,
"Author": {
"Name": "AName1",
"Age": 11
},
"Comments": {
"C1": {
"Body": "Comment1",
"Author": {
"Name": "AName2",
"Sex": "M"
},
"Date": "22-09-2018"
},
"C2": {
"Body": "Comment2",
"Author": {
"Name": "AName1",
"Sex": "F"
},
"Date": "21-09-2018"
}
}
},
"A2": {
"Title": "Title2",
"Body": "Body2",
"IsUnpublished": true,
"Author": {
"Name": "AName2",
"Age": 22
},
"Comments": {
"C3": {
"Body": "Comment1",
"Author": {
"Name": "AName1",
"Sex": "F"
},
"Date": "23-09-2018"
},
"C4": {
"Body": "Comment2",
"Author": {
"Name": "AName2",
"Sex": "M"
},
"Date": "24-09-2018"
}
}
}
},
"Authors": {
"AT1": {
"Name": "AName1",
"Age": 11,
"Sex": "F",
"Articles": {
"A1": {
"Title": "Title1"
}
}
},
"AT2": {
"Name": "AName2",
"Age": 22,
"Sex": "M",
"Articles": {
"A2": {
"Title": "Title2"
}
}
}
},
"Comments": {
"C1": {
"Body": "Comment1",
"Author": {
"Name": "AName2"
},
"Date": "22-09-2018"
},
"C2": {
"Body": "Comment2",
"Author": {
"Name": "AName1"
},
"Date": "21-09-2018"
},
"C3": {
"Body": "Comment1",
"Author": {
"Name": "AName1"
},
"Date": "23-09-2018"
},
"C4": {
"Body": "Comment2",
"Author": {
"Name": "AName2"
},
"Date": "24-09-2018"
}
}
}
```
4. Use the CLI to import the data:
```
npx firebase2graphql https://<app-name>.herokuapp.com --db=./path/to/db.json --normalize
```
5. That's it. You can now go to your GraphQL Engine URL `https://<app-name>.herokuapp.com` and make awesome GraphQL Queries like:
```graphql
query {
Authors (order_by: {Name:asc}){
Name
Age
Sex
Articles (
order_by: {Title:asc}
where: {
IsUnpublished: {
_eq: false
}
}
){
Title
Body
Comments (order_by: {Date:desc}){
Body
Authors {
Name
}
Date
}
}
}
}
```
Check out [next steps](#next-steps).
## Installation
```bash
npm install -g firebase2graphql
```
## Usage
**Without admin secret**
```
firebase2graphql https://hge.herokuapp.com -d ./path/to/db.json
```
**With admin secret**
```
firebase2graphql https://hge.herokuapp.com -s <admin-secret> -d ./path/to/db.json
```
## Command
```bash
firebase2graphql URL [flags]
```
### Args
* `URL`: The URL where Hasura GraphQL Engine is running
### Options
- `-d --db`: path to the JS file that exports your sample JSON database
- `-n --normalize`: normalize the schema while importing
- `-o --overwrite`: (experimental) overwrite tables if they already exist in database
- `-v --version`: show CLI version
- `-h, --help`: show CLI help
## Realtime
With Hasura, you can query the data in Postgres realtime in the form of GraphQL subscriptions. In this way, using this tool, you get exactly the same Firebase API over GraphQL while retaining the live queries feature.
Consider this query:
```graphql
query {
user {
id
name
email
}
}
```
You can convert it into a subscription by simply replacing `query` with `subscription`.
```
subscription {
user {
id
name
email
}
}
```
## Usage comparison - Firebase SDK vs GraphQL
A typical query to do a single read from the database using [Firebase SDK](https://firebase.google.com/docs/reference/), (javascript) would look something like:
```javascript
firebase.database().ref('/users/' + userId).once('value').then(function(snapshot) {
var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
// ...
});
```
Equivalent GraphQL Query would look like:
```graphql
query {
users(where: {uid: {_eq: userId}}) {
uid,
username
}
}
```
Similarly a write into database using Firebase SDK, would look something like:
```javascript
firebase.database().ref('users/' + userId).set({
username: name,
email: email,
profile_picture : imageUrl
});
```
And the equivalent GraphQL Mutation would look like:
```graphql
mutation {
insert_users(objects:[{
uid: userId
username: name,
email: email,
profile_picture: imageUrl
}])
}
```
## Authentication
Hasura can be integrated with most standard Authentication mechanisms including Firebase and Auth0. [Check out the authentication docs here](https://docs.hasura.io/1.0/graphql/manual/auth/index.html).
## Next steps
Once you have imported your data, it is recommended that you make it production ready.
1. [Normalize the data](#normalization)
2. Explore the GraphQL Engine Console to play with things such as
- [Relationships](https://docs.hasura.io/1.0/graphql/manual/schema/relationships/index.html)
- [Permissions](https://docs.hasura.io/1.0/graphql/manual/auth/index.html)
- Using SQL
- [Set up async business logic using event triggers](https://docs.hasura.io/1.0/graphql/manual/event-triggers/index.html)
- [Create new tables](https://docs.hasura.io/1.0/graphql/manual/schema/basics.html)
3. Set appropriate permissions. GraphQL Engine comes with [fine grained control layer](https://docs.hasura.io/1.0/graphql/manual/auth/index.html) that can be integrated with any standard Auth provider.
## More information
### Working
We flatten the JSON database into tables and create children tables when data nesting is detected.
In this way, you get almost the exact API over GraphQL that you had on Firebase.
If you use the flag `--normalize`, the CLI finds out if the children tables are duplicates of the original tables and tries to normalize the data by removing duplicates and creating respective relationships.
### Normalization
#### Automatic
The CLI provides a flag called `--normalize` if you want to normalize your denormalized database.
A lot of guess-work is done by the CLI while normalizing the database. Here are some thing you need to know:
1. Root level tables are never deleted. So if there are some relationships that you wish to create manually, you can do so.
2. Children tables are deleted if they are detected to be duplicates of some other root or child table.
3. In case of some children tables, when the data lacks a unique identifier, an extra unique field is added. In most cases, this field gets deleted while mergine a duplicate table with the original table.
#### Manual
In some cases, due to inconsistent field names or due to insufficient data, the CLI might not be able to normalize your database; but it makes sure that you do not lose any data.
In such cases, you can normalize the data yourself. Lets look at an example.
Consider this firebase database. This is the database of the official Firebase example app:
```json
{
"posts" : {
"-LMbLFOAW2q6GO1bD-5g" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "My first post content\nAnd body\nANd structure",
"starCount" : 0,
"title" : "My first post",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
},
"-LMbLIv6VKHYul7p_PZ-" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "AKsdjak\naklsdjaskldjklas\nasdklfjaklsdfjklsda\nasdklfjasklf",
"starCount" : 0,
"title" : "Whatta proaaa",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
}
},
"user-posts" : {
"4UPmbcaqZKT2NdAAqBahXj4tHYN2" : {
"-LMbLFOAW2q6GO1bD-5g" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "My first post content\nAnd body\nANd structure",
"starCount" : 0,
"title" : "My first post",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
},
"-LMbLIv6VKHYul7p_PZ-" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "AKsdjak\naklsdjaskldjklas\nasdklfjaklsdfjklsda\nasdklfjasklf",
"starCount" : 0,
"title" : "Whatta proaaa",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
}
}
},
"users" : {
"4UPmbcaqZKT2NdAAqBahXj4tHYN2" : {
"email" : "rishichandrawawhal@gmail.com",
"profile_picture" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"username" : "Eena"
}
}
}
```
In case of this JSON, the CLI will generate the following tables:
```sql
users (
_id text not null primary key,
email text,
profile_picture text,
username text
)
posts (
_id text not null primary key,
title text,
body text,
starCount int,
author text,
authorPic text,
uid text
)
user_posts (
_id text not null,
_id_2 text not null,
title text,
body text,
starCount int,
author text,
authorPic text,
uid text
)
```
As we can see, this is not the most efficient of schemas.
- `posts(uid)` can be a foreign key referencing `users(_id)`. `posts(author)` and `posts(authorPic)` can be deleted if `users` and `posts` are related via a foreign key.
- `user_posts` table is obsolete if `posts` and `users` tables are deleted.
To normalize it, here are the steps you must follow:
1. Create the following foreign key constraints:
```sql
ALTER TABLE "posts" ADD CONSTRAINT "posts_users__uid" FOREIGN KEY ("uid") REFERENCES "users"("_id");
ALTER TABLE "posts" DROP COLUMN "author", DROP COLUMN "authorPic";
DROP TABLE "user_posts";
```
To create them, go to the Hasura Console and run the SQL in the `Data > SQL` section.
2. Create the relationships. In the console, go the desired table and add the relationship suggested based on the Foreign key. For example for the `posts` table in the above schema, the suggested relationship would be suggested like:
![suggested](assets/suggested-rel.png)
Click on `Add`. You can name it whatever you like. Lets call it `author` in this case.
![added](assets/added-rel.png)
Similarly, add the suggested relationships for other tables as well.
3. Once you have created relationships, you can start making fancy GraphQL queries like:
```graphql
query {
users (order_by: {username:asc}){
username
profile_picture
email
posts (where: { starCount: { _gte: 100}}){
title
body
starCount
}
}
}
```
### Duplicates
By default, the CLI gives you almost the exact API that you originally had in Firebase (of course, over GraphQL). But in that case, some duplicate tables might be created and you might not be able to leverage the complete power of GraphQL and Postgres.
In such cases, you have three choices:
1. Use the API as such if you prefer the exact API.
2. Go to the UI Console and delete the duplicates and normalize the database as you feel fit.
3. Use the `--normalize` flag and rerun the migration. In this case, the CLI will detect duplicates and make appropriate relationships between root nodes. (This feature is experimental and needs more test cases to attain stability. Contributions are welcome)
### Overwrite
If your database already contains tables with the same name as the root fields of your JSON database, the command will fail. If you want to overwrite the database anyway, you should provide an additional flag `--overwrite`.
## Feedback
This project is still in alpha and we are actively looking for feedback about how the tool can be improved. If you are facing an issue, feel free to [open one here](https://github.com/hasura/graphql-engine/issues/new). Any positive or negative feedback would be appreciated.
---
Maintained with :heart: by <a href="https://hasura.io">Hasura</a>
## This project has been moved to [hasura/firebase2graphql](https://github.com/hasura/firebase2graphql)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,4 +0,0 @@
#!/usr/bin/env node
require('../src/command').run()
.catch(require('@oclif/errors/handle'))

View File

@ -1,3 +0,0 @@
@echo off
node "%~dp0\run" %*

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
{
"name": "firebase2graphql",
"description": "A CLI tool to get GraphQL over Firebase data dump",
"version": "0.0.2",
"author": "Hasura",
"bin": {
"firebase2graphql": "./bin/run",
"f2g": "./bin/run"
},
"bugs": "https://github.com/hasura/graphql-engine/issues?q=is%3Aissue+is%3Aopen+label%3Ac%2Ffirebase2graphql",
"dependencies": {
"@oclif/command": "^1.4.35",
"@oclif/config": "^1.6.33",
"@oclif/errors": "^1.1.2",
"@oclif/plugin-help": "^2.0.5",
"cli-ux": "^4.7.3",
"colors": "^1.3.2",
"graphqurl": "^0.3.2",
"moment": "^2.22.2",
"node-fetch": "^2.2.0",
"uuid": "^3.3.2",
"uuid-validate": "0.0.3"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint-config-oclif": "^1.5.1"
},
"engines": {
"node": ">=8.0.0"
},
"files": [
"/bin",
"/src"
],
"homepage": "https://github.com/wawhal/graphql-engine/tree/master/community/tools/firebase2graphql",
"keywords": [
"oclif",
"cli",
"graphql",
"grapql-engine",
"json",
"firebase"
],
"license": "MIT",
"main": "src/command.js",
"oclif": {
"bin": "firebase2graphql"
},
"repository": "hasura/graphql-engine",
"scripts": {
"eslint": "eslint .",
"eslintfix": "eslint . --fix",
"posttest": "npm run eslint",
"test": "cd test && ./test.sh"
},
"pre-commit": [
"eslintfix"
]
}

View File

@ -1,133 +0,0 @@
const {Command, flags} = require('@oclif/command');
const fetch = require('node-fetch');
const {CLIError} = require('@oclif/errors');
const throwError = require('./error');
const {spinnerStart, spinnerStop} = require('./log');
const resolve = require('path').resolve;
const importData = require('./import/import');
class Firebase2GraphQL extends Command {
async run() {
const {args, flags} = this.parse(Firebase2GraphQL);
const {url} = args;
if (!url) {
throw new CLIError('endpoint is required: \'firebase2graphql <url>\'');
}
const {db, overwrite, normalize} = flags;
const key = flags['access-key'];
const secret = flags['admin-secret'];
if (secret !== undefined && key !== undefined) {
throw new CLIError('cannot use both flags "access-key" and "admin-secret"', 'use "admin-secret" for versions greater than v1.0.0-alpha37 and "access-key" otherwise');
}
if (!url) {
throw new CLIError('endpoint is required: \'firebase2graphql <url> -d ./db.js\'');
}
const safeUrl = this.getSafeUrl(url);
if (!db) {
throw new CLIError('path to firebase JSON database is required: \'firebase2graphql <url> -d ./db.js\'');
}
const dbJson = this.getDbJson(db);
const headers = {
[secret ? 'x-hasura-admin-secret' : 'x-hasura-access-key']: secret || key,
};
const urlVerification = await this.verifyUrl(safeUrl, headers);
if (urlVerification.error) {
throwError(`Message: ${urlVerification.message}`);
} else {
spinnerStop('Done!');
await importData(dbJson, safeUrl, headers, overwrite, 1, normalize);
}
}
getDbJson(db) {
return require(resolve(db));
}
getSafeUrl(url) {
const urlLength = url.length;
return url[urlLength - 1] === '/' ? url.slice(0, -1) : url;
}
async verifyUrl(url, headers) {
try {
spinnerStart('Verifying URL');
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'run_sql',
args: {
sql: 'select * from hdb_catalog.hdb_version;',
},
}),
}
);
return resp.status === 200 ? {error: false} : {error: true, message: 'invalid access-key or admin-secret'};
} catch (e) {
return {error: true, message: 'invalid URL'};
}
}
}
Firebase2GraphQL.description = `firebase2graphql: Import JSON data to Hasura GraphQL Engine
# Examples:
# Import data from a Firebase JSON database to Hasura GraphQL Engine without admin secret
firebase2graphql https://hge.herokuapp.com --db=./path/to/db.json
# Import data from a Firebase JSON database to Hasura GraphQL Engine with admin secret
firebase2graphql https://hge.herokuapp.com --db=./path/to/db.json -s <admin-secret>
# Import data from a Firebase JSON database to Hasura GraphQL Engine while normalizing it
firebase2graphql https://hge.herokuapp.com --db=./path/to/db.json -n
`;
Firebase2GraphQL.usage = 'URL [-s SECRET]';
Firebase2GraphQL.flags = {
// add --version flag to show CLI version
version: flags.version(),
// add --help flag to show CLI version
help: flags.help({char: 'h'}),
// Admin secret to Hasura GraphQL Engine
'admin-secret': flags.string({
char: 's',
description: 'Admin secret to Hasura GraphQL Engine (X-Hasura-Admin-Secret). Use the flag --access-key if GraphQL Engine version is older than v1.0.0-alpha38',
}),
// Access key to Hasura GraphQL Engine
'access-key': flags.string({
char: 'k',
description: 'Access key to Hasura GraphQL Engine (X-Hasura-Access-Key). Use the flag --admin-secret if GraphQL Engine version is greater than v1.0.0-alpha37',
}),
db: flags.string({
char: 'd',
description: 'Path to the .js files that exports a firebase JSON database',
}),
normalize: flags.boolean({
char: 'n',
description: 'Normalize the data as it is imported to GraphQL Engine',
}),
overwrite: flags.boolean({
char: 'o',
description: 'Overwrite tables if they exist',
}),
};
Firebase2GraphQL.args = [
{
name: 'url',
description: 'URL where Hasura GraphQL Engine is running',
},
];
module.exports = Firebase2GraphQL;

View File

@ -1,13 +0,0 @@
const {cli} = require('cli-ux');
const {log} = require('./log');
const colors = require('colors/safe');
module.exports = (message, preExitHook) => {
cli.action.stop(colors.red('Error!'));
if (preExitHook) {
preExitHook(message);
}
console.log('');
log(message, 'red');
process.exit(1);
};

View File

@ -1,173 +0,0 @@
const uuid = require('uuid/v4');
const {
getParentPrimaryKeyMap,
getLastPrimaryKey,
getPrimaryKeyName,
isRandomList,
isList,
isObjectList,
makeFirebaseListFromObj,
makeFirebaseListFromArr,
} = require('./utils');
const throwError = require('../error');
const handleTableCandidate = (obj, tableName, tableDetectedCallback, isRootLevel) => {
const rowArray = [];
const flattenObject = (object, row, parent) => {
if (isObjectList(object)) {
const firebaseList = makeFirebaseListFromObj(object);
const dummyRow = {...row};
for (var objListKey in firebaseList) {
row[getPrimaryKeyName(dummyRow)] = objListKey;
const newRow = {...flattenObject(firebaseList[objListKey], row)};
if (newRow && Object.keys(newRow).length > 0) {
rowArray.push(newRow);
}
}
} else if (isList(object)) {
const firebaseObject = makeFirebaseListFromArr(object);
for (var listKey in firebaseObject) {
const dummyRow = {...row};
dummyRow[getPrimaryKeyName(dummyRow, null, 'self')] = uuid();
dummyRow._value = listKey;
if (Object.keys(dummyRow).length > 0) {
rowArray.push(dummyRow);
}
}
} else {
for (var objectKey in object) {
const value = object[objectKey];
if (value === null || !['Object', 'Array'].includes(value.constructor.name)) {
row[objectKey] = value;
} else if (['Object', 'Array'].includes(value.constructor.name)) {
const pkeyMap = getParentPrimaryKeyMap(row);
if (isList(value)) {
const firebaseList = makeFirebaseListFromArr(value);
tableDetectedCallback(
null,
{
tableName: parent || tableName,
name: objectKey,
pkeys: pkeyMap,
data: Object.keys(firebaseList).map(item => ({_value: item})),
}
);
} else if (isObjectList(value)) {
const firebaseList = makeFirebaseListFromObj(value);
tableDetectedCallback(
null,
{
tableName: parent || tableName,
name: objectKey,
pkeys: pkeyMap,
data: handleTableCandidate(firebaseList, `${parent || tableName}_${objectKey}`, tableDetectedCallback, false),
}
);
} else if (Object.keys(value).length !== 0) {
const newUUID = uuid();
row[`${tableName}_${objectKey}__idself`] = newUUID;
tableDetectedCallback(
{
tableName,
name: objectKey,
data: flattenObject(value, {_idself: newUUID}, `${tableName}_${objectKey}`),
}
);
}
}
}
return row;
}
};
if (!isObjectList(obj)) {
if (isList(obj)) {
const firebaseObject = makeFirebaseListFromArr(obj);
for (var listKey in firebaseObject) {
rowArray.push({
_value: listKey,
_id: uuid(),
});
}
return rowArray;
}
if (isRandomList(obj)) {
for (var objKey in obj) {
rowArray.push({
_key: objKey,
_value: obj[objKey],
_id: uuid(),
});
}
return rowArray;
}
throwError('Message: invalid JSON provided for node ' + tableName);
}
for (var id in obj) {
const randomUUID = uuid();
const initialRow = {_id: id};
if (!isRootLevel) {
initialRow._idself = randomUUID;
}
const flatRow = flattenObject(obj[id], initialRow);
if (flatRow && Object.keys(flatRow).length > 0) {
rowArray.push(flatRow);
}
}
return rowArray;
};
const handleFirebaseJson = db => {
const tablesMap = {};
const generateNewTable = (objectRelMetadata, arrayRelMetadata) => {
if (arrayRelMetadata) {
const newTableName = `${arrayRelMetadata.tableName}_${arrayRelMetadata.name}`;
const parentTableName = arrayRelMetadata.tableName;
const pkeys = arrayRelMetadata.pkeys;
if (!tablesMap[newTableName]) {
tablesMap[newTableName] = [];
}
tablesMap[newTableName] = [
...tablesMap[newTableName],
...arrayRelMetadata.data.map(item => {
const newItem = {
...item,
};
for (var pkey in pkeys) {
newItem[`${parentTableName}_${pkey}`] = pkeys[pkey];
}
if (newItem._idself === undefined) {
newItem[getLastPrimaryKey(newItem, 0, 'self')] = uuid();
}
return newItem;
}),
];
} else {
const newTableName = objectRelMetadata.tableName + '_' + objectRelMetadata.name;
const newItem = {
...objectRelMetadata.data,
};
if (!tablesMap[newTableName]) {
tablesMap[newTableName] = [];
}
tablesMap[newTableName].push(newItem);
}
};
const topLevelTables = [];
for (var tableName in db) {
topLevelTables.push({
_id: uuid(),
__tableName: tableName.replace(/[^a-zA-Z0-9]/g, '_'),
});
tablesMap[tableName] = handleTableCandidate(
db[tableName],
tableName,
generateNewTable,
true
);
}
tablesMap.__rootTables = topLevelTables;
return tablesMap;
};
module.exports = handleFirebaseJson;

View File

@ -1,148 +0,0 @@
const getParentPrimaryKeyMap = obj => {
const pkeyMap = {};
for (var pkey in obj) {
if (pkey.indexOf('_id') === 0) {
pkeyMap[pkey] = obj[pkey];
}
}
return pkeyMap;
};
const getLastPrimaryKey = (obj, index = 0, selfGenerated = '') => {
const id = index === 0 ? `_id${selfGenerated}` : `_id${selfGenerated}_${index}`;
const nextIndex = index === 0 ? 2 : index + 1;
if (!obj[`_id_${nextIndex}`]) {
return id;
}
getLastPrimaryKey(obj, nextIndex, selfGenerated);
};
const getPrimaryKeyName = (obj, index = 0, selfGenerated = '') => {
const id = index === 0 ? `_id${selfGenerated}` : `_id${selfGenerated}_${index}`;
const nextIndex = index === 0 ? 2 : index + 1;
if (obj[id] === undefined) {
return id;
}
return getPrimaryKeyName(obj, nextIndex, selfGenerated);
};
const isRandomList = obj => {
if (!obj) {
return false;
}
for (var objKey in obj) {
if (obj[objKey] !== null && typeof obj[objKey] === 'object') {
return false;
}
}
return true;
};
const isList = obj => {
if (Object.keys(obj).length === 0) {
return false;
}
if (obj.constructor.name === 'Array') {
let arrayElementDataType = null;
for (let _i = obj.length - 1; _i >= 0; _i--) {
if (arrayElementDataType === null) {
arrayElementDataType = typeof obj[_i];
} else if (arrayElementDataType !== typeof obj[_i]) {
return false;
}
}
return true;
}
for (var objkey in obj) {
if (obj[objkey] === null) {
return false;
}
if (obj[objkey].constructor.name !== 'Boolean' || !obj[objkey]) {
return false;
}
}
return true;
};
const makeFirebaseListFromObj = obj => {
if (obj.constructor.name === 'Array') {
const firebaseList = {};
for (var i = obj.length - 1; i >= 0; i--) {
const element = obj[i];
firebaseList[i.toString()] = element;
}
return firebaseList;
}
return obj;
};
const makeFirebaseListFromArr = obj => {
if (obj.constructor.name === 'Array') {
const firebaseList = {};
for (var i = obj.length - 1; i >= 0; i--) {
const element = obj[i];
firebaseList[element] = true;
}
return firebaseList;
}
return obj;
};
const isObjectList = obj => {
if (obj === null || obj === undefined) {
return false;
}
const listChildStructure = {};
const checkElementConsistency = element => {
if (element === null) {
return false;
}
if (typeof element !== 'object') {
return false;
}
if (Object.keys(obj).length === 0) {
return false;
}
for (var childKey in element) {
if (!listChildStructure[childKey]) {
if (element[childKey] !== null && element[childKey] !== undefined) {
listChildStructure[childKey] = typeof element[childKey];
}
} else if (element[childKey] !== null && element[childKey] !== undefined) {
if (typeof element[childKey] !== listChildStructure[childKey]) {
return false;
}
}
}
return true;
};
if (obj.constructor.name === 'Array') {
for (let _i = obj.length - 1; _i >= 0; _i--) {
let element = obj[_i];
let consistent = checkElementConsistency(element);
if (!consistent) {
return false;
}
}
return true;
}
for (var key in obj) {
const element = obj[key];
let consistent = checkElementConsistency(element);
if (!consistent) {
return false;
}
}
return true;
};
module.exports = {
getParentPrimaryKeyMap,
getLastPrimaryKey,
getPrimaryKeyName,
isRandomList,
isList,
isObjectList,
makeFirebaseListFromObj,
makeFirebaseListFromArr,
};

View File

@ -1,58 +0,0 @@
const fetch = require('node-fetch');
const throwError = require('../error');
const {spinnerStart, spinnerStop, spinnerStopColorless} = require('../log');
const createTables = async (tables, url, headers, overwrite, runSql, sql) => {
if (overwrite) {
spinnerStopColorless('Skipped!');
spinnerStart('Creating tables');
await runSql(sql, url, headers);
} else {
try {
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'select',
args: {
table: {
name: 'hdb_table',
schema: 'hdb_catalog',
},
columns: ['*.*'],
where: {
table_schema: 'public',
},
},
}),
}
);
const dbTables = await resp.json();
if (resp.status === 401) {
throw (dbTables);
} else {
let found = false;
tables.forEach(table => {
if (dbTables.find(dbTable => dbTable.table_name === table.name)) {
found = true;
throwError('Message: Your JSON database contains tables that already exist in Postgres. Please use the flag "--overwrite" to overwrite them.');
}
});
if (!found) {
spinnerStop('Done!');
spinnerStart('Creating tables');
await runSql(sql, url, headers);
}
}
} catch (e) {
console.log(e);
throwError(e);
}
}
};
module.exports = {
createTables,
};

View File

@ -1,90 +0,0 @@
const throwError = require('../error');
const validateUUID = require('uuid-validate');
const getDataType = (data, column) => {
if (typeof data === 'number') {
return (data === parseInt(data, 10)) ? 'bigint' : 'numeric';
}
if (typeof data === 'string' || data === null) {
if (data && validateUUID(data)) {
return 'uuid';
}
return 'text';
}
if (typeof data === 'boolean') {
return 'boolean';
}
if (data.constructor.name === 'Date') {
return 'timestamptz';
}
if (data.constructor.name === 'Object' || data.constructor.name === 'Array') {
return 'json';
}
throwError(`Message: invalid data type given for column ${column}: ${data.constructor.name}`);
};
const isForeign = (name, db) => {
const idPos = name.indexOf('__id');
if (idPos <= 0) {
return false;
}
if (Object.keys(db).find(tableName => tableName === name.substring(0, idPos))) {
return true;
}
return false;
};
const getColumnData = (dataArray, db) => {
if (dataArray.length === 0) {
return [];
}
const refRow = {
numOfCols: 0,
index: 0,
};
dataArray.forEach((row, i) => {
if (Object.keys(row).length > refRow.numOfCols) {
refRow.numOfCols = Object.keys(row).length;
refRow.index = i;
}
});
const refColumns = dataArray[refRow.index];
const columnData = [];
Object.keys(refColumns).forEach(column => {
const columnMetadata = {};
if (!column) {
throwError("Message: column names can't be empty strings");
}
columnMetadata.name = column;
const sampleData = refColumns[column];
columnMetadata.type = getDataType(sampleData, column, db);
columnMetadata.isForeign = isForeign(column, db);
columnData.push(columnMetadata);
});
return columnData;
};
const generate = db => {
const metaData = [];
Object.keys(db).forEach(rootField => {
if (db[rootField].length === 0) {
return;
}
const tableMetadata = {};
tableMetadata.name = rootField;
tableMetadata.columns = getColumnData(db[rootField], db);
tableMetadata.dependencies = [];
tableMetadata.columns.forEach(column => {
if (column.isForeign) {
tableMetadata.dependencies.push(
column.name.substring(0, column.name.indexOf('__id'))
);
}
});
metaData.push(tableMetadata);
});
return metaData;
};
module.exports = generate;

View File

@ -1,50 +0,0 @@
const {spinnerStart, spinnerStop, log} = require('../log');
const generate = require('./generateTables');
const {refineJson} = require('./utils');
const {generateSql, runSql, dropUtilityTables} = require('./sql');
const {trackTables} = require('./track');
const {getInsertOrder, insertData} = require('./insert');
const {createRelationships} = require('./relationships');
const {createTables} = require('./check');
const normalize = require('./normalize');
const generateGenericJson = require('../firebase/generateGenericJson');
const makeSuggestions = require('./suggest');
const importData = async (jsonDb, url, headers, overwrite, level = 1, shouldNormalize) => {
spinnerStart('Processing Firebase JSON');
const db = level === 1 ? refineJson(generateGenericJson(jsonDb)) : jsonDb;
const tables = generate(db);
const sql = generateSql(tables);
spinnerStop('Done!');
spinnerStart('Checking database');
createTables(tables, url, headers, overwrite, runSql, sql).then(() => {
spinnerStop('Done!');
spinnerStart('Tracking tables');
trackTables(tables, url, headers).then(() => {
spinnerStop('Done!');
spinnerStart('Creating relationships');
createRelationships(tables, url, headers).then(() => {
spinnerStop('Done!');
const insertOrder = getInsertOrder(tables);
insertData(insertOrder, db, tables, url, headers, async success => {
if (level <= 10 && shouldNormalize) {
normalize(tables, db, url, headers, level, importData);
} else if (success) {
spinnerStart('Dropping utility tables');
const resp = await dropUtilityTables(url, headers);
if (resp) {
spinnerStop('Done!');
}
log('');
log(`Success! Try out the GraphQL API at ${url}/console`, 'green');
if (!shouldNormalize) {
makeSuggestions();
}
}
});
});
});
});
};
module.exports = importData;

View File

@ -1,155 +0,0 @@
const {query} = require('graphqurl');
const fetch = require('node-fetch');
const moment = require('moment');
const throwError = require('../error');
const {log, spinnerStart, spinnerStop, spinnerStopColorless} = require('../log');
const colors = require('colors/safe');
const getInsertOrder = tables => {
let order = [];
const tablesHash = {};
tables.forEach(table => {
tablesHash[table.name] = table;
});
const pushedHash = {};
const setOrder = table => {
if (table.dependencies.length === 0) {
order.push(table.name);
pushedHash[table.name] = true;
} else {
table.dependencies.forEach(parentTable => {
if (!pushedHash[parentTable] && parentTable !== table.name) {
setOrder(tablesHash[parentTable]);
}
});
order.push(table.name);
pushedHash[table.name] = true;
}
};
tables.forEach(table => {
if (!pushedHash[table.name]) {
setOrder(table);
}
});
return order;
};
const transformData = (data, tables) => {
const newData = {};
tables.forEach(table => {
const tableData = data[table.name];
newData[table.name] = [];
tableData.forEach(row => {
const newRow = {...row};
table.columns.forEach(column => {
if (column.type === 'timestamptz' && row[column.name]) {
newRow[column.name] = moment(row[column.name]).format();
}
if (column.type === 'json' && row[column.name]) {
newRow[column.name] = JSON.stringify(row[column.name]);
}
});
newData[table.name].push(newRow);
});
});
return newData;
};
const deleteDataTill = async (tableName, insertOrder, url, headers) => {
spinnerStopColorless(colors.red('Error'));
spinnerStart('Restoring database to a safe state');
const truncate = async order => {
const resp = await fetch(
url,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'run_sql',
args: {
sql: `truncate table public."${insertOrder[order]}" cascade;`,
cascade: true,
},
}),
}
);
if (insertOrder[order] === tableName) {
spinnerStop('Done');
} else {
await truncate(order + 1, Boolean(resp));
}
};
if (insertOrder.length === 0) {
return;
}
return truncate(0);
};
const insertData = async (insertOrder, sampleData, tables, url, headers, callback) => {
const transformedData = transformData(sampleData, tables);
let numOfTables = insertOrder.length;
const insertToTable = j => {
if (j >= numOfTables) {
callback(true);
return true;
}
const tableName = insertOrder[j];
const numOfRows = transformedData[tableName].length;
let insertedRows = 0;
const insertHundredRows = i => {
let mutationString = '';
let objectString = '';
const variables = {};
const numOfelementsToInsert = Math.min(numOfRows - insertedRows, 100);
mutationString += `insert_${tableName} ( objects: $objects ) { affected_rows } \n`;
objectString += `$objects: [${tableName}_insert_input!]!,\n`;
variables.objects = [...transformedData[tableName].slice(i, numOfelementsToInsert + i)];
const mutation = `mutation ( ${objectString} ) { ${mutationString} }`;
spinnerStart(`Inserting ${i} to ${i + numOfelementsToInsert} rows of ${numOfRows} in table ${tableName}`);
return query(
{
query: mutation,
endpoint: `${url}/v1/graphql`,
variables,
headers,
}
).then(response => {
if (response.data) {
spinnerStop('Done!');
insertedRows += numOfelementsToInsert;
if (insertedRows >= numOfRows) {
return insertToTable(j + 1);
}
return insertHundredRows(i + 100);
}
deleteDataTill(tableName, insertOrder, url, headers).then(() => {
throwError(
JSON.stringify(response, null, 2),
() => {
log('Message: Schema has been imported. But the data could not be inserted due to the following error.', 'yellow');
callback(false);
}
);
});
}).catch(e => {
deleteDataTill(tableName, insertOrder, url, headers).then(() => {
throwError(
JSON.stringify(e, null, 2),
() => {
log('Message: Schema has been imported. But the data could not be imported due to the following error.', 'yellow');
callback(false);
}
);
});
});
};
insertHundredRows(0);
};
return insertToTable(0);
};
module.exports = {
getInsertOrder,
insertData,
};

View File

@ -1,341 +0,0 @@
const fetch = require('node-fetch');
const throwError = require('../error');
const {log, spinnerStart, spinnerStop} = require('../log');
const shouldIgnoreTable = table => {
return (table.columns.find(c => c.name === '_value'));
};
const getDupeCandidates = tables => {
const dupes = [];
for (var i = tables.length - 1; i >= 0; i--) {
const table = tables[i];
if (shouldIgnoreTable(table)) {
continue;
}
for (var j = tables.length - 1; j >= 0; j--) {
if (table.name !== tables[j].name) {
const dupeSuspect = tables[j];
if (shouldIgnoreTable(dupeSuspect)) {
continue;
}
let isDupe = true;
for (var k = dupeSuspect.columns.length - 1; k >= 0; k--) {
const columnName = dupeSuspect.columns[k].name;
if (columnName.indexOf('_id') < 0) {
if (!table.columns.find(col => col.name === columnName)) {
isDupe = false;
}
}
}
if (isDupe) {
dupes.push({
table1: table.name,
table2: dupeSuspect.name,
columnList: dupeSuspect.columns.filter(dupeCol => dupeCol.name.indexOf('_id') < 0).map(dupeCol => dupeCol.name),
});
}
}
}
}
return dupes;
};
const categorizeDupeCandidates = async (dupes, url, headers) => {
const bulkQueryArgs = [];
dupes.forEach(dupe => {
const {table1, table2, columnList} = dupe;
const table1Sql = `select count(public."${table1}".*) from public."${table1}";`;
const overlapSql = `select count(public."${table2}".*) from public."${table1}", public."${table2}"`;
let whereSql = '';
columnList.forEach((column, i) => {
whereSql += ` public."${table1}"."${column}" = public."${table2}"."${column}"`;
whereSql += i === columnList.length - 1 ? '' : ' and ';
});
const sql = `${overlapSql} where ${whereSql};`;
bulkQueryArgs.push({
type: 'run_sql',
args: {
sql: table1Sql,
},
});
bulkQueryArgs.push({
type: 'run_sql',
args: {
sql,
},
});
});
const response = await fetch(
`${url}/v1/query`,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'bulk',
args: bulkQueryArgs,
}),
}
);
const respObj = await response.json();
if (response.status !== 200) {
throwError('Message: Could not normalize your data');
}
const newDupes = {
confirmed: [],
unconfirmed: [],
};
dupes.forEach((dupe, i) => {
const overlapResult = respObj[(i * 2) + 1].result[1][0];
const table1Count = respObj[i].result[1][0];
if (!overlapResult || !table1Count) {
throwError('Message: Could not normalize your data');
}
if (table1Count > 0 && overlapResult > 0) {
if (table1Count === overlapResult) {
newDupes.confirmed.push(dupe);
} else if (overlapResult <= Number(table1Count) / 4) {
newDupes.unconfirmed.push(dupe);
} else {
newDupes.confirmed.push(dupe);
}
}
});
return newDupes;
};
const patchDupeDependentTables = (table, dupe, tables, data, pkeyMap) => {
const patchedData = {};
tables.forEach(otherTable => {
if (otherTable.name !== table && otherTable.name !== dupe) {
if (otherTable.columns.find(column => {
return column.name.indexOf(`${dupe}__id`) === 0 ||
column.name.indexOf(`${table}__idself`) === 0;
})) {
const newData = data[otherTable.name].map(row => {
const newRow = {
...row,
};
for (var c in row) {
if (c.indexOf(`${table}__idself`) === 0) {
delete newRow[c];
continue;
}
if (c.indexOf(`${dupe}__idself`) === 0) {
newRow[`${table}__id`] = pkeyMap[row[c]];
delete newRow[c];
continue;
}
if (c.indexOf(`${dupe}__id`) === 0) {
delete newRow[c];
continue;
}
}
return newRow;
});
patchedData[otherTable.name] = newData;
}
}
});
return patchedData;
};
const makePkeyMap = (table, dupe, columnList, data) => {
const map = {};
data[dupe].forEach(dupeRow => {
data[table].forEach(tableRow => {
let isSameRow = true;
columnList.forEach(column => {
if (dupeRow[column] !== tableRow[column]) {
isSameRow = false;
}
});
if (isSameRow) {
map[dupeRow._idself] = tableRow._id;
}
});
});
return map;
};
const getTablePriority = (table, dupe, topLevelTables) => {
let isDupeTopLevel = false;
let isTableTopLevel = false;
for (var i = topLevelTables.length - 1; i >= 0; i--) {
let row = topLevelTables[i];
if (row.__tableName === dupe) {
isDupeTopLevel = true;
}
if (row.__tableName === table) {
isTableTopLevel = true;
}
}
if (isDupeTopLevel && !isTableTopLevel) {
return {
table1: dupe,
table2: table,
};
}
if (!isDupeTopLevel && isTableTopLevel) {
return {
table1: table,
table2: dupe,
};
}
if (!isDupeTopLevel && !isTableTopLevel) {
return {
table1: table,
table2: dupe,
};
}
return {
table1: null,
table2: null,
};
};
const handleConfirmedDupes = (confirmedDupes, tables, data) => {
/*
1. Go through the dupes
2. Check which one of table1, table2 has _id (table) and _idself(dupe)
3. Spread all fields of dupe in table
4. Change column names and dependencies of all tables that have dupe as a dependency
*/
let newData = {
...data,
};
let filteredTables = [...tables];
const handle = (dupes, index) => {
if (dupes.length === 0 || index > dupes.length - 1) {
return;
}
const tableData = [];
const {table1, table2} = getTablePriority(dupes[index].table1, dupes[index].table2, data.__rootTables);
const columnList = dupes[index].columnList;
if (!table1) {
handle(dupes, index + 1);
return;
}
const table = filteredTables.find(t => t.name === table1);
const dupe = filteredTables.find(t => t.name === table2);
newData[table.name].forEach(r => {
const tableRow = {};
for (var c in r) {
if (c.indexOf('_idself') !== 0) {
tableRow[c] = r[c];
}
}
const dLength = data[dupe.name].length;
let found = false;
for (let j = 0; j < dLength; j++) {
const dupeRow = newData[dupe.name][j];
if (columnList.every(colName => dupeRow[colName] === tableRow[colName])) {
found = true;
const item = {};
for (var key in dupeRow) {
if (key.indexOf('_idself') !== 0) {
item[key.replace(dupe.name + '_', table.name + '_')] = dupeRow[key];
}
}
tableData.push({
...item,
...tableRow,
});
break;
}
}
if (!found) {
tableData.push(tableRow);
}
});
newData[table.name] = tableData;
filteredTables = filteredTables.filter(ft => ft.name !== dupe.name);
newData = {
...newData,
...patchDupeDependentTables(table.name, dupe.name, filteredTables, newData, makePkeyMap(table1, table2, columnList, newData)),
};
delete newData[dupe.name];
const filteredDupes = [];
for (var i = dupes.length - 1; i >= 0; i--) {
const d = dupes[i];
if ((d.table1 !== table1 && d.table2 !== table2) && (d.table2 !== table1 && d.table1 !== table2)) {
if (d.table1 === table2) {
filteredDupes.push({
table1,
table2: d.table2,
});
}
if (d.table2 === table2) {
filteredDupes.push({
table1,
table2: d.table1,
});
}
}
}
handle(
filteredDupes,
0
);
};
handle(confirmedDupes, 0);
return newData;
};
const dropTables = async (tableList, url, headers) => {
spinnerStop('Done!');
spinnerStart('Deleting unnecessary tables');
if (tableList.length === 0) {
spinnerStop('Done');
return true;
}
let sql = '';
tableList.forEach(t => {
sql += `drop table if exists public."${t}" cascade;`;
});
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'run_sql',
args: {
sql,
cascade: true,
},
}),
}
);
if (resp.status !== 200) {
log('Message: Could not delete unnecessary tables. Your database might have some unnecessary tables.', 'yellow');
}
spinnerStop('Done');
return true;
};
const normalize = async (tables, data, url, headers, level, importData) => {
spinnerStart('Normalizing your data');
const dupeCandidates = getDupeCandidates(tables);
const maybeDupes = await categorizeDupeCandidates(dupeCandidates, url, headers);
let newData;
if (level === 10) {
newData = handleConfirmedDupes(
[...maybeDupes.confirmed, ...maybeDupes.unconfirmed],
tables,
data
);
} else {
newData = handleConfirmedDupes(maybeDupes.confirmed, tables, data);
}
const tablesToDrop = tables.filter(t => newData[t.name] === undefined).map(tbl => tbl.name);
const dropResp = await dropTables(tablesToDrop, url, headers);
if (maybeDupes.unconfirmed.length === 0 && maybeDupes.confirmed.length === 0 && dropResp) {
await importData(newData, url, headers, true, 11, true);
} else {
await importData(newData, url, headers, true, level + 1, true);
}
};
module.exports = normalize;

View File

@ -1,124 +0,0 @@
const fetch = require('node-fetch');
const throwError = require('../error');
const getArrayRelType = (table, child) => {
const columnMapping = {};
let numOfMappings = 0;
table.columns.forEach(col => {
if (col.name.indexOf('_id') === 0) {
numOfMappings++;
columnMapping[col.name] = `${table.name}_${col.name}`;
}
});
if (numOfMappings === 1) {
return {
foreign_key_constraint_on: {
table: child.name,
column: columnMapping[Object.keys(columnMapping)[0]],
},
};
}
return {
manual_configuration: {
remote_table: child.name,
column_mapping: columnMapping,
},
};
};
const getObjRelType = (table, dep) => {
const columnMapping = {};
let numOfMappings = 0;
table.columns.forEach(col => {
if (col.name.indexOf(`${dep.name}__id`) === 0) {
numOfMappings++;
columnMapping[col.name] = col.name.substring(col.name.indexOf('_id'), col.name.length);
}
});
if (numOfMappings === 1) {
return {
foreign_key_constraint_on: Object.keys(columnMapping)[0],
};
}
return {
manual_configuration: {
remote_table: dep.name,
column_mapping: columnMapping,
},
};
};
const generateRelationships = tables => {
const objectRelationships = [];
const arrayRelationships = [];
tables.forEach(table => {
if (table.dependencies.length > 0) {
table.dependencies.forEach(dep => {
const objUsing = getObjRelType(table, tables.find(t => t.name === dep));
const arrUsing = getArrayRelType(tables.find(t => t.name === dep), table);
const newObjRel = {
type: 'create_object_relationship',
args: {
table: table.name,
name: dep,
using: objUsing,
},
};
if (!objectRelationships.find(or => {
return (
or.args.table === newObjRel.args.table &&
or.args.name === newObjRel.args.name
);
})) {
objectRelationships.push(newObjRel);
}
const newArrRel = {
type: 'create_array_relationship',
args: {
table: dep,
name: `${table.name}`,
using: arrUsing,
},
};
if (!arrayRelationships.find(ar => {
return (
ar.args.table === newArrRel.args.table &&
ar.args.name === newArrRel.args.name
);
})) {
arrayRelationships.push(newArrRel);
}
});
}
});
return {
objectRelationships,
arrayRelationships,
};
};
const createRelationships = async (tables, url, headers) => {
const relationships = generateRelationships(tables);
const bulkQuery = {
type: 'bulk',
args: [],
};
relationships.objectRelationships.forEach(or => bulkQuery.args.push(or));
relationships.arrayRelationships.forEach(ar => bulkQuery.args.push(ar));
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
body: JSON.stringify(bulkQuery),
headers,
}
);
if (resp.status !== 200) {
const error = await resp.json();
throwError(JSON.stringify(error, null, 2));
}
};
module.exports = {
createRelationships,
};

View File

@ -1,120 +0,0 @@
const fetch = require('node-fetch');
const throwError = require('../error');
const runSql = async (sqlArray, url, headers) => {
let sqlString = '';
sqlArray.forEach(sql => {
sqlString += sql;
});
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
body: JSON.stringify({
type: 'run_sql',
args: {
sql: sqlString,
cascade: true,
},
}),
headers,
}
);
if (resp.status !== 200) {
const error = await resp.json();
throwError(JSON.stringify(error, null, 2));
}
};
const generateCreateTableSql = metadata => {
const sqlArray = [];
metadata.forEach(table => {
sqlArray.push(`drop table if exists public."${table.name}" cascade;`);
let columnSql = '(';
const pkeyArr = [];
table.columns.forEach((column, i) => {
if (column.name.indexOf('_id') === 0) {
pkeyArr.push(column.name);
columnSql += `"${column.name}" ${column.type} not null,`;
} else {
columnSql += `"${column.name}" ${column.type},`;
}
if (table.columns.length === i + 1) {
columnSql += 'primary key (';
pkeyArr.forEach((key, j) => {
columnSql += `"${key}"`;
columnSql += j === pkeyArr.length - 1 ? ')' : ', ';
});
}
});
const createTableSql = `create table public."${table.name}" ${columnSql});`;
sqlArray.push(createTableSql);
});
return sqlArray;
};
const foreignKeySql = table => {
const sqlArray = [];
table.dependencies.forEach((dep, i) => {
let colNames = '';
let fks = '';
table.columns.forEach(col => {
if (col.name.indexOf(`${dep}__id`) === 0) {
colNames += `"${col.name}", `;
fks += `"${col.name.substring(col.name.indexOf('_id'), col.name.length)}", `;
}
});
fks = fks.substring(0, fks.length - 2);
colNames = colNames.substring(0, colNames.length - 2);
sqlArray.push(`alter table "${table.name}" add constraint "fk_${table.name}_${dep}_${i}" foreign key (${colNames}) references "${dep}"(${fks});`);
});
return sqlArray;
};
const generateConstraintsSql = metadata => {
let sqlArray = [];
metadata.forEach(table => {
sqlArray = [
...sqlArray,
...foreignKeySql(table),
];
});
return sqlArray;
};
const generateSql = metadata => {
const createTableSql = generateCreateTableSql(metadata);
const constraintsSql = generateConstraintsSql(metadata);
let sqlArray = [...createTableSql, ...constraintsSql];
return sqlArray;
};
const dropUtilityTables = async (url, headers) => {
const tablesToDrop = ['__rootTables'];
let sql = '';
tablesToDrop.forEach(table => {
sql += `drop table if exists "${table}" cascade;`;
});
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
headers,
body: JSON.stringify({
type: 'run_sql',
args: {
sql,
cascade: true,
},
}),
}
);
return Boolean(resp);
};
module.exports = {
generateSql,
runSql,
dropUtilityTables,
};

View File

@ -1,79 +0,0 @@
const {log} = require('../log');
const colors = require('colors/safe');
const isSubset = (array1, array2) => {
return array2.every(item => array1.includes(item));
};
const getTableColumns = obj => {
const columns = {};
for (var key in obj) {
if (key.indexOf('_id') === -1) {
columns[key] = [];
}
}
return columns;
};
const getColumnsMap = db => {
const columnMap = {};
for (var tableName in db) {
columnMap[tableName] = getTableColumns(db[tableName][0]);
db[tableName].forEach(row => {
for (var key in columnMap[tableName]) {
columnMap[tableName][key].push(row[key]);
}
});
}
return columnMap;
};
const getDuplicates = db => {
const tableColumnMap = getColumnsMap(db);
const maybeDuplicates = {};
for (var t1 in tableColumnMap) {
if (!maybeDuplicates[t1]) {
maybeDuplicates[t1] = [];
}
for (var t2 in tableColumnMap) {
if (!maybeDuplicates[t1]) {
maybeDuplicates[t2] = [];
}
if (t1 !== t2) {
for (var key in tableColumnMap[t1]) {
if (tableColumnMap[t2][key]) {
if (isSubset(tableColumnMap[t1][key], tableColumnMap[t2][key])) {
maybeDuplicates[t1].push(t2);
break;
}
}
}
}
}
}
return maybeDuplicates;
};
const suggest = (db, url) => {
const maybeDuplicates = (getDuplicates(db));
const newDuplicates = {
...maybeDuplicates,
};
let count = 1;
const dupes = [];
for (var tableName in newDuplicates) {
maybeDuplicates[tableName].forEach(dup => {
dupes.push(`${count++}. ${colors.yellow(tableName)} could be same as ${colors.yellow(dup)}`);
});
}
if (dupes.length > 0) {
log('');
log('Warning:', 'yellow');
log('While importing your data, the following duplicate tables might have been created:', 'yellow');
dupes.forEach(dupe => log(dupe));
log(`You can either re-run the command with the flag "--normalize", or normalize your database yourself at ${url}/console/data/schema/public`, 'yellow');
}
};
module.exports = suggest;

View File

@ -1,35 +0,0 @@
const fetch = require('node-fetch');
const throwError = require('../error');
const trackTables = async (tables, url, headers) => {
const bulkQueryArgs = [];
tables.forEach(table => {
bulkQueryArgs.push({
type: 'add_existing_table_or_view',
args: {
name: table.name,
schema: 'public',
},
});
});
const bulkQuery = {
type: 'bulk',
args: bulkQueryArgs,
};
const resp = await fetch(
`${url}/v1/query`,
{
method: 'POST',
body: JSON.stringify(bulkQuery),
headers,
}
);
if (resp.status !== 200) {
const error = await resp.json();
throwError(JSON.stringify(error, null, 2));
}
};
module.exports = {
trackTables,
};

View File

@ -1,19 +0,0 @@
const refineJson = db => {
const newDb = {};
for (var tableName in db) {
const newTableName = tableName.replace(/[^a-zA-Z0-9]/g, '_');
newDb[newTableName] = [];
db[tableName].forEach(row => {
const newRow = {};
for (var colName in row) {
newRow[colName.replace(/[^a-zA-Z0-9]/g, '_')] = row[colName];
}
newDb[newTableName].push(newRow);
});
}
return newDb;
};
module.exports = {
refineJson,
};

View File

@ -1,40 +0,0 @@
const colors = require('colors/safe');
const {cli} = require('cli-ux');
const shouldLog = process.env.F2G_LOG;
const log = (message, color) => {
if (shouldLog !== '0') {
if (color) {
console.log(colors[color](message));
} else {
console.log(message);
}
}
};
const spinnerStart = message => {
if (shouldLog !== '0') {
cli.action.start(message);
}
};
const spinnerStop = () => {
if (shouldLog !== '0') {
cli.action.stop(colors.green('Done!'));
}
};
const spinnerStopColorless = message => {
if (shouldLog !== '0') {
cli.action.stop(message);
}
};
module.exports = {
log,
spinnerStop,
spinnerStart,
spinnerStopColorless,
};

View File

@ -1,14 +0,0 @@
module.exports = {
appointment: {
'-LMlfYiyfmR7RxODd2lF': {
'appointment-notes': {
images: [
'https://firebasestorage.googleapis.comd2e34932caf',
],
'notes-content': 'testing........',
},
'created-time': 1537358052,
'work-request-id': '-LMlf80ePhwjG4Rkh8tY',
},
},
};

View File

@ -1,47 +0,0 @@
{
"f2g-test-posts" : {
"-LMbLFOAW2q6GO1bD-5g" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "My first post content\nAnd body\nANd structure",
"starCount" : 0,
"title" : "My first post",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
},
"-LMbLIv6VKHYul7p_PZ-" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "AKsdjak\naklsdjaskldjklas\nasdklfjaklsdfjklsda\nasdklfjasklf",
"starCount" : 0,
"title" : "Whatta proaaa",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
}
},
"f2g-test-user-posts" : {
"4UPmbcaqZKT2NdAAqBahXj4tHYN2" : {
"-LMbLFOAW2q6GO1bD-5g" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "My first post content\nAnd body\nANd structure",
"starCount" : 0,
"title" : "My first post",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
},
"-LMbLIv6VKHYul7p_PZ-" : {
"author" : "Eena",
"authorPic" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"body" : "AKsdjak\naklsdjaskldjklas\nasdklfjaklsdfjklsda\nasdklfjasklf",
"starCount" : 0,
"title" : "Whatta proaaa",
"uid" : "4UPmbcaqZKT2NdAAqBahXj4tHYN2"
}
}
},
"f2g-test-users" : {
"4UPmbcaqZKT2NdAAqBahXj4tHYN2" : {
"email" : "rishichandrawawhal@gmail.com",
"profile_picture" : "https://lh4.googleusercontent.com/-vPOIBOxCUpo/AAAAAAAAAAI/AAAAAAAAAFo/SKk9hpOB7v4/photo.jpg",
"username" : "Eena"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,136 +0,0 @@
module.exports = {
f2g_test_scores: {
Rishi: 24,
Rikin: 26,
Tanmai: 27,
},
f2g_test_author: {
someone: {
one: {
name: 'Rishi',
age: 24,
articles: {
first: {
title: 'Rishis article',
body: "Rishi's article's body",
comments: {
'Comment 1': true,
'Comment 2': true,
},
},
second: {
title: 'Rishis another article',
body: "Rishi's another article's body",
comments: {
'Comment 3': true,
},
},
},
friends: {
Rikin: true,
},
},
two: {
name: 'Rikin',
age: 30,
articles: {
third: {
title: "Rikin's article",
body: "Rikin's article's body",
comments: {
'Comment 4': true,
'Comment 5': true,
},
},
fourth: {
title: 'Rikins another article',
body: "Rikin's another article's body",
comments: {
'Comment 6': true,
'Comment df': true,
},
},
},
friends: {
Rishi: true,
Tanmai: true,
},
},
three: {
name: 'Tanmai',
age: 30,
articles: {
fifth: {
title: "Tanmai's article",
body: "Tanmai's article's body",
comments: {
'Comment asdjf': true,
'Comment dsiafjijf': true,
},
},
sixth: {
title: "Tanmai's another article",
body: "Tanmai's another article's body",
comments: {
'Coafsdfment asdjf': true,
'Commenasdft dsiafjijf': true,
},
},
},
friends: {
Rikin: true,
},
},
},
},
f2g_test_articles: {
first: {
title: 'Rishis article',
body: "Rishi's article's body",
author: {
name: 'Rishi',
age: 24,
},
},
second: {
title: 'Rishis another article',
body: "Rishi's another article's body",
author: {
name: 'Rishi',
age: 24,
},
},
third: {
title: "Rikin's article",
body: "Rikin's article's body",
author: {
name: 'Rikin',
age: 30,
},
},
fourth: {
title: 'Rikins another article',
body: "Rikin's another article's body",
author: {
name: 'Rikin',
age: 30,
},
},
fifth: {
title: "Tanmai's article",
body: "Tanmai's article's body",
author: {
name: 'Tanmai',
age: 30,
},
},
sixth: {
title: "Tanmai's another article",
body: "Tanmai's another article's body",
author: {
name: 'Tanmai',
age: 30,
},
},
},
};

View File

@ -1,110 +0,0 @@
{
"f2g_Articles": {
"A1": {
"Title": "Title1",
"Body": "Body1",
"IsUnpublished": false,
"Author": {
"Name": "AName1",
"Age": 11
},
"Comments": {
"C1": {
"Body": "Comment1",
"Author": {
"Name": "AName2",
"Sex": "M"
},
"Date": "22-09-2018"
},
"C2": {
"Body": "Comment2",
"Author": {
"Name": "AName1",
"Sex": "F"
},
"Date": "21-09-2018"
}
}
},
"A2": {
"Title": "Title2",
"Body": "Body2",
"IsUnpublished": true,
"Author": {
"Name": "AName2",
"Age": 22
},
"Comments": {
"C3": {
"Body": "Comment1",
"Author": {
"Name": "AName1",
"Sex": "F"
},
"Date": "23-09-2018"
},
"C4": {
"Body": "Comment2",
"Author": {
"Name": "AName2",
"Sex": "M"
},
"Date": "24-09-2018"
}
}
}
},
"f2g_test_Authors": {
"AT1": {
"Name": "AName1",
"Age": 11,
"Sex": "F",
"Articles": {
"A1": {
"Title": "Title1"
}
}
},
"AT2": {
"Name": "AName2",
"Age": 22,
"Sex": "M",
"Articles": {
"A2": {
"Title": "Title2"
}
}
}
},
"f2g_test_Comments": {
"C1": {
"Body": "Comment1",
"Author": {
"Name": "AName2"
},
"Date": "22-09-2018"
},
"C2": {
"Body": "Comment2",
"Author": {
"Name": "AName1"
},
"Date": "21-09-2018"
},
"C3": {
"Body": "Comment1",
"Author": {
"Name": "AName1"
},
"Date": "23-09-2018"
},
"C4": {
"Body": "Comment2",
"Author": {
"Name": "AName2"
},
"Date": "24-09-2018"
}
}
}

View File

@ -1,9 +0,0 @@
#!/bin/bash
if [ -z "$TEST_HGE_URL" ] && [ -z "$TEST_X_HASURA_ADMIN_SECRET" ]; then
echo "ERROR: Please run the test command with the environment variable TEST_HGE_URL"
else
F2G_LOG=0 ../bin/run $TEST_HGE_URL --admin-secret=$TEST_X_HASURA_ADMIN_SECRET --db=./data-sets/chinook.json --overwrite --normalize && node verifyChinook.js
F2G_LOG=0 ../bin/run $TEST_HGE_URL --admin-secret=$TEST_X_HASURA_ADMIN_SECRET --db=./data-sets/blog.json --overwrite --normalize && node verifyBlog.js
F2G_LOG=0 ../bin/run $TEST_HGE_URL --admin-secret=$TEST_X_HASURA_ADMIN_SECRET --db=./data-sets/chinook_nested.json --overwrite --normalize && node verifyChinookNested.js
F2G_LOG=0 ../bin/run $TEST_HGE_URL --admin-secret=$TEST_X_HASURA_ADMIN_SECRET --db=./data-sets/readme-example-1.json --overwrite --normalize && node verifyRE1.js
fi

View File

@ -1,62 +0,0 @@
const {query} = require('graphqurl');
const fetch = require('node-fetch');
const colors = require('colors/safe');
const complexQuery = `
query {
f2g_test_posts (order_by: {title:asc}) {
title
}
f2g_test_users (order_by: {username:asc}) {
username
}
f2g_test_user_posts (order_by:{title:asc}){
author
title
}
}
`;
const verifyDataImport = () => {
query({
query: complexQuery,
endpoint: `${process.env.TEST_HGE_URL}/v1/graphql`,
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
}).then(response => {
if (
response.data.f2g_test_posts[0].title === 'My first post' &&
response.data.f2g_test_users[0].username === 'Eena' &&
response.data.f2g_test_user_posts[1].title === 'Whatta proaaa'
) {
let sqlString = '';
['f2g_test_users', 'f2g_test_posts', 'f2g_test_user_posts'].forEach(t => {
sqlString += `drop table public."${t}" cascade;`;
});
fetch(
`${process.env.TEST_HGE_URL}/v1/query`,
{
method: 'POST',
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
body: JSON.stringify({
type: 'run_sql',
args: {
sql: sqlString,
cascade: true,
},
}),
}
).then(() => {
console.log(colors.green('✔︎ data-sets/blog.json: Test passed'));
process.exit();
}).catch(() => {
process.exit();
});
} else {
console.log(colors.red('✖ data-sets/blog.json: Test failed. Unexpected response.'));
console.log(response.data);
process.exit();
}
});
};
verifyDataImport();

View File

@ -1,70 +0,0 @@
const {query} = require('graphqurl');
const fetch = require('node-fetch');
const colors = require('colors/safe');
const complexQuery = `
query {
f2g_test_Album (
order_by:{_id:asc}
){
_id
f2g_test_Album_artist {
Name
ArtistId
}
f2g_test_Track (
order_by: {Name:asc}
) {
Name
Composer
}
}
}
`;
const verifyDataImport = () => {
query({
query: complexQuery,
endpoint: `${process.env.TEST_HGE_URL}/v1/graphql`,
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
}).then(response => {
if (
response.data.f2g_test_Album[0].f2g_test_Album_artist.ArtistId === 1 &&
response.data.f2g_test_Album[0].f2g_test_Track[0].Name === 'Breaking The Rules'
) {
let sqlString = '';
['Album', 'Album_artist', 'Track'].forEach(t => {
sqlString += `drop table public."f2g_test_${t}" cascade;`;
});
fetch(
`${process.env.TEST_HGE_URL}/v1/query`,
{
method: 'POST',
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
body: JSON.stringify({
type: 'run_sql',
args: {
sql: sqlString,
cascade: true,
},
}),
}
).then(() => {
console.log(colors.green('✔︎ data-sets/chinook.json: Test passed'));
process.exit();
}).catch(() => {
process.exit();
});
} else {
console.log(colors.red('✖ data-sets/chinook.json: Test failed. Unexpected response.'));
console.log(response.data);
process.exit();
}
}).catch(e => {
console.log(colors.red('✖ data-sets/chinook.json: Test failed. Unexpected response.'));
console.log(JSON.stringify(e, null, 2));
process.exit();
});
};
verifyDataImport();

View File

@ -1,76 +0,0 @@
const {query} = require('graphqurl');
const fetch = require('node-fetch');
const colors = require('colors/safe');
const complexQuery = `
query {
f2gt_Album (order_by:{_id:asc}){
_id
f2gt_Track (order_by: {_id:asc}) {
_id
Name
}
f2gt_Artist {
Name
f2gt_Album (order_by: {_id:desc}){
_id
Title
f2gt_Track (order_by: {Name:asc}){
Name
Composer
}
}
}
}
}
`;
const verifyDataImport = () => {
query({
query: complexQuery,
endpoint: `${process.env.TEST_HGE_URL}/v1/graphql`,
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
}).then(response => {
if (
response.data.f2gt_Album[0]._id === '1' &&
response.data.f2gt_Album[0].f2gt_Track[1]._id === '10' &&
response.data.f2gt_Album[0].f2gt_Artist.Name === 'AC/DC' &&
response.data.f2gt_Album[0].f2gt_Artist.f2gt_Album[0].Title === 'Let There Be Rock' &&
response.data.f2gt_Album[0].f2gt_Artist.f2gt_Album[0].f2gt_Track[0].Name === 'Bad Boy Boogie'
) {
let sqlString = '';
['Album', 'Artist', 'Tracks'].forEach(t => {
sqlString += `drop table public."f2gt_${t}" cascade;`;
});
fetch(
`${process.env.TEST_HGE_URL}/v1/query`,
{
method: 'POST',
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
body: JSON.stringify({
type: 'run_sql',
args: {
sql: sqlString,
cascade: true,
},
}),
}
).then(() => {
console.log(colors.green('✔︎ data-sets/chinook_nested.json: Test passed'));
process.exit();
}).catch(() => {
process.exit();
});
} else {
console.log(colors.red('✖ data-sets/chinook_nested.json: Test failed. Unexpected response.'));
process.exit();
}
}).catch(e => {
console.log(colors.red('✖ data-sets/chinook_nested.json: Test failed. Unexpected response.'));
console.log(JSON.stringify(e, null, 2));
process.exit();
});
};
verifyDataImport();

View File

@ -1,67 +0,0 @@
const {query} = require('graphqurl');
const fetch = require('node-fetch');
const colors = require('colors/safe');
const complexQuery = `
query {
f2g_test_Authors (order_by: {Name:asc}) {
_id
Name
f2g_Articles (order_by: {Title:asc}, where: { IsUnpublished: { _eq: true}}) {
Title
f2g_test_Comments (order_by: {Date:asc}) {
Body
Date
}
}
}
}
`;
const verifyDataImport = () => {
query({
query: complexQuery,
endpoint: `${process.env.TEST_HGE_URL}/v1/graphql`,
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
}).then(response => {
if (
response.data &&
response.data.f2g_test_Authors[0].f2g_Articles.length === 0 &&
response.data.f2g_test_Authors[1].f2g_Articles[0].f2g_test_Comments[0].Body === 'Comment1'
) {
let sqlString = '';
['Articles', 'Authors', 'Comments'].forEach(t => {
sqlString += `drop table public."f2g_test_${t}" cascade;`;
});
fetch(
`${process.env.TEST_HGE_URL}/v1/query`,
{
method: 'POST',
headers: {'x-hasura-admin-secret': process.env.TEST_X_HASURA_ADMIN_SECRET},
body: JSON.stringify({
type: 'run_sql',
args: {
sql: sqlString,
cascade: true,
},
}),
}
).then(() => {
console.log(colors.green('✔︎ data-sets/readme-example-1.json: Test passed'));
process.exit();
}).catch(() => {
process.exit();
});
} else {
console.log(colors.red('✖ data-sets/readme-example-1.json: Test failed. Unexpected response.'));
process.exit();
}
}).catch(e => {
console.log(colors.red('✖ data-sets/readme-example-1.json: Test failed. Unexpected response.'));
console.log(JSON.stringify(e, null, 2));
process.exit();
});
};
verifyDataImport();

View File

@ -1,38 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"transform-react-remove-prop-types",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
"@babel/plugin-proposal-optional-chaining",
[
"@babel/plugin-proposal-pipeline-operator",
{
"proposal": "minimal"
}
],
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-do-expressions",
"@babel/plugin-proposal-function-bind",
"extract-hoc/babel",
"react-hot-loader/babel",
"istanbul"
]
}

View File

@ -1,120 +0,0 @@
---
# Output debugging info
# loglevel: debug
# Major version of Bootstrap: 3 or 4
bootstrapVersion: 3
# If Bootstrap version 3 is used - turn on/off custom icon font path
useCustomIconFontPath: false
# Webpack loaders, order matters
styleLoaders:
- style
- css
- sass
# Extract styles to stand-alone css file
# Different settings for different environments can be used,
# It depends on value of NODE_ENV environment variable
# This param can also be set in webpack config:
# entry: 'bootstrap-loader/extractStyles'
#extractStyles: false
env:
development:
extractStyles: false
production:
extractStyles: true
# Customize Bootstrap variables that get imported before the original Bootstrap variables.
# Thus, derived Bootstrap variables can depend on values from here.
# See the Bootstrap _variables.scss file for examples of derived Bootstrap variables.
#
preBootstrapCustomizations: ./src/theme/variables.scss
# This gets loaded after bootstrap/variables is loaded
# Thus, you may customize Bootstrap variables
# based on the values established in the Bootstrap _variables.scss file
#
bootstrapCustomizations: ./src/theme/bootstrap.overrides.scss
# Import your custom styles here
# Usually this endpoint-file contains list of @imports of your application styles
#
# appStyles: ./path/to/your/app/styles/endpoint.scss
### Bootstrap styles
styles:
# Mixins
mixins: true
# Reset and dependencies
normalize: true
print: true
glyphicons: true
# Core CSS
scaffolding: true
type: true
code: true
grid: true
tables: true
forms: true
buttons: true
# Components
component-animations: true
dropdowns: true
button-groups: true
input-groups: true
navs: true
navbar: true
breadcrumbs: true
pagination: true
pager: true
labels: true
badges: true
jumbotron: true
thumbnails: true
alerts: true
progress-bars: true
media: true
list-group: true
panels: true
wells: true
responsive-embed: true
close: true
# Components w/ JavaScript
modals: true
tooltip: true
popovers: true
carousel: true
# Utility classes
utilities: true
responsive-utilities: true
### Bootstrap scripts
#scripts: false
scripts:
transition: false
alert: false
button: true
carousel: false
collapse: false
dropdown: true
modal: true
tooltip: false
popover: false
scrollspy: false
tab: false
affix: false

View File

@ -1,2 +0,0 @@
webpack/*
src/utils.js

View File

@ -1,106 +0,0 @@
{ "extends": "eslint-config-airbnb",
"env": {
"browser": true,
"node": true,
"mocha": true,
"cypress/globals": true
},
"parser": "babel-eslint",
"rules": {
"allowForLoopAfterthoughts": true,
"react/no-multi-comp": 0,
"import/default": 0,
"import/no-duplicates": 0,
"import/named": 0,
"import/first": 0,
"import/namespace": 0,
"import/no-unresolved": 0,
"import/no-named-as-default": 2,
"import/extensions": 0,
"import/no-extraneous-dependencies": 0,
"import/prefer-default-export": 0,
"comma-dangle": 0,
"id-length": [1, {"min": 1, "properties": "never"}],
"indent": [2, 2, {"SwitchCase": 1}],
"no-console": 0,
"arrow-parens": 0,
"no-alert": 0,
"no-plusplus": 0,
"no-unsafe-negation": 0,
"no-loop-func": 0,
"no-lonely-if": 0,
"no-bitwise": 0,
"global-require": 0,
"no-param-reassign": 0,
"no-underscore-dangle": 0,
"no-useless-return": 0,
"no-restricted-syntax": 0,
"no-prototype-builtins": 0,
"array-callback-return": 0,
"no-useless-concat": 0,
"consistent-return": 0,
"class-methods-use-this": 0,
"arrow-body-style": 0,
"prefer-template": 0,
"prefer-spread": 0,
"object-shorthand": 0,
"camelcase": 0,
"object-curly-newline": 0,
"spaced-comment": 0,
"prefer-destructuring": ["error", {"object": false, "array": false}],
"prefer-rest-params": 0,
"function-paren-newline": 0,
"no-case-declarations": 0,
"no-restricted-globals": 0,
"no-unneeded-ternary": 0,
"no-mixed-operators": 0,
"no-return-assign": 0,
"operator-assignment": 0,
"strict": 0,
"react/jsx-no-duplicate-props": 0,
"react/jsx-filename-extension": 0,
"react/jsx-curly-brace-presence": 0,
"react/forbid-prop-types": 0,
"react/require-default-props": 0,
"react/no-unused-prop-types": 0,
"react/no-string-refs": 0,
"react/no-unused-state": 0,
"react/no-array-index-key": 0,
"react/jsx-no-bind": 0,
"react/prop-types": 0,
"react/prefer-stateless-function": 0,
"react/no-unescaped-entities": 0,
"react/sort-comp": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/label-has-for": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/lang": 0,
"jsx-a11y/alt-text": 0,
"jsx-a11y/no-autofocus": 0,
"max-len": 0,
"no-continue": 0
},
"plugins": [
"react", "import", "cypress"
],
"settings": {
"import/parser": "babel-eslint",
"parser": "babel-esling",
"import/resolve": {
"moduleDirectory": ["node_modules", "src"]
}
},
"globals": {
"__DEVELOPMENT__": true,
"__CLIENT__": true,
"__SERVER__": true,
"__DISABLE_SSR__": true,
"__DEVTOOLS__": true,
"socket": true,
"webpackIsomorphicTools": true,
"CONSOLE_ASSET_VERSION": true
}
}

View File

@ -1,11 +0,0 @@
.env
node_modules
static/dist
webpack-assets.json
webpack-stats.json
npm-debug.log
*.swp
coverage
.idea/*
test
**/.tern-port

View File

@ -1,22 +0,0 @@
FROM node:8.9-alpine
# Create app directory
WORKDIR /app
# Install app dependencies
RUN npm config set unsafe-perm true
RUN npm -g install serve
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . /app
#Build react/vue/angular bundle static files
RUN npm run build
RUN rm -Rf node_modules
EXPOSE 8080
# serve dist folder on port 8080
CMD ["serve", "-s", "static", "-p", "8080"]

View File

@ -1,35 +1 @@
## GraphiQL Demo
This version of GraphiQL is a fork of the original version with a simple header management UI.
You can access it live here - https://graphiql-online.com
## Usage of Environment Variables
This app uses a few environment variables which are required for development. The production build uses values directly present in index.html serving this app.
We use [dotenv](https://github.com/motdotla/dotenv) for setting environment variables for development. Create a `.env` file in the root directory (wherever package.json is) and set the following values. Replace accordingly for testing.
```
PORT=3000
NODE_ENV=development
GRAPHQL_ENDPOINT=http://localhost:8090/v1/graphql
HEADER_STRING='{}'
VARIABLE_STRING='{}'
QUERY_STRING='query { test_table { id } }'
```
**Note**
The .env file should not be in version control.
## Deployment
```
$ npm run build
```
The static assets will be generated in `static` folder. There is an index.html file referencing the css and js assets inside `dist` folder.
For a quick Docker based deployment, use `docker build -t graphiql .` && `docker run -d -p 8080:8080 graphiql` for running the production build locally.
You can also use now.sh for a cloud deployment. Just simply run `now` to deploy this and get a live URL.
## This project has been moved to [hasura/graphiql-online](https://github.com/hasura/graphiql-online)

View File

@ -1,9 +0,0 @@
module.exports = {
hmrPort: parseInt(process.env.PORT, 10) + 1 || 3001,
hmrHost: process.env.HOST || '127.0.0.1',
appHost: '0.0.0.0',
port: { development: process.env.PORT, production: 8080 },
assetsPrefix: '/rstatic',
webpackPrefix: '/rstatic/dist/',
appPrefix: '/rapp',
};

View File

@ -1,15 +0,0 @@
// enable runtime transpilation to use ES6/7 in node
const fs = require('fs');
const babelrc = fs.readFileSync('.babelrc');
let config;
try {
config = JSON.parse(babelrc);
} catch (err) {
console.error('==> ERROR: Error parsing your .babelrc.');
console.error(err);
}
require('@babel/register')(config);

View File

@ -1,33 +0,0 @@
#!/usr/bin/env node
require('./server.babel'); // babel registration (runtime transpilation for node)
const path = require('path');
const rootDir = path.resolve(__dirname, '..');
/**
* Define isomorphic constants.
*/
global.__CLIENT__ = false;
global.__SERVER__ = true;
global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING
global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production';
if (__DEVELOPMENT__) {
if (
!require('piping')({
//Fork the process and supervise the child for hot-reloading code
hook: true,
ignore: /(\/\.|~$|\.json|\.scss$)/i,
})
) {
return; //The parent process ends, and child process continues from below
}
}
const WebpackIsomorphicTools = require('webpack-isomorphic-tools');
global.webpackIsomorphicTools = new WebpackIsomorphicTools(
require('../webpack/webpack-isomorphic-tools')
).server(rootDir, () => {
require('../src/server');
});
require('../src/server');

View File

@ -1,25 +0,0 @@
{
"version": 2,
"name": "graphiql-online",
"alias": ["graphiql-online.com"],
"builds": [
{
"src": "package.json",
"use": "@now/static-build",
"config": { "distDir": "static" }
}
],
"routes": [
{
"src": "/static/(.*)",
"headers": { "cache-control": "s-maxage=31536000,immutable" },
"dest": "/static/$1"
},
{ "src": "/favicon.ico", "dest": "/favicon.ico" },
{
"src": "/(.*)",
"headers": { "cache-control": "s-maxage=0" },
"dest": "/static/index.html"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,163 +0,0 @@
{
"name": "graphqurl-graphiql",
"description": "Explore GraphQL APIs with headers",
"author": "Praveen <praveen@hasura.io>",
"license": "Apache 2.0",
"version": "0.2.0",
"repository": {
"type": "git",
"url": "https://github.com/hasura/graphql-engine"
},
"main": "index.js",
"homepage": "https://hasura.io/",
"keywords": [],
"scripts": {
"start": "concurrently --kill-others \"npm run start-prod\"",
"start-prod": "better-npm-run start-prod",
"build": "webpack --progress -p --colors --display-error-details --config webpack/prod.config.js",
"now-build": "webpack --progress -p --colors --display-error-details --config webpack/prod.config.js",
"build-unused": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js --json | webpack-unused -s src",
"lint": "eslint -c .eslintrc src api",
"start-dev": "better-npm-run start-dev",
"watch-client": "better-npm-run watch-client",
"dev": "concurrently --kill-others \"npm run watch-client\" \"npm run start-dev\" ",
"deploy": "now"
},
"betterScripts": {
"start-prod": {
"command": "node ./bin/server.js",
"env": {
"NODE_PATH": "./src",
"NODE_ENV": "production",
"PORT": 8080
}
},
"start-dev": {
"command": "node -r dotenv/config ./bin/server.js"
},
"watch-client": {
"command": "node -r dotenv/config webpack/webpack-dev-server.js"
}
},
"dependencies": {
"apollo-link": "^1.2.2",
"apollo-link-ws": "^1.0.8",
"graphiql": "^0.13.2",
"graphiql-explorer": "0.4.4",
"graphql": "^14.3.0",
"graphql-voyager": "^1.0.0-rc.27",
"hasura-console-graphiql": "0.1.0-alpha.1",
"history": "^3.0.0",
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.5.0",
"less": "^3.7.1",
"lru-memoize": "^1.0.0",
"map-props": "^1.0.0",
"match-sorter": "^2.3.0",
"multireducer": "^1.0.2",
"piping": "^0.3.2",
"prettier": "^1.16.4",
"pretty-error": "^1.2.0",
"prop-types": "^15.6.0",
"react": "16.8.2",
"react-ace": "^6.1.1",
"react-bootstrap": "^0.32.1",
"react-copy-to-clipboard": "^5.0.0",
"react-dom": "16.8.2",
"react-helmet": "^5.2.0",
"react-notification-system": "^0.2.17",
"react-progress-bar-plus": "^1.3.1",
"react-redux": "^5.0.6",
"react-router": "^3.2.0",
"react-router-redux": "^4.0.8",
"react-tabs": "^2.1.0",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"semver": "5.5.1",
"subscriptions-transport-ws": "^0.9.12",
"valid-url": "^1.0.9"
},
"devDependencies": {
"@babel/core": "^7.3.3",
"@babel/plugin-proposal-class-properties": "^7.3.3",
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/plugin-proposal-do-expressions": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.2.0",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"@babel/runtime": "^7.0.0",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.0",
"babel-plugin-istanbul": "^5.1.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-plugin-typecheck": "^2.0.0",
"better-npm-run": "^0.1.0",
"bootstrap-loader": "^2.2.0",
"bootstrap-sass": "^3.3.7",
"clean-webpack-plugin": "^0.1.17",
"concurrently": "^3.5.0",
"css-loader": "^0.28.11",
"dotenv": "^5.0.1",
"eslint": "^4.19.1",
"eslint-config-airbnb": "16.1.0",
"eslint-loader": "^1.0.0",
"eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.9.1",
"express": "^4.13.3",
"express-session": "^1.12.1",
"extract-hoc": "0.0.5",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"font-awesome": "^4.7.0",
"font-awesome-webpack": "0.0.4",
"husky": "^0.14.3",
"ignore-loader": "^0.1.2",
"jquery": "^3.4.1",
"json-loader": "^0.5.4",
"less-loader": "^4.1.0",
"lint-staged": "^6.1.1",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.12.0",
"nyc": "^13.3.0",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"react-a11y": "^0.2.6",
"react-addons-test-utils": "^15.0.3",
"react-hot-loader": "^4.6.5",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.2",
"redux-devtools-log-monitor": "^1.3.0",
"resolve-url-loader": "^2.3.0",
"sass-loader": "^7.0.1",
"sinon": "^1.17.7",
"style-loader": "^0.20.3",
"timekeeper": "1.0.0",
"uglifyjs-webpack-plugin": "^1.2.7",
"unused-files-webpack-plugin": "^3.4.0",
"url-loader": "^1.0.1",
"webpack": "^4.14.0",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.0.8",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.2",
"webpack-isomorphic-tools": "^3.0.5"
},
"engines": {
"node": ">=8.9.1"
}
}

View File

@ -1,93 +0,0 @@
/**
* THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER.
*/
// import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { routerMiddleware, syncHistoryWithStore } from 'react-router-redux';
import { compose, createStore, applyMiddleware } from 'redux';
import { useBasename } from 'history';
import getRoutes from './routes';
import reducer from './reducer';
// Create the store
let _finalCreateStore;
if (__DEVELOPMENT__) {
_finalCreateStore = compose(
applyMiddleware(thunk, routerMiddleware(browserHistory), createLogger()),
require('redux-devtools').persistState(
window.location.href.match(/[?&]debug_session=([^&]+)\b/)
)
)(createStore);
} else {
_finalCreateStore = compose(
applyMiddleware(thunk, routerMiddleware(browserHistory))
)(createStore);
}
const hashLinkScroll = () => {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
}, 0);
} else {
// This is a hack to solve the issue with scroll retention during page change.
setTimeout(() => {
const element = document.getElementsByTagName('body');
if (element && element.length > 0) {
element[0].scrollIntoView();
}
}, 0);
}
};
const store = _finalCreateStore(reducer);
const history = syncHistoryWithStore(browserHistory, store);
/* ****************************************************************** */
// Enable hot reloading
if (__DEVELOPMENT__ && module.hot) {
module.hot.accept('./reducer', () => {
store.replaceReducer(require('./reducer'));
});
}
// Main routes and rendering
const main = (
<Router
history={useBasename(() => history)({ basename: '/' })}
routes={getRoutes(store)}
onUpdate={hashLinkScroll}
/>
);
const dest = document.getElementById('content');
ReactDOM.render(
<Provider store={store} key="provider">
{main}
</Provider>,
dest
);
if (process.env.NODE_ENV !== 'production') {
window.React = React; // enable debugger
}

View File

@ -1,110 +0,0 @@
import defaultState from './State';
const LOAD_REQUEST = 'App/ONGOING_REQUEST';
const DONE_REQUEST = 'App/DONE_REQUEST';
const FAILED_REQUEST = 'App/FAILED_REQUEST';
const ERROR_REQUEST = 'App/ERROR_REQUEST';
const CONNECTION_FAILED = 'App/CONNECTION_FAILED';
/**
* Global notification function
* options: type default, description
* level: string info, {success, error, warning, info}
* position: string br, {tr, tl, tc, br, bl, bc}
* title: string null
* message: string null
* autoDismiss: integer 5, set to 0 to not auto-dismiss
* dismissible: bool true, set if user can dismiss notification
* action: object null, action button with label string and callback function
* children: element/string, null, add custom element, over-rides action
* onAdd: function, null, called when notification is successfully created, 1st argument is the notification
* onRemove: function, null, same as onAdd
* uid: integer/string, null, unique identifier to the notification, same uid will not be shown again
*/
const showNotification = ({
level = 'info',
position = 'tr',
...options
} = {}) => {
return dispatch => {
if (level === 'success') {
dispatch(Notifications.removeAll());
}
dispatch(
Notifications.show(
{
position,
autoDismiss: ['error', 'warning'].includes(level) ? 0 : 5,
dismissible: ['error', 'warning'].includes(level) ? 'button' : 'both',
...options,
},
level
)
);
};
};
const progressBarReducer = (state = defaultState, action) => {
switch (action.type) {
case LOAD_REQUEST:
return {
...state,
ongoingRequest: true,
percent: 10,
requestSuccess: null,
requestError: null,
connectionFailed: false,
};
case DONE_REQUEST:
return {
...state,
percent: 100,
ongoingRequest: false,
requestSuccess: true,
requestError: null,
connectionFailed: false,
};
case FAILED_REQUEST:
return {
...state,
percent: 100,
ongoingRequest: false,
requestSuccess: null,
requestError: true,
connectionFailed: false,
};
case ERROR_REQUEST:
return {
...state,
modalOpen: true,
error: action.data,
reqURL: action.url,
reqData: action.params,
statusCode: action.statusCode,
connectionFailed: false,
};
case CONNECTION_FAILED:
return {
...state,
modalOpen: true,
error: true,
connectionFailed: true,
};
default:
return state;
}
};
export default progressBarReducer;
export {
LOAD_REQUEST,
DONE_REQUEST,
FAILED_REQUEST,
ERROR_REQUEST,
CONNECTION_FAILED,
showNotification,
};

View File

@ -1,72 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import ProgressBar from 'react-progress-bar-plus';
import { hot } from 'react-hot-loader';
import ErrorBoundary from '../Error/ErrorBoundary';
class App extends Component {
componentDidMount() {
// Hide the loader once the react component is ready.
// NOTE: This will execute only onces (since this is parent component for all other component).
const className = document.getElementById('content').className;
document.getElementById('content').className = className + ' show';
document.getElementById('loading').style.display = 'none';
}
render() {
const styles = require('./App.scss');
const {
requestError,
error,
ongoingRequest,
percent,
intervalTime,
children,
connectionFailed,
dispatch,
} = this.props;
return (
<ErrorBoundary dispatch={dispatch}>
<div>
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
</div>
</ErrorBoundary>
);
}
}
App.propTypes = {
reqURL: PropTypes.string,
reqData: PropTypes.object,
statusCode: PropTypes.number,
error: PropTypes.object,
ongoingRequest: PropTypes.bool,
requestError: PropTypes.bool,
connectionFailed: PropTypes.bool,
intervalTime: PropTypes.number,
percent: PropTypes.number,
children: PropTypes.element,
dispatch: PropTypes.func.isRequired,
};
const mapStateToProps = state => {
return {
...state.progressBar,
};
};
export default hot(module)(connect(mapStateToProps)(App));

View File

@ -1,68 +0,0 @@
@import "../Common/Common.scss";
:global {
@keyframes react-progress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.react-progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
visibility: visible;
opacity: 1;
transition: all 400ms;
z-index: 9999;
&.react-progress-bar-on-top {
height: 100%;
}
&.react-progress-bar-hide {
opacity: 0;
visibility: hidden;
z-index: -10;
}
}
.react-progress-bar-percent {
height: 2px;
background: #e8694d;
box-shadow: 0 2px 5px #d3291c, 0 2px 5px #d3291c;
transition: all 200ms ease;
}
.react-progress-bar-spinner {
display: block;
position: fixed;
top: 15px;
}
.react-progress-bar-spinner-left {
left: 15px;
right: auto;
}
.react-progress-bar-spinner-right {
left: auto;
right: 15px;
}
.react-progress-bar-spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #d3291c;
border-left-color: #d3291c;
border-radius: 50%;
animation: react-progress-spinner 400ms linear infinite;
}
}

View File

@ -1,9 +0,0 @@
const defaultState = {
percent: 0,
intervalTime: 200,
ongoingRequest: false,
requestSuccess: null,
requestError: null,
error: null,
};
export default defaultState;

View File

@ -1,14 +0,0 @@
const stateKey = 'graphiql';
const loadAppState = () => JSON.parse(window.localStorage.getItem(stateKey));
const saveAppState = state => {
window.localStorage.setItem(stateKey, JSON.stringify(state));
};
const clearState = () => window.localStorage.removeItem(stateKey);
export {
saveAppState,
loadAppState,
clearState,
};

View File

@ -1,82 +0,0 @@
import React from 'react';
/**
* Accepts following props
* `title, string || react-element `: Title of the collapsible toggle
* `isOpen`(optional, default to false): Whether the body should be shown or not
* `toggleHandler (optional)`: Function to call when the toggle is clicked
* `testId, string`: Test identifier
* `children, react-element`: The content which needs to be toggled
*/
class CollapsibleToggle extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: props.isOpen || false,
toggleHandler:
props.toggleHandler || this.defaultToggleHandler.bind(this),
};
}
defaultToggleHandler() {
this.setState({ isOpen: !this.state.isOpen });
}
componentWillReceiveProps(nextProps) {
const { isOpen, toggleHandler } = nextProps;
if (toggleHandler) {
this.setState({ isOpen: isOpen, toggleHandler: toggleHandler });
}
}
render() {
const styles = require('./CollapsibleToggle.scss');
const { title, children, testId, useDefaultTitleStyle } = this.props;
const { isOpen, toggleHandler } = this.state;
const getTitle = () => {
let _title;
if (useDefaultTitleStyle) {
_title = <div className={styles.defaultCollapsibleTitle}>{title}</div>;
} else {
_title = title;
}
return _title;
};
const getChildren = () => {
return <div className={styles.collapsibleContent}>{children}</div>;
};
return (
<div className={styles.collapsibleWrapper}>
<div
className={styles.collapsibleToggle}
data-test={testId}
onClick={toggleHandler}
>
<span className={styles.collapsibleIndicatorWrapper}>
<i
className={`fa fa-chevron-right ${
styles.collapsibleIndicator
} ${isOpen && styles.collapsibleIndicatorOpen}`}
/>
</span>
<span className={styles.titleWrapper}>{getTitle()}</span>
</div>
{isOpen && getChildren()}
</div>
);
}
}
export default CollapsibleToggle;

View File

@ -1,36 +0,0 @@
.collapsibleWrapper {
margin-bottom: 5px;
.collapsibleToggle {
cursor: pointer;
display: inline-block;
.collapsibleIndicatorWrapper {
padding-right: 10px;
font-size: 12px;
.collapsibleIndicator {
transition: transform 0.3s ease;
}
.collapsibleIndicatorOpen {
transform: rotate(90deg);
}
}
.titleWrapper {
display: inline-block;
}
.defaultCollapsibleTitle {
color: #788095;
font-weight: bold;
font-size: 14px;
}
}
.collapsibleContent {
margin-top: 10px;
display: block;
}
}

View File

@ -1,15 +0,0 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
const ModalWrapper = ({ show, onHide, dialogClassName, title, children }) => {
return (
<Modal show={show} onHide={onHide} dialogClassName={dialogClassName}>
<Modal.Header closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>{children}</Modal.Body>
</Modal>
);
};
export default ModalWrapper;

View File

@ -1,24 +0,0 @@
import React from 'react';
const Spinner = () => {
const styles = require('./Spinner.scss');
return (
<div className={styles.sk_circle}>
<div className={styles.sk_circle1 + ' ' + styles.sk_child} />
<div className={styles.sk_circle2 + ' ' + styles.sk_child} />
<div className={styles.sk_circle3 + ' ' + styles.sk_child} />
<div className={styles.sk_circle4 + ' ' + styles.sk_child} />
<div className={styles.sk_circle5 + ' ' + styles.sk_child} />
<div className={styles.sk_circle6 + ' ' + styles.sk_child} />
<div className={styles.sk_circle7 + ' ' + styles.sk_child} />
<div className={styles.sk_circle8 + ' ' + styles.sk_child} />
<div className={styles.sk_circle9 + ' ' + styles.sk_child} />
<div className={styles.sk_circle10 + ' ' + styles.sk_child} />
<div className={styles.sk_circle11 + ' ' + styles.sk_child} />
<div className={styles.sk_circle12 + ' ' + styles.sk_child} />
</div>
);
};
export default Spinner;

View File

@ -1,165 +0,0 @@
.sk_circle {
margin: 100px auto;
width: 40px;
height: 40px;
position: relative;
}
.sk_circle .sk_child {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.sk_circle .sk_child:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
background-color: #333;
border-radius: 100%;
-webkit-animation: sk_circleBounceDelay 1.2s infinite ease-in-out both;
animation: sk_circleBounceDelay 1.2s infinite ease-in-out both;
}
.sk_circle .sk_circle2 {
-webkit-transform: rotate(30deg);
-ms-transform: rotate(30deg);
transform: rotate(30deg);
}
.sk_circle .sk_circle3 {
-webkit-transform: rotate(60deg);
-ms-transform: rotate(60deg);
transform: rotate(60deg);
}
.sk_circle .sk_circle4 {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.sk_circle .sk_circle5 {
-webkit-transform: rotate(120deg);
-ms-transform: rotate(120deg);
transform: rotate(120deg);
}
.sk_circle .sk_circle6 {
-webkit-transform: rotate(150deg);
-ms-transform: rotate(150deg);
transform: rotate(150deg);
}
.sk_circle .sk_circle7 {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.sk_circle .sk_circle8 {
-webkit-transform: rotate(210deg);
-ms-transform: rotate(210deg);
transform: rotate(210deg);
}
.sk_circle .sk_circle9 {
-webkit-transform: rotate(240deg);
-ms-transform: rotate(240deg);
transform: rotate(240deg);
}
.sk_circle .sk_circle10 {
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg); }
.sk_circle .sk_circle11 {
-webkit-transform: rotate(300deg);
-ms-transform: rotate(300deg);
transform: rotate(300deg);
}
.sk_circle .sk_circle12 {
-webkit-transform: rotate(330deg);
-ms-transform: rotate(330deg);
transform: rotate(330deg); }
.sk_circle .sk_circle2:before {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.sk_circle .sk_circle3:before {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.sk_circle .sk_circle4:before {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.sk_circle .sk_circle5:before {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.sk_circle .sk_circle6:before {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.sk_circle .sk_circle7:before {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.sk_circle .sk_circle8:before {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.sk_circle .sk_circle9:before {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.sk_circle .sk_circle10:before {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.sk_circle .sk_circle11:before {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.sk_circle .sk_circle12:before {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes sk_circleBounceDelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes sk_circleBounceDelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@ -1,164 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class TextAreaWithCopy extends React.Component {
copyToClip(id, e) {
e.preventDefault();
const { copyText, textLanguage, containerId } = this.props;
let text = '';
if (copyText.length > 0) {
switch (textLanguage) {
case 'sql':
text = window.sqlFormatter
? window.sqlFormatter.format(copyText, { language: textLanguage })
: copyText;
break;
default:
text = copyText;
}
}
const textArea = document.createElement('textarea');
textArea.value = text;
const appendLoc = containerId
? document.getElementById(containerId)
: document.body;
appendLoc.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
// const msg = successful ? 'successful' : 'unsuccessful';
const tooltip = document.getElementById(id);
if (!successful) {
tooltip.innerHTML = 'Error copying';
throw new Error('Copy was unsuccessful');
} else {
tooltip.innerHTML = 'Copied';
}
} catch (err) {
alert('Oops, unable to copy - ' + err);
}
appendLoc.removeChild(textArea);
}
resetCopy(id) {
const tooltip = document.getElementById(id);
tooltip.innerHTML = 'Copy';
}
render() {
const style = require('./TextAreaWithCopy.scss');
const {
copyText,
toolTipClass,
id,
containerId,
textLanguage,
} = this.props;
const renderSimpleValue = () => {
return (
<pre className={style.schemaPreWrapper}>
<code className={style.formattedCode}>{copyText}</code>
</pre>
);
};
const renderSQLValue = () => {
if (!window || !window.hljs || !window.sqlFormatter) {
return renderSimpleValue();
}
return (
<pre>
<code
className={style.formattedCode}
dangerouslySetInnerHTML={{
__html: window.hljs.highlight(
'sql',
window.sqlFormatter.format(copyText, { language: textLanguage })
).value,
}}
/>
</pre>
);
};
const renderJSONValue = () => {
if (!window || !window.hljs) {
return renderSimpleValue();
}
return (
<pre>
<code
className={style.formattedCode}
dangerouslySetInnerHTML={{
__html: window.hljs.highlight(
'json',
JSON.stringify(JSON.parse(copyText), null, 4)
).value,
}}
/>
</pre>
);
};
const getTypeRenderer = type => {
let typeRenderer;
switch (type) {
case 'sql':
typeRenderer = renderSQLValue;
break;
case 'json':
typeRenderer = renderJSONValue;
break;
default:
typeRenderer = renderSimpleValue;
}
return typeRenderer;
};
return (
<div className={style.codeBlockCustom} id={containerId}>
<div className={style.copyGenerated}>
<div className={style.copyTooltip}>
<span
className={toolTipClass ? toolTipClass : style.tooltiptext}
id={id}
>
Copy
</span>
<i
className={'fa fa-copy'}
onClick={this.copyToClip.bind(this, id)}
onMouseLeave={this.resetCopy.bind(this, id)}
/>
</div>
</div>
{getTypeRenderer(textLanguage)()}
</div>
);
}
}
TextAreaWithCopy.propTypes = {
copyText: PropTypes.string.isRequired,
textLanguage: PropTypes.string,
id: PropTypes.string.isRequired,
containerId: PropTypes.string,
};
export default TextAreaWithCopy;

View File

@ -1,115 +0,0 @@
.sqlBlock {
position: relative;
width: 80%;
}
.codeBlockCustom {
/* position: relative;
padding: 10px 20px; */
/* background-color: white; */
background-color: rgb(253, 249, 237);
/* margin: 20px;
width: 100%; */
width: auto;
border-radius: 5px;
// max-height: calc(100% - 60px);
// overflow: auto;
margin-top: 0px;
// min-height: calc(100% - 60px);
position: relative;
}
.codeBlockCustom pre {
display: block;
padding: 10px 20px;
margin: 0px;
font-size: 13px;
line-height: unset;
word-break: unset;
word-wrap: unset;
color: #000;
background: none;
border: none;
border-radius: 0;
overflow: unset;
padding-bottom: 10px;
}
.codeBlockCustom code {
color: #000;
background: none;
}
.codeBlockCustom .formattedCode {
padding: 0px 0px !important;
}
.schemaPreWrapper {
position: relative;
min-height: 40px;
}
.copyGenerated {
position: absolute;
bottom: 10px;
right: 30px;
cursor: pointer;
z-index: 100;
}
.copyGenerated img, .copyExecution img {
width: 20px;
opacity: .6;
}
.copyGenerated img:hover, .copyExecution img:hover {
opacity: 1;
}
.copyGenerated:focus, .copyExecution:focus {
outline: none;
}
.copyTooltip {
position: relative;
display: inline-block;
}
.copyTooltip i {
color: #505050;
}
.copyTooltip .tooltiptext {
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 4px 0px;
font-size: 14px;
position: absolute;
z-index: 1000000000;
right: -21px;
bottom: 30px;
opacity: 0;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
display: none;
width: 57px;
}
.copyTooltip .tooltiptext::after {
content: "";
position: absolute;
top: 26px;
right: 22px;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.copyTooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
display: block;
}

View File

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Spinner from '../Common/Spinner/Spinner';
import PageNotFound, { NotFoundError } from './PageNotFound';
class ErrorBoundary extends React.Component {
initialState = {
hasError: false,
info: null,
error: null,
type: '500',
};
constructor(props) {
super(props);
this.state = this.initialState;
}
resetState = () => {
this.setState({ ...this.initialState });
};
componentDidCatch(error, info) {
const { dispatch } = this.props;
// for invalid path segment errors
if (error instanceof NotFoundError) {
this.setState({
type: '404',
});
}
this.setState({ hasError: true, info: info, error: error });
}
render() {
const { metadata } = this.props;
const { hasError, type, error } = this.state;
if (hasError && metadata.ongoingRequest) {
return (
<div>
<Spinner />
</div>
);
}
if (hasError) {
<PageNotFound resetCallback={this.resetState} />
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.element,
};
export default ErrorBoundary;

View File

@ -1,52 +0,0 @@
.container {
padding: 0;
}
.viewContainer {
height: 100vh;
width: 100vw;
display: table;
.centerContent{
display: table-cell;
vertical-align: middle;
.message {
padding: 50px 20%;
}
.message h1 {
font-size: 54px;
font-weight: bold;
}
.message p {
margin-left: 15px;
}
.message p > a {
font-weight: bold;
}
.errorStack {
max-height: 300px;
width: 450px;
}
}
}
.header {
background: #eee;
h2 {
margin: 0;
padding: 26px;
float: left;
line-height: 26px;
}
.nav {
padding: 20px;
float: left;
}
}

View File

@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import Helmet from 'react-helmet';
export class NotFoundError extends Error {}
class PageNotFound extends Component {
render() {
const styles = require('./ErrorPage.scss');
const { resetCallback } = this.props;
return (
<div className={styles.viewContainer}>
<Helmet title="404 - Page Not Found | Hasura" />
<div className={'container ' + styles.centerContent}>
<div className={'row ' + styles.message}>
<div className="col-xs-8">
<h1>404</h1>
<br />
<div>
This page doesn't exist. Head back{' '}
<Link to="/" onClick={resetCallback}>
Home
</Link>
.
</div>
</div>
</div>
</div>
</div>
);
}
}
PageNotFound.propTypes = {
dispatch: PropTypes.func.isRequired,
resetCallback: PropTypes.func.isRequired,
};
export default connect()(PageNotFound);

View File

@ -1,83 +0,0 @@
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400');
body
{
font-family: 'Open Sans';
font-size: 16px;
margin: 0;
}
.login {
text-align: center;
}
.loginTextbox {
font-size: 16px;
height: 43px;
width: 74%;
margin-right: 1%;
font-weight: 300;
border: 1px solid #ececec;
border-radius: 5px;
padding: 0;
padding-left: 10px;
display: inline-block;
}
.loginButton {
height: 45px;
width: 10%;
display: inline-block;
border-radius: 5px;
background-color: 'green' !important;
cursor: pointer;
font-size: 16px;
margin-right: 1%;
}
.loginButton:hover
{
background-color: #f6f6f6;
}
.loginHeading {
text-align: center;
font-family: 'Raleway';
margin-top: 0;
padding-bottom: 20px;
}
.loginWrapper
{
width: 600px;
padding: 30px;
margin: 0 auto;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid #ececec;
border-radius: 5px;
background-color: #f6f6f6;
}
.footerWrapper {
position: fixed;
bottom: 20px;
width: 100%;
display: flex;
justify-content: center;
}
.apiHasura {
font-size: 14px;
text-align: right;
}
.apiHasura i {
color: #757575;;
font-size: 22px;
}
.apiHasura i:hover {
color: #000;
}
.built {
text-align: right;
font-size: 14px;
margin-right: 10px;
}
.built i {
color: #f93c18;
}

View File

@ -1,89 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import './App.css';
import { updateGraphQLEndpoint } from '../Services/ApiExplorer/Actions';
class LoginComponent extends React.Component {
constructor() {
super();
this.state = { graphqlEndpoint: '' };
}
setGraphQLEndpoint(e) {
this.setState({ graphqlEndpoint: e.target.value });
}
render() {
const { dispatch } = this.props;
return (
<div>
<div className="loginWrapper">
<Helmet
title="GraphiQL Online with Headers | Built by Hasura"
description="An online version of GraphiQL. Manage headers easily. Test your GraphQL servers."
/>
<h2 className="loginHeading"> Online GraphiQL </h2>
<div className="login">
<div>
<form>
<input
type="text"
id="username"
className="loginTextbox"
placeholder="Enter GraphQL Endpoint URL"
onChange={this.setGraphQLEndpoint.bind(this)}
/>
<button
className="loginButton"
type="submit"
onClick={(e) => {
e.preventDefault();
const urlRegex= /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/;
if (!urlRegex.test(this.state.graphqlEndpoint)) {
// check if localhost url
const localhostRegex = /^http:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[.\w]*)*$/;
if(!localhostRegex.test(this.state.graphqlEndpoint)) {
alert('Please enter a valid URL');
} else {
dispatch(updateGraphQLEndpoint(this.state.graphqlEndpoint));
}
} else {
dispatch(updateGraphQLEndpoint(this.state.graphqlEndpoint));
}
}}
>
<i className={'fa fa-sign-in'} />
</button>
</form>
</div>
</div>
</div>
<div className="footerWrapper">
<div className="built">
Built with <i className="fa fa-heart" /> by <a href={'http://hasura.io/'} target={'_blank'}>Hasura</a>
</div>
<div className="apiHasura">
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphiql-online" target={'_blank'}>
<i className="fa fa-github" />
</a>
</div>
</div>
</div>
);
}
}
LoginComponent.propTypes = {
dispatch: PropTypes.func.isRequired,
};
const generatedLoginComponent = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
};
};
return connect(mapStateToProps)(LoginComponent);
};
export default generatedLoginComponent;

View File

@ -1,613 +0,0 @@
import defaultState from './state';
import requestAction from '../../../utils/requestAction';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { parse } from 'graphql';
import { execute } from 'apollo-link';
import { push } from 'react-router-redux';
import { getHeadersAsJSON } from './utils';
import { saveAppState, clearState } from '../../AppState.js';
const CHANGE_TAB = 'ApiExplorer/CHANGE_TAB';
const CHANGE_API_SELECTION = 'ApiExplorer/CHANGE_API_SELECTION';
const EXPAND_AUTH_API = 'ApiExplorer/EXPAND_AUTH_API';
const CODE_GENERATOR_OPEN = 'ApiExplorer/CODE_GENERATOR_OPEN';
const CODE_GENERATOR_CLOSE = 'ApiExplorer/CODE_GENERATOR_CLOSE';
const CODE_GENERATOR_CHANGE_SELECTION =
'ApiExplorer/CODE_GENERATOR_CHANGE_SELECTION';
const CODE_GENERATOR_COPY_TO_CLIPBOARD =
'ApiExplorer/CODE_GENERATOR_COPY_TO_CLIPBOARD';
const REQUEST_METHOD_CHANGED = 'ApiExplorer/REQUEST_METHOD_CHANGED';
const REQUEST_URL_CHANGED = 'ApiExplorer/REQUEST_URL_CHANGED';
const REQUEST_PARAMS_CHANGED = 'ApiExplorer/REQUEST_PARAMS_CHANGED';
const REQUEST_HEADER_CHANGED = 'ApiExplorer/REQUEST_HEADER_CHANGED';
const REQUEST_HEADER_ADDED = 'ApiExplorer/REQUEST_HEADER_ADDED';
const REQUEST_HEADER_REMOVED = 'ApiExplorer/REQUEST_HEADER_REMOVED';
const SET_INITIAL_HEADER_DATA = 'ApiExplorer/SET_INITIAL_HEADER_DATA';
const MAKING_API_REQUEST = 'ApiExplorer/MAKING_API_REQUEST';
const RESET_MAKING_REQUEST = 'ApiExplorer/RESET_MAKING_REQUEST';
const API_REQUEST_SUCCESS = 'ApiExplorer/API_REQUEST_SUCCESS';
const API_REQUEST_FAILURE = 'ApiExplorer/API_REQUEST_FAILURE';
const CLEAR_HISTORY = 'ApiExplorer/CLEAR_HISTORY';
const UPDATE_FILE_OBJECT = 'ApiExplorer/UPDATE_FILE_OBJECT';
const CREATE_WEBSOCKET_CLIENT = 'ApiExplorer/CREATE_WEBSOCKET_CLIENT';
const FOCUS_ROLE_HEADER = 'ApiExplorer/FOCUS_ROLE_HEADER';
const UNFOCUS_ROLE_HEADER = 'ApiExplorer/UNFOCUS_ROLE_HEADER';
const GRAPHQL_ENDPOINT_CHANGED = 'ApiExplorer/GRAPHQL_ENDPOINT_CHANGED';
const clearHistory = () => {
return {
type: CLEAR_HISTORY,
};
};
const updateGraphQLEndpoint = (endpoint) => {
return (dispatch) => {
dispatch({ type: GRAPHQL_ENDPOINT_CHANGED, data: endpoint });
// set local storage
window.localStorage.setItem('ONLINE_GRAPHIQL_ENDPOINT', endpoint);
dispatch(push('/graphiql'));
};
};
// This method adds a new empty header if no empty header is present
const getChangedHeaders = (headers, changedHeaderDetails) => {
const changedHeaderIndex = changedHeaderDetails.index;
const newHeaders = Object.assign([], headers);
if (newHeaders[changedHeaderIndex].isNewHeader) {
newHeaders[changedHeaderIndex].isNewHeader = false;
newHeaders[changedHeaderIndex].isActive = true;
newHeaders[changedHeaderIndex].isDisabled = false;
}
if (changedHeaderDetails.keyName === 'isActive') {
newHeaders[changedHeaderIndex].isActive = !newHeaders[changedHeaderIndex]
.isActive;
} else {
newHeaders[changedHeaderIndex][changedHeaderDetails.keyName] =
changedHeaderDetails.newValue;
}
newHeaders[changedHeaderIndex].isDisabled =
changedHeaderDetails.isDisabled === true;
const nonEmptyHeaders = newHeaders.filter(header => {
return !header.isNewHeader;
});
nonEmptyHeaders.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
isDisabled: false,
});
return nonEmptyHeaders;
};
const updateFileObject = fileObj => {
return { type: UPDATE_FILE_OBJECT, data: fileObj };
};
const focusHeaderTextbox = () => ({ type: FOCUS_ROLE_HEADER });
const unfocusTypingHeader = () => ({ type: UNFOCUS_ROLE_HEADER });
const copyCodeToClipboard = isCopying => {
return {
type: CODE_GENERATOR_COPY_TO_CLIPBOARD,
data: isCopying,
};
};
const changeCodeGeneratorSelection = newSelection => {
return {
type: CODE_GENERATOR_CHANGE_SELECTION,
data: newSelection,
};
};
const changeRequestMethod = newMethod => {
return {
type: REQUEST_METHOD_CHANGED,
data: newMethod,
};
};
const changeRequestUrl = newUrl => {
return {
type: REQUEST_URL_CHANGED,
data: newUrl,
};
};
const changeRequestParams = newParams => {
return dispatch => {
dispatch({ type: REQUEST_PARAMS_CHANGED, data: newParams });
};
};
const createWsClient = (url, headers) => {
const gqlUrl = new URL(url);
const websocketProtocol = gqlUrl.protocol === 'https:' ? 'wss' : 'ws';
const headersFinal = getHeadersAsJSON(headers);
const graphqlUrl = `${websocketProtocol}://${url.split('//')[1]}`;
const client = new SubscriptionClient(graphqlUrl, {
connectionParams: {
headers: {
...headersFinal,
},
},
reconnect: true,
});
return client;
};
const graphqlSubscriber = (graphQLParams, url, headers) => {
const link = new WebSocketLink(createWsClient(url, headers));
try {
const fetcher = operation => {
operation.query = parse(operation.query);
return execute(link, operation);
};
return fetcher(graphQLParams);
} catch (e) {
return e.json();
}
};
const isSubscription = graphQlParams => {
const queryDoc = parse(graphQlParams.query);
for (const definition of queryDoc.definitions) {
if (definition.kind === 'OperationDefinition') {
const operation = definition.operation;
if (operation === 'subscription') {
return true;
}
}
}
return false;
};
const graphQLFetcherFinal = (graphQLParams, url, headers) => {
if (isSubscription(graphQLParams)) {
return graphqlSubscriber(graphQLParams, url, headers);
}
return fetch(url, {
method: 'POST',
headers: getHeadersAsJSON(headers),
body: JSON.stringify(graphQLParams),
}).then(response => response.json());
};
const setInitialHeaderState = headerObj => {
return {
type: SET_INITIAL_HEADER_DATA,
data: headerObj,
};
};
const changeRequestHeader = (index, key, newValue, isDisabled) => {
return (dispatch, getState) => {
const currentState = getState().apiexplorer;
const updatedHeader = {
index: index,
keyName: key,
newValue: newValue,
isDisabled: isDisabled,
};
const updatedHeaders = getChangedHeaders(
currentState.displayedApi.request.headers,
updatedHeader
);
dispatch({
type: REQUEST_HEADER_CHANGED,
data: updatedHeaders,
});
return Promise.resolve(updatedHeaders);
};
};
const removeRequestHeader = index => {
return (dispatch, getState) => {
const currentState = getState().apiexplorer;
const updatedHeaders = currentState.displayedApi.request.headers.filter(
(header, i) => {
return !(i === index);
}
);
dispatch({
type: REQUEST_HEADER_REMOVED,
data: updatedHeaders,
});
// const { headers } = getState().apiexplorer.displayedApi.request;
return Promise.resolve(updatedHeaders);
};
};
const addRequestHeader = (key, value) => ({
type: REQUEST_HEADER_ADDED,
data: {
key: key,
value: value,
},
});
const generateApiCodeClicked = () => {
return {
type: CODE_GENERATOR_OPEN,
};
};
const closeCodeGeneratorClicked = () => {
return {
type: CODE_GENERATOR_CLOSE,
};
};
const changeTabSelection = newSelectionIndex => {
return {
type: CHANGE_TAB,
data: newSelectionIndex,
};
};
const changeApiSelection = (newSelectedApi, index) => {
return {
type: CHANGE_API_SELECTION,
data: newSelectedApi,
index: index,
};
};
const editGeneratedJson = () => {
return (dispatch, getState) => {
const tabs = getState().apiexplorer.tabs;
if (
tabs[0] &&
tabs[0].content &&
tabs[0].content[0] &&
tabs[0].content[0].content[1]
) {
const newSelectedApi = tabs[0].content[0].content[2];
const existingJson = getState().apiexplorer.displayedApi.request.params;
newSelectedApi.request.params = existingJson;
dispatch(changeApiSelection(newSelectedApi, 'Username-password Login'));
}
};
};
const expandAuthApi = index => {
return {
type: EXPAND_AUTH_API,
data: index,
};
};
// This method adds the new header and moves the empty header to the bottom of the list
const getHeadersAfterAddingNewHeader = (headers, newHeader) => {
const nonEmptyHeaders = headers.filter(header => {
return !header.isNewHeader;
});
nonEmptyHeaders.push(newHeader);
nonEmptyHeaders.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
});
return nonEmptyHeaders;
};
const getStateAfterAddingRequestToHistory = oldState => {
const newState = Object.assign({}, oldState);
// Check if history is present
const isHistoryPresent = newState.tabs[1].content.length > 0;
if (!isHistoryPresent) {
newState.tabs[1].content.push(
{
title: 'Clear History',
content: [],
},
{
title: '',
content: [],
}
);
}
const index = newState.tabs[1].content[1].content.length;
const newRequest = {
id: 'History-' + index,
details: {
title: newState.displayedApi.details.title,
description: '',
},
request: newState.displayedApi.request,
};
newState.tabs[1].content[1].content.unshift(newRequest);
// Saving the state to the local storage
saveAppState(newState.tabs[1]);
return newState;
};
const getStateAfterClearingHistory = state => {
clearState();
return {
...state,
tabs: [
state.tabs[0],
{
...state.tabs[1],
content: [],
},
],
};
};
const getRemoteQueries = (queryUrl, cb) => {
fetch(queryUrl)
.then(resp => resp.text().then(cb))
.catch(e => console.error('Invalid query file URL: ', e));
};
const apiExplorerReducer = (state = defaultState, action) => {
switch (action.type) {
case CHANGE_TAB:
return {
...state,
currentTab: action.data,
};
case CHANGE_API_SELECTION:
return {
...state,
displayedApi: action.data,
explorerData: {
...state.explorerData,
response: {},
},
};
case EXPAND_AUTH_API:
return {
...state,
authApiExpanded: action.data,
};
case CODE_GENERATOR_OPEN:
return {
...state,
modalState: {
...state.modalState,
isOpen: true,
},
};
case CODE_GENERATOR_CLOSE:
return {
...state,
modalState: {
...state.modalState,
isOpen: false,
},
};
case CODE_GENERATOR_CHANGE_SELECTION:
return {
...state,
modalState: {
...state.modalState,
selectedCodeGen: action.data,
},
};
case CODE_GENERATOR_COPY_TO_CLIPBOARD:
return {
...state,
modalState: {
...state.modalState,
isCopied: action.data,
},
};
case REQUEST_METHOD_CHANGED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
method: action.data,
},
},
};
case REQUEST_URL_CHANGED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
url: action.data,
},
},
};
case REQUEST_PARAMS_CHANGED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
params: action.data,
},
},
};
case REQUEST_HEADER_CHANGED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: [...action.data],
},
},
};
case SET_INITIAL_HEADER_DATA:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: [...action.data],
},
},
};
case REQUEST_HEADER_ADDED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: getHeadersAfterAddingNewHeader(
state.displayedApi.request.headers,
{
key: action.data.key,
value: action.data.value,
isActive: true,
isNewHeader: false,
}
),
},
},
};
case MAKING_API_REQUEST:
return {
...state,
explorerData: {
...state.explorerData,
sendingRequest: true,
enableResponseSection: false,
response: {},
},
};
case API_REQUEST_SUCCESS:
const newState = getStateAfterAddingRequestToHistory(state);
return {
...newState,
explorerData: {
...newState.explorerData,
sendingRequest: false,
enableResponseSection: true,
response: action.data,
},
};
case API_REQUEST_FAILURE:
const newState2 = getStateAfterAddingRequestToHistory(state);
return {
...newState2,
explorerData: {
...newState2.explorerData,
sendingRequest: false,
enableResponseSection: true,
response: action.data,
},
};
case REQUEST_HEADER_REMOVED:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: [...action.data],
},
},
};
case CLEAR_HISTORY:
return { ...state, ...getStateAfterClearingHistory(state) };
case UPDATE_FILE_OBJECT:
return {
...state,
explorerData: {
...state.explorerData,
fileObj: action.data,
},
};
case RESET_MAKING_REQUEST:
return {
...state,
explorerData: {
...state.explorerData,
sendingRequest: false,
enableResponseSection: false,
},
};
case CREATE_WEBSOCKET_CLIENT:
return {
...state,
webSocketClient: action.data,
};
case UNFOCUS_ROLE_HEADER:
return {
...state,
headerFocus: false,
};
case FOCUS_ROLE_HEADER:
return {
...state,
headerFocus: true,
};
case GRAPHQL_ENDPOINT_CHANGED:
return {
...state,
graphqlEndpoint: action.data,
};
default:
return state;
}
};
export default apiExplorerReducer;
export {
changeTabSelection,
changeApiSelection,
expandAuthApi,
generateApiCodeClicked,
closeCodeGeneratorClicked,
changeCodeGeneratorSelection,
copyCodeToClipboard,
changeRequestMethod,
changeRequestUrl,
changeRequestParams,
changeRequestHeader,
addRequestHeader,
removeRequestHeader,
clearHistory,
updateFileObject,
editGeneratedJson,
graphQLFetcherFinal,
createWsClient,
focusHeaderTextbox,
unfocusTypingHeader,
getRemoteQueries,
setInitialHeaderState,
updateGraphQLEndpoint,
};

View File

@ -1,81 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import ApiRequestWrapper from './ApiRequestWrapper';
class ApiExplorer extends Component {
componentDidMount() {
let localStorageUrl;
if (window.__env.graphqlEndpoint && window.__env.graphqlEndpoint !== 'undefined') {
localStorageUrl = window.__env.graphqlEndpoint;
} else {
localStorageUrl = window.localStorage.getItem('ONLINE_GRAPHIQL_ENDPOINT');
}
if (!this.props.graphqlEndpoint && (localStorageUrl === 'undefined' || localStorageUrl === null)) {
this.props.dispatch(push('/'));
}
}
render() {
const {
displayedApi,
credentials,
explorerData,
route,
dataHeaders,
tables,
headerFocus,
location,
serverVersion,
serverConfig,
} = this.props;
const styles = require('./ApiExplorer.scss');
const consoleUrl = window.location.protocol + '//' + window.location.host;
let localStorageUrl;
if (window.__env.graphqlEndpoint && window.__env.graphqlEndpoint !== 'undefined') {
localStorageUrl = window.__env.graphqlEndpoint;
} else {
localStorageUrl = window.localStorage.getItem('ONLINE_GRAPHIQL_ENDPOINT');
}
return (
<div className={'container-fluid ' + styles.padd_remove}>
<Helmet title="API Explorer | Hasura" />
<div className={styles.apiExplorerWrapper}>
<ApiRequestWrapper
dispatch={this.props.dispatch}
credentials={credentials}
explorerData={explorerData}
details={displayedApi.details}
request={displayedApi.request}
route={route}
dataHeaders={dataHeaders}
numberOfTables={0}
headerFocus={headerFocus}
queryParams={this.props.location.query}
graphqlEndpoint={localStorageUrl}
urlParams={location.query}
serverVersion={serverVersion}
consoleUrl={consoleUrl}
serverConfig={serverConfig}
/>
</div>
</div>
);
}
}
ApiExplorer.propTypes = {
modalState: PropTypes.object.isRequired,
displayedApi: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
route: PropTypes.object.isRequired,
tables: PropTypes.array.isRequired,
headerFocus: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired,
};
export default ApiExplorer;

View File

@ -1,867 +0,0 @@
@import "../../Common/Common.scss";
.display_inl {
display: inline-block;
}
.analyzerBearerModal {
width: 768px;
}
.analyzerLabel {
text-transform: uppercase;
margin: 0;
font-size: 12px;
color: #000;
zoom: 1;
border-bottom: 1px solid rgba(155,155,155,0.5);
line-height: 2.5;
padding-bottom: 2px;
.token_validity {
font-size: 14px;
.invalid_jwt_icon {
// cursor: pointer;
color: #dc3545;
}
.valid_jwt_token {
// cursor: pointer;
color: #28a745;
}
}
span {
color: #979797;
margin-left: 5px;
display: inline-block;
}
}
.jwt_verification_fail_message {
background-color: #e53935;
padding: 10px 10px;
color: #ffffff;
border-radius: 5px;
}
.width_80 {
width: 80%;
}
.responseHeader {
color: #788095;
font-weight: bold;
font-size: 14px;
}
.admin_token_align {
vertical-align: middle;
margin-left: 2px;
}
.marginBottom {
margin-bottom: 15px;
}
.apiExplorerMini {
width: 80%;
}
.wrapperOnBoarding {
// width: 80% !important;
}
.panelGreyed {
opacity: 0.1;
}
.requestGreyed {
opacity: 0.1;
}
.panelInFocus {
}
.requestInFocus {
}
.cursorNotAllowed {
cursor: not-allowed;
}
.apiExplorerWrapper {
display: flex;
height: $mainContainerHeight;
.ApiRequestWrapperVH {
height: 100%;
width: 100%;
}
.apiCollectionWrapper {
background-color: #fff;
height: 100%;
overflow-y: auto;
// Changed it from overfllow-y: scroll
.apiCollectionTabWrapper {
.apiCollectionTab {
-webkit-padding-start: 0px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
-moz-padding-start: 0px;
-moz-margin-before: 0;
-moz-margin-after: 0;
border-bottom: 1px solid #D8D8D8;
position: absolute;
width: 20%;
min-width: 240px;
background-color: #fff;
z-index: 10;
.apiCollectionTabList {
list-style-type: none;
display: inline-block;
width: 50%;
text-align: center;
padding: 10px 11px;
font-weight: bold;
font-size: 16px;
color: #D8D8D8;
cursor: pointer;
}
.activeApiCollectionTab {
border-bottom: 3px solid #ffca27;
color: #6B6B6B;
}
.apiCollectionTabList:focus {
outline: none;
}
}
.apiCollectionClearHistory {
padding: 0 15px;
margin: 0;
border-bottom: 1px solid #e5e5e5;
/*
position: absolute;
bottom: 0;
transform: translateX(-88%);
*/
.apiCollectionClearHistoryButton {
/*
float: right;
margin: 10px 0;
cursor: pointer;
*/
margin: 10px 0;
cursor: pointer;
position: fixed;
bottom: 15px;
text-align: center;
width: 20%;
margin-left: -15px;
padding-top: 10px;
border-top: 1px solid #ccc;
background-color: #fff;
z-index: 1;
i {
padding-right: 5px;
}
}
}
.apiPaddTop {
padding-top: 46px !important;
}
.apiCollectionTabListDetails {
padding: 0 15px;
margin: 10px 0;
// padding-top: 46px;
.apiCollectionTabListHead {
padding-left: 15px;
padding-bottom: 10px;
font-weight: bold;
font-size: 15px;
.serviceBaseDomain {
color: #bbb;
}
}
.add_ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.apiCollectionGetPost {
.apiCollectionGetWrapper {
padding: 5px 0;
cursor: pointer;
.apiCollectionGet {
text-align: left;
color: #70CD00;
font-size: 12px;
font-weight: bold;
padding-left: 15px;
}
.apiCollectionGetDetailsWrapper {
display: flex;
align-items: center;
.apiCollectionGetDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiCollectionPostDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiRightArrowWrapper {
padding-left: 5px;
}
}
.activeApiCollectionGetWrapperIcon {
display: none;
}
}
.activeApiCollectionGetWrapper {
background-color: #FFF3D5;
border-radius: 4px;
.activeApiCollectionGetWrapperIcon {
display: block;
}
}
.apiCollectionPostWrapper {
padding: 5px 0;
cursor: pointer;
.apiCollectionPost {
text-align: left;
color: #FD9540;
font-size: 12px;
font-weight: bold;
padding-left: 15px;
}
.apiCollectionGetDetailsWrapper {
display: flex;
align-items: center;
.apiCollectionGetDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiCollectionPostDetails {
word-wrap: break-word;
padding-right: 10px !important
}
.apiRightArrowWrapper {
padding-left: 5px;
}
}
.activeApiCollectionGetWrapperIcon {
display: none;
}
}
.activeApiCollectionGetWrapper {
background-color: #FFF3D5;
border-radius: 4px;
.activeApiCollectionGetWrapperIcon {
display: block;
}
}
}
}
}
}
.apiContentPadd {
padding-top: 20px;
border-color: rgba(23, 42, 58, .1);
border-width: 2px;
border-bottom-style: solid;
padding-bottom: 10px;
width: 100%;
float: left;
}
.closeHeader {
cursor: pointer;
font-size: 16px;
float: right;
display: inline-block;
}
.showAdminSecret, .showInspector {
cursor: pointer;
padding-right: 8px;
font-size: 16px;
float: left;
display: inline-block;
}
.showInspectorLoading {
cursor: pointer;
font-size: 16px;
float: left;
display: inline-block;
}
.apiRequestWrapper {
margin-bottom: 5px;
.headerWrapper {
margin-bottom: 20px;
}
.file_upload_wrapper {
width: 100%;
text-align: left;
display: inline-block;
padding: 20px;
border: 1px solid #ccc;
background-color: #fff;
margin-bottom: 15px;
input[type="file"] {
display: inline-block;
width: 162px;
}
}
.apiRequestheader {
font-weight: bold;
font-size: 18px;
padding-bottom: 10px;
color: #000;
}
.apiHasura {
font-size: 14px;
text-align: right;
i {
color: #757575;;
font-size: 22px;
position: absolute;
right: 15px;
top: 15px;
}
i:hover {
color: #000;
}
}
.built {
text-align: right;
font-size: 14px;
position: absolute;
right: 40px;
top: 16px;
i {
color: #f93c18;
}
}
.changeEndpoint {
padding: 10px 10px !important;
margin-left: 10px;
}
.apiRequestContent {
font-size: 14px;
font-weight: bold;
a {
color: #FEC53D;
text-decoration: underline;
}
a:hover {
color: #FEC53D;
text-decoration: underline;
}
code {
background-color: transparent;
border: 1px solid #767E93;
padding: 1px 4px !important;
color: #767E93;
}
}
.apiPostRequestWrapper {
// padding: 20px 0;
// padding-top: 20px;
padding-top: 5px;
background-color: #f8fafb;
.inputGroupWrapper {
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
border-radius: 5px;
.inputGroupBtn {
button {
width: 100px;
border: 0;
padding: 10px 12px;
background-color: #F9F9F9;
color: #FD9540;
font-size: 14px;
font-weight: bold;
text-align: left;
.caret {
position: absolute;
right: 10px;
top: 16px;
}
}
}
.inputGroupInput {
border: 0;
box-shadow: none;
padding: 10px 12px;
height: auto;
background-color: #fff;
}
}
.sendBtn {
button {
width: 100%;
text-align: center;
height: 39px;
color: #606060;
font-weight: bold;
border-radius: 5px;
background-color: #FEC53D;
border: 1px solid #FEC53D;
/*
background-color: #FFCA27;
border: 1px solid #FFCA27;
*/
&:hover {
background-color: #F2B130;
}
}
/*
button:hover {
border: 1px solid #F2B130;
background-color: #F2B130;
}
*/
}
.generateBtn {
button {
width: 100%;
background-color: transparent;
text-align: center;
height: 39px;
border: 1px solid #606060;
color: #606060;
font-weight: bold;
border-radius: 5px;
}
button:hover {
border: 1px solid #606060;
background-color: #efefef;
}
}
}
.responseWrapper {
clear: both;
display: flex;
align-items: center;
.responseHeader {
color: #788095;
font-weight: bold;
font-size: 14px;
.viewDetails {
padding-left: 10px;
font-weight: normal;
color: #FFCA27;
}
.addAdminToken {
text-align: right;
padding-left: 15px;
}
}
.addAdminToken {
text-align: right;
padding-left: 15px;
}
}
}
.apiResponseWrapper {
.apiResponseheaderWrapper {
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
.apiResponseheader {
font-weight: bold;
font-size: 14px;
padding-bottom: 10px;
// color: #000;
color: #788095
}
.statusDetails {
display: inline-block;
float: right;
padding-left: 20px;
.statusView {
padding-left: 5px;
font-weight: normal;
color: #FFCA27;
}
}
}
.helpTextWrapper {
padding: 15px;
border: 1px solid #ccc;
background-color: #fff;
clear: both;
margin-bottom: 20px;
i {
padding-right: 10px;
}
pre {
margin-top: 10px;
border-radius: 0;
}
.copyBtn {
padding: 9px;
}
}
.suggestionTextColor {
color: #111;
background-color: #ffd760;
border-color: #ffd760;
}
.noResponseWrapper {
width: 100%;
min-height: 200px;
background-color: #fff;
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
clear: both;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
.noResponseContainer {
width: 325px;
.noResponseHeader {
font-size: 18px;
opacity: 0.6;
}
.barWrapper {
padding-top: 15px;
text-align: center;
.bigBar {
width: 56%;
margin-right: 7px;
height: 20px;
background-color: #EEEEEE;
display: inline-block;
border-radius: 4px;
}
.mediumBar {
width: 23%;
margin-right: 7px;
height: 20px;
background-color: #FFCA27;
display: inline-block;
border-radius: 4px;
}
.smallBar {
width: 13%;
margin-right: 7px;
height: 20px;
background-color: #EEEEEE;
display: inline-block;
border-radius: 4px;
}
}
}
}
.responseHeader {
padding-top: 15px;
color: #788095;
font-weight: bold;
font-size: 14px;
clear: both;
.viewDetails {
padding-left: 10px;
font-weight: normal;
color: #FFCA27;
}
}
}
}
// Common
.responseTable {
padding-top: 15px;
.tableBorder {
background-color: #fff;
border: 1px solid #E3E5E5;
thead {
tr {
th {
border-bottom: 0px;
}
}
}
tbody {
tr {
td {
border-top: 0;
// padding: 5px;
padding: 0px 5px;
min-width: 50px;
vertical-align: middle;
.responseTableInput {
background-color: transparent;
border: 0;
box-shadow: none;
border-radius: 0;
// padding: 0;
}
}
.headerPadd {
padding: 15px !important;
}
.borderTop {
border-top: 1px solid #ccc;
}
.tableTdLeft {
padding-left: 5%;
}
.tableEnterKey {
// padding: 10px 0;
padding: 0px 5px;
padding-left: 4.5%;
}
.tableLastTd {
padding-left: 5px !important;
}
}
}
}
.headerHeading {
background-color: #f5f5f5;
font-weight: bold;
padding-left: 15px;
}
}
.queryBuilderWrapper {
padding-bottom: 20px;
.queryBuilderTab {
ul {
border: 1px solid #E7E7E7;
-webkit-padding-start: 0px;
-moz-padding-start: 0px;
display: inline-block;
li {
list-style-type: none;
display: inline-block;
padding: 12px 20px;
width: 150px;
text-align: center;
color: #788094;
cursor: pointer;
background-color: #fff;
font-weight: bold;
}
li:focus {
outline: none;
}
.activeQueryBuilderTab {
background-color: #FFF050;
}
}
}
}
.AceEditorWrapper {
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
/*
margin-top: 15px;
margin-bottom: 20px;
margin-top: 20px;
*/
}
.queryBuilderLayout {
background-color: #fff;
// padding: 15px;
}
.queryBuilderLayoutSub {
padding: 20px;
}
.qbTabHeading {
font-weight: bold;
padding-top: 15px;
padding-left: 20px;
padding-bottom: 15px;
i {
padding-left: 5px;
}
}
.common_checkbox {
opacity: 0;
position: absolute;
.common_checkbox_label {
display: inline-block;
vertical-align: middle;
margin: 0px;
cursor: pointer;
position: relative;
}
}
.common_checkbox_label {
margin-bottom: 0px !important;
padding-top: 0px;
display: flex;
}
.common_checkbox + .common_checkbox_label:before {
content: '';
background: #fff;
border: 1px solid #ddd;
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
padding-top: 2px;
margin-right: 2px;
text-align: center;
border-radius:4px;
cursor:pointer;
}
label {
font-weight: normal;
}
.common_checkbox:checked + .common_checkbox_label:before {
content: url('./tick.png');
background: #FFCA27;
color: #fff;
padding-top:0px;
}
.authPanelSubHeadings {
font-size: 16px;
font-weight: italic;
padding-left: 15px;
padding-bottom: 10px;
}
.apiResponseTab {
padding-top: 20px;
.apiResponseTabUl {
display: inline-block;
-webkit-padding-start: 0px;
-webkit-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
.apiResponseTabList {
display: inline-block;
padding: 15px 25px;
list-style-type: none;
background-color: #fff;
font-weight: 600;
color: #6B6B6B;
cursor: pointer;
}
.apiResponseTabList:focus {
outline: none;
}
.activeApiResponseTab {
background-color: #FFF3D5
}
}
.apiResponseTabPanel {
.AceEditorWrapper {
margin-top: 15px;
margin-bottom: 15px;
}
}
}

View File

@ -1,13 +0,0 @@
import ApiExplorer from './ApiExplorer';
const generatedApiExplorer = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
credentials: {},
};
};
return connect(mapStateToProps)(ApiExplorer);
};
export default generatedApiExplorer;

View File

@ -1,590 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import jwt from 'jsonwebtoken';
import { push } from 'react-router-redux';
import TextAreaWithCopy from '../../../Common/TextAreaWithCopy/TextAreaWithCopy';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import ModalWrapper from '../../../Common/ModalWrapper';
import { parseAuthHeader } from './utils';
import {
changeRequestHeader,
removeRequestHeader,
focusHeaderTextbox,
unfocusTypingHeader,
setInitialHeaderState,
} from '../Actions';
import GraphiQLWrapper from '../GraphiQLWrapper/GraphiQLWrapper';
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
import {
getEndPointSectionIsOpen,
setEndPointSectionIsOpen,
getHeadersSectionIsOpen,
setHeadersSectionIsOpen,
getGraphiQLHeadersFromLocalStorage,
setGraphiQLHeadersInLocalStorage,
} from './utils';
import styles from '../ApiExplorer.scss';
const inspectJWTTooltip = (
<Tooltip id="tooltip-inspect-jwt">Decode JWT</Tooltip>
);
const jwtValidityStatus = message => (
<Tooltip id="tooltip-jwt-validity-status">{message}</Tooltip>
);
/* When the page is loaded for the first time, hydrate the header state from the localStorage
* Keep syncing the localStorage state when user modifies.
* */
class ApiRequest extends Component {
constructor(props) {
super(props);
this.state = {
endpointSectionIsOpen: getEndPointSectionIsOpen(),
headersSectionIsOpen: getHeadersSectionIsOpen(),
isAnalyzingToken: false,
analyzingHeaderRow: -1,
tokenInfo: {
header: {},
payload: {},
error: null,
serverResp: {},
},
};
if (this.props.numberOfTables !== 0) {
const graphqlQueryInLS = window.localStorage.getItem('graphiql:query');
if (graphqlQueryInLS && graphqlQueryInLS.indexOf('do not have') !== -1) {
window.localStorage.removeItem('graphiql:query');
}
}
this.analyzeBearerToken = this.analyzeBearerToken.bind(this);
this.onAnalyzeBearerClose = this.onAnalyzeBearerClose.bind(this);
}
componentDidMount() {
const { headers } = this.props;
const HEADER_FROM_LS = getGraphiQLHeadersFromLocalStorage();
if (HEADER_FROM_LS) {
try {
const initialHeader = JSON.parse(HEADER_FROM_LS);
this.props.dispatch(setInitialHeaderState(initialHeader));
} catch (e) {
console.error(e);
setGraphiQLHeadersInLocalStorage(JSON.stringify(headers));
}
} else {
setGraphiQLHeadersInLocalStorage(JSON.stringify(headers));
}
}
onAnalyzeBearerClose() {
this.setState({
isAnalyzingToken: false,
analyzingHeaderRow: -1,
});
}
changeEndpoint() {
this.props.dispatch(push('/'));
}
analyzeBearerToken(e) {
const { dispatch } = this.props;
const token = e.target.getAttribute('token');
const analyzingHeaderRow = parseInt(
e.target.getAttribute('data-header-index'),
10
);
this.setState({
isAnalyzingToken: true,
analyzingHeaderRow,
tokenInfo: {
...this.state.tokenInfo,
serverResp: {},
error: null,
},
});
const decodeAndSetState = serverResp => {
const decoded = jwt.decode(token, { complete: true });
if (decoded) {
this.setState({
tokenInfo: {
...this.state.tokenInfo,
header: decoded.header,
payload: decoded.payload,
error: null,
serverResp: serverResp,
},
});
} else {
const message =
'This JWT seems to be invalid. Please check the token value and try again!';
this.setState({
tokenInfo: {
...this.state.tokenInfo,
error: message,
serverResp: serverResp,
},
});
}
};
}
render() {
const { isAnalyzingToken, tokenInfo, analyzingHeaderRow } = this.state;
const {
error: tokenAnalyzeError,
serverResp: tokenAnalyzeResp,
} = tokenInfo;
const getGraphQLEndpointBar = () => {
const { endpointSectionIsOpen } = this.state;
const toggleHandler = () => {
const newIsOpen = !endpointSectionIsOpen;
setEndPointSectionIsOpen(newIsOpen);
this.setState({ endpointSectionIsOpen: newIsOpen });
};
return (
<CollapsibleToggle
title={'GraphQL Endpoint'}
isOpen={endpointSectionIsOpen}
toggleHandler={toggleHandler}
useDefaultTitleStyle
>
<div
id="stickyHeader"
className={
styles.apiPostRequestWrapper +
' ' +
styles.wd100 +
' ' +
styles.stickyHeader
}
>
<div className={'col-xs-11 ' + styles.padd_remove}>
<div
className={
'input-group ' +
styles.inputGroupWrapper +
' ' +
styles.cursorNotAllowed
}
>
<div className={'input-group-btn ' + styles.inputGroupBtn}>
<button type="button" className={'btn btn-default'}>
{this.props.method}
</button>
</div>
<input
onChange={this.onUrlChanged}
value={this.props.url || ''}
type="text"
readOnly
className={styles.inputGroupInput + ' form-control '}
/>
</div>
</div>
<div className={'col-xs-1 ' + styles.padd_remove}>
<button onClick={this.changeEndpoint.bind(this)} className={styles.changeEndpoint + ' btn btn-sm btn-small btn-info'}>
Change Endpoint
</button>
</div>
<div className={styles.stickySeparator} />
</div>
</CollapsibleToggle>
);
};
const getHeaderTable = () => {
const { headersSectionIsOpen } = this.state;
const getHeaderRows = () => {
const headers = this.props.headers;
const handleFocus = () => {
this.props.dispatch(focusHeaderTextbox());
};
const handleBlur = () => {
this.props.dispatch(unfocusTypingHeader());
};
const onDeleteHeaderClicked = e => {
const index = parseInt(e.target.getAttribute('data-header-id'), 10);
this.props
.dispatch(removeRequestHeader(index))
.then(r => setGraphiQLHeadersInLocalStorage(JSON.stringify(r)));
};
const onHeaderValueChanged = e => {
const index = parseInt(e.target.getAttribute('data-header-id'), 10);
const key = e.target.getAttribute('data-element-name');
const newValue = e.target.value;
this.props
.dispatch(changeRequestHeader(index, key, newValue, false))
.then(r => setGraphiQLHeadersInLocalStorage(JSON.stringify(r)));
};
return headers.map((header, i) => {
const getHeaderActiveCheckBox = () => {
let headerActiveCheckbox = null;
if (!header.isNewHeader) {
headerActiveCheckbox = (
<td>
<input
type="checkbox"
name="sponsored"
className={styles.common_checkbox + ' common_checkbox'}
id={i + 1}
checked={header.isActive}
data-header-id={i}
onChange={onHeaderValueChanged}
data-element-name="isActive"
/>
<label
htmlFor={i + 1}
className={
styles.common_checkbox_label + ' common_checkbox_label'
}
/>
</td>
);
}
return headerActiveCheckbox;
};
const getColSpan = () => {
return header.isNewHeader ? '2' : '1';
};
const getHeaderKey = () => {
let className = '';
if (header.isNewHeader) {
className =
styles.border_right +
' ' +
styles.tableTdLeft +
' ' +
styles.borderTop +
' ' +
styles.tableEnterKey;
} else {
className = styles.border_right;
}
return (
<td colSpan={getColSpan()} className={className}>
<input
className={'form-control ' + styles.responseTableInput}
value={header.key || ''}
disabled={header.isDisabled === true}
data-header-id={i}
placeholder="Enter Key"
data-element-name="key"
onChange={onHeaderValueChanged}
onFocus={handleFocus}
onBlur={handleBlur}
type="text"
data-test={`header-key-${i}`}
/>
</td>
);
};
const getHeaderValue = () => {
let className = '';
if (header.isNewHeader) {
className =
styles.borderTop +
' ' +
styles.tableEnterKey +
' ' +
styles.tableLastTd;
}
let type = 'text';
return (
<td colSpan={getColSpan()} className={className}>
<input
className={'form-control ' + styles.responseTableInput}
value={header.value || ''}
disabled={header.isDisabled === true}
data-header-id={i}
placeholder="Enter Value"
data-element-name="value"
onChange={onHeaderValueChanged}
onFocus={handleFocus}
onBlur={handleBlur}
data-test={`header-value-${i}`}
type={type}
/>
</td>
);
};
const getHeaderAdminVal = () => {
let headerAdminVal = null;
return headerAdminVal;
};
const getHeaderRemoveBtn = () => {
return (
<i
className={styles.closeHeader + ' fa fa-times'}
data-header-id={i}
aria-hidden="true"
onClick={onDeleteHeaderClicked}
/>
);
};
const getHeaderActions = () => {
let headerActions = null;
if (!header.isNewHeader) {
headerActions = (
<td>
{getHeaderAdminVal()}
{getHeaderRemoveBtn()}
</td>
);
}
return headerActions;
};
return (
<tr key={i}>
{getHeaderActiveCheckBox()}
{getHeaderKey()}
{getHeaderValue()}
{getHeaderActions()}
</tr>
);
});
};
const toggleHandler = () => {
const newIsOpen = !headersSectionIsOpen;
setHeadersSectionIsOpen(newIsOpen);
this.setState({ headersSectionIsOpen: newIsOpen });
};
return (
<CollapsibleToggle
title={'Request Headers'}
testId="api-explorer-header"
isOpen={headersSectionIsOpen}
toggleHandler={toggleHandler}
useDefaultTitleStyle
>
<div className={styles.responseTable + ' ' + styles.remove_all_pad}>
<table className={'table ' + styles.tableBorder}>
<thead>
<tr>
<th className={styles.wd4 + ' ' + styles.headerHeading} />
<th
className={
styles.wd48 +
' ' +
styles.border_right +
' ' +
styles.headerHeading
}
>
Key
</th>
<th className={styles.wd48 + ' ' + styles.headerHeading}>
Value
</th>
<th className={styles.wd4 + ' ' + styles.headerHeading} />
</tr>
</thead>
<tbody>{getHeaderRows()}</tbody>
</table>
</div>
</CollapsibleToggle>
);
};
const getRequestBody = () => {
switch (this.props.bodyType) {
case 'graphql':
return (
<div className={styles.add_mar_top}>
<GraphiQLWrapper
data={this.props}
numberOfTables={this.props.numberOfTables}
dispatch={this.props.dispatch}
headerFocus={this.props.headerFocus}
urlParams={this.props.urlParams}
/>
</div>
);
default:
return '';
}
};
const getAnalyzeTokenModal = () => {
const getAnalyzeBearerBody = () => {
const {
claims_namespace: claimNameSpace = 'https://hasura.io/jwt/claims',
claims_format: claimFormat = 'json',
} =
this.props.serverConfig && this.props.serverConfig.jwt
? this.props.serverConfig.jwt
: {};
let tokenVerified = false;
let JWTError = '';
if (tokenAnalyzeResp && 'errors' in tokenAnalyzeResp) {
try {
JWTError =
tokenAnalyzeResp.errors.length > 0
? tokenAnalyzeResp.errors[0].message
: null;
} catch (e) {
JWTError = e.toString();
}
} else {
tokenVerified = true;
}
const generateJWTVerificationStatus = () => {
switch (true) {
case tokenVerified:
return (
<OverlayTrigger
placement="top"
overlay={jwtValidityStatus('Valid JWT token')}
>
<span className={styles.valid_jwt_token}>
<i className="fa fa-check" />
</span>
</OverlayTrigger>
);
case !tokenVerified && JWTError.length > 0:
return (
<span className={styles.invalid_jwt_icon}>
<i className="fa fa-times" />
</span>
);
default:
return null;
}
};
let analyzeBearerBody;
if (tokenAnalyzeError) {
analyzeBearerBody = <span>{tokenAnalyzeError}</span>;
} else {
analyzeBearerBody = (
<div>
<span className={styles.analyzerLabel}>
Token Validity:
<span className={styles.token_validity}>
{generateJWTVerificationStatus()}
</span>
</span>
<span className={styles.analyzerLabel}>
Header:
<span>Algorithm & Token Type</span>
</span>
<TextAreaWithCopy
copyText={JSON.stringify(tokenInfo.header, null, 2)}
textLanguage={'json'}
id="headerCopy"
containerId="headerCopyBlock"
/>
<br />
<span className={styles.analyzerLabel}>
Full Payload:
<span>Data</span>
</span>
<TextAreaWithCopy
copyText={JSON.stringify(tokenInfo.payload, null, 2)}
textLanguage={'json'}
id="payloadCopy"
containerId="payloadCopyBlock"
/>
</div>
);
}
return analyzeBearerBody;
};
return (
<ModalWrapper
show={
isAnalyzingToken &&
tokenAnalyzeResp &&
Object.keys(tokenAnalyzeResp).length > 0
}
onHide={this.onAnalyzeBearerClose}
dialogClassName={styles.analyzerBearerModal}
title={tokenAnalyzeError ? 'Error decoding JWT' : 'Decoded JWT'}
>
{getAnalyzeBearerBody()}
</ModalWrapper>
);
};
return (
<div className={styles.apiRequestWrapper}>
{getGraphQLEndpointBar()}
{getHeaderTable()}
{getRequestBody()}
</div>
);
}
}
ApiRequest.propTypes = {
method: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
headers: PropTypes.array,
params: PropTypes.string,
dispatch: PropTypes.func.isRequired,
explorerData: PropTypes.object.isRequired,
credentials: PropTypes.object.isRequired,
bodyType: PropTypes.string.isRequired,
route: PropTypes.object,
numberOfTables: PropTypes.number.isRequired,
headerFocus: PropTypes.bool.isRequired,
urlParams: PropTypes.object.isRequired,
consoleUrl: PropTypes.string.isRequired,
};
export default ApiRequest;

View File

@ -1,28 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ApiRequestDetails extends Component {
render() {
const styles = require('../ApiExplorer.scss');
return (
<div className={styles.apiRequestWrapper + ' ' + styles.apiContentPadd}>
<div className={styles.apiRequestContent}>{this.props.description}</div>
<div className={styles.built}>
Built with <i className="fa fa-heart" /> by <a href={'http://hasura.io/'} target={'_blank'}>Hasura</a>
</div>
<div className={styles.apiHasura}>
<a href="https://github.com/hasura/graphql-engine/tree/master/community/tools/graphiql-online" target={'_blank'}>
<i className="fa fa-github" />
</a>
</div>
</div>
);
}
}
ApiRequestDetails.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
};
export default ApiRequestDetails;

View File

@ -1,51 +0,0 @@
export const setEndPointSectionIsOpen = isOpen => {
window.localStorage.setItem('ApiExplorer:EndpointSectionIsOpen', isOpen);
};
export const getEndPointSectionIsOpen = () => {
const defaultIsOpen = true;
const isOpen = window.localStorage.getItem(
'ApiExplorer:EndpointSectionIsOpen'
);
return isOpen ? isOpen === 'true' : defaultIsOpen;
};
export const setHeadersSectionIsOpen = isOpen => {
window.localStorage.setItem('ApiExplorer:HeadersSectionIsOpen', isOpen);
};
export const getHeadersSectionIsOpen = () => {
const defaultIsOpen = true;
const isOpen = window.localStorage.getItem(
'ApiExplorer:HeadersSectionIsOpen'
);
return isOpen ? isOpen === 'true' : defaultIsOpen;
};
export const setGraphiQLHeadersInLocalStorage = headers => {
window.localStorage.setItem('HASURA_CONSOLE_GRAPHIQL_HEADERS', headers);
};
export const getGraphiQLHeadersFromLocalStorage = () => {
return window.localStorage.getItem('HASURA_CONSOLE_GRAPHIQL_HEADERS');
};
export const parseAuthHeader = header => {
let isAuthHeader = false;
let token = null;
if (header.key.toLowerCase() === 'authorization') {
const parseBearer = /^(Bearer) (.*)/gm;
const matches = parseBearer.exec(header.value);
if (matches) {
isAuthHeader = true;
token = matches[2];
}
}
return { isAuthHeader, token };
};

View File

@ -1,75 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ApiRequest from './ApiRequest/ApiRequest';
import ApiResponse from './ApiResponse/ApiResponse';
import ApiRequestDetails from './ApiRequest/ApiRequestDetails';
class ApiRequestWrapper extends Component {
render() {
const styles = require('./ApiExplorer.scss');
const getAPIRequestDetailsSection = () => {
return (
<ApiRequestDetails
title={this.props.details.title}
description={this.props.details.description}
/>
);
return null;
};
return (
<div
id="apiRequestBlock"
className={
styles.padd_left +
' ' +
styles.padd_right +
' ' +
styles.ApiRequestWrapperVH
}
>
{getAPIRequestDetailsSection()}
<ApiRequest
bodyType={
this.props.request.bodyType ? this.props.request.bodyType : ''
}
credentials={this.props.credentials}
method={this.props.request.method}
url={this.props.graphqlEndpoint}
headers={this.props.request.headers}
params={this.props.request.params}
explorerData={this.props.explorerData}
dispatch={this.props.dispatch}
dataHeaders={this.props.dataHeaders}
numberOfTables={this.props.numberOfTables}
headerFocus={this.props.headerFocus}
urlParams={this.props.urlParams}
serverVersion={this.props.serverVersion}
consoleUrl={this.props.consoleUrl}
serverConfig={this.props.serverConfig}
queryParams={this.props.queryParams}
/>
</div>
);
}
}
ApiRequestWrapper.propTypes = {
details: PropTypes.object.isRequired,
request: PropTypes.object.isRequired,
explorerData: PropTypes.object.isRequired,
credentials: PropTypes.object.isRequired,
bodyType: PropTypes.string,
showHelpBulb: PropTypes.bool,
dispatch: PropTypes.func,
numberOfTables: PropTypes.number,
headerFocus: PropTypes.bool.isRequired,
urlParams: PropTypes.bool.isRequired,
consoleUrl: PropTypes.string.isRequired,
};
export default ApiRequestWrapper;

View File

@ -1,230 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import AceEditor from 'react-ace';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import CopyToClipboard from 'react-copy-to-clipboard';
class ApiResponse extends React.Component {
constructor() {
super();
this.state = {
helpCopied: false,
tabIndex: 0,
};
}
render() {
const {
categoryType,
showHelpBulb,
enableResponseSection,
response,
url,
} = this.props;
const styles = require('../ApiExplorer.scss');
const isResponseError =
'statusCode' in response ? response.statusCode !== 200 : false;
const responseHtml = '';
const imgHTMLTag = `<img src='${url}' />`;
let formattedResponse = JSON.stringify(response.response, null, 4);
let responseMode = 'json';
let showGutter = true;
if (response.statusCode === 500) {
responseMode = 'text';
formattedResponse = 'Could not get any response';
formattedResponse += '\n\nThere was an error connecting to ' + url;
formattedResponse +=
'\n\nCheck if the URL is valid or the server is timing out';
showGutter = false;
}
const getHeaders = responseHeader => {
const currHeaders = [];
if (responseHeader.responseHeaders) {
responseHeader.responseHeaders.forEach((value, name) => {
currHeaders.push(
<tr key={name}>
<td
className={
styles.headerPadd +
' ' +
styles.wd48 +
' ' +
styles.border_right
}
>
{name}{' '}
</td>
<td className={styles.headerPadd + ' ' + styles.wd48}>{value}</td>
</tr>
);
});
}
return currHeaders.length > 0 ? currHeaders : '';
};
return (
<div className={styles.apiResponseWrapper}>
<div
id="apiResponseBlock"
className={styles.fixed_header_internal_link}
/>
<div className={styles.apiResponseheaderWrapper + ' ' + styles.wd100}>
<div
className={
styles.apiResponseheader + ' col-xs-6 ' + styles.padd_remove
}
>
Response
</div>
{enableResponseSection ? (
<div className={'col-xs-6 ' + styles.padd_remove}>
<div className={styles.statusDetails}>
Time:{' '}
<span className={styles.statusView}>
{response.timeElapsed} ms
</span>
</div>
<div className={styles.statusDetails}>
Status:{' '}
<span className={styles.statusView}>{response.statusCode}</span>
</div>
</div>
) : (
''
)}
</div>
{responseHtml}
{showHelpBulb ? (
<div className={styles.helpTextWrapper}>
<i className="fa fa-lightbulb-o" aria-hidden="true" />
Embed in your HTML as follows
<div className="input-group">
<pre>{imgHTMLTag}</pre>
<span className="input-group-btn">
<CopyToClipboard
text={imgHTMLTag}
onCopy={() => {
this.setState({ helpCopied: true });
const timer = setInterval(() => {
this.setState({ helpCopied: false });
clearInterval(timer);
}, 3000);
}}
>
<button className={styles.copyBtn + ' btn'} type="button">
{this.state.helpCopied ? 'Copied' : 'Copy'}
</button>
</CopyToClipboard>
</span>
</div>
</div>
) : (
''
)}
{enableResponseSection ? (
<Tabs
className={styles.apiResponseTab}
selectedIndex={this.state.tabIndex}
onSelect={tabIndex => this.setState({ tabIndex })}
>
<TabList className={styles.apiResponseTabUl}>
<Tab
className={
this.state.tabIndex === 0
? ' ' +
styles.activeApiResponseTab +
' ' +
styles.apiResponseTabList
: styles.apiResponseTabList
}
>
Body
</Tab>
<Tab
className={
this.state.tabIndex === 1
? ' ' +
styles.activeApiResponseTab +
' ' +
styles.apiResponseTabList
: styles.apiResponseTabList
}
>
Headers
</Tab>
</TabList>
<TabPanel className={styles.apiResponseTabPanel}>
<div className={styles.AceEditorWrapper}>
{response.isImage ? (
<img
src={response.response}
style={{ width: '100%', height: '100%' }}
/>
) : (
<AceEditor
readOnly
showPrintMargin={false}
mode={responseMode}
showGutter={showGutter}
theme="github"
name="api-explorer-request"
value={formattedResponse}
minLines={10}
maxLines={50}
width="100%"
/>
)}
</div>
</TabPanel>
<TabPanel className={styles.apiResponseTabPanel}>
<div className={styles.responseHeader + ' hide'}>
Header
<span className={styles.viewDetails + ' hide'}>
View Details
</span>
</div>
<div className={styles.responseTable}>
<table className={'table ' + styles.tableBorder}>
<tbody>{getHeaders(response)}</tbody>
</table>
</div>
</TabPanel>
</Tabs>
) : (
<div className={styles.noResponseWrapper}>
<div className={styles.noResponseContainer}>
<div className={styles.noResponseHeader}>
Hit the Send button to get a response
</div>
<div className={styles.barWrapper}>
<div className={styles.bigBar} />
<div className={styles.mediumBar} />
<div className={styles.smallBar} />
</div>
</div>
</div>
)}
</div>
);
}
}
ApiResponse.propTypes = {
enableResponseSection: PropTypes.bool.isRequired,
response: PropTypes.object.isRequired,
showHelpBulb: PropTypes.bool,
url: PropTypes.string,
categoryType: PropTypes.string,
};
export default ApiResponse;

View File

@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class GraphiQLErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, info: null };
}
componentDidCatch(error, info) {
this.setState({ hasError: true, info: info });
// most likely a localstorage issue
window.localStorage.clear();
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <div>{this.props.children}</div>;
}
return this.props.children;
}
}
GraphiQLErrorBoundary.propTypes = {
children: PropTypes.element,
};
export default GraphiQLErrorBoundary;

View File

@ -1,92 +0,0 @@
import React, { Component } from 'react';
import GraphiQL from 'hasura-console-graphiql';
import PropTypes from 'prop-types';
import GraphiQLErrorBoundary from './GraphiQLErrorBoundary';
import OneGraphExplorer from '../OneGraphExplorer/OneGraphExplorer';
import { clearCodeMirrorHints, setQueryVariableSectionHeight } from './utils';
import { graphQLFetcherFinal } from '../Actions';
import './GraphiQL.css';
class GraphiQLWrapper extends Component {
constructor(props) {
super(props);
this.state = {
schema: null,
error: false,
noSchema: false,
onBoardingEnabled: false,
};
}
componentDidMount() {
setQueryVariableSectionHeight();
}
componentWillUnmount() {
clearCodeMirrorHints();
}
render() {
const styles = require('../../../Common/Common.scss');
const { numberOfTables, urlParams, headerFocus } = this.props;
const graphqlNetworkData = this.props.data;
const graphQLFetcher = graphQLParams => {
if (headerFocus) {
return null;
}
return graphQLFetcherFinal(
graphQLParams,
graphqlNetworkData.url,
graphqlNetworkData.headers
);
};
const renderGraphiql = graphiqlProps => {
const voyagerUrl = graphqlNetworkData.consoleUrl + '/voyager-view';
return (
<GraphiQL
fetcher={graphQLFetcher}
voyagerUrl={voyagerUrl}
{...graphiqlProps}
/>
);
};
return (
<GraphiQLErrorBoundary>
<div
className={
'react-container-graphql ' +
styles.wd100 +
' ' +
styles.graphQLHeight
}
>
<OneGraphExplorer
renderGraphiql={renderGraphiql}
endpoint={graphqlNetworkData.url}
headers={graphqlNetworkData.headers}
headerFocus={headerFocus}
urlParams={urlParams}
numberOfTables={numberOfTables}
/>
</div>
</GraphiQLErrorBoundary>
);
}
}
GraphiQLWrapper.propTypes = {
dispatch: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
numberOfTables: PropTypes.number.isRequired,
headerFocus: PropTypes.bool.isRequired,
urlParams: PropTypes.object.isRequired,
};
export default GraphiQLWrapper;

View File

@ -1,20 +0,0 @@
export const getGraphiQLQueryFromLocalStorage = () => {
return window.localStorage.getItem('graphiql:query');
};
export const clearCodeMirrorHints = () => {
const cmNodes = document.querySelectorAll('.CodeMirror-hints.graphiql');
if (cmNodes.length > 0) {
cmNodes.forEach(cm => {
cm.remove();
});
}
};
export const setQueryVariableSectionHeight = () => {
const variableEditor = document.querySelectorAll('.variable-editor');
if (variableEditor && variableEditor.length > 0) {
variableEditor[0].style.height = '120px';
}
};

View File

@ -1,49 +0,0 @@
.graphiql-explorer-root {
font-size: 12px;
text-overflow: ellipsis;
overflow: unset;
white-space: nowrap;
margin: 0;
padding: 8px;
font-family: Consolas, Inconsolata, 'Droid Sans Mono', Monaco, monospace;
}
.graphiql-explorer-abstractfields-label {
color: #ca9800;
padding-left: 5px;
}
.graphiql-explorer-abstractargs-label {
color: #8b2bb9;
padding-left: 5px;
}
.graphiql-explorer-fieldname {
color: rgb(31, 97, 160);
padding-left: 5px;
}
.graphiql-explorer-node {
margin-top: 5px;
}
.graphiql-explorer-args-wrapper {
margin-top: 5px;
}
.explorerGraphiqlSeparator {
height: 100%;
right: -5px;
position: absolute;
top: 0;
width: 10px;
z-index: 10;
}
.explorerCursorResize {
cursor: col-resize;
}
.gqlexplorer {
position: relative;
}

View File

@ -1,212 +0,0 @@
import React from 'react';
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
import GraphiQLExplorer from 'graphiql-explorer';
import {
makeDefaultArg,
getDefaultScalarArgValue,
getExplorerWidth,
setExplorerWidth,
getExplorerIsOpen,
setExplorerIsOpen,
} from './utils';
import { getGraphiQLQueryFromLocalStorage } from '../GraphiQLWrapper/utils';
import { getRemoteQueries } from '../Actions';
import { getHeadersAsJSON } from '../utils';
import '../GraphiQLWrapper/GraphiQL.css';
import './OneGraphExplorer.css';
class OneGraphExplorer extends React.Component {
state = {
explorerOpen: getExplorerIsOpen(),
explorerWidth: getExplorerWidth(),
explorerClientX: null,
schema: null,
query: undefined,
isResizing: false,
loading: false,
previousIntrospectionHeaders: [],
};
componentDidMount() {
this.setPersistedQuery();
this.introspect();
}
componentDidUpdate() {
if (!this.props.headerFocus && !this.state.loading) {
if (
JSON.stringify(this.props.headers) !==
JSON.stringify(this.state.previousIntrospectionHeaders)
) {
this.introspect();
}
}
}
setPersistedQuery() {
const { urlParams, numberOfTables } = this.props;
const queryFile = urlParams ? urlParams.query_file : null;
if (queryFile) {
getRemoteQueries(queryFile, remoteQuery =>
this.setState({ query: remoteQuery })
);
} else if (numberOfTables === 0) {
const NO_TABLES_MESSAGE = `# Looks like you do not have any tables.
# Click on the "Data" tab on top to create tables
# Try out GraphQL queries here after you create tables
`;
this.setState({ query: NO_TABLES_MESSAGE });
} else {
const localStorageQuery = getGraphiQLQueryFromLocalStorage();
if (localStorageQuery) {
if (localStorageQuery.includes('do not have')) {
const FRESH_GRAPHQL_MSG = '# Try out GraphQL queries here\n';
this.setState({ query: FRESH_GRAPHQL_MSG });
} else {
this.setState({ query: localStorageQuery });
}
}
}
}
introspect() {
const { endpoint } = this.props;
const headers = JSON.parse(JSON.stringify(this.props.headers));
this.setState({ loading: true });
fetch(endpoint, {
method: 'POST',
headers: getHeadersAsJSON(headers || []),
body: JSON.stringify({
query: getIntrospectionQuery(),
}),
})
.then(response => response.json())
.then(result => {
this.setState({
schema: buildClientSchema(result.data),
loading: false,
previousIntrospectionHeaders: headers,
});
})
.catch(() => {
this.setState({
schema: null,
loading: false,
previousIntrospectionHeaders: headers,
});
});
}
onExplorerResize = e => {
const { explorerClientX, explorerWidth } = this.state;
if (explorerClientX === null) {
this.setState({ explorerClientX: e.clientX });
} else {
const newExplorerWidth = explorerWidth + e.clientX - explorerClientX;
setExplorerWidth(newExplorerWidth);
this.setState({
explorerWidth: newExplorerWidth,
explorerClientX: e.clientX,
});
}
};
editQuery = query => {
this.setState({ query });
};
handleToggle = () => {
const newIsOpen = !this.state.explorerOpen;
setExplorerIsOpen(newIsOpen);
this.setState({ explorerOpen: newIsOpen });
};
handleExplorerResize = e => {
e.preventDefault();
document.addEventListener('mousemove', this.onExplorerResize);
this.setState({
isResizing: true,
});
};
handleExplorerResizeStop = e => {
e.preventDefault();
document.removeEventListener('mousemove', this.onExplorerResize);
this.setState({
isResizing: false,
});
};
render() {
const {
schema,
explorerOpen,
query,
explorerWidth,
isResizing,
} = this.state;
const { renderGraphiql } = this.props;
const explorer = (
<GraphiQLExplorer
schema={schema}
query={query}
onEdit={this.editQuery}
explorerIsOpen={explorerOpen}
onToggleExplorer={this.handleToggle}
getDefaultScalarArgValue={getDefaultScalarArgValue}
makeDefaultArg={makeDefaultArg}
width={explorerWidth}
/>
);
let explorerSeparator;
if (explorerOpen) {
explorerSeparator = (
<div
className="explorerGraphiqlSeparator explorerCursorResize"
onMouseDown={this.handleExplorerResize}
onMouseUp={this.handleExplorerResizeStop}
/>
);
}
const graphiql = renderGraphiql({
query: query,
onEditQuery: this.editQuery,
schema: schema,
toggleExplorer: this.handleToggle,
});
return (
<div
className={
'graphiql-container' + (isResizing ? ' explorerCursorResize' : '')
}
onMouseUp={this.handleExplorerResizeStop}
>
<div className="gqlexplorer">
{explorer}
{explorerSeparator}
</div>
{graphiql}
</div>
);
}
}
export default OneGraphExplorer;

View File

@ -1,34 +0,0 @@
import GraphiQLExplorer from 'graphiql-explorer';
export const makeDefaultArg = () => {
return false;
};
export const getDefaultScalarArgValue = (parentField, arg, argType) => {
return GraphiQLExplorer.defaultValue(argType);
};
export const getExplorerWidth = () => {
const defaultWidth = 300;
const widthLSRaw = window.localStorage.getItem('graphiql:explorerWidth');
const widthLS = parseInt(widthLSRaw, 10);
return !isNaN(widthLS) ? widthLS : defaultWidth;
};
export const setExplorerWidth = width => {
localStorage.setItem('graphiql:explorerWidth', width);
};
export const getExplorerIsOpen = () => {
const defaultIsOpen = true;
const isOpen = window.localStorage.getItem('graphiql:explorerOpen');
return isOpen ? isOpen === 'true' : defaultIsOpen;
};
export const setExplorerIsOpen = isOpen => {
window.localStorage.setItem('graphiql:explorerOpen', isOpen);
};

View File

@ -1,66 +0,0 @@
const defaultHeader = [
{
key: 'content-type',
value: 'application/json',
isActive: true,
isNewHeader: false,
isDisabled: true,
},
];
defaultHeader.push({
key: '',
value: '',
isActive: false,
isNewHeader: true,
});
const getUrl = path => {
return `${window.__env.graphqlEndpoint}`;
};
const dataApisContent = [];
dataApisContent.push({
id: 'DataApi',
details: {
title: '',
description:
'Explore GraphQL APIs with headers',
category: 'data',
},
request: {
method: 'POST',
url: getUrl('/v1/graphql'),
headers: defaultHeader,
bodyType: 'graphql',
params: JSON.stringify({}, null, 4),
},
});
const dataApis = {
title: 'Data',
content: dataApisContent,
};
const explorerData = {
sendingRequest: false,
enableResponseSection: false,
response: {},
};
const defaultApi = dataApis.content[0];
const defaultState = {
currentTab: 0,
displayedApi: defaultApi,
modalState: {
isOpen: false,
isCopied: false,
},
explorerData,
headerFocus: false,
graphqlEndpoint: '',
};
export default defaultState;
export { defaultApi, defaultHeader };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

View File

@ -1,12 +0,0 @@
export const getHeadersAsJSON = (headers = []) => {
const headerJSON = {};
const nonEmptyHeaders = headers.filter(header => {
return header.key && header.isActive;
});
nonEmptyHeaders.forEach(header => {
headerJSON[header.key] = header.value;
});
return headerJSON;
};

View File

@ -1,55 +0,0 @@
import React, { Component } from 'react';
import { GraphQLVoyager } from 'graphql-voyager';
import {
getGraphiQLHeadersFromLocalStorage,
} from '../ApiExplorer/ApiRequest/utils';
import { getHeadersAsJSON } from '../ApiExplorer/utils';
import fetch from 'isomorphic-fetch';
import '../../../../node_modules/graphql-voyager/dist/voyager.css';
import './voyagerView.css';
class VoyagerView extends Component {
introspectionProvider(query) {
const url = window.localStorage.getItem('ONLINE_GRAPHIQL_ENDPOINT');
const HEADER_FROM_LS = getGraphiQLHeadersFromLocalStorage();
let parsedHeader = {};
if (HEADER_FROM_LS) {
try {
parsedHeader = JSON.parse(HEADER_FROM_LS);
} catch (e) {
parsedHeader = {};
console.error(e);
}
}
const headersFinal = getHeadersAsJSON(parsedHeader);
return fetch(url, {
method: 'POST',
headers: headersFinal,
body: JSON.stringify({ query: query }),
}).then(response => response.json());
}
render() {
return (
<div>
<GraphQLVoyager
introspection={this.introspectionProvider.bind(this)}
workerURI={
'https://cdn.jsdelivr.net/npm/graphql-voyager@1.0.0-rc.27/dist/voyager.worker.min.js'
}
/>
</div>
);
}
}
const generatedVoyagerConnector = connect => {
const mapStateToProps = state => {
return {
headers: state.apiexplorer.displayedApi.request.headers,
};
};
return connect(mapStateToProps)(VoyagerView);
};
export default generatedVoyagerConnector;

View File

@ -1,6 +0,0 @@
.graphql-voyager {
height: calc(100vh - 50px);
}
.graphql-voyager .type-doc > .doc-navigation {
padding: 5px 20px 20px 18px;
}

View File

@ -1,7 +0,0 @@
/**
* Export component modules
*/
export App from './App/App';
export PageNotFound from './Error/PageNotFound';
export Login from './Login/Login';

View File

@ -1,22 +0,0 @@
/* @flow */
const appConfig = require('../appconfig');
const host = appConfig.appHost;
const port = appConfig.port[process.env.NODE_ENV || 'development'];
const environment = {
development: {
isProduction: false,
},
production: {
isProduction: true,
},
}[process.env.NODE_ENV || 'development'];
module.exports = Object.assign(
{
host: host,
port: port,
},
environment
);

View File

@ -1 +0,0 @@
export const SERVER_CONSOLE_MODE = 'server';

View File

@ -1,94 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
/**
* Wrapper component containing HTML metadata and boilerplate tags.
* Used in server-side code only to wrap the string output of the
* rendered route component.
*
* The only thing this component doesn't (and can't) include is the
* HTML doctype declaration, which is added to the rendered output
* by the server.js file.
*/
export default class Html extends Component {
static propTypes = {
assets: PropTypes.object,
component: PropTypes.node,
baseDomain: PropTypes.string,
};
render() {
const { assets } = this.props;
const head = Helmet.rewind();
return (
<html lang="en-us">
<head>
<link rel="icon" type="image/png" href="/rstatic/favicon_green.png" />
{Object.keys(assets.styles).map((style, key) => (
<link
href={assets.styles[style]}
key={key}
media="screen, projection"
rel="stylesheet"
type="text/css"
charSet="UTF-8"
/>
))}
<script
dangerouslySetInnerHTML={{
__html: `window.__env={
graphqlEndpoint: '${process.env.GRAPHQL_ENDPOINT}',
query: '${process.env.QUERY_STRING}',
headers: '${process.env.HEADER_STRING}',
variables: '${process.env.VARIABLE_STRING}'
};`,
}}
/>
</head>
<body>
<style
dangerouslySetInnerHTML={{
__html: `
.content {
display: 'none';
opacity: 0;
transition: opacity .20s linear;
}
.content.show {
display: 'block';
opacity: 1;
transition: opacity .20s linear;
}
`,
}}
/>
<div
id="loading"
dangerouslySetInnerHTML={{
__html: `<div class="page-loading" style="
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
font-family: sans-serif;
justify-content: center;
">
<span class="" style="
font-size: 2em;
margin-top: -3em;
color: #848484;
">
Loading...
</span>
</div>`,
}}
/>
<div id="content" className="content" />
<script src={assets.javascript.main} charSet="UTF-8" />
</body>
</html>
);
}
}

View File

@ -1,9 +0,0 @@
/**
* Return the status code from the last matched route with a status property.
*
* @param matchedRoutes
* @returns {Number|null}
*/
export default matchedRoutes => {
return matchedRoutes.reduce((prev, cur) => cur.status || prev, null);
};

View File

@ -1,43 +0,0 @@
import { createRoutes } from 'react-router/lib/RouteUtils';
// Wrap the hooks so they don't fire if they're called before
// the store is initialised. This only happens when doing the first
// client render of a route that has an onEnter hook
function makeHooksSafe(routes, store) {
if (Array.isArray(routes)) {
return routes.map(route => makeHooksSafe(route, store));
}
const onEnter = routes.onEnter;
if (onEnter) {
routes.onEnter = function safeOnEnter(...args) {
try {
store.getState();
} catch (err) {
if (onEnter.length === 3) {
args[2]();
}
// There's no store yet so ignore the hook
return;
}
onEnter.apply(null, args);
};
}
if (routes.childRoutes) {
makeHooksSafe(routes.childRoutes, store);
}
if (routes.indexRoute) {
makeHooksSafe(routes.indexRoute, store);
}
return routes;
}
export default function makeRouteHooksSafe(_getRoutes) {
return store => makeHooksSafe(createRoutes(_getRoutes(store)), store);
}

View File

@ -1,12 +0,0 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import apiExplorerReducer from 'components/Services/ApiExplorer/Actions';
import progressBarReducer from 'components/App/Actions';
const reducer = combineReducers({
progressBar: progressBarReducer,
apiexplorer: apiExplorerReducer,
routing: routerReducer,
});
export default reducer;

View File

@ -1,33 +0,0 @@
import React from 'react';
import { Route, IndexRoute, IndexRedirect } from 'react-router';
import { connect } from 'react-redux';
import { App, Main, PageNotFound } from 'components';
import generatedApiExplorer from './components/Services/ApiExplorer/ApiExplorerGenerator';
import generatedVoyagerConnector from './components/Services/VoyagerView/VoyagerView';
import generatedLoginConnector from './components/Login/Login';
const routes = store => {
// loads schema
return (
<Route path="/" component={App}>
<Route path="">
<IndexRoute component={generatedLoginConnector(connect)} />
</Route>
<Route path="/graphiql" component={generatedApiExplorer(connect)} />
<Route
path="voyager-view"
component={generatedVoyagerConnector(connect)}
/>
<Route path="404" component={PageNotFound} status="404" />
<Route path="*" component={PageNotFound} status="404" />
</Route>
);
};
export default routes;

Some files were not shown because too many files have changed in this diff Show More