@cubejs-backend/api-gateway
Advanced tools
Comparing version 0.10.62 to 0.11.0
@@ -6,2 +6,13 @@ # Change Log | ||
# [0.11.0](https://github.com/statsbotco/cubejs-client/compare/v0.10.62...v0.11.0) (2019-10-15) | ||
### Features | ||
* Sockets Preview ([#231](https://github.com/statsbotco/cubejs-client/issues/231)) ([89fc762](https://github.com/statsbotco/cubejs-client/commit/89fc762)), closes [#221](https://github.com/statsbotco/cubejs-client/issues/221) | ||
## [0.10.62](https://github.com/statsbotco/cubejs-client/compare/v0.10.61...v0.10.62) (2019-10-11) | ||
@@ -8,0 +19,0 @@ |
@@ -8,4 +8,4 @@ const chrono = require('chrono-node'); | ||
dateString = dateString.toLowerCase(); | ||
if (dateString.match(/(this|last)\s+(day|week|month|year|quarter)/)) { | ||
const match = dateString.match(/(this|last)\s+(day|week|month|year|quarter)/); | ||
if (dateString.match(/(this|last)\s+(day|week|month|year|quarter|hour|minute|second)/)) { | ||
const match = dateString.match(/(this|last)\s+(day|week|month|year|quarter|hour|minute|second)/); | ||
let start = moment(); | ||
@@ -19,4 +19,4 @@ let end = moment(); | ||
momentRange = [start.startOf(span), end.endOf(span)]; | ||
} else if (dateString.match(/last\s+(\d+)\s+(day|week|month|year|quarter)/)) { | ||
const match = dateString.match(/last\s+(\d+)\s+(day|week|month|year|quarter)/); | ||
} else if (dateString.match(/last\s+(\d+)\s+(day|week|month|year|quarter|hour|minute|second)/)) { | ||
const match = dateString.match(/last\s+(\d+)\s+(day|week|month|year|quarter|hour|minute|second)/); | ||
const span = match[2] === 'week' ? 'isoWeek' : match[2]; | ||
@@ -28,6 +28,6 @@ momentRange = [ | ||
} else if (dateString.match(/today/)) { | ||
momentRange = [moment(), moment()]; | ||
momentRange = [moment().startOf('day'), moment().endOf('day')]; | ||
} else if (dateString.match(/yesterday/)) { | ||
const yesterday = moment().add(-1, 'day'); | ||
momentRange = [yesterday, yesterday]; | ||
momentRange = [yesterday.startOf('day'), yesterday.endOf('day')]; | ||
} else { | ||
@@ -46,3 +46,3 @@ const results = chrono.parse(dateString); | ||
} | ||
return momentRange.map(d => d.format(moment.HTML5_FMT.DATE)); | ||
return momentRange.map(d => d.format(moment.HTML5_FMT.DATETIME_LOCAL_MS)); | ||
}; |
300
index.js
@@ -8,2 +8,4 @@ const jwt = require('jsonwebtoken'); | ||
const UserError = require('./UserError'); | ||
const SubscriptionServer = require('./SubscriptionServer'); | ||
const LocalSubscriptionStore = require('./LocalSubscriptionStore'); | ||
@@ -220,3 +222,3 @@ const toConfigMap = (metaConfig) => ( | ||
const coerceForSqlQuery = (query, req) => ({ | ||
const coerceForSqlQuery = (query, context) => ({ | ||
...query, | ||
@@ -226,3 +228,3 @@ timeDimensions: (query.timeDimensions || []) | ||
contextSymbols: { | ||
userContext: req.authInfo && req.authInfo.u || {} | ||
userContext: context.authInfo && context.authInfo.u || {} | ||
} | ||
@@ -239,5 +241,7 @@ }); | ||
this.checkAuthMiddleware = options.checkAuthMiddleware || this.checkAuth.bind(this); | ||
this.checkAuthFn = options.checkAuth || this.defaultCheckAuth.bind(this); | ||
this.basePath = options.basePath || '/cubejs-api'; | ||
// eslint-disable-next-line no-unused-vars | ||
this.queryTransformer = options.queryTransformer || (async (query, context) => query); | ||
this.subscriptionStore = options.subscriptionStore || new LocalSubscriptionStore(); | ||
} | ||
@@ -247,77 +251,168 @@ | ||
app.get(`${this.basePath}/v1/load`, this.checkAuthMiddleware, (async (req, res) => { | ||
try { | ||
if (!req.query.query || req.query.query === 'undefined') { | ||
throw new UserError(`query param is required`); | ||
} | ||
const query = JSON.parse(req.query.query); | ||
this.log(req, { | ||
type: 'Load Request', | ||
query: req.query.query | ||
}); | ||
const normalizedQuery = await this.queryTransformer(normalizeQuery(query), this.contextByReq(req)); | ||
const [compilerSqlResult, metaConfigResult] = await Promise.all([ | ||
this.getCompilerApi(req).getSql(coerceForSqlQuery(normalizedQuery, req)), | ||
this.getCompilerApi(req).metaConfig() | ||
]); | ||
const sqlQuery = compilerSqlResult; | ||
const metaConfig = metaConfigResult; | ||
const annotation = prepareAnnotation(metaConfig, normalizedQuery); | ||
const aliasToMemberNameMap = prepareAliasToMemberNameMap(metaConfig, sqlQuery, normalizedQuery); | ||
const toExecute = { | ||
...sqlQuery, | ||
query: sqlQuery.sql[0], | ||
values: sqlQuery.sql[1], | ||
continueWait: true, | ||
renewQuery: normalizedQuery.renewQuery | ||
}; | ||
const response = await this.getAdapterApi(req).executeQuery(toExecute); | ||
this.log(req, { | ||
type: 'Load Request Success', | ||
query: req.query.query, | ||
}); | ||
const flattenAnnotation = { | ||
...annotation.measures, | ||
...annotation.dimensions, | ||
...annotation.timeDimensions | ||
}; | ||
res.json({ | ||
query: normalizedQuery, | ||
data: transformData(aliasToMemberNameMap, flattenAnnotation, response.data), | ||
annotation | ||
}); | ||
} catch (e) { | ||
this.handleError(e, req, res); | ||
} | ||
await this.load({ | ||
query: req.query.query, | ||
context: this.contextByReq(req), | ||
res: this.resToResultFn(res) | ||
}); | ||
})); | ||
app.get(`${this.basePath}/v1/subscribe`, this.checkAuthMiddleware, (async (req, res) => { | ||
await this.load({ | ||
query: req.query.query, | ||
context: this.contextByReq(req), | ||
res: this.resToResultFn(res) | ||
}); | ||
})); | ||
app.get(`${this.basePath}/v1/sql`, this.checkAuthMiddleware, (async (req, res) => { | ||
try { | ||
if (!req.query.query || req.query.query === 'undefined') { | ||
throw new UserError(`query param is required`); | ||
} | ||
const query = JSON.parse(req.query.query); | ||
const normalizedQuery = await this.queryTransformer(normalizeQuery(query), this.contextByReq(req)); | ||
const sqlQuery = await this.getCompilerApi(req).getSql(coerceForSqlQuery(normalizedQuery, req)); | ||
res.json({ | ||
sql: sqlQuery | ||
}); | ||
} catch (e) { | ||
this.handleError(e, req, res); | ||
} | ||
await this.sql({ | ||
query: req.query.query, | ||
context: this.contextByReq(req), | ||
res: this.resToResultFn(res) | ||
}); | ||
})); | ||
app.get(`${this.basePath}/v1/meta`, this.checkAuthMiddleware, (async (req, res) => { | ||
try { | ||
const metaConfig = await this.getCompilerApi(req).metaConfig(); | ||
const cubes = metaConfig.map(c => c.config); | ||
res.json({ cubes }); | ||
} catch (e) { | ||
this.handleError(e, req, res); | ||
} | ||
await this.meta({ | ||
context: this.contextByReq(req), | ||
res: this.resToResultFn(res) | ||
}); | ||
})); | ||
} | ||
getCompilerApi(req) { | ||
initSubscriptionServer(sendMessage) { | ||
return new SubscriptionServer(this, sendMessage, this.subscriptionStore); | ||
} | ||
async meta({ context, res }) { | ||
try { | ||
const metaConfig = await this.getCompilerApi(context).metaConfig(); | ||
const cubes = metaConfig.map(c => c.config); | ||
res({ cubes }); | ||
} catch (e) { | ||
this.handleError({ | ||
e, context, res | ||
}); | ||
} | ||
} | ||
async sql({ query, context, res }) { | ||
try { | ||
query = this.parseQueryParam(query); | ||
const normalizedQuery = await this.queryTransformer(normalizeQuery(query), context); | ||
const sqlQuery = await this.getCompilerApi(context).getSql(coerceForSqlQuery(normalizedQuery, context)); | ||
res.json({ | ||
sql: sqlQuery | ||
}); | ||
} catch (e) { | ||
this.handleError({ | ||
e, context, query, res | ||
}); | ||
} | ||
} | ||
async load({ query, context, res }) { | ||
try { | ||
query = this.parseQueryParam(query); | ||
this.log(context, { | ||
type: 'Load Request', | ||
query | ||
}); | ||
const normalizedQuery = await this.queryTransformer(normalizeQuery(query), context); | ||
const [compilerSqlResult, metaConfigResult] = await Promise.all([ | ||
this.getCompilerApi(context).getSql(coerceForSqlQuery(normalizedQuery, context)), | ||
this.getCompilerApi(context).metaConfig() | ||
]); | ||
const sqlQuery = compilerSqlResult; | ||
const metaConfig = metaConfigResult; | ||
const annotation = prepareAnnotation(metaConfig, normalizedQuery); | ||
const aliasToMemberNameMap = prepareAliasToMemberNameMap(metaConfig, sqlQuery, normalizedQuery); | ||
const toExecute = { | ||
...sqlQuery, | ||
query: sqlQuery.sql[0], | ||
values: sqlQuery.sql[1], | ||
continueWait: true, | ||
renewQuery: normalizedQuery.renewQuery | ||
}; | ||
const response = await this.getAdapterApi(context).executeQuery(toExecute); | ||
this.log(context, { | ||
type: 'Load Request Success', | ||
query, | ||
}); | ||
const flattenAnnotation = { | ||
...annotation.measures, | ||
...annotation.dimensions, | ||
...annotation.timeDimensions | ||
}; | ||
res({ | ||
query: normalizedQuery, | ||
data: transformData(aliasToMemberNameMap, flattenAnnotation, response.data), | ||
annotation | ||
}); | ||
} catch (e) { | ||
this.handleError({ | ||
e, context, query, res | ||
}); | ||
} | ||
} | ||
async subscribe({ | ||
query, context, res, subscribe, subscriptionState | ||
}) { | ||
try { | ||
this.log(context, { | ||
type: 'Subscribe', | ||
query | ||
}); | ||
let result = null; | ||
let error = null; | ||
if (!subscribe) { | ||
await this.load({ query, context, res }); | ||
return; | ||
} | ||
// TODO subscribe to refreshKeys instead of constantly firing load | ||
await this.load({ | ||
query, | ||
context, | ||
res: (message) => { | ||
if (message.error) { | ||
error = message; | ||
} else { | ||
result = message; | ||
} | ||
} | ||
}); | ||
const state = await subscriptionState(); | ||
if (result && (!state || JSON.stringify(state.result) !== JSON.stringify(result))) { | ||
res(result); | ||
} else if (error) { | ||
res(error); | ||
} | ||
await subscribe({ error, result }); | ||
} catch (e) { | ||
this.handleError({ | ||
e, context, query, res | ||
}); | ||
} | ||
} | ||
resToResultFn(res) { | ||
return (message, { status } = {}) => (status ? res.status(status).json(message) : res.json(message)); | ||
} | ||
parseQueryParam(query) { | ||
if (!query || query === 'undefined') { | ||
throw new UserError(`query param is required`); | ||
} | ||
if (typeof query === 'string') { | ||
query = JSON.parse(query); | ||
} | ||
return query; | ||
} | ||
getCompilerApi(context) { | ||
if (typeof this.compilerApi === 'function') { | ||
return this.compilerApi(this.contextByReq(req)); | ||
return this.compilerApi(context); | ||
} | ||
@@ -327,5 +422,5 @@ return this.compilerApi; | ||
getAdapterApi(req) { | ||
getAdapterApi(context) { | ||
if (typeof this.adapterApi === 'function') { | ||
return this.adapterApi(this.contextByReq(req)); | ||
return this.adapterApi(context); | ||
} | ||
@@ -339,37 +434,37 @@ return this.adapterApi; | ||
handleError(e, req, res) { | ||
handleError({ | ||
e, context, query, res | ||
}) { | ||
if (e instanceof UserError) { | ||
this.log(req, { | ||
this.log(context, { | ||
type: 'User Error', | ||
query: req.query && req.query.query, | ||
query, | ||
error: e.message | ||
}); | ||
res.status(400).json({ error: e.message }); | ||
res({ error: e.message }, { status: 400 }); | ||
} else if (e.error === 'Continue wait') { | ||
this.log(req, { | ||
this.log(context, { | ||
type: 'Continue wait', | ||
query: req.query && req.query.query, | ||
query, | ||
error: e.message | ||
}); | ||
res.status(200).json(e); | ||
res(e, { status: 200 }); | ||
} else if (e.error) { | ||
this.log(req, { | ||
this.log(context, { | ||
type: 'Orchestrator error', | ||
query: req.query && req.query.query, | ||
query, | ||
error: e.error | ||
}); | ||
res.status(400).json(e); | ||
res(e, { status: 400 }); | ||
} else { | ||
this.log(req, { | ||
this.log(context, { | ||
type: 'Internal Server Error', | ||
query: req.query && req.query.query, | ||
query, | ||
error: e.stack || e.toString() | ||
}); | ||
res.status(500).json({ error: e.toString() }); | ||
res({ error: e.toString() }, { status: 500 }); | ||
} | ||
} | ||
async checkAuth(req, res, next) { | ||
const auth = req.headers.authorization; | ||
async defaultCheckAuth(req, auth) { | ||
if (auth) { | ||
@@ -379,6 +474,5 @@ const secret = this.apiSecret; | ||
req.authInfo = jwt.verify(auth, secret); | ||
return next && next(); | ||
} catch (e) { | ||
if (process.env.NODE_ENV === 'production') { | ||
res.status(403).json({ error: 'Invalid token' }); | ||
throw new UserError('Invalid token'); | ||
} else { | ||
@@ -390,16 +484,34 @@ this.log(req, { | ||
}); | ||
return next && next(); | ||
} | ||
} | ||
} else if (process.env.NODE_ENV === 'production') { | ||
res.status(403).send({ error: "Authorization header isn't set" }); | ||
} else { | ||
return next && next(); | ||
throw new UserError("Authorization header isn't set"); | ||
} | ||
return null; | ||
} | ||
log(req, event) { | ||
async checkAuth(req, res, next) { | ||
const auth = req.headers.authorization; | ||
try { | ||
this.checkAuthFn(req, auth); | ||
} catch (e) { | ||
if (e instanceof UserError) { | ||
res.status(403).json({ error: e.message }); | ||
} else { | ||
this.log(req, { | ||
type: 'Auth Error', | ||
token: auth, | ||
error: e.stack || e.toString() | ||
}); | ||
res.status(500).json({ error: e.toString() }); | ||
} | ||
} | ||
if (next) { | ||
next(); | ||
} | ||
} | ||
log(context, event) { | ||
const { type, ...restParams } = event; | ||
this.logger(type, { ...restParams, authInfo: req.authInfo }); | ||
this.logger(type, { ...restParams, authInfo: context.authInfo }); | ||
} | ||
@@ -406,0 +518,0 @@ } |
@@ -5,3 +5,3 @@ { | ||
"author": "Statsbot, Inc.", | ||
"version": "0.10.62", | ||
"version": "0.11.0", | ||
"engines": { | ||
@@ -25,3 +25,3 @@ "node": ">=8.11.1" | ||
"license": "Apache-2.0", | ||
"gitHead": "cc6d0a247b344a55d01265daf04f2732975c40bc" | ||
"gitHead": "b62d0e18bc793a1182fb2e3d26baf114a9cd848c" | ||
} |
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
42615
10
688
1