From 1c88ee396bb17a08d2b4221c8ab11311a25d3eb4 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 16 Sep 2024 12:55:24 -0500 Subject: [PATCH] chore!: improve decoding of required fields (#254) A required field, confusingly, should probably be marked *optional* in the protobuf. That way, we can throw errors when decoding. See [#252] for more details. This change updates how we handle required fields across the schema. I tested this end-to-end, going "all the way" to comapeo-mobile. [#252]: https://github.com/digidem/mapeo-schema/issues/252 Co-authored-by: Gregor MacLennan --- proto/deviceInfo/v1.proto | 2 +- proto/field/v1.proto | 2 +- proto/icon/v1.proto | 2 +- proto/observation/v1.proto | 4 +- proto/preset/v1.proto | 2 +- proto/role/v1.proto | 4 +- proto/translation/v1.proto | 6 +- schema/common/v1.json | 12 +- schema/deviceInfo/v1.json | 2 +- schema/field/v1.json | 9 +- schema/icon/v1.json | 6 +- schema/observation/v1.json | 15 ++- schema/preset/v1.json | 15 ++- schema/role/v1.json | 3 +- schema/track/v1.json | 6 +- schema/translation/v1.json | 18 ++- src/lib/decode-conversions.ts | 208 +++++++++++++++++------------ src/lib/encode-conversions.ts | 4 +- test/fixtures/good-docs-minimal.js | 3 +- 19 files changed, 192 insertions(+), 131 deletions(-) diff --git a/proto/deviceInfo/v1.proto b/proto/deviceInfo/v1.proto index 37371da..900110b 100644 --- a/proto/deviceInfo/v1.proto +++ b/proto/deviceInfo/v1.proto @@ -22,5 +22,5 @@ message DeviceInfo_1 { } string name = 5; - optional DeviceType deviceType = 6; + DeviceType deviceType = 6; } diff --git a/proto/field/v1.proto b/proto/field/v1.proto index 9792ac8..9905534 100644 --- a/proto/field/v1.proto +++ b/proto/field/v1.proto @@ -22,7 +22,7 @@ message Field_1 { selectOne = 3; selectMultiple = 4; } - Type type = 6 [(required) = true]; + Type type = 6; string label = 7 [(required) = true]; enum Appearance { appearance_unspecified = 0; diff --git a/proto/icon/v1.proto b/proto/icon/v1.proto index c6acfae..fa90412 100644 --- a/proto/icon/v1.proto +++ b/proto/icon/v1.proto @@ -10,7 +10,7 @@ message Icon_1 { option (schemaName) = "icon"; Common_1 common = 1; - string name = 2 [(required) = true]; + string name = 2; message IconVariantSvg {} diff --git a/proto/observation/v1.proto b/proto/observation/v1.proto index 447d891..2bdbfb2 100644 --- a/proto/observation/v1.proto +++ b/proto/observation/v1.proto @@ -26,8 +26,8 @@ message Observation_1 { audio = 3; } message Attachment { - bytes driveDiscoveryId = 1; - string name = 2; + bytes driveDiscoveryId = 1 [(required) = true]; + string name = 2 [(required) = true]; AttachmentType type = 3; bytes hash = 4; } diff --git a/proto/preset/v1.proto b/proto/preset/v1.proto index dbeb7ad..d647c89 100644 --- a/proto/preset/v1.proto +++ b/proto/preset/v1.proto @@ -23,7 +23,7 @@ message Preset_1 { repeated FieldRef fieldRefs = 10; optional IconRef iconRef = 11; repeated string terms = 12; - string color = 13; + optional string color = 13; enum Geometry { geometry_unspecified = 0; diff --git a/proto/role/v1.proto b/proto/role/v1.proto index d382cb6..440013b 100644 --- a/proto/role/v1.proto +++ b/proto/role/v1.proto @@ -13,6 +13,6 @@ message Role_1 { Common_1 common = 1; - bytes roleId = 5; - uint32 fromIndex = 6; + bytes roleId = 5 [(required) = true]; + optional uint32 fromIndex = 6 [(required) = true]; } diff --git a/proto/translation/v1.proto b/proto/translation/v1.proto index 0d6601e..cd82e0f 100644 --- a/proto/translation/v1.proto +++ b/proto/translation/v1.proto @@ -13,9 +13,9 @@ message Translation_1 { Common_1 common = 1; DocRef docRef = 2; - string propertyRef = 3; - string languageCode = 4; - string regionCode = 5; + string propertyRef = 3 [(required) = true]; + string languageCode = 4 [(required) = true]; + optional string regionCode = 5; string message = 6; message DocRef { diff --git a/schema/common/v1.json b/schema/common/v1.json index 213d246..52da759 100644 --- a/schema/common/v1.json +++ b/schema/common/v1.json @@ -7,19 +7,23 @@ "properties": { "docId": { "description": "Hex-encoded 32-byte buffer", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 }, "originalVersionId": { "description": "Version ID of the original version of this document. For the original version, matches `versionId`.", - "type": "string" + "type": "string", + "minLength": 1 }, "schemaName": { "description": "Name of Mapeo data type / schema", - "type": "string" + "type": "string", + "minLength": 1 }, "createdAt": { "description": "RFC3339-formatted datetime of when the first version of the element was created", diff --git a/schema/deviceInfo/v1.json b/schema/deviceInfo/v1.json index 90bbe56..c591ba6 100644 --- a/schema/deviceInfo/v1.json +++ b/schema/deviceInfo/v1.json @@ -25,6 +25,6 @@ "description": "Type of device" } }, - "required": ["schemaName", "name"], + "required": ["schemaName", "name", "deviceType"], "additionalProperties": false } diff --git a/schema/field/v1.json b/schema/field/v1.json index 0675591..7c8f762 100644 --- a/schema/field/v1.json +++ b/schema/field/v1.json @@ -12,7 +12,8 @@ }, "tagKey": { "description": "They key in a tags object that this field applies to", - "type": "string" + "type": "string", + "minLength": 1 }, "type": { "description": "Type of field - defines how the field is displayed to the user.", @@ -35,7 +36,8 @@ }, "label": { "description": "Default language label for the form field label", - "type": "string" + "type": "string", + "minLength": 1 }, "appearance": { "description": "For text fields, display as a single-line or multi-line field", @@ -66,7 +68,8 @@ "type": "object", "properties": { "label": { - "type": "string" + "type": "string", + "minLength": 1 }, "value": { "anyOf": [ diff --git a/schema/icon/v1.json b/schema/icon/v1.json index 79f6b41..b8cb028 100644 --- a/schema/icon/v1.json +++ b/schema/icon/v1.json @@ -11,7 +11,8 @@ }, "blobVersionId": { "description": "Version id of the icon blob. Each id is id (hex-encoded 32 byte buffer) and index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "properties": { @@ -21,7 +22,8 @@ "const": "icon" }, "name": { - "type": "string" + "type": "string", + "minLength": 1 }, "variants": { "type": "array", diff --git a/schema/observation/v1.json b/schema/observation/v1.json index fe29cb3..41d89e0 100644 --- a/schema/observation/v1.json +++ b/schema/observation/v1.json @@ -77,11 +77,13 @@ "properties": { "driveDiscoveryId": { "type": "string", - "description": "core discovery id for the drive that the attachment belongs to" + "description": "core discovery id for the drive that the attachment belongs to", + "minLength": 1 }, "name": { "type": "string", - "description": "name of the attachment" + "description": "name of the attachment", + "minLength": 1 }, "type": { "type": "string", @@ -99,7 +101,8 @@ }, "hash": { "type": "string", - "description": "SHA256 hash of the attachment" + "description": "SHA256 hash of the attachment", + "minLength": 1 } }, "required": ["driveDiscoveryId", "name", "type", "hash"] @@ -192,11 +195,13 @@ "properties": { "docId": { "description": "hex-encoded id of the element that this observation references", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "required": ["docId", "versionId"] diff --git a/schema/preset/v1.json b/schema/preset/v1.json index 401cf8f..b3966fd 100644 --- a/schema/preset/v1.json +++ b/schema/preset/v1.json @@ -84,11 +84,13 @@ "properties": { "docId": { "description": "hex-encoded id of the element that this observation references", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "required": ["docId", "versionId"] @@ -100,11 +102,13 @@ "properties": { "docId": { "description": "hex-encoded id of the element that this observation references", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "required": ["docId", "versionId"] @@ -130,8 +134,7 @@ "removeTags", "fieldRefs", "schemaName", - "terms", - "color" + "terms" ], "additionalProperties": false } diff --git a/schema/role/v1.json b/schema/role/v1.json index ab9e4a8..3d6ce89 100644 --- a/schema/role/v1.json +++ b/schema/role/v1.json @@ -10,7 +10,8 @@ }, "roleId": { "type": "string", - "description": "Unique identifier for role assigned to device with auth core ID equal to `docId` of this record" + "description": "Unique identifier for role assigned to device with auth core ID equal to `docId` of this record", + "minLength": 1 }, "fromIndex": { "type": "integer", diff --git a/schema/track/v1.json b/schema/track/v1.json index e17f862..836feb4 100644 --- a/schema/track/v1.json +++ b/schema/track/v1.json @@ -69,11 +69,13 @@ "properties": { "docId": { "description": "hex-encoded id of the element that this track references", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "required": ["docId", "versionId"] diff --git a/schema/translation/v1.json b/schema/translation/v1.json index b25d456..0c875f3 100644 --- a/schema/translation/v1.json +++ b/schema/translation/v1.json @@ -14,11 +14,13 @@ "properties": { "docId": { "description": "hex-encoded id of the element that this observation references", - "type": "string" + "type": "string", + "minLength": 1 }, "versionId": { "description": "core discovery id (hex-encoded 32-byte buffer) and core index number, separated by '/'", - "type": "string" + "type": "string", + "minLength": 1 } }, "required": ["docId", "versionId"] @@ -40,15 +42,20 @@ }, "propertyRef": { "type": "string", - "description": "identifier for translated field/property in dot-prop notation" + "description": "identifier for translated field/property in dot-prop notation", + "minLength": 1 }, "languageCode": { "type": "string", - "description": "three-letter ISO 169-3 language code" + "description": "three-letter ISO 169-3 language code", + "minLength": 1, + "maxLength": 3 }, "regionCode": { "type": "string", - "description": "two-letter country code from ISO 3166-1 alpha-2 or a three-digit code from UN M.49 for geographical regions" + "description": "two-letter country code from ISO 3166-1 alpha-2 or a three-digit code from UN M.49 for geographical regions", + "minLength": 2, + "maxLength": 3 }, "message": { "type": "string", @@ -61,7 +68,6 @@ "docRefType", "propertyRef", "languageCode", - "regionCode", "message" ], "additionalProperties": false diff --git a/src/lib/decode-conversions.ts b/src/lib/decode-conversions.ts index 3fca2d4..601d496 100644 --- a/src/lib/decode-conversions.ts +++ b/src/lib/decode-conversions.ts @@ -20,7 +20,12 @@ import { type JsonTagValue, type MapeoDocDecode, } from '../types.js' -import type { Icon, Observation, Track } from '../index.js' +import { + type Icon, + type Observation, + type Track, + valueSchemas, +} from '../index.js' import type { Observation_1_Attachment, Observation_1_Metadata, @@ -58,10 +63,12 @@ export const convertProjectSettings: ConvertFunction<'projectSettings'> = ( ) => { const { common, schemaVersion, defaultPresets, ...rest } = message const jsonSchemaCommon = convertCommon(common, versionObj) - let configMetadata + + let configMetadata: undefined | ProjectSettings['configMetadata'] if (rest.configMetadata) { configMetadata = convertConfigMetadata(rest.configMetadata) } + return { ...jsonSchemaCommon, ...rest, @@ -81,13 +88,15 @@ export const convertProjectSettings: ConvertFunction<'projectSettings'> = ( function convertConfigMetadata( configMetadata: ProjectSettings_1_ConfigMetadata ): ProjectSettings['configMetadata'] { - if (!configMetadata?.importDate) { - throw new Error('Missing required property configMetadata.importDate') - } - if (!configMetadata?.buildDate) { - throw new Error('Missing required property configMetadata.buildDate') - } - return configMetadata as ProjectSettings['configMetadata'] + // TODO: Consider moving this default to the frontend. + const defaultDate = new Date(0).toISOString() + const { + name, + importDate = defaultDate, + buildDate = defaultDate, + fileVersion, + } = configMetadata + return { name, importDate, buildDate, fileVersion } } export const convertObservation: ConvertFunction<'observation'> = ( @@ -99,19 +108,27 @@ export const convertObservation: ConvertFunction<'observation'> = ( let presetRef if (rest.presetRef) { - if (!rest.presetRef.versionId) - throw new Error('found presetRef on observation but is missing versionId') - + ensure(rest.presetRef.versionId, 'observation.presetRef', 'versionId') presetRef = { docId: rest.presetRef.docId.toString('hex'), versionId: getVersionId(rest.presetRef.versionId), } } + const attachments: Observation['attachments'] = [] + for (const attachment of message.attachments) { + try { + const converted = convertAttachment(attachment) + attachments.push(converted) + } catch (_err) { + // TODO: Log something here. + } + } + const obs: Observation = { ...jsonSchemaCommon, ...rest, - attachments: message.attachments.map(convertAttachment), + attachments, tags: convertTags(message.tags), metadata: metadata ? removeInvalidPositionMetadata(metadata) : {}, presetRef, @@ -122,32 +139,29 @@ export const convertObservation: ConvertFunction<'observation'> = ( type FieldOptions = FilterBySchemaName['options'] export const convertField: ConvertFunction<'field'> = (message, versionObj) => { - const { common, schemaVersion, ...rest } = message + const { common, schemaVersion, tagKey, type, label, ...rest } = message const jsonSchemaCommon = convertCommon(common, versionObj) - if (!message.tagKey) { - // We can't do anything with a field without a tag key, so we ignore these - throw new Error('Missing tagKey on field') - } else { - return { - ...jsonSchemaCommon, - ...rest, - tagKey: message.tagKey, - label: message.label || message.tagKey, - options: - message.options.length > 0 - ? message.options.reduce>( - (acc, { label, value }) => { - // Filter out any options where value is undefined (this would still be valid protobuf, but not valid for our code) - if (!value) return acc - const convertedValue = convertTagPrimitive(value) - if (typeof convertedValue === 'undefined') return acc - acc.push({ label, value: convertedValue }) - return acc - }, - [] - ) - : undefined, - } + ensure(tagKey, 'field', 'tagKey') + return { + ...jsonSchemaCommon, + ...rest, + tagKey: tagKey, + label: label || tagKey, + type, + options: + message.options.length > 0 + ? message.options.reduce>( + (acc, { label, value }) => { + // Filter out any options where value is undefined (this would still be valid protobuf, but not valid for our code) + if (!value) return acc + const convertedValue = convertTagPrimitive(value) + if (typeof convertedValue === 'undefined') return acc + acc.push({ label, value: convertedValue }) + return acc + }, + [] + ) + : undefined, } } @@ -162,6 +176,7 @@ export const convertPreset: ConvertFunction<'preset'> = ( ) => { const { common, schemaVersion, ...rest } = message const jsonSchemaCommon = convertCommon(common, versionObj) + const geometry = rest.geometry.filter( (geomType): geomType is JsonSchemaPresetGeomItem => geomType !== 'UNRECOGNIZED' @@ -169,9 +184,7 @@ export const convertPreset: ConvertFunction<'preset'> = ( let iconRef if (rest.iconRef) { - // iconRef is not required property on the schema, but if it does exist, then a versionId must be present - if (!rest.iconRef.versionId) - throw new Error('found iconRef on preset but is missing versionId') + ensure(rest.iconRef.versionId, 'preset.iconRef', 'versionId') iconRef = { docId: rest.iconRef.docId.toString('hex'), versionId: getVersionId(rest.iconRef.versionId), @@ -197,15 +210,15 @@ export const convertPreset: ConvertFunction<'preset'> = ( } export const convertRole: ConvertFunction<'role'> = (message, versionObj) => { - if (message.roleId.length === 0) { - throw new Error('Invalid roleId') - } - const { common, schemaVersion, ...rest } = message + const { common, schemaVersion, fromIndex, roleId, ...rest } = message + ensure(roleId.length, 'role', 'roleId') + ensure(typeof fromIndex === 'number', 'role', 'fromIndex') const jsonSchemaCommon = convertCommon(common, versionObj) return { ...jsonSchemaCommon, ...rest, - roleId: message.roleId.toString('hex'), + roleId: roleId.toString('hex'), + fromIndex, } } @@ -213,11 +226,12 @@ export const convertDeviceInfo: ConvertFunction<'deviceInfo'> = ( message, versionObj ) => { - const { common, schemaVersion, ...rest } = message + const { common, schemaVersion, name, ...rest } = message const jsonSchemaCommon = convertCommon(common, versionObj) return { ...jsonSchemaCommon, ...rest, + name, } } @@ -225,9 +239,6 @@ export const convertCoreOwnership: ConvertFunction<'coreOwnership'> = ( message, versionObj ) => { - if (!message.coreSignatures) { - throw new Error('Invalid message: missing core signatures') - } const { common, schemaVersion, @@ -260,13 +271,16 @@ export const convertCoreOwnership: ConvertFunction<'coreOwnership'> = ( export const convertIcon: ConvertFunction<'icon'> = (message, versionObj) => { const { common, schemaVersion, ...rest } = message - const jsonSchemaCommon = convertCommon(common, versionObj) const variants: Icon['variants'] = [] for (const variant of message.variants) { - const converted = convertIconVariant(variant) - if (converted) variants.push(converted) + try { + const converted = convertIconVariant(variant) + variants.push(converted) + } catch (_err) { + // TODO: Log something here. + } } return { ...jsonSchemaCommon, ...rest, variants } @@ -276,14 +290,25 @@ export const convertTranslation: ConvertFunction<'translation'> = ( message, versionObj ) => { - const { common, schemaVersion, ...rest } = message + const { + common, + schemaVersion, + propertyRef, + languageCode, + regionCode, + ...rest + } = message const jsonSchemaCommon = convertCommon(common, versionObj) - if (!message.docRef) throw new Error('missing docRef for translation') - if (!message.docRef.versionId) - throw new Error('missing docRef.versionId for translation') + ensure(message.docRef, 'translation', 'docRef') + ensure(message.docRef.versionId, 'translation.docRef', 'versionId') + ensure(propertyRef, 'translation', 'propertyRef') + ensure(languageCode, 'translation', 'languageCode') return { ...jsonSchemaCommon, ...rest, + propertyRef, + languageCode, + regionCode, docRef: { docId: message.docRef.docId.toString('hex'), versionId: getVersionId(message.docRef.versionId), @@ -295,16 +320,20 @@ export const convertTrack: ConvertFunction<'track'> = (message, versionObj) => { const { common, schemaVersion, ...rest } = message const jsonSchemaCommon = convertCommon(common, versionObj) const locations = message.locations.map(convertTrackPosition) - const observationRefs = message.observationRefs.map( - ({ docId, versionId }) => { - if (!versionId) - throw new Error('missing observationRef.versionId from track') - return { + + const observationRefs: Track['observationRefs'] = [] + for (const { docId, versionId } of message.observationRefs) { + try { + ensure(versionId, 'track.observationRefs[]', 'versionId') + observationRefs.push({ docId: docId.toString('hex'), versionId: getVersionId(versionId), - } + }) + } catch (_err) { + // TODO: Log something here. } - ) + } + return { ...jsonSchemaCommon, ...rest, @@ -316,16 +345,21 @@ export const convertTrack: ConvertFunction<'track'> = (message, versionObj) => { function convertIconVariant( variant: Icon_1_IconVariant -): null | Icon['variants'][number] { +): Icon['variants'][number] { switch (variant.variant?.$case) { case 'pngIcon': { const { pixelDensity } = variant.variant.pngIcon + ensure( + pixelDensity !== 'pixel_density_unspecified', + 'icon.variants[].pngIcon', + 'pixelDensity' + ) return convertIconVariantPng({ ...variant, pixelDensity }) } case 'svgIcon': return convertIconVariantSvg(variant) case undefined: - return null + throw new Error('Cannot decode this icon variant') default: throw new ExhaustivenessError(variant.variant) } @@ -335,11 +369,9 @@ function convertIconVariantPng( variant: Icon_1_IconVariant & { pixelDensity: Icon_1_IconVariantPng_PixelDensity } -) { +): Icon['variants'][number] { const { blobVersionId, size, pixelDensity } = variant - if (!blobVersionId) { - throw new Error('Missing required property `blobVersionId`') - } + ensure(blobVersionId, 'icon.variants[]', 'blobVersionId') return { blobVersionId: getVersionId(blobVersionId), mimeType: 'image/png' as const, @@ -348,11 +380,11 @@ function convertIconVariantPng( } } -function convertIconVariantSvg(variant: Icon_1_IconVariant) { +function convertIconVariantSvg( + variant: Icon_1_IconVariant +): Icon['variants'][number] { const { blobVersionId, size } = variant - if (!blobVersionId) { - throw new Error('Missing required property `blobVersionId`') - } + ensure(blobVersionId, 'icon.variants[]', 'blobVersionId') return { blobVersionId: getVersionId(blobVersionId), mimeType: 'image/svg+xml' as const, @@ -435,7 +467,12 @@ function convertCommon( common: ProtoTypesWithSchemaInfo['common'], versionObj: VersionIdObject ): Omit { - if (!common || !common.docId || !common.createdAt || !common.updatedAt) { + if ( + !common || + !common.docId.byteLength || + !common.createdAt || + !common.updatedAt + ) { throw new Error('Missing required common properties') } @@ -467,6 +504,12 @@ function convertAttachment({ type, hash, }: Observation_1_Attachment): Observation['attachments'][number] { + ensure( + driveDiscoveryId.byteLength, + 'observation.attachments[]', + 'driveDiscoveryId' + ) + ensure(name, 'observation.attachments[]', 'name') return { driveDiscoveryId: driveDiscoveryId.toString('hex'), name, @@ -478,17 +521,10 @@ function convertAttachment({ function convertTrackPosition( position: Track_1_Position ): Track['locations'][number] { - if (!position.timestamp) { - throw new Error('Missing required property `timestamp`') - } - if (!position.coords) { - throw new Error('Missing required property `coords`') - } - return { - ...position, - coords: position.coords, - timestamp: position.timestamp, - } + const { timestamp, coords, ...rest } = position + ensure(timestamp, 'track.locations.position[]', 'timestamp') + ensure(coords, 'track.locations.position[]', 'coords') + return { coords, timestamp, ...rest } } /** diff --git a/src/lib/encode-conversions.ts b/src/lib/encode-conversions.ts index a2cd415..def3e42 100644 --- a/src/lib/encode-conversions.ts +++ b/src/lib/encode-conversions.ts @@ -64,8 +64,8 @@ export const convertField: ConvertFunction<'field'> = (mapeoDoc) => { } export const convertPreset: ConvertFunction<'preset'> = (mapeoDoc) => { - const colorRegex = valueSchemas.preset.properties.color.pattern - if (!mapeoDoc.color.match(colorRegex)) { + const colorRegex = RegExp(valueSchemas.preset.properties.color.pattern) + if (mapeoDoc.color && !colorRegex.test(mapeoDoc.color)) { throw new Error(`invalid color string ${mapeoDoc.color}`) } diff --git a/test/fixtures/good-docs-minimal.js b/test/fixtures/good-docs-minimal.js index 7175d84..cc1de59 100644 --- a/test/fixtures/good-docs-minimal.js +++ b/test/fixtures/good-docs-minimal.js @@ -87,7 +87,6 @@ export const goodDocsMinimal = [ }, terms: [], deleted: false, - color: '#ff00ff', }, expected: {}, }, @@ -116,6 +115,7 @@ export const goodDocsMinimal = [ updatedAt: cachedValues.updatedAt, links: [], name: 'my device name', + deviceType: 'mobile', deleted: false, }, expected: {}, @@ -185,7 +185,6 @@ export const goodDocsMinimal = [ docRefType: 'field', propertyRef: 'label', languageCode: 'qu', - regionCode: 'PE', message: `sach'a`, }, expected: {},