including CAIT data, models and seeds
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
bin/
|
||||
137
app.express.js
Normal file
137
app.express.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Serve GraphQL Backend
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import graphQLHTTP from 'express-graphql';
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer from 'webpack-dev-server';
|
||||
import {Schema} from './data/schema';
|
||||
|
||||
const APP_PORT = 3000;
|
||||
const GRAPHQL_PORT = 8080;
|
||||
|
||||
var graphql_server = express();
|
||||
|
||||
// Expose a GraphQL endpoint
|
||||
graphql_server.use('/', graphQLHTTP({
|
||||
graphiql: true,
|
||||
pretty: true,
|
||||
schema: Schema,
|
||||
}));
|
||||
graphql_server.listen(GRAPHQL_PORT, () => console.log(
|
||||
`GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}`
|
||||
));
|
||||
|
||||
/*
|
||||
* Compile and Serve Relay App w/ Webpack
|
||||
*/
|
||||
|
||||
var compiler = webpack({
|
||||
entry: {
|
||||
app: path.resolve(__dirname, 'relay', 'application.js')
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel',
|
||||
query: {
|
||||
plugins: ['./build/babelRelayPlugin'],
|
||||
},
|
||||
test: /\.js$/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {filename: 'application.js', path: '/'}
|
||||
});
|
||||
var dev_server = new WebpackDevServer(compiler, {
|
||||
contentBase: '/public/',
|
||||
proxy: {'/graphql': `http://localhost:${GRAPHQL_PORT}`},
|
||||
publicPath: '/assets/js/',
|
||||
stats: {colors: true}
|
||||
});
|
||||
|
||||
/*
|
||||
* Logging, Cookie, JSON Parsing Middleware
|
||||
*
|
||||
|
||||
import favicon from 'serve-favicon';
|
||||
import logger from 'morgan';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'bodyParser';
|
||||
|
||||
// uncomment after placing your favicon in /public
|
||||
//app.use(favicon(__dirname + '/public/favicon.ico'));
|
||||
app.use(logger('dev'));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());*/
|
||||
|
||||
/*
|
||||
* Serve Vendor Scripts, CSS, and Templates
|
||||
*/
|
||||
|
||||
// serve fonts in /assets/fonts
|
||||
import assets from "connect-assets";
|
||||
dev_server.app.use("/assets/fonts", express.static("node_modules/bootstrap/dist/fonts"));
|
||||
dev_server.app.use("/assets/fonts", express.static("node_modules/font-awesome/fonts"));
|
||||
// serve compiled vendor assets and application.css.
|
||||
dev_server.app.use(assets({
|
||||
paths: ["assets/js", "assets/css", "node_modules"],
|
||||
build: true,
|
||||
buildDir: false,
|
||||
//compile: false,
|
||||
compress: true
|
||||
}));
|
||||
// serve public static files.
|
||||
dev_server.app.use('/', express.static(path.resolve(__dirname, 'public')));
|
||||
|
||||
// view engine set up
|
||||
dev_server.app.set('views', path.join(__dirname, 'views'));
|
||||
dev_server.app.set('view engine', 'jade');
|
||||
dev_server.app.get("/", (req, res, next)=>{
|
||||
res.render("index");
|
||||
});
|
||||
|
||||
dev_server.listen(APP_PORT, () => {
|
||||
console.log(`App is now running on http://localhost:${APP_PORT}`);
|
||||
});
|
||||
|
||||
/*
|
||||
* Handle Errors
|
||||
*/
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
dev_server.use(function(req, res, next) {
|
||||
var err = new Error('Not Found');
|
||||
err.status = 404;
|
||||
next(err);
|
||||
});
|
||||
|
||||
/*
|
||||
// development error handler
|
||||
// will print stacktrace
|
||||
if (app.get('env') === 'development') {
|
||||
app.use(function(err, req, res, next) {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// production error handler
|
||||
// no stacktraces leaked to user
|
||||
app.use(function(err, req, res, next) {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: {}
|
||||
});
|
||||
});*/
|
||||
|
||||
|
||||
module.exports = dev_server;
|
||||
0
assets/css/application.scss.css
Normal file
0
assets/css/application.scss.css
Normal file
3
assets/css/vendor.scss.css
Normal file
3
assets/css/vendor.scss.css
Normal file
@@ -0,0 +1,3 @@
|
||||
/*=require bootstrap/dist/css/bootstrap.min
|
||||
*= require font-awesome/css/font-awesome.min
|
||||
*/
|
||||
3
assets/js/vendor.js
Normal file
3
assets/js/vendor.js
Normal file
@@ -0,0 +1,3 @@
|
||||
//= require d3/d3.min
|
||||
//= require jquery/dist/jquery.min
|
||||
//= require bootstrap/dist/js/bootstrap.min
|
||||
4
build/babelRelayPlugin.js
Normal file
4
build/babelRelayPlugin.js
Normal file
@@ -0,0 +1,4 @@
|
||||
var getbabelRelayPlugin = require('babel-relay-plugin'),
|
||||
schema = require('../data/schema.json');
|
||||
|
||||
module.exports = getbabelRelayPlugin(schema.data, {abortOnError: true});
|
||||
40
config/database.js
Normal file
40
config/database.js
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs"),
|
||||
models = [],
|
||||
model_path = __dirname + "/../models",
|
||||
files = fs.readdirSync(model_path),
|
||||
Sequelize = require("sequelize"),
|
||||
sequelize = new Sequelize("postgres://spikeuser:123456@localhost:5432/spike_proto", {
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idle: 10000
|
||||
}
|
||||
});
|
||||
|
||||
class Database {
|
||||
|
||||
static sync(){
|
||||
|
||||
// define each model
|
||||
for (var filename of files){
|
||||
var path = model_path + "/" + filename,
|
||||
stats = fs.statSync(path)
|
||||
if (stats.isFile()){
|
||||
models.push(require(path));
|
||||
}
|
||||
}
|
||||
|
||||
// add associations
|
||||
for (var model of models){
|
||||
model.associate();
|
||||
}
|
||||
|
||||
return sequelize.sync({force: true});
|
||||
}
|
||||
}
|
||||
Database.Sequelize = Sequelize;
|
||||
Database.sequelize = sequelize;
|
||||
|
||||
export Database;
|
||||
0
data/CAIT/citation.txt
Normal file
0
data/CAIT/citation.txt
Normal file
4325
data/CAIT/emissions.csv
Executable file
4325
data/CAIT/emissions.csv
Executable file
File diff suppressed because it is too large
Load Diff
9965
data/CAIT/socioeconomic.csv
Executable file
9965
data/CAIT/socioeconomic.csv
Executable file
File diff suppressed because it is too large
Load Diff
33
data/database.js
Normal file
33
data/database.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2015, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
// Model types
|
||||
class User extends Object {}
|
||||
class Widget extends Object {}
|
||||
|
||||
// Mock data
|
||||
var viewer = new User();
|
||||
viewer.id = '1';
|
||||
viewer.name = 'Anonymous';
|
||||
var widgets = ['What\'s-it', 'Who\'s-it', 'How\'s-it'].map((name, i) => {
|
||||
var widget = new Widget();
|
||||
widget.name = name;
|
||||
widget.id = `${i}`;
|
||||
return widget;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
// Export methods that your schema can use to interact with your database
|
||||
getUser: (id) => id === viewer.id ? viewer : null,
|
||||
getViewer: () => viewer,
|
||||
getWidget: (id) => widgets.find(w => w.id === id),
|
||||
getWidgets: () => widgets,
|
||||
User,
|
||||
Widget,
|
||||
};
|
||||
143
data/schema.js
Normal file
143
data/schema.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2015, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLFloat,
|
||||
GraphQLID,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLSchema,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
|
||||
import {
|
||||
connectionArgs,
|
||||
connectionDefinitions,
|
||||
connectionFromArray,
|
||||
fromGlobalId,
|
||||
globalIdField,
|
||||
mutationWithClientMutationId,
|
||||
nodeDefinitions,
|
||||
} from 'graphql-relay';
|
||||
|
||||
import {
|
||||
// Import methods that your schema can use to interact with your database
|
||||
User,
|
||||
Widget,
|
||||
getUser,
|
||||
getViewer,
|
||||
getWidget,
|
||||
getWidgets,
|
||||
} from './database';
|
||||
|
||||
/**
|
||||
* We get the node interface and field from the Relay library.
|
||||
*
|
||||
* The first method defines the way we resolve an ID to its object.
|
||||
* The second defines the way we resolve an object to its GraphQL type.
|
||||
*/
|
||||
var {nodeInterface, nodeField} = nodeDefinitions(
|
||||
(globalId) => {
|
||||
var {type, id} = fromGlobalId(globalId);
|
||||
if (type === 'User') {
|
||||
return getUser(id);
|
||||
} else if (type === 'Widget') {
|
||||
return getWidget(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
(obj) => {
|
||||
if (obj instanceof User) {
|
||||
return userType;
|
||||
} else if (obj instanceof Widget) {
|
||||
return widgetType;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Define your own types here
|
||||
*/
|
||||
|
||||
var userType = new GraphQLObjectType({
|
||||
name: 'User',
|
||||
description: 'A person who uses our app',
|
||||
fields: () => ({
|
||||
id: globalIdField('User'),
|
||||
widgets: {
|
||||
type: widgetConnection,
|
||||
description: 'A person\'s collection of widgets',
|
||||
args: connectionArgs,
|
||||
resolve: (_, args) => connectionFromArray(getWidgets(), args),
|
||||
},
|
||||
}),
|
||||
interfaces: [nodeInterface],
|
||||
});
|
||||
|
||||
var widgetType = new GraphQLObjectType({
|
||||
name: 'Widget',
|
||||
description: 'A shiny widget',
|
||||
fields: () => ({
|
||||
id: globalIdField('Widget'),
|
||||
name: {
|
||||
type: GraphQLString,
|
||||
description: 'The name of the widget',
|
||||
},
|
||||
}),
|
||||
interfaces: [nodeInterface],
|
||||
});
|
||||
|
||||
/**
|
||||
* Define your own connection types here
|
||||
*/
|
||||
var {connectionType: widgetConnection} =
|
||||
connectionDefinitions({name: 'Widget', nodeType: widgetType});
|
||||
|
||||
/**
|
||||
* This is the type that will be the root of our query,
|
||||
* and the entry point into our schema.
|
||||
*/
|
||||
var queryType = new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: () => ({
|
||||
node: nodeField,
|
||||
// Add your own root fields here
|
||||
viewer: {
|
||||
type: userType,
|
||||
resolve: () => getViewer(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* This is the type that will be the root of our mutations,
|
||||
* and the entry point into performing writes in our schema.
|
||||
*/
|
||||
var mutationType = new GraphQLObjectType({
|
||||
name: 'Mutation',
|
||||
fields: () => ({
|
||||
// Add your own mutations here
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Finally, we construct our schema (whose starting query type is the query
|
||||
* type we defined above) and export it.
|
||||
*/
|
||||
export var Schema = new GraphQLSchema({
|
||||
query: queryType,
|
||||
// Uncomment the following after adding some mutation fields:
|
||||
// mutation: mutationType
|
||||
});
|
||||
1160
data/schema.json
Normal file
1160
data/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
4
gulpfile.js
Normal file
4
gulpfile.js
Normal file
@@ -0,0 +1,4 @@
|
||||
var gulp = require("gulp"),
|
||||
seedDate = require("./lib/tasks/seed_data");
|
||||
|
||||
gulp.task('seedData', seedData);
|
||||
5
lib/base.model.js
Normal file
5
lib/base.model.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export class Model {
|
||||
|
||||
static associate(){}
|
||||
|
||||
}
|
||||
1
lib/d3/bar.d3.js
Normal file
1
lib/d3/bar.d3.js
Normal file
@@ -0,0 +1 @@
|
||||
bar.d3.js
|
||||
1
lib/d3/base.d3.js
Normal file
1
lib/d3/base.d3.js
Normal file
@@ -0,0 +1 @@
|
||||
base.d3.js
|
||||
1
lib/d3/composite.d3.js
Normal file
1
lib/d3/composite.d3.js
Normal file
@@ -0,0 +1 @@
|
||||
composite.d3.js
|
||||
0
lib/d3/grid.d3.js
Normal file
0
lib/d3/grid.d3.js
Normal file
1
lib/d3/line.d3.js
Normal file
1
lib/d3/line.d3.js
Normal file
@@ -0,0 +1 @@
|
||||
line.d3.js
|
||||
1
lib/d3/pie.d3.js
Normal file
1
lib/d3/pie.d3.js
Normal file
@@ -0,0 +1 @@
|
||||
pie.d3.js
|
||||
1
lib/dashboard/base.component.js
Normal file
1
lib/dashboard/base.component.js
Normal file
@@ -0,0 +1 @@
|
||||
base.widget.js
|
||||
1
lib/dashboard/campaign_orders.component.js
Normal file
1
lib/dashboard/campaign_orders.component.js
Normal file
@@ -0,0 +1 @@
|
||||
campaign_orders.widget.js
|
||||
1
lib/dashboard/energy_time_series.component.js
Normal file
1
lib/dashboard/energy_time_series.component.js
Normal file
@@ -0,0 +1 @@
|
||||
energy_time_series.widget.js
|
||||
51
lib/dashboard/power_time_series.component.js
Normal file
51
lib/dashboard/power_time_series.component.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import Relay from 'react-relay';
|
||||
|
||||
class PowerTimeSeries extends React.Component {
|
||||
|
||||
get timeframes(){
|
||||
return [
|
||||
{display: "Today", value: 'today'},
|
||||
{display: "1 week", value: 'week'},
|
||||
{display: "1 month", value: 'month'},
|
||||
{display: "6 months", value: 'half_year'},
|
||||
{display: "1 year", value: 'year'}
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
var power_time_series = this;
|
||||
return (
|
||||
<div>
|
||||
<h1>Power Time Series</h1>
|
||||
<ul>
|
||||
{power_time_series.props.viewer.widgets.edges.map(edge =>
|
||||
<li key={edge.node.id}>{edge.node.name} (ID: {edge.node.id})</li>
|
||||
)}
|
||||
</ul>
|
||||
<div class="spk-power-time-series-timeframes">
|
||||
{power_time_series.timeframes.map(timeframe =>
|
||||
<div data-value="{timeframe.value}" class="spk-power-time-series-timeframe">{timeframe.display}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Relay.createContainer(PowerTimeSeries, {
|
||||
fragments: {
|
||||
viewer: () => Relay.QL`
|
||||
fragment on User {
|
||||
widgets(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
production_radiation_time_series.widget.js
|
||||
6
lib/dashboard/readme.md
Normal file
6
lib/dashboard/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## Dashboard Widgets
|
||||
|
||||
After initialized:
|
||||
1. They get/ check the data they need.
|
||||
2. Render default graph settings.
|
||||
3. Update graphs based on user input.
|
||||
27
lib/node.relay.js
Normal file
27
lib/node.relay.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* We get the node interface and field from the Relay library.
|
||||
*
|
||||
* The first method defines the way we resolve an ID to its object.
|
||||
* The second defines the way we resolve an object to its GraphQL type.
|
||||
*/
|
||||
var {nodeInterface, nodeField} = nodeDefinitions(
|
||||
(globalId) => {
|
||||
var {type, id} = fromGlobalId(globalId);
|
||||
if (type === 'PowerDatum') {
|
||||
return getUser(id);
|
||||
} else if (type === 'Widget') {
|
||||
return getWidget(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
(obj) => {
|
||||
if (obj instanceof User) {
|
||||
return userType;
|
||||
} else if (obj instanceof Widget) {
|
||||
return widgetType;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
104
lib/tasks/seed_data.js
Normal file
104
lib/tasks/seed_data.js
Normal file
@@ -0,0 +1,104 @@
|
||||
module.exports = function(done) {
|
||||
|
||||
var csv = require("fast-csv"),
|
||||
Database = require("./config/database"),
|
||||
Country = require("./models/country"),
|
||||
Datum = require("./models/datum"),
|
||||
countries = [],
|
||||
data = new Map();
|
||||
|
||||
function seedEmissions(fn){
|
||||
var i = -1;
|
||||
csv
|
||||
.fromPath(__dirname + "/data/CAIT/emissions.csv")
|
||||
.on("data", (row)=>{
|
||||
i += 1;
|
||||
if (i == 0) return true;
|
||||
var name = row[0],
|
||||
year = parseInt(row[1]),
|
||||
key = name + year,
|
||||
energy = parseFloat(row[11]) || null,
|
||||
industrial = parseFloat(row[12]) || null,
|
||||
agriculture = parseFloat(row[13]) || null,
|
||||
waste = parseFloat(row[14]) || null,
|
||||
lucf = parseFloat(row[15]) || null,
|
||||
total_emissions = Math.max((energy + industrial + agriculture + waste + lucf), parseFloat(row[3])) || null; // MtCO2eq
|
||||
|
||||
if (countries.indexOf(name) < 0) countries.push(name);
|
||||
data.set(key, data.get(key) || {name: name, year: year});
|
||||
datum = data.get(key);
|
||||
datum.energy_emissions = energy; // MtCO2eq
|
||||
datum.industrial_emissions = industrial; // MtCO2eq
|
||||
datum.agriculture_emissions = agriculture; // MtCO2eq
|
||||
datum.waste_emissions = waste; // MtCO2eq
|
||||
datum.lucf_emissions = lucf; // MtCO2eq
|
||||
datum.total_emissions = total_emissions; // MtCO2eq
|
||||
})
|
||||
.on("end", fn);
|
||||
}
|
||||
function seedSocioEco(fn){
|
||||
var i = -1;
|
||||
csv
|
||||
.fromPath(__dirname + "/data/CAIT/socioeconomic.csv")
|
||||
.on("data", (row)=>{
|
||||
i += 1;
|
||||
if (i == 0) return true;
|
||||
var name = row[0],
|
||||
year = parseInt(row[1]),
|
||||
key = (name + year),
|
||||
population = parseInt(row[2]) || null,
|
||||
gdp = parseFloat(row[4]) || null, // Million 2005 USD
|
||||
energy = parseFloat(row[5]) || null; // ktoe
|
||||
|
||||
if (countries.indexOf(name) < 0) countries.push(name);
|
||||
data.set(key, data.get(key) || {name: name, year: year});
|
||||
datum = data.get(key);
|
||||
datum.population = population;
|
||||
datum.energy = energy;
|
||||
datum.gdp = gdp;
|
||||
})
|
||||
.on("end", fn);
|
||||
}
|
||||
|
||||
Database.sync().then(()=>{
|
||||
seedEmissions(()=>{
|
||||
seedSocioEco(()=>{
|
||||
|
||||
Country.sql.bulkCreate(countries.map((name)=>{ return {name: name}; }), {validate: true}).catch((errors)=>{
|
||||
console.error(`=== Error ===\n${JSON.stringify(errors)}\n`);
|
||||
}).then(()=>{
|
||||
return Country.sql.findAll();
|
||||
}).then((_countries)=>{
|
||||
var country_name_to_id = new Map();
|
||||
for (var country of _countries){
|
||||
country_name_to_id.set(country.name, country.id);
|
||||
}
|
||||
for (var datum of data.values()){
|
||||
datum.country_id = country_name_to_id.get(datum.name);
|
||||
delete datum["name"];
|
||||
}
|
||||
|
||||
var bulk_data = Array.from(data.values());
|
||||
Datum.sql.bulkCreate(bulk_data, {validate: true}).catch((errors)=>{
|
||||
console.error(`=== Error ===\n${JSON.stringify(errors)}\n`);
|
||||
}).error((err)=>{
|
||||
console.error(`=== Error ===\n${err}`);
|
||||
}).then(()=>{
|
||||
return Datum.sql.count();
|
||||
}).then((count)=>{
|
||||
console.log(`Count: ${count} ${bulk_data.length}`)
|
||||
if (count === bulk_data.length){
|
||||
done();
|
||||
} else {
|
||||
console.log("Error!");
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
54
models/country.js
Normal file
54
models/country.js
Normal file
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
var Database = require("./../config/database");
|
||||
|
||||
class Country {
|
||||
|
||||
static associate(){
|
||||
var Datum = require("./datum");
|
||||
this.sql.hasOne(Datum.sql, {as: "Data", foreignKey: "id", constraints: false});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Country.sql = Database.sequelize.define('Country', {
|
||||
id: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||
},
|
||||
name: {
|
||||
type: Database.Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false
|
||||
}
|
||||
}, {tableName: "countries"});
|
||||
|
||||
Country.graphql = new GraphQLObjectType({
|
||||
name: "Country",
|
||||
description: "A world country",
|
||||
fields: ()=>{
|
||||
id: {
|
||||
type: new GraphQLNonNull(GraphQLInteger)
|
||||
},
|
||||
name: {
|
||||
type: new GraphQLNonNull(GraphQLString)
|
||||
},
|
||||
data: {
|
||||
type: new GraphQLList(Datum.graphql),
|
||||
args: {
|
||||
year: {
|
||||
type: GraphQLList(GraphQLInteger)
|
||||
}
|
||||
},
|
||||
resolve: (country, args)=>{
|
||||
debugger
|
||||
if (args.year){
|
||||
country.getData({where: args});
|
||||
} else {
|
||||
country.getData();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Country;
|
||||
78
models/datum.js
Normal file
78
models/datum.js
Normal file
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
var Database = require("./../config/database");
|
||||
|
||||
class Datum {
|
||||
static associate(){
|
||||
var Country = require("./country");
|
||||
this.sql.belongsTo(Country.sql, {foreignKey: "country_id", targetKey: "id", constraints: false});
|
||||
}
|
||||
}
|
||||
|
||||
Datum.sql = Database.sequelize.define('Datum', {
|
||||
id: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||
},
|
||||
country_id: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
unique: "country_year",
|
||||
references: {
|
||||
model: "countries",
|
||||
key: "id",
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
year: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
unique: "country_year",
|
||||
allowNull: false
|
||||
},
|
||||
population: Database.Sequelize.BIGINT,
|
||||
gdp: Database.Sequelize.FLOAT,
|
||||
total_emissions: Database.Sequelize.FLOAT,
|
||||
energy_emissions: Database.Sequelize.FLOAT,
|
||||
industrial_emissions: Database.Sequelize.FLOAT,
|
||||
agriculture_emissions: Database.Sequelize.FLOAT,
|
||||
waste_emissions: Database.Sequelize.FLOAT,
|
||||
lucf_emissions: Database.Sequelize.FLOAT,
|
||||
energy: Database.Sequelize.FLOAT
|
||||
}, {tableName: "data"});
|
||||
|
||||
Datum.graphql = new GraphQLObjectType({
|
||||
name: "Datum",
|
||||
description: "A world country",
|
||||
fields: ()=>{
|
||||
year: {
|
||||
type: new GraphQLNonNull(GraphQLString)
|
||||
},
|
||||
population: {
|
||||
type: GraphQLInteger
|
||||
},
|
||||
gdp: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
total_emissions: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
energy_emissions: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
industrial_emissions: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
agriculture_emissions: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
waste_emissions: {
|
||||
type: GraphQLFloat
|
||||
},
|
||||
lucf_emissions: {
|
||||
type: GraphQLFloat
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
module.exports = Datum;
|
||||
49
models/house.js
Normal file
49
models/house.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
GraphQLString,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType
|
||||
} from 'graphql';
|
||||
|
||||
import {
|
||||
fromGlobalId,
|
||||
globalIdField,
|
||||
nodeDefinitions,
|
||||
} from 'graphql-relay';
|
||||
|
||||
import Database from "./../config/database";
|
||||
import PowerData from "./power_datum"
|
||||
|
||||
/**
|
||||
* Define your own types here
|
||||
*/
|
||||
|
||||
var House = Database.sequelize.define('House', {
|
||||
id: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||
},
|
||||
name: Database.Sequelize.STRING
|
||||
}, {
|
||||
tableName: "houses",
|
||||
instanceMethods: {
|
||||
|
||||
},
|
||||
classMethods: {
|
||||
associate: function(){
|
||||
House.hasMany(PowerDatum, {as: 'PowerData'})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
House.graphql_type = new GraphQLObjectType({
|
||||
name: 'House',
|
||||
description: 'A house',
|
||||
fields: () => ({
|
||||
id: globalIdField('House'),
|
||||
name: GraphQLNonNull(GraphQLInt)
|
||||
}),
|
||||
interfaces: [nodeInterface],
|
||||
});
|
||||
|
||||
export House;
|
||||
53
models/power_datum.js
Normal file
53
models/power_datum.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
GraphQLFloat,
|
||||
GraphQLInt,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType
|
||||
} from 'graphql';
|
||||
|
||||
import {
|
||||
fromGlobalId,
|
||||
globalIdField,
|
||||
nodeDefinitions,
|
||||
} from 'graphql-relay';
|
||||
|
||||
import Database from "./../config/database";
|
||||
import PowerData from "./house"
|
||||
|
||||
/**
|
||||
* Define your own types here
|
||||
*/
|
||||
|
||||
var PowerDatum = Database.sequelize.define('Datum', {
|
||||
id: {
|
||||
type: Database.Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true // Automatically gets converted to SERIAL for postgres
|
||||
},
|
||||
time: Database.Sequelize.FLOAT,
|
||||
power: Database.Sequelize.FLOAT
|
||||
}, {
|
||||
tableName: "power_data",
|
||||
instanceMethods: {
|
||||
|
||||
},
|
||||
classMethods: {
|
||||
associate: function(){
|
||||
PowerDatum.belongsTo(House);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
PowerDatum.graphql_type = new GraphQLObjectType({
|
||||
name: 'Power Datum',
|
||||
description: 'A person who uses our app',
|
||||
fields: () => ({
|
||||
id: globalIdField('PowerDatum'),
|
||||
time: GraphQLInt,
|
||||
power: GraphQLFloat
|
||||
}),
|
||||
interfaces: [nodeInterface],
|
||||
});
|
||||
|
||||
export PowerDatum;
|
||||
47
npm-debug.log
Normal file
47
npm-debug.log
Normal file
@@ -0,0 +1,47 @@
|
||||
0 info it worked if it ends with ok
|
||||
1 verbose cli [ '/home/eric/.nvm/v5.4.1/bin/node',
|
||||
1 verbose cli '/home/eric/.nvm/v5.4.1/bin/npm',
|
||||
1 verbose cli 'start' ]
|
||||
2 info using npm@3.5.3
|
||||
3 info using node@v5.4.1
|
||||
4 verbose run-script [ 'prestart', 'start', 'poststart' ]
|
||||
5 info lifecycle spike_proto@0.0.0~prestart: spike_proto@0.0.0
|
||||
6 silly lifecycle spike_proto@0.0.0~prestart: no script for prestart, continuing
|
||||
7 info lifecycle spike_proto@0.0.0~start: spike_proto@0.0.0
|
||||
8 verbose lifecycle spike_proto@0.0.0~start: unsafe-perm in lifecycle true
|
||||
9 verbose lifecycle spike_proto@0.0.0~start: PATH: /home/eric/.nvm/v5.4.1/lib/node_modules/npm/bin/node-gyp-bin:/home/eric/Code/spike_proto/node_modules/.bin:/home/eric/.nvm/v5.4.1/bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@oroeco_dev/bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@global/bin:/home/eric/.rvm/rubies/ruby-1.9.3-p484/bin:/home/eric/.rvm/bin:/home/eric/bin:/usr/local/heroku/bin:/home/eric/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/opt/lampp/bin
|
||||
10 verbose lifecycle spike_proto@0.0.0~start: CWD: /home/eric/Code/spike_proto
|
||||
11 silly lifecycle spike_proto@0.0.0~start: Args: [ '-c', 'babel-node ./app.js' ]
|
||||
12 silly lifecycle spike_proto@0.0.0~start: Returned: code: 1 signal: null
|
||||
13 info lifecycle spike_proto@0.0.0~start: Failed to exec start script
|
||||
14 verbose stack Error: spike_proto@0.0.0 start: `babel-node ./app.js`
|
||||
14 verbose stack Exit status 1
|
||||
14 verbose stack at EventEmitter.<anonymous> (/home/eric/.nvm/v5.4.1/lib/node_modules/npm/lib/utils/lifecycle.js:232:16)
|
||||
14 verbose stack at emitTwo (events.js:87:13)
|
||||
14 verbose stack at EventEmitter.emit (events.js:172:7)
|
||||
14 verbose stack at ChildProcess.<anonymous> (/home/eric/.nvm/v5.4.1/lib/node_modules/npm/lib/utils/spawn.js:24:14)
|
||||
14 verbose stack at emitTwo (events.js:87:13)
|
||||
14 verbose stack at ChildProcess.emit (events.js:172:7)
|
||||
14 verbose stack at maybeClose (internal/child_process.js:821:16)
|
||||
14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:211:5)
|
||||
15 verbose pkgid spike_proto@0.0.0
|
||||
16 verbose cwd /home/eric/Code/spike_proto
|
||||
17 error Linux 3.19.0-43-generic
|
||||
18 error argv "/home/eric/.nvm/v5.4.1/bin/node" "/home/eric/.nvm/v5.4.1/bin/npm" "start"
|
||||
19 error node v5.4.1
|
||||
20 error npm v3.5.3
|
||||
21 error code ELIFECYCLE
|
||||
22 error spike_proto@0.0.0 start: `babel-node ./app.js`
|
||||
22 error Exit status 1
|
||||
23 error Failed at the spike_proto@0.0.0 start script 'babel-node ./app.js'.
|
||||
23 error Make sure you have the latest version of node.js and npm installed.
|
||||
23 error If you do, this is most likely a problem with the spike_proto package,
|
||||
23 error not with npm itself.
|
||||
23 error Tell the author that this fails on your system:
|
||||
23 error babel-node ./app.js
|
||||
23 error You can get information on how to open an issue for this project with:
|
||||
23 error npm bugs spike_proto
|
||||
23 error Or if that isn't available, you can get their info via:
|
||||
23 error npm owner ls spike_proto
|
||||
23 error There is likely additional logging output above.
|
||||
24 verbose exit [ 1, true ]
|
||||
59
package.json
Normal file
59
package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "spike_proto",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "babel-node ./app.express.js",
|
||||
"update-schema": "babel-node ./scripts/updateSchema.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "~1.12.0",
|
||||
"cookie-parser": "~1.3.4",
|
||||
"debug": "~2.1.1",
|
||||
"jade": "~1.9.2",
|
||||
"morgan": "~1.5.1",
|
||||
"serve-favicon": "~2.2.0",
|
||||
"connect-assets": "~4.7.0",
|
||||
"connect-jade-static": "~0.2.2",
|
||||
"node-forge": "~0.6.26",
|
||||
"sequelize": "~3.15.1",
|
||||
"pg": "~4.4.3",
|
||||
"pg-hstore": "~2.3.2",
|
||||
"fast-csv": "0.6.0",
|
||||
"babel-core": "6.3.21",
|
||||
"babel-loader": "6.2.0",
|
||||
"babel-polyfill": "6.3.14",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babel-preset-react": "6.3.13",
|
||||
"babel-preset-stage-0": "6.3.13",
|
||||
"babel-relay-plugin": "0.6.0",
|
||||
"babel-core": "6.3.21",
|
||||
"babel-loader": "6.2.0",
|
||||
"babel-polyfill": "6.3.14",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"babel-preset-react": "6.3.13",
|
||||
"babel-preset-stage-0": "6.3.13",
|
||||
"babel-relay-plugin": "0.6.0",
|
||||
"express": "4.13.3",
|
||||
"express-graphql": "0.4.5",
|
||||
"graphql": "0.4.14",
|
||||
"graphql-relay": "0.3.6",
|
||||
"react": "0.14.3",
|
||||
"react-dom": "0.14.3",
|
||||
"react-relay": "0.6.0",
|
||||
"webpack": "1.12.9",
|
||||
"webpack-dev-server": "1.14.0",
|
||||
"jquery": "2.2.0",
|
||||
"bootstrap": "3.3.6",
|
||||
"d3": "3.5.12",
|
||||
"font-awesome": "4.5.0",
|
||||
"css-loader": "^0.15.5",
|
||||
"style-loader": "^0.12.3",
|
||||
"connect-assets":"~4.7.0",
|
||||
"node-sass": "3.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.0",
|
||||
"babel-cli": "^6.3.17"
|
||||
}
|
||||
}
|
||||
41
public/index2.html
Normal file
41
public/index2.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/assets/css/vendor.css" rel="stylesheet" type="text/css">
|
||||
<link href="/assets/css/application.css" rel="stylesheet" type="text/css">
|
||||
<title>Spike Prototype</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="spike_container">
|
||||
<div id="spike_content">
|
||||
<nav style="margin-bottom:0px;" class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button><a href="/" class="navbar-brand">Spike</a>
|
||||
</div>
|
||||
<div id="navbar" class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/">Spike</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="root"></div>
|
||||
</div>
|
||||
<div id="spike_footer">
|
||||
<div class="container">Footer</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
// Force `fetch` polyfill to workaround Chrome not displaying request body
|
||||
// in developer tools for the native `fetch`.
|
||||
self.fetch = null;
|
||||
</script>
|
||||
<script src="http://localhost:3000/webpack-dev-server.js"></script>
|
||||
<script src="/assets/js/vendor.js"></script>
|
||||
<script src="/assets/js/application.js"></script>
|
||||
</html>
|
||||
1
readme.md
Normal file
1
readme.md
Normal file
@@ -0,0 +1 @@
|
||||
[See necessary examples](https://drive.google.com/drive/u/0/folders/0B6Ys-9_Te2cFNktOU3VwSzA1VWs)
|
||||
15
relay/app.relay.js
Normal file
15
relay/app.relay.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'babel-polyfill';
|
||||
|
||||
import App from './components/App';
|
||||
import AppHomeRoute from './routes/AppHomeRoute';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Relay from 'react-relay';
|
||||
|
||||
ReactDOM.render(
|
||||
<Relay.RootContainer
|
||||
Component={App}
|
||||
route={new AppHomeRoute()}
|
||||
/>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
34
relay/components/App.js
Normal file
34
relay/components/App.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import Relay from 'react-relay';
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Widget list</h1>
|
||||
<ul>
|
||||
{this.props.viewer.widgets.edges.map(edge =>
|
||||
<li key={edge.node.id}>{edge.node.name} (ID: {edge.node.id})</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Relay.createContainer(App, {
|
||||
fragments: {
|
||||
viewer: () => Relay.QL`
|
||||
fragment on User {
|
||||
widgets(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id,
|
||||
name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
12
relay/routes/AppHomeRoute.js
Normal file
12
relay/routes/AppHomeRoute.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Relay from 'react-relay';
|
||||
|
||||
export default class extends Relay.Route {
|
||||
static queries = {
|
||||
viewer: () => Relay.QL`
|
||||
query {
|
||||
viewer
|
||||
}
|
||||
`,
|
||||
};
|
||||
static routeName = 'AppHomeRoute';
|
||||
}
|
||||
30
routes/query.graphql
Normal file
30
routes/query.graphql
Normal file
@@ -0,0 +1,30 @@
|
||||
query GetAllCountries {
|
||||
countries {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
query GetYearData {
|
||||
data(year: $years, country_id: $country_ids){
|
||||
...DatumFragment
|
||||
}
|
||||
}
|
||||
query GetCountryData($id: Integer!) {
|
||||
countries(id: $id){
|
||||
data {
|
||||
...DatumFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment DatumFragment on Datum {
|
||||
year
|
||||
population
|
||||
gdp
|
||||
total_emissions
|
||||
energy_emissions
|
||||
industrial_emissions
|
||||
agriculture_emissions
|
||||
waste_emissions
|
||||
lucf_emissions
|
||||
}
|
||||
37
routes/shema.js
Normal file
37
routes/shema.js
Normal file
@@ -0,0 +1,37 @@
|
||||
var queryType = new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: () => ({
|
||||
countries: {
|
||||
type: new GraphQLList(Country.graphql),
|
||||
args: {
|
||||
id: {
|
||||
type: new GraphQLNonNull(GraphQLInteger)
|
||||
},
|
||||
name: {
|
||||
type: new GraphQLNonNull(GraphQLString)
|
||||
}
|
||||
},
|
||||
resolve: (root, args) => {
|
||||
Country.sql.find(args)
|
||||
}
|
||||
},
|
||||
data: {
|
||||
type: new GraphQLList(Datum.graphql),
|
||||
args: {
|
||||
years: {
|
||||
type: new GraphQLList(GraphQLInteger)
|
||||
},
|
||||
country_id: {
|
||||
type: new GraphQLList(GraphQLInteger)
|
||||
}
|
||||
},
|
||||
resolve: (root, args)=>{
|
||||
Datum.findAll({where: args})
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = new GraphQLSchema({
|
||||
query: queryType
|
||||
});
|
||||
29
scripts/updateSchema.js
Executable file
29
scripts/updateSchema.js
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env babel-node --optional es7.asyncFunctions
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Schema } from '../data/schema';
|
||||
import { graphql } from 'graphql';
|
||||
import { introspectionQuery, printSchema } from 'graphql/utilities';
|
||||
|
||||
// Save JSON of full schema introspection for Babel Relay Plugin to use
|
||||
(async () => {
|
||||
var result = await (graphql(Schema, introspectionQuery));
|
||||
if (result.errors) {
|
||||
console.error(
|
||||
'ERROR introspecting schema: ',
|
||||
JSON.stringify(result.errors, null, 2)
|
||||
);
|
||||
} else {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../data/schema.json'),
|
||||
JSON.stringify(result, null, 2)
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
// Save user readable type system shorthand of schema
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../data/schema.graphql'),
|
||||
printSchema(Schema)
|
||||
);
|
||||
6
views/error.jade
Normal file
6
views/error.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
3
views/index.jade
Normal file
3
views/index.jade
Normal file
@@ -0,0 +1,3 @@
|
||||
extends layout
|
||||
block content
|
||||
div(id="root")
|
||||
35
views/layout.jade
Normal file
35
views/layout.jade
Normal file
@@ -0,0 +1,35 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(http-equiv='content-type', content='text/html; charset=UTF-8')
|
||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||
!= css("vendor")
|
||||
!= css("application")
|
||||
title Spike Prototype
|
||||
body
|
||||
#spike_container
|
||||
#spike_content
|
||||
nav.navbar.navbar-default(style='margin-bottom:0px;')
|
||||
.container
|
||||
.navbar-header
|
||||
button.navbar-toggle.collapsed(type='button', data-toggle='collapse', data-target='#navbar', aria-expanded='false', aria-controls='navbar')
|
||||
span.sr-only Toggle navigation
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
a.navbar-brand(href='/') Spike
|
||||
#navbar.collapse.navbar-collapse
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
li
|
||||
a(href='/') Spike
|
||||
block content
|
||||
#spike_footer
|
||||
.container Footer
|
||||
script(type='text/javascript').
|
||||
// Force `fetch` polyfill to workaround Chrome not displaying request body
|
||||
// in developer tools for the native `fetch`.
|
||||
self.fetch = null;
|
||||
script(src='http://localhost:3000/webpack-dev-server.js')
|
||||
!= js("vendor")
|
||||
script(src='/assets/js/application.js')
|
||||
Reference in New Issue
Block a user