diff --git a/examples/schema_test.js b/examples/schema_test.js index 4de7bc5e..9f832397 100644 --- a/examples/schema_test.js +++ b/examples/schema_test.js @@ -2,110 +2,33 @@ import { encode, decode, validate } from '../index.js' import Hypercore from 'hypercore' import ram from 'random-access-memory' -import { randomBytes } from 'node:crypto' - -// FILTER_1 -// const obj = { -// id: randomBytes(32).toString('hex'), -// type: 'filter', -// schemaVersion: 1, -// created_at: new Date().toJSON(), -// filter: ['observation'], -// name: 'john', -// } - -// DEVICE -// const obj = { -// schemaType: 'Device', -// schemaVersion: 1, -// id: randomBytes(32).toString('hex'), -// action: 'device:add', -// authorId: randomBytes(32).toString('hex'), -// projectId: randomBytes(32).toString('hex'), -// signature: 'hi', -// authorIndex: 10, -// deviceIndex: 10, -// } - -// ROLE -// const obj = { -// id: randomBytes(32).toString('hex'), -// schemaType: 'Role', -// schemaVersion: 1, -// role: 'project-creator', -// created_at: new Date(), -// projectId: randomBytes(32).toString('hex'), -// action: 'role:set', -// signature: 'hi', -// authorIndex: 10, -// deviceIndex: 10, -// } - -// CORE OWNERSHIP -// const obj = { -// type: 'coreOwnership', -// schemaVersion: 1, -// id: randomBytes(32).toString('hex'), -// coreId: randomBytes(32).toString('hex'), -// projectId: randomBytes(32).toString('hex'), -// storeType: 'blob', -// authorIndex: 10, -// deviceIndex: 10, -// action: 'core:owner', -// } - -// PRESET_1 -// const obj = { -// id: randomBytes(32).toString('hex'), -// type: 'Preset', -// schemaVersion: 1, -// tags: { nature: 'tree' }, -// geometry: ['point'], -// name: 'john', -// } - -// FIELD_1 -const obj = { - id: randomBytes(32).toString('hex'), - schemaType: 'Project', - schemaVersion: 1, - name: 'My Project', - created_at: new Date().toJSON(), -} - -// OBSERVATION 4 -// const obj = { -// id: randomBytes(32).toString('hex'), -// schemaType: 'observation', -// schemaVersion: 4, -// created_at: new Date().toJSON(), -// } - -// OBSERVATION 5 -// const obj = { -// id: randomBytes(32).toString('hex'), -// type: 'Observation', -// schemaVersion: 5, -// created_at: new Date().toJSON(), -// } - -const record = encode(obj) - -const core = new Hypercore(ram, { valueEncoding: 'binary' }) -await core.ready() -core.append(record) - -try { - const index = 0 - const data = await core.get(index) - const decodedData = decode(data, { coreId: core.key, seq: index }) - console.log('decoded', decodedData) - console.log('VALID?', validate(decodedData)) - if (Buffer.compare(data, record) !== 0) { - throw new Error(`data doesn't match: ${data} != ${record}`) - } else { - console.log('data matches <3') +import { docs } from '../test/docs.js' + +const objs = docs.good + +Object.keys(objs).forEach(test) + +async function test(key) { + const obj = objs[key] + const record = encode(obj) + const k = obj.schemaType || obj.type + const core = new Hypercore(ram, { valueEncoding: 'binary' }) + await core.ready() + core.append(record) + + try { + const index = 0 + const data = await core.get(index) + console.log(`trying ${k}`) + const decodedData = decode(data, { coreId: core.key, seq: index }) + console.log('data', decodedData) + console.log(`VALID? `, validate(decodedData), '\n') + if (Buffer.compare(data, record) !== 0) { + throw new Error(`data doesn't match: ${data} != ${record}`) + } else { + console.log('data matches <3') + } + } catch (err) { + console.log(err) } -} catch (err) { - console.log(err) } diff --git a/schema/field/v1.json b/old/schema/field/v1.json similarity index 100% rename from schema/field/v1.json rename to old/schema/field/v1.json diff --git a/schema/observation/v4.json b/old/schema/observation/v4.json similarity index 100% rename from schema/observation/v4.json rename to old/schema/observation/v4.json diff --git a/schema/preset/v1.json b/old/schema/preset/v1.json similarity index 100% rename from schema/preset/v1.json rename to old/schema/preset/v1.json diff --git a/proto/common/v1.proto b/proto/common/v1.proto index ac72ca4c..0a67d2bf 100644 --- a/proto/common/v1.proto +++ b/proto/common/v1.proto @@ -3,14 +3,17 @@ package mapeo; import "google/protobuf/timestamp.proto"; + message Common_1 { - google.protobuf.Timestamp created_at = 1; - optional string deviceId = 2; // 32-byte random generated number - bytes id = 3; - repeated string links = 4; - optional google.protobuf.Timestamp timestamp = 5; - optional string userId = 6; + optional bytes id = 1 [(required) = true]; + message Link { + bytes coreId = 1; + int32 seq = 2; + } + repeated Link links = 2; + google.protobuf.Timestamp createdAt = 3 [(required) = true]; + google.protobuf.Timestamp updatedAt = 4 [(required) = true]; } /* ignored fields and differences from common.json jsonSchema * id is a byte buffer here and a string in jsonSchema diff --git a/proto/coreOwnership/v1.proto b/proto/coreOwnership/v1.proto index 513e51ee..ccbaea2d 100644 --- a/proto/coreOwnership/v1.proto +++ b/proto/coreOwnership/v1.proto @@ -2,15 +2,22 @@ syntax = "proto3"; package mapeo; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "common/v1.proto"; +import "options.proto"; message CoreOwnership_1 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "9d4d39390125"; + option (schemaName) = "coreOwnership"; + Common_1 common = 1; - string action = 2; - string coreId = 3; - string projectId = 4; - string storeType = 5; - string signature = 6; - int32 authorIndex = 7; - int32 deviceIndex = 8; + + string action = 5; + string coreId = 6; + string projectId = 7; + string storeType = 8; + string signature = 9; + int32 authorIndex = 10; + int32 deviceIndex = 11; } diff --git a/proto/device/v1.proto b/proto/device/v1.proto index 6d9379b6..6429cb58 100644 --- a/proto/device/v1.proto +++ b/proto/device/v1.proto @@ -2,14 +2,21 @@ syntax = "proto3"; package mapeo; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "common/v1.proto"; +import "options.proto"; message Device_1 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "e96e6c68fcc0"; + option (schemaName) = "device"; + Common_1 common = 1; - string action = 2; - string authorId = 3; - string projectId = 4; - string signature = 5; - int32 authorIndex = 6; - int32 deviceIndex = 7; + + string action = 5; + string authorId = 6; + string projectId = 7; + string signature = 8; + int32 authorIndex = 9; + int32 deviceIndex = 10; } diff --git a/proto/field/v1.proto b/proto/field/v1.proto deleted file mode 100644 index 0bae68b5..00000000 --- a/proto/field/v1.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; -package mapeo; - -import "google/protobuf/any.proto"; - -message Field_1 { - bytes id = 1; - // keys can be an array of strings or a string - google.protobuf.Any key = 2; - string type = 3; -} diff --git a/proto/field/v2.proto b/proto/field/v2.proto new file mode 100644 index 00000000..e935f5d6 --- /dev/null +++ b/proto/field/v2.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package mapeo; + +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; +import "tags/v1.proto"; +import "common/v1.proto"; +import "options.proto"; + +message Field_2 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "9c5abfbee243"; + option (schemaName) = "field"; + + Common_1 common = 1; + + optional string tagKey = 5 [(required) = true]; + enum Type { + text = 0; + number = 1; + selectOne = 2; + selectMultiple = 3; + } + Type type = 6 [(required) = true]; + optional string label = 7 [(required) = true]; + enum Appearance { + multiline = 0; + singleline = 1; + } + Appearance appearance = 8; + bool snakeCase = 9; + message Option { + string label = 1; + TagValue_1.PrimitiveValue value = 2; + } + repeated Option options = 10; + bool universal = 11; + optional string placeholder = 12; + optional string helperText = 13; +} diff --git a/proto/filter/v1.proto b/proto/filter/v1.proto deleted file mode 100644 index b4c7d615..00000000 --- a/proto/filter/v1.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto3"; -package mapeo; - -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; - -message Filter_1 { - bytes id = 1; - google.protobuf.Timestamp created_at = 2; - repeated string filter = 3; - string name = 4; - optional google.protobuf.Timestamp timestamp = 5; - optional string userId = 6; - optional string deviceId = 7; - repeated string links = 8; -} diff --git a/proto/observation/v4.proto b/proto/observation/v4.proto deleted file mode 100644 index eca459a6..00000000 --- a/proto/observation/v4.proto +++ /dev/null @@ -1,48 +0,0 @@ -syntax = "proto3"; -package mapeo; - -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; - -message Observation_4 { - bytes id = 1; - google.protobuf.Timestamp created_at = 4; - optional google.protobuf.Timestamp timestamp = 5; - optional string userId = 6; - optional string deviceId = 7; - repeated string links = 8; - optional float lat = 9; - optional float lon = 10; - repeated google.protobuf.Struct refs = 11; - repeated google.protobuf.Struct attachments = 12; - optional google.protobuf.Struct tags = 13; - message Metadata { - optional bool manual_location = 1; - - message Position { - float timestamp = 1; - bool mocked = 2; - - message Coords { - float altitude = 1; - float heading = 2; - float longitude = 3; - float latitude = 4; - float speed = 5; - float acurracy = 6; - } - optional Coords coords = 3; - } - - message PositionProvider { - bool gpsAvailable = 1; - bool passiveAvailable = 2; - bool locationServicesEnabled = 3; - bool networkAvailable = 4; - } - optional Position position = 3; - optional Position lastSavedPosition = 4; - optional PositionProvider positionProvider = 5; - } - optional Metadata metadata = 14; -} diff --git a/proto/observation/v5.proto b/proto/observation/v5.proto index bfa9e77d..f224fdda 100644 --- a/proto/observation/v5.proto +++ b/proto/observation/v5.proto @@ -1,25 +1,72 @@ syntax = "proto3"; package mapeo; +import "google/protobuf/timestamp.proto"; import "google/protobuf/struct.proto"; +import "tags/v1.proto"; import "common/v1.proto"; +import "options.proto"; message Observation_5 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "31d090df2e16"; + option (schemaName) = "observation"; + Common_1 common = 1; - optional float lat = 2; - optional float lon = 3; - repeated google.protobuf.Struct refs = 4; - repeated google.protobuf.Struct attachments = 5; - optional google.protobuf.Struct tags = 6; + optional float lat = 5; + optional float lon = 6; + + message Ref { + bytes id = 1; + } + repeated Ref refs = 7; + + // ATTACHMENT + enum AttachmentType { + photo = 0; + video = 1; + audio = 2; + } + message Attachment { + bytes driveId = 1; + string name = 2; + AttachmentType type = 3; + } + repeated Attachment attachments = 8; + + // TAGS + map tags = 9; + // METADATA message Metadata { - message Location { - optional float precision = 1; - optional int32 altitude = 2; + optional bool manualLocation = 1; + + message Position { + google.protobuf.Timestamp timestamp = 1; + bool mocked = 2; + + message Coords { + float latitude = 1; + float longitude = 2; + float altitude = 3; + float heading = 4; + float speed = 5; + float acurracy = 6; + } + optional Coords coords = 3; } - optional Location location = 1; - optional bool manual_location = 2; + + message PositionProvider { + bool gpsAvailable = 1; + bool passiveAvailable = 2; + bool locationServicesEnabled = 3; + bool networkAvailable = 4; + } + + optional Position position = 3; + optional Position lastSavedPosition = 4; + optional PositionProvider positionProvider = 5; } - optional Metadata metadata = 7; + optional Metadata metadata = 10; } diff --git a/proto/options.proto b/proto/options.proto new file mode 100644 index 00000000..a6c93b25 --- /dev/null +++ b/proto/options.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +import "google/protobuf/descriptor.proto"; +package mapeo; + +extend google.protobuf.MessageOptions { + // This should be a hex-encoded 48-bit random number, and should never change + // for a given message type. Generated with `openssl rand -hex 6` + // It is designed to be globally unique, such that Mapeo can parse any + // data blocks and ignore data types that it does not recognize. + string dataTypeId = 50001 [retention = RETENTION_SOURCE]; + // This is a unique (within Mapeo) name for the message type. + // Should be camelCase. + string schemaName = 50002 [retention = RETENTION_SOURCE]; +} + +extend google.protobuf.FieldOptions { + // proto3 has no concept of "required" fields (all fields are optional by + // default), but our code _does_ require some fields to be present. We use + // this to annotate the protobuf definitions and (TODO) validate that the + // corresponding JSONSchema defines this field as required + // + // For ts-proto to type this as ` | undefined`` then required primitive fields + // should be marked as optional in the protobuf definition, because otherwise + // they are set to the default value and the generated type does not include + // `undefined` + boolean required = 50003 [retention = RETENTION_SOURCE] +} diff --git a/proto/preset/v1.proto b/proto/preset/v1.proto deleted file mode 100644 index 71d5413f..00000000 --- a/proto/preset/v1.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto3"; -package mapeo; - -import "google/protobuf/struct.proto"; - -message Preset_1 { - bytes id = 1; - string name = 2; - repeated string geometry = 3; - optional google.protobuf.Struct tags = 4; - optional google.protobuf.Struct addTags = 5; - optional google.protobuf.Struct removeTags = 6; - repeated string fields = 7; - repeated string additionalFields = 8; - optional string icon = 9; - repeated string terms = 10; - optional int32 sort = 11; -} diff --git a/proto/preset/v2.proto b/proto/preset/v2.proto new file mode 100644 index 00000000..b9801c3c --- /dev/null +++ b/proto/preset/v2.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; +package mapeo; + +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "tags/v1.proto"; +import "common/v1.proto"; +import "options.proto"; + +message Preset_2 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "d6aa854b7a99"; + option (schemaName) = "preset"; + + Common_1 common = 1; + + string name = 5; + enum Geometry { + point = 0; + vertex = 1; + line = 2; + area = 3; + relation = 4; + } + repeated Geometry geometry = 6; + map tags = 7; + map addTags = 8; + map removeTags = 9; + repeated bytes fieldIds = 10; + optional bytes iconId = 11; + repeated string terms = 12; +} diff --git a/proto/project/v1.proto b/proto/project/v1.proto index a2f0301c..25ea37ad 100644 --- a/proto/project/v1.proto +++ b/proto/project/v1.proto @@ -1,10 +1,16 @@ syntax = "proto3"; package mapeo; -import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "common/v1.proto"; +import "options.proto"; message Project_1 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "626b45fe2942"; + option (schemaName) = "project"; + Common_1 common = 1; - string name = 2; + + string name = 5; } diff --git a/proto/project/v2.proto b/proto/project/v2.proto new file mode 100644 index 00000000..2b688c5a --- /dev/null +++ b/proto/project/v2.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package mapeo; + +import "google/protobuf/timestamp.proto"; +import "common/v1.proto"; +import "options.proto"; + +// Same as v1, created to test forward compatibility + +message Project_2 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "626b45fe2942"; + option (schemaName) = "project"; + + Common_1 common = 1; + + string name = 5; +} diff --git a/proto/role/v1.proto b/proto/role/v1.proto index 6d58419e..097cdc53 100644 --- a/proto/role/v1.proto +++ b/proto/role/v1.proto @@ -2,14 +2,21 @@ syntax = "proto3"; package mapeo; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; import "common/v1.proto"; +import "options.proto"; message Role_1 { + // **DO NOT CHANGE dataTypeId** generated with `openssl rand -hex 6` + option (dataTypeId) = "69acce0ea09b"; + option (schemaName) = "role"; + Common_1 common = 1; - string role = 2; - string projectId = 3; - string action = 4; - string signature = 5; - int32 authorIndex = 6; - int32 deviceIndex = 7; + + string role = 5; + string projectId = 6; + string action = 7; + string signature = 8; + int32 authorIndex = 9; + int32 deviceIndex = 10; } diff --git a/proto/tags/v1.proto b/proto/tags/v1.proto new file mode 100644 index 00000000..4bf2345d --- /dev/null +++ b/proto/tags/v1.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package mapeo; + +import "google/protobuf/struct.proto"; + +message TagValue_1 { + message PrimitiveValue { + oneof kind { + google.protobuf.NullValue null_value = 1; + double number_value = 2; + string string_value = 3; + bool boolean_value = 4; + } + } + message ListValue { + repeated PrimitiveValue list_value = 5; + } + oneof kind { + PrimitiveValue primitive_value = 1; + ListValue list_value = 2; + } +} diff --git a/schema/common/v1.json b/schema/common/v1.json index d38608bd..27dd83e5 100644 --- a/schema/common/v1.json +++ b/schema/common/v1.json @@ -6,48 +6,31 @@ "type": "object", "properties": { "id": { - "description": "Unique value that identifies this element", + "description": "Hex-encoded 32-byte buffer", "type": "string" }, "version": { - "description": "Unique value that identifies this particular version of this element", + "description": "id (hex-encoded 32-byte buffer) and core sequence number, separated by '/'", "type": "string" }, - "created_at": { + "createdAt": { "description": "RFC3339-formatted datetime of when the first version of the element was created", "type": "string", "format": "date-time" }, - "timestamp": { + "updatedAt": { "description": "RFC3339-formatted datetime of when this version of the element was created", "type": "string", "format": "date-time" }, - "userId": { - "description": "ID of the user who made this edit", - "type": "string" - }, - "deviceId": { - "description": "ID of the device that made this edit", - "type": "string" - }, - "schemaType": { - "description": "enum that defines the type of document in the database (defines which schema should be used)", - "type": "string" - }, "links": { - "description": "Version ids of the previous document versions this one is replacing", + "description": "Version ids of the previous document versions this one is replacing. Each link is id (hex-encoded 32 byte buffer) and sequence number, separated by '/'", "type": "array", "uniqueItems": true, "items": { "type": "string" } - }, - "schemaVersion": { - "description": "Version of schema. Should increment for breaking changes to the schema", - "type": "number", - "minimum": 1 } }, - "required": ["id", "created_at", "schemaType"] + "required": ["id", "createdAt", "updatedAt", "links", "version"] } diff --git a/schema/coreOwnership/v1.json b/schema/coreOwnership/v1.json index d79419ec..16a9db6f 100644 --- a/schema/coreOwnership/v1.json +++ b/schema/coreOwnership/v1.json @@ -3,17 +3,13 @@ "$id": "http://mapeo.world/schemas/coreOwnership/v1.json", "title": "CoreOwnership", "type": "object", - "allOf":[{"$ref": "../common/v1.json"}], "properties": { - "schemaType": { "type": "string", "pattern": "^coreOwnership$" }, - "action": { + "schemaName": { "type": "string", - "enum": ["core:owner"] + "enum": ["coreOwnership"] }, - "schemaVersion": { - "type": "number", - "minimum": 1, - "enum": [1] + "action": { + "type": "string" }, "coreId": { "type": "string" }, "projectId": { "type": "string" }, @@ -21,5 +17,6 @@ "signature": { "type": "string" }, "authorIndex": { "type": "integer" }, "deviceIndex": { "type": "integer" } - } + }, + "required": ["schemaName"] } diff --git a/schema/device/v1.json b/schema/device/v1.json index eae8274e..8abb9124 100644 --- a/schema/device/v1.json +++ b/schema/device/v1.json @@ -3,19 +3,13 @@ "$id": "http://mapeo.world/schemas/device/v1.json", "title": "Device", "type": "object", - "allOf":[{"$ref": "../common/v1.json"}], "properties": { - "schemaType": { + "schemaName": { "type": "string", - "pattern": "^Device$" + "enum": ["device"] }, "action": { - "type": "string", - "enum": [ - "device:add", - "device:remove", - "device:restore" - ] + "type": "string" }, "authorId": { "type": "string" @@ -32,5 +26,7 @@ "deviceIndex": { "type": "integer" } - } + }, + "required": ["schemaName"], + "additionalProperties": false } diff --git a/schema/field/v2.json b/schema/field/v2.json new file mode 100644 index 00000000..5bfa16ad --- /dev/null +++ b/schema/field/v2.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://mapeo.world/schemas/field/v2.json", + "title": "Field", + "description": "A field defines a form field that will be shown to the user when creating or editing a map entity. Presets define which fields are shown to the user for a particular map entity. The field definition defines whether the field should show as a text box, multiple choice, single-select, etc. It defines what tag-value is set when the field is entered.", + "type": "object", + "properties": { + "schemaName": { + "description": "Must be `field`", + "type": "string", + "enum": ["field"] + }, + "tagKey": { + "description": "They key in a tags object that this field applies to", + "type": "string" + }, + "type": { + "description": "Type of field - defines how the field is displayed to the user.", + "type": "string", + "meta:enum": { + "text": "Freeform text field", + "number": "Allows only numbers", + "selectOne": "Select one item from a list of pre-defined options", + "selectMultiple": "Select any number of items from a list of pre-defined options" + }, + "enum": ["text", "number", "selectOne", "selectMultiple"] + }, + "label": { + "description": "Default language label for the form field label", + "type": "string" + }, + "appearance": { + "description": "For text fields, display as a single-line or multi-line field", + "type": "string", + "meta:enum": { + "singleline": "Text will be cut-off if more than one line", + "multiline": "Text will wrap to multiple lines within text field" + }, + "enum": ["singleline", "multiline"], + "default": "multiline" + }, + "snakeCase": { + "description": "Convert field value into snake_case (replace spaces with underscores and convert to lowercase)", + "type": "boolean", + "default": false + }, + "options": { + "description": "List of options the user can select for single- or multi-select fields", + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": ["label", "value"] + } + }, + "universal": { + "description": "If true, this field will appear in the Add Field list for all presets", + "type": "boolean", + "default": false + }, + "placeholder": { + "description": "Displayed as a placeholder in an empty text or number field before the user begins typing. Use 'helperText' for important information, because the placeholder is not visible after the user has entered data.", + "type": "string" + }, + "helperText": { + "description": "Additional context about the field, e.g. hints about how to answer the question.", + "type": "string" + } + }, + "required": ["tagKey", "type", "label", "schemaName"], + "additionalProperties": false +} diff --git a/schema/filter/v1.json b/schema/filter/v1.json deleted file mode 100644 index 6cf154cb..00000000 --- a/schema/filter/v1.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://mapeo.world/schemas/filter.json", - "title": "Filter", - "description": "A filter is a saved view of data in the Mapeo database, filtered by tag or date. E.g. a filter could define observations between two dates, or only observations with the tag `public=true`", - "type": "object", - "properties": { - "id": { - "description": "Unique value that identifies this element", - "type": "string" - }, - "version": { - "description": "Unique value that identifies this particular version of this element", - "type": "string" - }, - "created_at": { - "description": "RFC3339-formatted datetime of when the first version of the element was created", - "type": "string", - "format": "date-time" - }, - "timestamp": { - "description": "RFC3339-formatted datetime of when this version of the element was created", - "type": "string", - "format": "date-time" - }, - "userId": { - "description": "ID of the user who edited/created this record", - "type": "string" - }, - "deviceId": { - "description": "ID of the device that made this edit", - "type": "string" - }, - "type": { - "description": "Must be `filter`", - "type": "string", - "enum": ["filter"] - }, - "links": { - "description": "Version ids of the previous document versions this one is replacing", - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "schemaVersion": { - "description": "Version of this schema. Should increment for breaking changes to the schema", - "type": "number", - "minimum": 1, - "enum": [1] - }, - "filter": { - "type": "array", - "description": "A filter expression as defined in https://docs.mapbox.com/mapbox-gl-js/style-spec/#other-filter but where the special fields `$type` refers to the mapeo type (observation, node, way etc) and `$id` is the mapeo id." - }, - "name": { - "type": "string", - "description": "A human-readable name for this filter." - } - }, - "required": ["id", "version", "created_at", "type", "schemaVersion", "filter", "name"] -} diff --git a/schema/observation/v5.json b/schema/observation/v5.json index 1c1c533e..66093da3 100644 --- a/schema/observation/v5.json +++ b/schema/observation/v5.json @@ -10,7 +10,8 @@ "properties": { "timestamp": { "description": "Timestamp of when the current position was obtained", - "type": "number" + "type": "string", + "format": "date-time" }, "mocked": { "description": "`true` if the position was mocked", @@ -21,19 +22,19 @@ "description": "Position details, should be self explanatory. Units in meters", "type": "object", "properties": { - "altitude": { + "latitude": { "type": "number" }, - "heading": { + "longitude": { "type": "number" }, - "longitude": { + "altitude": { "type": "number" }, - "speed": { + "heading": { "type": "number" }, - "latitude": { + "speed": { "type": "number" }, "accuracy": { @@ -41,23 +42,15 @@ } } } - } } }, "type": "object", - "allOf":[{"$ref": "../common/v1.json"}], "properties": { - "schemaType": { - "description": "Must be `Observation`", + "schemaName": { + "description": "Must be `observation`", "type": "string", - "enum": ["Observation"] - }, - "schemaVersion": { - "description": "Version of this schema. Should increment for breaking changes to the schema", - "type": "number", - "minimum": 1, - "enum": [5] + "enum": ["observation"] }, "lat": { "description": "latitude of the observation", @@ -71,10 +64,95 @@ "minimum": -180, "maximum": 180 }, + "refs": { + "type": "array", + "description": "References to any nodes or ways that this observation is related to.", + "items": { + "type": "object", + "properties": { + "id": { + "description": "hex-encoded id of the element that this observation references", + "type": "string" + } + }, + "required": ["id"] + } + }, + "attachments": { + "type": "array", + "description": "media or other data that are attached to this observation", + "items": { + "type": "object", + "properties": { + "driveId": { + "type": "string", + "description": "" + }, + "name": { + "type": "string", + "description": "name of the attachment" + }, + "type": { + "type": "string", + "description": "string that describes the type of the attachment", + "meta:enum": { + "UNRECOGNIZED": "future attachment type" + }, + "enum": ["photo", "video", "audio", "UNRECOGNIZED"] + } + }, + "required": ["driveId", "name", "type"] + } + }, + "tags": { + "type": "object", + "description": "User-defined key-value pairs relevant to this observation", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, "metadata": { "description": "Additional metadata associated with the observation (e.g. location precision, altitude, heading)", "type": "object", "properties": { + "manualLocation": { + "description": "Whether location has been set manually", + "type": "boolean", + "default": false + }, "position": { "$ref": "#/definitions/position", "description": "Details of the position recorded for the observation" @@ -104,53 +182,11 @@ "type": "boolean" } } - }, - "manualLocation": { - "description": "Whether location has been set manually", - "type": "boolean", - "default": false } }, - "additionalProperties": true - }, - "refs": { - "type": "array", - "description": "References to any nodes or ways that this observation is related to.", - "items": { - "type": "object", - "properties": { - "id": { - "description": "ID of the element that this observation references", - "type": "string" - } - }, - "required": ["id"] - } - }, - "attachments": { - "type": "array", - "description": "media or other data that are attached to this observation", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "unique ID that identifies the attachment" - }, - "type": { - "type": "string", - "description": "string that describes the type of the attachment" - } - }, - "required": ["id"] - } - }, - "tags": { - "type": "object", - "description": "User-defined key-value pairs relevant to this observation", - "properties": {}, - "additionalProperties": true + "additionalProperties": false } }, - "required": ["id", "version", "created_at", "schemaType", "schemaVersion"] + "required": ["schemaName", "tags"], + "additionalProperties": false } diff --git a/schema/preset/v2.json b/schema/preset/v2.json new file mode 100644 index 00000000..5eb0441f --- /dev/null +++ b/schema/preset/v2.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://mapeo.world/schemas/preset/v2.json", + "title": "Preset", + "description": "Presets define how map entities are displayed to the user. They define the icon used on the map, and the fields / questions shown to the user when they create or edit the entity on the map. The `tags` property of a preset is used to match the preset with observations, nodes, ways and relations. If multiple presets match, the one that matches the most tags is used.", + "type": "object", + "properties": { + "schemaName": { + "description": "Must be `preset`", + "type": "string", + "enum": ["preset"] + }, + "name": { + "description": "Name for the feature in default language.", + "type": "string" + }, + "geometry": { + "description": "Valid geometry types for the feature - this preset will only match features of this geometry type `\"point\", \"vertex\", \"line\", \"area\", \"relation\"`", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": ["point", "vertex", "line", "area", "relation"] + } + }, + "tags": { + "description": "The tags are used to match the preset to existing map entities. You can match based on multiple tags E.g. if you have existing points with the tags `nature:tree` and `species:oak` then you can add both these tags here in order to match only oak trees.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "addTags": { + "description": "Tags that are added when changing to the preset (default is the same value as 'tags')", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "removeTags": { + "description": "Tags that are removed when changing to another preset (default is the same value as 'addTags' which in turn defaults to 'tags')", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "fields": { + "description": "hex-encoded string. IDs of fields to displayed to the user when the preset is created or edited", + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "description": "hex-encoded string. ID of preset icon which represents this preset", + "type": "string" + }, + "terms": { + "description": "Synonyms or related terms (used for search)", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["name", "geometry", "tags", "schemaName"], + "additionalProperties": false +} diff --git a/schema/project/v1.json b/schema/project/v1.json index a0ca7bdc..df986e2b 100644 --- a/schema/project/v1.json +++ b/schema/project/v1.json @@ -3,18 +3,17 @@ "$id": "http://mapeo.world/schemas/project/v1.json", "title": "Project", "type": "object", - "allOf":[{"$ref": "../common/v1.json"}], "properties": { - "schemaType": { "type": "string", "pattern": "^Project$" }, - "schemaVersion": { - "type": "number", - "minimum": 1, - "enum": [1] + "schemaName": { + "description": "Must be `project`", + "type": "string", + "enum": ["project"] }, - "name" : { + "name": { "description": "name of the project", "type": "string" } }, - "required": ["schemaType", "schemaVersion", "name"] + "required": ["schemaName", "name"], + "additionalProperties": false } diff --git a/schema/project/v2.json b/schema/project/v2.json new file mode 100644 index 00000000..df986e2b --- /dev/null +++ b/schema/project/v2.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://mapeo.world/schemas/project/v1.json", + "title": "Project", + "type": "object", + "properties": { + "schemaName": { + "description": "Must be `project`", + "type": "string", + "enum": ["project"] + }, + "name": { + "description": "name of the project", + "type": "string" + } + }, + "required": ["schemaName", "name"], + "additionalProperties": false +} diff --git a/schema/role/v1.json b/schema/role/v1.json index ad8afaf3..7e81b43d 100644 --- a/schema/role/v1.json +++ b/schema/role/v1.json @@ -3,29 +3,20 @@ "$id": "http://mapeo.world/schemas/role/v1.json", "title": "Role", "type": "object", - "allOf":[{"$ref": "../common/v1.json"}], "properties": { - "schemaType": { + "schemaName": { "type": "string", - "pattern": "^Role$" + "enum": ["role"] }, "role": { - "type": "string", - "enum": [ - "project-creator", - "coordinator", - "member", - "non-member" - ] + "type": "string" }, "projectId": { "type": "string" }, "action": { "type": "string", - "enum": [ - "role:set" - ] + "enum": ["role:set"] }, "signature": { "type": "string" @@ -36,5 +27,7 @@ "deviceIndex": { "type": "integer" } - } + }, + "required": ["schemaName"], + "additionalProperties": false } diff --git a/test/docs.js b/test/docs.js index 83c5c84e..ab4aaa4e 100644 --- a/test/docs.js +++ b/test/docs.js @@ -5,9 +5,8 @@ export const docs = { badDocType: { id: randomBytes(32).toString('hex'), schemaType: 'doesnotexist', - schemaVersion: 4, links: [], - created_at: new Date().toJSON(), + createdAt: new Date().toJSON(), refs: [], attachments: [], metadata: { @@ -17,9 +16,9 @@ export const docs = { badSchemaVersion: { id: randomBytes(32).toString('hex'), schemaType: 'observation', - schemaVersion: null, + schemaVersion: 2, links: [], - created_at: new Date().toJSON(), + createdAt: new Date().toJSON(), refs: [], attachments: [], metadata: { @@ -27,53 +26,35 @@ export const docs = { }, }, good: { - observation_4: { + observation_5: { id: randomBytes(32).toString('hex'), schemaType: 'observation', - schemaVersion: 4, + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), links: [], - created_at: new Date().toJSON(), - timestamp: new Date().toJSON(), - refs: [], - attachments: [], - metadata: { - manual_location: true, - }, - }, - observation_5: { - id: randomBytes(32).toString('hex'), - schemaType: 'Observation', - schemaVersion: 5, - created_at: new Date().toJSON(), - timestamp: new Date().toJSON(), - }, - filter: { - id: randomBytes(32).toString('hex'), - timestamp: new Date().toJSON(), - schemaType: 'filter', - schemaVersion: 1, - created_at: new Date().toJSON(), - filter: ['observation'], - name: 'john', + tags: { fields: { myTag: 10 } }, }, preset: { id: randomBytes(32).toString('hex'), - schemaType: 'Preset', - schemaVersion: 1, + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), + schemaType: 'preset', + links: [], tags: { nature: 'tree' }, geometry: ['point'], name: 'john', }, field: { id: randomBytes(32).toString('hex'), - schemaType: 'Field', - schemaVersion: 1, - key: 'hi', + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), + schemaType: 'field', + tagKey: 'hi', type: 'text', + links: [], }, coreOwnership: { schemaType: 'coreOwnership', - schemaVersion: 1, id: randomBytes(32).toString('hex'), coreId: randomBytes(32).toString('hex'), projectId: randomBytes(32).toString('hex'), @@ -81,12 +62,12 @@ export const docs = { authorIndex: 10, deviceIndex: 10, action: 'core:owner', - created_at: new Date().toJSON(), - timestamp: new Date().toJSON(), + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), + links: [], }, device: { - schemaType: 'Device', - schemaVersion: 1, + schemaType: 'device', id: randomBytes(32).toString('hex'), action: 'device:add', authorId: randomBytes(32).toString('hex'), @@ -94,29 +75,30 @@ export const docs = { signature: 'hi', authorIndex: 10, deviceIndex: 10, - created_at: new Date().toJSON(), - timestamp: new Date().toJSON(), + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), + links: [], }, role: { id: randomBytes(32).toString('hex'), - schemaType: 'Role', - schemaVersion: 1, + schemaType: 'role', role: 'project-creator', projectId: randomBytes(32).toString('hex'), action: 'role:set', signature: 'hi', authorIndex: 10, deviceIndex: 10, - created_at: new Date().toJSON(), - timestamp: new Date().toJSON(), + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), + links: [], }, project: { id: randomBytes(32).toString('hex'), - schemaType: 'Project', - schemaVersion: 1, - created_at: new Date().toJSON(), + schemaType: 'project', + createdAt: new Date().toJSON(), + updatedAt: new Date().toJSON(), name: 'My Project', + links:[] }, }, } -// Object.keys(docs).forEach(save)