V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Generated from contracts/api/kbx.api.json. Do not edit.
|
||||
namespace Shared.Contracts.Generated;
|
||||
|
||||
public sealed record KbxApiOperationContract(
|
||||
string Id, string Method, string Path, string? Permission, string Kind, string Idempotency, IReadOnlyList<int> SuccessStatuses);
|
||||
|
||||
public static class KbxApiCatalog
|
||||
{
|
||||
public const string SourceSha256 = "8a8008b0eca039ef22b703b142dd7fa2efeaf5b14784ba9f3a954f656a1a0c8f";
|
||||
public static readonly IReadOnlyList<KbxApiOperationContract> All = new KbxApiOperationContract[]
|
||||
{
|
||||
new("common.ai.ask", "POST", "/api/common/ai/ask", "common.ai.use", "command", "supported", new[] { 200 }),
|
||||
new("common.experiments.assignments", "GET", "/api/kbx/experiments/assignments", "common.experiment.evaluate", "query", "none", new[] { 200 }),
|
||||
new("common.experiments.overview", "GET", "/api/kbx/experiments", "common.experiment.read", "query", "none", new[] { 200 }),
|
||||
new("common.experiments.rollback", "POST", "/api/kbx/experiments/{experimentId}/rollback", "common.experiment.manage", "command", "supported", new[] { 200 }),
|
||||
new("common.experiments.rollout", "POST", "/api/kbx/experiments/{experimentId}/rollout", "common.experiment.manage", "command", "supported", new[] { 200 }),
|
||||
new("common.externalData.refresh", "POST", "/api/kbx/external-data/{datasetId}/refresh", "common.external-data.refresh", "command", "supported", new[] { 202 }),
|
||||
new("common.externalData.status", "GET", "/api/kbx/external-data/status", "common.external-data.read", "query", "none", new[] { 200 }),
|
||||
new("common.imports.commit", "POST", "/api/imports/sessions/{sessionId:guid}/commit", "imports.execute", "command", "supported", new[] { 200 }),
|
||||
new("common.imports.createSession", "POST", "/api/imports/sessions", "imports.execute", "upload", "supported", new[] { 200 }),
|
||||
new("common.imports.errorWorkbook", "GET", "/api/imports/sessions/{sessionId:guid}/errors.xlsx", "imports.execute", "query", "none", new[] { 200 }),
|
||||
new("common.imports.getSession", "GET", "/api/imports/sessions/{sessionId:guid}", "imports.execute", "query", "none", new[] { 200 }),
|
||||
new("common.imports.saveMapping", "PUT", "/api/imports/sessions/{sessionId:guid}/mapping", "imports.execute", "command", "supported", new[] { 200 }),
|
||||
new("common.imports.saveNamedMapping", "POST", "/api/imports/sessions/{sessionId:guid}/saved-mappings", "imports.execute", "command", "supported", new[] { 204 }),
|
||||
new("common.imports.template", "GET", "/api/imports/{importType}/template", "imports.execute", "query", "none", new[] { 200 }),
|
||||
new("common.imports.validate", "POST", "/api/imports/sessions/{sessionId:guid}/validate", "imports.execute", "command", "supported", new[] { 200 }),
|
||||
new("common.integrations.getAttempt", "GET", "/api/integrations/attempts/{attemptId:guid}", "common.integration.read", "query", "none", new[] { 200 }),
|
||||
new("common.integrations.retryAttempt", "POST", "/api/integrations/attempts/{attemptId:guid}/retry", "common.integration.retry", "command", "required", new[] { 200 }),
|
||||
new("common.operations.claim", "POST", "/api/operations/work-items/claim", "common.operations.claim", "command", "supported", new[] { 200 }),
|
||||
new("common.operations.resolve", "POST", "/api/operations/work-items/resolve", "common.operations.resolve", "command", "supported", new[] { 200 }),
|
||||
new("common.operations.retry", "POST", "/api/operations/work-items/{id:guid}/retry", "common.operations.retry", "command", "supported", new[] { 200 }),
|
||||
new("common.operations.search", "GET", "/api/operations/work-items", "common.operations.read", "query", "none", new[] { 200 }),
|
||||
new("common.reconcile.createExceptions", "POST", "/api/reconcile/items/create-exceptions", "common.operations.create", "command", "supported", new[] { 200 }),
|
||||
new("common.reconcile.search", "GET", "/api/reconcile/items", "common.reconcile.read", "query", "none", new[] { 200 }),
|
||||
new("common.runtime.notice", "GET", "/api/kbx/runtime/notice", "common.runtime.read", "query", "none", new[] { 200 }),
|
||||
new("common.runtime.notificationRead", "POST", "/api/kbx/runtime/notifications/{Id}/read", "common.runtime.read", "command", "supported", new[] { 204 }),
|
||||
new("common.runtime.notifications", "GET", "/api/kbx/runtime/notifications", "common.runtime.read", "query", "none", new[] { 200 }),
|
||||
new("common.runtime.operations", "GET", "/api/kbx/runtime/operations", "common.runtime.read", "query", "none", new[] { 200 }),
|
||||
new("common.suggestions.submit", "POST", "/api/common/suggestions", "common.suggestion.create", "command", "supported", new[] { 200 }),
|
||||
new("common.uxTelemetry.ingest", "POST", "/api/kbx/ux/events", "common.telemetry.write", "command", "none", new[] { 204 }),
|
||||
new("common.uxTelemetry.metrics", "GET", "/api/kbx/ux/metrics", "common.ux.read", "query", "none", new[] { 200 }),
|
||||
new("erp.inventory.history", "GET", "/api/erp/inventory/{itemId}/history", "erp.inventory.read", "query", "none", new[] { 200 }),
|
||||
new("erp.inventory.locations", "GET", "/api/erp/inventory/{itemId}/locations", "erp.inventory.read", "query", "none", new[] { 200 }),
|
||||
new("erp.inventory.search", "GET", "/api/erp/inventory", "erp.inventory.read", "query", "none", new[] { 200 }),
|
||||
new("erp.itemPrices.bulkSave", "POST", "/api/erp/item-prices/bulk", "erp.item.price.write", "command", "required", new[] { 200 }),
|
||||
new("erp.items.get", "GET", "/api/erp/items/{id}", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("erp.items.search", "GET", "/api/erp/items", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.customers.resolveByCode", "GET", "/api/lookups/customers/by-code/{code}", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.customers.resolveById", "GET", "/api/lookups/customers/{id}", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.customers.search", "GET", "/api/lookups/customers", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.items.resolveByCode", "GET", "/api/lookups/items/by-code/{code}", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.items.resolveById", "GET", "/api/lookups/items/{id}", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.items.search", "GET", "/api/lookups/items", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.warehouses.resolveByCode", "GET", "/api/lookups/warehouses/by-code/{code}", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.warehouses.resolveById", "GET", "/api/lookups/warehouses/{id}", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("lookup.warehouses.search", "GET", "/api/lookups/warehouses", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("oms.claims.approve", "POST", "/api/oms/claims/{id:guid}/approve", "oms.claim.approve", "command", "supported", new[] { 200 }),
|
||||
new("oms.claims.complete", "POST", "/api/oms/claims/{id:guid}/complete", "oms.claim.process", "command", "supported", new[] { 200 }),
|
||||
new("oms.claims.hold", "POST", "/api/oms/claims/{id:guid}/hold", "oms.claim.hold", "command", "supported", new[] { 200 }),
|
||||
new("oms.claims.search", "GET", "/api/oms/claims", "oms.claim.read", "query", "none", new[] { 200 }),
|
||||
new("oms.claims.start", "POST", "/api/oms/claims/{id:guid}/start", "oms.claim.process", "command", "supported", new[] { 200 }),
|
||||
new("oms.orders.register", "POST", "/api/oms/orders/register", "oms.order.create", "command", "supported", new[] { 200 }),
|
||||
new("oms.orders.search", "GET", "/api/oms/orders", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("oms.orders.ship", "POST", "/api/oms/orders/ship", "oms.order.ship", "command", "required", new[] { 200 }),
|
||||
new("wms.picking.getTask", "GET", "/api/wms/picking/tasks/{taskId:guid}", "wms.picking.execute", "query", "none", new[] { 200 }),
|
||||
new("wms.picking.reportException", "POST", "/api/wms/picking/tasks/{taskId:guid}/exceptions", "wms.picking.execute", "command", "required", new[] { 200 }),
|
||||
new("wms.picking.scan", "POST", "/api/wms/picking/tasks/{taskId:guid}/scan", "wms.picking.execute", "command", "required", new[] { 200, 422 }),
|
||||
new("wms.picking.setQuantity", "POST", "/api/wms/picking/tasks/{taskId:guid}/quantity", "wms.picking.execute", "command", "required", new[] { 200 }),
|
||||
new("wms.picking.start", "POST", "/api/wms/picking/tasks/{taskId:guid}/start", "wms.picking.execute", "command", "supported", new[] { 200 }),
|
||||
new("erp.items.create", "POST", "/api/erp/items", "erp.item.create", "command", "none", new[] { 200 }),
|
||||
new("erp.items.update", "PUT", "/api/erp/items/{id}", "erp.item.write", "command", "none", new[] { 200 }),
|
||||
new("erp.items.deactivate", "POST", "/api/erp/items/{id}/deactivate", "erp.item.write", "command", "none", new[] { 200 }),
|
||||
new("erp.items.audit", "GET", "/api/erp/items/{id}/audit", "erp.item.read", "query", "none", new[] { 200 }),
|
||||
new("oms.orders.get", "GET", "/api/oms/orders/{id}", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("oms.orders.confirm", "POST", "/api/oms/orders/{id}/confirm", "oms.order.confirm", "command", "required", new[] { 200 }),
|
||||
new("oms.orders.audit", "GET", "/api/oms/orders/{id}/audit", "oms.order.read", "query", "none", new[] { 200 }),
|
||||
new("oms.orders.update", "PUT", "/api/oms/orders/{id}", "oms.order.write", "command", "supported", new[] { 200 })
|
||||
};
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// <auto-generated />
|
||||
// Source: contracts/fields/kbx.fields.json
|
||||
// Source SHA256: f76e073cd8052e9fddb25cc73322686c889822a22fab712ff1a25bef29b32e3c
|
||||
namespace Kbx.Contracts.Generated;
|
||||
|
||||
public sealed record KbxFieldMetadata(
|
||||
string Key, string Label, string DataType, IReadOnlyList<string> Aliases,
|
||||
bool Required, int? MaxLength, int? Precision, int? Scale, string? LookupEntity,
|
||||
bool Readonly, bool Importable, bool Exportable, bool Sensitive, string? Masking);
|
||||
|
||||
public static class KbxFieldKeys
|
||||
{
|
||||
public const string Address1 = "address1";
|
||||
public const string Address2 = "address2";
|
||||
public const string AllocatedQty = "allocatedQty";
|
||||
public const string Amount = "amount";
|
||||
public const string AvailableQty = "availableQty";
|
||||
public const string Barcode = "barcode";
|
||||
public const string ChannelId = "channelId";
|
||||
public const string ChannelName = "channelName";
|
||||
public const string CreatedAt = "createdAt";
|
||||
public const string CreatedBy = "createdBy";
|
||||
public const string CustomerCode = "customerCode";
|
||||
public const string CustomerId = "customerId";
|
||||
public const string CustomerName = "customerName";
|
||||
public const string DamagedQty = "damagedQty";
|
||||
public const string EffectiveDate = "effectiveDate";
|
||||
public const string ExceptionCount = "exceptionCount";
|
||||
public const string ExpiryDate = "expiryDate";
|
||||
public const string HoldQty = "holdQty";
|
||||
public const string ItemCode = "itemCode";
|
||||
public const string ItemId = "itemId";
|
||||
public const string ItemName = "itemName";
|
||||
public const string ItemSummary = "itemSummary";
|
||||
public const string LocationCode = "locationCode";
|
||||
public const string LocationId = "locationId";
|
||||
public const string LotNo = "lotNo";
|
||||
public const string OccurredAt = "occurredAt";
|
||||
public const string OnHandQty = "onHandQty";
|
||||
public const string OrderDate = "orderDate";
|
||||
public const string OrderedAt = "orderedAt";
|
||||
public const string OrderId = "orderId";
|
||||
public const string OrderNo = "orderNo";
|
||||
public const string OrderQty = "orderQty";
|
||||
public const string OwnerName = "ownerName";
|
||||
public const string Phone = "phone";
|
||||
public const string PickedQty = "pickedQty";
|
||||
public const string PostalCode = "postalCode";
|
||||
public const string Quantity = "quantity";
|
||||
public const string ReceiverName = "receiverName";
|
||||
public const string ReferenceNo = "referenceNo";
|
||||
public const string Remark = "remark";
|
||||
public const string ShippedQty = "shippedQty";
|
||||
public const string Specification = "specification";
|
||||
public const string Status = "status";
|
||||
public const string TotalQty = "totalQty";
|
||||
public const string UnitPrice = "unitPrice";
|
||||
public const string UpdatedAt = "updatedAt";
|
||||
public const string UpdatedBy = "updatedBy";
|
||||
public const string Version = "version";
|
||||
public const string WarehouseCode = "warehouseCode";
|
||||
public const string WarehouseId = "warehouseId";
|
||||
public const string WarehouseName = "warehouseName";
|
||||
}
|
||||
|
||||
public static class KbxFieldCatalog
|
||||
{
|
||||
public const string Version = "1.1.0";
|
||||
private static readonly IReadOnlyDictionary<string, KbxFieldMetadata> Items =
|
||||
new Dictionary<string, KbxFieldMetadata>(StringComparer.Ordinal)
|
||||
{
|
||||
["address1"] = new("address1", "주소", "text", new[] { "기본주소", "배송주소", "Address1" }, true, 500, null, null, null, false, true, true, true, "address"),
|
||||
["address2"] = new("address2", "상세주소", "text", new[] { "주소2", "Address2" }, false, 500, null, null, null, false, true, true, true, "address"),
|
||||
["allocatedQty"] = new("allocatedQty", "할당수량", "quantity", new[] { "AllocatedQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["amount"] = new("amount", "금액", "money", new[] { "Amount", "주문금액" }, false, null, 18, 2, null, true, false, true, false, null),
|
||||
["availableQty"] = new("availableQty", "가용재고", "quantity", new[] { "AvailableQty", "출고가능수량" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["barcode"] = new("barcode", "바코드", "code", new[] { "Barcode", "EAN", "JAN" }, false, 100, null, null, "item", false, true, true, false, null),
|
||||
["channelId"] = new("channelId", "판매채널ID", "lookup", new[] { "ChannelId" }, false, null, null, null, "salesChannel", true, false, false, false, null),
|
||||
["channelName"] = new("channelName", "판매채널", "text", new[] { "ChannelName", "채널" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["createdAt"] = new("createdAt", "등록일시", "datetime", new[] { "CreatedAt" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["createdBy"] = new("createdBy", "등록자", "text", new[] { "CreatedBy" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["customerCode"] = new("customerCode", "거래처코드", "code", new[] { "거래처", "업체코드", "CustomerCode" }, true, 50, null, null, "customer", false, true, true, false, null),
|
||||
["customerId"] = new("customerId", "거래처ID", "lookup", new[] { "CustomerId" }, false, null, null, null, "customer", true, false, false, false, null),
|
||||
["customerName"] = new("customerName", "거래처명", "text", new[] { "CustomerName", "업체명" }, false, 200, null, null, null, true, false, true, false, null),
|
||||
["damagedQty"] = new("damagedQty", "불량수량", "quantity", new[] { "DamagedQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["effectiveDate"] = new("effectiveDate", "적용일", "date", new[] { "EffectiveDate", "시작일", "적용시작일" }, true, null, null, null, null, false, true, true, false, null),
|
||||
["exceptionCount"] = new("exceptionCount", "예외건수", "integer", new[] { "ExceptionCount", "오류건수" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["expiryDate"] = new("expiryDate", "유통기한", "date", new[] { "ExpiryDate", "ExpirationDate" }, false, null, null, null, null, false, true, true, false, null),
|
||||
["holdQty"] = new("holdQty", "보류수량", "quantity", new[] { "HoldQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["itemCode"] = new("itemCode", "품목코드", "code", new[] { "상품코드", "SKU", "ItemCode" }, true, 80, null, null, "item", false, true, true, false, null),
|
||||
["itemId"] = new("itemId", "품목ID", "lookup", new[] { "ItemId" }, false, null, null, null, "item", true, false, false, false, null),
|
||||
["itemName"] = new("itemName", "품목명", "text", new[] { "상품명", "ItemName" }, false, 300, null, null, null, true, false, true, false, null),
|
||||
["itemSummary"] = new("itemSummary", "대표상품", "text", new[] { "ItemSummary" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["locationCode"] = new("locationCode", "로케이션", "code", new[] { "Location", "LocationCode", "위치" }, false, 80, null, null, "location", false, true, true, false, null),
|
||||
["locationId"] = new("locationId", "로케이션ID", "lookup", new[] { "LocationId" }, false, null, null, null, "location", true, false, false, false, null),
|
||||
["lotNo"] = new("lotNo", "LOT", "code", new[] { "LotNo", "LOT번호" }, false, 100, null, null, null, false, true, true, false, null),
|
||||
["occurredAt"] = new("occurredAt", "발생시각", "datetime", new[] { "OccurredAt" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["onHandQty"] = new("onHandQty", "현재고", "quantity", new[] { "OnHandQty", "장부재고" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["orderDate"] = new("orderDate", "주문일", "date", new[] { "주문일자", "OrderDate" }, true, null, null, null, null, false, true, true, false, null),
|
||||
["orderedAt"] = new("orderedAt", "주문일시", "datetime", new[] { "OrderedAt", "주문시간" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["orderId"] = new("orderId", "주문ID", "code", new[] { "OrderId" }, false, null, null, null, null, true, false, false, false, null),
|
||||
["orderNo"] = new("orderNo", "주문번호", "code", new[] { "OrderNo", "주문ID" }, true, 50, null, null, null, false, true, true, false, null),
|
||||
["orderQty"] = new("orderQty", "주문수량", "quantity", new[] { "주문수량", "수량", "Qty", "OrderQty" }, true, null, 18, 4, null, false, true, true, false, null),
|
||||
["ownerName"] = new("ownerName", "담당자", "text", new[] { "OwnerName", "담당" }, false, 100, null, null, null, true, false, true, false, null),
|
||||
["phone"] = new("phone", "연락처", "text", new[] { "휴대폰", "전화번호", "Phone" }, true, 50, null, null, null, false, true, true, true, "phone"),
|
||||
["pickedQty"] = new("pickedQty", "피킹수량", "quantity", new[] { "PickedQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["postalCode"] = new("postalCode", "우편번호", "text", new[] { "ZipCode", "PostalCode" }, false, 20, null, null, null, false, true, true, true, "address"),
|
||||
["quantity"] = new("quantity", "수량", "quantity", new[] { "Qty" }, false, null, 18, 4, null, false, false, false, false, null),
|
||||
["receiverName"] = new("receiverName", "수취인", "text", new[] { "받는분", "수령인", "ReceiverName" }, true, 100, null, null, null, false, true, true, true, "name"),
|
||||
["referenceNo"] = new("referenceNo", "참조번호", "code", new[] { "ReferenceNo" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["remark"] = new("remark", "비고", "text", new[] { "메모", "배송메모", "Remark" }, false, 500, null, null, null, false, true, true, false, null),
|
||||
["shippedQty"] = new("shippedQty", "출고수량", "quantity", new[] { "ShippedQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["specification"] = new("specification", "규격", "text", new[] { "Specification", "Spec" }, false, 200, null, null, null, false, true, true, false, null),
|
||||
["status"] = new("status", "상태", "status", new[] { "Status" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["totalQty"] = new("totalQty", "총수량", "quantity", new[] { "TotalQty" }, false, null, 18, 4, null, true, false, true, false, null),
|
||||
["unitPrice"] = new("unitPrice", "단가", "money", new[] { "판매단가", "UnitPrice" }, false, null, 18, 4, null, false, true, true, false, null),
|
||||
["updatedAt"] = new("updatedAt", "수정일시", "datetime", new[] { "UpdatedAt" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["updatedBy"] = new("updatedBy", "수정자", "text", new[] { "UpdatedBy" }, false, null, null, null, null, true, false, true, false, null),
|
||||
["version"] = new("version", "버전", "integer", new[] { "Version", "RowVersion" }, false, null, null, null, null, true, false, false, false, null),
|
||||
["warehouseCode"] = new("warehouseCode", "창고코드", "code", new[] { "창고", "창고코드", "출고창고", "WarehouseCode" }, true, 50, null, null, "warehouse", false, true, true, false, null),
|
||||
["warehouseId"] = new("warehouseId", "창고ID", "lookup", new[] { "WarehouseId" }, false, null, null, null, "warehouse", true, false, false, false, null),
|
||||
["warehouseName"] = new("warehouseName", "창고명", "text", new[] { "WarehouseName" }, false, 200, null, null, null, true, false, true, false, null),
|
||||
};
|
||||
|
||||
public static KbxFieldMetadata Get(string key) => Items.TryGetValue(key, out var value)
|
||||
? value
|
||||
: throw new KeyNotFoundException($"Unknown KBX field key: {key}");
|
||||
|
||||
public static bool TryGet(string key, out KbxFieldMetadata? value) => Items.TryGetValue(key, out value);
|
||||
public static IReadOnlyCollection<KbxFieldMetadata> All => Items.Values;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Shared.Contracts.Generated;
|
||||
using Shared.Problems;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Shared.Contracts;
|
||||
|
||||
public static class KbxApiContractSwaggerExtensions
|
||||
{
|
||||
public static void AddKbxApiContracts(this SwaggerGenOptions options)
|
||||
{
|
||||
options.OperationFilter<KbxApiContractOperationFilter>();
|
||||
options.OperationFilter<KbxProblemOpenApiOperationFilter>();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KbxApiContractOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var method = context.ApiDescription.HttpMethod?.ToUpperInvariant();
|
||||
var relative = "/" + (context.ApiDescription.RelativePath ?? string.Empty).TrimStart('/');
|
||||
var normalized = Normalize(relative);
|
||||
var contract = KbxApiCatalog.All.FirstOrDefault(x =>
|
||||
x.Method == method && Normalize(x.Path) == normalized);
|
||||
if (contract is null) return;
|
||||
|
||||
operation.OperationId = contract.Id;
|
||||
operation.Extensions["x-kbx-permission"] = new OpenApiString(contract.Permission ?? string.Empty);
|
||||
operation.Extensions["x-kbx-idempotency"] = new OpenApiString(contract.Idempotency);
|
||||
operation.Extensions["x-kbx-kind"] = new OpenApiString(contract.Kind);
|
||||
}
|
||||
|
||||
private static string Normalize(string value) =>
|
||||
Regex.Replace(value, @"\{([^}:]+):[^}]+\}", "{$1}").ToLowerInvariant();
|
||||
}
|
||||
|
||||
public sealed class KbxProblemOpenApiOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var problem = new OpenApiSchema
|
||||
{
|
||||
OneOf = new List<OpenApiSchema>
|
||||
{
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxValidationProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxBusinessProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxConflictProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxPermissionProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxNotFoundProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxIntegrationProblem), context.SchemaRepository),
|
||||
context.SchemaGenerator.GenerateSchema(typeof(KbxSystemProblem), context.SchemaRepository),
|
||||
}
|
||||
};
|
||||
|
||||
Add(operation, "400", "Validation problem", problem);
|
||||
Add(operation, "403", "Permission problem", problem);
|
||||
Add(operation, "404", "Not found problem", problem);
|
||||
Add(operation, "409", "Conflict or business rule problem", problem);
|
||||
Add(operation, "422", "Business rule problem", problem);
|
||||
Add(operation, "500", "System problem", problem);
|
||||
}
|
||||
|
||||
private static void Add(OpenApiOperation operation, string status, string description, OpenApiSchema schema)
|
||||
{
|
||||
if (operation.Responses.ContainsKey(status)) return;
|
||||
operation.Responses[status] = new OpenApiResponse
|
||||
{
|
||||
Description = description,
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
["application/json"] = new() { Schema = schema }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Kbx.Contracts;
|
||||
|
||||
public static class KbxFieldContractSwaggerExtensions
|
||||
{
|
||||
public static void AddKbxFieldContract(this SwaggerGenOptions options)
|
||||
=> options.SchemaFilter<KbxFieldOpenApiSchemaFilter>();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Kbx.Contracts;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public sealed class KbxFieldKeyAttribute(string fieldKey) : Attribute
|
||||
{
|
||||
public string FieldKey { get; } = fieldKey;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Kbx.Contracts.Generated;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Kbx.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Adds non-business KBX field metadata to OpenAPI properties.
|
||||
/// Domain validation rules are intentionally not emitted from the field dictionary.
|
||||
/// </summary>
|
||||
public sealed class KbxFieldOpenApiSchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
foreach (var property in context.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var attribute = property.GetCustomAttribute<KbxFieldKeyAttribute>();
|
||||
if (attribute is null || !KbxFieldCatalog.TryGet(attribute.FieldKey, out var field) || field is null)
|
||||
continue;
|
||||
|
||||
var jsonName = JsonNamingPolicy.CamelCase.ConvertName(property.Name);
|
||||
if (!schema.Properties.TryGetValue(jsonName, out var propertySchema))
|
||||
{
|
||||
var match = schema.Properties.FirstOrDefault(x => string.Equals(x.Key, property.Name, StringComparison.OrdinalIgnoreCase));
|
||||
propertySchema = match.Value;
|
||||
}
|
||||
if (propertySchema is null) continue;
|
||||
|
||||
propertySchema.Extensions["x-kbx-field-key"] = new OpenApiString(field.Key);
|
||||
propertySchema.Extensions["x-kbx-field-label"] = new OpenApiString(field.Label);
|
||||
propertySchema.Extensions["x-kbx-sensitive"] = new OpenApiBoolean(field.Sensitive);
|
||||
if (!string.IsNullOrWhiteSpace(field.Masking))
|
||||
propertySchema.Extensions["x-kbx-masking"] = new OpenApiString(field.Masking);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Shared.Contracts.Generated;
|
||||
using Xunit;
|
||||
|
||||
namespace Shared.Contracts.Tests;
|
||||
|
||||
public sealed class KbxApiCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void Operation_ids_and_routes_are_unique()
|
||||
{
|
||||
Assert.NotEmpty(KbxApiCatalog.SourceSha256);
|
||||
Assert.Equal(KbxApiCatalog.All.Count, KbxApiCatalog.All.Select(x => x.Id).Distinct().Count());
|
||||
Assert.Equal(KbxApiCatalog.All.Count, KbxApiCatalog.All.Select(x => $"{x.Method} {x.Path}").Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Required_idempotency_is_not_used_by_get_queries()
|
||||
{
|
||||
Assert.DoesNotContain(KbxApiCatalog.All, x => x.Method == "GET" && x.Idempotency == "required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Kbx.Contracts.Generated;
|
||||
using Xunit;
|
||||
|
||||
namespace Kbx.Contracts.Tests;
|
||||
|
||||
public sealed class KbxFieldCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void Sensitive_fields_have_masking_metadata()
|
||||
{
|
||||
var invalid = KbxFieldCatalog.All.Where(x => x.Sensitive && string.IsNullOrWhiteSpace(x.Masking)).ToArray();
|
||||
Assert.Empty(invalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Order_quantity_is_canonical()
|
||||
{
|
||||
var field = KbxFieldCatalog.Get(KbxFieldKeys.OrderQty);
|
||||
Assert.Equal("quantity", field.DataType);
|
||||
Assert.True(field.Importable);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user