Commit 077df679 authored by Rodrigo Tapia-McClung's avatar Rodrigo Tapia-McClung

Serve MVTs form PostGIS and interact with them

parent 774ca6b6
node_modules
config/dbconfig.json
public/files
cache
\ No newline at end of file
{
"host": "host.example.com",
"user": "dbuser",
"password": "dbpassword",
"database": "dbname",
"ssl": true,
"port": 5432,
"max": 20,
"idleTimeoutMillis": 30000,
"connectionTimeoutMillis": 2000
}
\ No newline at end of file
{
"port": 8090
}
\ No newline at end of file
const {Pool} = require('pg');
const dbconfig = require('./config/dbconfig.json');
const pool = new Pool(dbconfig);
pool.connect();
// based on https://github.com/tobinbradley/dirt-simple-postgis-http-api/blob/master/routes/geobuf.js
const sqlTableName = require('./utils/sqltablename.js');
const sql = (params, query) => {
let bounds = query.bounds ? query.bounds.split(',').map(Number) : null
bounds && bounds.length === 3
? (bounds = merc.bbox(bounds[1], bounds[2], bounds[0]))
: null
return `
SELECT
ST_AsGeobuf(q, 'geom')
FROM
(
SELECT
ST_Transform(${query.geom_column}, 4326) as geom
${query.columns ? `, ${query.columns}` : ''}
FROM
${sqlTableName(params.table)}
${
bounds
? `, (SELECT ST_SRID(${query.geom_column}) as srid FROM ${
sqlTableName(params.table)
} LIMIT 1) sq`
: ''
}
-- Optional Filter
${query.filter || bounds ? 'WHERE' : ''}
${query.filter ? `${query.filter}` : ''}
${query.filter && bounds ? 'AND' : ''}
${
bounds
? `
${query.geom_column} &&
ST_Transform(
ST_MakeEnvelope(${bounds.join()}, 4326),
srid
)
`
: ''
}
) as q;
`
}
module.exports = function(app, pool) {
/**
* @swagger
*
* /data/geobuf/{table}:
* get:
* description: return table as geobuf
* tags: ['geodata']
* produces:
* - application/x-protobuf
* parameters:
* - name: table
* description: name of table or view
* in: path
* required: true
* type: string
* - name: geom_column
* description: name of geometry column (default 'geom')
* in: query
* required: false
* - name: columns
* description: optional comma seperated list of attribute columns to be added to the mvt geometries
* in: query
* required: false
* type: string
* - name: filter
* description: 'Optional filter parameters for a SQL WHERE statement.'
* in: query
* type: string
* required: false
* - name: bounds
* description: 'Optionally limit output to features that intersect bounding box. Can be expressed as a bounding box (sw.lng, sw.lat, ne.lng, ne.lat) or a Z/X/Y tile (0,0,0).'
* in: query
* type: string
* pattern: '^-?[0-9]{0,20}.?[0-9]{1,20}?(,-?[0-9]{0,20}.?[0-9]{1,20}?){2,3}$'
* responses:
* 200:
* description: geobuf data
* 422:
* description: invalid datasource or columnname
*/
app.get('/data/geobuf/:table', async (req, res)=> {
if (!req.query.geom_column) {
req.query.geom_column = 'geom';
}
const sqlString = sql(req.params, req.query);
try {
const result = await pool.query(sqlString);
res.set('Content-Type', 'text/x-protobuf').send(result.rows[0].st_asgeobuf);
} catch(err) {
console.log(err);
let status = 500;
switch (err.code) {
case '42P01':
// table does not exist
status = 422;
break;
case '42703':
// column does not exist
status = 422;
break;
default:
}
res.status(status).json({error:err.message})
}
})
}
\ No newline at end of file
// based on https://raw.githubusercontent.com/tobinbradley/dirt-simple-postgis-http-api/master/routes/geojson.js
const sqlTableName = require('./utils/sqltablename.js');
const sql = (params, query) => {
let bounds = query.bounds ? query.bounds.split(',').map(Number) : null;
bounds && bounds.length === 3 ? bounds = merc.bbox(bounds[1], bounds[2], bounds[0]) : null;
return `
SELECT
Row_to_json(fc) as geojson
FROM (
SELECT
'FeatureCollection' AS type,
COALESCE(Array_to_json(Array_agg(f)), '[]'::json) AS features
FROM (
SELECT
'Feature' AS type,
St_asgeojson(ST_Transform(lg.${query.geom_column}, 4326))::json AS geometry,
${query.columns ? `
Row_to_json(
(
SELECT
l
FROM
(SELECT ${query.columns}) AS l
)
) AS properties
` : `'{}'::json AS properties`}
FROM
${sqlTableName(params.table)} AS lg
${bounds ? `, (SELECT ST_SRID(${query.geom_column}) as srid FROM ${sqlTableName(params.table)} LIMIT 1) sq` : ''}
-- Optional Filter
${query.filter || bounds ? 'WHERE' : ''}
${query.filter ? `${query.filter}` : '' }
${query.filter && bounds ? 'AND' : ''}
${bounds ? `
${query.geom_column} &&
ST_Transform(
ST_MakeEnvelope(${bounds.join()}, 4326),
srid
)
` : ''}
) AS f
) AS fc;
`
}
module.exports = function(app, pool) {
/**
* @swagger
*
* /data/geojson/{table}:
* get:
* description: return table as geojson
* tags: ['geodata']
* produces:
* - application/json
* parameters:
* - name: table
* description: name of table or view
* in: path
* required: true
* type: string
* - name: geom_column
* description: name of geometry column (default 'geom')
* in: query
* required: false
* - name: columns
* description: optional comma seperated list of attribute columns to be added to the mvt geometries
* in: query
* required: false
* type: string
* - name: filter
* description: 'Optional filter parameters for a SQL WHERE statement.'
* in: query
* type: string
* required: false
* - name: bounds
* description: 'Optionally limit output to features that intersect bounding box. Can be expressed as a bounding box (sw.lng, sw.lat, ne.lng, ne.lat) or a Z/X/Y tile (0,0,0).'
* in: query
* type: string
* pattern: '^-?[0-9]{0,20}.?[0-9]{1,20}?(,-?[0-9]{0,20}.?[0-9]{1,20}?){2,3}$'
* responses:
* 200:
* description: geojson
* 422:
* description: invalid datasource or columnname
*/
app.get('/data/geojson/:table', async (req, res) => {
if (!req.query.geom_column) {
req.query.geom_column = 'geom';
}
const sqlString = sql(req.params, req.query);
try {
const result = await pool.query(sqlString);
res.json(result.rows[0].geojson)
} catch(err) {
console.log(err);
let status = 500;
switch (err.code) {
case '42P01':
// table does not exist
status = 422;
break;
case '42703':
// column does not exist
status = 422;
break;
default:
}
res.status(status).json({error:err.message})
}
})
}
\ No newline at end of file
// based on https://raw.githubusercontent.com/tobinbradley/dirt-simple-postgis-http-api/master/routes/list_layers.js
const sql = () => {
return `
SELECT current_database()::character varying(256) AS f_table_catalog,
n.nspname AS f_table_schema,
c.relname AS f_table_name,
a.attname AS f_geometry_column,
COALESCE(postgis_typmod_dims(a.atttypmod), sn.ndims, 2) AS coord_dimension,
COALESCE(NULLIF(postgis_typmod_srid(a.atttypmod), 0), sr.srid, 0) AS srid,
replace(replace(COALESCE(NULLIF(upper(postgis_typmod_type(a.atttypmod)), 'GEOMETRY'::text), st.type, 'GEOMETRY'::text), 'ZM'::text, ''::text), 'Z'::text, ''::text)::character varying(30) AS type,
((c.reltuples/case when c.relpages=0 then 1 else c.relpages end) * (pg_relation_size(c.oid) / (current_setting('block_size')::integer)))::bigint as estimated_rows,
case when relkind='r' then 'table' when relkind='v' then 'view' when relkind='m' then 'mview' when relkind='f' then 'ftable' else 'other(' || relkind || ')' end table_type
FROM pg_class c
JOIN pg_attribute a ON a.attrelid = c.oid AND NOT a.attisdropped
JOIN pg_namespace n ON c.relnamespace = n.oid
JOIN pg_type t ON a.atttypid = t.oid
LEFT JOIN ( SELECT s.connamespace,
s.conrelid,
s.conkey,
replace(split_part(s.consrc, ''''::text, 2), ')'::text, ''::text) AS type
FROM pg_constraint s
WHERE s.consrc ~~* '%geometrytype(% = %'::text) st ON st.connamespace = n.oid AND st.conrelid = c.oid AND (a.attnum = ANY (st.conkey))
LEFT JOIN ( SELECT s.connamespace,
s.conrelid,
s.conkey,
replace(split_part(s.consrc, ' = '::text, 2), ')'::text, ''::text)::integer AS ndims
FROM pg_constraint s
WHERE s.consrc ~~* '%ndims(% = %'::text) sn ON sn.connamespace = n.oid AND sn.conrelid = c.oid AND (a.attnum = ANY (sn.conkey))
LEFT JOIN ( SELECT s.connamespace,
s.conrelid,
s.conkey,
replace(replace(split_part(s.consrc, ' = '::text, 2), ')'::text, ''::text), '('::text, ''::text)::integer AS srid
FROM pg_constraint s
WHERE s.consrc ~~* '%srid(% = %'::text) sr ON sr.connamespace = n.oid AND sr.conrelid = c.oid AND (a.attnum = ANY (sr.conkey))
WHERE (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'm'::"char", 'f'::"char", 'p'::"char"])) AND NOT c.relname = 'raster_columns'::name AND t.typname = 'geometry'::name AND NOT pg_is_other_temp_schema(c.relnamespace) AND has_table_privilege(c.oid, 'SELECT'::text);
`;
}
module.exports = function(app, pool) {
/**
* @swagger
*
* /data/list_layers:
* get:
* description: get list of available tables
* tags: ['meta']
* summary: 'list PostGIS layers'
* produces:
* - application/json
* responses:
* 200:
* description: list of layers
* content:
* application/json
* schema:
* type: array
* items:
* type: object
* properties:
* f_table_catalog:
* description: name of database (group of tables)
* type: string
* f_table_schema:
* description: schema name (sub-group of tables)
* type: string
* f_table_name:
* description: name of table or view
* type: string
* f_geometry_column:
* description: name of geometry column for geometries
* type: string
* coord_dimension:
* description: number of dimensions, usually 2 or 3
* type: integer
* srid:
* description: EPSG id of spatial reference system (4326=WGS 84/GPS coordinates, 3857=webmercator coordinates)
* type: integer
* estimated_rows:
* description: estimated number of rows in table, 0 (unknown) for views or foreign tables
* type: integer
* table_type:
* description: type of table, 1 of 'table', 'view', 'mview' (material view), 'ftable' (foreign table), 'other'
* type: string
* 500:
* description: unexpected error
*/
app.get('/data/list_layers', async (req, res)=>{
try {
const sqlString = sql()
const result = await pool.query(sqlString);
const layers = result.rows
res.json(layers)
} catch(err) {
res.status(500).json({error: err.message})
}
})
}
\ No newline at end of file
// based on https://raw.githubusercontent.com/tobinbradley/dirt-simple-postgis-http-api/master/routes/mvt.js
const sqlTableName = require('./utils/sqltablename.js');
const sm = require('@mapbox/sphericalmercator');
const merc = new sm({
size: 256
})
function toBoolean(value) {
if (!value) {
return false;
}
if (typeof value === 'string'){
if (value.trim() === '') {
return false;
} else if (Number(value) === NaN) {
switch (value.toLowerCase().trim()) {
case 'no':
case 'false':
case 'n':
case 'f':
return false;
default:
return true;
}
} else {
return Number(value) !== 0;
}
}
if (Number(value)!== NaN) {
return Number(value) !== 0;
}
return true;
}
function queryColumnsNotNull(query) {
const queryColumns = query.columns;
const includeNulls = toBoolean(query.include_nulls);
if (!includeNulls && queryColumns) {
return ` and (${queryColumns.split(',').map(column=>`${column} is not null`).join(' or ')})`
}
return ''
}
const sql = (params, query) => {
let bounds = merc.bbox(params.x, params.y, params.z, false, '900913')
return `
SELECT ST_AsMVT(q, '${params.table}', 4096, 'geom')
FROM
(SELECT ${query.columns ? `${query.columns},` : ''}
ST_AsMVTGeom(
ST_Transform(${query.geom_column}, 3857),
ST_MakeBox2D(ST_Point(${bounds[0]}, ${bounds[1]}), ST_Point(${bounds[2]}, ${bounds[3]}))
) geom
FROM
(SELECT ${query.columns ? `${query.columns},` : ''} ${query.geom_column}, srid
FROM
${sqlTableName(params.table)},
(SELECT ST_SRID(${query.geom_column}) AS srid FROM ${sqlTableName(params.table)}
WHERE ${query.geom_column} is not null LIMIT 1) a
WHERE
${query.geom_column} is not null AND
ST_transform(
ST_MakeEnvelope(${bounds.join()}, 3857),
srid
) && ${query.geom_column}
${queryColumnsNotNull(query)}
-- Optional Filter
${query.filter ? `AND ${query.filter}` : ''}
) r
) q
`
} // TODO, use sql place holders $1, $2 etc. instead of inserting user-parameters into query
module.exports = function(app, pool, cache) {
let cacheMiddleWare = async(req, res, next) => {
if (!cache) {
next();
return;
}
const cacheDir = `${req.params.datasource}/mvt/${req.params.z}/${req.params.x}/${req.params.y}`;
const key = ((req.query.geom_column?req.query.geom_column:'geom') + (req.query.columns?','+req.query.columns:'')) + (toBoolean(req.query.include_nulls)?'_includenulls':'')
.replace(/[\W]+/g, '_');
const mvt = await cache.getCachedFile(cacheDir, key);
if (mvt) {
//console.log(`cache hit for ${cacheDir}?${key}`);
if (mvt.length === 0) {
res.status(204)
}
res.header('Content-Type', 'application/x-protobuf').send(mvt);
return;
} else {
res.sendResponse = res.send;
res.send = (body) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
cache.setCachedFile(cacheDir, key, body);
}
res.sendResponse(body);
}
next();
}
}
/**
* @swagger
*
* /data/{datasource}/mvt/{z}/{x}/{y}:
* get:
* description: get mapbox vector tile (mvt)
* tags: ['geodata']
* produces:
* - application/x-protobuf
* parameters:
* - name: datasource
* description: name of postgis datasource
* in: path
* required: true
* type: string
* - name: z
* description: zoom level of tile
* in: path
* required: true
* type: number
* - name: x
* description: x value (column number) of tile
* in: path
* required: true
* type: number
* - name: y
* description: y value (row number) of tile
* in: path
* required: true
* type: number
* - name: geom_column
* description: name of geometry column (default 'geom')
* in: query
* required: false
* - name: columns
* description: optional comma seperated list of attribute columns to be added to the mvt geometries
* in: query
* required: false
* type: string
* - name: include_nulls
* description: 'optional parameter to include geometries where all attribute columns are null (default: false)'
* in: query
* required: false
* type: string
* responses:
* 200:
* description: vector tile
* 204:
* description: no data (empty tile)
* 422:
* description: invalid datasource or columnname
*/
app.get('/data/:datasource/mvt/:z/:x/:y', cacheMiddleWare, async (req, res)=>{
if (!req.query.geom_column) {
req.query.geom_column = 'geom'; // default
}
req.params.table = req.params.datasource;
const sqlString = sql(req.params, req.query);
// console.log(sqlString);
try {
const result = await pool.query(sqlString);
const mvt = result.rows[0].st_asmvt
if (mvt.length === 0) {
res.status(204)
}
res.header('Content-Type', 'application/x-protobuf').send(mvt);
} catch(err) {
console.log(err);
let status = 500;
switch (err.code) {
case '42P01':
// table does not exist
status = 422;
break;
case '42703':
// column does not exist
status = 422;
break;
default:
}
res.status(status).json({error:err.message})
}
})
}
\ No newline at end of file
This diff is collapsed.
{
"name": "pgserver",
"version": "0.0.0",
"description": "PGserver receives, converts and serves geo-data between postgis and http-clients.",
"main": "pgserver.js",
"scripts": {
"dev": "nodemon pgserver.js",
"start": "node pgserver.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Anne Blankert, Geodan",
"license": "ISC",
"dependencies": {
"@mapbox/sphericalmercator": "^1.1.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"morgan": "^1.9.1",
"nodemon": "^1.19.2",
"pg": "^7.12.1"
}
}
const pgserverconfig = require('./config/pgserver.json')
const express = require('express');
const logger = require('morgan');
const cors = require('cors');
const app = express();
//app.use(logger('dev'));
app.use(logger('combined', {
skip: function (req, res) { return res.statusCode < 400 }
}));
app.use(cors());
app.use('/', express.static(__dirname + '/public'));
const {Pool} = require('pg');
const dbconfig = require('./config/dbconfig.json');
const readOnlyPool = new Pool(dbconfig);
readOnlyPool.connect();
const DirCache = require('./utils/dircache.js')
const cache = new DirCache(`./cache/${dbconfig.database?dbconfig.database:process.env.PGDATABASE?process.env.PGDATABASE:''}`);
const mvt = require('./mvt.js')(app, readOnlyPool, cache);
const geojson = require('./geojson.js')(app, readOnlyPool);
const geobuf = require('./geobuf.js')(app, readOnlyPool);
const listLayers = require('./list_layers.js')(app, readOnlyPool);
const query = require('./query.js')(app, readOnlyPool);
const server = app.listen(pgserverconfig.port);
server.setTimeout(600000);
console.log(`pgserver listening on port ${pgserverconfig.port}`);
module.exports = app;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Interactuar con MVTs</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#mexmap { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="mexmap"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>
<script src="../js/functions.js"></script>
</body>
</html>
\ No newline at end of file
/*
* Copyright 2020 - All rights reserved.
* Rodrigo Tapia-McClung
*
* November 2020
*/
// define MVT layer for given table and all trimester column
const mapboxLayer = (table) => {
const baseUrl = new URL(`/data`, window.location.href).href;
let pbfLayer = {
id: table,
source: {
type: "vector",
tiles: [`${baseUrl}/${table}/mvt/{z}/{x}/{y}?geom_column=denue_geom_4326&columns=denue_nombre, sector, sector_nombre`],
maxzoom: 19,
minzoom: 6
},
"source-layer": table,
type: "circle",
"paint": {
"circle-opacity": 0.5,
"circle-radius": 3.5,
"circle-color": [
"interpolate",
["linear"],
["get", "sector"],
10, "rgb(49,54,149)",
50, "rgb(255,255,191)",
100, "rgb(158,1,66)"
]
}
}
return pbfLayer;
}
let map = new mapboxgl.Map({
container: 'mexmap',
accessToken: "pk.eyJ1IjoiZGV2ZWxvcGdlbyIsImEiOiJja2dwcXFic20wYnJnMzBrbG11d3dwYTkyIn0.4WwFOH6C7hDQXV9obU6mAw",
style: 'mapbox://styles/mapbox/dark-v10',
center: [-100.94, 21.15],
zoom: 7
});
map.addControl(new mapboxgl.NavigationControl());
let tiles = mapboxLayer("dim_denue2019");
map.on("style.load", () => {
map.addLayer(tiles);
});
map.on('click', function (e) {
var features = map.queryRenderedFeatures(e.point,
{ layers: ['dim_denue2019'] }
);
if (features.length != 0) {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(features[0].properties['denue_nombre'])
.addTo(map);
}
});
// Change the cursor to a pointer when the mouse is over the states layer.
map.on('mouseenter', 'dim_denue2019', function () {
map.getCanvas().style.cursor = 'pointer';
});
// Change it back to a pointer when it leaves.
map.on('mouseleave', 'dim_denue2019', function () {
map.getCanvas().style.cursor = '';
});
\ No newline at end of file
const sqlTableName = require('./utils/sqltablename.js');
const sql = (params, query) => {
return `
SELECT
${query.columns}
FROM
${sqlTableName(params.table)}
-- Optional Filter
${query.filter ? `WHERE ${query.filter}` : '' }
-- Optional Group
${query.group ? `GROUP BY ${query.group}` : '' }
-- Optional sort
${query.sort ? `ORDER BY ${query.sort}` : '' }
-- Optional limit
${query.limit ? `LIMIT ${query.limit}` : '' }
`
}
module.exports = function(app, pool) {
/**
* @swagger
*
* /data/query/{table}:
* get:
* description: Query a table or view.
* tags: ['api']
* produces:
* - application/json
* parameters:
* - name: table
* description: The name of the table or view.
* in: path
* required: true
* type: string
* - name: columns
* description: columns to return (default '*')
* in: query
* required: false
* - name: filter
* description: Optional filter parameters for a SQL WHERE statement.
* in: query
* required: false
* type: string
* - name: sort
* description: Optional sort by column(s).
* in: query
* required: false
* type: string
* - name: limit
* description: Optional limit to the number of output features.
* in: query
* required: false
* type: integer
* - name: group
* description: Optional column(s) to group by.
* in: query
* required: false
* type: string
* responses:
* 200:
* description: query result
* 422:
* description: invalid table or column name
*/
app.get('/data/query/:table', async (req, res)=> {
const sqlString = sql(req.params, req.query);
try {
const result = await pool.query(sqlString);
res.json(result.rows);
} catch (err) {
res.status(422).json({error: err});
}
})
}
const fs = require('fs')
const path = require('path');
module.exports = class DirCache {
constructor(cacheRoot) {
if (!cacheRoot) {
this.cacheRoot = path.resolve('./cache/mvt')
} else {
this.cacheRoot = cacheRoot;
}
}
getCachedFile(dir, key) {
const file = path.join(this.cacheRoot, dir, key);
return new Promise((resolve, reject)=>{
fs.readFile(file, (err, data) => {
if (err) {
resolve(null);
} else {
resolve(data)
}
})
})
}
setCachedFile(dir, key, data) {
const dirName = path.join(this.cacheRoot, dir);
const filePath = path.join(dirName, key);
fs.mkdirSync(dirName, {recursive: true});
return new Promise((resolve, reject) => {
fs.writeFile(filePath, data, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
})
}
}
function cacheDirName(params) {
return `${path.dirname(__dirname)}/cache/mvt/${params.table}/${params.z}/${params.x}/${params.y}`
}
function cacheFileName(query) {
if (query.columns) {
return query.columns;
}
return 'noquery';
}
function getCache(params, query) {
const dirname = cacheDirName(params);
const filename = cacheFileName(query);
//console.log(`getCache: ${dirname}`);
return fsPromises.readFile(`${dirname}/${filename}`)
.then(data=>data)
.catch(error=>null);
}
function setCache(params, query, data) {
const dirname = cacheDirName(params);
const filename = cacheFileName(query);
//console.log(`setCache: ${dirname}`);
return fsPromises.writeFile(`${dirname}/${filename}`, data)
.then(() => {return})
.catch(err=>err);
}
function lockCache(params, query) {
const dirname = cacheDirName(params);
const filename = cacheFileName(query);
fs.mkdirSync(dirname, {recursive: true});
return fsPromises.writeFile(`${dirname}/${filename}.lck`, 'lock', {flag: 'wx'})
.then(()=>{
return true
})
.catch(err=>{
return fsPromises.stat(`${dirname}/${filename}.lck`)
.then(st=>{
console.log(Date.now() - st.ctimeMs);
if (Date.now() - st.ctimeMs > 240000) {
return unlockCache(params,query).then(()=>lockCache(params,query));
} else {
return false;
}
})
.catch(err=>{
console.log(err);
return false;
});
});
}
function unlockCache(params, query){
const dirname = cacheDirName(params);
const filename = cacheFileName(query);
return fsPromises.unlink(`${dirname}/${filename}.lck`)
.then(()=>true)
.catch(err=>{
console.log(`unlockCache: error: ${err}`);
return false;
})
}
function wait(ms) {
return new Promise((r, j)=>setTimeout(r, ms));
}
async function waitForCache(params, query) {
const dirname = cacheDirName(params);
const filename = cacheFileName(query);
for (let i = 0; i < 180; i++) {
console.log(`waiting for cache.. ${i}`);
await wait(1000);
data = await getCache(params, query);
if (data) {
console.log(`cache wait done.. ${i}`)
return data;
}
}
console.log(`cache wait failed`);
return null;
}
\ No newline at end of file
module.exports = function(tableName) {
return tableName
.split('.')
.map(part=>{
if (part.search(/^[0-9]|[A-Z]|\s/) === -1) {
return part;
}
let newPart = part;
if (part.search('"') !== -1) {
newPart = part.replace(/"/g, '\"')
}
return `"${newPart}"`
})
.join('.')
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment