|
| 1 | +# Data Model — n8n-nodes-fedex |
| 2 | + |
| 3 | +> Audience: contributors. These are the typed shapes the pure cores assemble from node parameters |
| 4 | +> and the shapes they emit. See [Integration Specification](integration-spec.md) for how each shape |
| 5 | +> sits inside a request/response, and the [System Overview](system-overview.md) for the architecture. |
| 6 | +
|
| 7 | +The node has no database. "Data model" here means the in-memory shapes that flow from n8n node |
| 8 | +parameters → pure cores → FedEx request bodies, and from FedEx responses → pure cores → n8n output. |
| 9 | +The typed shapes live in |
| 10 | +[cores/fedexTypes.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/fedexTypes.ts); |
| 11 | +they are deliberately free of any n8n coupling so the cores stay unit-testable |
| 12 | +([ADR-0003](adr/0003-pure-assembly-cores-with-unit-test-runner.md)). |
| 13 | + |
| 14 | +## Shapes |
| 15 | + |
| 16 | +```mermaid |
| 17 | +classDiagram |
| 18 | + class AddressInput { |
| 19 | + +string streetLines |
| 20 | + +string city |
| 21 | + +string stateOrProvinceCode |
| 22 | + +string postalCode |
| 23 | + +string countryCode |
| 24 | + +boolean residential |
| 25 | + } |
| 26 | + class FedexAddress { |
| 27 | + +string[] streetLines |
| 28 | + +string city |
| 29 | + +string stateOrProvinceCode |
| 30 | + +string postalCode |
| 31 | + +string countryCode |
| 32 | + +boolean residential |
| 33 | + } |
| 34 | + class ContactInput { |
| 35 | + +string personName |
| 36 | + +string companyName |
| 37 | + +string phoneNumber |
| 38 | + +string emailAddress |
| 39 | + } |
| 40 | + class FedexContact { |
| 41 | + +string personName |
| 42 | + +string companyName |
| 43 | + +string phoneNumber |
| 44 | + +string emailAddress |
| 45 | + } |
| 46 | + class FedexWeight { |
| 47 | + +WeightUnit units |
| 48 | + +number value |
| 49 | + } |
| 50 | + class FedexDimensions { |
| 51 | + +number length |
| 52 | + +number width |
| 53 | + +number height |
| 54 | + +DimensionUnit units |
| 55 | + } |
| 56 | + class ShapedRate { |
| 57 | + +string serviceType |
| 58 | + +string serviceName |
| 59 | + +number negotiatedRate |
| 60 | + +number listRate |
| 61 | + +string currency |
| 62 | + } |
| 63 | + class ExtractedLabel { |
| 64 | + +Buffer buffer |
| 65 | + +string fileName |
| 66 | + +string mimeType |
| 67 | + +string trackingNumber |
| 68 | + +JsonObject json |
| 69 | + } |
| 70 | +
|
| 71 | + AddressInput ..> FedexAddress : toFedexAddress |
| 72 | + ContactInput ..> FedexContact : toFedexContact |
| 73 | +``` |
| 74 | + |
| 75 | +### Inputs versus FedEx shapes |
| 76 | + |
| 77 | +`AddressInput` / `ContactInput` are the loose, flat values read from node parameters; |
| 78 | +`FedexAddress` / `FedexContact` are the cleaned shapes FedEx expects. The cores own the rules: |
| 79 | + |
| 80 | +- **`toFedexAddress`** splits Street Lines on newlines (max 3), trims everything, defaults |
| 81 | + `countryCode` to `US`, includes `stateOrProvinceCode` only when present, and includes |
| 82 | + `residential` only when it was explicitly provided (it is meaningful for a recipient, omitted |
| 83 | + elsewhere). |
| 84 | +- **`toFedexContact`** always carries a trimmed `phoneNumber` and omits blank identity fields. |
| 85 | + |
| 86 | +### Output shapes |
| 87 | + |
| 88 | +- **`ShapedRate`** is one flattened row per service produced by `shapeRates` from |
| 89 | + `output.rateReplyDetails`. `negotiatedRate` is the account (`ACCOUNT`) price; `listRate` is the |
| 90 | + published (`LIST`) price; either can be `null`. |
| 91 | +- **`ExtractedLabel`** is produced by `extractLabel`: the decoded label `buffer`, a sanitized |
| 92 | + `fileName`, the `mimeType` from the chosen format, the `trackingNumber`, and `json` — the FedEx |
| 93 | + output with every base64 `encodedLabel` recursively stripped. |
| 94 | + |
| 95 | +## Node parameter → FedEx field mapping |
| 96 | + |
| 97 | +| Node parameter | FedEx field (via core) | Used by | |
| 98 | +| -------------- | ---------------------- | ------- | |
| 99 | +| `{role}StreetLines` | `address.streetLines[]` (split, max 3) | Validate, Get Rates, Create | |
| 100 | +| `{role}City` | `address.city` | Validate, Get Rates, Create | |
| 101 | +| `{role}StateOrProvinceCode` | `address.stateOrProvinceCode` (omitted if blank) | Validate, Get Rates, Create | |
| 102 | +| `{role}PostalCode` | `address.postalCode` | Validate, Get Rates, Create | |
| 103 | +| `{role}CountryCode` | `address.countryCode` (default `US`) | Validate, Get Rates, Create | |
| 104 | +| `recipientResidential` | `address.residential` (recipient only) | Get Rates, Create | |
| 105 | +| `{role}PersonName` | `contact.personName` | Create | |
| 106 | +| `{role}CompanyName` | `contact.companyName` | Create | |
| 107 | +| `{role}PhoneNumber` | `contact.phoneNumber` (required) | Create | |
| 108 | +| `{role}EmailAddress` | `contact.emailAddress` | Create | |
| 109 | +| `shippingAccountNumber` | `accountNumber.value` (required) | Get Rates, Create | |
| 110 | +| `packageWeight` / `weightUnit` | `requestedPackageLineItems[0].weight` | Get Rates, Create | |
| 111 | +| `packageLength/Width/Height` / `dimensionUnit` | `requestedPackageLineItems[0].dimensions` (sent only when all three greater than 0) | Get Rates, Create | |
| 112 | +| `serviceType` | `requestedShipment.serviceType` | Get Rates (optional), Create (required) | |
| 113 | +| `labelImageType` | `labelSpecification.imageType` + binary MIME | Create | |
| 114 | +| `labelStockType` | `labelSpecification.labelStockType` | Create | |
| 115 | + |
| 116 | +`{role}` is `shipper` or `recipient`; the same builders are reused across Get Rates and Create so |
| 117 | +values carry over when switching operation. |
| 118 | + |
| 119 | +## The one-Address, four-positions crux |
| 120 | + |
| 121 | +The single `FedexAddress` shape lands in four structurally different request positions. This is the |
| 122 | +core reason address assembly is a pure function rather than per-field declarative routing |
| 123 | +([ADR-0003](adr/0003-pure-assembly-cores-with-unit-test-runner.md)): |
| 124 | + |
| 125 | +```mermaid |
| 126 | +flowchart LR |
| 127 | + core["toFedexAddress(input)"] --> v["Validate<br/>addressesToValidate[0].address"] |
| 128 | + core --> rs["Get Rates<br/>requestedShipment.shipper.address"] |
| 129 | + core --> rr["Get Rates<br/>requestedShipment.recipient.address"] |
| 130 | + core --> cs["Create<br/>requestedShipment.shipper.address"] |
| 131 | + core --> cr["Create<br/>requestedShipment.recipients[0].address"] |
| 132 | +``` |
| 133 | + |
| 134 | +Two of those carry an array index, `recipient` is singular for Get Rates but a plural `recipients` |
| 135 | +array for Create, and `residential` is a user **input** for a recipient but the **output** of |
| 136 | +Validate. One pure core normalizes all of it. |
| 137 | + |
| 138 | +## Traceability to repo artifacts |
| 139 | + |
| 140 | +| Shape / logic | Source | |
| 141 | +| ------------- | ------ | |
| 142 | +| Typed FedEx shapes | [cores/fedexTypes.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/fedexTypes.ts) | |
| 143 | +| Address assembly | [cores/toFedexAddress.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/toFedexAddress.ts) | |
| 144 | +| Contact assembly | [cores/toFedexContact.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/toFedexContact.ts) | |
| 145 | +| Rate shaping | [cores/shapeRates.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/shapeRates.ts) | |
| 146 | +| Label extraction | [cores/extractLabel.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/cores/extractLabel.ts) | |
| 147 | +| Parameter readers | [resources/shared.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/resources/shared.ts) | |
| 148 | +| Field builders | [fields.ts](https://github.com/nodrel-dev/n8n-fedex-node/blob/main/nodes/Fedex/fields.ts) | |
0 commit comments