@twinfinity/twin 6.1.1 → 6.1.2-ci.31342

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.
Files changed (99) hide show
  1. package/README.md +63 -2
  2. package/dist/BuildInfo.js +3 -3
  3. package/dist/TwinClient.d.ts +312 -9
  4. package/dist/TwinClient.d.ts.map +1 -1
  5. package/dist/TwinClient.js +321 -6
  6. package/dist/TwinClient.js.map +1 -1
  7. package/dist/errors.d.ts +120 -5
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +116 -2
  10. package/dist/errors.js.map +1 -1
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/dist/types/generated.d.ts +95 -33
  13. package/dist/types/generated.d.ts.map +1 -1
  14. package/dist/types/twin-api.d.ts +54 -1
  15. package/dist/types/twin-api.d.ts.map +1 -1
  16. package/dist/types/twin-permissions-api.d.ts +226 -6
  17. package/dist/types/twin-permissions-api.d.ts.map +1 -1
  18. package/docs/assets/hierarchy.js +1 -1
  19. package/docs/assets/navigation.js +1 -1
  20. package/docs/assets/search.js +1 -1
  21. package/docs/classes/AssetAttachmentConflictError.html +1 -1
  22. package/docs/classes/AssetAttachmentNotFoundError.html +1 -1
  23. package/docs/classes/AssetBatchError.html +1 -1
  24. package/docs/classes/AssetCatalogTooLargeError.html +1 -1
  25. package/docs/classes/AssetCategoryConflictError.html +1 -1
  26. package/docs/classes/AssetCategoryForbiddenError.html +1 -1
  27. package/docs/classes/AssetCategoryNotFoundError.html +1 -1
  28. package/docs/classes/AssetCategoryValidationError.html +1 -1
  29. package/docs/classes/AssetConflictError.html +1 -1
  30. package/docs/classes/AssetForbiddenError.html +1 -1
  31. package/docs/classes/AssetNotFoundError.html +1 -1
  32. package/docs/classes/AssetPermissionsConflictError.html +16 -0
  33. package/docs/classes/AssetTypeConflictError.html +1 -1
  34. package/docs/classes/AssetTypeForbiddenError.html +1 -1
  35. package/docs/classes/AssetTypeNotFoundError.html +1 -1
  36. package/docs/classes/AssetTypeValidationError.html +1 -1
  37. package/docs/classes/AssetValidationError.html +1 -1
  38. package/docs/classes/ProcessorNotFoundError.html +1 -1
  39. package/docs/classes/TwinClient.html +251 -73
  40. package/docs/classes/TwinConflictError.html +1 -1
  41. package/docs/classes/TwinFileConflictError.html +1 -1
  42. package/docs/classes/TwinFileNotFoundError.html +1 -1
  43. package/docs/classes/TwinFileValidationError.html +1 -1
  44. package/docs/classes/TwinNotFoundError.html +4 -2
  45. package/docs/classes/TwinPermissionsConflictError.html +19 -0
  46. package/docs/classes/TwinPermissionsForbiddenError.html +12 -0
  47. package/docs/classes/TwinPrincipalNotFoundError.html +5 -0
  48. package/docs/classes/TwinTemporarilyUnavailableError.html +10 -0
  49. package/docs/classes/TwinUnauthenticatedError.html +1 -1
  50. package/docs/classes/TwinValidationError.html +1 -1
  51. package/docs/hierarchy.html +1 -1
  52. package/docs/index.html +56 -4
  53. package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +2 -2
  54. package/docs/interfaces/AssetGroupGet.html +9 -0
  55. package/docs/interfaces/AssetGroupsGet.html +3 -0
  56. package/docs/interfaces/AssetPermissionsConflictErrorBody.html +12 -0
  57. package/docs/interfaces/AssetPermissionsPut.html +9 -0
  58. package/docs/interfaces/AssetPrincipalPermissionsGet.html +23 -0
  59. package/docs/interfaces/Coordinates.html +6 -0
  60. package/docs/interfaces/EntityCollection.html +7 -2
  61. package/docs/interfaces/EntityRepresentation.html +12 -0
  62. package/docs/interfaces/Facility.html +8 -3
  63. package/docs/interfaces/FacilityPart.html +5 -3
  64. package/docs/interfaces/ITwinClient.html +251 -73
  65. package/docs/interfaces/InheritedPermissions.html +8 -0
  66. package/docs/interfaces/ListTwinItem.html +18 -2
  67. package/docs/interfaces/Location.html +4 -1
  68. package/docs/interfaces/MyPermissions.html +2 -2
  69. package/docs/interfaces/MyTwinPermissions.html +5 -0
  70. package/docs/interfaces/Site.html +11 -4
  71. package/docs/interfaces/Space.html +6 -3
  72. package/docs/interfaces/TwinErrorBody.html +2 -2
  73. package/docs/interfaces/TwinGet.html +29 -11
  74. package/docs/interfaces/TwinGroupGet.html +10 -0
  75. package/docs/interfaces/TwinGroupsGet.html +3 -0
  76. package/docs/interfaces/TwinPermissionsConflictErrorBody.html +8 -0
  77. package/docs/interfaces/TwinPermissionsPut.html +11 -0
  78. package/docs/interfaces/TwinPrincipalPermissionsGet.html +34 -0
  79. package/docs/interfaces/TwinPut.html +8 -10
  80. package/docs/interfaces/TwinReference.html +4 -0
  81. package/docs/interfaces/TwinSpecification.html +7 -9
  82. package/docs/media/twin-management.md +197 -0
  83. package/docs/modules.html +1 -1
  84. package/docs/types/SinglePropertyValueType.html +1 -1
  85. package/docs/types/TwinBoundCapabilityName.html +8 -0
  86. package/docs/types/TwinCapability.html +11 -5
  87. package/docs/types/TwinCapabilityName.html +8 -0
  88. package/docs/types/TwinDiagnosticCode.html +1 -1
  89. package/docs/types/TwinErrorCause.html +1 -1
  90. package/docs/types/TwinErrorItemKind.html +1 -1
  91. package/docs/types/TwinPermissionsConflictCause.html +6 -0
  92. package/docs/types/TwinScopedCapability.html +9 -0
  93. package/docs/types/TwinScopedCapabilityName.html +8 -0
  94. package/package.json +3 -3
  95. package/src/TwinClient.ts +776 -15
  96. package/src/errors.ts +191 -4
  97. package/src/types/generated.ts +105 -35
  98. package/src/types/twin-api.ts +55 -1
  99. package/src/types/twin-permissions-api.ts +243 -6
@@ -0,0 +1,197 @@
1
+ # Twin Management
2
+
3
+ ## Overview
4
+
5
+ Twins are the central entity in the Twin service. A twin represents a digital twin specification with versioned state, soft-delete support, and optimistic concurrency via etags. Twins reference containers and files, and their processed content (T8CM, T8GI formats) is available for download.
6
+
7
+ ## Storage and Versioning
8
+
9
+ Twins use a two-table versioning pattern:
10
+
11
+ - `twin.twins` — The `version` column points to the current version. The `etag` column provides optimistic concurrency control, regenerated on every write.
12
+ - `twin.twin_versions` — Immutable version history.
13
+
14
+ Every update creates a new row in `twin_versions` and bumps the version pointer in `twins`. Soft deletion sets `is_deleted = true` in a new version row rather than removing data.
15
+
16
+ The twin's `name`, its WGS84 `location`, and the set of entity `classes` it contains are persisted on the version row, so the list endpoint and the version-history endpoint can surface them without reading the specification blob. `name` and `location` are **derived from the twin's root entity** (see [Root Entity](#root-entity)) at write time; `classes` is derived from the specification's spatial hierarchy. Existing twins were backfilled across all versions (current and historical), so every version reports its own `name`, `location`, and `classes`.
17
+
18
+ ## REST API
19
+
20
+ Twins are managed through `TwinsController` at route `twins`.
21
+
22
+ | Operation | Method | Route | Permission |
23
+ | ------------- | ------ | ------------------------------------ | ---------- |
24
+ | List | GET | `/twins` | (filtered) |
25
+ | Get | GET | `/twins/{twinId}` | View |
26
+ | List versions | GET | `/twins/{twinId}/versions` | View |
27
+ | Get version | GET | `/twins/{twinId}/versions/{version}` | View |
28
+ | Create/Update | PUT | `/twins/{twinId}` | Edit |
29
+ | Delete | DELETE | `/twins/{twinId}` | Delete |
30
+ | Restore | PUT | `/twins/{twinId}/restore` | Delete |
31
+
32
+ Content and diagnostics are handled by `TwinContentController`:
33
+
34
+ | Operation | Method | Route | Permission |
35
+ | ------------------------ | ------ | -------------------------------------------------------------------- | ---------- |
36
+ | Get content (latest) | GET | `/twins/{twinId}/contents` | View |
37
+ | Get version content | GET | `/twins/{twinId}/versions/{version}/contents` | View |
38
+ | Download content blob | GET | `/twins/{twinId}/versions/{version}/contents/{contentId}/content` | View |
39
+ | Get diagnostics (latest) | GET | `/twins/{twinId}/diagnostics` | View |
40
+ | Get version diagnostics | GET | `/twins/{twinId}/versions/{version}/diagnostics` | View |
41
+ | Get version events | GET | `/twins/{twinId}/versions/{version}/events` | View |
42
+ | Get processor events | GET | `/twins/{twinId}/versions/{version}/processors/{processorId}/events` | View |
43
+
44
+ ### Request/Response Types
45
+
46
+ Request DTO (`TwinPut`): `rootEntity` (required; see [Root Entity](#root-entity)), `entities` (sites, facilities, spaces, and `twinReferences`), `propertySets`, `elementQuantities`, `relatedContainerIds`, `fileReferences` (array of `{fileId, fileVersion, twinNodeId?}`), `etag` (required for updates). The legacy top-level `site` field is deprecated; `rootEntity` is authoritative. `name` and `location` are **not** accepted on input — they are derived from the root entity (a client that still sends them has them ignored, not rejected).
47
+
48
+ Response DTO (`TwinGet`): extends `TwinSpecification` with `id`, `etag`, `relatedContainerIds`, `version`, `isDeleted`, `createdBy`, `createdUtc`, `modifiedBy`, `modifiedUtc`, `fileReferences`, `computedPropertySets`, the two computed nested-twin fields `aggregatedTwins` and `parentTwin` (see [Nested Twins](#nested-twins-twin-references)), and the derived `name` and full `location` (the latter omitted when the root entity declares no location). `name`/`location` are read-only projections of the root entity, not part of the stored specification body.
49
+
50
+ List item (`ListTwinItem`): subset with `id`, `name`, `etag`, `propertySets`, `computedPropertySets`, `elementQuantities`, `relatedContainerIds`, `version`, `isDeleted`, `createdBy`, `createdUtc`, `modifiedBy`, `modifiedUtc`, `aggregatedTwins`, `parentTwin`, `location`, `classes`.
51
+
52
+ The list `location` is a **coordinates-only** view: `{ epsg, latitude, longitude }`, where `epsg` is always `"4326"`. It is present only when the twin's specification declares a location, and is omitted entirely otherwise. `elevation` and `trueNorth` are intentionally left off the list to keep the payload small; the full single-twin GET (`TwinGet`) continues to return the complete `location` (including `elevation` and `trueNorth`) from the specification.
53
+
54
+ The list `classes` is the deduplicated, ordinally-sorted set of IFC entity classes the twin's specification declares across its spatial hierarchy (sites, facilities, facility parts, and spaces) — e.g. `["IfcBuilding", "IfcBuildingStorey", "IfcSite", "IfcSpace"]`. It lets applications see *what a twin contains* without reading the specification blob. It is **always present** as an array and is `[]` when the twin declares no such entities. The set is derived from the specification at write time; the full single-twin GET (`TwinGet`) does not repeat it, since that response already returns every entity with its `class` inline.
55
+
56
+ Delete/Restore request: `{ etag: string }`.
57
+
58
+ ### Query Parameters
59
+
60
+ **List twins:** `relatedTo` (container ID filter), `includeDeleted` (bool, default: false), `page` (pagination token), `limit` (default: 100, max: 1000).
61
+
62
+ **List versions:** `includeDeleted` (bool), `page`, `limit` (default: 100, max: 1000).
63
+
64
+ **Events:** `offset`, `limit` (default: 100, max: 500).
65
+
66
+ ## Optimistic Concurrency
67
+
68
+ All mutating operations use the `etag` field for conflict detection:
69
+
70
+ - **Create**: `etag` is omitted or null. If the twin already exists, the server treats it as an update and requires a matching etag.
71
+ - **Update**: `etag` must match the server's current etag. On mismatch, the server returns **409 Conflict** with the current twin state in the response body.
72
+ - **Delete/Restore**: `etag` is sent in the request body. Same 409 behavior on mismatch.
73
+
74
+ The 409 response containing the current state allows clients to implement merge-and-retry logic without an extra round-trip.
75
+
76
+ ## Soft Delete and Restore
77
+
78
+ - `DELETE /twins/{twinId}` marks the twin as deleted (new version with `isDeleted = true`).
79
+ - `PUT /twins/{twinId}/restore` restores a soft-deleted twin.
80
+ - Both operations require the `Delete` permission and the current etag.
81
+ - Deleted twins return redacted responses: list results (`TwinEntry`) redact the name to `"[Redacted]"`, omit properties and `location`, and empty `classes` to `[]` (it is always present, so it cannot be omitted), while GET-by-id (`TwinGet`) keeps the real name but strips the rest of the specification and relationships — emptying `entities` to `{}`, emptying `aggregatedTwins`, `aggregatedSpaces`, `elementQuantities`, `propertySets` and `fileReferences` to `[]`, setting `location` and `site` to `null`, and omitting `parentTwin`.
82
+ - List operations exclude deleted twins by default; use `includeDeleted=true` to include them.
83
+
84
+ ## Root Entity
85
+
86
+ Every twin declares a `rootEntity` (required) naming the entity that roots its model tree. The service enforces (`RootEntityValidator`):
87
+
88
+ - `rootEntity` must be a **site or facility** present in `entities`; facility parts and spaces are not allowed as roots. A value that is not a key in `entities.sites` or `entities.facilities` — including a `twinReferences` key — is rejected, so a nested twin reference can never be a root.
89
+ - A **facility-rooted** twin must not contain a site (neither in `entities.sites` nor the legacy `site` field) and cannot aggregate twins. The converter aggregates only the root under the project, so a site or reference on a facility root would be orphaned from the model tree.
90
+ - The root entity must have a **non-empty `name`**, for **both** site and facility roots. `Site.name` is required by the schema, but `Facility.name` is optional, so a nameless facility root is rejected — the root's name becomes the twin's name and a twin always has a name.
91
+
92
+ The legacy top-level `site` field is deprecated; `rootEntity` is authoritative. A failing root is rejected with cause `InvalidRootEntity`.
93
+
94
+ > **Back-compat note.** Requiring a name on the root is a behavioural tightening: before name/location moved onto the root, a client could author a top-level `name` and leave the root entity nameless. Such a write is now rejected — clients must put the name on the root entity (the site or facility named by `rootEntity`). This is intentional (the no-legacy-compat stance); there is no transition fallback that accepts a top-level name.
95
+
96
+ ### Name and location are derived from the root entity
97
+
98
+ A twin's `name` and `location` are **owned by its root entity**, not authored at the top level of the specification. `Site` and `Facility` each carry an optional `location` (WGS84); the root entity's `name` and `location` are extracted at write time, persisted on the version row, and surfaced on `TwinGet` (full `location`) and the list (`name` + coordinates-only `location`). They are read-only on input: a `TwinPut` cannot set them, mirroring the computed nested-twin fields (`aggregatedTwins`/`parentTwin`). Non-root sites/facilities may also carry a `location`, but only the root's is the twin's location.
99
+
100
+ The T8 client-model converter likewise reads the project name from the root entity. If a twin's `rootEntity` is unresolvable (names no site/facility in `entities` — an edge case the validator blocks on write, reachable only for pre-existing data), the converter produces a project with **no name** rather than falling back to a top-level value (there is none after migration). This graceful degradation only affects already-malformed twins.
101
+
102
+ ## Nested Twins (Twin References)
103
+
104
+ A twin can nest other twins as children, forming a parent/child tree. This is the **twin reference** feature (TWINS-3703). The behavioural rationale — especially how references that involve deleted twins are handled — is recorded in the ADR [Twin References Are Spec-Declared Edges With Computed Parentage](../../../../decisions/twin-reference-lifecycle.md); this section summarises the schema surface and the rules the service enforces.
105
+
106
+ ### Schema surface
107
+
108
+ A reference has two halves, both authored in the parent's specification blob (so both are versioned):
109
+
110
+ - **Declaration** — `entities.twinReferences` is a `{ refId → { twin: <twinId> } }` map. A `TwinReference` is intentionally minimal: it carries only the referenced twin's id (no transform, no name).
111
+ - **Placement** — a `Site`'s `aggregatedTwinReferences` lists the `refId`s nested directly under it. Placement is what gives a reference a position in the spatial tree.
112
+
113
+ A child twin must hang on a site: **every declared reference must be placed under exactly one site**. A reference declared in `twinReferences` but placed by no site (a dangling reference) is rejected, as is placing a `refId` that is not declared, or placing the same `refId` under more than one site. The holding site must also be **reachable from `rootEntity`** through the site aggregation chain (`site → aggregatedSites`). Only a site aggregates sites — facilities, facility parts and spaces never aggregate a site — so a site is reachable only along that chain; a reference under a site disconnected from the root is rejected, since the converter would orphan it from the model tree. Because a reference is placed under a `Site`, a **facility-rooted twin cannot aggregate twins** (it has no site). These cross-field rules are enforced in the service, since JSON Schema (draft-06) cannot express them.
114
+
115
+ ### Computed fields
116
+
117
+ From those declarations the service maintains two **computed** read-model fields, returned on `TwinGet` and `ListTwinItem`:
118
+
119
+ - **`aggregatedTwins`** (`string[]`) — the ids of the twins this twin parents (the flat projection of its `twinReferences`).
120
+ - **`parentTwin`** (`string?`) — the id of the twin that references this twin, or omitted for a root twin.
121
+
122
+ These are *projections of the actual edges, not authored input*. A `TwinPut` that sends them has them ignored. The authoritative relational edge is `twin.twins.parent_twin_id`, maintained transactionally by `upsert_twin`; `parentTwin` reads it directly.
123
+
124
+ ### Spec-versions-vs-computed-doesn't
125
+
126
+ This is the load-bearing rule:
127
+
128
+ - Changing **specification** data (name, location, `entities` including `twinReferences`/placement) creates a new version, a new etag, and emits a `TwinUpdated` event.
129
+ - Changing **computed** data (`parentTwin`, `aggregatedTwins`) is maintained in place — **no new version, no etag change, no event**.
130
+
131
+ Because parentage is computed, re-parenting a child is *not an edit of the child* and never bumps the child's version. A single PUT that adds N references versions exactly one twin (the parent), not N+1.
132
+
133
+ ### Enforced invariants
134
+
135
+ `upsert_twin` validates the references that are *newly added* in a write (references carried over from the previous version are left untouched) and enforces, transactionally:
136
+
137
+ | Rule | SQL violation | HTTP `cause` |
138
+ | ---- | --------- | --- |
139
+ | A twin must not reference itself | `self_reference` | `SelfTwinReference` |
140
+ | A referenced twin must exist | `referenced_row_not_found` | `ReferencedItemNotFound` |
141
+ | A referenced twin must not be deleted (a **new** reference to a deleted twin is rejected) | `referenced_row_deleted` | `ReferencedItemDeleted` |
142
+ | A twin has at most one parent — unless its current parent is itself deleted, in which case it may be re-parented | `twin_already_referenced` | `TwinAlreadyReferenced` |
143
+ | References must not form a cycle | `cycle` | `CyclicTwinReference` |
144
+
145
+ Further rules are enforced in C# *before* the transaction (returned as a 400 with the named `cause`), because the SQL function only sees a de-duplicated id array: a twin may be **referenced at most once** within a single specification (`DuplicateTwinReference`), and each `refId` must be **placed under exactly one site reachable from `rootEntity`** (`InvalidTwinReferencePlacement`).
146
+
147
+ ### Permission to reference
148
+
149
+ Editing a twin requires `Edit` on that twin. Adding a reference additionally requires **`View` on the referenced twin**: a reference places the child in this twin's tree and repoints the child's computed parent, so the caller must be able to read it. The check applies only to references *newly added* by a write (those carried over from the current version are not re-checked, mirroring `upsert_twin`'s claim loop — re-saving a twin that still references a child it can no longer read is allowed). A newly referenced twin that is missing **or** not visible to the caller is reported identically, as `ReferencedItemNotFound`, so the response never reveals the existence of a twin the caller cannot read (consistent with the service's "no View → not found, not forbidden" convention).
150
+
151
+ ### Deleted twins
152
+
153
+ A reference to a twin that is deleted *after* it was first referenced is **kept** (the parent may keep and re-save it), but a **new** reference to an already-deleted twin is rejected. Consequently a live twin's `aggregatedTwins`, or a live child's `parentTwin`, may name a deleted (and therefore redacted) twin — consumers walking the tree must expect that.
154
+
155
+ A child whose current parent is *deleted* may be claimed by another twin (the "steal"). At steal time only the relational edge moves; the deleted parent's specification still declares the lost child, but this is invisible because deleted twins are redacted on read. The declaration is reconciled against the actual edges on **restore**. See the ADR for the full lifecycle.
156
+
157
+ ## Content and Processing
158
+
159
+ After a twin is updated, processors generate content in various formats:
160
+
161
+ - **t8cm** — T8 Client Model (BIM product hierarchy, properties, classifications)
162
+ - **t8cm-ext** — T8 Client Model extensions
163
+ - **t8gi** — T8 Geometry Info (mesh descriptors, styles, regions)
164
+ - **t8gd** — T8 Geometry Data
165
+
166
+ The content endpoint returns metadata about available content items and their associated processors. Content blobs are downloaded separately via the content blob endpoint, which supports HTTP range requests.
167
+
168
+ ### Version Content Response
169
+
170
+ ```typescript
171
+ interface TwinVersionContentResponse {
172
+ twinId: string;
173
+ twinVersion: number;
174
+ status: string;
175
+ processors: TwinProcessorResponse[];
176
+ content: TwinContentResponse[];
177
+ computedPropertySets: TwinComputedPropertySetsResponse[];
178
+ }
179
+ ```
180
+
181
+ ### Diagnostics
182
+
183
+ Each twin version may have diagnostics from processing, returned as a summary (fatal/error/warning counts) and detailed diagnostics data.
184
+
185
+ ### Processor Events
186
+
187
+ Processor events provide a timeline of processing activity for a twin version. Events can be queried for all processors or filtered to a specific processor.
188
+
189
+ ## Container Relationship
190
+
191
+ - `relatedContainerIds` links a twin to one or more containers (projects/folders).
192
+ - Container IDs are **immutable after creation** — they cannot be changed on subsequent updates.
193
+ - When creating a twin linked to multiple containers, the service computes intersected ACLs across all container scopes.
194
+
195
+ ## Client SDK
196
+
197
+ The `@twinfinity/twin` package (`packages/twinfinity-twin/`) provides a typed TypeScript client. See its [README](../../../../../packages/twinfinity-twin/README.md) for usage examples. For the conventions all TypeScript clients follow, see [TypeScript Client Conventions](../../../packages/client-conventions/typescript-client-conventions.md).