@pdtf/schemas 3.6.0-dev.20 → 3.6.0-dev.21
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/docs/v4-mapping-and-recomposition.md +416 -0
- package/index.js +18 -0
- package/package.json +2 -1
- package/src/schemas/v3/combined.json +65 -0
- package/src/schemas/v3/compactSkeleton.txt +10 -0
- package/src/schemas/v3/pdtf-transaction.json +65 -0
- package/src/schemas/v3/skeleton.json +12 -0
- package/src/schemas/v4/Gift.json +88 -0
- package/src/schemas/v4/Offer.json +384 -0
- package/src/schemas/v4/Organisation.json +167 -0
- package/src/schemas/v4/Person.json +321 -0
- package/src/schemas/v4/Property.json +27769 -0
- package/src/schemas/v4/Representation.json +67 -0
- package/src/schemas/v4/SellerCapacity.json +193 -0
- package/src/schemas/v4/Title.json +14672 -0
- package/src/schemas/v4/Transaction.json +1570 -0
- package/src/schemas/v4/TransactionRole.json +59 -0
- package/src/schemas/v4/mapping.json +1129 -0
- package/src/utils/generateV4Schemas.js +1341 -0
- package/src/utils/v4.js +765 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# V4 mapping and recomposition
|
|
2
|
+
|
|
3
|
+
PDTF 1.x verified claims are JSON Pointers against the V3 transaction schema.
|
|
4
|
+
PDTF 2.0 credentials are per-entity documents. The V4 generator
|
|
5
|
+
(`src/utils/generateV4Schemas.js`) already encodes every rule needed to move
|
|
6
|
+
between the two — but until now it emitted only schemas, so any consumer
|
|
7
|
+
wanting to map a claim onto a credential (or back) had to re-implement those
|
|
8
|
+
rules by hand and then keep them in step.
|
|
9
|
+
|
|
10
|
+
This adds three things, all produced by the same generation run:
|
|
11
|
+
|
|
12
|
+
| Artefact | What it is |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `src/schemas/v4/mapping.json` | Generated manifest: V3 pointer → entity + pointer, and the inverse |
|
|
15
|
+
| `src/utils/v4.js` | `decompose` / `recompose` / `resolveV3Pointer`, driven entirely by the manifest |
|
|
16
|
+
| `src/tests/v4/roundTrip.test.js` | V3 → entities → V3 equality, with documented exceptions |
|
|
17
|
+
|
|
18
|
+
Regenerate everything with:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run generate:v4
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The generator remains read-only against V3.
|
|
25
|
+
|
|
26
|
+
## Using it
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const {
|
|
30
|
+
decomposeToV4,
|
|
31
|
+
recomposeFromV4,
|
|
32
|
+
resolveV3Pointer,
|
|
33
|
+
v4Mapping,
|
|
34
|
+
} = require("@pdtf/schemas");
|
|
35
|
+
|
|
36
|
+
const entities = decomposeToV4(v3Transaction);
|
|
37
|
+
// { Property, Title: [...], Transaction, Person: [...] }
|
|
38
|
+
|
|
39
|
+
const v3Again = recomposeFromV4(entities);
|
|
40
|
+
|
|
41
|
+
// Map a verified-claim pointer onto the credential that now carries it
|
|
42
|
+
resolveV3Pointer("/propertyPack/ownership/ownershipsToBeTransferred/1/ownershipType");
|
|
43
|
+
// { entity: "Title", entityPointer: "/ownershipType", rule: "title.ownershipsToBeTransferred" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Pass `idFactory` to `decomposeToV4` to mint real URNs/DIDs; the default is
|
|
47
|
+
deterministic and derived from `transactionId`, so repeated runs are stable and
|
|
48
|
+
diffable. A supplied factory may be **partial** — anything it does not implement
|
|
49
|
+
falls back to the default, so adding an entity type here cannot break a caller's
|
|
50
|
+
factory on upgrade.
|
|
51
|
+
|
|
52
|
+
## The manifest
|
|
53
|
+
|
|
54
|
+
Rules are **prefix rewrites**, not a leaf enumeration. A V3 pointer is resolved
|
|
55
|
+
by longest-prefix match against `rule.v3Pointer` (with `{index}` matching an
|
|
56
|
+
array index), which means pointers far deeper than anything listed — the kind
|
|
57
|
+
claims actually carry — resolve correctly, and the file does not churn every
|
|
58
|
+
time a field is added to V3.
|
|
59
|
+
|
|
60
|
+
`mapping.source` records exactly which V3 input produced the output:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"packageVersion": "3.6.0-31",
|
|
65
|
+
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
66
|
+
"combinedSha256": "…"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same block is stamped on every entity schema as `x-pdtf-source`, so a
|
|
71
|
+
consumer holding a single schema can still pin.
|
|
72
|
+
|
|
73
|
+
The content hash is deliberately the only identifier. A commit sha would tie
|
|
74
|
+
generated output to git history: it could never be correct in the same commit
|
|
75
|
+
that changes `combined.json`, and it would churn on every rebase or cherry-pick
|
|
76
|
+
without the schema content differing at all. To find the commit for a hash,
|
|
77
|
+
search the history for it.
|
|
78
|
+
|
|
79
|
+
### It cannot drift
|
|
80
|
+
|
|
81
|
+
`verifyCoverage()` runs at generation time and **fails the build** if any V3 key
|
|
82
|
+
is not claimed by a rule, or if a rule claims a key that is absent from its
|
|
83
|
+
target entity schema. Adding a field to V3 that has no home in V4 is a hard
|
|
84
|
+
error rather than a field that silently disappears from the round trip.
|
|
85
|
+
|
|
86
|
+
## The four ambiguous cases
|
|
87
|
+
|
|
88
|
+
### 1. `Title` merges two V3 arrays
|
|
89
|
+
|
|
90
|
+
`propertyPack.titlesToBeSold` and
|
|
91
|
+
`propertyPack.ownership.ownershipsToBeTransferred` both contribute to `Title`.
|
|
92
|
+
|
|
93
|
+
- **Key collision:** `titleNumber`, and only `titleNumber`. It is the same fact
|
|
94
|
+
on both sides, and it is declared in `mapping.collisions`.
|
|
95
|
+
- **How the indices correlate: they don't.** The arrays are correlated by
|
|
96
|
+
`titleNumber` value. A V3 instance may order them differently or list a title
|
|
97
|
+
on only one side, and the tests cover both.
|
|
98
|
+
- **Recomposition** splits a `Title` back by the `ownedKeys` each rule declares.
|
|
99
|
+
A title carrying only shared keys goes to `titlesToBeSold` alone
|
|
100
|
+
(`instance.primarySource`).
|
|
101
|
+
|
|
102
|
+
### 2. `propertyPack.ownership` splits two ways
|
|
103
|
+
|
|
104
|
+
`ownershipsToBeTransferred` → `Title`; everything else →
|
|
105
|
+
`Transaction.saleContext`.
|
|
106
|
+
|
|
107
|
+
`propertyPack.legalOwners` also lands in `saleContext`. It is excluded from
|
|
108
|
+
`Property` and, being an unidentified name list with no link to `participants`,
|
|
109
|
+
cannot be correlated to `Person`/`Organisation` entities. It is a sale-level
|
|
110
|
+
ownership fact of the same family as `numberOfSellers` and
|
|
111
|
+
`isLimitedCompanySale`, so it sits alongside them — which keeps the round trip
|
|
112
|
+
lossless. Previously it was dropped entirely.
|
|
113
|
+
|
|
114
|
+
### 3. Relationship fields on `participants`
|
|
115
|
+
|
|
116
|
+
`role`, `sellersCapacity`, `organisation`, `organisationReference`,
|
|
117
|
+
`participantId` and `actingFor` describe a party's relationship to *this
|
|
118
|
+
transaction*, not the party, so they stay off `Person`. They are carried on
|
|
119
|
+
`Transaction.participants[]`, each entry pairing a `participant` DID with those
|
|
120
|
+
fields.
|
|
121
|
+
|
|
122
|
+
This means the round trip needs only four entities — `Property`, `Title`,
|
|
123
|
+
`Transaction`, `Person` (`mapping.roundTrip.entities`). `Organisation`,
|
|
124
|
+
`Representation`, `SellerCapacity` and `Offer` are credential-facing
|
|
125
|
+
projections of data those four already carry; they are not required to
|
|
126
|
+
reconstruct V3.
|
|
127
|
+
|
|
128
|
+
### 4. Array order
|
|
129
|
+
|
|
130
|
+
V3 arrays become id-keyed entities, so order has to live somewhere. It lives on
|
|
131
|
+
`Transaction` — `titlesToBeSold` and `participants` are ordered reference lists,
|
|
132
|
+
and they are authoritative for recomposition. No index or provenance field is
|
|
133
|
+
added to the entity documents themselves, so a credential never carries a
|
|
134
|
+
position that could diverge from another producer's.
|
|
135
|
+
|
|
136
|
+
## Documented round-trip exceptions
|
|
137
|
+
|
|
138
|
+
`mapping.roundTrip.knownExceptions` carries these at runtime:
|
|
139
|
+
|
|
140
|
+
1. **Overlay metadata** (`*Ref`, `*Required` keys) is stripped from V4. It is
|
|
141
|
+
schema-level only and never appears in instance data.
|
|
142
|
+
2. A `Title` carrying only keys shared by both source arrays recomposes into
|
|
143
|
+
`titlesToBeSold` only, not into `ownershipsToBeTransferred`.
|
|
144
|
+
3. **`ownershipsToBeTransferred` order is canonicalised** to `titlesToBeSold`
|
|
145
|
+
order. The array is correlated by `titleNumber` and its order carries no
|
|
146
|
+
meaning in V3, so this is a deliberate normalisation that removes the
|
|
147
|
+
index-divergence class of bug rather than reproducing it. Entry *content* is
|
|
148
|
+
preserved exactly.
|
|
149
|
+
|
|
150
|
+
4. **An explicitly empty container recomposes as absent** — `propertyPack {}`,
|
|
151
|
+
`propertyPack.ownership {}` and `propertyPack.titlesToBeSold []` are all valid
|
|
152
|
+
V3 but carry no data, and the entity model has no place to record "present but
|
|
153
|
+
empty". (`participants []` is invalid V3 — `minItems: 1` — so it does not
|
|
154
|
+
arise.)
|
|
155
|
+
5. **`recompose` always emits `$schema`**, set to `source.v3SchemaId`. An
|
|
156
|
+
instance that omitted one gains it.
|
|
157
|
+
|
|
158
|
+
Everything else round-trips to deep equality, including the full example
|
|
159
|
+
transaction in `src/examples/v3/exampleTransaction.json`.
|
|
160
|
+
|
|
161
|
+
## What the round trip is actually tested against
|
|
162
|
+
|
|
163
|
+
Worth being precise about, because "it round-trips" is easy to overclaim:
|
|
164
|
+
|
|
165
|
+
| | |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `exampleTransaction.json` | One real instance — roughly **12% of the V3 leaf-path surface** (456 of ~3,300 paths) |
|
|
168
|
+
| Synthetic awkward instance | The structural transforms: two-array merge, index divergence, one-sided titles |
|
|
169
|
+
| `roundTripEdges.test.js` | The six top-level keys the example never touches, plus the boundaries below |
|
|
170
|
+
|
|
171
|
+
The untested majority is `propertyPack` subtree, which passes through wholesale
|
|
172
|
+
under a single prefix rule — low risk, but not zero. The transforms that could
|
|
173
|
+
actually lose data (title merge/split, participant split, ownership split) are
|
|
174
|
+
covered directly.
|
|
175
|
+
|
|
176
|
+
**Refused rather than guessed:** a `titleNumber` repeated within one source
|
|
177
|
+
array throws. It is valid V3 — neither array declares uniqueness — but a title
|
|
178
|
+
number identifies a title, so merging the two entries would silently drop one.
|
|
179
|
+
|
|
180
|
+
**Not yet done:** generating fixtures from the schema to exercise the remaining
|
|
181
|
+
~88%, and round-tripping real production transactions. Either would turn "no
|
|
182
|
+
known failures" into something stronger.
|
|
183
|
+
|
|
184
|
+
## Defects this surfaced in the generated schemas
|
|
185
|
+
|
|
186
|
+
Two fields were being lost because the generator read only `node.properties` and
|
|
187
|
+
ignored V3's `discriminator` + `oneOf` branches:
|
|
188
|
+
|
|
189
|
+
- **`Title` was missing every tenure detail** — `leaseholdInformation`,
|
|
190
|
+
`managedFreeholdOrCommonholdInformation`, `estateRentcharges`,
|
|
191
|
+
`wholeFreeholdForSale`, `otherOwnershipDetails` all live on
|
|
192
|
+
`ownershipsToBeTransferred` branches.
|
|
193
|
+
- **`Person`/`SellerCapacity` were missing** `sellersCapacity`,
|
|
194
|
+
`dateBecameOwnerOrAuthority` and `offerId`, which live on `participants`
|
|
195
|
+
branches. `SellerCapacity` was hand-authored with a four-value `capacityType`
|
|
196
|
+
enum against V3's nine-value `capacity`.
|
|
197
|
+
|
|
198
|
+
`mergedProperties()` now folds branch properties in, and `SellerCapacity` is
|
|
199
|
+
derived from the V3 seller branch rather than hand-authored.
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
## Representation
|
|
203
|
+
|
|
204
|
+
V3 previously referred to people **by role only** (`originatorRole`,
|
|
205
|
+
`destinationRole` on enquiry messages) and nothing anywhere referenced a
|
|
206
|
+
specific participant. That made a representation relationship underivable — you
|
|
207
|
+
could see that a transaction had a "Seller's Conveyancer", but not which seller
|
|
208
|
+
they acted for — and it made two sellers with separate conveyancers
|
|
209
|
+
unrepresentable, because both participants would carry the same role with
|
|
210
|
+
nothing to tell them apart.
|
|
211
|
+
|
|
212
|
+
Two additive, optional fields close that gap:
|
|
213
|
+
|
|
214
|
+
| Field | Purpose |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `participants[].participantId` | Stable identity within the transaction |
|
|
217
|
+
| `participants[].actingFor` | The `participantId`s this participant is instructed by |
|
|
218
|
+
|
|
219
|
+
Both follow the existing key idiom (`^[A-Za-z0-9][A-Za-z0-9._:+-]*$`, the same
|
|
220
|
+
pattern `offers` and `enquiries` keys use). Neither carries a form `*Ref`, so no
|
|
221
|
+
overlay changes, and both are optional, so existing instances stay valid.
|
|
222
|
+
|
|
223
|
+
### Cardinality
|
|
224
|
+
|
|
225
|
+
One `Representation` per **(representative, represented party)** pair.
|
|
226
|
+
|
|
227
|
+
- A conveyancer instructed jointly by two sellers → `actingFor: ["s1", "s2"]` →
|
|
228
|
+
**two** Representations. Each is a separate statement that can be presented,
|
|
229
|
+
or revoked, on its own.
|
|
230
|
+
- A separated couple instructing their own conveyancers → two representatives,
|
|
231
|
+
each `actingFor` one seller → **one each**. This is the case the old
|
|
232
|
+
role-only model could not express at all.
|
|
233
|
+
|
|
234
|
+
### A conveyancer is instructed by a person, not by an offer
|
|
235
|
+
|
|
236
|
+
`actingFor` references participants. The retainer is with the client, so a
|
|
237
|
+
buyer's conveyancer acts for the buyer; `participants[].offerId` remains the
|
|
238
|
+
separate link from that buyer to their offer. This also means a **prospective
|
|
239
|
+
buyer**, who has no `offerId` until they make an offer, can still be
|
|
240
|
+
represented — which a buyer-via-offer model could not do.
|
|
241
|
+
|
|
242
|
+
### The relationship credentials
|
|
243
|
+
|
|
244
|
+
`Representation`, `SellerCapacity`, `Offer`, `Gift` and `TransactionRole` are
|
|
245
|
+
`kind: "credential"`. They are the linking entities that **embody a party's
|
|
246
|
+
role**, and role is stored nowhere else.
|
|
247
|
+
|
|
248
|
+
| Credential | Asserts | Role |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `SellerCapacity` | this person sells, in this capacity | **implied**: Seller |
|
|
251
|
+
| `Offer` | this person made this offer | **implied**: Buyer |
|
|
252
|
+
| `Gift` | this person gifts funds towards a purchase | **implied**: Gift Donor |
|
|
253
|
+
| `Representation` | this party is instructed by that party | explicit — which kind |
|
|
254
|
+
| `TransactionRole` | this party takes this role | explicit — which role |
|
|
255
|
+
|
|
256
|
+
For the first three, role is implied 1:1 by the credential's existence: V3 puts
|
|
257
|
+
`sellersCapacity` only on the Seller branch and `giftDetails` only on Gift Donor.
|
|
258
|
+
`offerId` is on the Buyer **and** Gift Donor branches, so a donor's offer link
|
|
259
|
+
lives on `Gift` rather than `Offer`, which keeps `Offer ⇒ Buyer` exact.
|
|
260
|
+
|
|
261
|
+
`Representation` and `TransactionRole` each span several roles, so they carry
|
|
262
|
+
role as their own discriminator. That is not a duplicated participant attribute:
|
|
263
|
+
"what kind of representation is this" is a property of the representation, and
|
|
264
|
+
asserting a role is `TransactionRole`'s entire purpose. Representation's role
|
|
265
|
+
cannot be derived from the edge — acting for a seller tells you the side, not
|
|
266
|
+
whether the party is the conveyancer, the agent or the surveyor.
|
|
267
|
+
|
|
268
|
+
### Identity: `participants[].did`
|
|
269
|
+
|
|
270
|
+
V3 participants carry a `did` — the party's own identifier, minted when the
|
|
271
|
+
party is created, so in practice always present. V4 uses it verbatim as
|
|
272
|
+
`Person.id`; the `idFactory` mints one only for instances that carry none.
|
|
273
|
+
|
|
274
|
+
That matters because identity must be stable *across* transactions. A person
|
|
275
|
+
selling one property and buying another appears in two transactions, and should
|
|
276
|
+
be the same subject in both — one wallet, accumulating credentials, not one
|
|
277
|
+
identity per participation. A transaction-scoped id (`did:web:…:transactions:
|
|
278
|
+
<txId>:participants:<pid>`) cannot do that; `did:key` minted per person can.
|
|
279
|
+
|
|
280
|
+
`participantId` remains as the transaction-local fallback for implementations
|
|
281
|
+
that do not mint DIDs. `actingFor` names a party by `did` where one exists and
|
|
282
|
+
by `participantId` otherwise.
|
|
283
|
+
|
|
284
|
+
**One party, one entry.** A `did` repeated within a transaction is the same
|
|
285
|
+
party listed twice, which `decompose` refuses: a transaction is a single sale,
|
|
286
|
+
so nobody is both its buyer and its seller. The same person across a sale and an
|
|
287
|
+
onward purchase is two transactions, each with its own credentials, and needs
|
|
288
|
+
nothing special.
|
|
289
|
+
|
|
290
|
+
### Why role is not on the participant
|
|
291
|
+
|
|
292
|
+
Because a second copy would not be revoked.
|
|
293
|
+
|
|
294
|
+
Firing a conveyancer means revoking their `Representation`. If the transaction
|
|
295
|
+
also recorded `role: "Seller's Conveyancer"` against that participant, the
|
|
296
|
+
revocation would remove the relationship and leave the role assertion standing —
|
|
297
|
+
two sources of truth, disagreeing, with the stale one still readable.
|
|
298
|
+
|
|
299
|
+
So `Transaction.participants[]` is a **roster**, carrying only what remains true
|
|
300
|
+
of a party regardless of any relationship:
|
|
301
|
+
|
|
302
|
+
```jsonc
|
|
303
|
+
{ "participant": "did:pdtf:person-tx-c1",
|
|
304
|
+
"participantId": "c1",
|
|
305
|
+
"organisation": "Suemme and Profitt",
|
|
306
|
+
"organisationReference": "R1" }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`organisation` stays here rather than on the credential for the same
|
|
310
|
+
single-home reason: where someone works does not stop being true when a
|
|
311
|
+
representation ends. A credential names its parties by DID, so resolving the
|
|
312
|
+
representative gives the firm.
|
|
313
|
+
|
|
314
|
+
### They are part of the round trip
|
|
315
|
+
|
|
316
|
+
Because role and every relationship live only on the credentials, `recompose`
|
|
317
|
+
needs them — they are not redundant copies. `decompose` therefore returns them
|
|
318
|
+
alongside the other entities:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
const { decomposeToV4, recomposeFromV4 } = require("@pdtf/schemas");
|
|
322
|
+
|
|
323
|
+
const entities = decomposeToV4(v3Transaction);
|
|
324
|
+
// { Property, Title, Transaction, Person,
|
|
325
|
+
// Representation, SellerCapacity, Offer, Gift, TransactionRole }
|
|
326
|
+
|
|
327
|
+
recomposeFromV4(entities); // === v3Transaction
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Dropping a credential and recomposing gives back a V3 instance in which that
|
|
331
|
+
party has no role and no relationship — which is exactly what revocation should
|
|
332
|
+
mean, and is asserted by a test.
|
|
333
|
+
|
|
334
|
+
Every participant carrying a role receives exactly one role-bearing credential:
|
|
335
|
+
the specific one where the relationship exists, `TransactionRole` otherwise. A
|
|
336
|
+
participant with no role receives none.
|
|
337
|
+
|
|
338
|
+
`SellerCapacity` is scoped to the transaction rather than to a title, since the
|
|
339
|
+
transaction already carries the titles to be sold. It is emitted for **every**
|
|
340
|
+
seller, whether or not a capacity has been declared, so the credential embodying
|
|
341
|
+
the Seller role is never missing while a form is still being filled in.
|
|
342
|
+
|
|
343
|
+
### If you want an identifier everyone can mint
|
|
344
|
+
|
|
345
|
+
Three patterns, of which V3 already uses two:
|
|
346
|
+
|
|
347
|
+
- **First-writer-mints** — whoever adds the item sets the canonical id, and
|
|
348
|
+
everyone else adopts it because it is in the shared payload. What the seven new
|
|
349
|
+
identifiers assume. A source can simply use its own id as the value, promoting
|
|
350
|
+
it out of its private namespace. Its failure mode is duplication, not ambiguity:
|
|
351
|
+
two parties adding the same document independently mint two ids.
|
|
352
|
+
- **Authority-issued** — `uprn`, `titleNumber`, `lmkKey` and `hmlrReference`
|
|
353
|
+
already work this way. It needs a registry and one authoritative issuer, which
|
|
354
|
+
exists for properties, titles and EPCs but not for documents. Transactions are
|
|
355
|
+
identified by `transactionId` and need no second identifier alongside it.
|
|
356
|
+
- **Content-derived** — a hash of the file bytes. Interoperable with no issuer,
|
|
357
|
+
and it deduplicates for free, which first-writer-mints does not. It changes if a
|
|
358
|
+
file is re-rendered, and you need the bytes to compute it. Not unprecedented
|
|
359
|
+
here: `contracts[].signatures` already requires a `contractHash`.
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
## Identifiers and `did:web`
|
|
363
|
+
|
|
364
|
+
Entity ids come from the `idFactory` you pass to `decompose`. The built-in
|
|
365
|
+
default is deterministic and derived from `transactionId`, which keeps repeated
|
|
366
|
+
runs stable and diffable, but it is a placeholder — supply your own to mint real
|
|
367
|
+
identifiers.
|
|
368
|
+
|
|
369
|
+
`did:web` is a natural fit, because the DID document is hosted rather than
|
|
370
|
+
anchored to a ledger and can carry service endpoints, so a party holding a
|
|
371
|
+
credential can resolve where to fetch what it refers to:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
const entities = decomposeToV4(v3Transaction, {
|
|
375
|
+
idFactory: {
|
|
376
|
+
transaction: () => `did:web:moverly.com:transactions:${v3Transaction.transactionId}`,
|
|
377
|
+
person: (participant, index) =>
|
|
378
|
+
`did:web:moverly.com:transactions:${v3Transaction.transactionId}` +
|
|
379
|
+
`:participants:${participant?.participantId ?? index}`,
|
|
380
|
+
property: () => `urn:pdtf:property:${v3Transaction.transactionId}`,
|
|
381
|
+
title: (title, index) => `urn:pdtf:title:…`,
|
|
382
|
+
representation: (representative, representedParty) => `urn:pdtf:representation:…`,
|
|
383
|
+
sellerCapacity: (seller) => `urn:pdtf:sellercapacity:…`,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`did:web:moverly.com:transactions:<id>` resolves to
|
|
389
|
+
`https://moverly.com/transactions/<id>/did.json`.
|
|
390
|
+
|
|
391
|
+
### The DID pattern accepts colons, as the spec requires
|
|
392
|
+
|
|
393
|
+
A `did:web` encodes its path as colon-separated segments, and a port is
|
|
394
|
+
percent-encoded (`did:web:example.com%3A3000:…`). The W3C ABNF allows both —
|
|
395
|
+
`method-specific-id = *( *idchar ":" ) 1*idchar` — so the schema pattern follows
|
|
396
|
+
it exactly rather than approximating:
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
^did:[a-z0-9]+:(?:(?:[a-zA-Z0-9._-]|%[0-9A-Fa-f]{2})*:)*(?:[a-zA-Z0-9._-]|%[0-9A-Fa-f]{2})+$
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
An earlier pattern here allowed no colons after the method name, which rejected
|
|
403
|
+
every `did:web` carrying a path — in nine places across five entities.
|
|
404
|
+
|
|
405
|
+
### It does not disturb the round trip
|
|
406
|
+
|
|
407
|
+
`Transaction.id` is synthetic: `decompose` mints it and `recompose` drops it,
|
|
408
|
+
because V3 has no field for it. `transactionId` is the real V3 data and survives
|
|
409
|
+
untouched. So a `did:web` transaction id **embeds** the V3 identifier rather than
|
|
410
|
+
replacing it, both are present on the entity, and which DID method you choose
|
|
411
|
+
makes no difference to the V3 that comes back. There is a test asserting exactly
|
|
412
|
+
that.
|
|
413
|
+
|
|
414
|
+
`Property` and `Title` keep `urn:` ids: they are things described, not parties
|
|
415
|
+
that resolve to a DID document. Nothing stops you minting `did:web` for them —
|
|
416
|
+
the URN pattern already permits colons — but nothing requires it either.
|
package/index.js
CHANGED
|
@@ -821,6 +821,16 @@ const setCacheMaxSize = (maxSize) => {
|
|
|
821
821
|
return keysToDelete.length;
|
|
822
822
|
};
|
|
823
823
|
|
|
824
|
+
const {
|
|
825
|
+
mapping: v4Mapping,
|
|
826
|
+
decompose: decomposeToV4,
|
|
827
|
+
recompose: recomposeFromV4,
|
|
828
|
+
arrayKeyFor: v4ArrayKeyFor,
|
|
829
|
+
identifyV3Pointer,
|
|
830
|
+
resolveV3Pointer,
|
|
831
|
+
resolveV3PointerAll,
|
|
832
|
+
} = require("./src/utils/v4");
|
|
833
|
+
|
|
824
834
|
module.exports = {
|
|
825
835
|
ajv,
|
|
826
836
|
getTransactionSchema,
|
|
@@ -843,4 +853,12 @@ module.exports = {
|
|
|
843
853
|
warmupCache,
|
|
844
854
|
pruneCacheByAge,
|
|
845
855
|
setCacheMaxSize,
|
|
856
|
+
// V3 <-> V4 entity decomposition (see src/schemas/v4/mapping.json)
|
|
857
|
+
v4Mapping,
|
|
858
|
+
decomposeToV4,
|
|
859
|
+
recomposeFromV4,
|
|
860
|
+
v4ArrayKeyFor,
|
|
861
|
+
identifyV3Pointer,
|
|
862
|
+
resolveV3Pointer,
|
|
863
|
+
resolveV3PointerAll,
|
|
846
864
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdtf/schemas",
|
|
3
|
-
"version": "3.6.0-dev.
|
|
3
|
+
"version": "3.6.0-dev.21",
|
|
4
4
|
"description": "Property Data Trust Framework Schemas and Utilities",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test:watch": "jest --watch",
|
|
20
20
|
"extract-overlays": "cd src/utils && node extractOverlay.js",
|
|
21
21
|
"extract-extension-overlays": "cd src/utils && node extractExtensionOverlays.js",
|
|
22
|
+
"generate:v4": "node src/utils/generateV4Schemas.js",
|
|
22
23
|
"publish:dev": "npm publish --tag dev",
|
|
23
24
|
"publish:next": "npm publish --tag next",
|
|
24
25
|
"publish:latest": "npm publish --tag latest"
|
|
@@ -243,6 +243,29 @@
|
|
|
243
243
|
"title": "Reference",
|
|
244
244
|
"type": "string"
|
|
245
245
|
},
|
|
246
|
+
"did": {
|
|
247
|
+
"title": "Participant DID",
|
|
248
|
+
"description": "Decentralised Identifier for this party - the primary way to identify them. Unlike participantId it is global and stable: the same person carries the same DID across transactions, and across two entries in one transaction where they are both buying and selling. Minted when the party is created, so it should always be present.",
|
|
249
|
+
"type": "string",
|
|
250
|
+
"pattern": "^did:[a-z0-9]+:(?:(?:[a-zA-Z0-9._-]|%[0-9A-Fa-f]{2})*:)*(?:[a-zA-Z0-9._-]|%[0-9A-Fa-f]{2})+$"
|
|
251
|
+
},
|
|
252
|
+
"participantId": {
|
|
253
|
+
"title": "Participant id",
|
|
254
|
+
"description": "Transaction-local identifier for this participant, for implementations that do not mint DIDs. Prefer did. Referencing by array position is unsafe because participants may be added or removed.",
|
|
255
|
+
"type": "string",
|
|
256
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
257
|
+
},
|
|
258
|
+
"actingFor": {
|
|
259
|
+
"title": "Acting for",
|
|
260
|
+
"description": "The parties this participant is instructed by, for example the sellers a conveyancer acts for, identified by their did or - where no DID is minted - their participantId. One entry per represented party: a conveyancer instructed jointly by two sellers lists both, and sellers who instruct separate conveyancers appear as separate representatives each acting for one of them. A conveyancer is instructed by a person, not by an offer - the offer link is participants[].offerId.",
|
|
261
|
+
"type": "array",
|
|
262
|
+
"items": {
|
|
263
|
+
"title": "Represented party",
|
|
264
|
+
"description": "did, or participantId, of a party this participant is instructed by",
|
|
265
|
+
"type": "string",
|
|
266
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
246
269
|
"role": {
|
|
247
270
|
"ta6ed6Ref": "1",
|
|
248
271
|
"ta6ed6v2Ref": "1",
|
|
@@ -1287,6 +1310,12 @@
|
|
|
1287
1310
|
"baspi4Required": ["mediaUrl"],
|
|
1288
1311
|
"baspi5Required": ["mediaUrl"],
|
|
1289
1312
|
"properties": {
|
|
1313
|
+
"mediaId": {
|
|
1314
|
+
"title": "Media id",
|
|
1315
|
+
"description": "Stable identifier for this media item within the transaction. Array position is not a safe reference: media is added, removed and re-ordered independently by different parties.",
|
|
1316
|
+
"type": "string",
|
|
1317
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
1318
|
+
},
|
|
1290
1319
|
"mediaType": {
|
|
1291
1320
|
"title": "Type of media",
|
|
1292
1321
|
"type": "string",
|
|
@@ -33424,6 +33453,12 @@
|
|
|
33424
33453
|
"type": "object",
|
|
33425
33454
|
"required": ["name", "signedOn"],
|
|
33426
33455
|
"properties": {
|
|
33456
|
+
"signatureId": {
|
|
33457
|
+
"title": "Signature id",
|
|
33458
|
+
"description": "Stable identifier for this signature within the transaction. Array position is not a safe reference: signatories sign independently and in any order.",
|
|
33459
|
+
"type": "string",
|
|
33460
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
33461
|
+
},
|
|
33427
33462
|
"name": {
|
|
33428
33463
|
"sr24Ref": "1.3.1",
|
|
33429
33464
|
"title": "Name of signatory",
|
|
@@ -46096,6 +46131,12 @@
|
|
|
46096
46131
|
"items": {
|
|
46097
46132
|
"type": "object",
|
|
46098
46133
|
"properties": {
|
|
46134
|
+
"searchId": {
|
|
46135
|
+
"title": "Search id",
|
|
46136
|
+
"description": "Stable identifier for this search within the transaction. Array position is not a safe reference: searches are ordered and updated independently by different parties.",
|
|
46137
|
+
"type": "string",
|
|
46138
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
46139
|
+
},
|
|
46099
46140
|
"displayName": {
|
|
46100
46141
|
"title": "Human-readable name of instructed search product",
|
|
46101
46142
|
"type": "string",
|
|
@@ -46182,6 +46223,12 @@
|
|
|
46182
46223
|
"items": {
|
|
46183
46224
|
"type": "object",
|
|
46184
46225
|
"properties": {
|
|
46226
|
+
"documentId": {
|
|
46227
|
+
"title": "Document id",
|
|
46228
|
+
"description": "Stable identifier for this document within the transaction. Array position is not a safe reference: documents are added, removed and re-ordered independently by different parties.",
|
|
46229
|
+
"type": "string",
|
|
46230
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
46231
|
+
},
|
|
46185
46232
|
"displayName": {
|
|
46186
46233
|
"title": "Human-readable name of document",
|
|
46187
46234
|
"type": "string",
|
|
@@ -46243,6 +46290,12 @@
|
|
|
46243
46290
|
"items": {
|
|
46244
46291
|
"type": "object",
|
|
46245
46292
|
"properties": {
|
|
46293
|
+
"surveyId": {
|
|
46294
|
+
"title": "Survey id",
|
|
46295
|
+
"description": "Stable identifier for this survey within the transaction. Array position is not a safe reference: a property may be surveyed more than once, by different parties.",
|
|
46296
|
+
"type": "string",
|
|
46297
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
46298
|
+
},
|
|
46246
46299
|
"outside": {
|
|
46247
46300
|
"type": "object",
|
|
46248
46301
|
"properties": {
|
|
@@ -48968,6 +49021,12 @@
|
|
|
48968
49021
|
"items": {
|
|
48969
49022
|
"type": "object",
|
|
48970
49023
|
"properties": {
|
|
49024
|
+
"contractId": {
|
|
49025
|
+
"title": "Contract id",
|
|
49026
|
+
"description": "Stable identifier for this contract within the transaction. Array position is not a safe reference: contracts are added and superseded independently by different parties.",
|
|
49027
|
+
"type": "string",
|
|
49028
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
49029
|
+
},
|
|
48971
49030
|
"contract": {
|
|
48972
49031
|
"type": "object",
|
|
48973
49032
|
"required": ["template", "terms"],
|
|
@@ -49048,6 +49107,12 @@
|
|
|
49048
49107
|
"type": "object",
|
|
49049
49108
|
"required": ["name", "signedOn", "role", "contractHash"],
|
|
49050
49109
|
"properties": {
|
|
49110
|
+
"signatureId": {
|
|
49111
|
+
"title": "Signature id",
|
|
49112
|
+
"description": "Stable identifier for this signature within the contract. Array position is not a safe reference: signatories sign independently and in any order.",
|
|
49113
|
+
"type": "string",
|
|
49114
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:+-]*$"
|
|
49115
|
+
},
|
|
49051
49116
|
"name": {
|
|
49052
49117
|
"type": "string"
|
|
49053
49118
|
},
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
countryCode
|
|
27
27
|
organisation
|
|
28
28
|
organisationReference
|
|
29
|
+
did
|
|
30
|
+
participantId
|
|
31
|
+
actingFor[]
|
|
29
32
|
role
|
|
30
33
|
externalIds{*}
|
|
31
34
|
participantStatus
|
|
@@ -125,6 +128,7 @@
|
|
|
125
128
|
summaryDescription
|
|
126
129
|
marketingTenure
|
|
127
130
|
media[
|
|
131
|
+
mediaId
|
|
128
132
|
mediaType
|
|
129
133
|
mediaUrl
|
|
130
134
|
caption
|
|
@@ -2712,6 +2716,7 @@
|
|
|
2712
2716
|
authorisedToActOnBehalfOfAllSellers
|
|
2713
2717
|
authorisationToShare
|
|
2714
2718
|
sellerSignatures[
|
|
2719
|
+
signatureId
|
|
2715
2720
|
name
|
|
2716
2721
|
signedOn
|
|
2717
2722
|
externalIds{*}
|
|
@@ -3670,6 +3675,7 @@
|
|
|
3670
3675
|
covenantAnalysis
|
|
3671
3676
|
]
|
|
3672
3677
|
searches[
|
|
3678
|
+
searchId
|
|
3673
3679
|
displayName
|
|
3674
3680
|
productCode
|
|
3675
3681
|
providerName
|
|
@@ -3681,6 +3687,7 @@
|
|
|
3681
3687
|
reportDate
|
|
3682
3688
|
]
|
|
3683
3689
|
documents[
|
|
3690
|
+
documentId
|
|
3684
3691
|
displayName
|
|
3685
3692
|
fileName
|
|
3686
3693
|
mimeType
|
|
@@ -3690,6 +3697,7 @@
|
|
|
3690
3697
|
summary
|
|
3691
3698
|
]
|
|
3692
3699
|
surveys[
|
|
3700
|
+
surveyId
|
|
3693
3701
|
outside
|
|
3694
3702
|
chimneyStacks
|
|
3695
3703
|
condition
|
|
@@ -4430,6 +4438,7 @@
|
|
|
4430
4438
|
credibilitySources[]
|
|
4431
4439
|
pricingMethodology
|
|
4432
4440
|
contracts[
|
|
4441
|
+
contractId
|
|
4433
4442
|
contract
|
|
4434
4443
|
template
|
|
4435
4444
|
name
|
|
@@ -4441,6 +4450,7 @@
|
|
|
4441
4450
|
]
|
|
4442
4451
|
terms
|
|
4443
4452
|
signatures[
|
|
4453
|
+
signatureId
|
|
4444
4454
|
name
|
|
4445
4455
|
signedOn
|
|
4446
4456
|
externalIds{*}
|