UNPKG

@mapeo/schema

Version:

JSON schema and flow types for Mapeo

194 lines (172 loc) 6.53 kB
// @ts-check /** * @module mapeo-schema */ import * as cenc from 'compact-encoding' import * as Schemas from './dist/schemas.js' import * as ProtobufSchemas from './types/proto/index.js' import schemasPrefix from './schemasPrefix.js' import { inheritsFromCommon, formatSchemaKey } from './utils.js' const dataTypeIdSize = 6 const schemaVersionSize = 2 /** * @param {import('./types').JSONSchema} obj - Object to be encoded * @returns {import('./types').ProtobufSchema} */ const jsonSchemaToProto = (obj) => { const commonKeys = [ 'id', 'created_at', 'links', 'timestamp', 'userId', 'deviceId', ] /** @type {Object} */ const uncommon = Object.keys(obj) .filter((k) => !commonKeys.includes(k)) .reduce((uncommon, field) => ({ ...uncommon, [field]: obj[field] }), {}) /** @type {Object} */ const common = commonKeys .filter((field) => obj[field]) .reduce((common, field) => ({ ...common, [field]: obj[field] }), {}) common.id = Buffer.from(obj['id'], 'hex') // turn date represented as string to Date common.created_at = new Date(common.created_at) common.timestamp = new Date(common.timestamp) const key = formatSchemaKey(obj.schemaType, obj.schemaVersion) // when we inherit from common, common is actually a field inside the protobuf object, // so we don't destructure it return inheritsFromCommon(key) ? { ...uncommon, common } : { ...uncommon, ...common } } /** * @param {import('./types').ProtobufSchema} protobufObj * @param {Object} obj * @param {Number} obj.schemaVersion * @param {String} obj.schemaType * @param {String} obj.version * @returns {import('./types').JSONSchema} */ const protoToJsonSchema = ( protobufObj, { schemaVersion, schemaType, version } ) => { /** @type {Object} */ let obj = { ...protobufObj, schemaVersion, schemaType } if (obj.common) { obj = { ...obj, ...obj.common } delete obj.common } // Preset_1 and Field_1 don't have a version field and don't accept additional fields const key = formatSchemaKey(schemaType, schemaVersion) if (key !== 'Preset_1' && key !== 'Field_1') { obj.version = version } obj.id = obj.id.toString('hex') // turn date represented as Date to string if (obj.created_at) obj.created_at = obj.created_at.toJSON() if (obj.timestamp) obj.timestamp = obj.timestamp.toJSON() return obj } /** * given a schemaVersion and type, return a buffer with the corresponding data * @param {Object} obj * @param {string} obj.dataTypeId hex encoded string of a 6-byte buffer indicating schemaType * @param {number | undefined} obj.schemaVersion number to indicate version. Gets converted to a padded 4-byte hex string * @returns {Buffer} blockPrefix for corresponding schema */ export const encodeBlockPrefix = ({ dataTypeId, schemaVersion }) => { // @ts-ignore return Buffer.concat([ cenc.encode(cenc.hex.fixed(dataTypeIdSize), dataTypeId), cenc.encode(cenc.uint16, schemaVersion), ]) } /** * given a buffer, return schemaVersion and dataTypeId * @param {Buffer} buf * @returns {{dataTypeId:String, schemaVersion:Number}} */ export const decodeBlockPrefix = (buf) => { const state = cenc.state() // @ts-ignore state.buffer = buf state.start = 0 state.end = dataTypeIdSize const dataTypeId = cenc.hex.fixed(dataTypeIdSize).decode(state) state.start = dataTypeIdSize state.end = dataTypeIdSize + schemaVersionSize const schemaVersion = cenc.uint16.decode(state) return { dataTypeId, schemaVersion } } /** * Validate an object against the schema type * @param {import('./types').JSONSchema} obj - Object to be encoded * @returns {Boolean} indicating if the object is valid */ export const validate = (obj) => { const key = formatSchemaKey(obj.schemaType, obj.schemaVersion) // Preset_1 doesn't have a type field, so validation won't pass // but we still need it to now which schema to validate, so we delete it after grabbing the key if (key === 'Preset_1') delete obj['schemaType'] // Field_1 doesn't have a schemaVersion nor schemaType field, so validation won't pass // but we still need it to now which schema to validate, so we delete it after grabbing the key if (key === 'Field_1') { delete obj['schemaVersion'] delete obj['schemaType'] } if (key === 'Observation_4' || key === 'Filter_1') { obj.type = obj.schemaType delete obj.schemaType } const validatefn = Schemas[key] const isValid = validatefn(obj) if (!isValid) throw new Error(JSON.stringify(validatefn.errors, null, 4)) return isValid } /** * Encode a an object validated against a schema as a binary protobuf to send to an hypercore. * @param {import('./types').JSONSchema} obj - Object to be encoded * @returns {Buffer} protobuf encoded buffer with dataTypeIdSize + schemaVersionSize bytes prepended, one for the type of record and the other for the version of the schema */ export const encode = (obj) => { const key = formatSchemaKey(obj.schemaType, obj.schemaVersion) // some schemas don't have type field so it can be undefined const schemaType = obj.schemaType || '' if (!ProtobufSchemas[key]) { throw new Error( `Invalid schemaVersion for ${schemaType} version ${obj.schemaVersion}` ) } const blockPrefix = encodeBlockPrefix({ dataTypeId: schemasPrefix[schemaType].dataTypeId, schemaVersion: obj.schemaVersion, }) const record = jsonSchemaToProto(obj) const partial = ProtobufSchemas[key].fromPartial(record) const protobuf = ProtobufSchemas[key].encode(partial).finish() return Buffer.concat([blockPrefix, protobuf]) } /** * Decode a Buffer as an object validated against the corresponding schema * @param {Buffer} buf - Buffer to be decoded * @returns {import('./types').JSONSchema} * */ export const decode = (buf, { coreId, seq }) => { const { dataTypeId, schemaVersion } = decodeBlockPrefix(buf) const schemaType = Object.keys(schemasPrefix).reduce( (type, key) => (schemasPrefix[key].dataTypeId === dataTypeId ? key : type), '' ) const key = formatSchemaKey(schemaType, schemaVersion) if (!ProtobufSchemas[key]) { throw new Error( `Invalid schemaVersion for ${schemaType} version ${schemaVersion}` ) } const version = `${coreId.toString('hex')}/${seq.toString()}` const record = buf.subarray(dataTypeIdSize + schemaVersionSize, buf.length) const protobufObj = ProtobufSchemas[key].decode(record) return protoToJsonSchema(protobufObj, { schemaVersion, schemaType, version }) }