@masterportal/masterportalapi 2.51.0 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,8 +2,8 @@
2
2
  All important changes in this project are stored in this file.
3
3
 
4
4
  The [Semantic Versioning](https://semver.org/spec/v2.0.0.html) is used.
5
-
6
- ## Unreleased - in development
5
+
6
+ ## Unreleased - in development
7
7
  ### __Breaking Changes__
8
8
 
9
9
  ### Added
@@ -18,6 +18,17 @@
18
18
 
19
19
  ---
20
20
 
21
+ ## 2.52.0 - 2025-11-13
22
+
23
+ ### Added
24
+ - wfs: Normalize WFS-T transaction XML to ensure WFS 2.0.0 compatibility.
25
+ - OGC API: Added a polling function that polls until either result is ready or process failed.
26
+
27
+ ### Fixed
28
+ - wfs: Fixed handling of the fetch option `credentials` for `loadWFSFilter`, `loadWFS`, and `getGeometryTypeFromWFS`.
29
+
30
+ ---
31
+
21
32
  ## 2.51.0 - 2025-09-18
22
33
 
23
34
  ### __Breaking Changes__
package/example/index.js CHANGED
@@ -253,8 +253,14 @@ async function testOgcApiProcesses (processUrl) {
253
253
  console.log("Execute Process:", await window.mpapi.ogcApiProcesses.executeProcess(processUrl, processList[0].id, data)); // eslint-disable-line
254
254
 
255
255
  console.log("Job List:", jobList); // eslint-disable-line
256
+
257
+ // option one: check status and results separately
256
258
  console.log("Job Status of last job:", await window.mpapi.ogcApiProcesses.jobStatus(processUrl, jobList.at(-1).jobID)); // eslint-disable-line
257
259
  console.log("Job Results of last job:", await window.mpapi.ogcApiProcesses.jobResults(processUrl, jobList.at(-1).jobID)); // eslint-disable-line
260
+
261
+ // option two: poll until failure (throws) or success (returns results)
262
+ console.log("Job Results of last job:", await window.mpapi.ogcApiProcesses.pollJobResults(processUrl, jobList.at(-1).jobID)); // eslint-disable-line
263
+
258
264
  console.log("Cancle the last Job:", await window.mpapi.ogcApiProcesses.cancelJob(processUrl, jobList.at(-1).jobID)); // eslint-disable-line
259
265
  console.log("Job list:", await window.mpapi.ogcApiProcesses.jobList(processUrl)); // eslint-disable-line
260
266
  }
package/package.json CHANGED
@@ -1,85 +1,85 @@
1
- {
2
- "name": "@masterportal/masterportalapi",
3
- "author": "Implementierungspartnerschaft Masterportal <info@masterportal.org> (https://www.masterportal.org)",
4
- "version": "2.51.0",
5
- "license": "MIT",
6
- "description": "Basic functions of the Masterportal as api",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://bitbucket.org/geowerkstatt-hamburg/masterportalapi.git"
10
- },
11
- "source": "src/index.js",
12
- "main": "src/index.js",
13
- "exports": {
14
- ".": "./src/index.js"
15
- },
16
- "targets": {
17
- "library": {
18
- "source": "src/index.js",
19
- "distDir": "dist",
20
- "outputFormat": "commonjs"
21
- }
22
- },
23
- "scripts": {
24
- "test": "jest .test.js --config ./jest.config.js --collectCoverage",
25
- "test:trace": "node --trace-warnings node_modules/jest/bin/jest.js .test.js --config ./jest.config.js",
26
- "test:watch": "jest .test.js --config ./jest.config.js --watch",
27
- "example": "parcel example/index.html",
28
- "generate-jsdoc": "jsdoc ./src -r -d ./docs/ -R ./README.md -c jsdoc.json",
29
- "prePushHook": "eslint --max-warnings 0 \"./**/*.js\" && npm run test",
30
- "prepare": "husky install"
31
- },
32
- "dependencies": {
33
- "core-js": "^3.33.1",
34
- "dayjs": "^1.11.10",
35
- "ol": "^10.6.0",
36
- "ol-mapbox-style": "12.5.0",
37
- "olcs": "^2.22.1",
38
- "proj4": "^2.10.0",
39
- "xml2js": "^0.6.2"
40
- },
41
- "devDependencies": {
42
- "@babel/core": "7.26.0",
43
- "@babel/plugin-transform-modules-commonjs": "^7.23.0",
44
- "@parcel/config-default": "^2.15.4",
45
- "@parcel/transformer-sass": "^2.15.4",
46
- "@swc/jest": "^0.2.39",
47
- "assert": "^2.1.0",
48
- "browserify-zlib": "^0.2.0",
49
- "buffer": "^6.0.3",
50
- "canvas": "3.2.0",
51
- "eslint": "8.57.1",
52
- "events": "^3.3.0",
53
- "https-browserify": "^1.0.0",
54
- "husky": "8.0.3",
55
- "jest": "30.1.3",
56
- "jest-canvas-mock": "2.5.2",
57
- "jest-environment-jsdom": "30.1.2",
58
- "jest-fetch-mock": "^3.0.3",
59
- "jsdoc": "4.0.2",
60
- "parcel": "^2.15.4",
61
- "process": "^0.11.10",
62
- "sass": "1.69.4",
63
- "stream-browserify": "^3.0.0",
64
- "stream-http": "^3.2.0",
65
- "timers-browserify": "^2.0.12",
66
- "url": "^0.11.3",
67
- "use-resize-observer": "^9.1.0",
68
- "util": "^0.12.5"
69
- },
70
- "peerDependencies": {
71
- "@cesium/engine": "^20.0.1"
72
- },
73
- "engines": {
74
- "node": "^22.19.0",
75
- "npm": "^10.5.0"
76
- },
77
- "homepage": "https://bitbucket.org/geowerkstatt-hamburg/masterportalapi#readme",
78
- "directories": {
79
- "example": "example",
80
- "test": "test"
81
- },
82
- "bugs": {
83
- "url": "https://bitbucket.org/geowerkstatt-hamburg/masterportalapi/issues"
84
- }
85
- }
1
+ {
2
+ "name": "@masterportal/masterportalapi",
3
+ "author": "Implementierungspartnerschaft Masterportal <info@masterportal.org> (https://www.masterportal.org)",
4
+ "version": "2.52.0",
5
+ "license": "MIT",
6
+ "description": "Basic functions of the Masterportal as api",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://bitbucket.org/geowerkstatt-hamburg/masterportalapi.git"
10
+ },
11
+ "source": "src/index.js",
12
+ "exports": {
13
+ ".": "./src/index.js"
14
+ },
15
+ "targets": {
16
+ "library": {
17
+ "source": "src/index.js",
18
+ "distDir": "dist",
19
+ "outputFormat": "commonjs"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "test": "jest .test.js --config ./jest.config.js --collectCoverage",
24
+ "test:trace": "node --trace-warnings node_modules/jest/bin/jest.js .test.js --config ./jest.config.js",
25
+ "test:watch": "jest .test.js --config ./jest.config.js --watch",
26
+ "example": "parcel example/index.html",
27
+ "generate-jsdoc": "jsdoc ./src -r -d ./docs/ -R ./README.md -c jsdoc.json",
28
+ "prePushHook": "eslint --max-warnings 0 \"./**/*.js\" && npm run test",
29
+ "prepare": "husky install"
30
+ },
31
+ "dependencies": {
32
+ "core-js": "^3.33.1",
33
+ "dayjs": "^1.11.10",
34
+ "ol": "^10.6.0",
35
+ "ol-mapbox-style": "12.5.0",
36
+ "olcs": "^2.22.1",
37
+ "proj4": "^2.10.0",
38
+ "xml2js": "^0.6.2"
39
+ },
40
+ "devDependencies": {
41
+ "@babel/core": "7.26.0",
42
+ "@babel/plugin-transform-modules-commonjs": "^7.23.0",
43
+ "@parcel/config-default": "^2.15.4",
44
+ "@parcel/transformer-sass": "^2.15.4",
45
+ "@swc/jest": "^0.2.39",
46
+ "assert": "^2.1.0",
47
+ "browserify-zlib": "^0.2.0",
48
+ "buffer": "^6.0.3",
49
+ "canvas": "3.2.0",
50
+ "eslint": "8.57.1",
51
+ "events": "^3.3.0",
52
+ "https-browserify": "^1.0.0",
53
+ "husky": "8.0.3",
54
+ "jest": "30.1.3",
55
+ "jest-canvas-mock": "2.5.2",
56
+ "jest-environment-jsdom": "30.1.2",
57
+ "jest-fetch-mock": "^3.0.3",
58
+ "jsdoc": "4.0.2",
59
+ "parcel": "^2.15.4",
60
+ "process": "^0.11.10",
61
+ "sass": "1.69.4",
62
+ "stream-browserify": "^3.0.0",
63
+ "stream-http": "^3.2.0",
64
+ "timers-browserify": "^2.0.12",
65
+ "url": "^0.11.3",
66
+ "use-resize-observer": "^9.1.0",
67
+ "util": "^0.12.5"
68
+ },
69
+ "peerDependencies": {
70
+ "@cesium/engine": "^20.0.1"
71
+ },
72
+ "engines": {
73
+ "node": "^22.19.0",
74
+ "npm": "^10.5.0"
75
+ },
76
+ "homepage": "https://bitbucket.org/geowerkstatt-hamburg/masterportalapi#readme",
77
+ "directories": {
78
+ "example": "example",
79
+ "test": "test"
80
+ },
81
+ "bugs": {
82
+ "url": "https://bitbucket.org/geowerkstatt-hamburg/masterportalapi/issues"
83
+ },
84
+ "main": "jest.config.js"
85
+ }
@@ -194,6 +194,39 @@ async function jobResults (processUrl, jobId, format = "json") {
194
194
  return responseJson;
195
195
  }
196
196
 
197
+ /**
198
+ * Polls status for a job until it is successful and then returns its results.
199
+ * @param {string} processUrl – URL of the OGC API process
200
+ * @param {string} jobId – ID of the job to poll results to
201
+ * @param {object} pollParameters – how to poll
202
+ * @param {number?} [pollParameters.timeout=5000] – how long to try polling in ms
203
+ * @param {number?} [pollParameters.stallFor=100] – how long to wait between retries
204
+ * @returns {object} job results as json
205
+ * @throws
206
+ */
207
+ async function pollJobResults (processUrl, jobId, {timeout = 5000, stallFor = 100} = {}) {
208
+ const start = Date.now();
209
+
210
+ // exit through throw/return/timeout
211
+ for (;;) {
212
+ const jobStatusResponse = await jobStatus(processUrl, jobId);
213
+
214
+ if (jobStatusResponse?.status === "successful") {
215
+ return jobResults(processUrl, jobId);
216
+ }
217
+
218
+ if (["failed", "dismissed"].includes(jobStatusResponse?.status)) {
219
+ throw new Error(`Polling job ${jobId} at ${processUrl} resulted in status "${jobStatusResponse?.status}".`);
220
+ }
221
+
222
+ if (Date.now() - start >= timeout) {
223
+ throw new Error(`Polling job ${jobId} at ${processUrl} timed out after ${timeout} ms with last status "${jobStatusResponse?.status}".`);
224
+ }
225
+
226
+ await new Promise(res => setTimeout(res, stallFor));
227
+ }
228
+ }
229
+
197
230
  export default {
198
231
  processList,
199
232
  processDescription,
@@ -201,5 +234,6 @@ export default {
201
234
  jobList,
202
235
  cancelJob,
203
236
  jobStatus,
204
- jobResults
237
+ jobResults,
238
+ pollJobResults
205
239
  };
@@ -1,5 +1,5 @@
1
1
  import {Style, Icon, Stroke, Fill} from "ol/style.js";
2
- import markerSvg from "../../../public/marker.svg";
2
+
3
3
 
4
4
  let customStyles = {};
5
5
 
@@ -15,7 +15,7 @@ export function setCustomStyles (styles) {
15
15
  // // // STYLE PARTS
16
16
  const marker = new Icon({
17
17
  crossOrigin: "anonymous",
18
- src: markerSvg,
18
+ src: "../../../public/marker.svg",
19
19
  // center bottom of marker 📍 is intended to show the spot
20
20
  anchor: [0.5, 1]
21
21
  }),
package/src/layer/wfs.js CHANGED
@@ -81,7 +81,13 @@ function getFilterRequestParams (payload) {
81
81
  * @returns {void}
82
82
  */
83
83
  function loadWFS (url, params, source, {onErrorFn, success, failure}, options) {
84
- fetch(url, params)
84
+ const xhrParameters = options?.loadingParams?.xhrParameters ?? {},
85
+ mergedParams = {
86
+ ...params,
87
+ ...xhrParameters
88
+ };
89
+
90
+ fetch(url, mergedParams)
85
91
  .then((response) => handleErrors(response, onErrorFn, options, failure))
86
92
  .then((response) => response.text())
87
93
  .then(responseString => source.getFormat().readFeatures(responseString))
@@ -175,13 +181,11 @@ function createLayerSource (rawLayer, options = {}) {
175
181
  }
176
182
  else {
177
183
  const bboxParam = options.loadingStrategy === bbox ? `&bbox=${extent.join(",")},${projection.getCode()}` : "";
178
- let url = createUrl(rawLayer, version, projection, bboxParam),
179
- params = {};
184
+ let url = createUrl(rawLayer, version, projection, bboxParam);
180
185
 
181
186
  if (options.loadingParams) {
182
187
  for (const key in options.loadingParams) {
183
188
  if (key === "xhrParameters") {
184
- params = options.loadingParams.xhrParameters;
185
189
  continue;
186
190
  }
187
191
  const option = Array.isArray(options.loadingParams[key]) ? options.loadingParams[key].join(",") : options.loadingParams[key];
@@ -194,7 +198,7 @@ function createLayerSource (rawLayer, options = {}) {
194
198
  }
195
199
  }
196
200
  }
197
- loadWFS(url, params, source, {onErrorFn: onError, success, failure}, options);
201
+ loadWFS(url, {}, source, {onErrorFn: onError, success, failure}, options);
198
202
  }
199
203
  }
200
204
  format.featureType = rawLayer.featureType;
@@ -266,6 +270,50 @@ function createLayer (rawLayer = {}, {layerParams = {}, options = {}} = {}) {
266
270
  return layer;
267
271
  }
268
272
 
273
+ /**
274
+ * Ensures that the WFS-T transaction is compatible with a specific WFS version.
275
+ *
276
+ * Updates for WFS 2.0.0:
277
+ * - Filter needs fes namespace (Filter Encoding Standard)
278
+ * - Filter FeatureId (fid) must be replaced by ResourceId (rid)
279
+ *
280
+ * @param {string} version - WFS-T version
281
+ * @param {string} xml - Original WFS-T transaction xml
282
+ * @returns {string} Normalized WFS-T transaction xml
283
+ */
284
+ function normalizeWfsTransaction (version, xml) {
285
+ if (version === "2.0.0") {
286
+ const domParser = new DOMParser(),
287
+ xmlDocument = domParser.parseFromString(xml, "application/xml");
288
+
289
+ xmlDocument.querySelectorAll("Filter").forEach(filterNode => {
290
+ const newFilter = xmlDocument.createElementNS("http://www.opengis.net/fes/2.0", "fes:Filter");
291
+
292
+ while (filterNode.firstChild) {
293
+ const child = filterNode.firstChild;
294
+
295
+ if (child.nodeType === 1 && child.localName === "FeatureId") {
296
+ const fidAttribute = child.getAttribute("fid"),
297
+ resourceIdNode = xmlDocument.createElementNS("http://www.opengis.net/fes/2.0", "fes:ResourceId");
298
+
299
+ resourceIdNode.setAttribute("rid", fidAttribute);
300
+ newFilter.appendChild(resourceIdNode);
301
+ filterNode.removeChild(child);
302
+ }
303
+ else {
304
+ newFilter.appendChild(child);
305
+ }
306
+ }
307
+
308
+ filterNode.parentNode.replaceChild(newFilter, filterNode);
309
+ });
310
+
311
+ return new XMLSerializer().serializeToString(xmlDocument);
312
+ }
313
+
314
+ return xml;
315
+ }
316
+
269
317
  /**
270
318
  * Writes a wfs transaction including the given feature for given layer
271
319
  * inserting, updated or deleting the feature depending on the given selectedInteraction.
@@ -273,10 +321,9 @@ function createLayer (rawLayer = {}, {layerParams = {}, options = {}} = {}) {
273
321
  * @param {ol/Feature} feature Feature to be inserted, updated or deleted.
274
322
  * @param {Object} transactionOptions Information about the layer to be manipulated.
275
323
  * @param {string} transactionMethod Which transaction to perform. Possible values are: "insert"|"delete"|"selectedUpdate"
276
- * @param {string} srsName EPSG code currently used by the map.
324
+ * @param {string} version WFS-T version
277
325
  * @returns {string} WFS Transaction as an XML Node as String.
278
326
  */
279
-
280
327
  function writeTransactionBody (feature, transactionOptions, transactionMethod, version = "1.1.0") {
281
328
  const transactionPosition = {
282
329
  insert: 0,
@@ -290,9 +337,10 @@ function writeTransactionBody (feature, transactionOptions, transactionMethod, v
290
337
  }
291
338
 
292
339
  transaction[transactionPosition[transactionMethod]].push(feature);
293
- return new XMLSerializer()
340
+
341
+ return normalizeWfsTransaction(version, new XMLSerializer()
294
342
  .serializeToString(new WFS({version})
295
- .writeTransaction(...transaction, transactionOptions));
343
+ .writeTransaction(...transaction, transactionOptions)));
296
344
  }
297
345
 
298
346
  /**
@@ -130,11 +130,9 @@ function getGeometryTypeFromWFS (rules, wfsURL, version, featureType, styleGeome
130
130
  });
131
131
  url = url.slice(0, -1);
132
132
 
133
-
134
133
  return fetch(url, {
135
134
  method: "get",
136
- withCredentials: isSecured,
137
- responseType: "text"
135
+ credentials: isSecured ? "include" : "omit"
138
136
  }).then(response => response.text())
139
137
  .then(responseAsString => new window.DOMParser().parseFromString(responseAsString, "text/xml"))
140
138
  .then(responseXML => {
@@ -5,22 +5,26 @@ describe("ogcApiProcesses.js", () => {
5
5
  processId = "process-id",
6
6
  jobId = "job-id";
7
7
 
8
- beforeEach(() => {
9
- if (global.fetch) {
10
- fetch.mockClear();
11
- }
12
-
13
- global.fetch = jest.fn().mockImplementationOnce(() => {
8
+ function mockFetch (returnValue = {}) {
9
+ global.fetch = jest.fn().mockImplementation(() => {
14
10
  return new Promise((resolve) => {
15
11
  resolve({
16
12
  ok: true,
17
13
  status: 200,
18
14
  json: () => {
19
- return {};
15
+ return returnValue;
20
16
  }
21
17
  });
22
18
  });
23
19
  });
20
+ }
21
+
22
+ beforeEach(() => {
23
+ if (global.fetch) {
24
+ fetch.mockClear();
25
+ }
26
+
27
+ mockFetch();
24
28
  });
25
29
 
26
30
  describe("processList", () => {
@@ -110,4 +114,34 @@ describe("ogcApiProcesses.js", () => {
110
114
  expect(fetch).toHaveBeenCalledWith(`${processUrl}/jobs/${jobId}/results?f=html`, {method: "GET"});
111
115
  });
112
116
  });
117
+
118
+ describe("pollJobResults", () => {
119
+ it("returns job results when status is successful", async () => {
120
+ mockFetch({status: "successful", result: "It worked!"});
121
+
122
+ const result = await ogcApiProcesses.pollJobResults(processUrl, jobId);
123
+
124
+ expect(result).toEqual({result: "It worked!", status: "successful"});
125
+ expect(fetch).toHaveBeenCalledWith(`${processUrl}/jobs/${jobId}`, {method: "GET"});
126
+ expect(fetch).toHaveBeenCalledWith(`${processUrl}/jobs/${jobId}/results?f=json`, {method: "GET"});
127
+ });
128
+
129
+ it("throws error when status is failed", async () => {
130
+ mockFetch({status: "failed"});
131
+
132
+ await expect(ogcApiProcesses.pollJobResults(processUrl, jobId)).rejects.toThrow("Polling job job-id at https://example.de resulted in status \"failed\".");
133
+ });
134
+
135
+ it("throws error when status is dismissed", async () => {
136
+ mockFetch({status: "dismissed"});
137
+
138
+ await expect(ogcApiProcesses.pollJobResults(processUrl, jobId)).rejects.toThrow("Polling job job-id at https://example.de resulted in status \"dismissed\".");
139
+ });
140
+
141
+ it("throws error when polling times out", async () => {
142
+ mockFetch({status: "running"});
143
+
144
+ await expect(ogcApiProcesses.pollJobResults(processUrl, jobId, {timeout: 10, stallFor: 1})).rejects.toThrow(/timed out/);
145
+ });
146
+ });
113
147
  });
@@ -676,6 +676,20 @@ describe("wfs.js", function () {
676
676
  expect(() => wfs.writeTransactionBody(feature, transactionOptions, "remove")).toThrow(Error);
677
677
  expect(() => wfs.writeTransactionBody(feature, transactionOptions, "remove")).toThrow("transactionMethod has to be \"insert\", \"selectedUpdate\" or \"delete\".");
678
678
  });
679
+
680
+ it("should change WFS-T 2.0.0 filter namespace to fes and filter featureId (fid) to resourceId (rid) for selectedUpdate", async () => {
681
+ const transactionBody = wfs.writeTransactionBody(feature, transactionOptions, "selectedUpdate", "2.0.0"),
682
+ expectedBody = "<Transaction xmlns=\"http://www.opengis.net/wfs/2.0\" service=\"WFS\" version=\"2.0.0\" xmlns:ns1=\"http://www.w3.org/2001/XMLSchema-instance\" ns1:schemaLocation=\"http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd\"><Update typeName=\"myPrefix:Polygon\" xmlns:myPrefix=\"myNS\"><Property><ValueReference>geometry</ValueReference><Value><Polygon xmlns=\"http://www.opengis.net/gml/3.2\" srsName=\"EPSG:4326\"><exterior><LinearRing srsName=\"EPSG:4326\"><posList srsDimension=\"2\">50.20836600730087 9.17782024967994 50.20836600730087 9.200676227149245 50.20873353776312 9.200676227149245 50.20873353776312 9.17782024967994 50.20836600730087 9.17782024967994</posList></LinearRing></exterior></Polygon></Value></Property><Property><ValueReference>name</ValueReference><Value>My Polygon</Value></Property><fes:Filter xmlns:fes=\"http://www.opengis.net/fes/2.0\"><fes:ResourceId rid=\"1\"/></fes:Filter></Update></Transaction>";
683
+
684
+ expect(transactionBody).toEqual(expectedBody);
685
+ });
686
+
687
+ it("should change WFS-T 2.0.0 filter namespace to fes and filter featureId (fid) to resourceId (rid) for delete", async () => {
688
+ const transactionBody = wfs.writeTransactionBody(feature, transactionOptions, "delete", "2.0.0"),
689
+ expectedBody = "<Transaction xmlns=\"http://www.opengis.net/wfs/2.0\" service=\"WFS\" version=\"2.0.0\" xmlns:ns1=\"http://www.w3.org/2001/XMLSchema-instance\" ns1:schemaLocation=\"http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd\"><Delete typeName=\"myPrefix:Polygon\" xmlns:myPrefix=\"myNS\"><fes:Filter xmlns:fes=\"http://www.opengis.net/fes/2.0\"><fes:ResourceId rid=\"1\"/></fes:Filter></Delete></Transaction>";
690
+
691
+ expect(transactionBody).toEqual(expectedBody);
692
+ });
679
693
  });
680
694
 
681
695
  describe("receivePossibleProperties", () => {