import fetch from 'node-fetch';
import { getGeoServerResponseText, GeoServerResponseError } from './util/geoserver.js';
import AboutClient from './about.js'
/**
* Client for GeoServer layers
*
* @module LayerClient
*/
export default class LayerClient {
/**
* Creates a GeoServer REST LayerClient instance.
*
* @param {String} url The URL of the GeoServer REST API endpoint
* @param {String} auth The Basic Authentication string
*/
constructor (url, auth) {
this.url = url;
this.auth = auth;
}
/**
* Returns a GeoServer layer by the given workspace and layer name,
* e.g. "myWs:myLayer".
*
* @param {String} workspace The name of the workspace, can be undefined
* @param {String} layerName The name of the layer to query
*
* @throws Error if request fails
*
* @returns {Object} An object with layer information or undefined if it cannot be found
*/
async get (workspace, layerName) {
let qualifiedName;
if (workspace) {
qualifiedName = `${workspace}:${layerName}`;
} else {
qualifiedName = layerName;
}
const response = await fetch(this.url + 'layers/' + qualifiedName + '.json', {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth
}
});
if (!response.ok) {
const grc = new AboutClient(this.url, this.auth);
if (await grc.exists()) {
// GeoServer exists, but requested item does not exist, we return empty
return;
} else {
// There was a general problem with GeoServer
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
return response.json();
}
/**
* Sets the attribution text and link of a layer.
*
* @param {String} workspace The name of the workspace, can be undefined
* @param {String} layerName The name of the layer to query
* @param {String} [attributionText] The attribution text
* @param {String} [attributionLink] The attribution link
*
* @throws Error if request fails
*/
async modifyAttribution (workspace, layerName, attributionText, attributionLink) {
let qualifiedName;
if (workspace) {
qualifiedName = `${workspace}:${layerName}`;
} else {
qualifiedName = layerName;
}
// take existing layer properties as template
const jsonBody = await this.get(workspace, layerName);
if (!jsonBody || !jsonBody.layer || !jsonBody.layer.attribution) {
throw new GeoServerResponseError(
`layer '${workspace}:${layerName}' misses the property 'attribution'`
);
}
// set attribution text and link
if (attributionText) {
jsonBody.layer.attribution.title = attributionText;
}
if (attributionLink) {
jsonBody.layer.attribution.href = attributionLink;
}
const url = this.url + 'layers/' + qualifiedName + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'PUT',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonBody)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Returns all layers in the GeoServer.
*
* @throws Error if request fails
*
* @returns {Object} An object with all layer information
*/
async getAll () {
const response = await fetch(this.url + 'layers.json', {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth
}
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
return response.json();
}
/**
* Get all layers of a workspace.
*
* @param {String} workspace The workspace
*
* @throws Error if request fails
*
* @return {Object} An object with the information about the layers
*/
async getLayers (workspace) {
const response = await fetch(this.url + 'workspaces/' + workspace + '/layers.json', {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth,
}
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
return await response.json();
}
/**
* Returns information about a cascaded WMS layer.
*
* @param {String} workspace The workspace
* @param {String} datastore The datastore
* @param {String} layerName The WMS layer name
*
* @throws Error if request fails
*
* @returns {Object} An object with layer information or undefined if it cannot be found
*/
async getWmsLayer (workspace, datastore, layerName) {
const response = await fetch(this.url + 'workspaces/' + workspace + '/wmsstores/' + datastore + '/wmslayers/' + layerName + '.json', {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth,
}
});
if (!response.ok) {
const grc = new AboutClient(this.url, this.auth);
if (await grc.exists()) {
// GeoServer exists, but requested item does not exist, we return empty
return;
} else {
// There was a general problem with GeoServer
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
return await response.json();
}
// TODO: automated test needed
/**
* Returns information about a cascaded WMTS layer.
*
* @param {String} workspace The workspace
* @param {String} datastore The datastore
* @param {String} layerName The WMTS layer name
*
* @throws Error if request fails
*
* @returns {Object} An object with layer information or undefined if it cannot be found
*/
async getWmtsLayer (workspace, datastore, layerName) {
const response = await fetch(this.url + 'workspaces/' + workspace + '/wmtsstores/' + datastore + '/layers/' + layerName + '.json', {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth,
}
});
if (!response.ok) {
const grc = new AboutClient(this.url, this.auth);
if (await grc.exists()) {
// GeoServer exists, but requested item does not exist, we return empty
return;
} else {
// There was a general problem with GeoServer
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
return await response.json();
}
/**
* Publishes a FeatureType in the default data store of the workspace.
*
* @param {String} workspace Workspace to publish FeatureType in
* @param {String} [nativeName] Native name of FeatureType
* @param {String} name Published name of FeatureType
* @param {String} [title] Published title of FeatureType
* @param {String} [srs="EPSG:4326"] The SRS of the FeatureType
* @param {String} enabled Flag to enable FeatureType by default
* @param {String} [abstract] The abstract of the layer
*
* @throws Error if request fails
*/
async publishFeatureTypeDefaultDataStore (workspace, nativeName, name, title, srs, enabled, abstract) {
const body = {
featureType: {
name: name,
nativeName: nativeName || name,
title: title || name,
srs: srs || 'EPSG:4326',
enabled: enabled,
abstract: abstract || ''
}
};
const response = await fetch(this.url + 'workspaces/' + workspace + '/featuretypes', {
credentials: 'include',
method: 'POST',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Publishes a FeatureType in the given data store of the workspace.
*
* @param {String} workspace Workspace to publish FeatureType in
* @param {String} dataStore The datastore where the FeatureType's data is in
* @param {String} [nativeName] Native name of FeatureType
* @param {String} name Published name of FeatureType
* @param {String} [title] Published title of FeatureType
* @param {String} [srs="EPSG:4326"] The SRS of the FeatureType
* @param {String} enabled Flag to enable FeatureType by default
* @param {String} [abstract] The abstract of the layer
* @param {String} [nativeBoundingBox] The native BoundingBox of the FeatureType (has to be set if no data is in store at creation time)
*
* @throws Error if request fails
*/
async publishFeatureType (workspace, dataStore, nativeName, name, title, srs, enabled, abstract, nativeBoundingBox) {
// apply CRS info for native BBOX if not provided
if (nativeBoundingBox && !nativeBoundingBox.crs) {
nativeBoundingBox.crs = {
'@class': 'projected',
$: srs
}
}
const body = {
featureType: {
name: name || nativeName,
nativeName: nativeName,
title: title || name,
srs: srs || 'EPSG:4326',
enabled: enabled,
abstract: abstract || '',
nativeBoundingBox: nativeBoundingBox
}
};
const response = await fetch(this.url + 'workspaces/' + workspace + '/datastores/' + dataStore + '/featuretypes', {
credentials: 'include',
method: 'POST',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Get detailed information about a FeatureType.
*
* @param {String} workspace The workspace of the FeatureType
* @param {String} datastore The datastore of the FeatureType
* @param {String} name The name of the FeatureType
*
* @throws Error if request fails
*
* @returns {Object} The object of the FeatureType
*/
async getFeatureType (workspace, datastore, name) {
const url = this.url + 'workspaces/' + workspace + '/datastores/' + datastore + '/featuretypes/' + name + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth
}
});
if (!response.ok) {
const grc = new AboutClient(this.url, this.auth);
if (await grc.exists()) {
// GeoServer exists, but requested item does not exist, we return empty
return;
} else {
// There was a general problem with GeoServer
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
return response.json();
}
/**
* Publishes a WMS layer.
*
* @param {String} workspace Workspace to publish WMS layer in
* @param {String} dataStore The datastore where the WMS is connected
* @param {String} nativeName Native name of WMS layer
* @param {String} [name] Published name of WMS layer
* @param {String} [title] Published title of WMS layer
* @param {String} [srs="EPSG:4326"] The SRS of the WMS layer
* @param {String} enabled Flag to enable WMS layer by default
* @param {String} [abstract] The abstract of the layer
*
* @throws Error if request fails
*/
async publishWmsLayer (workspace, dataStore, nativeName, name, title, srs, enabled, abstract) {
const body = {
wmsLayer: {
name: name || nativeName,
nativeName: nativeName,
title: title || name || nativeName,
srs: srs || 'EPSG:4326',
enabled: enabled,
abstract: abstract || ''
}
};
const response = await fetch(this.url + 'workspaces/' + workspace + '/wmsstores/' + dataStore + '/wmslayers', {
credentials: 'include',
method: 'POST',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Publishes a raster stored in a database.
*
* @param {String} workspace Workspace to publish layer in
* @param {String} coverageStore The coveragestore where the layer's data is in
* @param {String} nativeName Native name of raster
* @param {String} name Published name of layer
* @param {String} [title] Published title of layer
* @param {String} [srs="EPSG:4326"] The SRS of the layer
* @param {String} enabled Flag to enable layer by default
* @param {String} [abstract] The abstract of the layer
*
* @throws Error if request fails
*/
async publishDbRaster (workspace, coverageStore, nativeName, name, title, srs, enabled, abstract) {
const body = {
coverage: {
name: name || nativeName,
nativeName: nativeName,
title: title || name,
srs: srs,
enabled: enabled,
abstract: abstract || ''
}
};
const response = await fetch(this.url + 'workspaces/' + workspace + '/coveragestores/' + coverageStore + '/coverages', {
credentials: 'include',
method: 'POST',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Deletes a FeatureType.
*
* @param {String} workspace Workspace where layer to delete is in
* @param {String} datastore The datastore where the layer to delete is in
* @param {String} name Layer to delete
* @param {Boolean} recurse Flag to enable recursive deletion
*
* @throws Error if request fails
*/
async deleteFeatureType (workspace, datastore, name, recurse) {
const response = await fetch(this.url + 'workspaces/' + workspace + '/datastores/' + datastore + '/featuretypes/' + name + '?recurse=' + recurse, {
credentials: 'include',
method: 'DELETE',
headers: {
Authorization: this.auth
}
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Enables TIME dimension for the given coverage layer.
*
* @param {String} workspace Workspace where layer to enable time dimension for is in
* @param {String} datastore The datastore where the layer to enable time dimension for is in
* @param {String} name Layer to enable time dimension for
* @param {String} presentation Presentation type: 'LIST' or 'DISCRETE_INTERVAL' or 'CONTINUOUS_INTERVAL'
* @param {Number} resolution Resolution in milliseconds, e.g. 3600000 for 1 hour
* @param {String} defaultValue The default time value, e.g. 'MINIMUM' or 'MAXIMUM' or 'NEAREST' or 'FIXED'
* @param {Boolean} [nearestMatchEnabled] Enable nearest match
* @param {Boolean} [rawNearestMatchEnabled] Enable raw nearest match
* @param {String} [acceptableInterval] Acceptable interval for nearest match, e.g.'PT30M'
*
* @throws Error if request fails
*/
async enableTimeCoverage (workspace, dataStore, name, presentation, resolution, defaultValue, nearestMatchEnabled, rawNearestMatchEnabled, acceptableInterval) {
const body = {
coverage: {
metadata: {
entry: [
{
'@key': 'time',
dimensionInfo: {
enabled: true,
presentation: presentation || 'DISCRETE_INTERVAL',
resolution: resolution,
units: 'ISO8601',
defaultValue: {
strategy: defaultValue
},
nearestMatchEnabled: nearestMatchEnabled,
rawNearestMatchEnabled: rawNearestMatchEnabled,
acceptableInterval: acceptableInterval
}
}
]
}
}
};
const url = this.url + 'workspaces/' + workspace + '/coveragestores/' + dataStore + '/coverages/' + name + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'PUT',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Enables TIME dimension for the given FeatureType layer.
*
* @param {String} workspace Workspace containing layer to enable time dimension for
* @param {String} datastore The datastore containing the FeatureType to enable time dimension for
* @param {String} name FeatureType to enable time dimension for
* @param {String} attribute Data column / attribute holding the time values
* @param {String} presentation Presentation type: 'LIST' or 'DISCRETE_INTERVAL' or 'CONTINUOUS_INTERVAL'
* @param {Number} resolution Resolution in milliseconds, e.g. 3600000 for 1 hour
* @param {String} defaultValue The default time value, e.g. 'MINIMUM' or 'MAXIMUM' or 'NEAREST' or 'FIXED'
* @param {Boolean} [nearestMatchEnabled] Enable nearest match
* @param {Boolean} [rawNearestMatchEnabled] Enable raw nearest match
*
* @throws Error if request fails
*/
async enableTimeFeatureType (workspace, dataStore, name, attribute, presentation, resolution, defaultValue, nearestMatchEnabled, rawNearestMatchEnabled, acceptableInterval) {
const body = {
featureType: {
metadata: {
entry: [
{
'@key': 'time',
dimensionInfo: {
attribute: attribute,
presentation: presentation,
resolution: resolution,
units: 'ISO8601',
defaultValue: {
strategy: defaultValue
},
nearestMatchEnabled: nearestMatchEnabled,
rawNearestMatchEnabled: rawNearestMatchEnabled,
acceptableInterval: acceptableInterval
}
}
]
}
}
};
const url = this.url + 'workspaces/' + workspace + '/datastores/' + dataStore + '/featuretypes/' + name + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'PUT',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
/**
* Returns a dedicated coverage object.
*
* @param {String} workspace Workspace containing the coverage
* @param {String} coverageStore The coveragestore containing the coverage
* @param {String} name Coverage to query
*
* @throws Error if request fails
*
* @returns {Object} An object with coverage information or undefined if it cannot be found
*/
async getCoverage (workspace, coverageStore, name) {
const url = this.url + 'workspaces/' + workspace + '/coveragestores/' + coverageStore + '/coverages/' + name + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'GET',
headers: {
Authorization: this.auth
}
});
if (!response.ok) {
const grc = new AboutClient(this.url, this.auth);
if (await grc.exists()) {
// GeoServer exists, but requested item does not exist, we return empty
return;
} else {
// There was a general problem with GeoServer
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
return response.json();
}
/**
* Renames the existing bands of a coverage layer.
*
* Make sure to provide the same number of bands as existing in the layer.
*
* @param {String} workspace Workspace of layer
* @param {String} datastore The datastore of the layer
* @param {String} layername The layer name
* @param {String[]} bandNames An array of the new band names in correct order
*
* @throws Error if request fails
*/
async renameCoverageBands (workspace, dataStore, layername, bandNames) {
const body = {
coverage: {
dimensions: {
coverageDimension: [
]
}
}
};
// dynamically create the body
bandNames.forEach(bandName => {
body.coverage.dimensions.coverageDimension.push(
{
name: bandName
}
);
})
const url = this.url + 'workspaces/' + workspace + '/coveragestores/' + dataStore + '/coverages/' + layername + '.json';
const response = await fetch(url, {
credentials: 'include',
method: 'PUT',
headers: {
Authorization: this.auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const geoServerResponse = await getGeoServerResponseText(response);
throw new GeoServerResponseError(null, geoServerResponse);
}
}
}