diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index 0eb2f3141b..9845fc4305 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -8,7 +8,11 @@ * Support for attributes of type VocabularyProperty * Support for attributes of type JsonProperty * Support for the new URL parameter "format" for output formats (normalized, concise, simplified) + * Support for JsonProperty attribute type * Fixed a possible crash for TRoE and attributes of type Vocab/Json/Language + +## Notes + * TRoE is still not prepared for attributes of type Vocab/Json/Language, so, attributes of those types are not stored in the historical database * Modified the @context hosting feature to be according to API spec * Support for URI param 'kind' for GET Contexts * Improved counters for GET Context/Contexts diff --git a/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp b/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp index b583cf29ce..59114d9457 100644 --- a/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp @@ -149,13 +149,13 @@ bool dbModelFromApiAttribute(KjNode* attrP, KjNode* dbAttrsP, KjNode* attrAddedV // // Move special fields back to "attrP" - const char* specialV[] = { "type", "value", "object", "languageMap", "vocab", "datasetId" }; // observedAt+unitCode are mds (db-model) + const char* specialV[] = { "type", "value", "object", "languageMap", "vocab", "json", "datasetId" }; // observedAt+unitCode are mds (db-model) for (unsigned int ix = 0; ix < K_VEC_SIZE(specialV); ix++) { KjNode* nodeP = kjLookup(mdP, specialV[ix]); if (nodeP != NULL) { - if ((ix == 2) || (ix == 3) || (ix == 4)) // "object", "languageMap", "vocab": change name to "value" (Orion's DB model) + if ((ix >= 2) && (ix <= 5)) // "object", "languageMap", "vocab", "json": change name to "value" (Orion's DB model) nodeP->name = (char*) "value"; kjChildRemove(mdP, nodeP); diff --git a/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp b/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp index a316374629..0c88f392e3 100644 --- a/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp @@ -113,6 +113,7 @@ void dbModelToApiAttribute(KjNode* dbAttrP, bool sysAttrs, bool eqsForDots) { if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; + else if (strcmp(typeP->value.s, "JsonProperty") == 0) valueP->name = (char*) "json"; else if (strcmp(typeP->value.s, "VocabularyProperty") == 0) { valueP->name = (char*) "vocab"; @@ -398,6 +399,17 @@ KjNode* dbModelToApiAttribute2(KjNode* dbAttrP, KjNode* datasetP, bool sysAttrs, valueP->name = (char*) "vocab"; attrP = dbAttrP; } + else if (strcmp(attrTypeNodeP->value.s, "JsonProperty") == 0) + { + KjNode* valueP = kjLookup(dbAttrP, "value"); + + // Remove everything except the value, and change its name to "vocab" + dbAttrP->value.firstChildP = valueP; + dbAttrP->lastChild = valueP; + valueP->next = NULL; + valueP->name = (char*) "json"; + attrP = dbAttrP; + } else { // "Steal" the value node and rename it to have the attribute's name instead - that's all that's needed for SIMPLIFIED FORMAT @@ -435,6 +447,8 @@ KjNode* dbModelToApiAttribute2(KjNode* dbAttrP, KjNode* datasetP, bool sysAttrs, { if (attrType == Relationship) nodeP->name = (char*) "object"; + else if (attrType == JsonProperty) + nodeP->name = (char*) "json"; else if (attrType == VocabularyProperty) { nodeP->name = (char*) "vocab"; diff --git a/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp b/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp index 034b9018a1..6a9042ee6e 100644 --- a/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp @@ -85,6 +85,7 @@ void dbModelToApiSubAttribute(KjNode* dbSubAttrP) if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; else if (strcmp(typeP->value.s, "VocabularyProperty") == 0) valueP->name = (char*) "vocab"; + else if (strcmp(typeP->value.s, "JsonProperty") == 0) valueP->name = (char*) "json"; } } @@ -144,6 +145,7 @@ KjNode* dbModelToApiSubAttribute2(KjNode* dbSubAttributeP, bool sysAttrs, Orionl if (subAttrType == Relationship) nodeP->name = (char*) "object"; else if (subAttrType == LanguageProperty) nodeP->name = (char*) "languageMap"; else if (subAttrType == VocabularyProperty) nodeP->name = (char*) "vocab"; + else if (subAttrType == JsonProperty) nodeP->name = (char*) "json"; kjChildAdd(subAttrP, nodeP); } diff --git a/src/lib/orionld/payloadCheck/pCheckAttribute.cpp b/src/lib/orionld/payloadCheck/pCheckAttribute.cpp index e29561f0a8..fd5dd92fcc 100644 --- a/src/lib/orionld/payloadCheck/pCheckAttribute.cpp +++ b/src/lib/orionld/payloadCheck/pCheckAttribute.cpp @@ -71,6 +71,7 @@ static const char* attrTypeChangeTitle(OrionldAttributeType oldType, OrionldAttr if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Property"; if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Property"; if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a Property"; + if (oldType == JsonProperty) return "Attempt to transform a JsonProperty into a Property"; } else if (newType == Relationship) { @@ -78,6 +79,7 @@ static const char* attrTypeChangeTitle(OrionldAttributeType oldType, OrionldAttr if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Relationship"; if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Relationship"; if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a Relationship"; + if (oldType == JsonProperty) return "Attempt to transform a JsonProperty into a Relationship"; } else if (newType == GeoProperty) { @@ -85,6 +87,7 @@ static const char* attrTypeChangeTitle(OrionldAttributeType oldType, OrionldAttr if (oldType == Relationship) return "Attempt to transform a Relationship into a GeoProperty"; if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a GeoProperty"; if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a GeoProperty"; + if (oldType == JsonProperty) return "Attempt to transform a JsonProperty into a GeoProperty"; } else if (newType == LanguageProperty) { @@ -92,13 +95,23 @@ static const char* attrTypeChangeTitle(OrionldAttributeType oldType, OrionldAttr if (oldType == Relationship) return "Attempt to transform a Relationship into a LanguageProperty"; if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a LanguageProperty"; if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a LanguageProperty"; + if (oldType == JsonProperty) return "Attempt to transform a JsonProperty into a LanguageProperty"; } else if (newType == VocabularyProperty) { if (oldType == Property) return "Attempt to transform a Property into a VocabularyProperty"; if (oldType == Relationship) return "Attempt to transform a Relationship into a VocabularyProperty"; if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a VocabularyProperty"; - if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a GeoProperty"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a VocabularyProperty"; + if (oldType == JsonProperty) return "Attempt to transform a JsonProperty into a VocabularyProperty"; + } + else if (newType == JsonProperty) + { + if (oldType == Property) return "Attempt to transform a Property into a JsonProperty"; + if (oldType == Relationship) return "Attempt to transform a Relationship into a JsonProperty"; + if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a JsonProperty"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a JsonProperty"; + if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a JsonProperty"; } return "Attribute type inconsistency"; @@ -413,6 +426,7 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a KjNode* objectP = kjLookup(attrP, "object"); KjNode* languageMapP = kjLookup(attrP, "languageMap"); KjNode* vocabP = kjLookup(attrP, "vocab"); + KjNode* jsonP = kjLookup(attrP, "json"); if (attributeType == Property) { @@ -431,6 +445,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a Property: vocab", attrP->name, 400); return false; } + else if (jsonP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a Property: json", attrP->name, 400); + return false; + } else if ((valueP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /value/ field for Property at creation time", attrP->name, 400); @@ -454,6 +473,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a GeoProperty: vocab", attrP->name, 400); return false; } + else if (jsonP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a GeoProperty: json", attrP->name, 400); + return false; + } else if ((valueP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /value/ field for GeoProperty at creation time", attrP->name, 400); @@ -477,6 +501,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a Relationship: vocab", attrP->name, 400); return false; } + else if (jsonP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a Relationship: json", attrP->name, 400); + return false; + } else if ((objectP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /object/ field for Relationship at creation time", attrP->name, 400); @@ -500,6 +529,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a LanguageProperty: vocab", attrP->name, 400); return false; } + else if (jsonP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a LanguageProperty: json", attrP->name, 400); + return false; + } else if ((languageMapP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /languageMap/ field for LanguageProperty at creation time", attrP->name, 400); @@ -523,12 +557,45 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a VocabularyProperty: languageMap", attrP->name, 400); return false; } + else if (jsonP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a VocabularyProperty: json", attrP->name, 400); + return false; + } else if ((vocabP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /vocab/ field for VocabularyProperty at creation time", attrP->name, 400); return false; } } + else if (attributeType == JsonProperty) + { + if (valueP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a JsonProperty: value", attrP->name, 400); + return false; + } + else if (objectP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a JsonProperty: object", attrP->name, 400); + return false; + } + else if (languageMapP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a JsonProperty: languageMap", attrP->name, 400); + return false; + } + else if (vocabP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a JsonProperty: vocab", attrP->name, 400); + return false; + } + else if ((jsonP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing + { + orionldError(OrionldBadRequestData, "Missing /json/ field for JsonProperty at creation time", attrP->name, 400); + return false; + } + } return true; } @@ -837,6 +904,15 @@ bool deletionWithTypePresent(KjNode* attrP, KjNode* typeP) } } } + else if (strcmp(typeP->value.s, "JsonProperty") == 0) + { + valueP = kjLookup(attrP, "json"); + if ((valueP != NULL) && (valueP->type == KjString) && (strcmp(valueP->value.s, "urn:ngsi-ld:null") == 0)) + { + attrP->type = KjNull; + return true; + } + } return false; } @@ -854,7 +930,8 @@ static bool deletionWithoutTypePresent KjNode* valueP, KjNode* objectP, KjNode* languageMapP, - KjNode* vocabP + KjNode* vocabP, + KjNode* jsonP ) { if ((attributeType == Property) || (attributeType == GeoProperty)) @@ -893,6 +970,14 @@ static bool deletionWithoutTypePresent } } } + else if (attributeType == JsonProperty) + { + if ((jsonP != NULL) && (jsonP->type == KjString) && (strcmp(jsonP->value.s, "urn:ngsi-ld:null") == 0)) + { + attrP->type = KjNull; + return true; + } + } return false; } @@ -1057,6 +1142,7 @@ static bool pCheckAttributeObject KjNode* objectP = kjLookup(attrP, "object"); KjNode* languageMapP = kjLookup(attrP, "languageMap"); KjNode* vocabP = kjLookup(attrP, "vocab"); + KjNode* jsonP = kjLookup(attrP, "json"); if (valueP != NULL) { @@ -1080,6 +1166,11 @@ static bool pCheckAttributeObject attributeType = VocabularyProperty; arrayReduce(vocabP); } + else if (jsonP != NULL) + { + attributeType = JsonProperty; + arrayReduce(jsonP); + } else { // If new attribute and no value field at all - error @@ -1110,7 +1201,7 @@ static bool pCheckAttributeObject // if ((orionldState.serviceP->options & ORIONLD_SERVICE_OPTION_ACCEPT_JSONLD_NULL) != 0) { - if (deletionWithoutTypePresent(attrP, attributeType, valueP, objectP, languageMapP, vocabP) == true) + if (deletionWithoutTypePresent(attrP, attributeType, valueP, objectP, languageMapP, vocabP, jsonP) == true) return true; } } @@ -1157,7 +1248,7 @@ static bool pCheckAttributeObject fieldP->value = fieldP->value.firstChildP->value; } } - if ((attributeType == Relationship) || (attributeType == LanguageProperty) || (attributeType == VocabularyProperty)) + if ((attributeType == Relationship) || (attributeType == LanguageProperty) || (attributeType == VocabularyProperty) || (attributeType == JsonProperty)) { orionldError(OrionldBadRequestData, "Invalid member /value/", "valid for Property/GeoProperty attributes only", 400); return false; @@ -1193,6 +1284,9 @@ static bool pCheckAttributeObject if (pCheckVocabulary(fieldP, attrP->name) == false) return false; } + else if ((attributeType == JsonProperty) && (strcmp(fieldP->name, "json") == 0)) + { + } else if (strcmp(fieldP->name, "observedAt") == 0) { // @@ -1395,6 +1489,12 @@ static bool validAttrName(const char* attrName, bool isAttribute) // - observedAt // - datasetId (sub-attributes don't have datasetId) // +// - Attributes of type JsonProperty can have the following special attributes: +// - type +// - json +// - observedAt +// - datasetId (sub-attributes don't have datasetId) +// bool pCheckAttribute ( const char* entityId, diff --git a/src/lib/orionld/troe/pgAttributeAppend.cpp b/src/lib/orionld/troe/pgAttributeAppend.cpp index 2d27eb041a..ef5734bb07 100644 --- a/src/lib/orionld/troe/pgAttributeAppend.cpp +++ b/src/lib/orionld/troe/pgAttributeAppend.cpp @@ -216,7 +216,7 @@ void pgAttributeAppend comma, instanceId, attributeName, opMode, entityId, observedAt, hasSubProperties, unitCode, datasetId, coordsString, orionldState.requestTimeString); } } - else // Property + else // Property OR JsonProperty { if (valueNodeP->type == KjString) { @@ -242,6 +242,12 @@ void pgAttributeAppend } else if ((valueNodeP->type == KjArray) || (valueNodeP->type == KjObject)) { + if (strcmp(type, "JsonProperty") == 0) + { + LM_W(("TRoE for Compound JsonProperty still to be implemented")); + return; + } + // WARNING: If an attribute is HUGE, it may not have room enough in a buffer allocated by kaAlloc (there's a max-size) int renderedValueSize = kjFastRenderSize(valueNodeP); char* renderedValue = kaAlloc(&orionldState.kalloc, renderedValueSize); diff --git a/src/lib/orionld/troe/pgAttributeBuild.cpp b/src/lib/orionld/troe/pgAttributeBuild.cpp index 0ca2f69c04..f403e1cbce 100644 --- a/src/lib/orionld/troe/pgAttributeBuild.cpp +++ b/src/lib/orionld/troe/pgAttributeBuild.cpp @@ -83,6 +83,7 @@ bool pgAttributeBuild else if (strcmp(nodeP->name, "type") == 0) type = nodeP->value.s; else if (strcmp(nodeP->name, "datasetId") == 0) datasetId = nodeP->value.s; else if (strcmp(nodeP->name, "value") == 0) valueNodeP = nodeP; + else if (strcmp(nodeP->name, "json") == 0) valueNodeP = nodeP; else if (strcmp(nodeP->name, "object") == 0) { if (nodeP->type == KjString) diff --git a/src/lib/orionld/types/OrionldAttributeType.cpp b/src/lib/orionld/types/OrionldAttributeType.cpp index ae22d51e6e..cb6f9f2811 100644 --- a/src/lib/orionld/types/OrionldAttributeType.cpp +++ b/src/lib/orionld/types/OrionldAttributeType.cpp @@ -44,6 +44,7 @@ const char* orionldAttributeTypeName(OrionldAttributeType attributeType) case GeoProperty: return "GeoProperty"; case LanguageProperty: return "LanguageProperty"; case VocabularyProperty: return "VocabularyProperty"; + case JsonProperty: return "JsonProperty"; } return "InvalidAttributeType"; @@ -62,6 +63,7 @@ OrionldAttributeType orionldAttributeType(const char* typeString) else if (strcmp(typeString, "GeoProperty") == 0) return GeoProperty; else if (strcmp(typeString, "LanguageProperty") == 0) return LanguageProperty; else if (strcmp(typeString, "VocabularyProperty") == 0) return VocabularyProperty; + else if (strcmp(typeString, "JsonProperty") == 0) return JsonProperty; return NoAttributeType; } diff --git a/src/lib/orionld/types/OrionldAttributeType.h b/src/lib/orionld/types/OrionldAttributeType.h index 8141fc4e02..1a5419546b 100644 --- a/src/lib/orionld/types/OrionldAttributeType.h +++ b/src/lib/orionld/types/OrionldAttributeType.h @@ -39,7 +39,8 @@ typedef enum OrionldAttributeType Relationship, GeoProperty, LanguageProperty, - VocabularyProperty + VocabularyProperty, + JsonProperty } OrionldAttributeType; diff --git a/test/functionalTest/cases/0000_ngsild/ngsild_json-property.test b/test/functionalTest/cases/0000_ngsild/ngsild_json-property.test new file mode 100644 index 0000000000..994c986563 --- /dev/null +++ b/test/functionalTest/cases/0000_ngsild/ngsild_json-property.test @@ -0,0 +1,290 @@ +# Copyright 2024 FIWARE Foundation e.V. +# +# This file is part of Orion-LD Context Broker. +# +# Orion-LD Context Broker is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Orion-LD Context Broker is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Orion-LD Context Broker. If not, see http://www.gnu.org/licenses/. +# +# For those usages not covered by this license please contact with +# orionld at fiware dot org + +# VALGRIND_READY - to mark the test ready for valgrindTestSuite.sh + +--NAME-- +VocabularyProperty + +--SHELL-INIT-- +dbInit CB + +orionldStart CB -experimental +accumulatorStart --pretty-print + +--SHELL-- + +# +# 01. Create a subscription on entity type T +# 02. Create an entity urn:E1, type T, with a JsonProperty V1: "abc" +# 03. Create an entity urn:E2, type T, with a JsonProperty V2: [ "vocab", "id" ] +# 04. GET urn:E1 to see V1 +# 05. GET urn:E2 to see V2 +# 06. Dump/Reset accumulator, see urn:E1+V1 and urn:E2+V2 +# +# 07. GET urn:E2 in simplified format +# + +echo '01. Create a subscription on entity type T' +echo '==========================================' +payload='{ + "id": "urn:ngsi-ld:subs:S1", + "type": "Subscription", + "entities": [ + { + "type": "T" + } + ], + "notification": { + "endpoint": { + "uri": "http://127.0.0.1:'${LISTENER_PORT}'/notify" + }, + "showChanges": true + } +}' +orionCurl --url /ngsi-ld/v1/subscriptions --payload "$payload" +echo +echo + + +echo '02. Create an entity urn:E1, type T, with a JsonProperty V1: "abc"' +echo '========================================================================' +payload='{ + "id": "urn:E1", + "type": "T", + "V1": { + "json": "abc", + "type": "JsonProperty" + } +}' +orionCurl --url /ngsi-ld/v1/entities --payload "$payload" +echo +echo + + +echo '03. Create an entity urn:E2, type T, with a JsonProperty V2: [ "json", "id" ]' +echo '====================================================================================' +payload='{ + "id": "urn:E2", + "type": "T", + "V2": { + "json": [ "json", "id" ], + "type": "JsonProperty" + }, + "V3": { + "json": [ "json" ] + } +}' +orionCurl --url /ngsi-ld/v1/entities --payload "$payload" +echo +echo + + +echo '04. GET urn:E1 to see V1' +echo '========================' +orionCurl --url /ngsi-ld/v1/entities/urn:E1 +echo +echo + + +echo '05. GET urn:E2 to see V2' +echo '========================' +orionCurl --url /ngsi-ld/v1/entities/urn:E2 +echo +echo + + +echo "06. Dump/Reset accumulator, see urn:E1+V1 and urn:E2+V2" +echo "=======================================================" +sleep .2 +accumulatorDump +accumulatorReset +echo +echo + + +echo "07. GET urn:E2 in simplified format" +echo "===================================" +orionCurl --url /ngsi-ld/v1/entities/urn:E2?options=simplified +echo +echo + + +--REGEXPECT-- +01. Create a subscription on entity type T +========================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/subscriptions/urn:ngsi-ld:subs:S1 + + + +02. Create an entity urn:E1, type T, with a JsonProperty V1: "abc" +======================================================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/entities/urn:E1 + + + +03. Create an entity urn:E2, type T, with a JsonProperty V2: [ "json", "id" ] +==================================================================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/entities/urn:E2 + + + +04. GET urn:E1 to see V1 +======================== +HTTP/1.1 200 OK +Content-Length: 68 +Content-Type: application/json +Date: REGEX(.*) +Link: