import _ from "lodash" import DataSource from "./DataSource" import LRU from "lru-cache" import querystring from "querystring" import { JsonQLQuery } from "@mwater/jsonql" import { Row } from "./types" /** Caching data source for mWater. Requires jQuery. require explicitly: require('@mwater/expressions/lib/MWaterDataSource') */ export default class MWaterDataSource extends DataSource { apiUrl: string client: string | null | undefined cacheExpiry: number options: { serverCaching?: boolean; localCaching?: boolean; imageApiUrl?: string; source?: string } cache?: LRU origin: string | undefined /** * @param apiUrl * @param options serverCaching: allows server to send cached results. default true * localCaching allows local MRU cache. default true * imageApiUrl: overrides apiUrl for images */ constructor( apiUrl: string, client?: string | null, /** * @param options Configuration options * @param options.serverCaching Allows server to send cached results. Default true. * @param options.localCaching Allows local MRU cache. Default true. * @param options.imageApiUrl Overrides apiUrl for images. * @param options.origin Origin of usage. e.g. "dashboards:43445364..." */ options: { serverCaching?: boolean localCaching?: boolean imageApiUrl?: string origin?: string } = {} ) { super() this.apiUrl = apiUrl this.client = client this.origin = options.origin // cacheExpiry is time in ms from epoch that is oldest data that can be accepted. 0 = any (if serverCaching is true) this.cacheExpiry = 0 _.defaults(options, { serverCaching: true, localCaching: true }) this.options = options if (this.options.localCaching) { this.cache = new LRU({ max: 500, ttl: 1000 * 15 * 60 }) } } performQuery(query: JsonQLQuery): Promise performQuery(query: JsonQLQuery, cb: (error: any, rows: Row[]) => void): void performQuery(query: any, cb?: any): any { // If no callback, use promise let cacheKey: any, method if (!cb) { return new Promise((resolve, reject) => { return this.performQuery(query, (error: any, rows: any) => { if (error) { return reject(error) } else { return resolve(rows) } }) }) } if (this.options.localCaching) { cacheKey = JSON.stringify(query) const cachedRows = this.cache!.get(cacheKey) if (cachedRows) { cb(null, cachedRows) return } } const queryParams: any = {} if (this.client) { queryParams.client = this.client } if (this.origin) { queryParams.origin = this.origin } const jsonqlStr = JSON.stringify(query) // Add as GET if short, POST otherwise if (jsonqlStr.length < 2000) { queryParams.jsonql = jsonqlStr method = "GET" } else { method = "POST" } // Setup caching const headers: any = { 'Content-Type': 'application/json', } if (method === "GET") { if (!this.options.serverCaching) { // Using headers forces OPTIONS call, so use timestamp to disable caching // headers['Cache-Control'] = "no-cache" queryParams.ts = Date.now() } else if (this.cacheExpiry) { const seconds = Math.floor((new Date().getTime() - this.cacheExpiry) / 1000) headers["Cache-Control"] = `max-age=${seconds}` } } // Create URL const url = this.apiUrl + "jsonql?" + querystring.stringify(queryParams) fetch(url, { method: method, headers: headers, body: method === "POST" ? JSON.stringify({ jsonql: jsonqlStr }) : undefined, }) .then(async response => { if (!response.ok) { const text = await response.text() cb(new Error(text || response.statusText)) return } return await response.json() }) .then(rows => { if (this.options.localCaching) { // Cache rows this.cache!.set(cacheKey, rows) } cb(null, rows) }) .catch(error => { cb(error) }) } // Get the cache expiry time in ms from epoch. No cached items before this time will be used getCacheExpiry() { return this.cacheExpiry } // Clears the local cache clearCache() { this.cache?.clear() // Set new cache expiry this.cacheExpiry = new Date().getTime() } /** Get the url to download an image (by id from an image or imagelist column) * Height, if specified, is minimum height needed. May return larger image */ getImageUrl(imageId: string, height?: number) { const apiUrl = this.options.imageApiUrl || this.apiUrl let url = apiUrl + `images/${imageId}` const query: any = {} if (height) { query.h = height } if (!_.isEmpty(query)) { url += "?" + querystring.stringify(query) } return url } /** Get the url to upload an image (by id from an image or imagelist column) POST to upload */ getImageUploadUrl(imageId: string) { const apiUrl = this.options.imageApiUrl || this.apiUrl let url = apiUrl + `images/${imageId}` const query: any = {} if (this.client) { query.client = this.client } if (!_.isEmpty(query)) { url += "?" + querystring.stringify(query) } return url } /** Get the url to download a file (by id from a file or filelist column). * filename optionally overrides the downloaded filename. */ getFileUrl(fileId: string, filename?: string) { let url = this.apiUrl + `files/${fileId}` const query: any = {} if (filename) { query.filename = filename } if (!_.isEmpty(query)) { url += "?" + querystring.stringify(query) } return url } /** Get the url to upload a file (by id from a file or filelist column). POST to upload. */ getFileUploadUrl(fileId: string) { let url = this.apiUrl + `files/${fileId}` const query: any = {} if (this.client) { query.client = this.client } if (!_.isEmpty(query)) { url += "?" + querystring.stringify(query) } return url } }