Skip to content

Commit 1b32033

Browse files
committed
v4: several breaking changes for changeset APIs to be consistent with...
...the OSM API's new json endpoints.
1 parent d08c923 commit 1b32033

File tree

9 files changed

+146
-95
lines changed

9 files changed

+146
-95
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## 4.0.0 (----------)
11+
12+
- 💥 BREAKING CHANGE: `uploadChangeset` now returns a object instead of a single changeset ID. This is because:
13+
1. the function supports chunking uploads into multiple changesets if it exceeds the limit of 10,000 features per changeset.
14+
2. For each feature that was created, the response now includes a mapping between the temporary ID used by the uploader, and the permanent ID allocated by the server.
15+
- 💥 BREAKING CHANGE: The type defintions for `Changeset` have been changed to mark several properties as optional. (see [#14](https://github.com/osmlab/osm-api-js/issues/14))
16+
- 💥 BREAKING CHANGE: `Changeset.created_at`, `Changeset.closed_at`, and `ChangesetComment.date` are now a `string`, not a `Date`. This makes it more consistent with the XML format, and easier to serialise to JSON.
17+
- 💥 BREAKING CHANGE: `ChangesetComment.uid` is now a `number`, not a `string`. This matches the behaviour of OSM's new json API.
18+
- 💥 BREAKING CHANGE: `Changeset.discussion` has been renamed to `Changeset.comments`. This matches the behaviour of OSM's new json API.
19+
1020
## 3.3.0 (2025-09-18)
1121

1222
- [uploadChangeset] add an `onProgress` callback, so that apps can show a progress bar while uploading

examples/uploadChangeset.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@ await uploadChangeset(
2626

2727
Response:
2828

29-
```json
30-
12345
29+
```jsonc
30+
{
31+
// 12345 is the changeset number
32+
"12345": {
33+
// the contents of this object is the diff result.
34+
// - for created features, this object allows you to map the temporary ID used by the uploader, to the permananet ID that the server allocated to this feature.
35+
// - for updated & deleted features, it includes the new version number
36+
},
37+
}
3138
```
3239

33-
(changeset number)
34-
3540
## Detailed Examples
3641

3742
### Updating existing features

src/api/_rawResponse.d.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
11
import type { Feature, FeatureCollection, Point } from "geojson";
2-
import type {
3-
Changeset,
4-
ChangesetComment,
5-
OsmFeatureType,
6-
OsmNote,
7-
} from "../types";
8-
9-
/** @internal */
10-
export type RawChangeset = Omit<
11-
Changeset,
12-
"discussion" | "created_at" | "closed_at"
13-
> & {
14-
created_at: string;
15-
closed_at?: string;
16-
comments?: (Omit<ChangesetComment, "date" | "uid"> & {
17-
/** ISO Date */
18-
date: string;
19-
uid: number;
20-
})[];
21-
};
2+
import type { OsmFeatureType, OsmNote } from "../types";
223

234
/** @internal */
245
export type RawNotesSearch = FeatureCollection<

src/api/changesets/__tests__/uploadChangeset.test.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ describe("uploadChangeset", () => {
236236
{ comment: "added a building" },
237237
{ create: [], modify: [], delete: [] }
238238
);
239-
expect(output).toBe(1);
239+
expect(output).toStrictEqual({ 1: { diffResult: {} } });
240240

241241
expect(osmFetch).toHaveBeenCalledTimes(3);
242242
expect(osmFetch).toHaveBeenNthCalledWith(
@@ -277,7 +277,56 @@ describe("uploadChangeset", () => {
277277
);
278278
}
279279

280-
expect(output).toBe(1);
280+
expect(output).toStrictEqual({
281+
// create nodes first.
282+
1: {
283+
diffResult: {
284+
node: {
285+
"-10": { newId: 2, newVersion: 1 },
286+
"-11": { newId: 1, newVersion: 1 },
287+
"-2": { newId: 5, newVersion: 1 },
288+
"-7": { newId: 6, newVersion: 1 },
289+
"-8": { newId: 4, newVersion: 1 },
290+
"-9": { newId: 3, newVersion: 1 },
291+
},
292+
},
293+
},
294+
// create nodes then ways then relations next
295+
2: {
296+
diffResult: {
297+
node: {
298+
"-100": { newId: 10, newVersion: 1 },
299+
"-4": { newId: 9, newVersion: 1 },
300+
"-5": { newId: 8, newVersion: 1 },
301+
"-6": { newId: 7, newVersion: 1 },
302+
},
303+
relation: { "-300000": { newId: 1, newVersion: 1 } },
304+
way: { "-3": { newId: 1, newVersion: 1 } },
305+
},
306+
},
307+
// modify and delete next (any order)
308+
3: {
309+
diffResult: {
310+
node: {
311+
1: { newId: 1, newVersion: 2 },
312+
2: { newId: 2, newVersion: 3 },
313+
},
314+
relation: {
315+
2: { newId: 2, newVersion: 22 },
316+
3: { newId: 3, newVersion: 2 },
317+
4: { newId: 4, newVersion: 2 },
318+
},
319+
way: { 2: { newId: 2, newVersion: 2 } },
320+
},
321+
},
322+
// delete last
323+
4: {
324+
diffResult: {
325+
relation: { 1: { newId: 1, newVersion: 11 } },
326+
way: { 1: { newId: 1, newVersion: 3 } },
327+
},
328+
},
329+
});
281330
});
282331

283332
it("splits changesets into chunks and suports a custom tag function", async () => {
@@ -311,7 +360,9 @@ describe("uploadChangeset", () => {
311360
);
312361
}
313362

314-
expect(output).toBe(1);
363+
// don't need to assert the output, since it's the same
364+
// as the previous test case.
365+
expect(Object.keys(output).map(Number)).toStrictEqual([1, 2, 3, 4]);
315366
});
316367

317368
// end of tests

src/api/changesets/createChangesetComment.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import type { Changeset } from "../../types";
22
import { type FetchOptions, osmFetch } from "../_osmFetch";
3-
import type { RawChangeset } from "../_rawResponse";
4-
import { mapRawChangeset } from "./getChangesets";
53

64
/** Add a comment to a changeset. The changeset must be closed. */
75
export async function createChangesetComment(
86
changesetId: number,
97
commentText: string,
108
options?: FetchOptions
119
): Promise<Changeset> {
12-
const result = await osmFetch<{ changeset: RawChangeset }>(
10+
const result = await osmFetch<{ changeset: Changeset }>(
1311
`/0.6/changeset/${changesetId}/comment.json`,
1412
undefined,
1513
{
@@ -22,5 +20,5 @@ export async function createChangesetComment(
2220
},
2321
}
2422
);
25-
return mapRawChangeset(result.changeset);
23+
return result.changeset;
2624
}

src/api/changesets/getChangesets.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
import type { BBox, Changeset } from "../../types";
22
import { type FetchOptions, osmFetch } from "../_osmFetch";
3-
import type { RawChangeset } from "../_rawResponse";
4-
5-
/** @internal */
6-
export const mapRawChangeset = ({
7-
comments,
8-
...raw
9-
}: RawChangeset): Changeset => ({
10-
...raw,
11-
created_at: new Date(raw.created_at),
12-
closed_at: raw.closed_at ? new Date(raw.closed_at) : undefined!,
13-
14-
discussion: comments?.map((comment) => ({
15-
...comment,
16-
date: new Date(comment.date),
17-
uid: `${comment.uid}`,
18-
})),
19-
});
203

214
// does not extend BasicFilters for historical reasons
225
export type ListChangesetOptions = {
@@ -57,7 +40,7 @@ export async function listChangesets(
5740
): Promise<Changeset[]> {
5841
const { only, ...otherQueries } = query;
5942

60-
const raw = await osmFetch<{ changesets: RawChangeset[] }>(
43+
const raw = await osmFetch<{ changesets: Changeset[] }>(
6144
"/0.6/changesets.json",
6245
{
6346
...(only && { [only]: true }),
@@ -66,7 +49,7 @@ export async function listChangesets(
6649
options
6750
);
6851

69-
return raw.changesets.map(mapRawChangeset);
52+
return raw.changesets;
7053
}
7154

7255
/** get a single changeset */
@@ -76,11 +59,11 @@ export async function getChangeset(
7659
includeDiscussion = true,
7760
options?: FetchOptions
7861
): Promise<Changeset> {
79-
const raw = await osmFetch<{ changeset: RawChangeset }>(
62+
const raw = await osmFetch<{ changeset: Changeset }>(
8063
`/0.6/changeset/${id}.json`,
8164
includeDiscussion ? { include_discussion: 1 } : {},
8265
options
8366
);
8467

85-
return mapRawChangeset(raw.changeset);
68+
return raw.changeset;
8669
}

src/api/changesets/uploadChangeset.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { OsmChange, Tags } from "../../types";
1+
import type { OsmChange, OsmFeatureType, Tags } from "../../types";
22
import { type FetchOptions, osmFetch } from "../_osmFetch";
33
import { version } from "../../../package.json";
4+
import type { RawUploadResponse } from "../_rawResponse";
45
import {
56
createChangesetMetaXml,
67
createOsmChangeXml,
@@ -19,6 +20,20 @@ export interface UploadChunkInfo {
1920

2021
export type UploadPhase = "upload" | "merge_conflicts";
2122

23+
/** Can include multiple changeset IDs if the upload was chunked. */
24+
export interface UploadResult {
25+
[changesetId: number]: {
26+
diffResult: {
27+
[Type in OsmFeatureType]?: {
28+
[oldId: number]: {
29+
newId: number;
30+
newVersion: number;
31+
};
32+
};
33+
};
34+
};
35+
}
36+
2237
/** @internal */
2338
export function compress(input: string) {
2439
// check if it's supported
@@ -61,7 +76,7 @@ export async function uploadChangeset(
6176
tags: Tags,
6277
diff: OsmChange,
6378
options?: FetchOptions & UploadOptions
64-
): Promise<number> {
79+
): Promise<UploadResult> {
6580
const {
6681
onChunk,
6782
onProgress,
@@ -71,10 +86,10 @@ export async function uploadChangeset(
7186
} = options || {};
7287

7388
const chunks = chunkOsmChange(diff);
74-
const csIds: number[] = [];
75-
7689
const featureCount = getOsmChangeSize(diff);
7790

91+
const result: UploadResult = {};
92+
7893
for (const [index, chunk] of chunks.entries()) {
7994
let tagsForChunk = tags;
8095

@@ -114,23 +129,41 @@ export async function uploadChangeset(
114129

115130
const compressed = !disableCompression && (await compress(osmChangeXml));
116131

117-
await osmFetch(`/0.6/changeset/${csId}/upload`, undefined, {
118-
...fetchOptions,
119-
method: "POST",
120-
body: compressed || osmChangeXml,
121-
headers: {
122-
...fetchOptions.headers,
123-
...(compressed && { "Content-Encoding": "gzip" }),
124-
"content-type": "application/xml; charset=utf-8",
125-
},
126-
});
132+
const idMap = await osmFetch<RawUploadResponse>(
133+
`/0.6/changeset/${csId}/upload`,
134+
undefined,
135+
{
136+
...fetchOptions,
137+
method: "POST",
138+
body: compressed || osmChangeXml,
139+
headers: {
140+
...fetchOptions.headers,
141+
...(compressed && { "Content-Encoding": "gzip" }),
142+
"content-type": "application/xml; charset=utf-8",
143+
},
144+
}
145+
);
146+
147+
// convert the XML format into a more concise JSON format
148+
result[csId] = { diffResult: {} };
149+
for (const _type in idMap.diffResult[0]) {
150+
if (_type === "$") continue;
151+
const type = <OsmFeatureType>_type;
152+
const items = idMap.diffResult[0][type] || [];
153+
for (const item of items) {
154+
result[csId].diffResult[type] ||= {};
155+
result[csId].diffResult[type][item.$.old_id] = {
156+
newId: +item.$.new_id,
157+
newVersion: +item.$.new_version,
158+
};
159+
}
160+
}
127161

128162
await osmFetch(`/0.6/changeset/${csId}/close`, undefined, {
129163
...fetchOptions,
130164
method: "PUT",
131165
});
132-
csIds.push(csId);
133166
}
134167

135-
return csIds[0]; // TODO:(semver breaking) return an array of IDs
168+
return result;
136169
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import type { Changeset } from "../../types";
22
import { type FetchOptions, osmFetch } from "../_osmFetch";
3-
import type { RawChangeset } from "../_rawResponse";
4-
import { mapRawChangeset } from "../changesets";
53

64
/** DWG only */
75
export async function hideChangesetComment(
86
changesetCommentId: number,
97
action?: "hide" | "unhide",
108
options?: FetchOptions
119
): Promise<Changeset> {
12-
const result = await osmFetch<{ changeset: RawChangeset }>(
10+
const result = await osmFetch<{ changeset: Changeset }>(
1311
`/0.6/changeset_comments/${changesetCommentId}/visibility.json`,
1412
{},
1513
{ ...options, method: action === "unhide" ? "POST" : "DELETE" }
1614
);
17-
return mapRawChangeset(result.changeset);
15+
return result.changeset;
1816
}

0 commit comments

Comments
 (0)