summary: 'Adds PATCH /api/v2/assets/{assetId} to API2, allowing partial (RFC 7396 merge-patch) updates to the scalar fields of an Asset (currently only the Name field). Authorized using the existing project Asset-edit role; no new permissions are introduced. Note: Asset is project-scoped in Postgres, so the endpoint is implemented under the project-scoped path /api/v2/projects/{projectId}/assets/{assetId} to resolve ProjectShardId. ' target_service: api2 postgres_changes: - kind: procedure schema: xyz name: usp_UpdateAsset action: amend file: Database/xyz/Procedures/usp_UpdateAsset.sql notes: 'Amend (or create if not present) the GENERAL update procedure for the Asset entity so it covers the full mutable scalar column set of xyz."Asset". The Asset table''s only trivial (non-relational, scalar) mutable field is "Name"; "AssetTypeId" and "ParentAssetId" are relational and OUT of scope per the spec, and audit/identity columns ("AssetId", "ProjectShardId", "InsertedOn", "CreatedBy") are immutable. Procedure resolves the shard id via SELECT xyz."fn_GetProjectShardId"(_projectId) INTO _projectShardId; then performs the update filtered by (ProjectShardId, AssetId). Merge-patch semantics are handled by COALESCE-style parameters: a NULL parameter means "field omitted, leave unchanged". Must validate existence (raise NOT_FOUND if no matching, non-deleted Asset row) and set "LastModifiedOn" = TIMEZONE(''UTC'', NOW()) and "LastModifiedBy" = _lastModifiedBy. Parameters: (_projectId UUID, _assetId UUID, _name TEXT, _lastModifiedBy TEXT). Include the standard EXCEPTION block logging to xyz."DbException". Body must DROP PROCEDURE IF EXISTS then CREATE OR REPLACE, endDelimiter:/. ' - kind: function schema: xyz name: fn_GetAsset action: amend file: Database/xyz/Functions/fn_GetAsset.sql notes: 'Amend (or create if not present) the GENERAL read function for the Asset entity so it returns the FULL column set of xyz."Asset" (AssetId, AssetTypeId, ParentAssetId, Name, CreatedBy, InsertedOn, LastModifiedOn, LastModifiedBy, IsDeleted, DeletedOn, DeletedBy). Used by the controller to return the updated Asset representation in one round-trip and to 404 when the asset does not exist / is deleted. Signature: fn_GetAsset(_projectId UUID, _assetId UUID) RETURNS TABLE(...). Resolves shard id via fn_GetProjectShardId, filters by ProjectShardId and AssetId. runOnChange:true, endDelimiter:/. ' citus_changes: none index_changes: none reporting_changes: none seed_data_changes: none mongo_changes: none api2_changes: - method: PATCH path: /api/v2/projects/{projectId}/assets/{assetId} swagger_tag: Assets permission: ASSET_EDIT files: routes: src/api/v2/projects/assets/assets.routes.ts controller: src/api/v2/projects/assets/assets.controller.ts validator: src/api/v2/projects/assets/assets.validator.ts service: src/services/assets.service.ts request_dto: name: UpdateAssetRequest location: src/models/ingress.ts merge_patch: true properties: name: type: string required: false nullable: false description: The asset name. Omit to leave unchanged (RFC 7396). response_dto: name: AssetResponse location: src/models/egress.ts properties: assetId: type: string format: uuid assetTypeId: type: string format: uuid parentAssetId: type: string format: uuid nullable: true name: type: string createdBy: type: string insertedOn: type: string format: date-time lastModifiedOn: type: string format: date-time nullable: true lastModifiedBy: type: string nullable: true db_calls: - usp_UpdateAsset - fn_GetAsset responses: - 200 - 400 - 401 - 403 - 404 - 500 notes: 'Path is project-scoped so the procedure can resolve ProjectShardId via fn_GetProjectShardId. The spec''s bare /api/v2/assets/:id shape is not viable because Asset is a project-scoped table; this is the closest convention-compliant equivalent (mirrors the existing room-capture-points / model-versions PATCH endpoints). Merge-patch semantics: only "name" is patchable (the sole trivial scalar mutable field on Asset). Omitted -> unchanged. Since "Name" is NOT NULL on the table, an explicit null for "name" is REJECTED with 400. Relational fields (assetTypeId, parentAssetId) and identity/audit fields (assetId, projectShardId, createdBy, insertedOn) are immutable and rejected/ignored per the out-of-scope list. Authorization uses the existing ASSET_EDIT project-scoped permission via existing RBAC middleware (verifyToken + hasPermission on projectId). NO new role/permission is introduced. lastModifiedBy is taken from the authenticated session, not the body. Result reuse: usp_UpdateAsset validates existence + performs the update in one procedure (raising NOT_FOUND if the asset is missing or soft-deleted); the controller then calls fn_GetAsset ONCE to return the updated representation. The controller does NOT re-fetch for a separate existence check. ' api1_changes: none inter_service_calls: none new_permissions: none java_frozen_resources: none risks: - 'The spec requests bare PATCH /api/v2/assets/:id, but xyz."Asset" is a project-scoped table keyed by (ProjectShardId, AssetId). A non-project path cannot resolve the shard id via fn_GetProjectShardId. Routed as PATCH /api/v2/projects/{projectId}/assets/{assetId} instead. Confirm this path shape with the team; if a bare /assets/:id is mandatory a project-lookup-by-asset function would be required (extra round-trip). ' - 'Open question from spec: patchable scalar fields. On the current Asset schema the only trivial (non-relational, scalar) mutable field is "Name". AssetTypeId and ParentAssetId are relational (explicitly out-of-scope). If the team expects more scalar fields, the Asset schema would need new columns first (separate feature). ' - 'Explicit-null behavior: "Name" is NOT NULL, so a null in the patch body is rejected with 400 rather than clearing the field. Confirm this matches the intended merge-patch semantics. ' - 'No optimistic-concurrency (ETag / If-Match) is implemented, matching the other API2 PATCH endpoints. Confirm this is acceptable. ' out_of_scope: - JSON Patch (RFC 6902) operation syntax - Patching relational fields on Asset (AssetTypeId, ParentAssetId) or nested/linked entities - Bulk / multi-asset patch in a single request - "New roles or field-level permission granularity \u2014 uses existing ASSET_EDIT\ \ permission only" - New Asset columns / schema additions testing_plan: "API2:\n - Unit tests for assets.controller/service covering: successful\ \ name\n update (200 with full updated Asset), omitted field leaves value\n \ \ unchanged, explicit null name rejected (400), non-existent asset (404\n propagated\ \ from procedure), unauthorized caller lacking ASSET_EDIT\n (403), and invalid\ \ body shape (400).\n - e2e test for PATCH /api/v2/projects/{projectId}/assets/{assetId}\n\ \ exercising the RBAC middleware and merge-patch semantics against the\n procedure.\n\ \ - Add/adjust mocks/fixtures for usp_UpdateAsset and fn_GetAsset so\n `npm\ \ test` stays green.\nPostgres:\n - Confirm usp_UpdateAsset and fn_GetAsset deploy\ \ cleanly via ./build.\n - Optionally extend IntegrationTest/main.py only if update\ \ behavior\n materially affects existing query paths (pure additions usually\ \ do\n not require it)." _meta: model: claude-opus-4-8 atom_ids: - endpoint.api2.patch__api_v2_projects__projectId__model-versions__modelVersionId_ - endpoint.api2.patch__api_v2_projects__projectId__models__modelId_ - endpoint.api2.patch__api_v2_projects__projectId__issues__issueId_ - endpoint.api2.patch__api_v2_projects__projectId__issues__issueId__models_unlink-model - endpoint.api2.patch__api_v2_projects__projectId__issues_activity-categories__issueId__unlink - endpoint.api2.patch__api_v2_projects__projectId__room-capture-points__roomCapturePointId_ - endpoint.api2.patch__api_v2_projects__projectId__issues__issueId__file-references_unlink - endpoint.api2.post__api_v2_projects__projectId__model__modelId__translate-query - endpoint.api2.post__api_v2_projects__projectId__models__modelId__move - endpoint.api2.patch__api_v2_portfolios__portfolioId_ - endpoint.api2.patch__api_v2_projects__projectId__photos__fileReferenceId_ - endpoint.api2.post__api_v2_projects__projectId__models_folders_move - pg.reporting.CalculationMethod - pg.reporting.ProgressOutput - pg.reporting.ProjectCalculationMethod - pg.reporting.ProjectPerformanceSnapshot - pg.reporting.ProjectProgress - pg.staging.DuplicatedMigratedMongoElement spec: title: "PATCH /api/v2/assets/:id \u2014 Asset Update Endpoint" project_kind: modify user_facing_behavior: Clients can partially update an Asset by sending a PATCH request to /api/v2/assets/:id with a JSON merge-patch body (RFC 7396). Omitted fields remain unchanged. All trivial (non-relational, scalar) fields on Asset are patchable. The response returns the updated Asset. Requests are authorized using the existing role-based access control model. data_touched: - Asset api_surface: write non_functional_requirements: - "Follows RFC 7396 merge-patch semantics \u2014 omitted fields are left unchanged,\ \ explicit null may clear a nullable field" - "Uses existing RBAC middleware \u2014 only roles currently permitted to modify\ \ assets may call this endpoint" - Returns the full updated Asset representation on success (200 OK) - Returns 404 if the asset does not exist - Returns 403 if the caller lacks permission - Returns 400 for invalid field values or constraint violations - Should be consistent with existing API2 conventions (error shape, auth middleware, response envelope if any) out_of_scope: - JSON Patch (RFC 6902) operation syntax - Patching relational / nested entities (e.g. linked records, associations) - Bulk / multi-asset patch in a single request - Field-level permission granularity beyond existing roles open_questions: - Which specific roles are permitted to patch an asset (e.g. admin only, or also editor/owner)? - Should null values in the patch body explicitly clear nullable fields, or be rejected? - Is there an optimistic-concurrency requirement (e.g. ETag / If-Match header)? - "Exact list of patchable scalar fields \u2014 architect should enumerate all\ \ trivial fields from the Asset schema and confirm any that must remain immutable\ \ (e.g. createdAt, externalId)."