@onlyworlds/sdk 4.2.0 → 4.4.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/AGENTS.md +12 -1
- package/CHANGELOG.md +58 -1
- package/SCHEMA.md +1 -1
- package/dist/index.d.ts +37 -14
- package/dist/index.js +52 -16
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -37,7 +37,18 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
37
37
|
A v4 or v7 id you supply is accepted. **Never sort elements by id**: worlds mix v7, v4
|
|
38
38
|
and legacy `06x…` ids (nibble 7 too, but seconds-first). For creation order use
|
|
39
39
|
`created_at`; `change_seq` is last-write order, not creation.
|
|
40
|
-
A
|
|
40
|
+
A create with an id that already exists, or a PUT whose id belongs to **another world**,
|
|
41
|
+
returns 409 `id_conflict` (`err.isIdConflict`; retrying won't help). `/bulk` never throws:
|
|
42
|
+
check each slot for `status: 409` with `error.code: 'id_conflict'`. A different 409, `idempotency_error`, means an
|
|
43
|
+
Idempotency-Key was reused with another body (`err.isIdempotencyConflict`).
|
|
44
|
+
- Under load keel answers 503 `server_busy` with `Retry-After` (`err.isBusy`, `err.retryAfter`
|
|
45
|
+
in seconds). The client does not retry for you; back off and retry yourself. Works the same
|
|
46
|
+
in browsers.
|
|
47
|
+
- `created_by` rides every element read: the id of the membership that created it, `null` for
|
|
48
|
+
the world's owner and for anything made before memberships existed. It is read-only; a write
|
|
49
|
+
body that carries it has it dropped, never an error. A **contributor** can change only what
|
|
50
|
+
they created: anything else is 403 `not_author` (`err.isNotAuthor`; retrying will not help).
|
|
51
|
+
`/bulk` reports it per slot, not as a throw.
|
|
41
52
|
- A string holding an unpaired surrogate (text cut mid-emoji) is a 422 naming the field.
|
|
42
53
|
Slice strings by code point, not by UTF-16 unit.
|
|
43
54
|
- Colour carries the element's FAMILY (`elementColor(type, mode)`); the icon
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,64 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
-
## [4.
|
|
8
|
+
## [4.4.0] — 2026-10-04 (staged, not published)
|
|
9
|
+
|
|
10
|
+
Membership, as far as the server has shipped it: who created an element, and the refusal a
|
|
11
|
+
contributor meets on someone else's. Additive; nothing is removed or narrows.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`created_by`** on `OwElementBase` (so on every element type): `string | null`, the id of the
|
|
15
|
+
membership that created the element, `null` for the world's owner and for anything made
|
|
16
|
+
before memberships existed (keel D72 phase 2, live 2026-09-28; keel spec §4). Read-only. The
|
|
17
|
+
schema does not carry it (like `created_at` it is server bookkeeping), so the generator adds
|
|
18
|
+
it to the wire base under the declared mapping. A write body that carries it has it dropped
|
|
19
|
+
by the server, so the client does not strip it and a read body still round-trips.
|
|
20
|
+
- **`OwApiError.isNotAuthor`**: 403 `not_author`, a contributor changing, replacing, relinking
|
|
21
|
+
or deleting an element someone else created. `/bulk` answers 200 and reports it in the item
|
|
22
|
+
slot, so check the slot (documented on `OwBulkItemResult`).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`FIELD_SCHEMA` no longer marks `marker.map/zone/x/y/order` and `pin.map/element_type/element_id/x/y`
|
|
26
|
+
as `required: true`.** The wire requires `name` and nothing else (rulings.yaml `nullable-by-default`,
|
|
27
|
+
Captain 2026-07-28; keel's OpenAPI write schemas say `required: [name]`), but the schema files still
|
|
28
|
+
list those fields and the generator copied the lists, so a form built from the table demanded
|
|
29
|
+
coordinates the server never did. Found by comparing the SDK with keel's OpenAPI document (the
|
|
30
|
+
new `verify-sdk-wire` gate in Assembly). **If you read `required` from this table, marker and pin
|
|
31
|
+
fields now read as optional**, which is what the server accepts. The generator notes the lists it
|
|
32
|
+
ignores on every run and says when canonical stops carrying them.
|
|
33
|
+
|
|
34
|
+
### Not yet
|
|
35
|
+
- `me()` and `members()` wait for the server routes (keel's M2b). Nothing in this release
|
|
36
|
+
guesses at their names.
|
|
37
|
+
|
|
38
|
+
## [4.3.0] — 2026-09-28
|
|
39
|
+
|
|
40
|
+
Errors you can act on: the two 409s told apart, and the busy server's `Retry-After`, in Node and
|
|
41
|
+
browsers. One getter narrows (see Fixed); nothing is removed.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- **`OwApiError.isIdempotencyConflict` no longer claims every 409.** keel sends two: 409
|
|
45
|
+
`idempotency_error` (a key reused with a different body) and 409 `id_conflict` (the id is
|
|
46
|
+
taken: a create with an existing id since D39, or a PUT / bulk id held by another world since
|
|
47
|
+
D70). The getter was true for both, so an id conflict read as a key problem, and retrying with
|
|
48
|
+
a fresh key could never help. It now matches `idempotency_error` only, and the new
|
|
49
|
+
**`isIdConflict`** names the other. A caller who relied on the old catch-all for `id_conflict`
|
|
50
|
+
should switch to `isIdConflict`. A 409 with no code (a proxy, a non-JSON body) now reads
|
|
51
|
+
false for both. `/bulk` is unaffected: it answers 200 and reports a per-item 409 in the slot.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- **`OwApiError.retryAfter`** (seconds, or null) is parsed from `Retry-After`, as a delay or an
|
|
55
|
+
HTTP date, and **`isBusy`** marks keel's 503 `server_busy` (admission control, keel D71: six
|
|
56
|
+
requests at once per worker, up to 10 s of queueing, then 503 with `Retry-After: 5`). The client
|
|
57
|
+
still never retries on its own; it gives callers what they need to back off. A malformed
|
|
58
|
+
`Retry-After` is null, never "retry now". **Works in browsers too** since keel `4949406`
|
|
59
|
+
(2026-09-28): every response exposes `Retry-After`, `Idempotent-Replay` and
|
|
60
|
+
`X-OW-Schema-Version` over CORS (probed from an allowed origin), and keel reports that the
|
|
61
|
+
admission gate's 503 now carries CORS headers, so a busy server reads as busy, not as
|
|
62
|
+
`OwNetworkError` (keel's word; the 503 can't be triggered on demand to probe). Six tests; the
|
|
63
|
+
four core ones watched failing on the previous `errors.ts`.
|
|
64
|
+
|
|
65
|
+
## [4.2.0] — 2026-09-28
|
|
9
66
|
|
|
10
67
|
Ids, honest filter docs, and the schema repin. Nothing is removed; no call needs to change.
|
|
11
68
|
|
package/SCHEMA.md
CHANGED
|
@@ -6,7 +6,7 @@ GENERATED from the canonical schema YAML — do not hand-edit (regenerate: `pyth
|
|
|
6
6
|
Written for both humans and AI agents reading this package locally.
|
|
7
7
|
|
|
8
8
|
**The shape rules** (v2 wire dialect): every element carries `id` (UUID), `name`, optional
|
|
9
|
-
`description`/`supertype`/`subtype`/`image_url`, server-managed `type`/`created_at`/`updated_at`/`change_seq`,
|
|
9
|
+
`description`/`supertype`/`subtype`/`image_url`, server-managed `type`/`created_at`/`updated_at`/`change_seq`/`created_by`,
|
|
10
10
|
and namespaced extension fields (`x_*` etc.) returned verbatim. Link fields use ONE bare
|
|
11
11
|
name in both read and write (no `_ids` suffix). Single links are `UUID | null`; multi links
|
|
12
12
|
are `UUID[]`. **Links are owned one-way**: the type listed below owns the field (e.g.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Every element carries these. The extension index signature admits namespaced
|
|
2
2
|
* pass-through fields (atlas_* / shadow_* / x_*) returned verbatim by the server.
|
|
3
3
|
* Derived from base_properties.yaml: `World` is dropped (the API rejects it in
|
|
4
|
-
* bodies -- the key determines the world) and the
|
|
4
|
+
* bodies -- the key determines the world) and the five server-managed fields are
|
|
5
5
|
* added, since they ride every wire body and appear in no element YAML. */
|
|
6
6
|
interface OwElementBase {
|
|
7
7
|
/** Element type slug (server-managed, read-only). */
|
|
@@ -24,6 +24,8 @@ interface OwElementBase {
|
|
|
24
24
|
updated_at?: string;
|
|
25
25
|
/** Per-world change cursor, stamped on every write (server-managed, read-only). */
|
|
26
26
|
change_seq?: number;
|
|
27
|
+
/** The membership that created this element (server-managed, read-only); null for the world's owner and for anything created before memberships existed. */
|
|
28
|
+
created_by?: string | null;
|
|
27
29
|
/** Namespaced extension fields (atlas_* / shadow_* / x_*), returned verbatim. */
|
|
28
30
|
[ext: string]: unknown;
|
|
29
31
|
}
|
|
@@ -1100,27 +1102,22 @@ declare const FIELD_SCHEMA: {
|
|
|
1100
1102
|
readonly map: {
|
|
1101
1103
|
readonly type: "single_link";
|
|
1102
1104
|
readonly target: "map";
|
|
1103
|
-
readonly required: true;
|
|
1104
1105
|
};
|
|
1105
1106
|
readonly zone: {
|
|
1106
1107
|
readonly type: "single_link";
|
|
1107
1108
|
readonly target: "zone";
|
|
1108
|
-
readonly required: true;
|
|
1109
1109
|
};
|
|
1110
1110
|
readonly x: {
|
|
1111
1111
|
readonly type: "integer";
|
|
1112
|
-
readonly required: true;
|
|
1113
1112
|
};
|
|
1114
1113
|
readonly y: {
|
|
1115
1114
|
readonly type: "integer";
|
|
1116
|
-
readonly required: true;
|
|
1117
1115
|
};
|
|
1118
1116
|
readonly z: {
|
|
1119
1117
|
readonly type: "integer";
|
|
1120
1118
|
};
|
|
1121
1119
|
readonly order: {
|
|
1122
1120
|
readonly type: "integer";
|
|
1123
|
-
readonly required: true;
|
|
1124
1121
|
};
|
|
1125
1122
|
};
|
|
1126
1123
|
readonly narrative: {
|
|
@@ -1407,24 +1404,19 @@ declare const FIELD_SCHEMA: {
|
|
|
1407
1404
|
readonly map: {
|
|
1408
1405
|
readonly type: "single_link";
|
|
1409
1406
|
readonly target: "map";
|
|
1410
|
-
readonly required: true;
|
|
1411
1407
|
};
|
|
1412
1408
|
readonly element_type: {
|
|
1413
1409
|
readonly type: "text";
|
|
1414
|
-
readonly required: true;
|
|
1415
1410
|
};
|
|
1416
1411
|
readonly element_id: {
|
|
1417
1412
|
readonly type: "single_link";
|
|
1418
1413
|
readonly target: "any";
|
|
1419
|
-
readonly required: true;
|
|
1420
1414
|
};
|
|
1421
1415
|
readonly x: {
|
|
1422
1416
|
readonly type: "integer";
|
|
1423
|
-
readonly required: true;
|
|
1424
1417
|
};
|
|
1425
1418
|
readonly y: {
|
|
1426
1419
|
readonly type: "integer";
|
|
1427
|
-
readonly required: true;
|
|
1428
1420
|
};
|
|
1429
1421
|
readonly z: {
|
|
1430
1422
|
readonly type: "integer";
|
|
@@ -1918,6 +1910,8 @@ interface OwBulkItem {
|
|
|
1918
1910
|
* One slot of a /bulk response. WIRE-CORRECTED (fixtures P2a/P2c): `status` is
|
|
1919
1911
|
* the NUMERIC HTTP status of that slot (201/400/...), success slots echo
|
|
1920
1912
|
* created_at/updated_at, error slots carry an OwErrorBody under `error`.
|
|
1913
|
+
* A contributor's write on someone else's element fails in its slot as
|
|
1914
|
+
* `{status: 403, error: {code: 'not_author'}}` (keel D72), never as a thrown error.
|
|
1921
1915
|
*/
|
|
1922
1916
|
interface OwBulkItemResult {
|
|
1923
1917
|
status: number;
|
|
@@ -2014,12 +2008,41 @@ declare class OwApiError extends Error {
|
|
|
2014
2008
|
readonly docUrl: string | null;
|
|
2015
2009
|
/** Raw parsed envelope (or body text when the body wasn't JSON). */
|
|
2016
2010
|
readonly detail: unknown;
|
|
2017
|
-
|
|
2011
|
+
/**
|
|
2012
|
+
* Seconds to wait before retrying, from the `Retry-After` header (a delay in
|
|
2013
|
+
* seconds or an HTTP date), else null. keel sends it on 503 `server_busy`
|
|
2014
|
+
* (admission control, D71) and 429 `rate_limited`. This client never retries
|
|
2015
|
+
* on its own; the value is here so callers can back off politely.
|
|
2016
|
+
* Readable in browsers too: keel exposes `Retry-After` over CORS and its
|
|
2017
|
+
* admission gate's 503 carries CORS headers (keel `4949406`, 2026-09-28).
|
|
2018
|
+
*/
|
|
2019
|
+
readonly retryAfter: number | null;
|
|
2020
|
+
constructor(status: number, code: string | null, message: string, docUrl: string | null, detail: unknown, type?: string | null, param?: string | null, retryAfter?: number | null);
|
|
2018
2021
|
get isAuthError(): boolean;
|
|
2019
2022
|
/** 422s/400s name the offending param/field -- typos error loudly platform-wide. */
|
|
2020
2023
|
get isValidationError(): boolean;
|
|
2021
|
-
/**
|
|
2024
|
+
/**
|
|
2025
|
+
* Same Idempotency-Key replayed with a different payload (409 `idempotency_error`).
|
|
2026
|
+
* Through 4.2.0 this was true for ANY 409, which misread an id conflict as a key
|
|
2027
|
+
* conflict: keel also answers 409 `id_conflict` (see isIdConflict).
|
|
2028
|
+
*/
|
|
2022
2029
|
get isIdempotencyConflict(): boolean;
|
|
2030
|
+
/**
|
|
2031
|
+
* The id is already taken (409 `id_conflict`): a create with an id that exists
|
|
2032
|
+
* (keel D39), or a PUT whose id belongs to another world (D70). Retrying will not
|
|
2033
|
+
* help; the id is the problem. `/bulk` never throws for this: it answers 200 with a
|
|
2034
|
+
* per-item slot `{status: 409, error: {code: 'id_conflict'}}` — check the slot.
|
|
2035
|
+
*/
|
|
2036
|
+
get isIdConflict(): boolean;
|
|
2037
|
+
/** keel's admission control turned the request away (503 `server_busy`); see retryAfter. */
|
|
2038
|
+
get isBusy(): boolean;
|
|
2039
|
+
/**
|
|
2040
|
+
* A contributor tried to change, replace, relink or delete an element someone else
|
|
2041
|
+
* created (403 `not_author`, keel D72 membership phase 2). Retrying will not help; the
|
|
2042
|
+
* element is not theirs. `/bulk` never throws for this: it answers 200 with a per-item
|
|
2043
|
+
* slot `{status: 403, error: {code: 'not_author'}}`. Owners and co-builders never see it.
|
|
2044
|
+
*/
|
|
2045
|
+
get isNotAuthor(): boolean;
|
|
2023
2046
|
}
|
|
2024
2047
|
/** Network-level failure (fetch rejected) -- no envelope to parse. */
|
|
2025
2048
|
declare class OwNetworkError extends Error {
|
|
@@ -2029,7 +2052,7 @@ declare class OwNetworkError extends Error {
|
|
|
2029
2052
|
/** Parse a wire envelope into OwApiError parts (exported for the error type-tests). */
|
|
2030
2053
|
/** Parse the platform ERROR envelope into an OwApiError. (Renamed from parseEnvelope in 4.0 —
|
|
2031
2054
|
* distinct from the world-export envelope, which is a different artifact entirely.) */
|
|
2032
|
-
declare function parseErrorEnvelope(status: number, body: unknown): OwApiError;
|
|
2055
|
+
declare function parseErrorEnvelope(status: number, body: unknown, retryAfter?: number | null): OwApiError;
|
|
2033
2056
|
/** Build an OwApiError from a non-2xx response, tolerating non-JSON bodies. */
|
|
2034
2057
|
declare function errorFromResponse(res: Response): Promise<OwApiError>;
|
|
2035
2058
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/v2/errors.ts
|
|
2
2
|
var OwApiError = class extends Error {
|
|
3
|
-
constructor(status, code, message, docUrl, detail, type = null, param = null) {
|
|
3
|
+
constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
|
|
4
4
|
super(message);
|
|
5
5
|
this.name = "OwApiError";
|
|
6
6
|
this.status = status;
|
|
@@ -9,6 +9,7 @@ var OwApiError = class extends Error {
|
|
|
9
9
|
this.param = param;
|
|
10
10
|
this.docUrl = docUrl;
|
|
11
11
|
this.detail = detail;
|
|
12
|
+
this.retryAfter = retryAfter;
|
|
12
13
|
}
|
|
13
14
|
get isAuthError() {
|
|
14
15
|
return this.code === "invalid_credentials" || this.code === "key_revoked" || this.code === "world_gone";
|
|
@@ -17,9 +18,35 @@ var OwApiError = class extends Error {
|
|
|
17
18
|
get isValidationError() {
|
|
18
19
|
return this.status === 422 || this.status === 400;
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Same Idempotency-Key replayed with a different payload (409 `idempotency_error`).
|
|
23
|
+
* Through 4.2.0 this was true for ANY 409, which misread an id conflict as a key
|
|
24
|
+
* conflict: keel also answers 409 `id_conflict` (see isIdConflict).
|
|
25
|
+
*/
|
|
21
26
|
get isIdempotencyConflict() {
|
|
22
|
-
return this.status === 409;
|
|
27
|
+
return this.status === 409 && this.code === "idempotency_error";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The id is already taken (409 `id_conflict`): a create with an id that exists
|
|
31
|
+
* (keel D39), or a PUT whose id belongs to another world (D70). Retrying will not
|
|
32
|
+
* help; the id is the problem. `/bulk` never throws for this: it answers 200 with a
|
|
33
|
+
* per-item slot `{status: 409, error: {code: 'id_conflict'}}` — check the slot.
|
|
34
|
+
*/
|
|
35
|
+
get isIdConflict() {
|
|
36
|
+
return this.status === 409 && this.code === "id_conflict";
|
|
37
|
+
}
|
|
38
|
+
/** keel's admission control turned the request away (503 `server_busy`); see retryAfter. */
|
|
39
|
+
get isBusy() {
|
|
40
|
+
return this.status === 503 && this.code === "server_busy";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A contributor tried to change, replace, relink or delete an element someone else
|
|
44
|
+
* created (403 `not_author`, keel D72 membership phase 2). Retrying will not help; the
|
|
45
|
+
* element is not theirs. `/bulk` never throws for this: it answers 200 with a per-item
|
|
46
|
+
* slot `{status: 403, error: {code: 'not_author'}}`. Owners and co-builders never see it.
|
|
47
|
+
*/
|
|
48
|
+
get isNotAuthor() {
|
|
49
|
+
return this.status === 403 && this.code === "not_author";
|
|
23
50
|
}
|
|
24
51
|
};
|
|
25
52
|
var OwNetworkError = class extends Error {
|
|
@@ -29,7 +56,7 @@ var OwNetworkError = class extends Error {
|
|
|
29
56
|
this.cause2 = cause;
|
|
30
57
|
}
|
|
31
58
|
};
|
|
32
|
-
function parseErrorEnvelope(status, body) {
|
|
59
|
+
function parseErrorEnvelope(status, body, retryAfter = null) {
|
|
33
60
|
const env = body && typeof body === "object" ? body : {};
|
|
34
61
|
const nested = typeof env.error === "object" && env.error !== null ? env.error : void 0;
|
|
35
62
|
const code = env.code ?? nested?.code ?? (typeof env.error === "string" ? env.error : null) ?? null;
|
|
@@ -37,7 +64,16 @@ function parseErrorEnvelope(status, body) {
|
|
|
37
64
|
const param = env.param ?? nested?.param ?? null;
|
|
38
65
|
const docUrl = env.doc_url ?? nested?.doc_url ?? null;
|
|
39
66
|
const message = env.message ?? nested?.message ?? (typeof env.detail === "string" ? env.detail : void 0) ?? `OnlyWorlds API error ${status}${code ? ` (${code})` : ""}`;
|
|
40
|
-
return new OwApiError(status, code, message, docUrl, body, type, param);
|
|
67
|
+
return new OwApiError(status, code, message, docUrl, body, type, param, retryAfter);
|
|
68
|
+
}
|
|
69
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
70
|
+
if (value == null) return null;
|
|
71
|
+
const v = value.trim();
|
|
72
|
+
if (/^\d+$/.test(v)) return Number(v);
|
|
73
|
+
if (!/[A-Za-z]/.test(v)) return null;
|
|
74
|
+
const when = Date.parse(v);
|
|
75
|
+
if (Number.isNaN(when)) return null;
|
|
76
|
+
return Math.max(0, Math.ceil((when - now) / 1e3));
|
|
41
77
|
}
|
|
42
78
|
async function errorFromResponse(res) {
|
|
43
79
|
let body = null;
|
|
@@ -48,7 +84,7 @@ async function errorFromResponse(res) {
|
|
|
48
84
|
} catch {
|
|
49
85
|
body = text || null;
|
|
50
86
|
}
|
|
51
|
-
return parseErrorEnvelope(res.status, body);
|
|
87
|
+
return parseErrorEnvelope(res.status, body, parseRetryAfter(res.headers?.get?.("Retry-After") ?? null));
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
// src/v2/keys.ts
|
|
@@ -819,12 +855,12 @@ var FIELD_SCHEMA = {
|
|
|
819
855
|
subtype: { type: "text", required: false },
|
|
820
856
|
image_url: { type: "text", required: false },
|
|
821
857
|
// Details
|
|
822
|
-
map: { type: "single_link", target: "map"
|
|
823
|
-
zone: { type: "single_link", target: "zone"
|
|
824
|
-
x: { type: "integer"
|
|
825
|
-
y: { type: "integer"
|
|
858
|
+
map: { type: "single_link", target: "map" },
|
|
859
|
+
zone: { type: "single_link", target: "zone" },
|
|
860
|
+
x: { type: "integer" },
|
|
861
|
+
y: { type: "integer" },
|
|
826
862
|
z: { type: "integer" },
|
|
827
|
-
order: { type: "integer"
|
|
863
|
+
order: { type: "integer" }
|
|
828
864
|
},
|
|
829
865
|
narrative: {
|
|
830
866
|
// Base fields (shared by all elements)
|
|
@@ -917,11 +953,11 @@ var FIELD_SCHEMA = {
|
|
|
917
953
|
subtype: { type: "text", required: false },
|
|
918
954
|
image_url: { type: "text", required: false },
|
|
919
955
|
// Details
|
|
920
|
-
map: { type: "single_link", target: "map"
|
|
921
|
-
element_type: { type: "text"
|
|
922
|
-
element_id: { type: "single_link", target: "any"
|
|
923
|
-
x: { type: "integer"
|
|
924
|
-
y: { type: "integer"
|
|
956
|
+
map: { type: "single_link", target: "map" },
|
|
957
|
+
element_type: { type: "text" },
|
|
958
|
+
element_id: { type: "single_link", target: "any" },
|
|
959
|
+
x: { type: "integer" },
|
|
960
|
+
y: { type: "integer" },
|
|
925
961
|
z: { type: "integer" }
|
|
926
962
|
},
|
|
927
963
|
relation: {
|