Skip to content

server: Discovery PostAllAssetLinksById does not replace links #669

Open

Description

@zrgt

PostAllAssetLinksById is specified as "Creates or replaces all
asset links associated to the Asset Administration Shell":

https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.1.1/specification/interfaces.html#PostAllAssetLinksById

The server merges the posted links into the existing ones instead:

def post_all_asset_links_by_id(
self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs
) -> Response:
aas_identifier = str(url_args["aas_id"])
specific_asset_ids = HTTPApiDecoder.request_body_list(request, model.SpecificAssetId, False)
self.persistent_store.add_specific_asset_ids_to_aas(aas_identifier, specific_asset_ids)
for asset_id in specific_asset_ids:
self.persistent_store._add_aas_id_to_specific_asset_id(asset_id, aas_identifier)
updated = {aas_identifier: self.persistent_store.get_all_specific_asset_ids_by_aas_id(aas_identifier)}
return response_t(updated)

Steps to reproduce:

  1. POST /lookup/shells/{aasId} with [{"name": "serial", "value": "123"}]
  2. POST /lookup/shells/{aasId} with [{"name": "batch", "value": "xyz"}]
  3. GET /lookup/shells/{aasId}

Expected behavior: Only batch=xyz is linked to the AAS.

Actual behavior: Both links are returned, and a lookup by
serial=123 still finds the AAS. The test test_post_is_additive
asserts this behavior:

def test_post_is_additive(self) -> None:
self.register(self.AAS_ID, [("serial", "123")])
response = self.format_client.post(
f"/lookup/shells/{base64url_encode(self.AAS_ID)}", obj=[{"name": "batch", "value": "xyz"}]
)
self.assert_ok(response)
stored = {(a["name"], a["value"]) for a in self.format_client.parse_object(response)[self.AAS_ID]}
self.assertEqual({("serial", "123"), ("batch", "xyz")}, stored)

Environment: develop @ 2773ac0.

Possible fix: In post_all_asset_links_by_id, drop the AAS from
the reverse index for its old links, then store the new ones. Adapt
test_post_is_additive. #625 / #641 fix the status codes of the same
endpoint, but not this.

Activity

  1. zrgt commented on Oct 22, 2026

    @zrgt
    ContributorAuthor

    While fixing this: the reverse index (asset_id_to_aas_ids) is
    maintained in the handlers, not in DiscoveryStore.
    post_all_asset_links_by_id calls the private
    _add_aas_id_to_specific_asset_id(), and
    delete_all_asset_links_by_id loops over the store's dict and
    leaves empty sets behind:

    def post_all_asset_links_by_id(
    self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs
    ) -> Response:
    aas_identifier = str(url_args["aas_id"])
    specific_asset_ids = HTTPApiDecoder.request_body_list(request, model.SpecificAssetId, False)
    self.persistent_store.add_specific_asset_ids_to_aas(aas_identifier, specific_asset_ids)
    for asset_id in specific_asset_ids:
    self.persistent_store._add_aas_id_to_specific_asset_id(asset_id, aas_identifier)
    updated = {aas_identifier: self.persistent_store.get_all_specific_asset_ids_by_aas_id(aas_identifier)}
    return response_t(updated)
    def delete_all_asset_links_by_id(
    self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs
    ) -> Response:
    aas_identifier = str(url_args["aas_id"])
    self.persistent_store.delete_specific_asset_ids_by_aas_id(aas_identifier)
    for key in list(self.persistent_store.asset_id_to_aas_ids.keys()):
    self.persistent_store.asset_id_to_aas_ids[key].discard(aas_identifier)
    return response_t()

    Moving both updates into add_specific_asset_ids_to_aas() and
    delete_specific_asset_ids_by_aas_id() keeps the two maps in sync in
    one place, which is also where #663 would persist the store.

  2. zrgt commented on Oct 22, 2026

    @zrgt
    ContributorAuthor

    Small addition: search_aas_ids_by_asset_link() scans the whole
    reverse index on every lookup, because it is keyed by
    SpecificAssetId (which also compares externalSubjectId and
    semantic ids) while a lookup only has name and value:

    def search_aas_ids_by_asset_link(self, asset_link: server_model.AssetLink) -> List[model.Identifier]:
    result = []
    for asset_key, aas_ids in self.asset_id_to_aas_ids.items():
    if asset_key.name == asset_link.name and asset_key.value == asset_link.value:
    result.extend(list(aas_ids))
    return result

    If the index moves into DiscoveryStore anyway, keying it by
    (name, value) makes the lookup a dict access.

  3. added
    bugSomething isn't working
    serverSomething to do with the `server` package
    on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingserverSomething to do with the `server` package

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions