stash
This commit is contained in:
parent
d56784eb8d
commit
6ee5ae38b1
8 changed files with 246 additions and 153 deletions
|
|
@ -11,29 +11,29 @@ from toolshed.models import InventoryItem, WorkflowInstance
|
||||||
|
|
||||||
|
|
||||||
def _get_authorized_item(identity, item_id):
|
def _get_authorized_item(identity, item_id):
|
||||||
"""Look up an item by its owner-scoped id and confirm identity may act on it - either as its
|
"""Owner-or-group-scoped item lookup; returns None if identity may not act on it."""
|
||||||
personal owner (requires a local ToolshedUser account) or as a current member of its owning
|
|
||||||
group (works for a remote member too, since group membership is identity-level, see
|
|
||||||
docs/design-in-progress/groups-mvp.md). id is only unique within one owner/group's own items,
|
|
||||||
so the lookup itself must be scoped rather than a bare global get. Returns None if not found
|
|
||||||
or not authorized, the same shape InventoryItem.DoesNotExist handling around it already
|
|
||||||
expects."""
|
|
||||||
if identity.user.exists():
|
if identity.user.exists():
|
||||||
try:
|
try:
|
||||||
return InventoryItem.objects.get(owner=identity.user.get(), id=item_id)
|
return InventoryItem.objects.get(owner=identity.user.get(), id=item_id)
|
||||||
except InventoryItem.DoesNotExist:
|
except InventoryItem.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
try:
|
# Checked one group at a time, not owner_group__in=<all>, since id is only unique within one group's own items and a combined query could raise MultipleObjectsReturned on a collision.
|
||||||
return InventoryItem.objects.get(owner_group__in=identity.member_of_groups.all(), id=item_id)
|
for group in identity.member_of_groups.all():
|
||||||
except InventoryItem.DoesNotExist:
|
item = InventoryItem.objects.filter(owner_group=group, id=item_id).first()
|
||||||
return None
|
if item:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@authentication_classes([SignatureAuthenticationLocal])
|
@authentication_classes([SignatureAuthenticationLocal])
|
||||||
def list_all_files(request, format=None): # /files/
|
def list_all_files(request, format=None): # /files/
|
||||||
files = File.objects.select_related().filter(connected_items__owner=request.user).distinct()
|
# request.user is a ToolshedUser here; reach group membership via public_identity.
|
||||||
|
files = File.objects.select_related().filter(
|
||||||
|
Q(connected_items__owner=request.user) |
|
||||||
|
Q(connected_items__owner_group__in=request.user.public_identity.member_of_groups.all())
|
||||||
|
).distinct()
|
||||||
return Response(FileSerializer(files, many=True).data)
|
return Response(FileSerializer(files, many=True).data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ and talks to it directly, regardless of where the frontend itself was loaded fro
|
||||||
A name that's unique within its own scope and carries, as part of itself, enough information to say
|
A name that's unique within its own scope and carries, as part of itself, enough information to say
|
||||||
where it's authoritative, without needing a central registry to look it up. The general term
|
where it's authoritative, without needing a central registry to look it up. The general term
|
||||||
covering [user handles](#user-handle), [group handles](#group-handle) (proposed),
|
covering [user handles](#user-handle), [group handles](#group-handle) (proposed),
|
||||||
[classification handles](#classification-handle), and [item handles](#item-handle) (proposed).
|
[classification handles](#classification-handle), [User-Qualified IDs](#user-qualified-id), and
|
||||||
|
[Domain-Qualified Short IDs](#domain-qualified-short-id).
|
||||||
*See: [federation.md](federation.md#unique-handles)*
|
*See: [federation.md](federation.md#unique-handles)*
|
||||||
|
|
||||||
**Strict resolution** (Implemented)
|
**Strict resolution** (Implemented)
|
||||||
|
|
@ -109,7 +110,8 @@ The asymmetric keypair backing exactly one [user handle](#user-handle); together
|
||||||
keypair backing it are what's called an [identity](#identity). The private key signs requests made
|
keypair backing it are what's called an [identity](#identity). The private key signs requests made
|
||||||
as that user and never leaves their control; the public key is handed out to establish trust in the
|
as that user and never leaves their control; the public key is handed out to establish trust in the
|
||||||
handle (at registration, or via a [friend](#friend-friendship) exchange) and is used to verify
|
handle (at registration, or via a [friend](#friend-friendship) exchange) and is used to verify
|
||||||
signatures. Only user handles carry a keypair, not groups, classification handles, or item handles.
|
signatures. Only user handles carry a keypair, not groups, classification handles, or
|
||||||
|
User-Qualified IDs.
|
||||||
*See: [federation.md](federation.md#cryptography)*
|
*See: [federation.md](federation.md#cryptography)*
|
||||||
|
|
||||||
**Membership list** (Proposed)
|
**Membership list** (Proposed)
|
||||||
|
|
@ -157,8 +159,8 @@ a property alias specifically, also states a unit conversion (even if it's "iden
|
||||||
**Classification handle** (Implemented)
|
**Classification handle** (Implemented)
|
||||||
Umbrella term for a [tag](#tag-property-category), [property](#tag-property-category), or
|
Umbrella term for a [tag](#tag-property-category), [property](#tag-property-category), or
|
||||||
[category](#tag-property-category) handle, of the form `origin#type:name`, e.g.
|
[category](#tag-property-category) handle, of the form `origin#type:name`, e.g.
|
||||||
`git:base#property:length`. Distinct from a [user handle](#user-handle) or [item
|
`git:base#property:length`. Distinct from a [user handle](#user-handle) or [User-Qualified
|
||||||
handle](#item-handle): it names a reusable classification concept, not an actor or an owned thing.
|
ID](#user-qualified-id): it names a reusable classification concept, not an actor or an owned thing.
|
||||||
*See: [federation.md](federation.md#tags-properties-and-categories)*
|
*See: [federation.md](federation.md#tags-properties-and-categories)*
|
||||||
|
|
||||||
**Definition fingerprint** (Partially implemented)
|
**Definition fingerprint** (Partially implemented)
|
||||||
|
|
@ -201,29 +203,47 @@ strings today.
|
||||||
|
|
||||||
## Items & Physical Labels
|
## Items & Physical Labels
|
||||||
|
|
||||||
**Item Handle** (Proposed)
|
**Domain-Qualified Short ID** (Implemented)
|
||||||
The compact identifier for a specific item that's meaningful outside its owner's own account:
|
A [domain](#domain) paired with a [short id](handles-and-shortids.md#short-ids) token, e.g.
|
||||||
`user@domain.tld:id`, the owner's [user handle](#user-handle) plus a [local id](#local-id). Used
|
`toolsheddomain.tld:~DyU`. A bare short id token is opaque and scoped to whichever backend minted
|
||||||
where it's already clear from context that it's a Toolshed item, e.g. inside the app, in exports,
|
it, nothing in the token itself says which backend's id numbering to read it against, so it only
|
||||||
in logs, so it doesn't need to spell that out or be openable on its own. Contrast with [Item
|
means something inside the app instance that's currently talking to that backend. Prefixing it with
|
||||||
URL](#item-url), the self-contained form for when no such context can be assumed.
|
its domain supplies the missing piece, the same way any other handle's domain half does, so the
|
||||||
*See: [items-labels.md](design-in-progress/items-labels.md#open-design-questions)*
|
pair keeps resolving correctly even after being copied out of the instance it was minted on. Unlike
|
||||||
|
a [User-Qualified ID](#user-qualified-id) or [Item URL](#item-url), it isn't limited to owned kinds
|
||||||
|
(any kind in the short id registry can be domain-qualified) and isn't meant to be openable with zero
|
||||||
|
context, no scheme, no path, opaque bit-packed payload, it belongs inside the app or between things
|
||||||
|
that already speak its short-id format, not on a physical label.
|
||||||
|
*See: [handles-and-shortids.md](handles-and-shortids.md#domain-qualified-short-id)*
|
||||||
|
|
||||||
**Item Label** (Proposed)
|
**Item Label** (Implemented)
|
||||||
A physical, scannable encoding (QR code, barcode, or similar) of an item's [Item URL](#item-url),
|
A physical, scannable encoding (QR code, barcode, or similar) of an item's [Item URL](#item-url),
|
||||||
meant to be printed and stuck on the physical object it refers to.
|
meant to be printed and stuck on the physical object it refers to.
|
||||||
*See: [items-labels.md](design-in-progress/items-labels.md#goals)*
|
*See: [items-labels.md](design-in-progress/items-labels.md#goals)*
|
||||||
|
|
||||||
**Item URL** (Proposed)
|
**Item URL** (Implemented)
|
||||||
The self-contained URL form of an [Item Handle](#item-handle), for use with no context at all, e.g.
|
The self-contained URL form of an item's [User-Qualified ID](#user-qualified-id), for use with no
|
||||||
an [Item Label](#item-label): `https://<any frontend>/i/user@domain.tld/id`. Has to open directly
|
context at all, e.g. an [Item Label](#item-label): `https://<any frontend>/i/user@domain.tld/id`.
|
||||||
to the right frontend and land on the right item on its own, since the reader can't be assumed to
|
Has to open directly to the right frontend and land on the right item on its own, since the reader
|
||||||
already know what it is or which server it belongs to. Any frontend can serve this URL, the host
|
can't be assumed to already know what it is or which server it belongs to. Any frontend can serve
|
||||||
named in it isn't part of the item's identity, only the handle in its path is.
|
this URL, the host named in it isn't part of the item's identity, only the handle in its path is.
|
||||||
*See: [items-labels.md](design-in-progress/items-labels.md#open-design-questions)*
|
*See: [items-labels.md](design-in-progress/items-labels.md#open-design-questions)*
|
||||||
|
|
||||||
**Local id** (Implemented)
|
**Local id** (Implemented)
|
||||||
An item's identifier as it exists today: unique only within its owner's own inventory, not
|
An item's identifier as it exists today: unique only within its owner's own inventory, not
|
||||||
meaningful outside that owner's account. The starting point both the [Item
|
meaningful outside that owner's account. The starting point both a [User-Qualified
|
||||||
Handle](#item-handle) and [Item URL](#item-url) build on.
|
ID](#user-qualified-id) and [Item URL](#item-url) build on.
|
||||||
*See: [federation.md](federation.md#items), [items-labels.md](design-in-progress/items-labels.md#problem)*
|
*See: [federation.md](federation.md#items), [items-labels.md](design-in-progress/items-labels.md#problem)*
|
||||||
|
|
||||||
|
**User-Qualified ID** (Implemented)
|
||||||
|
The compact identifier for a specific owned thing (an item, a storage location, ...) that's
|
||||||
|
meaningful outside its owner's own account: `<owner-handle>:<kind><local-id>`, e.g.
|
||||||
|
`alice@example.com:i42`, the owner's [user handle](#user-handle) (or [group
|
||||||
|
handle](#group-handle) for a group-owned thing) plus a one-letter kind tag (`i` for item, `s` for
|
||||||
|
storage location) and a [local id](#local-id). Used where it's already clear from context that it's
|
||||||
|
Toolshed data, e.g. inside the app, in exports, in logs, so it doesn't need to spell that out or be
|
||||||
|
openable on its own. Generalizes what used to be a bespoke, item-only "Item Handle" concept: the
|
||||||
|
kind letter is what an item-specific `user@domain.tld:id` shape was missing, nothing in that string
|
||||||
|
said it was specifically an item. Contrast with [Item URL](#item-url), the self-contained form for
|
||||||
|
when no such context can be assumed.
|
||||||
|
*See: [handles-and-shortids.md](handles-and-shortids.md#user-qualified-id)*
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ This is the syntax-level reference for two related but separate naming schemes.
|
||||||
"Unique Handles" section covers *why* Toolshed hands out handles at all and what each kind (user,
|
"Unique Handles" section covers *why* Toolshed hands out handles at all and what each kind (user,
|
||||||
group, tag/property/category) means conceptually; this document covers the parsing rules those
|
group, tag/property/category) means conceptually; this document covers the parsing rules those
|
||||||
handles have to follow once they're written down or embedded somewhere - legal characters and
|
handles have to follow once they're written down or embedded somewhere - legal characters and
|
||||||
escaping. It also covers short ids end to end: a separate, newer scheme for packing small integer
|
escaping. It also covers short ids end to end: a separate scheme for packing small integer id
|
||||||
id chains into a compact token, implemented in `frontend/src/short-id.js`.
|
chains into a compact token.
|
||||||
|
|
||||||
## Handle syntax
|
## Handle syntax
|
||||||
|
|
||||||
|
|
@ -15,14 +15,14 @@ A username ends up embedded, unescaped, in several composite formats beyond its
|
||||||
can't contain any character that already means something else in one of those: `@` (the
|
can't contain any character that already means something else in one of those: `@` (the
|
||||||
user/domain separator in a user handle), `#` (the group-handle prefix, and the origin/type
|
user/domain separator in a user handle), `#` (the group-handle prefix, and the origin/type
|
||||||
separator in a classification handle, see federation.md's Tags, Properties, and Categories
|
separator in a classification handle, see federation.md's Tags, Properties, and Categories
|
||||||
section), `:` (the id delimiter in a proposed Item Handle, the type/name delimiter in a
|
section), `:` (the kind/id delimiter in a User-Qualified ID (see below), the type/name delimiter in
|
||||||
classification handle, and the delimiter in a signed request's `Authorization` header), `+`
|
a classification handle, the domain/token delimiter in a Domain-Qualified Short ID (see below), and
|
||||||
(reserved as the URL-embedding escape for `#`, see below), `~` (the short-id token prefix, see
|
the delimiter in a signed request's `Authorization` header), `+` (reserved as the URL-embedding
|
||||||
Short IDs below, and a proposed collision-disambiguation suffix delimiter on a tag's origin), and
|
escape for `#`, see below), `~` (the short-id token prefix, see Short IDs below, and a
|
||||||
`/` (the path-segment delimiter every handle and id ultimately sits next to once embedded in a
|
collision-disambiguation suffix delimiter on a tag's origin), and `/` (the path-segment delimiter
|
||||||
URL). This has to be enforced by an explicit validator rather than left to Django's default
|
every handle and id ultimately sits next to once embedded in a URL). This has to be enforced by an
|
||||||
`UnicodeUsernameValidator` (`^[\w.@+-]+\Z`), which currently permits both `@` and `+` (its own
|
explicit validator rather than left to a framework default, which doesn't draw the line in the same
|
||||||
regex doesn't happen to allow `#`, `:`, or `/`, but that's incidental, not a designed restriction).
|
place.
|
||||||
|
|
||||||
### Embedding a `#`-bearing handle in a URL
|
### Embedding a `#`-bearing handle in a URL
|
||||||
|
|
||||||
|
|
@ -44,17 +44,45 @@ otherwise forbidden in every field a handle is built from (see Reserved characte
|
||||||
group name or tag name could itself contain a literal `+`, it would be indistinguishable from an
|
group name or tag name could itself contain a literal `+`, it would be indistinguishable from an
|
||||||
escaped `#` once decoded.
|
escaped `#` once decoded.
|
||||||
|
|
||||||
Implemented in `frontend/src/handle-url.js` (`encodeHandleForUrl`/`decodeHandleFromUrl`).
|
### User-Qualified ID
|
||||||
|
|
||||||
|
Owned things (an item, a storage location, ...) can be referred to outside their owner's own
|
||||||
|
account: anything whose id is only unique within one owner's own numbering needs an owner-qualified
|
||||||
|
form to resolve globally.
|
||||||
|
|
||||||
|
A **User-Qualified ID** is an owner handle (a user handle, or a group handle for a group-owned
|
||||||
|
thing) with a kind letter and a local id appended, separated by a single `:`:
|
||||||
|
`<owner-handle>:<kind><local-id>`, e.g. `alice@example.com:i42`. This is the same `origin#type:name`
|
||||||
|
pattern classification handles already use, built on `:` instead of `#` so it avoids the URL
|
||||||
|
fragment-escaping problem (see Embedding a `#`-bearing handle in a URL above) — a User-Qualified ID
|
||||||
|
isn't meant to sit directly in a URL path segment.
|
||||||
|
|
||||||
|
Its payload is a plain decimal local id with nothing self-describing baked in, unlike a Short ID's
|
||||||
|
bit-packed payload, so the owner is spelled out as a full handle rather than just a domain:
|
||||||
|
`example.com:i42` would be ambiguous between every user on that domain with local item id `42`;
|
||||||
|
`alice@example.com:i42` isn't. An owner handle already carries its domain, so a User-Qualified ID is
|
||||||
|
cross-domain-resolvable as written, with no separate domain-qualified wrapper needed.
|
||||||
|
|
||||||
|
The kind letter is a small, closed registry. A group handle already looks visibly different from a
|
||||||
|
user handle (`#` prefix), so unlike the short id kind registry below, this one doesn't need separate
|
||||||
|
letters for a user-owned vs. group-owned kind — the owner half already says which it is.
|
||||||
|
|
||||||
|
| letter | kind | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `i` | item | covers both a user-owned item and a group-owned item |
|
||||||
|
| `s` | storage location | covers both a user-owned and a group-owned storage location |
|
||||||
|
|
||||||
|
`workflow` has no letter assigned; the registry can grow without breaking anything already printed.
|
||||||
|
Kinds with no owner (`category`, `group`, `file`) don't fit this scheme: `category` and `group` each
|
||||||
|
already have their own dedicated handle form. `file` has neither an owner to qualify by nor a
|
||||||
|
dedicated handle of its own; a Domain-Qualified Short ID (below) is the fallback for it.
|
||||||
|
|
||||||
## Short IDs
|
## Short IDs
|
||||||
|
|
||||||
A general encoding for turning a small, fixed-shape list of integers into a compact, URL-safe
|
A general encoding for turning a small, fixed-shape list of integers into a compact, URL-safe
|
||||||
token, with no server-side lookup table involved: the code *is* the data, nothing is stored
|
token, with no server-side lookup table involved: the code *is* the data, nothing is stored
|
||||||
server-side to make it resolvable. Originally proposed to answer items-labels.md's open "what does
|
server-side to make it resolvable. Not item-specific; anything currently addressed by a short chain
|
||||||
the handle/URL actually look like" question, but the encoding itself isn't item-specific; anything
|
of small integers is a candidate.
|
||||||
currently addressed by a short chain of small integers is a candidate. Implemented and tested in
|
|
||||||
`frontend/src/short-id.js`; try it live at `/~<token>` (`frontend/src/views/ShortId.vue`), which
|
|
||||||
decodes whatever token is in the URL and also lists worked examples for every registered kind.
|
|
||||||
|
|
||||||
### Shape: a kind tag, then a fixed list of integers
|
### Shape: a kind tag, then a fixed list of integers
|
||||||
|
|
||||||
|
|
@ -67,29 +95,21 @@ range" - so kind 3 is encoded as escape + chunked-int `0`, kind 4 as escape + `1
|
||||||
costs nothing for a kind that already fits in the direct range, and keeps the tag itself extensible
|
costs nothing for a kind that already fits in the direct range, and keeps the tag itself extensible
|
||||||
forever without ever having to widen it out from under codes that were already printed. A narrow
|
forever without ever having to widen it out from under codes that were already printed. A narrow
|
||||||
tag only pays off if kind usage is actually skewed the way id values are (a few kinds dominate),
|
tag only pays off if kind usage is actually skewed the way id values are (a few kinds dominate),
|
||||||
which is why the registry below is ordered by expected frequency, cheapest (most-used) kind first:
|
which is why the registry below is ordered by expected frequency, cheapest (most-used) kind first.
|
||||||
|
Kind ids are scoped **per arity**, not globally: the same id can (and does) name a different kind
|
||||||
|
depending on how many fields follow it, since the arity is always known before the kind id needs
|
||||||
|
disambiguating:
|
||||||
|
|
||||||
| kind | name | fields | notes |
|
| kind id | arity | name | fields | notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 0 | `item` | `owner_identity_id`, `item_local_id` | dominant case - the primary physical-label use case |
|
| 0 | 2 | `item` | `owner_identity_id`, `item_local_id` | dominant case - a personally-owned item, the primary physical-label use case |
|
||||||
| 1 | `storage_location` | `owner_identity_id`, `storage_location_id` | also label-printed |
|
| 0 | 1 | `category` | `category_id` | label-adjacent (tagging); global, no owner - shares id 0 with `item` since the two never need the same arity |
|
||||||
| 2 | `category` | `category_id` | label-adjacent (tagging); global, no owner |
|
| 1 | 2 | `group_item` | `owner_group_id`, `item_local_id` | same use case as `item`, but for a group-owned item |
|
||||||
| 3 | `workflow` | `owner_identity_id`, `workflow_id` | shared in-app, not printed - first to pay the escape's cost |
|
| 1 | 1 | `group` | `group_id` | shared even less often; global, no owner |
|
||||||
| 4 | `group` | `group_id` | shared even less often; global, no owner |
|
| 2 | 2 | `storage_location` | `owner_identity_id`, `storage_location_id` | also label-printed |
|
||||||
| 5 | `file` | `file_id` | least often shared standalone; global, deduplicated by content hash |
|
| 2 | 1 | `file` | `file_id` | least often shared standalone; global, deduplicated by content hash |
|
||||||
|
| 3 | 2 | `workflow` | `owner_identity_id`, `workflow_id` | shared in-app, not printed - needs the escape range, since every other slot in the direct range (0-2) is already double-booked across the two arities in use |
|
||||||
`owner_identity_id` is `KnownIdentity.pk` (`backend/authentication/models.py`), not
|
| 4 | 2 | `group_storage_location` | `owner_group_id`, `storage_location_id` | same use case as `storage_location`, but for a group-owned location (see `group_item` above for the same owner/owner_group split); the direct range is fully double-booked, so this is the second kind that needs the escape range |
|
||||||
`ToolshedUser.pk`. Every local account already has exactly one stable `KnownIdentity` row
|
|
||||||
(`ToolshedUser.public_identity`, created once at registration and never recreated), and every
|
|
||||||
friend this backend knows about - local or remote - is represented by that same table, unique on
|
|
||||||
`(username, domain)`. So one small integer already stands in for "this owner, as known by this
|
|
||||||
backend" for both cases, with no separate local-vs-remote branching needed, and it's the same row
|
|
||||||
federation.md's Cryptography section already treats as the trust anchor for a handle's public key.
|
|
||||||
It appears on `item`, `storage_location`, and `workflow` because their backing models
|
|
||||||
(`InventoryItem`, `StorageLocation`, `WorkflowInstance`) all FK `ToolshedUser` directly; `category`,
|
|
||||||
`group`, and `file` skip it because their models are global/unscoped (`Group` has an unowned
|
|
||||||
`members` M2M, `File` is deduplicated globally by content hash), so a bare row id is already
|
|
||||||
everything needed to look them up.
|
|
||||||
|
|
||||||
A short id is inherently scoped to the backend that minted it (an "owner" field is a row that only
|
A short id is inherently scoped to the backend that minted it (an "owner" field is a row that only
|
||||||
exists in, and only means anything to, that one backend's database), not a portable replacement for
|
exists in, and only means anything to, that one backend's database), not a portable replacement for
|
||||||
|
|
@ -119,13 +139,12 @@ Standard base64 assumes byte-aligned (8-bit) input, grouping 3 bytes into 4 outp
|
||||||
padding to a byte boundary before encoding. Since there's no byte layer here to begin with, the
|
padding to a byte boundary before encoding. Since there's no byte layer here to begin with, the
|
||||||
bit-packed stream is instead packed directly into 6-bit groups and mapped straight onto the
|
bit-packed stream is instead packed directly into 6-bit groups and mapped straight onto the
|
||||||
URL-safe base64 alphabet (RFC 4648 §5: `-` and `_` in place of `+` and `/`), with the final
|
URL-safe base64 alphabet (RFC 4648 §5: `-` and `_` in place of `+` and `/`), with the final
|
||||||
character's unused low bits padded with zeros. That padding is safe by construction: the decoder
|
character's unused low bits padded with zeros. That padding is safe by construction: it's always
|
||||||
always knows exactly how many integers a given kind calls for, and a chunk's continuation bit is
|
0-5 zero bits (just enough to reach the next multiple of 6), and the decoder stops reading fields
|
||||||
`1 = more follows`, so a run of zero-padding at the very end can never be misread as "one more
|
the moment a chunk decodes to 0 with nothing left after it - by the last-field-nonzero rule above,
|
||||||
chunk" - it decodes as a terminated chunk, at which point every field the schema called for has
|
that can only be the padding, never a genuine field, so it's discarded rather than counted. No `=`
|
||||||
already been produced and decoding simply stops. No `=` padding characters are needed either; those
|
padding characters are needed either; those exist in classic base64 purely to communicate
|
||||||
exist in classic base64 purely to communicate trailing-byte padding, and there is no byte layer
|
trailing-byte padding, and there is no byte layer here to need that.
|
||||||
here to need that.
|
|
||||||
|
|
||||||
### The leading `~`
|
### The leading `~`
|
||||||
|
|
||||||
|
|
@ -140,6 +159,35 @@ short id can be handed to any part of the stack without first checking which enc
|
||||||
there.
|
there.
|
||||||
|
|
||||||
|
|
||||||
|
### Domain-Qualified Short ID
|
||||||
|
|
||||||
|
A short id token is deliberately opaque and scoped to whichever backend minted it: handed a bare
|
||||||
|
`~DyU`, nothing in the token itself says which backend's numbering it should be read against.
|
||||||
|
That's fine as long as the token stays inside a context that already knows the answer (the app the
|
||||||
|
user is currently looking at), but not once a token needs to travel outside that context, e.g.
|
||||||
|
pasted into a message to a friend on a different domain, or logged somewhere not tied to one
|
||||||
|
backend.
|
||||||
|
|
||||||
|
The fix is the same one every other cross-domain handle in this project already uses: pair the
|
||||||
|
opaque part with the domain that's authoritative for it. A **Domain-Qualified Short ID** is a short
|
||||||
|
id token prefixed with a domain and a literal `:`, e.g. `toolsheddomain.tld:~DyU`. The domain half
|
||||||
|
is exactly a handle's domain half (see federation.md's Unique Handles section): not a location,
|
||||||
|
just a statement of which backend to resolve the token against. Decoding still means handing the
|
||||||
|
`~token` half to that backend's decoder, same as ever; it's just no longer ambiguous which
|
||||||
|
backend's decoder to hand it to.
|
||||||
|
|
||||||
|
This is deliberately not the same thing as a User-Qualified ID (`owner-handle:i42`) or an Item URL.
|
||||||
|
It isn't a URL and isn't meant to be openable by something that doesn't already know what a
|
||||||
|
Toolshed short id is: there's no scheme, no path, and the payload after `:~` is bit-packed base64,
|
||||||
|
opaque to a human. It belongs to the same "context already makes clear it's Toolshed data" class as
|
||||||
|
a compact User-Qualified ID, meant for use inside the app (or between things that already speak the
|
||||||
|
short-id format), not for a physical label or a link shared outside it. What it adds over a bare
|
||||||
|
`~token` is that it no longer depends on "whichever backend I currently happen to be talking to" —
|
||||||
|
the domain travels with it, so it keeps resolving to the same entity once copied elsewhere. Unlike
|
||||||
|
a User-Qualified ID, it isn't limited to owned kinds: every kind in the short id registry can be
|
||||||
|
domain-qualified the same way, since the domain only ever names which backend's token namespace
|
||||||
|
applies, not which kind the token decodes to or whether that kind has an owner.
|
||||||
|
|
||||||
### Worked examples
|
### Worked examples
|
||||||
|
|
||||||
Encoding `kind = item` (0), `owner_identity_id = 7`, `item_local_id = 42`:
|
Encoding `kind = item` (0), `owner_identity_id = 7`, `item_local_id = 42`:
|
||||||
|
|
@ -152,17 +200,19 @@ Total: 17 meaningful bits, padded to the next multiple of 6 (18) with one zero b
|
||||||
base64 characters: **`~DyU`**.
|
base64 characters: **`~DyU`**.
|
||||||
|
|
||||||
The same worked-out form for one example of every registered kind - `Bits` is the same
|
The same worked-out form for one example of every registered kind - `Bits` is the same
|
||||||
space-separated segmentation (kind tag, escape offset if present, each field, then padding) the
|
space-separated segmentation: kind tag, escape offset if present, each field, then padding. Kinds
|
||||||
Examples table on `/~<token>` (`frontend/src/views/ShortId.vue`) shows for every registered kind:
|
are grouped by shared id below to make the per-arity reuse visible:
|
||||||
|
|
||||||
| Kind | Fields | Serialized | Bits | Token |
|
| Kind | Fields | Serialized | Bits | Token |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `item` | `owner_identity_id: 7`, `item_local_id: 42` | `[0, 7, 42]` | `00 00111 1001001010 0` | `~DyU` |
|
| `item` | `owner_identity_id: 7`, `item_local_id: 42` | `[0, 7, 42]` | `00 00111 1001001010 0` | `~DyU` |
|
||||||
| `storage_location` | `owner_identity_id: 3`, `storage_location_id: 1000` | `[1, 3, 1000]` | `01 00011 100111111001000 00` | `~Rz8g` |
|
| `category` | `category_id: 5` | `[0, 5]` | `00 00101 00000` | `~Cg` |
|
||||||
| `category` | `category_id: 5` | `[2, 5]` | `10 00101 00000` | `~ig` |
|
| `group_item` | `owner_group_id: 5`, `item_local_id: 42` | `[1, 5, 42]` | `01 00101 1001001010 0` | `~SyU` |
|
||||||
| `workflow` | `owner_identity_id: 2`, `workflow_id: 9` | `[3, 2, 9]` | `11 00000 00010 01001 0` | `~wCS` |
|
| `group` | `group_id: 11` | `[1, 11]` | `01 01011 00000` | `~Vg` |
|
||||||
| `group` | `group_id: 11` | `[4, 11]` | `11 00001 01011` | `~wr` |
|
| `storage_location` | `owner_identity_id: 3`, `storage_location_id: 1000` | `[2, 3, 1000]` | `10 00011 100111111001000 00` | `~hz8g` |
|
||||||
| `file` | `file_id: 123` | `[5, 123]` | `11 00010 1011101011 0` | `~xXW` |
|
| `file` | `file_id: 123` | `[2, 123]` | `10 1011101011` | `~rr` |
|
||||||
|
| `workflow` | `owner_identity_id: 2`, `workflow_id: 9` | `[3, 2, 9]` | `11 00000 00010 01001 0` | `~wiS` |
|
||||||
|
| `group_storage_location` | `owner_group_id: 5`, `storage_location_id: 1000` | `[4, 5, 1000]` | `11 00001 00101 100111111001000 000` | `~wln5A` |
|
||||||
|
|
||||||
`workflow`, `group`, and `file` are kinds 3-5, so their `Bits` column shows the escape tag (`11`)
|
`workflow`, `group`, and `file` are kinds 3-5, so their `Bits` column shows the escape tag (`11`)
|
||||||
followed by its own offset segment payload fields.
|
followed by its own offset segment payload fields.
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Pixel fonts used for small label text (see ../../../scss/_pixel-fonts.scss)
|
|
||||||
|
|
||||||
- **Tom Thumb** (`TomThumb.ttf`) - by Brian Swetland, TTF conversion by gheja
|
|
||||||
(https://github.com/gheja/tom-thumb-ttf). Licensed CC0 or CC-BY 3.0 (original:
|
|
||||||
https://robey.lag.net/2010/01/23/tiny-monospace-font.html).
|
|
||||||
- **PICO-8** (`PICO-8.ttf`) - reproduction by Jacob Pierce
|
|
||||||
(https://github.com/jacobpierce/pico-8-font). MIT License, Copyright (c) 2016 Jacob Pierce.
|
|
||||||
- **Silkscreen** (`Silkscreen-Regular.woff2`) - by Jason Kottke, served via Google Fonts
|
|
||||||
(https://fonts.google.com/specimen/Silkscreen). SIL Open Font License 1.1.
|
|
||||||
|
|
@ -149,10 +149,16 @@ export const LABEL_TEMPLATES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "item-handle", name: "Item handle",
|
id: "item-handle", name: "Item handle",
|
||||||
description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
|
description: "The compact owner@domain:i<id> User-Qualified ID - meaningful in-app, not scannable on its own.",
|
||||||
required_vars: ["itemHandle"], tags: ["internal"],
|
required_vars: ["itemHandle"], tags: ["internal"],
|
||||||
layout: [{type: "text", content: c => c.itemHandle}]
|
layout: [{type: "text", content: c => c.itemHandle}]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "location-handle", name: "Storage location handle",
|
||||||
|
description: "The compact owner@domain:s<id> User-Qualified ID - meaningful in-app, not scannable on its own.",
|
||||||
|
required_vars: ["locationHandle"], tags: ["internal"],
|
||||||
|
layout: [{type: "text", content: c => c.locationHandle}]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "item-url", name: "Item URL",
|
id: "item-url", name: "Item URL",
|
||||||
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
||||||
|
|
@ -252,11 +258,16 @@ export const DERIVED_VARS = {
|
||||||
inputs: ["webdomain", "userHandle", "itemId"],
|
inputs: ["webdomain", "userHandle", "itemId"],
|
||||||
calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`,
|
calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`,
|
||||||
},
|
},
|
||||||
// Compact "owner handle + id" form (see docs/design-in-progress/items-labels.md) - meaningful
|
// Compact User-Qualified ID (see docs/handles-and-shortids.md#user-qualified-id) - owner
|
||||||
// only where context already makes clear it's a Toolshed item, unlike itemUrl.
|
// handle + a one-letter kind tag + local id, meaningful only where context already makes
|
||||||
|
// clear it's Toolshed data, unlike itemUrl. `i` = item, `s` = storage location.
|
||||||
itemHandle: {
|
itemHandle: {
|
||||||
inputs: ["userHandle", "itemId"],
|
inputs: ["userHandle", "itemId"],
|
||||||
calc: (f) => `${f.userHandle}:${f.itemId}`,
|
calc: (f) => `${f.userHandle}:i${f.itemId}`,
|
||||||
|
},
|
||||||
|
locationHandle: {
|
||||||
|
inputs: ["userHandle", "locationId"],
|
||||||
|
calc: (f) => `${f.userHandle}:s${f.locationId}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import {markRaw, nextTick} from "vue";
|
import {markRaw, nextTick} from "vue";
|
||||||
import {mapActions, mapGetters} from "vuex";
|
import {mapActions, mapGetters, mapState} from "vuex";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
||||||
import printerManager from "@/printerManager.js";
|
import printerManager from "@/printerManager.js";
|
||||||
|
|
@ -337,7 +337,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(["user"]),
|
||||||
...mapGetters(["identityIdByHandle", "groupIdByHandle"]),
|
...mapGetters(["identityIdByHandle", "groupIdByHandle"]),
|
||||||
|
homeDomain() {
|
||||||
|
const at = this.user ? this.user.indexOf("@") : -1;
|
||||||
|
return at === -1 ? null : this.user.slice(at + 1);
|
||||||
|
},
|
||||||
baseVars() {
|
baseVars() {
|
||||||
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR);
|
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR);
|
||||||
},
|
},
|
||||||
|
|
@ -357,7 +362,8 @@ export default {
|
||||||
if (shortId) {
|
if (shortId) {
|
||||||
derived[SHORT_ID_VAR] = shortId;
|
derived[SHORT_ID_VAR] = shortId;
|
||||||
if (derived.webdomain) {
|
if (derived.webdomain) {
|
||||||
derived[SHORT_URL_VAR] = derived.webdomain + "/" + shortId;
|
derived[SHORT_URL_VAR] = derived.webdomain + "/"
|
||||||
|
+ (this.homeDomain ? this.homeDomain + ":" + shortId : shortId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return derived;
|
return derived;
|
||||||
|
|
@ -502,9 +508,6 @@ export default {
|
||||||
return ticks;
|
return ticks;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reads the recently-printed template ids back from localStorage; same try/catch shape as
|
|
||||||
// cameraManager.js's getRecentCameras since either a disabled/full localStorage shouldn't
|
|
||||||
// break printing.
|
|
||||||
loadRecentTemplateIds() {
|
loadRecentTemplateIds() {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem(RECENT_TEMPLATES_KEY);
|
const saved = localStorage.getItem(RECENT_TEMPLATES_KEY);
|
||||||
|
|
@ -514,10 +517,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Moves `id` to the front of the recent-templates list (deduping any earlier occurrence),
|
rememberPrintedTemplate(id) {
|
||||||
// capped to MAX_RECENT_TEMPLATES, so LabelLayoutPreview.vue's grid always bubbles the
|
|
||||||
// last four printed/downloaded layouts to the top.
|
|
||||||
rememberPrintedTemplate(id) {
|
|
||||||
const updated = [id, ...this.recentTemplateIds.filter(t => t !== id)].slice(0, MAX_RECENT_TEMPLATES);
|
const updated = [id, ...this.recentTemplateIds.filter(t => t !== id)].slice(0, MAX_RECENT_TEMPLATES);
|
||||||
this.recentTemplateIds = updated;
|
this.recentTemplateIds = updated;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -70,17 +70,10 @@
|
||||||
import {mapActions} from "vuex";
|
import {mapActions} from "vuex";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
import CameraScanner from "@/components/CameraScanner.vue";
|
import CameraScanner from "@/components/CameraScanner.vue";
|
||||||
import {encodeHandleForUrl, expandedRoute} from "@/router";
|
import {encodeHandleForUrl, expandedRoute, OWNED_KIND_FIELDS} from "@/router";
|
||||||
import {decodeShortId, deserializeShortId} from "@/short-id";
|
import {decodeShortId, deserializeShortId, isDomainQualifiedShortId, decodeDomainQualifiedShortId} from "@/short-id";
|
||||||
|
|
||||||
// A scanned label can encode any of these (see label-layouts.js's DERIVED_VARS): a full
|
const OWNER_QUALIFIED_ID_RE = /^(#?[^\s@:/#+~]+@[^\s@:/#+~]+):([is])(\d+)$/;
|
||||||
// self-contained URL (itemUrl/shortUrl), a bare short-id.js token with no domain at all (the
|
|
||||||
// "mqr"/id-only layout), or the compact no-URL "<userHandle>:<itemId>" form (itemHandle). Detects
|
|
||||||
// which and returns a link target, or null if the text doesn't match any known format. `decoded`/
|
|
||||||
// `itemHandle` carry enough of the parsed token for describeLink (below) to also resolve a
|
|
||||||
// human-readable "what this points at" - not needed for the two URL cases, which link to
|
|
||||||
// somewhere already showing that.
|
|
||||||
const ITEM_HANDLE_RE = /^(#?[^\s@:/#+~]+@[^\s@:/#+~]+):(\d+)$/;
|
|
||||||
|
|
||||||
function classifyScanText(text) {
|
function classifyScanText(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -88,32 +81,29 @@ function classifyScanText(text) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const url = new URL(text);
|
const url = new URL(text);
|
||||||
// Same-origin URLs (the common case - a label printed by this same app) get routed
|
|
||||||
// in-app instead of forcing a full page reload through an <a> tag. `immediate`: a URL
|
|
||||||
// needs no async lookup to confirm it's real (unlike the token/handle formats below), so
|
|
||||||
// it's already "resolved" the moment it's classified - see resolveDescription and
|
|
||||||
// maybeVisitFirstMatch.
|
|
||||||
return url.origin === window.location.origin
|
return url.origin === window.location.origin
|
||||||
? {to: url.pathname + url.search + url.hash, immediate: true}
|
? {to: url.pathname + url.search + url.hash, immediate: true}
|
||||||
: {href: url.href};
|
: {href: url.href};
|
||||||
} catch {
|
} catch {
|
||||||
// Not an absolute URL - fall through to the other known formats below.
|
// Not an absolute URL - fall through to the other known formats below.
|
||||||
}
|
}
|
||||||
if (text.startsWith('~')) {
|
if (text.startsWith('~') || isDomainQualifiedShortId(text)) {
|
||||||
try {
|
try {
|
||||||
const decoded = deserializeShortId(decodeShortId(text));
|
const qualified = isDomainQualifiedShortId(text) ? decodeDomainQualifiedShortId(text) : null;
|
||||||
// expandedRoute needs idmap already loaded to resolve item/group_item (see
|
const decoded = deserializeShortId(qualified ? qualified.ints : decodeShortId(text));
|
||||||
// router.js's NEEDS_IDMAP) - fall back to the token's own URL (ShortId.vue resolves
|
return {to: (!qualified && expandedRoute(decoded)) || '/' + text, decoded, domain: qualified?.domain};
|
||||||
// it from there, same as a cold-opened short link) when it can't yet.
|
|
||||||
return {to: expandedRoute(decoded) || '/' + text, decoded};
|
|
||||||
} catch {
|
} catch {
|
||||||
return null; // starts with '~' but isn't a real short id - leave as plain text
|
return null; // looked like a short id but isn't a real one - leave as plain text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleMatch = text.match(ITEM_HANDLE_RE);
|
const ownerMatch = text.match(OWNER_QUALIFIED_ID_RE);
|
||||||
if (handleMatch) {
|
if (ownerMatch) {
|
||||||
const [, handle, id] = handleMatch;
|
const [, handle, kindLetter, id] = ownerMatch;
|
||||||
return {to: `/inventory/${encodeHandleForUrl(handle)}/${id}`, itemHandle: {handle, id}};
|
const kind = kindLetter === "s" ? "storage_location" : "item";
|
||||||
|
const to = kind === "storage_location"
|
||||||
|
? `/storage-locations/${encodeHandleForUrl(handle)}/${id}`
|
||||||
|
: `/inventory/${encodeHandleForUrl(handle)}/${id}`;
|
||||||
|
return {to, ownerQualifiedId: {handle, kind, id}};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +144,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(["fetchItemByHandle", "fetchStorageLocationByHandle", "fetchIdMap"]),
|
...mapActions(["fetchItemByHandle", "fetchStorageLocationByHandle", "fetchIdMap", "resolveShortId"]),
|
||||||
|
|
||||||
resolveDescription(entry) {
|
resolveDescription(entry) {
|
||||||
const {link, text} = entry;
|
const {link, text} = entry;
|
||||||
|
|
@ -190,13 +180,17 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async describeLink(link) {
|
async describeLink(link) {
|
||||||
if (link.itemHandle) {
|
if (link.ownerQualifiedId) {
|
||||||
return this.describeItem(link.itemHandle.handle, link.itemHandle.id);
|
const {handle, kind, id} = link.ownerQualifiedId;
|
||||||
|
return kind === "storage_location" ? this.describeLocation(handle, id) : this.describeItem(handle, id);
|
||||||
}
|
}
|
||||||
const decoded = link.decoded;
|
const decoded = link.decoded;
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (link.domain) {
|
||||||
|
return this.describeForeignShortId(link.domain, decoded);
|
||||||
|
}
|
||||||
if (decoded.kind === "item" || decoded.kind === "group_item") {
|
if (decoded.kind === "item" || decoded.kind === "group_item") {
|
||||||
const byId = decoded.kind === "item"
|
const byId = decoded.kind === "item"
|
||||||
? this.$store.getters.identityHandleById
|
? this.$store.getters.identityHandleById
|
||||||
|
|
@ -236,6 +230,22 @@ export default {
|
||||||
return null; // workflow, category, file: no per-item title lookup wired up yet
|
return null; // workflow, category, file: no per-item title lookup wired up yet
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async describeForeignShortId(domain, decoded) {
|
||||||
|
const owned = OWNED_KIND_FIELDS[decoded.kind];
|
||||||
|
if (!owned) {
|
||||||
|
return null; // no owner id to resolve a foreign backend against yet (group/category/file/workflow)
|
||||||
|
}
|
||||||
|
const resolved = await this.resolveShortId({
|
||||||
|
domain, kind: decoded.kind, ownerId: decoded[owned.ownerField], localId: decoded[owned.localField],
|
||||||
|
});
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return owned.isLocation
|
||||||
|
? this.describeLocation(resolved.handle, resolved.id)
|
||||||
|
: this.describeItem(resolved.handle, resolved.id);
|
||||||
|
},
|
||||||
|
|
||||||
async describeItem(handle, id) {
|
async describeItem(handle, id) {
|
||||||
const item = await this.fetchItemByHandle({handle, id});
|
const item = await this.fetchItemByHandle({handle, id});
|
||||||
return item ? `[#${item.id}] ${item.name}` : null;
|
return item ? `[#${item.id}] ${item.name}` : null;
|
||||||
|
|
@ -263,20 +273,11 @@ export default {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
this.cameraLog.unshift(entry);
|
this.cameraLog.unshift(entry);
|
||||||
// Resolve against cameraLog[0], not the plain `entry` object above: Vue's reactivity
|
|
||||||
// tracks property sets through the reactive proxy unshift() just installed, and
|
|
||||||
// mutating the pre-insertion raw object later bypasses that proxy entirely, so the
|
|
||||||
// description would never appear to update (stuck on "resolving...") even once the
|
|
||||||
// lookup actually finished.
|
|
||||||
this.resolveDescription(this.cameraLog[0]);
|
this.resolveDescription(this.cameraLog[0]);
|
||||||
// Caps the log (old entries trimmed) rather than growing forever, matching
|
|
||||||
// CameraScanner's own dedupe window that makes them stale for re-matching.
|
|
||||||
this.cameraLog.length = Math.min(this.cameraLog.length, 20);
|
this.cameraLog.length = Math.min(this.cameraLog.length, 20);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
// Not component data: resolveDescription's cached values are read back into each entry's
|
|
||||||
// own (reactive) `description` field, so the cache itself never needs to be reactive.
|
|
||||||
this.descriptionCache = new Map();
|
this.descriptionCache = new Map();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
<BaseLayout hide-search>
|
<BaseLayout hide-search>
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
<div v-if="qualified" class="card">
|
||||||
|
<div class="card-header">Domain</div>
|
||||||
|
<div class="card-body">{{ qualified.domain }}</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Decoded</div>
|
<div class="card-header">Decoded</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -64,8 +68,11 @@
|
||||||
<script>
|
<script>
|
||||||
import {mapActions} from 'vuex';
|
import {mapActions} from 'vuex';
|
||||||
import BaseLayout from '@/components/BaseLayout.vue';
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
import {decodeShortId, deserializeShortId, encodeShortId, serializeShortId} from '@/short-id';
|
import {
|
||||||
import {expandedRoute as buildExpandedRoute, NEEDS_IDMAP} from '@/router';
|
decodeShortId, deserializeShortId, encodeShortId, serializeShortId,
|
||||||
|
isDomainQualifiedShortId, decodeDomainQualifiedShortId
|
||||||
|
} from '@/short-id';
|
||||||
|
import {expandedRoute as buildExpandedRoute, NEEDS_IDMAP, domainQualifiedRoute} from '@/router';
|
||||||
|
|
||||||
const EXAMPLES = [
|
const EXAMPLES = [
|
||||||
{kind: 'item', owner_identity_id: 7, item_local_id: 42},
|
{kind: 'item', owner_identity_id: 7, item_local_id: 42},
|
||||||
|
|
@ -130,12 +137,17 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
error: null
|
error: null,
|
||||||
|
foreignRoute: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// '<domain>:~<token>' vs a bare '~<token>' - see docs/handles-and-shortids.md#domain-qualified-short-id.
|
||||||
|
qualified() {
|
||||||
|
return isDomainQualifiedShortId(this.short_id) ? decodeDomainQualifiedShortId(this.short_id) : null;
|
||||||
|
},
|
||||||
decoded() {
|
decoded() {
|
||||||
return decodeShortId(this.short_id);
|
return this.qualified ? this.qualified.ints : decodeShortId(this.short_id);
|
||||||
},
|
},
|
||||||
deserialized() {
|
deserialized() {
|
||||||
return deserializeShortId(this.decoded);
|
return deserializeShortId(this.decoded);
|
||||||
|
|
@ -145,7 +157,7 @@ export default {
|
||||||
return fields;
|
return fields;
|
||||||
},
|
},
|
||||||
expandedRoute() {
|
expandedRoute() {
|
||||||
return buildExpandedRoute(this.deserialized);
|
return this.qualified ? (this.foreignRoute || null) : buildExpandedRoute(this.deserialized);
|
||||||
},
|
},
|
||||||
examples() {
|
examples() {
|
||||||
return EXAMPLES.map(named => {
|
return EXAMPLES.map(named => {
|
||||||
|
|
@ -165,13 +177,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['fetchIdMap'])
|
...mapActions(['fetchIdMap']),
|
||||||
|
async resolveIfQualified() {
|
||||||
|
if (this.qualified) {
|
||||||
|
this.foreignRoute = await domainQualifiedRoute(this.qualified.domain, this.decoded) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.resolveIfQualified();
|
||||||
if (NEEDS_IDMAP.has(this.deserialized.kind) && !this.$store.state.idmapLoaded) {
|
if (NEEDS_IDMAP.has(this.deserialized.kind) && !this.$store.state.idmapLoaded) {
|
||||||
this.fetchIdMap().catch(e => {
|
this.fetchIdMap()
|
||||||
this.error = e.message;
|
.then(() => this.resolveIfQualified())
|
||||||
});
|
.catch(e => {
|
||||||
|
this.error = e.message;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue